@integrity-labs/agt-cli 0.15.33 → 0.15.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agt.js +3 -3
- package/dist/{chunk-AFUG4KD3.js → chunk-66ZLF2MI.js} +2 -5
- package/dist/chunk-66ZLF2MI.js.map +1 -0
- package/dist/{chunk-6J5SG6JQ.js → chunk-6DIVYBKA.js} +1 -1
- package/dist/{claude-pair-runtime-5ZZVGIR3.js → claude-pair-runtime-H4YBW4YE.js} +17 -76
- package/dist/claude-pair-runtime-H4YBW4YE.js.map +1 -0
- package/dist/lib/manager-worker.js +24 -56
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/{persistent-session-VRS3MFQ3.js → persistent-session-HUQXZSHP.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-AFUG4KD3.js.map +0 -1
- package/dist/claude-pair-runtime-5ZZVGIR3.js.map +0 -1
- /package/dist/{chunk-6J5SG6JQ.js.map → chunk-6DIVYBKA.js.map} +0 -0
- /package/dist/{persistent-session-VRS3MFQ3.js.map → persistent-session-HUQXZSHP.js.map} +0 -0
package/dist/bin/agt.js
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
success,
|
|
49
49
|
table,
|
|
50
50
|
warn
|
|
51
|
-
} from "../chunk-
|
|
51
|
+
} from "../chunk-6DIVYBKA.js";
|
|
52
52
|
|
|
53
53
|
// src/bin/agt.ts
|
|
54
54
|
import { join as join10 } from "path";
|
|
@@ -3732,7 +3732,7 @@ import { execFileSync, execSync } from "child_process";
|
|
|
3732
3732
|
import { existsSync as existsSync5, realpathSync } from "fs";
|
|
3733
3733
|
import chalk17 from "chalk";
|
|
3734
3734
|
import ora15 from "ora";
|
|
3735
|
-
var cliVersion = true ? "0.15.
|
|
3735
|
+
var cliVersion = true ? "0.15.34" : "dev";
|
|
3736
3736
|
async function fetchLatestVersion() {
|
|
3737
3737
|
const host2 = getHost();
|
|
3738
3738
|
if (!host2) return null;
|
|
@@ -4181,7 +4181,7 @@ function handleError(err) {
|
|
|
4181
4181
|
}
|
|
4182
4182
|
|
|
4183
4183
|
// src/bin/agt.ts
|
|
4184
|
-
var cliVersion2 = true ? "0.15.
|
|
4184
|
+
var cliVersion2 = true ? "0.15.34" : "dev";
|
|
4185
4185
|
var program = new Command();
|
|
4186
4186
|
program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
|
|
4187
4187
|
program.hook("preAction", (thisCommand) => {
|
|
@@ -50,9 +50,6 @@ function buildAllowedTools(mcpServerNames) {
|
|
|
50
50
|
function syncClaudeCredsToRoot() {
|
|
51
51
|
if (platform() !== "linux") return true;
|
|
52
52
|
if (typeof process.getuid !== "function" || process.getuid() !== 0) return true;
|
|
53
|
-
for (const filename of [".credentials.json", "credentials.json"]) {
|
|
54
|
-
if (existsSync(join("/root/.claude", filename))) return true;
|
|
55
|
-
}
|
|
56
53
|
let sourcePath = null;
|
|
57
54
|
try {
|
|
58
55
|
const entries = readdirSync("/home", { withFileTypes: true });
|
|
@@ -181,7 +178,7 @@ function spawnSession(config, session) {
|
|
|
181
178
|
if (claudeAuthMode === "subscription") {
|
|
182
179
|
const credsSynced = syncClaudeCredsToRoot();
|
|
183
180
|
if (!credsSynced && platform() === "linux" && typeof process.getuid === "function" && process.getuid() === 0) {
|
|
184
|
-
log(`[persistent-session] No Claude Code credentials found under /
|
|
181
|
+
log(`[persistent-session] No Claude Code credentials found under /home/*. Run 'claude /login' on the host first.`);
|
|
185
182
|
}
|
|
186
183
|
} else {
|
|
187
184
|
const claudeDir = join(homedir(), ".claude");
|
|
@@ -552,4 +549,4 @@ export {
|
|
|
552
549
|
stopAllSessionsAndWait,
|
|
553
550
|
getProjectDir
|
|
554
551
|
};
|
|
555
|
-
//# sourceMappingURL=chunk-
|
|
552
|
+
//# sourceMappingURL=chunk-66ZLF2MI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/persistent-session.ts","../src/lib/mcp-sanitize.ts","../src/lib/claude-tools.ts"],"sourcesContent":["/**\n * Persistent session manager for Claude Code agents.\n *\n * Hybrid approach:\n * - **tmux** for the interactive session (channels like Slack/Telegram\n * require a real TTY that only tmux provides)\n * - **acpx** for task injection (reliable prompt delivery via --no-wait,\n * avoids the tmux send-keys paste-not-submitting issue)\n *\n * On manager restart, detects existing tmux sessions and reattaches\n * without creating duplicates.\n */\n\nimport { spawn, execSync, execFileSync, type ChildProcess } from 'node:child_process';\nimport { join, dirname } from 'node:path';\nimport { homedir, platform } from 'node:os';\nimport { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, chmodSync, copyFileSync, rmSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { sanitizeMcpJson } from './mcp-sanitize.js';\nimport { buildAllowedTools } from './claude-tools.js';\n\n/**\n * When running as root on Linux, the tmux-spawned claude process reads\n * ~/.claude/.credentials.json from /root. But operators log in via `claude\n * /login` as ssm-user or ec2-user, leaving creds under their own home.\n * Copy the first valid creds file into /root/.claude so claude (running as\n * root inside tmux) finds them. Idempotent — safe to call on every spawn.\n *\n * Returns true if a copy was made (or the file is already up to date),\n * false if no creds could be found at all.\n */\nfunction syncClaudeCredsToRoot(): boolean {\n if (platform() !== 'linux') return true;\n if (typeof process.getuid !== 'function' || process.getuid() !== 0) return true;\n\n let sourcePath: string | null = null;\n try {\n const entries = readdirSync('/home', { withFileTypes: true });\n outer: for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n // Both filenames Claude Code has historically used — keep in sync\n // with findClaudeCredentialsPaths() in claude-auth-detect.ts.\n for (const filename of ['.credentials.json', 'credentials.json']) {\n const candidate = join('/home', entry.name, '.claude', filename);\n if (existsSync(candidate)) {\n sourcePath = candidate;\n break outer;\n }\n }\n }\n } catch { /* no /home or unreadable — fall through */ }\n\n if (!sourcePath) return false;\n\n const targetDir = '/root/.claude';\n // Preserve source filename so the resulting file matches what claude's\n // reader expects (it accepts either '.credentials.json' or 'credentials.json').\n const sourceFilename = sourcePath.endsWith('credentials.json') && !sourcePath.endsWith('.credentials.json')\n ? 'credentials.json'\n : '.credentials.json';\n const targetPath = join(targetDir, sourceFilename);\n try {\n if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true, mode: 0o700 });\n copyFileSync(sourcePath, targetPath);\n chmodSync(targetPath, 0o600);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the claude binary to an absolute path. The manager runs under a\n * minimal PATH (cloud-init root env) that doesn't include\n * /home/linuxbrew/.linuxbrew/bin, so a bare `claude` reference in the tmux\n * shell fails immediately — session exits, manager sees it as \"unhealthy\",\n * restarts, loops forever.\n *\n * Cached at first call: claude's location doesn't change between cycles,\n * and `which` spawns aren't free.\n */\nlet cachedClaudePath: string | null = null;\nexport function resolveClaudeBinary(): string {\n if (cachedClaudePath) return cachedClaudePath;\n // Operator override: honour CLAUDE_PATH for non-standard installs.\n const override = process.env.CLAUDE_PATH;\n if (override && existsSync(override)) {\n cachedClaudePath = override;\n return override;\n }\n // Try PATH first — respects an operator's custom install.\n try {\n const out = execSync('which claude 2>/dev/null', { encoding: 'utf-8' }).trim();\n if (out && existsSync(out)) {\n cachedClaudePath = out;\n return out;\n }\n } catch { /* fall through to canonical paths */ }\n const candidates = [\n '/home/linuxbrew/.linuxbrew/bin/claude',\n '/opt/homebrew/bin/claude',\n '/usr/local/bin/claude',\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n cachedClaudePath = p;\n return p;\n }\n }\n // Last resort — let the shell fail so logs show the missing binary.\n return 'claude';\n}\n\n/**\n * Collect MCP server names from the project .mcp.json to build the\n * --allowedTools pattern for tool isolation.\n */\nfunction collectMcpServerNames(mcpConfigPath: string): string[] {\n if (!existsSync(mcpConfigPath)) return [];\n try {\n const data = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));\n const servers = data.mcpServers as Record<string, unknown> | undefined;\n return servers ? Object.keys(servers) : [];\n } catch {\n return [];\n }\n}\n\n// ---------------------------------------------------------------------------\n// acpx binary resolver (used for task injection only)\n// ---------------------------------------------------------------------------\n\nlet _acpxBin: string | null = null;\nfunction getAcpxBin(): string {\n if (_acpxBin) return _acpxBin;\n\n // Walk up from this file to find node_modules/.bin/acpx.\n // Covers: dev (src/lib → ../../node_modules), built (dist/lib → ../../node_modules),\n // and npm global install (lib/node_modules/@scope/pkg/dist/lib → ../../node_modules).\n const moduleDir = dirname(fileURLToPath(import.meta.url));\n let dir = moduleDir;\n for (let i = 0; i < 6; i++) {\n const candidate = join(dir, 'node_modules', '.bin', 'acpx');\n if (existsSync(candidate)) {\n _acpxBin = candidate;\n return _acpxBin;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n try {\n execSync('which acpx', { stdio: 'ignore' });\n _acpxBin = 'acpx';\n return _acpxBin;\n } catch {\n // acpx not available — injection will fall back to tmux send-keys\n return '';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Types and state\n// ---------------------------------------------------------------------------\n\nexport interface PersistentSessionConfig {\n codeName: string;\n agentId: string;\n projectDir: string;\n mcpConfigPath: string;\n claudeMdPath: string;\n channels: string[];\n devChannels: string[];\n apiHost?: string;\n /**\n * Operator-configured Claude Code auth mode. 'subscription' (default) runs\n * `syncClaudeCredsToRoot()` so claude finds OAuth creds under /root/.claude.\n * 'api_key' puts ANTHROPIC_API_KEY into the spawn env AND deletes any\n * stored OAuth creds so the two auth paths are mutually exclusive.\n */\n claudeAuthMode?: 'subscription' | 'api_key';\n /** Decrypted Anthropic API key. Only used when claudeAuthMode === 'api_key'. */\n anthropicApiKey?: string | null;\n log: (msg: string) => void;\n}\n\nexport interface PersistentSession {\n codeName: string;\n startedAt: number | null;\n restartCount: number;\n status: 'starting' | 'running' | 'stopped' | 'crashed';\n}\n\nconst sessions = new Map<string, PersistentSession>();\n\n// ---------------------------------------------------------------------------\n// Session lifecycle (tmux-based)\n// ---------------------------------------------------------------------------\n\nexport function startPersistentSession(config: PersistentSessionConfig): PersistentSession {\n const existing = sessions.get(config.codeName);\n if (existing && existing.status === 'running') {\n return existing;\n }\n\n // Backoff on repeated crashes\n const restartCount = existing?.restartCount ?? 0;\n if (existing?.status === 'crashed' && existing.startedAt) {\n const backoffMs = Math.min(5000 * Math.pow(2, restartCount), 60_000);\n if (Date.now() - existing.startedAt < backoffMs) {\n return existing;\n }\n }\n\n const session: PersistentSession = {\n codeName: config.codeName,\n startedAt: null,\n restartCount,\n status: 'starting',\n };\n sessions.set(config.codeName, session);\n\n spawnSession(config, session);\n return session;\n}\n\nfunction spawnSession(config: PersistentSessionConfig, session: PersistentSession): void {\n const { codeName, projectDir, mcpConfigPath, claudeMdPath, channels, devChannels, apiHost, log } = config;\n const claudeAuthMode = config.claudeAuthMode ?? 'subscription';\n const tmuxSession = `agt-${codeName}`;\n\n log(`[persistent-session] Starting tmux session '${tmuxSession}' for '${codeName}' (auth=${claudeAuthMode})`);\n\n try {\n sanitizeMcpJson(mcpConfigPath, apiHost);\n\n // Also write acpx config for task injection\n writeAcpxConfig(config);\n\n // Kill any existing tmux session (clean slate)\n try {\n execSync(`tmux kill-session -t ${tmuxSession} 2>/dev/null`, { stdio: 'ignore' });\n } catch { /* no existing session */ }\n\n // When running as root, claude looks at $HOME/.claude/.credentials.json\n // Auth mode branch (mutually exclusive — never leave both channels armed):\n //\n // subscription: sync OAuth creds from /home/*/.claude into /root/.claude\n // (idempotent). Do NOT set ANTHROPIC_API_KEY in env.\n // api_key: DELETE any /root/.claude creds so claude can't fall\n // back to a stale OAuth session, then inject\n // ANTHROPIC_API_KEY into the spawn env below.\n //\n // Leaving both present is the \"confused deputy\" path: claude's internal\n // precedence between ANTHROPIC_API_KEY and OAuth has changed between\n // versions and is undocumented. Keep exactly one channel live.\n if (claudeAuthMode === 'subscription') {\n const credsSynced = syncClaudeCredsToRoot();\n if (!credsSynced && platform() === 'linux' && typeof process.getuid === 'function' && process.getuid() === 0) {\n log(`[persistent-session] No Claude Code credentials found under /home/*. Run 'claude /login' on the host first.`);\n }\n } else {\n // api_key mode — purge subscription creds under the current user's\n // home. Previously this was hardcoded to /root/.claude, which missed\n // non-root runs and macOS dev setups — letting OAuth creds silently\n // override the api_key in those environments. homedir() is what\n // claude-code itself reads, so that's the directory to clear.\n const claudeDir = join(homedir(), '.claude');\n for (const filename of ['.credentials.json', 'credentials.json']) {\n const p = join(claudeDir, filename);\n if (existsSync(p)) {\n try {\n rmSync(p, { force: true });\n log(`[persistent-session] Removed ${p} (api_key mode active — preventing OAuth fallback)`);\n } catch { /* non-fatal */ }\n }\n }\n if (!config.anthropicApiKey) {\n log(`[persistent-session] api_key mode but no anthropicApiKey passed. Session will fail auth.`);\n }\n }\n\n // Build claude args\n const args: string[] = [];\n if (channels.length > 0) args.push('--channels', ...channels);\n if (devChannels.length > 0) args.push('--dangerously-load-development-channels', ...devChannels);\n args.push('--mcp-config', mcpConfigPath);\n if (existsSync(claudeMdPath)) args.push('--system-prompt-file', claudeMdPath);\n args.push('--allow-dangerously-skip-permissions');\n args.push('--dangerously-skip-permissions');\n args.push('--strict-mcp-config');\n args.push('--name', tmuxSession);\n\n // Restrict tools to only the agent's configured MCP servers + built-in tools.\n // Without this, agents inherit the user's personal MCPs (Gmail, Calendar, etc.)\n const mcpServerNames = collectMcpServerNames(mcpConfigPath);\n args.push('--allowedTools', buildAllowedTools(mcpServerNames));\n\n // NOTE: CLAUDE_CODE_SIMPLE=1 blocks account plugins BUT also breaks\n // channel auth (Slack/Telegram require claude.ai OAuth). Instead, rely on\n // --strict-mcp-config + --allowedTools for tool isolation. Account plugins\n // may appear in the tool list but --allowedTools prevents calling them.\n //\n // IS_SANDBOX=1 bypasses claude's refusal to run under root/sudo with\n // --dangerously-skip-permissions. Dedicated EC2 hosts running only\n // agent workloads are effectively sandboxed (org-scoped VPC, no inbound,\n // no other tenants). Without this, the tmux session exits immediately\n // with \"cannot be used with root/sudo privileges for security reasons\".\n let envPrefix = 'IS_SANDBOX=1 ';\n const envIntegrationsPath = join(projectDir, '.env.integrations');\n if (existsSync(envIntegrationsPath)) {\n try {\n const envContent = readFileSync(envIntegrationsPath, 'utf-8');\n const envVars = envContent.split('\\n')\n .filter((line: string) => line && !line.startsWith('#') && line.includes('='))\n .map((line: string) => {\n const eqIdx = line.indexOf('=');\n const key = line.slice(0, eqIdx);\n const value = line.slice(eqIdx + 1);\n // Always quote values to prevent shell injection\n return `${key}=${JSON.stringify(value)}`;\n })\n .join(' ');\n if (envVars) envPrefix = `IS_SANDBOX=1 ${envVars} `;\n } catch { /* non-fatal */ }\n }\n\n // ANTHROPIC_API_KEY is passed via `tmux new-session -e` so it lands in\n // the session shell's env without ever appearing in the claude shell's\n // argv — `ps aux` on the long-running `bash -c \"claude ...\"` process\n // would otherwise expose the raw key for the session's lifetime.\n // The `-e` flag's exposure is bounded to the new-session invocation,\n // which exits in well under a second.\n const tmuxSessionEnvArgs: string[] = [];\n if (claudeAuthMode === 'api_key' && config.anthropicApiKey) {\n tmuxSessionEnvArgs.push('-e', `ANTHROPIC_API_KEY=${config.anthropicApiKey}`);\n }\n\n const initPrompt = 'You are now online. Say \"Ready.\" and wait for incoming messages. Do not run any tools or load any data until a message arrives.';\n const claudeBin = resolveClaudeBinary();\n const claudeCmd = `${envPrefix}${JSON.stringify(claudeBin)} ${JSON.stringify(initPrompt)} ${args.map(a => (a.includes(' ') || a.includes('*')) ? JSON.stringify(a) : a).join(' ')}`;\n\n // Start tmux session with claude in it\n const child = spawn('tmux', [\n 'new-session', '-d', '-s', tmuxSession, '-c', projectDir,\n ...tmuxSessionEnvArgs, claudeCmd,\n ], {\n cwd: projectDir,\n stdio: ['ignore', 'pipe', 'pipe'],\n env: process.env,\n });\n\n child.on('close', (code) => {\n if (code !== 0) {\n log(`[persistent-session] Failed to create tmux session for '${codeName}' (exit ${code})`);\n session.status = 'crashed';\n session.startedAt = Date.now();\n session.restartCount++;\n return;\n }\n log(`[persistent-session] tmux session '${tmuxSession}' created for '${codeName}'`);\n\n // Auto-accept startup dialogs\n acceptDialogs(tmuxSession, codeName, log).catch(() => {});\n });\n\n child.on('error', (err) => {\n log(`[persistent-session] Failed to start tmux for '${codeName}': ${err.message}`);\n session.status = 'crashed';\n session.startedAt = Date.now();\n session.restartCount++;\n });\n\n session.startedAt = Date.now();\n session.status = 'running';\n session.restartCount = 0;\n } catch (err) {\n log(`[persistent-session] Failed to start session for '${codeName}': ${(err as Error).message}`);\n session.status = 'crashed';\n session.startedAt = Date.now();\n session.restartCount++;\n }\n}\n\nasync function acceptDialogs(tmuxSession: string, codeName: string, log: (msg: string) => void): Promise<void> {\n for (let i = 0; i < 15; i++) {\n await new Promise((r) => setTimeout(r, 2000));\n try {\n const screen = execSync(`tmux capture-pane -t ${tmuxSession} -p 2>/dev/null`, { encoding: 'utf-8' });\n\n // First-run theme picker. Has to be dismissed BEFORE the\n // generic \"❯ no Enter to confirm\" exit branch below, since the\n // picker's selected row also renders with `❯`. Accept the\n // highlighted default (Dark mode); operator can change later\n // via /theme.\n if (\n screen.includes('Choose the text style') ||\n (screen.includes('Dark mode') && screen.includes('Light mode'))\n ) {\n execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: 'ignore' });\n log(`[persistent-session] Auto-accepted theme picker for '${codeName}'`);\n continue;\n }\n if (screen.includes('Yes, I trust this folder')) {\n execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: 'ignore' });\n log(`[persistent-session] Auto-accepted workspace trust for '${codeName}'`);\n continue;\n }\n if (screen.includes('I am using this for local development')) {\n execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: 'ignore' });\n log(`[persistent-session] Auto-accepted dev channels for '${codeName}'`);\n continue;\n }\n if (screen.includes('Enter to confirm') && screen.includes('MCP')) {\n execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: 'ignore' });\n log(`[persistent-session] Auto-accepted MCP servers for '${codeName}'`);\n continue;\n }\n if (screen.includes('Yes, I accept') && screen.includes('Bypass Permissions')) {\n execSync(`tmux send-keys -t ${tmuxSession} 2`, { stdio: 'ignore' });\n await new Promise((r) => setTimeout(r, 300));\n execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: 'ignore' });\n log(`[persistent-session] Auto-accepted bypass permissions for '${codeName}'`);\n continue;\n }\n if (screen.includes('❯') && !screen.includes('Enter to confirm')) {\n log(`[persistent-session] Session ready for '${codeName}' — no more dialogs`);\n break;\n }\n } catch { break; }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Task injection (acpx preferred, tmux send-keys fallback)\n// ---------------------------------------------------------------------------\n\nexport async function injectMessage(\n codeName: string,\n type: 'task' | 'chat' | 'system',\n content: string,\n meta?: Record<string, string>,\n log?: (msg: string) => void,\n): Promise<boolean> {\n const _log = log ?? ((_: string) => {});\n const session = sessions.get(codeName);\n if (!session || session.status !== 'running') {\n _log(`[inject] SKIP '${codeName}' — session ${session ? `status=${session.status}` : 'not found in Map'}`);\n return false;\n }\n\n const prefix = meta?.task_name ? `[Task: ${meta.task_name}] ` : '';\n const text = prefix + content;\n const projectDir = getProjectDir(codeName);\n\n // Preferred: use acpx exec for reliable injection (no paste issues).\n // Fire-and-forget — spawn detached so the manager loop isn't blocked.\n const acpx = getAcpxBin();\n if (acpx) {\n try {\n // Write prompt to temp file to avoid shell escaping issues\n const tmpDir = join(projectDir, '.claude');\n mkdirSync(tmpDir, { recursive: true });\n const tmpFile = join(tmpDir, '.agt-inject-prompt.txt');\n writeFileSync(tmpFile, text);\n\n _log(`[inject] acpx exec (fire-and-forget): cwd=${projectDir}, file=${tmpFile}`);\n const child = spawn(acpx, ['claude', 'exec', '-f', tmpFile], {\n cwd: projectDir,\n stdio: 'ignore',\n detached: true,\n });\n child.on('error', (err) => {\n _log(`[inject] acpx spawn error for '${codeName}': ${err.message}`);\n });\n child.unref();\n return true;\n } catch (err) {\n _log(`[inject] acpx exec failed for '${codeName}': ${(err as Error).message}`);\n // Fall through to tmux\n }\n } else {\n _log(`[inject] acpx binary not found — falling back to tmux send-keys`);\n }\n\n // Fallback: tmux send-keys (may have paste issues with long text)\n // Use execFileSync to avoid shell injection — text passed as literal arg\n try {\n execFileSync('tmux', ['send-keys', '-t', `agt-${codeName}`, text, 'Enter'], { stdio: 'ignore' });\n // tmux send-keys doesn't guarantee submission — return false so caller\n // doesn't advance scheduler state on an unverified keystroke\n _log(`[inject] tmux send-keys sent for '${codeName}' — unverified (returning false)`);\n return false;\n } catch (err) {\n _log(`[inject] tmux send-keys failed for '${codeName}': ${(err as Error).message}`);\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Session management\n// ---------------------------------------------------------------------------\n\nexport function stopPersistentSession(codeName: string, log: (msg: string) => void): void {\n const session = sessions.get(codeName);\n if (!session) return;\n\n log(`[persistent-session] Stopping session for '${codeName}'`);\n session.status = 'stopped';\n\n try {\n execSync(`tmux kill-session -t agt-${codeName} 2>/dev/null`, { stdio: 'ignore' });\n } catch { /* session may already be dead */ }\n\n // Also close any acpx session\n try {\n const acpx = getAcpxBin();\n if (acpx) {\n execFileSync(acpx, ['claude', 'sessions', 'close', `agt-${codeName}`], {\n cwd: getProjectDir(codeName),\n timeout: 5_000,\n stdio: 'ignore',\n });\n }\n } catch { /* non-fatal */ }\n\n sessions.delete(codeName);\n}\n\nexport function getSessionState(codeName: string): PersistentSession | null {\n return sessions.get(codeName) ?? null;\n}\n\n/**\n * Check if a persistent session is healthy.\n * Uses tmux has-session to check if the tmux session exists.\n * Also detects sessions from previous manager runs (not in the Map).\n */\nexport function isSessionHealthy(codeName: string): boolean {\n const tmuxSession = `agt-${codeName}`;\n\n // Check if tmux session exists\n try {\n execSync(`tmux has-session -t ${tmuxSession} 2>/dev/null`, { stdio: 'ignore' });\n } catch {\n // tmux session doesn't exist — mark as crashed but don't increment\n // restartCount here (that happens in spawnSession on actual failure)\n const session = sessions.get(codeName);\n if (session && session.status === 'running') {\n session.status = 'crashed';\n }\n return false;\n }\n\n // tmux session exists — ensure it's tracked in the Map\n if (!sessions.has(codeName)) {\n sessions.set(codeName, {\n codeName,\n startedAt: Date.now(),\n restartCount: 0,\n status: 'running',\n });\n }\n\n const session = sessions.get(codeName)!;\n if (session.status !== 'running') {\n session.status = 'running';\n }\n\n return true;\n}\n\nexport function resetRestartCount(codeName: string): void {\n const session = sessions.get(codeName);\n if (session) session.restartCount = 0;\n}\n\n// ---------------------------------------------------------------------------\n// Diagnostics — collect session health info for remote debugging\n// ---------------------------------------------------------------------------\n\nexport interface SessionDiagnostics {\n codeName: string;\n status: 'running' | 'starting' | 'stopped' | 'crashed' | 'unknown';\n startedAt: string | null;\n restartCount: number;\n tmuxAlive: boolean;\n screenCapture: string | null; // last N lines from tmux pane\n launchArgs: string | null; // process args\n channelStatus: string | null; // extracted from screen capture\n}\n\nexport function collectDiagnostics(codeNames: string[]): SessionDiagnostics[] {\n return codeNames.map((codeName) => {\n const session = sessions.get(codeName);\n const tmuxSession = `agt-${codeName}`;\n let tmuxAlive = false;\n let screenCapture: string | null = null;\n let launchArgs: string | null = null;\n let channelStatus: string | null = null;\n\n // Check tmux session (execFileSync to avoid shell injection)\n try {\n execFileSync('tmux', ['has-session', '-t', tmuxSession], { stdio: 'ignore' });\n tmuxAlive = true;\n } catch { /* session doesn't exist */ }\n\n // Capture last 30 lines from tmux pane\n if (tmuxAlive) {\n try {\n screenCapture = execFileSync('tmux', ['capture-pane', '-t', tmuxSession, '-p', '-S', '-30'], {\n encoding: 'utf-8',\n timeout: 3000,\n }).trim();\n } catch { /* non-fatal */ }\n }\n\n // Get process args via ps (safe — no user input in command)\n try {\n const psOutput = execFileSync('ps', ['aux'], { encoding: 'utf-8', timeout: 3000 });\n const line = psOutput.split('\\n').find((l) => l.includes(`agt-${codeName}`) && !l.includes('grep'));\n if (line) {\n const match = line.match(/claude\\s+.*/);\n launchArgs = match ? match[0].slice(0, 500) : null;\n }\n } catch { /* non-fatal */ }\n\n // Extract channel status from screen capture.\n // Only check the last 5 lines for current state — startup errors\n // may linger in scroll history but the agent could be healthy now.\n if (screenCapture) {\n const recentLines = screenCapture.split('\\n').slice(-5).join('\\n');\n const isIdle = recentLines.includes('❯');\n\n if (isIdle) {\n // Agent is at prompt — channels are likely working\n // Check full capture for persistent errors only\n if (screenCapture.includes('Channels require claude.ai authentication')) {\n channelStatus = 'error: auth required';\n } else {\n channelStatus = 'ok';\n }\n } else if (recentLines.includes('CHANNEL_ERROR') || recentLines.includes('CLOSED')) {\n channelStatus = 'error: disconnected';\n } else if (recentLines.includes('no MCP server configured')) {\n channelStatus = 'error: MCP server not found';\n } else if (recentLines.includes('ignored')) {\n channelStatus = 'error: channels ignored';\n } else {\n channelStatus = 'ok';\n }\n }\n\n return {\n codeName,\n status: tmuxAlive\n ? (session?.status ?? 'running')\n : (session?.status === 'running' ? 'crashed' : session?.status ?? 'unknown'),\n startedAt: session?.startedAt ? new Date(session.startedAt).toISOString() : null,\n restartCount: session?.restartCount ?? 0,\n tmuxAlive,\n screenCapture: screenCapture ? screenCapture.slice(-2000) : null, // limit size\n launchArgs,\n channelStatus,\n };\n });\n}\n\nexport function stopAllSessions(log: (msg: string) => void): void {\n for (const codeName of sessions.keys()) {\n stopPersistentSession(codeName, log);\n }\n}\n\nexport async function stopAllSessionsAndWait(\n log: (msg: string) => void,\n opts: { timeoutMs: number },\n): Promise<void> {\n const codeNames = [...sessions.keys()];\n if (codeNames.length === 0) return;\n\n for (const codeName of codeNames) {\n stopPersistentSession(codeName, log);\n }\n\n await new Promise<void>((resolve) => setTimeout(resolve, Math.min(opts.timeoutMs, 2000)));\n}\n\nexport function getProjectDir(codeName: string): string {\n return join(homedir(), '.augmented', codeName, 'project');\n}\n\n// ---------------------------------------------------------------------------\n// acpx config (needed for prompt-based injection)\n// ---------------------------------------------------------------------------\n\nfunction writeAcpxConfig(config: PersistentSessionConfig): void {\n const {\n projectDir,\n mcpConfigPath,\n claudeMdPath,\n channels,\n devChannels,\n anthropicApiKey,\n } = config;\n const claudeAuthMode = config.claudeAuthMode ?? 'subscription';\n\n const claudeArgs: string[] = [];\n if (channels.length > 0) claudeArgs.push('--channels', ...channels);\n if (devChannels.length > 0) claudeArgs.push('--dangerously-load-development-channels', ...devChannels);\n claudeArgs.push('--mcp-config', mcpConfigPath);\n if (existsSync(claudeMdPath)) claudeArgs.push('--system-prompt-file', claudeMdPath);\n claudeArgs.push('--allow-dangerously-skip-permissions');\n claudeArgs.push('--dangerously-skip-permissions');\n claudeArgs.push('--strict-mcp-config');\n\n // Tool isolation for acpx exec (same as tmux session)\n const mcpServerNames2 = collectMcpServerNames(mcpConfigPath);\n claudeArgs.push('--allowedTools', buildAllowedTools(mcpServerNames2));\n\n // Write a wrapper script that sources .env.integrations then runs the ACP\n // adapter. This avoids ENAMETOOLONG from inlining long tokens (e.g. Xero\n // JWTs) into the command string, and works around acpx not supporting an\n // `env` field on agent configs.\n const acpCmd = `npx -y @agentclientprotocol/claude-agent-acp ${claudeArgs.map(a => (a.includes(' ') || a.includes('*')) ? JSON.stringify(a) : a).join(' ')}`;\n const envIntegrationsPath = join(projectDir, '.env.integrations');\n const wrapperPath = join(projectDir, '.claude', 'acpx-agent.sh');\n const wrapperLines = ['#!/usr/bin/env bash'];\n if (existsSync(envIntegrationsPath)) {\n wrapperLines.push(`set -a`, `source ${JSON.stringify(envIntegrationsPath)}`, `set +a`);\n }\n // Mirror the tmux-session auth branch: when mode=api_key we've purged the\n // OAuth creds under /root/.claude, so ACP task injections (acpx) also need\n // ANTHROPIC_API_KEY or every injected task fails auth. JSON.stringify is\n // shell-safe under bash for sk-ant-* tokens (no $/` chars).\n if (claudeAuthMode === 'api_key' && anthropicApiKey) {\n wrapperLines.push(`export ANTHROPIC_API_KEY=${JSON.stringify(anthropicApiKey)}`);\n }\n wrapperLines.push(`exec ${acpCmd}`);\n mkdirSync(join(projectDir, '.claude'), { recursive: true });\n writeFileSync(wrapperPath, wrapperLines.join('\\n') + '\\n', { mode: 0o755 });\n\n const acpxConfig = {\n defaultAgent: 'claude',\n defaultPermissions: 'approve-all',\n agents: {\n claude: {\n command: wrapperPath,\n },\n },\n };\n\n writeFileSync(join(projectDir, '.acpxrc.json'), JSON.stringify(acpxConfig, null, 2));\n}\n","/**\n * Sanitize a Claude Code .mcp.json file for compatibility.\n *\n * Fixes:\n * 1. Relative proxy URLs (e.g., /mcp-proxy/...) — resolved to absolute if\n * apiHost is provided, otherwise removed.\n * 2. URL-based entries (type: \"sse\") — converted to mcp-remote stdio bridge\n * since Claude Code doesn't support SSE MCP servers natively.\n *\n * Returns true if the file was modified.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\n\nexport function sanitizeMcpJson(\n mcpConfigPath: string,\n apiHost?: string,\n): boolean {\n try {\n const mcpRaw = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));\n const servers = mcpRaw.mcpServers as Record<string, Record<string, unknown>> | undefined;\n if (!servers) return false;\n\n let changed = false;\n for (const [key, val] of Object.entries(servers)) {\n if (typeof val?.url !== 'string') continue;\n\n // Resolve relative URLs\n if (val.url.startsWith('/')) {\n if (apiHost) {\n val.url = `${apiHost}${val.url}`;\n changed = true;\n } else {\n delete servers[key];\n changed = true;\n continue;\n }\n }\n\n // Convert URL-based entries to mcp-remote stdio bridge\n // Claude Code doesn't support type: \"sse\" natively\n const url = val.url as string;\n delete val.url;\n delete val.type;\n val.command = 'npx';\n val.args = ['-y', 'mcp-remote', url, '--allow-http'];\n changed = true;\n }\n\n if (changed) writeFileSync(mcpConfigPath, JSON.stringify(mcpRaw, null, 2));\n return changed;\n } catch {\n return false;\n }\n}\n","// Shared helper for building Claude Code's --allowedTools string (ENG-4487).\n//\n// The manager spawns claude in three modes: persistent tmux session, acpx\n// exec wrapper, and one-shot `claude -p` for scheduled tasks + webapp direct\n// chat. Each site used to hand-roll its own allowedTools list, which drifted:\n// the one-shot paths forgot Skill and Agent, so integration skills under\n// .claude/skills/integration-... were silently invisible during scheduled-task\n// execution. Agents produced apologetic \"no data sources connected\" outputs\n// when the skills were actually on disk and their API keys were in env\n// vars — they just couldn't call the Skill tool.\n//\n// Invariant: every Claude Code invocation the manager spawns must include\n// Skill and Agent. Their absence disables integration-skill activation and\n// subagent dispatch without warning. Keep that list in one place so a new\n// spawn site physically cannot miss them.\n\n// Order is stable for test snapshots.\nconst BASE_TOOLS = ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Agent', 'Skill'] as const;\n\n// Build the comma-separated allowedTools string for a Claude Code spawn.\n// Each MCP server name becomes a wildcard pattern matching every tool that\n// server exposes; plus the eight base built-ins.\nexport function buildAllowedTools(mcpServerNames: readonly string[]): string {\n // Claude Code's allowedTools patterns use underscore-separated names; MCP\n // server IDs in .mcp.json can use hyphens (e.g. direct-chat), so normalise.\n const mcpPatterns = mcpServerNames.map((name) => `mcp__${name.replace(/-/g, '_')}__*`);\n return [...mcpPatterns, ...BASE_TOOLS].join(',');\n}\n"],"mappings":"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","names":["readFileSync","writeFileSync","readFileSync","writeFileSync","session"]}
|
|
@@ -14,29 +14,20 @@ var TWO_BYTE_RE = new RegExp(`${ANSI_ESC}[=>cM78]`, "g");
|
|
|
14
14
|
function stripAnsi(text) {
|
|
15
15
|
return text.replace(CSI_RE, "").replace(OSC_RE, "").replace(TWO_BYTE_RE, "");
|
|
16
16
|
}
|
|
17
|
-
var OAUTH_URL_RE = /https:\/\/(?:claude\.
|
|
18
|
-
function dewrapPane(rawPane) {
|
|
19
|
-
const stripped = stripAnsi(rawPane);
|
|
20
|
-
let dewrapped = stripped;
|
|
21
|
-
let prev = "";
|
|
22
|
-
while (prev !== dewrapped) {
|
|
23
|
-
prev = dewrapped;
|
|
24
|
-
dewrapped = dewrapped.replace(/(https?:\/\/\S+?)\n(?=\S)/, (_m, head) => head);
|
|
25
|
-
}
|
|
26
|
-
return dewrapped;
|
|
27
|
-
}
|
|
17
|
+
var OAUTH_URL_RE = /https:\/\/(?:claude\.ai|console\.anthropic\.com|auth\.anthropic\.com)\/[^\s)\]]*/;
|
|
28
18
|
function extractOAuthUrl(rawPane) {
|
|
29
|
-
const
|
|
19
|
+
const stripped = stripAnsi(rawPane);
|
|
20
|
+
const match = OAUTH_URL_RE.exec(stripped);
|
|
30
21
|
if (!match) return null;
|
|
31
22
|
return match[0].replace(/[.,;:!?]+$/, "");
|
|
32
23
|
}
|
|
33
24
|
var URL_PROMPT_RE = /(?:Paste code here|Paste your code|Enter (?:the )?code|Authorization code)/i;
|
|
34
25
|
function isUrlPromptReady(rawPane) {
|
|
35
|
-
const
|
|
36
|
-
return OAUTH_URL_RE.test(
|
|
26
|
+
const stripped = stripAnsi(rawPane);
|
|
27
|
+
return OAUTH_URL_RE.test(stripped) && URL_PROMPT_RE.test(stripped);
|
|
37
28
|
}
|
|
38
|
-
var SUCCESS_RE = /(?:Logged in|
|
|
39
|
-
var FAILURE_RE = /(?:Invalid (?:code|authorization code)|
|
|
29
|
+
var SUCCESS_RE = /(?:Logged in|Successfully (?:logged in|authenticated)|Authentication successful)/i;
|
|
30
|
+
var FAILURE_RE = /(?:Invalid (?:code|authorization code)|Authentication failed|Error (?:logging in|during authentication)|Login failed)/i;
|
|
40
31
|
function detectAuthOutcome(rawPane) {
|
|
41
32
|
const stripped = stripAnsi(rawPane);
|
|
42
33
|
const failureMatch = lastMatch(stripped, FAILURE_RE);
|
|
@@ -97,44 +88,25 @@ async function spawnPairSession(session) {
|
|
|
97
88
|
return { ok: true };
|
|
98
89
|
} catch {
|
|
99
90
|
}
|
|
100
|
-
const { resolveClaudeBinary } = await import("./persistent-session-
|
|
91
|
+
const { resolveClaudeBinary } = await import("./persistent-session-HUQXZSHP.js");
|
|
101
92
|
const claudeBin = resolveClaudeBinary();
|
|
102
93
|
try {
|
|
103
94
|
await execFileAsync("tmux", [
|
|
104
95
|
"new-session",
|
|
105
96
|
"-d",
|
|
106
|
-
"-x",
|
|
107
|
-
"240",
|
|
108
|
-
"-y",
|
|
109
|
-
"50",
|
|
110
97
|
"-s",
|
|
111
98
|
session,
|
|
112
99
|
claudeBin
|
|
113
100
|
]);
|
|
101
|
+
return { ok: true };
|
|
114
102
|
} catch (err) {
|
|
115
103
|
return { ok: false, error: classifyTmuxError(err) };
|
|
116
104
|
}
|
|
117
|
-
await sleep(500);
|
|
118
|
-
try {
|
|
119
|
-
await execFileAsync("tmux", ["has-session", "-t", session]);
|
|
120
|
-
} catch {
|
|
121
|
-
return {
|
|
122
|
-
ok: false,
|
|
123
|
-
error: {
|
|
124
|
-
kind: "unknown",
|
|
125
|
-
message: `claude exited immediately after launch (binary at ${claudeBin}). Run \`${claudeBin}\` manually on the host to see why \u2014 likely missing TTY, missing HOME, or a startup error.`
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
return { ok: true };
|
|
130
105
|
}
|
|
131
106
|
async function killPairSession(session) {
|
|
132
107
|
try {
|
|
133
108
|
await execFileAsync("tmux", ["kill-session", "-t", session]);
|
|
134
|
-
|
|
135
|
-
} catch (err) {
|
|
136
|
-
if (classifyTmuxError(err).kind === "no-session") return true;
|
|
137
|
-
return false;
|
|
109
|
+
} catch {
|
|
138
110
|
}
|
|
139
111
|
}
|
|
140
112
|
async function startClaudePair(opts) {
|
|
@@ -161,7 +133,7 @@ async function startClaudePair(opts) {
|
|
|
161
133
|
await sleep(pollIntervalMs);
|
|
162
134
|
let pane;
|
|
163
135
|
try {
|
|
164
|
-
pane = await capturePane(session
|
|
136
|
+
pane = await capturePane(session);
|
|
165
137
|
} catch (err) {
|
|
166
138
|
return { kind: "error", error: classifyTmuxError(err) };
|
|
167
139
|
}
|
|
@@ -169,17 +141,6 @@ async function startClaudePair(opts) {
|
|
|
169
141
|
const url = extractOAuthUrl(pane);
|
|
170
142
|
if (url) return { kind: "url", url };
|
|
171
143
|
}
|
|
172
|
-
const hasOAuthInvalidCode = /OAuth error[\s\S]*Invalid code/i.test(pane);
|
|
173
|
-
const hasOAuthRetryPrompt = /OAuth error/i.test(pane) && /Press Enter to retry/i.test(pane);
|
|
174
|
-
if (hasOAuthInvalidCode || hasOAuthRetryPrompt) {
|
|
175
|
-
return {
|
|
176
|
-
kind: "error",
|
|
177
|
-
error: {
|
|
178
|
-
kind: "unknown",
|
|
179
|
-
message: "claude is stuck on a previous failed-login retry prompt. SSH to the host and clear ~/.claude/.credentials.json (and any *.json next to it), then retry pair-via-browser."
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
144
|
if (/Select login method:/i.test(pane)) {
|
|
184
145
|
await dispatchEnter();
|
|
185
146
|
continue;
|
|
@@ -243,44 +204,24 @@ async function submitClaudePairCode(opts) {
|
|
|
243
204
|
};
|
|
244
205
|
}
|
|
245
206
|
try {
|
|
246
|
-
await
|
|
247
|
-
} catch (err) {
|
|
248
|
-
return { kind: "error", error: classifyTmuxError(err) };
|
|
249
|
-
}
|
|
250
|
-
await sleep(250);
|
|
251
|
-
try {
|
|
252
|
-
await sendKeys(session, "Enter");
|
|
207
|
+
await sendKeys(session, opts.code.trim(), "C-m");
|
|
253
208
|
} catch (err) {
|
|
254
209
|
return { kind: "error", error: classifyTmuxError(err) };
|
|
255
210
|
}
|
|
256
211
|
const deadline = Date.now() + timeoutMs;
|
|
257
|
-
let enterRetried = false;
|
|
258
|
-
let lastPane = "";
|
|
259
212
|
while (Date.now() < deadline) {
|
|
260
213
|
await sleep(pollIntervalMs);
|
|
214
|
+
let pane;
|
|
261
215
|
try {
|
|
262
|
-
|
|
216
|
+
pane = await capturePane(session);
|
|
263
217
|
} catch (err) {
|
|
264
218
|
return { kind: "error", error: classifyTmuxError(err) };
|
|
265
219
|
}
|
|
266
|
-
const outcome = detectAuthOutcome(
|
|
220
|
+
const outcome = detectAuthOutcome(pane);
|
|
267
221
|
if (outcome.kind === "success") return { kind: "success", rawMatch: outcome.rawMatch };
|
|
268
222
|
if (outcome.kind === "failure") return { kind: "failure", rawMatch: outcome.rawMatch };
|
|
269
|
-
if (!enterRetried && Date.now() - (deadline - timeoutMs) > 5e3 && /Paste code here/i.test(lastPane)) {
|
|
270
|
-
enterRetried = true;
|
|
271
|
-
try {
|
|
272
|
-
await sendKeys(session, "C-m");
|
|
273
|
-
} catch {
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
223
|
}
|
|
277
|
-
return {
|
|
278
|
-
kind: "error",
|
|
279
|
-
error: {
|
|
280
|
-
kind: "unknown",
|
|
281
|
-
message: `submit timed out after ${timeoutMs}ms \u2014 outcome regex didn't match. Last pane: ${lastPane.slice(-600)}`
|
|
282
|
-
}
|
|
283
|
-
};
|
|
224
|
+
return { kind: "timeout" };
|
|
284
225
|
}
|
|
285
226
|
async function getClaudePairStatus(session) {
|
|
286
227
|
try {
|
|
@@ -313,4 +254,4 @@ export {
|
|
|
313
254
|
startClaudePair,
|
|
314
255
|
submitClaudePairCode
|
|
315
256
|
};
|
|
316
|
-
//# sourceMappingURL=claude-pair-runtime-
|
|
257
|
+
//# sourceMappingURL=claude-pair-runtime-H4YBW4YE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/claude-pair-runtime.ts","../src/lib/claude-pair-parser.ts"],"sourcesContent":["/**\n * ENG-4580: manager-side runtime for the Claude Code OAuth pairing flow.\n *\n * These functions own the actual tmux dance — sending `/login`,\n * polling the pane until Claude Code prints the OAuth URL, sending\n * the auth code, and detecting success/failure. They are the\n * counterpart to the pure parser in `claude-pair-parser.ts`.\n *\n * The API surface in ENG-4581 will wrap these — they don't include\n * any HTTP / DB code so the unit tests can target the parser layer\n * without spinning up a fake API. Errors are classified into the\n * `SessionError` shape so the API can translate them into structured\n * 4xx responses (e.g. `session_missing`).\n *\n * Architectural note: tmux capture-pane on a session that doesn't\n * exist exits non-zero with `can't find session`. Same goes for tmux\n * not being installed. classifyTmuxError covers both.\n */\n\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nimport {\n classifyTmuxError,\n detectAuthOutcome,\n extractOAuthUrl,\n isUrlPromptReady,\n type AuthOutcome,\n type SessionError,\n} from './claude-pair-parser.js';\n\nconst execFileAsync = promisify(execFile);\n\n// Pair-scoped tmux session name. The pairing flow runs inside a\n// throwaway `claude` instance — never inside an agent's persistent\n// session — so first-time auth on a fresh host works (no chicken-and-\n// egg) and re-auth doesn't disrupt an in-flight agent conversation.\nexport function pairTmuxSession(pairId: string): string {\n return `agt-pair-${pairId.slice(0, 12)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Low-level helpers\n// ---------------------------------------------------------------------------\n\ninterface CapturePaneOpts {\n /** How many lines of scrollback to include (negative = lines back). Default -200. */\n scrollback?: number;\n}\n\nasync function capturePane(session: string, opts: CapturePaneOpts = {}): Promise<string> {\n const scrollback = opts.scrollback ?? -200;\n const { stdout } = await execFileAsync('tmux', [\n 'capture-pane',\n '-t',\n session,\n '-p',\n '-S',\n String(scrollback),\n ]);\n return stdout;\n}\n\nasync function sendKeys(session: string, ...keys: string[]): Promise<void> {\n await execFileAsync('tmux', ['send-keys', '-t', session, ...keys]);\n}\n\nasync function sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n// ---------------------------------------------------------------------------\n// Pair-scoped tmux session lifecycle\n// ---------------------------------------------------------------------------\n\n/**\n * Spawn a throwaway tmux session running `claude` for the pairing flow.\n * Idempotent — if the session already exists, returns success silently.\n * Caller is responsible for `killPairSession` once the pair reaches a\n * terminal state.\n *\n * Uses an absolute path to the claude binary so we don't depend on the\n * inherited PATH (the manager runs with cloud-init's minimal env on\n * EC2). resolveClaudeBinary checks CLAUDE_PATH, then `which`, then\n * canonical Linux/macOS Homebrew install dirs.\n */\nexport async function spawnPairSession(session: string): Promise<{ ok: true } | { ok: false; error: SessionError }> {\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n return { ok: true };\n } catch {\n // session doesn't exist yet — fall through to create it\n }\n\n const { resolveClaudeBinary } = await import('./persistent-session.js');\n const claudeBin = resolveClaudeBinary();\n\n try {\n await execFileAsync('tmux', [\n 'new-session',\n '-d',\n '-s',\n session,\n claudeBin,\n ]);\n return { ok: true };\n } catch (err) {\n return { ok: false, error: classifyTmuxError(err) };\n }\n}\n\nexport async function killPairSession(session: string): Promise<void> {\n try {\n await execFileAsync('tmux', ['kill-session', '-t', session]);\n } catch {\n // Best-effort cleanup; missing-session errors are fine.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Result shapes\n// ---------------------------------------------------------------------------\n\nexport type ClaudePairStartResult =\n | { kind: 'url'; url: string }\n | { kind: 'timeout' }\n | { kind: 'error'; error: SessionError };\n\nexport type ClaudePairSubmitResult =\n | { kind: 'success'; rawMatch: string }\n | { kind: 'failure'; rawMatch: string }\n | { kind: 'timeout' }\n | { kind: 'error'; error: SessionError };\n\nexport type ClaudePairStatusResult =\n | { kind: 'idle' }\n | { kind: 'awaiting-code'; url: string }\n | { kind: 'success' }\n | { kind: 'failure'; rawMatch: string }\n | { kind: 'session-missing' }\n | { kind: 'error'; error: SessionError };\n\n// ---------------------------------------------------------------------------\n// start — send `/login`, wait for the OAuth URL prompt\n// ---------------------------------------------------------------------------\n\nexport interface ClaudePairStartOpts {\n /** Pair-scoped tmux session name (see pairTmuxSession). */\n session: string;\n /** Total time to wait for Claude Code to print the URL prompt. Default 60s. */\n timeoutMs?: number;\n /** How often to re-capture and check the pane. Default 500ms. */\n pollIntervalMs?: number;\n}\n\nexport async function startClaudePair(opts: ClaudePairStartOpts): Promise<ClaudePairStartResult> {\n const { session } = opts;\n // Pair sessions cold-start `claude` from scratch — first-run on a fresh\n // host can take 10-30s before the prompt is interactive enough to\n // accept `/login`. Stay generous.\n const timeoutMs = opts.timeoutMs ?? 60_000;\n const pollIntervalMs = opts.pollIntervalMs ?? 500;\n\n // Quick precheck — fail fast if the tmux session doesn't exist\n // rather than blasting `/login` into an unrelated pane.\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Drive claude through its first-run onboarding to reach a state\n // where the OAuth URL is on screen. Possible paths:\n //\n // • Fresh host: theme picker → \"Trust folder?\" → login method picker\n // (option 1 = Claude subscription, already highlighted) → OAuth URL.\n // No `/login` needed — option 1 IS the login.\n // • Already onboarded: regular prompt → we send `/login` ourselves →\n // login method picker → OAuth URL.\n //\n // Auto-advance every onboarding screen we recognize. Keep going until\n // we either see the URL prompt or hit the deadline.\n const onboardingDeadline = Date.now() + Math.min(45_000, timeoutMs);\n let lastDispatchAt = 0;\n let loginCommandSent = false;\n const dispatchEnter = async (): Promise<void> => {\n if (Date.now() - lastDispatchAt < 1_500) return;\n lastDispatchAt = Date.now();\n try { await sendKeys(session, 'C-m'); } catch { /* keep polling */ }\n };\n\n while (Date.now() < onboardingDeadline) {\n await sleep(pollIntervalMs);\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Already at the URL prompt — done driving onboarding, fall through\n // to the URL-extraction loop below.\n if (isUrlPromptReady(pane)) {\n const url = extractOAuthUrl(pane);\n if (url) return { kind: 'url', url };\n }\n\n // Login method picker — option 1 (Claude subscription) is already\n // highlighted, Enter selects it.\n if (/Select login method:/i.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // First-run theme picker. Accept the highlighted default; operator\n // can change it later via /theme.\n if (/\\bDark mode\\b/.test(pane) && /\\bLight mode\\b/.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // \"Trust this folder?\" — Enter accepts the default (Yes).\n if (/Do you trust the files in this folder\\?/i.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // Onboarding \"Press Enter to continue\" splashes.\n if (/press\\s+enter\\s+to\\s+continue/i.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // Regular interactive prompt — this means claude was already\n // onboarded. Send `/login` once to surface the login picker, then\n // the next iteration handles it via the picker branch above.\n const promptVisible =\n /[│|]\\s*>\\s/.test(pane) || /Try .+ to .+/.test(pane) || /^\\s*>\\s*$/m.test(pane);\n if (promptVisible && !loginCommandSent) {\n loginCommandSent = true;\n try {\n await sendKeys(session, '/login', 'C-m');\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n continue;\n }\n }\n\n // Onboarding deadline hit without seeing a URL — surface the pane so\n // operators can see what was on screen when we gave up.\n let lastPane = '';\n try { lastPane = await capturePane(session); } catch { /* best-effort */ }\n return {\n kind: 'error',\n error: {\n kind: 'unknown',\n message: `claude never reached OAuth URL prompt within ${timeoutMs}ms. Last pane: ${lastPane.slice(-500)}`,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// submit-code — paste the auth code, wait for outcome\n// ---------------------------------------------------------------------------\n\nexport interface ClaudePairSubmitOpts {\n /** Pair-scoped tmux session name (see pairTmuxSession). */\n session: string;\n code: string;\n /** Total time to wait for the success/failure marker. Default 30s. */\n timeoutMs?: number;\n pollIntervalMs?: number;\n}\n\nexport async function submitClaudePairCode(\n opts: ClaudePairSubmitOpts,\n): Promise<ClaudePairSubmitResult> {\n const { session } = opts;\n const timeoutMs = opts.timeoutMs ?? 30_000;\n const pollIntervalMs = opts.pollIntervalMs ?? 500;\n\n // Validate code shape minimally — Claude Code's auth codes are\n // alphanumeric with dashes, ~40-80 chars. Accept anything within\n // that envelope; reject blank or whitespace-only to avoid\n // accidentally submitting an empty buffer.\n if (!opts.code || !opts.code.trim()) {\n return {\n kind: 'error',\n error: { kind: 'unknown', message: 'empty auth code' },\n };\n }\n if (opts.code.length > 1024) {\n return {\n kind: 'error',\n error: { kind: 'unknown', message: 'auth code suspiciously long' },\n };\n }\n\n // Send the code + Enter. We use the literal value as one send-keys\n // argument; tmux handles spaces fine, but newlines would terminate\n // early so reject those as well.\n if (/[\\r\\n]/.test(opts.code)) {\n return {\n kind: 'error',\n error: { kind: 'unknown', message: 'auth code contains newline' },\n };\n }\n\n try {\n await sendKeys(session, opts.code.trim(), 'C-m');\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n await sleep(pollIntervalMs);\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n const outcome: AuthOutcome = detectAuthOutcome(pane);\n if (outcome.kind === 'success') return { kind: 'success', rawMatch: outcome.rawMatch };\n if (outcome.kind === 'failure') return { kind: 'failure', rawMatch: outcome.rawMatch };\n }\n return { kind: 'timeout' };\n}\n\n// ---------------------------------------------------------------------------\n// status — non-mutating peek at the pane state\n// ---------------------------------------------------------------------------\n\nexport async function getClaudePairStatus(session: string): Promise<ClaudePairStatusResult> {\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n } catch (err) {\n const classified = classifyTmuxError(err);\n if (classified.kind === 'no-session') return { kind: 'session-missing' };\n return { kind: 'error', error: classified };\n }\n\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Outcome takes priority — if the pane already shows success/failure\n // from a recent submission, the API can short-circuit without\n // restarting the flow.\n const outcome = detectAuthOutcome(pane);\n if (outcome.kind === 'success') return { kind: 'success' };\n if (outcome.kind === 'failure') return { kind: 'failure', rawMatch: outcome.rawMatch };\n\n if (isUrlPromptReady(pane)) {\n const url = extractOAuthUrl(pane);\n if (url) return { kind: 'awaiting-code', url };\n }\n return { kind: 'idle' };\n}\n","/**\n * ENG-4580: pane-scrape parser for Claude Code's `/login` OAuth flow.\n *\n * The manager drives the flow by sending `/login` into the agent's\n * persistent tmux session, capturing the pane after a short poll, and\n * extracting the OAuth URL Claude Code prints. After the operator\n * pastes the auth code via the UI, the manager sends it back into the\n * pane and polls for a success / failure marker.\n *\n * Everything in this module is pure — no tmux calls, no fs I/O. The\n * runtime side (apps/cli/src/lib/manager-worker.ts) shells out and\n * feeds the captured pane string through these functions.\n *\n * Why a dedicated module: pane scraping is fragile across Claude Code\n * versions, terminal widths, and locale changes. Centralising the\n * regexes + the ANSI stripper makes them easy to fixture-test and\n * iterate on without touching the runtime path.\n */\n\n// ---------------------------------------------------------------------------\n// ANSI escape sequence stripper\n// ---------------------------------------------------------------------------\n\n/**\n * Strip the ANSI escape sequences a terminal emits for colour, cursor\n * movement, screen clears, and bracketed paste mode. The pattern below\n * covers:\n *\n * - CSI sequences: `ESC [ ... <final byte>` where the final byte is\n * in the 0x40-0x7E range (covers SGR colour, cursor-position,\n * erase-in-line/display, etc.)\n * - OSC sequences: `ESC ] ... BEL` or `ESC ] ... ESC \\` (used for\n * window titles and hyperlinks)\n * - Single-character `ESC <char>` two-byte escapes (e.g. `ESC =`,\n * `ESC >`, the `ESC c` reset)\n *\n * We keep newlines and printable text intact so pane content remains\n * matchable after stripping.\n *\n * The regex uses Unicode-friendly character classes; we explicitly\n * avoid `\\x1b` named escapes in source to keep the file ASCII-safe.\n */\nconst ANSI_ESC = String.fromCharCode(0x1b);\nconst ANSI_BEL = String.fromCharCode(0x07);\n\nconst CSI_RE = new RegExp(`${ANSI_ESC}\\\\[[0-?]*[ -/]*[@-~]`, 'g');\nconst OSC_RE = new RegExp(\n `${ANSI_ESC}\\\\][^${ANSI_BEL}${ANSI_ESC}]*(?:${ANSI_BEL}|${ANSI_ESC}\\\\\\\\)`,\n 'g',\n);\nconst TWO_BYTE_RE = new RegExp(`${ANSI_ESC}[=>cM78]`, 'g');\n\nexport function stripAnsi(text: string): string {\n return text.replace(CSI_RE, '').replace(OSC_RE, '').replace(TWO_BYTE_RE, '');\n}\n\n// ---------------------------------------------------------------------------\n// OAuth URL extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Anchored to Anthropic-owned domains that Claude Code's `/login`\n * actually prints. Adding more hosts is fine — keep them allowlisted\n * rather than matching arbitrary `https://` to avoid pulling random\n * URLs from the user's previous shell output.\n */\nconst OAUTH_URL_RE =\n /https:\\/\\/(?:claude\\.ai|console\\.anthropic\\.com|auth\\.anthropic\\.com)\\/[^\\s)\\]]*/;\n\nexport function extractOAuthUrl(rawPane: string): string | null {\n const stripped = stripAnsi(rawPane);\n const match = OAUTH_URL_RE.exec(stripped);\n if (!match) return null;\n // Trim trailing punctuation that often clings to URLs in TUIs.\n return match[0].replace(/[.,;:!?]+$/, '');\n}\n\n// ---------------------------------------------------------------------------\n// Prompt readiness — \"we've printed the URL, now waiting for a code\"\n// ---------------------------------------------------------------------------\n\nconst URL_PROMPT_RE =\n /(?:Paste code here|Paste your code|Enter (?:the )?code|Authorization code)/i;\n\nexport function isUrlPromptReady(rawPane: string): boolean {\n const stripped = stripAnsi(rawPane);\n // Both anchors must be present: the URL itself AND the paste-code\n // prompt. The prompt alone could appear during a stale screen redraw;\n // the URL alone could be a stray match in command history.\n return OAUTH_URL_RE.test(stripped) && URL_PROMPT_RE.test(stripped);\n}\n\n// ---------------------------------------------------------------------------\n// Outcome detection after submitting the code\n// ---------------------------------------------------------------------------\n\nconst SUCCESS_RE =\n /(?:Logged in|Successfully (?:logged in|authenticated)|Authentication successful)/i;\nconst FAILURE_RE =\n /(?:Invalid (?:code|authorization code)|Authentication failed|Error (?:logging in|during authentication)|Login failed)/i;\n\nexport type AuthOutcome =\n | { kind: 'success'; rawMatch: string }\n | { kind: 'failure'; rawMatch: string }\n | { kind: 'pending' };\n\nexport function detectAuthOutcome(rawPane: string): AuthOutcome {\n const stripped = stripAnsi(rawPane);\n // Failure first — Claude Code sometimes prints a stale \"logged in\" from\n // a previous successful session above the new failure banner. The\n // most-recent line wins, so we scan from the end of the pane.\n const failureMatch = lastMatch(stripped, FAILURE_RE);\n const successMatch = lastMatch(stripped, SUCCESS_RE);\n\n if (failureMatch && successMatch) {\n // Whichever is later on the pane is the live state.\n if (failureMatch.index > successMatch.index) {\n return { kind: 'failure', rawMatch: failureMatch.match };\n }\n return { kind: 'success', rawMatch: successMatch.match };\n }\n if (failureMatch) return { kind: 'failure', rawMatch: failureMatch.match };\n if (successMatch) return { kind: 'success', rawMatch: successMatch.match };\n return { kind: 'pending' };\n}\n\nfunction lastMatch(haystack: string, re: RegExp): { match: string; index: number } | null {\n // Construct a sticky/global variant if needed. Most of our REs are\n // anchored to small phrases; iterating with a `g`-flagged RegExp is\n // cheap and correct.\n const globalRe = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);\n let last: RegExpExecArray | null = null;\n let m: RegExpExecArray | null;\n while ((m = globalRe.exec(haystack)) !== null) {\n last = m;\n // Prevent zero-length matches from looping.\n if (m.index === globalRe.lastIndex) globalRe.lastIndex++;\n }\n return last ? { match: last[0], index: last.index } : null;\n}\n\n// ---------------------------------------------------------------------------\n// \"Session not running / tmux missing\" — surface as a structured signal\n// ---------------------------------------------------------------------------\n\n/**\n * The runtime path will throw when `tmux capture-pane` fails. This\n * helper classifies the failure for the API layer so the UI can show\n * \"start a session first\" rather than a generic 500.\n */\nexport type SessionError =\n | { kind: 'no-session' }\n | { kind: 'tmux-missing' }\n | { kind: 'pane-empty' }\n | { kind: 'unknown'; message: string };\n\nexport function classifyTmuxError(err: unknown): SessionError {\n const msg = err instanceof Error ? err.message : String(err);\n if (/can't find session|no server running/i.test(msg)) return { kind: 'no-session' };\n if (/command not found.*tmux|ENOENT.*tmux/i.test(msg)) return { kind: 'tmux-missing' };\n return { kind: 'unknown', message: msg };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
resolveChannels,
|
|
23
23
|
resolveDmTarget,
|
|
24
24
|
wrapScheduledTaskPrompt
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-6DIVYBKA.js";
|
|
26
26
|
import {
|
|
27
27
|
findTaskByTemplate,
|
|
28
28
|
getProjectDir,
|
|
@@ -42,11 +42,11 @@ import {
|
|
|
42
42
|
startPersistentSession,
|
|
43
43
|
stopAllSessionsAndWait,
|
|
44
44
|
stopPersistentSession
|
|
45
|
-
} from "../chunk-
|
|
45
|
+
} from "../chunk-66ZLF2MI.js";
|
|
46
46
|
|
|
47
47
|
// src/lib/manager-worker.ts
|
|
48
48
|
import { createHash } from "crypto";
|
|
49
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync, existsSync as existsSync2, rmSync as rmSync2, readdirSync as readdirSync2, statSync, unlinkSync, copyFileSync } from "fs";
|
|
49
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, appendFileSync, mkdirSync as mkdirSync2, chmodSync, existsSync as existsSync2, rmSync as rmSync2, readdirSync as readdirSync2, statSync, unlinkSync, copyFileSync } from "fs";
|
|
50
50
|
import https from "https";
|
|
51
51
|
import { execFileSync as syncExecFile } from "child_process";
|
|
52
52
|
import { join as join3, dirname } from "path";
|
|
@@ -1435,7 +1435,6 @@ function clearAgentCaches(agentId, codeName) {
|
|
|
1435
1435
|
var cachedFrameworkVersion = null;
|
|
1436
1436
|
var lastVersionCheckAt = 0;
|
|
1437
1437
|
var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
1438
|
-
var agtCliVersion = true ? "0.15.33" : "dev";
|
|
1439
1438
|
function resolveBrewPath(execFileSync2) {
|
|
1440
1439
|
try {
|
|
1441
1440
|
const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
|
|
@@ -1448,21 +1447,7 @@ function resolveBrewPath(execFileSync2) {
|
|
|
1448
1447
|
}
|
|
1449
1448
|
var toolkitCliEnsured = /* @__PURE__ */ new Set();
|
|
1450
1449
|
var toolkitCliRetryAfter = /* @__PURE__ */ new Map();
|
|
1451
|
-
var toolkitCliFailureCount = /* @__PURE__ */ new Map();
|
|
1452
1450
|
var TOOLKIT_INSTALL_RETRY_MS = 5 * 6e4;
|
|
1453
|
-
var TOOLKIT_INSTALL_MAX_FAILURES = 3;
|
|
1454
|
-
function recordToolkitFailure(toolkitSlug, reason) {
|
|
1455
|
-
const count = (toolkitCliFailureCount.get(toolkitSlug) ?? 0) + 1;
|
|
1456
|
-
toolkitCliFailureCount.set(toolkitSlug, count);
|
|
1457
|
-
if (count >= TOOLKIT_INSTALL_MAX_FAILURES) {
|
|
1458
|
-
log(`[toolkit-install] ${toolkitSlug}: ${reason} (giving up after ${count} attempts \u2014 restart the manager to retry)`);
|
|
1459
|
-
toolkitCliEnsured.add(toolkitSlug);
|
|
1460
|
-
toolkitCliRetryAfter.delete(toolkitSlug);
|
|
1461
|
-
} else {
|
|
1462
|
-
log(`[toolkit-install] ${toolkitSlug}: ${reason} (attempt ${count}/${TOOLKIT_INSTALL_MAX_FAILURES}, retrying in ${TOOLKIT_INSTALL_RETRY_MS / 6e4}m)`);
|
|
1463
|
-
toolkitCliRetryAfter.set(toolkitSlug, Date.now() + TOOLKIT_INSTALL_RETRY_MS);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
1451
|
async function ensureToolkitCli(toolkitSlug) {
|
|
1467
1452
|
if (toolkitCliEnsured.has(toolkitSlug)) return;
|
|
1468
1453
|
const retryAfter = toolkitCliRetryAfter.get(toolkitSlug) ?? 0;
|
|
@@ -1479,7 +1464,6 @@ async function ensureToolkitCli(toolkitSlug) {
|
|
|
1479
1464
|
execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
|
|
1480
1465
|
toolkitCliEnsured.add(toolkitSlug);
|
|
1481
1466
|
toolkitCliRetryAfter.delete(toolkitSlug);
|
|
1482
|
-
toolkitCliFailureCount.delete(toolkitSlug);
|
|
1483
1467
|
return;
|
|
1484
1468
|
} catch {
|
|
1485
1469
|
}
|
|
@@ -1514,7 +1498,7 @@ async function ensureToolkitCli(toolkitSlug) {
|
|
|
1514
1498
|
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
1515
1499
|
log(`[toolkit-install] ${toolkitSlug}: installing via brew (${pkg})\u2026`);
|
|
1516
1500
|
if (isRoot) {
|
|
1517
|
-
execFileSync2("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe"
|
|
1501
|
+
execFileSync2("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe" });
|
|
1518
1502
|
} else {
|
|
1519
1503
|
execFileSync2(brewPath, ["install", pkg], { timeout: 18e4, stdio: "pipe" });
|
|
1520
1504
|
}
|
|
@@ -1529,7 +1513,8 @@ async function ensureToolkitCli(toolkitSlug) {
|
|
|
1529
1513
|
}
|
|
1530
1514
|
} catch (err) {
|
|
1531
1515
|
const msg = err.message.slice(0, 200);
|
|
1532
|
-
|
|
1516
|
+
log(`[toolkit-install] ${toolkitSlug}: installer=${resolvedInstaller} failed \u2014 ${msg} (retrying in ${TOOLKIT_INSTALL_RETRY_MS / 6e4}m)`);
|
|
1517
|
+
toolkitCliRetryAfter.set(toolkitSlug, Date.now() + TOOLKIT_INSTALL_RETRY_MS);
|
|
1533
1518
|
return;
|
|
1534
1519
|
}
|
|
1535
1520
|
if (brewBinDir && !process.env.PATH?.split(":").includes(brewBinDir)) {
|
|
@@ -1540,15 +1525,15 @@ async function ensureToolkitCli(toolkitSlug) {
|
|
|
1540
1525
|
log(`[toolkit-install] ${toolkitSlug}: installed \u2014 ${binary} now on PATH`);
|
|
1541
1526
|
toolkitCliEnsured.add(toolkitSlug);
|
|
1542
1527
|
toolkitCliRetryAfter.delete(toolkitSlug);
|
|
1543
|
-
toolkitCliFailureCount.delete(toolkitSlug);
|
|
1544
1528
|
} catch {
|
|
1545
|
-
|
|
1529
|
+
log(`[toolkit-install] ${toolkitSlug}: installer=${resolvedInstaller} completed but ${binary} still not on PATH (retrying in ${TOOLKIT_INSTALL_RETRY_MS / 6e4}m)`);
|
|
1530
|
+
toolkitCliRetryAfter.set(toolkitSlug, Date.now() + TOOLKIT_INSTALL_RETRY_MS);
|
|
1546
1531
|
}
|
|
1547
1532
|
}
|
|
1548
1533
|
function runAsync(cmd, args, opts) {
|
|
1549
1534
|
return new Promise((resolve, reject) => {
|
|
1550
1535
|
import("child_process").then(({ spawn }) => {
|
|
1551
|
-
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"]
|
|
1536
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1552
1537
|
let stdout = "";
|
|
1553
1538
|
let stderr = "";
|
|
1554
1539
|
let settled = false;
|
|
@@ -1599,7 +1584,7 @@ async function ensureFrameworkBinary(frameworkId) {
|
|
|
1599
1584
|
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
1600
1585
|
const runBrew = (args, opts) => {
|
|
1601
1586
|
if (isRoot) {
|
|
1602
|
-
return runAsync("sudo", ["-u", "ec2-user", "-H", brewPath, ...args],
|
|
1587
|
+
return runAsync("sudo", ["-u", "ec2-user", "-H", brewPath, ...args], opts);
|
|
1603
1588
|
}
|
|
1604
1589
|
return runAsync(brewPath, args, opts);
|
|
1605
1590
|
};
|
|
@@ -1827,15 +1812,19 @@ function log(msg) {
|
|
|
1827
1812
|
const safeMsg = redactForDiskLog(msg);
|
|
1828
1813
|
process.stderr.write(`[manager-worker ${ts}] ${safeMsg}
|
|
1829
1814
|
`);
|
|
1830
|
-
|
|
1831
|
-
|
|
1815
|
+
try {
|
|
1816
|
+
if (!managerLogPath) {
|
|
1832
1817
|
managerLogPath = join3(homedir3(), ".augmented", "manager.log");
|
|
1833
1818
|
mkdirSync2(dirname(managerLogPath), { recursive: true });
|
|
1834
|
-
if (existsSync2(managerLogPath)) {
|
|
1819
|
+
if (!existsSync2(managerLogPath)) {
|
|
1820
|
+
appendFileSync(managerLogPath, "", { mode: 384 });
|
|
1821
|
+
} else {
|
|
1835
1822
|
chmodSync(managerLogPath, 384);
|
|
1836
1823
|
}
|
|
1837
|
-
} catch {
|
|
1838
1824
|
}
|
|
1825
|
+
appendFileSync(managerLogPath, `[manager-worker ${ts}] ${safeMsg}
|
|
1826
|
+
`);
|
|
1827
|
+
} catch {
|
|
1839
1828
|
}
|
|
1840
1829
|
}
|
|
1841
1830
|
function sha256(content) {
|
|
@@ -2217,7 +2206,7 @@ async function pollCycle() {
|
|
|
2217
2206
|
}
|
|
2218
2207
|
try {
|
|
2219
2208
|
const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
|
|
2220
|
-
const { collectDiagnostics } = await import("../persistent-session-
|
|
2209
|
+
const { collectDiagnostics } = await import("../persistent-session-HUQXZSHP.js");
|
|
2221
2210
|
const diagCodeNames = [...persistentSessionAgents];
|
|
2222
2211
|
const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
|
|
2223
2212
|
let tailscaleHostname;
|
|
@@ -2253,7 +2242,6 @@ async function pollCycle() {
|
|
|
2253
2242
|
await api.post("/host/heartbeat", {
|
|
2254
2243
|
host_id: hostId,
|
|
2255
2244
|
framework_version: cachedFrameworkVersion ?? void 0,
|
|
2256
|
-
agt_version: agtCliVersion,
|
|
2257
2245
|
host_security: detectHostSecurity() ?? void 0,
|
|
2258
2246
|
agent_runtime_authenticated: agentRuntimeAuthenticated,
|
|
2259
2247
|
agent_diagnostics: agentDiagnostics,
|
|
@@ -5256,32 +5244,19 @@ async function reportDeliveryStatus(agentId, taskId, payload) {
|
|
|
5256
5244
|
log(`[delivery] Failed to report delivery status for ${agentId}/${taskId}: ${err.message}`);
|
|
5257
5245
|
}
|
|
5258
5246
|
}
|
|
5259
|
-
var spawnedPairIds = /* @__PURE__ */ new Set();
|
|
5260
5247
|
async function processClaudePairSessions(agents) {
|
|
5261
|
-
if (agents.length === 0
|
|
5248
|
+
if (agents.length === 0) return;
|
|
5262
5249
|
const agentIds = agents.map((a) => a.agentId);
|
|
5263
5250
|
const codeNameByAgentId = new Map(agents.map((a) => [a.agentId, a.codeName]));
|
|
5264
|
-
const pendingResp = await api.post("/host/claude-pair/pending", {
|
|
5265
|
-
|
|
5266
|
-
spawned_pair_ids: Array.from(spawnedPairIds)
|
|
5267
|
-
});
|
|
5251
|
+
const pendingResp = await api.post("/host/claude-pair/pending", { agent_ids: agentIds });
|
|
5252
|
+
if (!pendingResp.pending || pendingResp.pending.length === 0) return;
|
|
5268
5253
|
const {
|
|
5269
5254
|
startClaudePair,
|
|
5270
5255
|
submitClaudePairCode,
|
|
5271
5256
|
spawnPairSession,
|
|
5272
5257
|
killPairSession,
|
|
5273
5258
|
pairTmuxSession
|
|
5274
|
-
} = await import("../claude-pair-runtime-
|
|
5275
|
-
for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
|
|
5276
|
-
log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
|
|
5277
|
-
const killed = await killPairSession(pairTmuxSession(pairId));
|
|
5278
|
-
if (killed) {
|
|
5279
|
-
spawnedPairIds.delete(pairId);
|
|
5280
|
-
} else {
|
|
5281
|
-
log(`[claude-pair] kill-session failed for pair ${pairId.slice(0, 8)} \u2014 will retry on next poll`);
|
|
5282
|
-
}
|
|
5283
|
-
}
|
|
5284
|
-
if (!pendingResp.pending || pendingResp.pending.length === 0) return;
|
|
5259
|
+
} = await import("../claude-pair-runtime-H4YBW4YE.js");
|
|
5285
5260
|
const TERMINAL = /* @__PURE__ */ new Set([
|
|
5286
5261
|
"success",
|
|
5287
5262
|
"failure",
|
|
@@ -5291,13 +5266,7 @@ async function processClaudePairSessions(agents) {
|
|
|
5291
5266
|
async function reportAndCleanup(pairId, body) {
|
|
5292
5267
|
await api.post("/host/claude-pair/result", { pair_id: pairId, ...body });
|
|
5293
5268
|
if (typeof body.status === "string" && TERMINAL.has(body.status)) {
|
|
5294
|
-
|
|
5295
|
-
await new Promise((r) => setTimeout(r, 3e3));
|
|
5296
|
-
}
|
|
5297
|
-
const killed = await killPairSession(pairTmuxSession(pairId));
|
|
5298
|
-
if (killed) {
|
|
5299
|
-
spawnedPairIds.delete(pairId);
|
|
5300
|
-
}
|
|
5269
|
+
await killPairSession(pairTmuxSession(pairId));
|
|
5301
5270
|
}
|
|
5302
5271
|
}
|
|
5303
5272
|
for (const session of pendingResp.pending) {
|
|
@@ -5315,7 +5284,6 @@ async function processClaudePairSessions(agents) {
|
|
|
5315
5284
|
});
|
|
5316
5285
|
continue;
|
|
5317
5286
|
}
|
|
5318
|
-
spawnedPairIds.add(session.pair_id);
|
|
5319
5287
|
log(`[claude-pair] dispatching /login (pair ${session.pair_id.slice(0, 8)})`);
|
|
5320
5288
|
const result = await startClaudePair({ session: pairSession });
|
|
5321
5289
|
if (result.kind === "url") {
|