@integrity-labs/agt-cli 0.15.10 → 0.15.11
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-EC6FSHOW.js → chunk-LYWDRDJZ.js} +41 -5
- package/dist/chunk-LYWDRDJZ.js.map +1 -0
- package/dist/claude-pair-runtime-WGIKIPJV.js +187 -0
- package/dist/claude-pair-runtime-WGIKIPJV.js.map +1 -0
- package/dist/lib/manager-worker.js +101 -38
- package/dist/lib/manager-worker.js.map +1 -1
- package/mcp/slack-channel.js +822 -0
- package/package.json +1 -1
- package/dist/chunk-EC6FSHOW.js.map +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// src/lib/claude-pair-runtime.ts
|
|
2
|
+
import { execFile } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
|
|
5
|
+
// src/lib/claude-pair-parser.ts
|
|
6
|
+
var ANSI_ESC = String.fromCharCode(27);
|
|
7
|
+
var ANSI_BEL = String.fromCharCode(7);
|
|
8
|
+
var CSI_RE = new RegExp(`${ANSI_ESC}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
9
|
+
var OSC_RE = new RegExp(
|
|
10
|
+
`${ANSI_ESC}\\][^${ANSI_BEL}${ANSI_ESC}]*(?:${ANSI_BEL}|${ANSI_ESC}\\\\)`,
|
|
11
|
+
"g"
|
|
12
|
+
);
|
|
13
|
+
var TWO_BYTE_RE = new RegExp(`${ANSI_ESC}[=>cM78]`, "g");
|
|
14
|
+
function stripAnsi(text) {
|
|
15
|
+
return text.replace(CSI_RE, "").replace(OSC_RE, "").replace(TWO_BYTE_RE, "");
|
|
16
|
+
}
|
|
17
|
+
var OAUTH_URL_RE = /https:\/\/(?:claude\.ai|console\.anthropic\.com|auth\.anthropic\.com)\/[^\s)\]]*/;
|
|
18
|
+
function extractOAuthUrl(rawPane) {
|
|
19
|
+
const stripped = stripAnsi(rawPane);
|
|
20
|
+
const match = OAUTH_URL_RE.exec(stripped);
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
return match[0].replace(/[.,;:!?]+$/, "");
|
|
23
|
+
}
|
|
24
|
+
var URL_PROMPT_RE = /(?:Paste code here|Paste your code|Enter (?:the )?code|Authorization code)/i;
|
|
25
|
+
function isUrlPromptReady(rawPane) {
|
|
26
|
+
const stripped = stripAnsi(rawPane);
|
|
27
|
+
return OAUTH_URL_RE.test(stripped) && URL_PROMPT_RE.test(stripped);
|
|
28
|
+
}
|
|
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;
|
|
31
|
+
function detectAuthOutcome(rawPane) {
|
|
32
|
+
const stripped = stripAnsi(rawPane);
|
|
33
|
+
const failureMatch = lastMatch(stripped, FAILURE_RE);
|
|
34
|
+
const successMatch = lastMatch(stripped, SUCCESS_RE);
|
|
35
|
+
if (failureMatch && successMatch) {
|
|
36
|
+
if (failureMatch.index > successMatch.index) {
|
|
37
|
+
return { kind: "failure", rawMatch: failureMatch.match };
|
|
38
|
+
}
|
|
39
|
+
return { kind: "success", rawMatch: successMatch.match };
|
|
40
|
+
}
|
|
41
|
+
if (failureMatch) return { kind: "failure", rawMatch: failureMatch.match };
|
|
42
|
+
if (successMatch) return { kind: "success", rawMatch: successMatch.match };
|
|
43
|
+
return { kind: "pending" };
|
|
44
|
+
}
|
|
45
|
+
function lastMatch(haystack, re) {
|
|
46
|
+
const globalRe = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
|
|
47
|
+
let last = null;
|
|
48
|
+
let m;
|
|
49
|
+
while ((m = globalRe.exec(haystack)) !== null) {
|
|
50
|
+
last = m;
|
|
51
|
+
if (m.index === globalRe.lastIndex) globalRe.lastIndex++;
|
|
52
|
+
}
|
|
53
|
+
return last ? { match: last[0], index: last.index } : null;
|
|
54
|
+
}
|
|
55
|
+
function classifyTmuxError(err) {
|
|
56
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
57
|
+
if (/can't find session|no server running/i.test(msg)) return { kind: "no-session" };
|
|
58
|
+
if (/command not found.*tmux|ENOENT.*tmux/i.test(msg)) return { kind: "tmux-missing" };
|
|
59
|
+
return { kind: "unknown", message: msg };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/lib/claude-pair-runtime.ts
|
|
63
|
+
var execFileAsync = promisify(execFile);
|
|
64
|
+
function tmuxSessionFor(codeName) {
|
|
65
|
+
return `agt-${codeName}`;
|
|
66
|
+
}
|
|
67
|
+
async function capturePane(session, opts = {}) {
|
|
68
|
+
const scrollback = opts.scrollback ?? -200;
|
|
69
|
+
const { stdout } = await execFileAsync("tmux", [
|
|
70
|
+
"capture-pane",
|
|
71
|
+
"-t",
|
|
72
|
+
session,
|
|
73
|
+
"-p",
|
|
74
|
+
"-S",
|
|
75
|
+
String(scrollback)
|
|
76
|
+
]);
|
|
77
|
+
return stdout;
|
|
78
|
+
}
|
|
79
|
+
async function sendKeys(session, ...keys) {
|
|
80
|
+
await execFileAsync("tmux", ["send-keys", "-t", session, ...keys]);
|
|
81
|
+
}
|
|
82
|
+
async function sleep(ms) {
|
|
83
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
84
|
+
}
|
|
85
|
+
async function startClaudePair(opts) {
|
|
86
|
+
const session = tmuxSessionFor(opts.codeName);
|
|
87
|
+
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
88
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 500;
|
|
89
|
+
try {
|
|
90
|
+
await execFileAsync("tmux", ["has-session", "-t", session]);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return { kind: "error", error: classifyTmuxError(err) };
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await sendKeys(session, "/login", "C-m");
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { kind: "error", error: classifyTmuxError(err) };
|
|
98
|
+
}
|
|
99
|
+
const deadline = Date.now() + timeoutMs;
|
|
100
|
+
while (Date.now() < deadline) {
|
|
101
|
+
await sleep(pollIntervalMs);
|
|
102
|
+
let pane;
|
|
103
|
+
try {
|
|
104
|
+
pane = await capturePane(session);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
return { kind: "error", error: classifyTmuxError(err) };
|
|
107
|
+
}
|
|
108
|
+
if (isUrlPromptReady(pane)) {
|
|
109
|
+
const url = extractOAuthUrl(pane);
|
|
110
|
+
if (url) return { kind: "url", url };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { kind: "timeout" };
|
|
114
|
+
}
|
|
115
|
+
async function submitClaudePairCode(opts) {
|
|
116
|
+
const session = tmuxSessionFor(opts.codeName);
|
|
117
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
118
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 500;
|
|
119
|
+
if (!opts.code || !opts.code.trim()) {
|
|
120
|
+
return {
|
|
121
|
+
kind: "error",
|
|
122
|
+
error: { kind: "unknown", message: "empty auth code" }
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (opts.code.length > 1024) {
|
|
126
|
+
return {
|
|
127
|
+
kind: "error",
|
|
128
|
+
error: { kind: "unknown", message: "auth code suspiciously long" }
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (/[\r\n]/.test(opts.code)) {
|
|
132
|
+
return {
|
|
133
|
+
kind: "error",
|
|
134
|
+
error: { kind: "unknown", message: "auth code contains newline" }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await sendKeys(session, opts.code.trim(), "C-m");
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return { kind: "error", error: classifyTmuxError(err) };
|
|
141
|
+
}
|
|
142
|
+
const deadline = Date.now() + timeoutMs;
|
|
143
|
+
while (Date.now() < deadline) {
|
|
144
|
+
await sleep(pollIntervalMs);
|
|
145
|
+
let pane;
|
|
146
|
+
try {
|
|
147
|
+
pane = await capturePane(session);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
return { kind: "error", error: classifyTmuxError(err) };
|
|
150
|
+
}
|
|
151
|
+
const outcome = detectAuthOutcome(pane);
|
|
152
|
+
if (outcome.kind === "success") return { kind: "success", rawMatch: outcome.rawMatch };
|
|
153
|
+
if (outcome.kind === "failure") return { kind: "failure", rawMatch: outcome.rawMatch };
|
|
154
|
+
}
|
|
155
|
+
return { kind: "timeout" };
|
|
156
|
+
}
|
|
157
|
+
async function getClaudePairStatus(codeName) {
|
|
158
|
+
const session = tmuxSessionFor(codeName);
|
|
159
|
+
try {
|
|
160
|
+
await execFileAsync("tmux", ["has-session", "-t", session]);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
const classified = classifyTmuxError(err);
|
|
163
|
+
if (classified.kind === "no-session") return { kind: "session-missing" };
|
|
164
|
+
return { kind: "error", error: classified };
|
|
165
|
+
}
|
|
166
|
+
let pane;
|
|
167
|
+
try {
|
|
168
|
+
pane = await capturePane(session);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return { kind: "error", error: classifyTmuxError(err) };
|
|
171
|
+
}
|
|
172
|
+
const outcome = detectAuthOutcome(pane);
|
|
173
|
+
if (outcome.kind === "success") return { kind: "success" };
|
|
174
|
+
if (outcome.kind === "failure") return { kind: "failure", rawMatch: outcome.rawMatch };
|
|
175
|
+
if (isUrlPromptReady(pane)) {
|
|
176
|
+
const url = extractOAuthUrl(pane);
|
|
177
|
+
if (url) return { kind: "awaiting-code", url };
|
|
178
|
+
}
|
|
179
|
+
return { kind: "idle" };
|
|
180
|
+
}
|
|
181
|
+
export {
|
|
182
|
+
getClaudePairStatus,
|
|
183
|
+
startClaudePair,
|
|
184
|
+
submitClaudePairCode,
|
|
185
|
+
tmuxSessionFor
|
|
186
|
+
};
|
|
187
|
+
//# sourceMappingURL=claude-pair-runtime-WGIKIPJV.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// Session names follow the manager's convention: `agt-<code_name>`.\n// Exposed so the API layer can build the same name without re-parsing.\nexport function tmuxSessionFor(codeName: string): string {\n return `agt-${codeName}`;\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// 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 codeName: string;\n /** Total time to wait for Claude Code to print the URL prompt. Default 15s. */\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 = tmuxSessionFor(opts.codeName);\n const timeoutMs = opts.timeoutMs ?? 15_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 // Send the slash command. Most TUIs need a deliberate Enter after\n // the literal slash text to submit; tmux's Enter token is `C-m`.\n try {\n await sendKeys(session, '/login', 'C-m');\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Poll until both the URL and \"Paste code here\" prompt are visible.\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 if (isUrlPromptReady(pane)) {\n const url = extractOAuthUrl(pane);\n if (url) return { kind: 'url', url };\n }\n }\n return { kind: 'timeout' };\n}\n\n// ---------------------------------------------------------------------------\n// submit-code — paste the auth code, wait for outcome\n// ---------------------------------------------------------------------------\n\nexport interface ClaudePairSubmitOpts {\n codeName: 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 = tmuxSessionFor(opts.codeName);\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(codeName: string): Promise<ClaudePairStatusResult> {\n const session = tmuxSessionFor(codeName);\n\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-LYWDRDJZ.js";
|
|
26
26
|
import {
|
|
27
27
|
findTaskByTemplate,
|
|
28
28
|
getProjectDir,
|
|
@@ -2311,6 +2311,14 @@ async function pollCycle() {
|
|
|
2311
2311
|
clearAgentCaches(prev.agentId, prev.codeName);
|
|
2312
2312
|
}
|
|
2313
2313
|
}
|
|
2314
|
+
try {
|
|
2315
|
+
await processClaudePairSessions(agents.map((a) => ({
|
|
2316
|
+
agentId: a.agent_id,
|
|
2317
|
+
codeName: a.code_name
|
|
2318
|
+
})));
|
|
2319
|
+
} catch (err) {
|
|
2320
|
+
log(`[claude-pair] poll failed: ${err.message}`);
|
|
2321
|
+
}
|
|
2314
2322
|
await healthCheckGateways(agentStates);
|
|
2315
2323
|
if (Date.now() - lastChannelSweepAt >= CHANNEL_SWEEP_INTERVAL_MS) {
|
|
2316
2324
|
lastChannelSweepAt = Date.now();
|
|
@@ -2742,7 +2750,7 @@ async function processAgent(agent, agentStates) {
|
|
|
2742
2750
|
}
|
|
2743
2751
|
try {
|
|
2744
2752
|
const sessionMode2 = refreshData.agent.session_mode;
|
|
2745
|
-
frameworkAdapter.writeChannelCredentials(agent.code_name, channelId, entry.config, { sessionMode: sessionMode2 });
|
|
2753
|
+
frameworkAdapter.writeChannelCredentials(agent.code_name, channelId, entry.config, { sessionMode: sessionMode2, agentId: agent.agent_id });
|
|
2746
2754
|
knownChannelConfigHashes.set(cacheKey, configHash);
|
|
2747
2755
|
log(`Channel credentials written for '${agent.code_name}/${channelId}' (reason=${reason}, hash=${configHash.slice(0, 8)}${prevHash ? `, prev=${prevHash.slice(0, 8)}` : ""})`);
|
|
2748
2756
|
} catch (err) {
|
|
@@ -2750,41 +2758,6 @@ async function processAgent(agent, agentStates) {
|
|
|
2750
2758
|
}
|
|
2751
2759
|
}
|
|
2752
2760
|
}
|
|
2753
|
-
if (frameworkId === "claude-code") {
|
|
2754
|
-
const channelPluginMap = {
|
|
2755
|
-
telegram: "telegram",
|
|
2756
|
-
discord: "discord",
|
|
2757
|
-
slack: "slack"
|
|
2758
|
-
};
|
|
2759
|
-
for (const channelId of Object.keys(refreshData.channel_configs)) {
|
|
2760
|
-
const pluginName = channelPluginMap[channelId];
|
|
2761
|
-
if (!pluginName) continue;
|
|
2762
|
-
const entry = refreshData.channel_configs[channelId];
|
|
2763
|
-
if (!entry || entry.status !== "active" && entry.status !== "pending") continue;
|
|
2764
|
-
try {
|
|
2765
|
-
const installedPath = join3(homedir3(), ".claude", "plugins", "installed_plugins.json");
|
|
2766
|
-
if (existsSync2(installedPath)) {
|
|
2767
|
-
const installed = JSON.parse(readFileSync2(installedPath, "utf-8"));
|
|
2768
|
-
const pluginKeys = Object.keys(installed.plugins ?? {});
|
|
2769
|
-
if (pluginKeys.some((k) => k.startsWith(`${pluginName}@`))) continue;
|
|
2770
|
-
}
|
|
2771
|
-
} catch {
|
|
2772
|
-
}
|
|
2773
|
-
try {
|
|
2774
|
-
const { execFileSync: efs } = await import("child_process");
|
|
2775
|
-
const claudePath = efs("which", ["claude"], { timeout: 5e3 }).toString().trim();
|
|
2776
|
-
if (claudePath) {
|
|
2777
|
-
efs(claudePath, ["plugin", "install", pluginName], {
|
|
2778
|
-
timeout: 3e4,
|
|
2779
|
-
stdio: "pipe"
|
|
2780
|
-
});
|
|
2781
|
-
log(`Auto-installed '${pluginName}' Claude Code plugin for '${agent.code_name}'`);
|
|
2782
|
-
}
|
|
2783
|
-
} catch (err) {
|
|
2784
|
-
log(`Failed to auto-install '${pluginName}' plugin for '${agent.code_name}': ${err.message}`);
|
|
2785
|
-
}
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
2761
|
} else if (agent.status === "paused") {
|
|
2789
2762
|
if (frameworkAdapter.setChannelEnabled) {
|
|
2790
2763
|
for (const channelId of Object.keys(refreshData.channel_configs)) {
|
|
@@ -5091,7 +5064,12 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
|
|
|
5091
5064
|
medium: "slack",
|
|
5092
5065
|
error_code: sent.ok ? null : "SLACK_SEND_FAILED"
|
|
5093
5066
|
});
|
|
5094
|
-
if (sent.ok)
|
|
5067
|
+
if (sent.ok) {
|
|
5068
|
+
await maybePostSlackThreadHint(agentCodeName, channelId, sent.ts);
|
|
5069
|
+
if (sent.ts && taskId) {
|
|
5070
|
+
await maybePostScheduledTaskRatingPrompt(agentId, taskId, channelId, sent.ts);
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5095
5073
|
return;
|
|
5096
5074
|
}
|
|
5097
5075
|
const chatId = parsed.chat_id ?? "";
|
|
@@ -5266,6 +5244,91 @@ async function reportDeliveryStatus(agentId, taskId, payload) {
|
|
|
5266
5244
|
log(`[delivery] Failed to report delivery status for ${agentId}/${taskId}: ${err.message}`);
|
|
5267
5245
|
}
|
|
5268
5246
|
}
|
|
5247
|
+
async function processClaudePairSessions(agents) {
|
|
5248
|
+
if (agents.length === 0) return;
|
|
5249
|
+
const agentIds = agents.map((a) => a.agentId);
|
|
5250
|
+
const codeNameByAgentId = new Map(agents.map((a) => [a.agentId, a.codeName]));
|
|
5251
|
+
const pendingResp = await api.post("/host/claude-pair/pending", { agent_ids: agentIds });
|
|
5252
|
+
if (!pendingResp.pending || pendingResp.pending.length === 0) return;
|
|
5253
|
+
const { startClaudePair, submitClaudePairCode } = await import("../claude-pair-runtime-WGIKIPJV.js");
|
|
5254
|
+
for (const session of pendingResp.pending) {
|
|
5255
|
+
const codeName = codeNameByAgentId.get(session.agent_id);
|
|
5256
|
+
if (!codeName) continue;
|
|
5257
|
+
try {
|
|
5258
|
+
if (session.status === "initiating") {
|
|
5259
|
+
log(`[claude-pair] dispatching /login for '${codeName}' (pair ${session.pair_id.slice(0, 8)})`);
|
|
5260
|
+
const result = await startClaudePair({ codeName });
|
|
5261
|
+
if (result.kind === "url") {
|
|
5262
|
+
await api.post("/host/claude-pair/result", {
|
|
5263
|
+
pair_id: session.pair_id,
|
|
5264
|
+
status: "awaiting_code",
|
|
5265
|
+
url: result.url
|
|
5266
|
+
});
|
|
5267
|
+
} else if (result.kind === "timeout") {
|
|
5268
|
+
await api.post("/host/claude-pair/result", {
|
|
5269
|
+
pair_id: session.pair_id,
|
|
5270
|
+
status: "timeout",
|
|
5271
|
+
error_code: "url_prompt_not_seen",
|
|
5272
|
+
error_message: "Claude Code did not print the URL prompt within the deadline"
|
|
5273
|
+
});
|
|
5274
|
+
} else {
|
|
5275
|
+
const errKind = result.error.kind;
|
|
5276
|
+
await api.post("/host/claude-pair/result", {
|
|
5277
|
+
pair_id: session.pair_id,
|
|
5278
|
+
status: errKind === "no-session" ? "session_missing" : "failure",
|
|
5279
|
+
error_code: errKind,
|
|
5280
|
+
error_message: errKind === "unknown" ? result.error.message : void 0
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
} else if (session.status === "code_submitted" && session.code) {
|
|
5284
|
+
log(`[claude-pair] submitting code for '${codeName}' (pair ${session.pair_id.slice(0, 8)})`);
|
|
5285
|
+
const result = await submitClaudePairCode({ codeName, code: session.code });
|
|
5286
|
+
if (result.kind === "success") {
|
|
5287
|
+
await api.post("/host/claude-pair/result", {
|
|
5288
|
+
pair_id: session.pair_id,
|
|
5289
|
+
status: "success"
|
|
5290
|
+
});
|
|
5291
|
+
} else if (result.kind === "failure") {
|
|
5292
|
+
await api.post("/host/claude-pair/result", {
|
|
5293
|
+
pair_id: session.pair_id,
|
|
5294
|
+
status: "failure",
|
|
5295
|
+
error_code: "invalid_code",
|
|
5296
|
+
error_message: result.rawMatch
|
|
5297
|
+
});
|
|
5298
|
+
} else if (result.kind === "timeout") {
|
|
5299
|
+
await api.post("/host/claude-pair/result", {
|
|
5300
|
+
pair_id: session.pair_id,
|
|
5301
|
+
status: "timeout",
|
|
5302
|
+
error_code: "outcome_not_seen",
|
|
5303
|
+
error_message: "Claude Code did not show a success/failure marker after submitting the code"
|
|
5304
|
+
});
|
|
5305
|
+
} else {
|
|
5306
|
+
const errKind = result.error.kind;
|
|
5307
|
+
await api.post("/host/claude-pair/result", {
|
|
5308
|
+
pair_id: session.pair_id,
|
|
5309
|
+
status: errKind === "no-session" ? "session_missing" : "failure",
|
|
5310
|
+
error_code: errKind,
|
|
5311
|
+
error_message: errKind === "unknown" ? result.error.message : void 0
|
|
5312
|
+
});
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
} catch (err) {
|
|
5316
|
+
log(`[claude-pair] dispatch failed for pair ${session.pair_id.slice(0, 8)}: ${err.message}`);
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
async function maybePostScheduledTaskRatingPrompt(agentId, taskId, channelId, messageTs) {
|
|
5321
|
+
try {
|
|
5322
|
+
await api.post("/host/scheduled-task/rating-prompt", {
|
|
5323
|
+
agent_id: agentId,
|
|
5324
|
+
task_id: taskId,
|
|
5325
|
+
channel: channelId,
|
|
5326
|
+
message_ts: messageTs
|
|
5327
|
+
});
|
|
5328
|
+
} catch (err) {
|
|
5329
|
+
log(`[rating-prompt] Failed to post rating prompt for ${agentId}/${taskId}: ${err.message}`);
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5269
5332
|
async function sendTaskNotification(agentCodeName, channel, to, text) {
|
|
5270
5333
|
const tokens = agentChannelTokens.get(agentCodeName);
|
|
5271
5334
|
if (channel === "slack") {
|