@integrity-labs/agt-cli 0.15.37 → 0.16.0

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.
@@ -180,7 +180,7 @@ async function startClaudePair(opts) {
180
180
  return {
181
181
  kind: "error",
182
182
  error: {
183
- kind: "unknown",
183
+ kind: "oauth-retry-stuck",
184
184
  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."
185
185
  }
186
186
  };
@@ -318,4 +318,4 @@ export {
318
318
  startClaudePair,
319
319
  submitClaudePairCode
320
320
  };
321
- //# sourceMappingURL=claude-pair-runtime-GLO2D7WP.js.map
321
+ //# sourceMappingURL=claude-pair-runtime-GS6AOYHS.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 // -x 240 -y 50 — wide enough that Claude Code's OAuth URL (~350\n // chars after PKCE expansion) doesn't soft-wrap. The dewrap pass in\n // extractOAuthUrl handles any leftover wrapping, but giving the URL\n // a single line in the first place is more robust against future\n // claude UI changes.\n await execFileAsync('tmux', [\n 'new-session',\n '-d',\n '-x',\n '240',\n '-y',\n '50',\n '-s',\n session,\n claudeBin,\n ]);\n } catch (err) {\n return { ok: false, error: classifyTmuxError(err) };\n }\n\n // `tmux new-session` returns 0 even if the command exited immediately\n // (binary missing, claude crashed, etc.) — the session is created and\n // then torn down. Without this verification we'd propagate a generic\n // `session_missing` to the operator on the next has-session call,\n // hiding the real failure. Sleep briefly to give claude a beat to\n // crash-or-stay-alive, then check.\n await sleep(500);\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n } catch {\n return {\n ok: false,\n error: {\n kind: 'unknown',\n message: `claude exited immediately after launch (binary at ${claudeBin}). Run \\`${claudeBin}\\` manually on the host to see why — likely missing TTY, missing HOME, or a startup error.`,\n },\n };\n }\n return { ok: true };\n}\n\n/**\n * Returns true if the session is gone afterwards (either kill succeeded\n * or it was already missing). Returns false only if `kill-session`\n * failed for a non-missing-session reason — caller should treat this\n * as \"still tracked, will retry next poll\".\n */\nexport async function killPairSession(session: string): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['kill-session', '-t', session]);\n return true;\n } catch (err) {\n // \"can't find session\" is success-equivalent — the desired end state\n // is \"this session does not exist\" and that's already true.\n if (classifyTmuxError(err).kind === 'no-session') return true;\n return false;\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 // VIEWPORT ONLY (no scrollback). With scrollback included, the\n // theme picker / login picker text lingers in the buffer after\n // claude advances to the URL-paste prompt. The match would then\n // dispatch Enter on the empty paste field — submitting an empty\n // code and triggering \"OAuth error: Invalid code\". Visible-only\n // capture ensures we only react to what's actually on screen.\n pane = await capturePane(session, { scrollback: 0 });\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 // Stuck on \"OAuth error: Invalid code. Press Enter to retry\" — usually\n // because claude has cached partial state from a previous failed login\n // attempt on this host. Pressing Enter would just resubmit the bad\n // code and loop forever, so bail with an actionable error.\n //\n // Capture WITH scrollback for this check: claude's TUI sometimes\n // re-renders the splash/banner over the OAuth error so the viewport\n // alone misses it (we'd then time out instead of giving the operator\n // an actionable error). OAuth-retry is a sticky state — claude waits\n // for input — so checking recent scrollback is safe; a false positive\n // from stale scrollback would only fire if a previous attempt actually\n // failed, in which case bailing is the right call.\n let scrollPane = pane;\n try {\n scrollPane = await capturePane(session, { scrollback: -50 });\n } catch { /* fall back to viewport-only check */ }\n const hasOAuthInvalidCode = /OAuth error[\\s\\S]*Invalid code/i.test(scrollPane);\n const hasOAuthRetryPrompt = /OAuth error/i.test(scrollPane) && /Press Enter to retry/i.test(scrollPane);\n if (hasOAuthInvalidCode || hasOAuthRetryPrompt) {\n return {\n kind: 'error',\n error: {\n kind: 'oauth-retry-stuck',\n message:\n '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.',\n },\n };\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 // Send the code as LITERAL text (-l) so tmux doesn't try to interpret\n // any chars as key tokens. Then a 250ms breather so claude's\n // ink/React render loop finishes processing the paste before Enter\n // lands. Without that wait the Enter often gets swallowed mid-render\n // and the code stays in the input box unsubmitted.\n try {\n await execFileAsync('tmux', ['send-keys', '-t', session, '-l', opts.code.trim()]);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n await sleep(250);\n try {\n // Use Enter (semantic) plus a fallback C-m on the next iteration if\n // claude still hasn't moved.\n await sendKeys(session, 'Enter');\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n const deadline = Date.now() + timeoutMs;\n let enterRetried = false;\n let lastPane = '';\n while (Date.now() < deadline) {\n await sleep(pollIntervalMs);\n try {\n lastPane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n const outcome: AuthOutcome = detectAuthOutcome(lastPane);\n if (outcome.kind === 'success') return { kind: 'success', rawMatch: outcome.rawMatch };\n if (outcome.kind === 'failure') return { kind: 'failure', rawMatch: outcome.rawMatch };\n\n // After ~5s with no outcome, retry Enter once — covers the case\n // where the first Enter landed mid-render and claude swallowed it.\n // The \"Paste code here\" prompt is still on screen if submission\n // didn't take; if it advanced, we'd already have an outcome above.\n if (!enterRetried && Date.now() - (deadline - timeoutMs) > 5_000 && /Paste code here/i.test(lastPane)) {\n enterRetried = true;\n try { await sendKeys(session, 'C-m'); } catch { /* keep polling */ }\n }\n }\n // Include pane snippet in the error path so operators can see what\n // claude was actually showing — outcome detection is brittle and the\n // session is killed immediately after this returns, so this is our\n // only chance to capture state for debugging regex updates.\n return {\n kind: 'error',\n error: {\n kind: 'unknown',\n message: `submit timed out after ${timeoutMs}ms — outcome regex didn't match. Last pane: ${lastPane.slice(-600)}`,\n },\n };\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 *\n * Note: Claude Code currently emits URLs at `claude.com/cai/oauth/...`\n * (the consumer-facing domain), but `claude.ai` and the console hosts\n * have appeared historically and are kept here for tolerance.\n */\nconst OAUTH_URL_RE =\n /https:\\/\\/(?:claude\\.com|claude\\.ai|platform\\.claude\\.com|console\\.anthropic\\.com|auth\\.anthropic\\.com)\\/[^\\s)\\]]*/;\n\n/**\n * Strip ANSI + reassemble URLs that were soft-wrapped across terminal\n * lines. tmux capture-pane emits hard newlines for wrapped lines, and\n * Claude Code's OAuth URL routinely runs >300 chars — much wider than\n * the manager's default tmux window. Iteratively strip newlines that\n * fall inside what looks like a URL until convergence (a single URL\n * can wrap 5+ times).\n *\n * Shared between extractOAuthUrl and isUrlPromptReady so the readiness\n * check sees the same string the extractor would.\n */\nfunction dewrapPane(rawPane: string): string {\n const stripped = stripAnsi(rawPane);\n let dewrapped = stripped;\n let prev = '';\n while (prev !== dewrapped) {\n prev = dewrapped;\n dewrapped = dewrapped.replace(/(https?:\\/\\/\\S+?)\\n(?=\\S)/, (_m, head: string) => head);\n }\n return dewrapped;\n}\n\nexport function extractOAuthUrl(rawPane: string): string | null {\n const match = OAUTH_URL_RE.exec(dewrapPane(rawPane));\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 dewrapped = dewrapPane(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. Use the\n // dewrapped pane so a wrapped URL still matches OAUTH_URL_RE.\n return OAUTH_URL_RE.test(dewrapped) && URL_PROMPT_RE.test(dewrapped);\n}\n\n// ---------------------------------------------------------------------------\n// Outcome detection after submitting the code\n// ---------------------------------------------------------------------------\n\n// Claude Code's success/failure copy has drifted across versions\n// (\"Logged in\" → \"Login successful\" → \"Signed in as ...\" → etc.).\n// Keep the alternations broad-but-specific — phrases that only appear\n// after a real auth roundtrip, never in welcome / tutorial text.\n// \"Welcome back\" was tried and rejected: it shows up in onboarding\n// help blurbs and produced false positives.\nconst SUCCESS_RE =\n /(?:Logged in|Login successful|Successfully (?:logged in|authenticated|signed in)|Authentication successful|Sign-?in (?:complete|successful)|You(?:'|’)?re signed in|Signed in as)/i;\nconst FAILURE_RE =\n /(?:Invalid (?:code|authorization code)|OAuth error|Authentication failed|Error (?:logging in|during authentication)|Login failed|Sign-?in failed|Failed to (?:authenticate|sign in|log in))/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 /** Claude is sitting on the \"OAuth error: Invalid code. Press Enter to\n * retry.\" prompt from a previous failed login — pressing Enter would\n * resubmit the bad code, so the runtime bails with operator-actionable\n * guidance instead. Distinct from `unknown` so telemetry / UI can\n * surface a specific message rather than a generic 500. */\n | { kind: 'oauth-retry-stuck'; message: string }\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-XB7IGF3K.js";
25
+ } from "../chunk-LU6L2J32.js";
26
26
  import {
27
27
  findTaskByTemplate,
28
28
  getProjectDir,
@@ -53,7 +53,7 @@ import { join as join3, dirname } from "path";
53
53
  import { homedir as homedir3 } from "os";
54
54
  import { fileURLToPath } from "url";
55
55
 
56
- // src/lib/plugin-context-render.ts
56
+ // src/lib/integration-context-render.ts
57
57
  var PLUGIN_CONTEXT_PLACEHOLDER_RE = /\{\{\s*context\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
58
58
  var TEAM_OVERRIDES_HEADER = "## Team Overrides\n\n> **The following overrides anything you've read above.** If any rule here conflicts with earlier instructions in this skill, follow what is written here. These are user-supplied directives that take precedence over the plugin's default guidance.\n";
59
59
  function formatContextValue(value) {
@@ -66,7 +66,7 @@ function formatContextValue(value) {
66
66
  return "";
67
67
  }
68
68
  }
69
- function renderPluginSkillContent(raw, values, overrides, warn = () => {
69
+ function renderIntegrationSkillContent(raw, values, overrides, warn = () => {
70
70
  }) {
71
71
  const substituted = raw.replace(PLUGIN_CONTEXT_PLACEHOLDER_RE, (_match, fieldName) => {
72
72
  if (!(fieldName in values)) {
@@ -85,7 +85,7 @@ ${trimmedOverrides}
85
85
  `;
86
86
  }
87
87
 
88
- // src/lib/plugin-skill-layout.ts
88
+ // src/lib/integration-skill-layout.ts
89
89
  function extractDescription(content) {
90
90
  const match = content.match(/^---\s*([\s\S]*?)---/);
91
91
  if (!match) return null;
@@ -98,20 +98,20 @@ function extractDescription(content) {
98
98
  }
99
99
  return descMatch[2]?.trim() ?? null;
100
100
  }
101
- function sanitizeScopeSlug(skillId, pluginSlug) {
101
+ function sanitizeScopeSlug(skillId, integrationSlug) {
102
102
  const normalized = skillId.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-");
103
- const prefix = pluginSlug.replace(/-/g, "");
103
+ const prefix = integrationSlug.replace(/-/g, "");
104
104
  const stripped = normalized.startsWith(`${prefix}-`) ? normalized.slice(prefix.length + 1) : normalized;
105
105
  return stripped.replace(/^-|-$/g, "") || normalized || "scope";
106
106
  }
107
- function buildPluginBundle(skills) {
107
+ function buildIntegrationBundle(skills) {
108
108
  if (skills.length === 0) {
109
- throw new Error("buildPluginBundle: empty skills list");
109
+ throw new Error("buildIntegrationBundle: empty skills list");
110
110
  }
111
- const pluginSlug = skills[0].plugin_slug;
111
+ const integrationSlug = skills[0].plugin_slug;
112
112
  const ordered = [...skills].sort((a, b) => a.skill_id.localeCompare(b.skill_id));
113
113
  const entries = ordered.map((s) => {
114
- const scopeSlug = sanitizeScopeSlug(s.skill_id, pluginSlug);
114
+ const scopeSlug = sanitizeScopeSlug(s.skill_id, integrationSlug);
115
115
  const description = extractDescription(s.content);
116
116
  return {
117
117
  skillId: s.skill_id,
@@ -132,11 +132,11 @@ function buildPluginBundle(skills) {
132
132
  if (group.length > 1) {
133
133
  const conflicts = group.map((e) => `${e.skillId} (${e.skillName})`).join(", ");
134
134
  throw new Error(
135
- `buildPluginBundle: duplicate scope path '${scopePath}' for plugin '${pluginSlug}' \u2014 conflicting skills: ${conflicts}`
135
+ `buildIntegrationBundle: duplicate scope path '${scopePath}' for integration '${integrationSlug}' \u2014 conflicting skills: ${conflicts}`
136
136
  );
137
137
  }
138
138
  }
139
- const pluginName = slugToTitle(pluginSlug);
139
+ const integrationName = slugToTitle(integrationSlug);
140
140
  const umbrellaDescription = entries.map((e) => e.description ?? `Use for ${e.skillName}.`).join(" ");
141
141
  const scopeList = entries.map((e) => {
142
142
  const label = e.skillName || slugToTitle(e.scopeSlug);
@@ -145,13 +145,13 @@ function buildPluginBundle(skills) {
145
145
  }).join("\n");
146
146
  const umbrella = [
147
147
  "---",
148
- `name: "${pluginName}"`,
148
+ `name: "${integrationName}"`,
149
149
  `description: "${escapeYamlDouble(umbrellaDescription)}"`,
150
150
  "---",
151
151
  "",
152
- `# ${pluginName}`,
152
+ `# ${integrationName}`,
153
153
  "",
154
- `This skill bundles ${entries.length === 1 ? "one scope" : `${entries.length} scopes`} for the ${pluginSlug} plugin. Each scope has a dedicated reference under \`scopes/\` that you should load on demand when the user's intent maps to it.`,
154
+ `This skill bundles ${entries.length === 1 ? "one scope" : `${entries.length} scopes`} for the ${integrationSlug} integration. Each scope has a dedicated reference under \`scopes/\` that you should load on demand when the user's intent maps to it.`,
155
155
  "",
156
156
  "## Scopes",
157
157
  "",
@@ -170,7 +170,7 @@ function buildPluginBundle(skills) {
170
170
  }))
171
171
  ];
172
172
  return {
173
- pluginSlug,
173
+ integrationSlug,
174
174
  files,
175
175
  scopePaths: entries.map((e) => e.scopePath)
176
176
  };
@@ -181,7 +181,7 @@ function slugToTitle(slug) {
181
181
  function escapeYamlDouble(value) {
182
182
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
183
183
  }
184
- function groupSkillsByPlugin(skills) {
184
+ function groupSkillsByIntegration(skills) {
185
185
  const map = /* @__PURE__ */ new Map();
186
186
  for (const s of skills) {
187
187
  const bucket = map.get(s.plugin_slug);
@@ -1033,7 +1033,7 @@ var driftChannel = null;
1033
1033
  var assignChannel = null;
1034
1034
  var configChannel = null;
1035
1035
  var kanbanChannel = null;
1036
- var pluginContextChannel = null;
1036
+ var integrationContextChannel = null;
1037
1037
  var connected = false;
1038
1038
  var tearingDown = false;
1039
1039
  function ensureClient(config2) {
@@ -1078,7 +1078,7 @@ function startRealtimeChat(config2) {
1078
1078
  assignChannel = null;
1079
1079
  configChannel = null;
1080
1080
  kanbanChannel = null;
1081
- pluginContextChannel = null;
1081
+ integrationContextChannel = null;
1082
1082
  if (client) {
1083
1083
  try {
1084
1084
  client.removeAllChannels();
@@ -1251,12 +1251,12 @@ function startRealtimeKanban(config2) {
1251
1251
  log2(`[realtime] Subscribing to agent_kanban_items for ${agentIds.length} agent(s)`);
1252
1252
  void formatActorId;
1253
1253
  }
1254
- function startRealtimePluginContext(config2) {
1254
+ function startRealtimeIntegrationContext(config2) {
1255
1255
  const { agentIds, onContextChange, log: log2 } = config2;
1256
1256
  if (agentIds.length === 0) return;
1257
1257
  const sb = ensureClient(config2);
1258
1258
  const filterStr = agentIds.length === 1 ? `agent_id=eq.${agentIds[0]}` : `agent_id=in.(${agentIds.join(",")})`;
1259
- pluginContextChannel = sb.channel("plugin-context-realtime").on("postgres_changes", {
1259
+ integrationContextChannel = sb.channel("plugin-context-realtime").on("postgres_changes", {
1260
1260
  event: "INSERT",
1261
1261
  schema: "public",
1262
1262
  table: "plugin_context",
@@ -1276,9 +1276,9 @@ function startRealtimePluginContext(config2) {
1276
1276
  onContextChange(row);
1277
1277
  }).subscribe((status) => {
1278
1278
  if (status === "SUBSCRIBED") {
1279
- log2("[realtime] Plugin context channel connected");
1279
+ log2("[realtime] Integration context channel connected");
1280
1280
  } else if (status === "CLOSED" || status === "CHANNEL_ERROR") {
1281
- log2(`[realtime] Plugin context channel: ${status}`);
1281
+ log2(`[realtime] Integration context channel: ${status}`);
1282
1282
  }
1283
1283
  });
1284
1284
  log2(`[realtime] Subscribing to plugin_context for ${agentIds.length} agent(s)`);
@@ -1325,12 +1325,12 @@ function stopRealtimeChat() {
1325
1325
  }
1326
1326
  kanbanChannel = null;
1327
1327
  }
1328
- if (pluginContextChannel) {
1328
+ if (integrationContextChannel) {
1329
1329
  try {
1330
- pluginContextChannel.unsubscribe();
1330
+ integrationContextChannel.unsubscribe();
1331
1331
  } catch {
1332
1332
  }
1333
- pluginContextChannel = null;
1333
+ integrationContextChannel = null;
1334
1334
  }
1335
1335
  if (client) {
1336
1336
  try {
@@ -1435,15 +1435,21 @@ 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.37" : "dev";
1438
+ var agtCliVersion = true ? "0.16.0" : "dev";
1439
1439
  function resolveBrewPath(execFileSync2) {
1440
1440
  try {
1441
1441
  const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
1442
1442
  if (out) return out;
1443
1443
  } catch {
1444
1444
  }
1445
- const fallback = "/home/linuxbrew/.linuxbrew/bin/brew";
1446
- if (existsSync2(fallback)) return fallback;
1445
+ const fallbacks = [
1446
+ "/home/linuxbrew/.linuxbrew/bin/brew",
1447
+ "/opt/homebrew/bin/brew",
1448
+ "/usr/local/bin/brew"
1449
+ ];
1450
+ for (const path of fallbacks) {
1451
+ if (existsSync2(path)) return path;
1452
+ }
1447
1453
  return null;
1448
1454
  }
1449
1455
  var toolkitCliEnsured = /* @__PURE__ */ new Set();
@@ -1656,44 +1662,51 @@ var UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
1656
1662
  var selfUpdateUpToDateLogged = false;
1657
1663
  var restartAfterUpgrade = false;
1658
1664
  var pendingUpgradeVersion = null;
1665
+ var selfUpdateInFlight = false;
1659
1666
  async function checkAndUpdateCli() {
1660
- const cliPath = process.argv[1] ?? "";
1661
- const isDevMode = cliPath.includes("/src/") || cliPath.includes("tsx");
1662
- if (isDevMode) return;
1663
- let resolvedPath = cliPath;
1664
- try {
1665
- const { realpathSync } = await import("fs");
1666
- resolvedPath = realpathSync(cliPath);
1667
- } catch {
1668
- }
1669
- const isBrewFormula = /\/Cellar\/[^/]+\//.test(resolvedPath);
1670
- const isNpmGlobal = !isBrewFormula && resolvedPath.includes("node_modules");
1671
- if (!isBrewFormula && !isNpmGlobal) return;
1672
- const { readFileSync: readF, writeFileSync: writeF } = await import("fs");
1673
- const markerPath = join3(homedir3(), ".augmented", ".last-update-check");
1674
- try {
1675
- const lastCheck = parseInt(readF(markerPath, "utf-8").trim(), 10);
1676
- if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return;
1677
- } catch {
1678
- }
1679
- if (isBrewFormula) {
1680
- await checkAndUpdateCliViaBrew();
1681
- } else {
1682
- await checkAndUpdateCliViaNpm();
1683
- }
1667
+ if (selfUpdateInFlight) return;
1668
+ selfUpdateInFlight = true;
1684
1669
  try {
1685
- writeF(markerPath, String(Date.now()));
1686
- } catch {
1670
+ const cliPath = process.argv[1] ?? "";
1671
+ const isDevMode = cliPath.includes("/src/") || cliPath.includes("tsx");
1672
+ if (isDevMode) return;
1673
+ let resolvedPath = cliPath;
1674
+ try {
1675
+ const { realpathSync } = await import("fs");
1676
+ resolvedPath = realpathSync(cliPath);
1677
+ } catch {
1678
+ }
1679
+ const isBrewFormula = /\/Cellar\/[^/]+\//.test(resolvedPath);
1680
+ const isNpmGlobal = !isBrewFormula && resolvedPath.includes("node_modules");
1681
+ if (!isBrewFormula && !isNpmGlobal) return;
1682
+ const { readFileSync: readF, writeFileSync: writeF } = await import("fs");
1683
+ const markerPath = join3(homedir3(), ".augmented", ".last-update-check");
1684
+ try {
1685
+ const lastCheck = parseInt(readF(markerPath, "utf-8").trim(), 10);
1686
+ if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return;
1687
+ } catch {
1688
+ }
1689
+ try {
1690
+ writeF(markerPath, String(Date.now()));
1691
+ } catch {
1692
+ }
1693
+ if (isBrewFormula) {
1694
+ await checkAndUpdateCliViaBrew();
1695
+ } else {
1696
+ await checkAndUpdateCliViaNpm();
1697
+ }
1698
+ try {
1699
+ writeF(markerPath, String(Date.now()));
1700
+ } catch {
1701
+ }
1702
+ } finally {
1703
+ selfUpdateInFlight = false;
1687
1704
  }
1688
1705
  }
1689
1706
  async function checkAndUpdateCliViaBrew() {
1690
1707
  const { execFileSync: execFileSync2 } = await import("child_process");
1691
- let brewPath;
1692
- try {
1693
- brewPath = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
1694
- } catch {
1695
- return;
1696
- }
1708
+ const brewPath = resolveBrewPath(execFileSync2);
1709
+ if (!brewPath) return;
1697
1710
  try {
1698
1711
  execFileSync2(brewPath, ["update", "--quiet"], { timeout: 6e4, stdio: "pipe" });
1699
1712
  } catch (err) {
@@ -2442,7 +2455,7 @@ async function pollCycle() {
2442
2455
  ensureRealtimeAssignStarted(agentStates);
2443
2456
  ensureRealtimeConfigStarted(agentStates);
2444
2457
  ensureRealtimeKanbanStarted(agentStates);
2445
- ensureRealtimePluginContextStarted(agentStates);
2458
+ ensureRealtimeIntegrationContextStarted(agentStates);
2446
2459
  try {
2447
2460
  const spawnData = await api.post("/host/kanban/recurring/spawn");
2448
2461
  if (spawnData.spawned > 0) {
@@ -3123,42 +3136,46 @@ async function processAgent(agent, agentStates) {
3123
3136
  }
3124
3137
  }
3125
3138
  if (frameworkAdapter.installSkillFiles) {
3126
- const currentPluginSkillIds = /* @__PURE__ */ new Set();
3127
- const installedPluginSkills = [];
3139
+ const currentIntegrationSkillIds = /* @__PURE__ */ new Set();
3140
+ const installedIntegrationSkills = [];
3128
3141
  const { createHash: createHash2 } = await import("crypto");
3142
+ const refreshAny = refreshData;
3143
+ const contexts = refreshAny.integration_contexts ?? refreshAny.plugin_contexts ?? [];
3129
3144
  const contextBySlug = /* @__PURE__ */ new Map();
3130
- for (const ctx of refreshData.plugin_contexts ?? []) {
3131
- contextBySlug.set(ctx.plugin_slug, { values: ctx.values ?? {}, overrides: (ctx.overrides ?? "").trim() });
3145
+ for (const ctx of contexts) {
3146
+ const slug = ctx.integration_slug ?? ctx.plugin_slug;
3147
+ if (!slug) continue;
3148
+ contextBySlug.set(slug, { values: ctx.values ?? {}, overrides: (ctx.overrides ?? "").trim() });
3132
3149
  }
3133
- const pluginGroups = groupSkillsByPlugin(
3134
- refreshData.plugin_skills ?? []
3150
+ const integrationGroups = groupSkillsByIntegration(
3151
+ refreshAny.integration_skills ?? refreshAny.plugin_skills ?? []
3135
3152
  );
3136
- for (const [pluginSlug, scopes] of pluginGroups) {
3153
+ for (const [integrationSlug, scopes] of integrationGroups) {
3137
3154
  try {
3138
- const pluginSkillId = `integration-${pluginSlug}`.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3139
- currentPluginSkillIds.add(pluginSkillId);
3140
- const ctx = contextBySlug.get(pluginSlug);
3155
+ const integrationSkillId = `integration-${integrationSlug}`.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3156
+ currentIntegrationSkillIds.add(integrationSkillId);
3157
+ const ctx = contextBySlug.get(integrationSlug);
3141
3158
  const renderedScopes = scopes.map((s) => ({
3142
3159
  plugin_slug: s.plugin_slug,
3143
3160
  skill_id: s.skill_id,
3144
3161
  skill_name: s.skill_name,
3145
- content: renderPluginSkillContent(
3162
+ content: renderIntegrationSkillContent(
3146
3163
  s.content,
3147
3164
  ctx?.values ?? {},
3148
3165
  ctx?.overrides ?? "",
3149
3166
  (warning) => log(`[plugin-context] ${s.plugin_slug}/${s.skill_id}: ${warning}`)
3150
3167
  )
3151
3168
  }));
3152
- const bundle = buildPluginBundle(renderedScopes);
3169
+ const bundle = buildIntegrationBundle(renderedScopes);
3153
3170
  const contentHash = createHash2("sha256").update(bundleFingerprint(bundle.files)).digest("hex").slice(0, 12);
3154
- const hashKey = `plugin-skill:${agent.agent_id}:${pluginSkillId}`;
3171
+ const hashKey = `plugin-skill:${agent.agent_id}:${integrationSkillId}`;
3155
3172
  if (knownSkillHashes.get(hashKey) === contentHash) continue;
3156
- frameworkAdapter.installSkillFiles(agent.code_name, pluginSkillId, bundle.files);
3173
+ frameworkAdapter.installSkillFiles(agent.code_name, integrationSkillId, bundle.files);
3157
3174
  knownSkillHashes.set(hashKey, contentHash);
3158
- for (const s of scopes) installedPluginSkills.push(s.skill_name);
3159
- log(`Installed plugin '${pluginSkillId}' for '${agent.code_name}' (${scopes.length} scope(s))`);
3175
+ for (const s of scopes) installedIntegrationSkills.push(s.skill_name);
3176
+ log(`Installed integration skill bundle '${integrationSkillId}' for '${agent.code_name}' (${scopes.length} scope(s))`);
3160
3177
  } catch (err) {
3161
- log(`Plugin install failed for '${agent.code_name}' / '${pluginSlug}': ${err.message}`);
3178
+ log(`Integration skill install failed for '${agent.code_name}' / '${integrationSlug}': ${err.message}`);
3162
3179
  }
3163
3180
  }
3164
3181
  try {
@@ -3198,12 +3215,12 @@ async function processAgent(agent, agentStates) {
3198
3215
  log(`Removed ${reason} '${entry}' for '${agent.code_name}' (framework=${frameworkId2})`);
3199
3216
  };
3200
3217
  for (const entry of discoveredEntries) {
3201
- if (!currentPluginSkillIds.has(entry)) {
3218
+ if (!currentIntegrationSkillIds.has(entry)) {
3202
3219
  removeSkillFolder(entry, "orphaned skill folder");
3203
3220
  }
3204
3221
  }
3205
3222
  } catch (err) {
3206
- log(`Plugin skill cleanup failed for '${agent.code_name}': ${err.message}`);
3223
+ log(`Integration skill cleanup failed for '${agent.code_name}': ${err.message}`);
3207
3224
  }
3208
3225
  try {
3209
3226
  const agentFwForIndex = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
@@ -3213,40 +3230,44 @@ async function processAgent(agent, agentStates) {
3213
3230
  } catch (err) {
3214
3231
  log(`Skills index refresh failed for '${agent.code_name}': ${err.message}`);
3215
3232
  }
3216
- if (frameworkAdapter.executePluginHook && refreshData.plugin_install_hooks?.length) {
3217
- for (const hook of refreshData.plugin_install_hooks) {
3233
+ const installHooks = refreshAny.integration_install_hooks ?? refreshAny.plugin_install_hooks ?? [];
3234
+ if (frameworkAdapter.executePluginHook && installHooks.length) {
3235
+ for (const hook of installHooks) {
3236
+ const slug = hook.integration_slug ?? hook.plugin_slug;
3237
+ if (!slug) continue;
3218
3238
  try {
3219
3239
  const scriptHash = createHash2("sha256").update(hook.script).digest("hex").slice(0, 12);
3220
- const hookKey = `${agent.agent_id}:${frameworkAdapter.id}:plugin-hook:${hook.plugin_slug}:on_install`;
3240
+ const hookKey = `${agent.agent_id}:${frameworkAdapter.id}:plugin-hook:${slug}:on_install`;
3221
3241
  if (knownSkillHashes.get(hookKey) === scriptHash) continue;
3222
3242
  const result = await frameworkAdapter.executePluginHook({
3223
3243
  codeName: agent.code_name,
3224
- pluginSlug: hook.plugin_slug,
3244
+ pluginSlug: slug,
3225
3245
  hookName: "on_install",
3226
3246
  script: hook.script
3227
3247
  });
3228
3248
  if (result.exitCode === 0) {
3229
3249
  knownSkillHashes.set(hookKey, scriptHash);
3230
- log(`Plugin hook on_install '${hook.plugin_slug}' succeeded for '${agent.code_name}' (${result.durationMs}ms)`);
3250
+ log(`Integration hook on_install '${slug}' succeeded for '${agent.code_name}' (${result.durationMs}ms)`);
3231
3251
  } else if (result.timedOut) {
3232
- log(`Plugin hook on_install '${hook.plugin_slug}' TIMED OUT for '${agent.code_name}' after ${result.durationMs}ms`);
3252
+ log(`Integration hook on_install '${slug}' TIMED OUT for '${agent.code_name}' after ${result.durationMs}ms`);
3233
3253
  } else {
3234
3254
  const stderrHash = createHash2("sha256").update(result.stderr).digest("hex").slice(0, 12);
3235
3255
  const missingCmd = result.exitCode === 127 ? extractCommandNotFound(result.stderr) : null;
3236
3256
  const missingCmdHash = missingCmd ? createHash2("sha256").update(missingCmd).digest("hex").slice(0, 8) : null;
3237
3257
  log(
3238
- `Plugin hook on_install '${hook.plugin_slug}' exited ${result.exitCode} for '${agent.code_name}' ` + (missingCmdHash ? `[missing_command_hash=${missingCmdHash}] ` : "") + `[stderr_hash=${stderrHash} stderr_len=${result.stderr.length}]`
3258
+ `Integration hook on_install '${slug}' exited ${result.exitCode} for '${agent.code_name}' ` + (missingCmdHash ? `[missing_command_hash=${missingCmdHash}] ` : "") + `[stderr_hash=${stderrHash} stderr_len=${result.stderr.length}]`
3239
3259
  );
3240
3260
  }
3241
3261
  } catch (err) {
3242
- log(`Plugin hook on_install failed for '${agent.code_name}' / '${hook.plugin_slug}': ${err.message}`);
3262
+ log(`Integration hook on_install failed for '${agent.code_name}' / '${slug}': ${err.message}`);
3243
3263
  }
3244
3264
  }
3245
3265
  }
3246
3266
  const agentFwForToolkits = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
3247
- if (agentFwForToolkits === "claude-code" && refreshData.plugin_toolkits?.length) {
3267
+ const toolkitsList = refreshAny.integration_toolkits ?? refreshAny.plugin_toolkits ?? [];
3268
+ if (agentFwForToolkits === "claude-code" && toolkitsList.length) {
3248
3269
  const toolkitUnion = /* @__PURE__ */ new Set();
3249
- for (const { toolkits } of refreshData.plugin_toolkits) {
3270
+ for (const { toolkits } of toolkitsList) {
3250
3271
  for (const t of toolkits) toolkitUnion.add(t);
3251
3272
  }
3252
3273
  for (const toolkitSlug of toolkitUnion) {
@@ -3254,17 +3275,17 @@ async function processAgent(agent, agentStates) {
3254
3275
  }
3255
3276
  }
3256
3277
  const agentFw2 = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
3257
- if (agentFw2 === "claude-code" && installedPluginSkills.length > 0 && isSessionHealthy(agent.code_name)) {
3258
- const names = installedPluginSkills.join(", ");
3278
+ if (agentFw2 === "claude-code" && installedIntegrationSkills.length > 0 && isSessionHealthy(agent.code_name)) {
3279
+ const names = installedIntegrationSkills.join(", ");
3259
3280
  injectMessage(
3260
3281
  agent.code_name,
3261
3282
  "system",
3262
- `New plugin skills installed: ${names}. These are available immediately \u2014 Claude Code loads skills on demand from .claude/skills/.`,
3283
+ `New integration skills installed: ${names}. These are available immediately \u2014 Claude Code loads skills on demand from .claude/skills/.`,
3263
3284
  { task_name: "plugin-skill-update" },
3264
3285
  log
3265
3286
  ).catch(() => {
3266
3287
  });
3267
- log(`[hot-reload] Notified '${agent.code_name}' about new plugin skills: ${names}`);
3288
+ log(`[hot-reload] Notified '${agent.code_name}' about new integration skills: ${names}`);
3268
3289
  }
3269
3290
  }
3270
3291
  }
@@ -4155,7 +4176,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4155
4176
  var realtimeStarted = false;
4156
4177
  var realtimeDriftStarted = false;
4157
4178
  var realtimeKanbanStarted = false;
4158
- var realtimePluginContextStarted = false;
4179
+ var realtimeIntegrationContextStarted = false;
4159
4180
  var realtimeAssignStarted = false;
4160
4181
  var realtimeConfigStarted = false;
4161
4182
  var realtimeSubscribedAgentIds = /* @__PURE__ */ new Set();
@@ -4174,7 +4195,7 @@ function ensureRealtimeStarted(agentStates) {
4174
4195
  realtimeAssignStarted = false;
4175
4196
  realtimeConfigStarted = false;
4176
4197
  realtimeKanbanStarted = false;
4177
- realtimePluginContextStarted = false;
4198
+ realtimeIntegrationContextStarted = false;
4178
4199
  }
4179
4200
  const activeAgentIds = agentStates.filter((a) => a.status === "active").map((a) => a.agentId);
4180
4201
  if (activeAgentIds.length === 0) return;
@@ -4214,6 +4235,7 @@ function ensureRealtimeStarted(agentStates) {
4214
4235
  realtimeAssignStarted = false;
4215
4236
  realtimeConfigStarted = false;
4216
4237
  realtimeKanbanStarted = false;
4238
+ realtimeIntegrationContextStarted = false;
4217
4239
  }
4218
4240
  },
4219
4241
  log
@@ -4353,15 +4375,15 @@ function ensureRealtimeKanbanStarted(agentStates) {
4353
4375
  log(`[realtime] Kanban subscription failed: ${err.message}`);
4354
4376
  });
4355
4377
  }
4356
- function ensureRealtimePluginContextStarted(agentStates) {
4357
- if (realtimePluginContextStarted) return;
4378
+ function ensureRealtimeIntegrationContextStarted(agentStates) {
4379
+ if (realtimeIntegrationContextStarted) return;
4358
4380
  const activeAgentIds = agentStates.filter((a) => a.status === "active").map((a) => a.agentId);
4359
4381
  if (activeAgentIds.length === 0) return;
4360
4382
  const apiKey = process.env["AGT_API_KEY"];
4361
4383
  if (!apiKey) return;
4362
4384
  void exchangeApiKey(apiKey).then((exchange) => {
4363
4385
  if (!exchange.supabaseUrl || !exchange.supabaseAnonKey) return;
4364
- startRealtimePluginContext({
4386
+ startRealtimeIntegrationContext({
4365
4387
  supabaseUrl: exchange.supabaseUrl,
4366
4388
  supabaseAnonKey: exchange.supabaseAnonKey,
4367
4389
  token: exchange.token,
@@ -4375,14 +4397,14 @@ function ensureRealtimePluginContextStarted(agentStates) {
4375
4397
  }
4376
4398
  }
4377
4399
  }
4378
- triggerEarlyPoll(`plugin context changed for agent ${payload.agent_id}`);
4400
+ triggerEarlyPoll(`integration context changed for agent ${payload.agent_id}`);
4379
4401
  },
4380
4402
  log
4381
4403
  });
4382
- realtimePluginContextStarted = true;
4383
- log(`[realtime] Plugin context subscription started for ${activeAgentIds.length} agent(s)`);
4404
+ realtimeIntegrationContextStarted = true;
4405
+ log(`[realtime] Integration context subscription started for ${activeAgentIds.length} agent(s)`);
4384
4406
  }).catch((err) => {
4385
- log(`[realtime] Plugin context subscription failed: ${err.message}`);
4407
+ log(`[realtime] Integration context subscription failed: ${err.message}`);
4386
4408
  });
4387
4409
  }
4388
4410
  function triggerEarlyPoll(reason) {
@@ -5353,7 +5375,7 @@ async function processClaudePairSessions(agents) {
5353
5375
  spawnPairSession,
5354
5376
  killPairSession,
5355
5377
  pairTmuxSession
5356
- } = await import("../claude-pair-runtime-GLO2D7WP.js");
5378
+ } = await import("../claude-pair-runtime-GS6AOYHS.js");
5357
5379
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
5358
5380
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
5359
5381
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -5414,10 +5436,11 @@ async function processClaudePairSessions(agents) {
5414
5436
  });
5415
5437
  } else {
5416
5438
  const errKind = result.error.kind;
5439
+ const errMessage = "message" in result.error ? result.error.message : void 0;
5417
5440
  await reportAndCleanup(session.pair_id, {
5418
5441
  status: errKind === "no-session" ? "session_missing" : "failure",
5419
5442
  error_code: errKind,
5420
- error_message: errKind === "unknown" ? result.error.message : void 0
5443
+ error_message: errMessage
5421
5444
  });
5422
5445
  }
5423
5446
  } else if (session.status === "code_submitted" && session.code) {
@@ -5442,10 +5465,11 @@ async function processClaudePairSessions(agents) {
5442
5465
  });
5443
5466
  } else {
5444
5467
  const errKind = result.error.kind;
5468
+ const errMessage = "message" in result.error ? result.error.message : void 0;
5445
5469
  await reportAndCleanup(session.pair_id, {
5446
5470
  status: errKind === "no-session" ? "session_missing" : "failure",
5447
5471
  error_code: errKind,
5448
- error_message: errKind === "unknown" ? result.error.message : void 0
5472
+ error_message: errMessage
5449
5473
  });
5450
5474
  }
5451
5475
  }