@openagents-org/agent-launcher 0.2.143 → 0.2.145
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/package.json +1 -1
- package/registry.json +274 -66
- package/src/adapters/aider.js +634 -0
- package/src/adapters/amp.js +477 -0
- package/src/adapters/base.js +108 -7
- package/src/adapters/claude.js +36 -34
- package/src/adapters/cline-stream.js +632 -0
- package/src/adapters/cline.js +914 -0
- package/src/adapters/codex.js +14 -16
- package/src/adapters/copilot-stream-parser.js +380 -0
- package/src/adapters/copilot.js +796 -0
- package/src/adapters/cursor.js +9 -15
- package/src/adapters/gemini.js +10 -12
- package/src/adapters/goose-stream.js +268 -0
- package/src/adapters/goose.js +649 -0
- package/src/adapters/health-status.js +166 -0
- package/src/adapters/hermes.js +8 -17
- package/src/adapters/index.js +16 -1
- package/src/adapters/nanoclaw-bridge.js +264 -0
- package/src/adapters/nanoclaw-channel-install.js +411 -0
- package/src/adapters/nanoclaw-control.js +491 -0
- package/src/adapters/nanoclaw-delivery-store.js +0 -0
- package/src/adapters/nanoclaw-protocol.js +349 -0
- package/src/adapters/nanoclaw.js +661 -9
- package/src/adapters/opencode.js +541 -75
- package/src/agent-rows.js +13 -1
- package/src/auth-guidance.js +98 -0
- package/src/cli.js +24 -7
- package/src/config.js +8 -2
- package/src/daemon.js +86 -5
- package/src/installer.js +509 -10
- package/src/paths.js +125 -2
- package/src/tui.js +29 -2
package/src/adapters/codex.js
CHANGED
|
@@ -19,6 +19,7 @@ const { execSync, spawn } = require('child_process');
|
|
|
19
19
|
const http = require('http');
|
|
20
20
|
const https = require('https');
|
|
21
21
|
|
|
22
|
+
const { whereBinary } = require('../paths');
|
|
22
23
|
const BaseAdapter = require('./base');
|
|
23
24
|
const { buildOpenclawSystemPrompt } = require('./workspace-prompt');
|
|
24
25
|
|
|
@@ -49,7 +50,8 @@ class CodexAdapter extends BaseAdapter {
|
|
|
49
50
|
this._loadSessions();
|
|
50
51
|
|
|
51
52
|
// Determine mode:
|
|
52
|
-
// - CLI mode:
|
|
53
|
+
// - CLI mode: works with OpenAI's native Responses API (api.openai.com)
|
|
54
|
+
// OR with subscription-based auth via 'codex login'
|
|
53
55
|
// - Direct API mode: works with any OpenAI-compatible chat completions endpoint
|
|
54
56
|
this._codexBin = this._findCodexBinary();
|
|
55
57
|
this._directMode = false;
|
|
@@ -59,9 +61,10 @@ class CodexAdapter extends BaseAdapter {
|
|
|
59
61
|
const isOpenAiNative = !this._directBaseUrl ||
|
|
60
62
|
this._directBaseUrl.includes('api.openai.com');
|
|
61
63
|
|
|
62
|
-
if (this._codexBin && isOpenAiNative) {
|
|
64
|
+
if (this._codexBin && (isOpenAiNative || !this._directApiKey)) {
|
|
65
|
+
// CLI mode: either OpenAI native API or subscription auth (no API key)
|
|
63
66
|
this._useCliMode = true;
|
|
64
|
-
this._log(`CLI mode: ${this._codexBin}`);
|
|
67
|
+
this._log(`CLI mode: ${this._codexBin}${!this._directApiKey ? ' (subscription auth)' : ''}`);
|
|
65
68
|
} else if (this._directApiKey && this._directBaseUrl) {
|
|
66
69
|
this._directMode = true;
|
|
67
70
|
if (this._codexBin) {
|
|
@@ -70,7 +73,7 @@ class CodexAdapter extends BaseAdapter {
|
|
|
70
73
|
this._log(`Direct LLM mode: ${this._directBaseUrl} model=${this._directModel || 'gpt-4o'}`);
|
|
71
74
|
}
|
|
72
75
|
} else if (this._codexBin) {
|
|
73
|
-
// CLI binary found, no custom base URL — assume OpenAI
|
|
76
|
+
// CLI binary found, no custom base URL — assume OpenAI or subscription auth
|
|
74
77
|
this._useCliMode = true;
|
|
75
78
|
this._log(`CLI mode: ${this._codexBin}`);
|
|
76
79
|
} else {
|
|
@@ -123,17 +126,12 @@ class CodexAdapter extends BaseAdapter {
|
|
|
123
126
|
const portableCandidate = path.join(home, '.openagents', 'nodejs', 'node_modules', '.bin', `codex${ext}`);
|
|
124
127
|
if (fs.existsSync(portableCandidate)) return portableCandidate;
|
|
125
128
|
|
|
126
|
-
// Tier 1: PATH search
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return r.split(/\r?\n/)[0].trim();
|
|
133
|
-
} else {
|
|
134
|
-
return execSync('which codex', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
135
|
-
}
|
|
136
|
-
} catch {}
|
|
129
|
+
// Tier 1: PATH search via a codepage-safe lookup (whereBinary forces UTF-8
|
|
130
|
+
// output + verifies existence so a non-ASCII/Chinese username isn't mangled
|
|
131
|
+
// into an ENOENT; it also no longer returns an empty string on a miss, which
|
|
132
|
+
// used to short-circuit the Node-derived fallback tiers below).
|
|
133
|
+
const viaWhere = whereBinary('codex');
|
|
134
|
+
if (viaWhere) return viaWhere;
|
|
137
135
|
|
|
138
136
|
// Tier 2: Next to current Node.js interpreter (npm global)
|
|
139
137
|
const nodeBinDir = path.dirname(process.execPath);
|
|
@@ -270,7 +268,7 @@ class CodexAdapter extends BaseAdapter {
|
|
|
270
268
|
} else if (this._directMode) {
|
|
271
269
|
await this._handleViaDirectApi(content, msgChannel);
|
|
272
270
|
} else {
|
|
273
|
-
await this.sendError(msgChannel, 'codex CLI not found. Install with: npm install -g @openai/codex');
|
|
271
|
+
await this.sendError(msgChannel, 'codex CLI not found. Install with: npm install -g @openai/codex\n\nOr configure OPENAI_API_KEY + OPENAI_BASE_URL for direct API mode.');
|
|
274
272
|
}
|
|
275
273
|
}
|
|
276
274
|
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot CLI — JSONL stream parser (pure, no I/O).
|
|
3
|
+
*
|
|
4
|
+
* This module turns the raw byte stream produced by
|
|
5
|
+
* `copilot -p "<prompt>" --output-format=json`
|
|
6
|
+
* into a sequence of *normalized* events the CopilotAdapter can map onto
|
|
7
|
+
* OpenAgents workspace messages. It is deliberately split into two layers:
|
|
8
|
+
*
|
|
9
|
+
* 1. FRAMING (schema-agnostic) — `frameChunk()` / `CopilotStreamParser`
|
|
10
|
+
* Reassembles complete text lines from arbitrary chunk boundaries. This
|
|
11
|
+
* is the part that is fully specified regardless of the CLI version:
|
|
12
|
+
* one JSON object per line (JSONL), lines may be split across chunks,
|
|
13
|
+
* a single chunk may carry many lines, line endings may be `\n` or
|
|
14
|
+
* `\r\n`, blank lines and non-JSON noise must be tolerated. This layer
|
|
15
|
+
* is exhaustively unit-tested and does not depend on any field names.
|
|
16
|
+
*
|
|
17
|
+
* 2. CLASSIFICATION (schema-aware) — `classifyEvent()`
|
|
18
|
+
* Maps one parsed JSON object onto a normalized event `{ kind, ... }`.
|
|
19
|
+
* The concrete field/type names live in ONE table (`EVENT_KIND_BY_TYPE`
|
|
20
|
+
* + the field extractors) so that, once the JSONL schema of the locally
|
|
21
|
+
* installed `@github/copilot` build is captured, only this table needs
|
|
22
|
+
* to change — the adapter and the framing layer stay untouched.
|
|
23
|
+
*
|
|
24
|
+
* ── Schema status ────────────────────────────────────────────────────────
|
|
25
|
+
* The GitHub Copilot CLI (`@github/copilot`, executable `copilot`) ships its
|
|
26
|
+
* structured output as JSONL via `--output-format=json`. The exact `type`
|
|
27
|
+
* strings and payload shapes evolve between releases and MUST be confirmed
|
|
28
|
+
* against the actually-installed build (see docs/agents/github-copilot-cli.md
|
|
29
|
+
* and the delivery report). Until then the mapping below recognizes the
|
|
30
|
+
* documented/observed families and ALWAYS degrades unknown objects to a
|
|
31
|
+
* redacted `unknown` event instead of throwing — so a schema drift downgrades
|
|
32
|
+
* fidelity, never crashes a task. Never infer an event's meaning from a field
|
|
33
|
+
* name alone; verify against real output or the official source.
|
|
34
|
+
*
|
|
35
|
+
* Normalized event kinds (the adapter's stable contract):
|
|
36
|
+
* { kind: 'session', sessionId } real CLI session id/name
|
|
37
|
+
* { kind: 'text_delta', text } streaming assistant text
|
|
38
|
+
* { kind: 'text', text } final/complete assistant text
|
|
39
|
+
* { kind: 'reasoning', text } thinking / status narration
|
|
40
|
+
* { kind: 'tool_start', tool, input } a tool/command is starting
|
|
41
|
+
* { kind: 'tool_result', tool, result, isError } a tool/command finished
|
|
42
|
+
* { kind: 'file_change', path, action } a file was read/written
|
|
43
|
+
* { kind: 'shell', command, exitCode, output } a shell command ran
|
|
44
|
+
* { kind: 'permission', detail, granted } a permission decision
|
|
45
|
+
* { kind: 'ask_user', question } CLI wants interactive input
|
|
46
|
+
* { kind: 'usage', model, usage } model / token accounting
|
|
47
|
+
* { kind: 'done', status } turn completed (terminal)
|
|
48
|
+
* { kind: 'error', message, code } failure (terminal)
|
|
49
|
+
* { kind: 'unknown', raw } unrecognized (redacted)
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
'use strict';
|
|
53
|
+
|
|
54
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Redaction (shared by the adapter for logs + unknown-event diagnostics)
|
|
56
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
// GitHub tokens: ghp_/gho_/ghu_/ghs_/ghr_ (classic + fine-grained github_pat_),
|
|
59
|
+
// plus generic bearer/secret shapes. Kept conservative to avoid mangling code.
|
|
60
|
+
const TOKEN_PATTERNS = [
|
|
61
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
62
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
63
|
+
/\b(?:sk|pk|rk)-[A-Za-z0-9_-]{16,}\b/g,
|
|
64
|
+
/\bBearer\s+[A-Za-z0-9._-]{16,}\b/gi,
|
|
65
|
+
/\b[A-Fa-f0-9]{40,}\b/g, // long hex blobs (OAuth/refresh tokens)
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Env-style assignments of well-known secret-bearing variables — redact the
|
|
69
|
+
// value, keep the key so logs still say *which* var was involved.
|
|
70
|
+
const SECRET_ENV_KEYS = [
|
|
71
|
+
'COPILOT_GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_TOKEN', 'GH_COPILOT_TOKEN',
|
|
72
|
+
'GITHUB_COPILOT_TOKEN', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY',
|
|
73
|
+
'AZURE_API_KEY', 'GOOGLE_API_KEY', 'OA_WORKSPACE_TOKEN',
|
|
74
|
+
];
|
|
75
|
+
const SECRET_ENV_RE = new RegExp(
|
|
76
|
+
`\\b(${SECRET_ENV_KEYS.join('|')})(\\s*[=:]\\s*)("?)([^\\s"']+)`,
|
|
77
|
+
'gi',
|
|
78
|
+
);
|
|
79
|
+
const AUTH_HEADER_RE = /\b(authorization|x-api-key|x-github-token)(\s*:\s*)(\S+)/gi;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Redact secrets from an arbitrary string for safe logging / diagnostics.
|
|
83
|
+
* Never throws; non-strings are coerced. Pure.
|
|
84
|
+
*/
|
|
85
|
+
function redactSensitive(value) {
|
|
86
|
+
let s;
|
|
87
|
+
if (typeof value === 'string') s = value;
|
|
88
|
+
else { try { s = JSON.stringify(value); } catch { s = String(value); } }
|
|
89
|
+
if (!s) return s;
|
|
90
|
+
s = s.replace(SECRET_ENV_RE, (_m, k, sep, q) => `${k}${sep}${q}***`);
|
|
91
|
+
s = s.replace(AUTH_HEADER_RE, (_m, k, sep) => `${k}${sep}***`);
|
|
92
|
+
for (const re of TOKEN_PATTERNS) s = s.replace(re, '***');
|
|
93
|
+
return s;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Produce a short, redacted, single-line diagnostic for an unknown event so a
|
|
98
|
+
* schema drift is observable in logs without dumping a full transcript or any
|
|
99
|
+
* secret. Pure.
|
|
100
|
+
*/
|
|
101
|
+
function diagnosticForUnknown(obj, maxLen = 300) {
|
|
102
|
+
let s;
|
|
103
|
+
try { s = JSON.stringify(obj); } catch { s = String(obj); }
|
|
104
|
+
s = redactSensitive(s).replace(/\s+/g, ' ').trim();
|
|
105
|
+
return s.length > maxLen ? s.slice(0, maxLen) + '…' : s;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
109
|
+
// Framing (schema-agnostic) — pure
|
|
110
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Append `chunk` to `buffer` and split off every complete line.
|
|
114
|
+
* Returns `{ lines, rest }` where `lines` are complete lines (no trailing
|
|
115
|
+
* newline, `\r` stripped) and `rest` is the leftover partial line to carry
|
|
116
|
+
* into the next call. Handles `\n` and `\r\n`. Pure — no state retained.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} buffer carry-over from the previous call
|
|
119
|
+
* @param {string|Buffer} chunk new bytes
|
|
120
|
+
* @returns {{ lines: string[], rest: string }}
|
|
121
|
+
*/
|
|
122
|
+
function frameChunk(buffer, chunk) {
|
|
123
|
+
const text = (buffer || '') + (chunk == null ? '' : chunk.toString('utf-8'));
|
|
124
|
+
const parts = text.split('\n');
|
|
125
|
+
const rest = parts.pop(); // last element is the incomplete tail (maybe '')
|
|
126
|
+
const lines = parts.map((l) => (l.endsWith('\r') ? l.slice(0, -1) : l));
|
|
127
|
+
return { lines, rest };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse one JSONL line into an object, or null when the line is blank or not
|
|
132
|
+
* valid JSON (CLIs occasionally interleave human-readable banners/noise). A
|
|
133
|
+
* single corrupt line must never abort the surrounding task. Pure.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} line
|
|
136
|
+
* @returns {object|null}
|
|
137
|
+
*/
|
|
138
|
+
function parseLine(line) {
|
|
139
|
+
if (line == null) return null;
|
|
140
|
+
const trimmed = String(line).trim();
|
|
141
|
+
if (!trimmed) return null;
|
|
142
|
+
if (trimmed[0] !== '{' && trimmed[0] !== '[') return null; // fast reject noise
|
|
143
|
+
try {
|
|
144
|
+
const v = JSON.parse(trimmed);
|
|
145
|
+
return (v && typeof v === 'object') ? v : null;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
152
|
+
// Classification (schema-aware) — pure. SINGLE source of field/type mapping.
|
|
153
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Maps a Copilot CLI event `type` (lowercased) to a normalized kind.
|
|
157
|
+
*
|
|
158
|
+
* ── Verification status (IMPORTANT) ──────────────────────────────────────
|
|
159
|
+
* VERIFIED against real CLI v1.0.63: `--output-format json` is "JSONL, one JSON
|
|
160
|
+
* object per line"; auth/session/network failures print to STDERR with EMPTY
|
|
161
|
+
* stdout (no JSONL error event) — so those are NOT parsed here, the adapter
|
|
162
|
+
* classifies them from stderr. The success-path stdout event names below
|
|
163
|
+
* (text/tool/file/done/etc.) could NOT be captured (no Copilot subscription in
|
|
164
|
+
* CI), so they are BEST-EFFORT, intentionally focused rather than broad, and
|
|
165
|
+
* must be confirmed against a real authenticated run. The deliberately small
|
|
166
|
+
* alias set avoids misclassifying unrelated objects; anything unrecognized
|
|
167
|
+
* degrades to a redacted `unknown` event (never a crash, never a false `done`).
|
|
168
|
+
* When real samples are available, extend THIS table only.
|
|
169
|
+
*/
|
|
170
|
+
const EVENT_KIND_BY_TYPE = {
|
|
171
|
+
// session lifecycle / identity (kind only mapped when an id is also present)
|
|
172
|
+
session: 'session', 'session.created': 'session', 'session.started': 'session',
|
|
173
|
+
session_started: 'session', thread: 'session', 'thread.started': 'session',
|
|
174
|
+
// streaming assistant text (qualified names only — no bare 'delta'/'token')
|
|
175
|
+
'text.delta': 'text_delta', text_delta: 'text_delta',
|
|
176
|
+
'message.delta': 'text_delta', 'assistant.delta': 'text_delta',
|
|
177
|
+
content_block_delta: 'text_delta',
|
|
178
|
+
// final assistant text (qualified/explicit names only — no bare 'message')
|
|
179
|
+
text: 'text', 'message.completed': 'text', 'assistant.message': 'text',
|
|
180
|
+
assistant: 'text', completion: 'text', agent_message: 'text',
|
|
181
|
+
// reasoning / status narration
|
|
182
|
+
reasoning: 'reasoning', thinking: 'reasoning', thought: 'reasoning',
|
|
183
|
+
status: 'reasoning', progress: 'reasoning',
|
|
184
|
+
// tool calls
|
|
185
|
+
tool_call: 'tool_start', 'tool.call': 'tool_start', tool_use: 'tool_start',
|
|
186
|
+
'tool.started': 'tool_start', tool_start: 'tool_start', function_call: 'tool_start',
|
|
187
|
+
tool_result: 'tool_result', 'tool.result': 'tool_result',
|
|
188
|
+
'tool.completed': 'tool_result', tool_output: 'tool_result',
|
|
189
|
+
function_result: 'tool_result',
|
|
190
|
+
// file + shell specializations (may also arrive as generic tool events)
|
|
191
|
+
file_change: 'file_change', file_edit: 'file_change', 'file.changed': 'file_change',
|
|
192
|
+
file_write: 'file_change', file_read: 'file_change',
|
|
193
|
+
shell: 'shell', command_execution: 'shell', 'shell.exec': 'shell',
|
|
194
|
+
// permissions + interactive prompts
|
|
195
|
+
permission: 'permission', 'permission.request': 'permission',
|
|
196
|
+
permission_request: 'permission', permission_denied: 'permission',
|
|
197
|
+
approval: 'permission',
|
|
198
|
+
ask_user: 'ask_user', 'user.prompt': 'ask_user', input_request: 'ask_user',
|
|
199
|
+
// accounting (explicit only — bare 'model'/'tokens' are too generic)
|
|
200
|
+
usage: 'usage',
|
|
201
|
+
// terminal (no bare 'end'/'result' — 'result' collides with tool results)
|
|
202
|
+
done: 'done', completed: 'done', 'turn.completed': 'done', finish: 'done',
|
|
203
|
+
error: 'error', failure: 'error', 'turn.failed': 'error', failed: 'error',
|
|
204
|
+
fatal: 'error', aborted: 'error',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
function _firstString(obj, keys) {
|
|
208
|
+
for (const k of keys) {
|
|
209
|
+
const v = obj[k];
|
|
210
|
+
if (typeof v === 'string' && v.length) return v;
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function _coerceText(v) {
|
|
216
|
+
if (typeof v === 'string') return v;
|
|
217
|
+
if (v && typeof v === 'object') {
|
|
218
|
+
if (typeof v.text === 'string') return v.text;
|
|
219
|
+
if (Array.isArray(v.content)) {
|
|
220
|
+
return v.content
|
|
221
|
+
.map((b) => (typeof b === 'string' ? b : (b && typeof b.text === 'string' ? b.text : '')))
|
|
222
|
+
.join('');
|
|
223
|
+
}
|
|
224
|
+
if (typeof v.content === 'string') return v.content;
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Classify a parsed JSON object into a normalized event. Never throws; an
|
|
231
|
+
* unrecognized shape becomes `{ kind: 'unknown', raw }` with a redacted
|
|
232
|
+
* diagnostic. Pure — the single place that knows Copilot field names.
|
|
233
|
+
*
|
|
234
|
+
* @param {object} obj
|
|
235
|
+
* @returns {{kind: string} & object}
|
|
236
|
+
*/
|
|
237
|
+
function classifyEvent(obj) {
|
|
238
|
+
if (!obj || typeof obj !== 'object') {
|
|
239
|
+
return { kind: 'unknown', raw: diagnosticForUnknown(obj) };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const rawType = _firstString(obj, ['type', 'event', 'event_type', 'kind', 'name']);
|
|
243
|
+
let kind = rawType ? EVENT_KIND_BY_TYPE[rawType.toLowerCase()] : undefined;
|
|
244
|
+
|
|
245
|
+
// Nested item types: some CLIs wrap the real type in `item.type`
|
|
246
|
+
// (e.g. { type: 'item.completed', item: { type: 'command_execution' } }).
|
|
247
|
+
const item = (obj.item && typeof obj.item === 'object') ? obj.item : null;
|
|
248
|
+
if (item) {
|
|
249
|
+
const itemType = _firstString(item, ['type', 'kind']);
|
|
250
|
+
const itemKind = itemType ? EVENT_KIND_BY_TYPE[itemType.toLowerCase()] : undefined;
|
|
251
|
+
if (itemKind) kind = itemKind;
|
|
252
|
+
}
|
|
253
|
+
const src = item || obj;
|
|
254
|
+
|
|
255
|
+
switch (kind) {
|
|
256
|
+
case 'session': {
|
|
257
|
+
const sessionId = _firstString(src, [
|
|
258
|
+
'session_id', 'sessionId', 'session', 'thread_id', 'threadId', 'id', 'name',
|
|
259
|
+
]);
|
|
260
|
+
if (!sessionId) break; // not actually a session event → fall to unknown
|
|
261
|
+
return { kind: 'session', sessionId };
|
|
262
|
+
}
|
|
263
|
+
case 'text_delta': {
|
|
264
|
+
const text = _coerceText(src.delta) ?? _coerceText(src.text) ?? _coerceText(src.content) ?? _coerceText(src);
|
|
265
|
+
return { kind: 'text_delta', text: text || '' };
|
|
266
|
+
}
|
|
267
|
+
case 'text': {
|
|
268
|
+
const text = _coerceText(src.text) ?? _coerceText(src.message) ?? _coerceText(src.content) ?? _coerceText(src);
|
|
269
|
+
return { kind: 'text', text: text || '' };
|
|
270
|
+
}
|
|
271
|
+
case 'reasoning': {
|
|
272
|
+
const text = _coerceText(src.text) ?? _coerceText(src.reasoning) ?? _coerceText(src.message)
|
|
273
|
+
?? _coerceText(src.content) ?? _firstString(src, ['status', 'label', 'title']) ?? '';
|
|
274
|
+
return { kind: 'reasoning', text };
|
|
275
|
+
}
|
|
276
|
+
case 'tool_start': {
|
|
277
|
+
const tool = _firstString(src, ['tool', 'tool_name', 'name', 'function']) || 'tool';
|
|
278
|
+
const input = src.input ?? src.arguments ?? src.args ?? src.parameters ?? src.params;
|
|
279
|
+
return { kind: 'tool_start', tool, input };
|
|
280
|
+
}
|
|
281
|
+
case 'tool_result': {
|
|
282
|
+
const tool = _firstString(src, ['tool', 'tool_name', 'name', 'function']) || 'tool';
|
|
283
|
+
const result = src.result ?? src.output ?? src.content ?? src.stdout;
|
|
284
|
+
const isError = !!(src.is_error ?? src.isError ?? src.error ?? (src.exit_code != null && src.exit_code !== 0));
|
|
285
|
+
return { kind: 'tool_result', tool, result, isError };
|
|
286
|
+
}
|
|
287
|
+
case 'file_change': {
|
|
288
|
+
const filePath = _firstString(src, ['path', 'file', 'file_path', 'filename', 'filePath']);
|
|
289
|
+
const action = _firstString(src, ['action', 'operation', 'change', 'mode'])
|
|
290
|
+
|| (rawType && /read/i.test(rawType) ? 'read' : 'edit');
|
|
291
|
+
return { kind: 'file_change', path: filePath || '', action };
|
|
292
|
+
}
|
|
293
|
+
case 'shell': {
|
|
294
|
+
const command = _firstString(src, ['command', 'cmd', 'shell', 'script']) || '';
|
|
295
|
+
const exitCode = src.exit_code ?? src.exitCode ?? src.code ?? null;
|
|
296
|
+
const output = _coerceText(src.output) ?? _coerceText(src.stdout) ?? '';
|
|
297
|
+
return { kind: 'shell', command, exitCode, output };
|
|
298
|
+
}
|
|
299
|
+
case 'permission': {
|
|
300
|
+
const detail = _firstString(src, ['detail', 'message', 'tool', 'resource', 'reason'])
|
|
301
|
+
|| diagnosticForUnknown(src, 120);
|
|
302
|
+
const granted = !!(src.granted ?? src.allowed ?? src.approved);
|
|
303
|
+
return { kind: 'permission', detail, granted };
|
|
304
|
+
}
|
|
305
|
+
case 'ask_user': {
|
|
306
|
+
const question = _firstString(src, ['question', 'prompt', 'message', 'text']) || '';
|
|
307
|
+
return { kind: 'ask_user', question };
|
|
308
|
+
}
|
|
309
|
+
case 'usage': {
|
|
310
|
+
const model = _firstString(src, ['model', 'model_id', 'modelName']);
|
|
311
|
+
const usage = src.usage ?? src.tokens ?? src.cost ?? null;
|
|
312
|
+
return { kind: 'usage', model, usage };
|
|
313
|
+
}
|
|
314
|
+
case 'done': {
|
|
315
|
+
const status = _firstString(src, ['status', 'reason', 'result']) || 'completed';
|
|
316
|
+
return { kind: 'done', status };
|
|
317
|
+
}
|
|
318
|
+
case 'error': {
|
|
319
|
+
const err = (src.error && typeof src.error === 'object') ? src.error : src;
|
|
320
|
+
const message = _firstString(err, ['message', 'error', 'detail', 'reason'])
|
|
321
|
+
|| (typeof src.error === 'string' ? src.error : '')
|
|
322
|
+
|| 'Copilot CLI reported an error';
|
|
323
|
+
const code = _firstString(err, ['code', 'type', 'status']) || null;
|
|
324
|
+
return { kind: 'error', message: redactSensitive(message), code };
|
|
325
|
+
}
|
|
326
|
+
default:
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
return { kind: 'unknown', raw: diagnosticForUnknown(obj) };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
333
|
+
// Stateful incremental parser — thin wrapper over the pure functions
|
|
334
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
class CopilotStreamParser {
|
|
337
|
+
constructor() {
|
|
338
|
+
this._buffer = '';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Feed a stdout chunk; returns the normalized events newly completed by it.
|
|
343
|
+
* Partial trailing data is retained until the next push()/flush().
|
|
344
|
+
* @param {string|Buffer} chunk
|
|
345
|
+
* @returns {Array<object>}
|
|
346
|
+
*/
|
|
347
|
+
push(chunk) {
|
|
348
|
+
const { lines, rest } = frameChunk(this._buffer, chunk);
|
|
349
|
+
this._buffer = rest;
|
|
350
|
+
const events = [];
|
|
351
|
+
for (const line of lines) {
|
|
352
|
+
const obj = parseLine(line);
|
|
353
|
+
if (obj === null) continue; // blank or non-JSON noise — skip, never throw
|
|
354
|
+
events.push(classifyEvent(obj));
|
|
355
|
+
}
|
|
356
|
+
return events;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Flush any buffered trailing line (e.g. last line without a newline at
|
|
361
|
+
* process exit). Returns the normalized events it yields. Idempotent.
|
|
362
|
+
* @returns {Array<object>}
|
|
363
|
+
*/
|
|
364
|
+
flush() {
|
|
365
|
+
const tail = this._buffer;
|
|
366
|
+
this._buffer = '';
|
|
367
|
+
const obj = parseLine(tail);
|
|
368
|
+
return obj === null ? [] : [classifyEvent(obj)];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = {
|
|
373
|
+
CopilotStreamParser,
|
|
374
|
+
frameChunk,
|
|
375
|
+
parseLine,
|
|
376
|
+
classifyEvent,
|
|
377
|
+
redactSensitive,
|
|
378
|
+
diagnosticForUnknown,
|
|
379
|
+
EVENT_KIND_BY_TYPE,
|
|
380
|
+
};
|