{"version":3,"file":"pull-command.mjs","names":[],"sources":["../../../src/commands/pull-command.ts"],"sourcesContent":["/**\n * /pull <path> — Read a file from the running bot and send it to chat.\n *\n * Small files: inline as code block.\n * Large files on Telegram: sent as document attachment.\n * Directories: list contents.\n *\n * Security: blocks sensitive paths (.env, private keys, credentials, bot tokens).\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';\nimport { basename, join, resolve } from 'node:path';\nimport { guardedFetch } from '../services/endpoint-allowlist.js';\nimport { getCredentialVault } from '../services/credential-vault.js';\nimport { extractChannelId } from '../services/channel-sender.js';\n\n// ─── Constants ──────────────────────────────────────────────────────────\n\nconst MAX_TEXT = 3800;       // leave margin below Telegram's 4096 limit\nconst MAX_FILE_SIZE = 50_000_000; // Telegram doc limit: 50 MB\nconst CAPTION_LIMIT = 1024;\n\nconst HOME = process.env.HOME ?? '/home/openclawnch';\nconst WORKSPACE = '/workspace';\nconst OPENCLAWNCH_DIR = join(HOME, '.openclawnch');\n\n/** Shortcuts so users don't need to type full paths. */\nconst PATH_SHORTCUTS: Record<string, string> = {\n  'state':       join(WORKSPACE, '.openclaw-state'),\n  'sessions':    join(WORKSPACE, '.openclaw-state', 'sessions'),\n  'plans':       join(OPENCLAWNCH_DIR, 'plans'),\n  'orders':      join(OPENCLAWNCH_DIR, 'orders'),\n  'memory':      join(OPENCLAWNCH_DIR, 'memory'),\n  'recall':      join(OPENCLAWNCH_DIR, 'recall'),\n  'skills':      join(OPENCLAWNCH_DIR, 'learned-skills'),\n  'tools':       join(OPENCLAWNCH_DIR, 'user-tools'),\n  'webhooks':    join(OPENCLAWNCH_DIR, 'webhooks'),\n  'agents':      join(OPENCLAWNCH_DIR, 'agents'),\n  'ledger':      join(OPENCLAWNCH_DIR, 'ledger'),\n  'budget':      join(OPENCLAWNCH_DIR, 'budget-audit'),\n  'cost-basis':  join(OPENCLAWNCH_DIR, 'data'),\n  'modes':       join(OPENCLAWNCH_DIR, 'modes'),\n  'evolution':   join(OPENCLAWNCH_DIR, 'evolution'),\n  'onboarding':  join(OPENCLAWNCH_DIR, 'onboarding'),\n  'home':        HOME,\n  'workspace':   WORKSPACE,\n};\n\n/** Patterns that must never be returned. */\nconst BLOCKED_PATTERNS = [\n  /\\.env$/i,\n  /private[_-]?key/i,\n  /credentials?\\.(json|yaml|yml|toml)$/i,\n  /secret/i,\n  /bot[_-]?token/i,\n  /\\.pem$/i,\n  /\\.p12$/i,\n  /id_rsa/i,\n  /id_ed25519/i,\n  /keystore/i,\n];\n\n// ─── Helpers ────────────────────────────────────────────────────────────\n\nfunction isBlocked(filePath: string): boolean {\n  const name = basename(filePath).toLowerCase();\n  const full = filePath.toLowerCase();\n  return BLOCKED_PATTERNS.some(p => p.test(name) || p.test(full));\n}\n\nfunction resolvePath(input: string): string {\n  const shortcut = PATH_SHORTCUTS[input.toLowerCase()];\n  if (shortcut) return shortcut;\n\n  // Absolute path\n  if (input.startsWith('/')) return resolve(input);\n\n  // Relative to home\n  return resolve(HOME, input);\n}\n\nfunction formatSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`;\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction formatDirListing(dirPath: string): string {\n  const entries = readdirSync(dirPath);\n  if (entries.length === 0) return '(empty directory)';\n\n  const lines: string[] = [];\n  for (const entry of entries.sort()) {\n    try {\n      const entryPath = join(dirPath, entry);\n      const stat = statSync(entryPath);\n      const suffix = stat.isDirectory() ? '/' : '';\n      const size = stat.isFile() ? `  ${formatSize(stat.size)}` : '';\n      lines.push(`  ${entry}${suffix}${size}`);\n    } catch {\n      lines.push(`  ${entry}  (unreadable)`);\n    }\n  }\n  return lines.join('\\n');\n}\n\nasync function sendTelegramDocument(\n  chatId: string,\n  filePath: string,\n  caption: string,\n): Promise<void> {\n  const token = getCredentialVault().getSecret('bot.telegram.botToken', 'pull-command');\n  if (!token) throw new Error('TELEGRAM_BOT_TOKEN not available');\n\n  const fileBuffer = readFileSync(filePath);\n  const fileName = basename(filePath);\n\n  const form = new FormData();\n  form.append('chat_id', chatId);\n  form.append('document', new Blob([fileBuffer]), fileName);\n  form.append('caption', caption.slice(0, CAPTION_LIMIT));\n\n  const url = `https://api.telegram.org/bot${token}/sendDocument`;\n  const resp = await guardedFetch(url, {\n    method: 'POST',\n    body: form,\n    signal: AbortSignal.timeout(30_000),\n  });\n\n  const data = await resp.json() as any;\n  if (!data.ok) {\n    throw new Error(data.description ?? 'sendDocument failed');\n  }\n}\n\n// ─── Command ────────────────────────────────────────────────────────────\n\nexport const pullCommand = {\n  name: 'pull',\n  description: 'Read a file from the bot: /pull <path|shortcut>',\n  acceptsArgs: true,\n  requireAuth: true,\n  handler: async (ctx?: any) => {\n    const args = (ctx?.args ?? '').trim();\n\n    // No args: show shortcuts\n    if (!args) {\n      const shortcuts = Object.entries(PATH_SHORTCUTS)\n        .map(([k, v]) => `  **${k}** → \\`${v}\\``)\n        .join('\\n');\n      return {\n        text: [\n          '**Usage:** `/pull <path>` or `/pull <shortcut>`',\n          '',\n          '**Shortcuts:**',\n          shortcuts,\n          '',\n          'Examples: `/pull plans`, `/pull /workspace/.openclaw-state/sessions`',\n        ].join('\\n'),\n      };\n    }\n\n    const filePath = resolvePath(args);\n\n    // Security check\n    if (isBlocked(filePath)) {\n      return { text: `Blocked: \\`${basename(filePath)}\\` matches a sensitive file pattern.` };\n    }\n\n    // Existence check\n    if (!existsSync(filePath)) {\n      return { text: `Not found: \\`${filePath}\\`` };\n    }\n\n    const stat = statSync(filePath);\n\n    // ── Directory: list contents ──────────────────────────────────\n    if (stat.isDirectory()) {\n      const listing = formatDirListing(filePath);\n      return {\n        text: `**${filePath}/**\\n\\`\\`\\`\\n${listing}\\n\\`\\`\\``,\n      };\n    }\n\n    // ── File too large even for document ─────────────────────────\n    if (stat.size > MAX_FILE_SIZE) {\n      return {\n        text: `File too large: \\`${basename(filePath)}\\` is ${formatSize(stat.size)} (max ${formatSize(MAX_FILE_SIZE)}).`,\n      };\n    }\n\n    // ── Small file: inline as code block ─────────────────────────\n    if (stat.size <= MAX_TEXT) {\n      try {\n        const content = readFileSync(filePath, 'utf8');\n        return {\n          text: `**${basename(filePath)}** (${formatSize(stat.size)})\\n\\`\\`\\`\\n${content}\\n\\`\\`\\``,\n        };\n      } catch {\n        return { text: `Could not read \\`${filePath}\\` as text.` };\n      }\n    }\n\n    // ── Large file: try Telegram document, fall back to truncation ─\n    const channel = extractChannelId(ctx);\n    const chatId = ctx?.conversationId ?? ctx?.senderId ?? '';\n\n    if (channel === 'telegram' && chatId) {\n      try {\n        await sendTelegramDocument(\n          chatId,\n          filePath,\n          `${basename(filePath)} (${formatSize(stat.size)})`,\n        );\n        return { text: `Sent \\`${basename(filePath)}\\` as document (${formatSize(stat.size)}).` };\n      } catch (err) {\n        // Fall through to truncation\n      }\n    }\n\n    // Truncated fallback\n    try {\n      const content = readFileSync(filePath, 'utf8');\n      const truncated = content.slice(0, MAX_TEXT);\n      return {\n        text: `**${basename(filePath)}** (${formatSize(stat.size)}, truncated)\\n\\`\\`\\`\\n${truncated}\\n\\`\\`\\`\\n\\n...truncated at ${formatSize(MAX_TEXT)} of ${formatSize(stat.size)}`,\n      };\n    } catch {\n      return { text: `Could not read \\`${filePath}\\` as text. Binary file?` };\n    }\n  },\n};\n"],"mappings":"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"}