@openagents-org/agent-launcher 0.2.142 → 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 +50 -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 +665 -79
- package/src/agent-rows.js +13 -1
- package/src/auth-guidance.js +98 -0
- package/src/cli.js +29 -7
- package/src/config.js +8 -2
- package/src/daemon.js +108 -6
- package/src/installer.js +509 -10
- package/src/paths.js +125 -2
- package/src/tui.js +29 -2
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, side-effect-free helpers for the Cline CLI adapter.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is I/O-free and deterministic so it can be unit-tested
|
|
5
|
+
* without spawning the `cline` binary or touching the network: the NDJSON
|
|
6
|
+
* stream parser, the event interpreter, secret redaction, error/auth/version
|
|
7
|
+
* classification, the (shell-safe) argument builder, and the session-id
|
|
8
|
+
* correlation used for `--id` resume.
|
|
9
|
+
*
|
|
10
|
+
* Verified against Cline CLI v3.0.26 (`cline --json`). The `--json` stream is
|
|
11
|
+
* NDJSON on stdout where each line is an envelope:
|
|
12
|
+
* {ts, type:"hook_event", hookEventName, agentId, taskId, parentAgentId}
|
|
13
|
+
* {ts, type:"run_start", providerId, modelId, catalog, thinking, mode} (verbose)
|
|
14
|
+
* {ts, type:"agent_event", event:{...}} <- the model's activity
|
|
15
|
+
* {ts, type:"team_event", event:{...}} <- only when teams/spawn enabled
|
|
16
|
+
* {ts, type:"run_result", finishReason, iterations, usage, durationMs, text, model}
|
|
17
|
+
* and stderr carries {ts, type:"error", message} on fatal errors.
|
|
18
|
+
*
|
|
19
|
+
* The inner agent_event `event.type` is one of nine variants; text, reasoning
|
|
20
|
+
* and tool activity are multiplexed through content_start/content_update/
|
|
21
|
+
* content_end keyed by `contentType` ("text" | "reasoning" | "tool").
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
// Minimum Cline CLI version whose headless `--json` / `-c` / `--id` interface
|
|
27
|
+
// matches what this adapter drives. Cline's CLI changed its surface across
|
|
28
|
+
// majors (v1 preview → v2 → v3); the v3 line is what we target.
|
|
29
|
+
//
|
|
30
|
+
// Policy is a HARD minimum: a CONFIRMED-older version (parseable, < 3.0.0) is
|
|
31
|
+
// incompatible and the agent must not start. Only an UNDETERMINED version
|
|
32
|
+
// (output unparseable, or `cline --version` failed) is treated leniently as
|
|
33
|
+
// "unknown" — we proceed rather than lock users out on a future `--version`
|
|
34
|
+
// format change — but it is never reported as compatible.
|
|
35
|
+
const MIN_CLINE_VERSION = '3.0.0';
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Version handling
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pull a dotted version (e.g. "3.0.26") out of `cline --version` output.
|
|
43
|
+
* Returns null when nothing version-like is present.
|
|
44
|
+
*/
|
|
45
|
+
function parseClineVersion(raw) {
|
|
46
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
47
|
+
const m = raw.match(/(\d+)\.(\d+)\.(\d+)(?:[-.][0-9A-Za-z.]+)?/);
|
|
48
|
+
return m ? m[0] : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Compare two dotted versions. Returns -1, 0, or 1. */
|
|
52
|
+
function compareVersions(a, b) {
|
|
53
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
|
|
54
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
|
|
55
|
+
const len = Math.max(pa.length, pb.length);
|
|
56
|
+
for (let i = 0; i < len; i++) {
|
|
57
|
+
const x = pa[i] || 0;
|
|
58
|
+
const y = pb[i] || 0;
|
|
59
|
+
if (x < y) return -1;
|
|
60
|
+
if (x > y) return 1;
|
|
61
|
+
}
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Classify an installed Cline version against MIN_CLINE_VERSION.
|
|
67
|
+
* @returns {{version: string|null, supported: boolean|null}}
|
|
68
|
+
* supported === true → meets the minimum (>= 3.0.0) → may start
|
|
69
|
+
* supported === false → CONFIRMED too old → incompatible, must NOT start
|
|
70
|
+
* supported === null → version undetermined (unparseable) → unknown, proceed
|
|
71
|
+
* leniently but never claim compatible
|
|
72
|
+
*/
|
|
73
|
+
function classifyClineVersion(rawVersion) {
|
|
74
|
+
const version = parseClineVersion(rawVersion);
|
|
75
|
+
if (!version) return { version: null, supported: null };
|
|
76
|
+
return { version, supported: compareVersions(version, MIN_CLINE_VERSION) >= 0 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Secret redaction
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
// Redaction is applied to anything that could reach a log line: stderr text,
|
|
84
|
+
// error messages, and the spawn argv we echo for debugging. We never log the
|
|
85
|
+
// user's prompt or raw env, but defense-in-depth keeps key material out of the
|
|
86
|
+
// daemon log even if an upstream message echoes it back.
|
|
87
|
+
const SECRET_PATTERNS = [
|
|
88
|
+
// Provider API keys: sk-..., sk-ant-..., sk-or-..., etc.
|
|
89
|
+
/\bsk-[A-Za-z0-9._-]{8,}\b/g,
|
|
90
|
+
// OpenRouter / generic long opaque tokens prefixed with a vendor tag
|
|
91
|
+
/\b(?:or|rk|gsk|ghp|gho|ghu|ghs|github_pat)[-_][A-Za-z0-9._-]{12,}\b/g,
|
|
92
|
+
// Bearer / Authorization header values
|
|
93
|
+
/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
94
|
+
// AWS access key ids
|
|
95
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// Sensitive keys we scrub when serializing an arbitrary object (e.g. a tool
|
|
99
|
+
// input) for a preview/log line.
|
|
100
|
+
const SENSITIVE_KEY_RE = /(api[_-]?key|secret|token|password|passwd|authorization|cookie|credential)/i;
|
|
101
|
+
|
|
102
|
+
/** Redact obvious secret material from a free-text string. */
|
|
103
|
+
function redactSecrets(text) {
|
|
104
|
+
if (text == null) return text;
|
|
105
|
+
let out = String(text);
|
|
106
|
+
for (const re of SECRET_PATTERNS) out = out.replace(re, '«redacted»');
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Redact an arbitrary value for previews: any object key matching
|
|
112
|
+
* SENSITIVE_KEY_RE has its value masked, and remaining strings are run through
|
|
113
|
+
* redactSecrets. Returns a JSON-safe clone (never mutates the input).
|
|
114
|
+
*/
|
|
115
|
+
function redactValue(value, depth = 0) {
|
|
116
|
+
if (depth > 6) return '…';
|
|
117
|
+
if (value == null) return value;
|
|
118
|
+
if (typeof value === 'string') return redactSecrets(value);
|
|
119
|
+
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
|
120
|
+
if (Array.isArray(value)) return value.map((v) => redactValue(v, depth + 1));
|
|
121
|
+
if (typeof value === 'object') {
|
|
122
|
+
const out = {};
|
|
123
|
+
for (const [k, v] of Object.entries(value)) {
|
|
124
|
+
out[k] = SENSITIVE_KEY_RE.test(k) ? '«redacted»' : redactValue(v, depth + 1);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Error classification
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Map a raw Cline error string to a coarse category plus a user-facing message.
|
|
137
|
+
* Frontends should show `userMessage`; the raw (redacted) text goes to logs.
|
|
138
|
+
*
|
|
139
|
+
* @returns {{kind: string, userMessage: string}}
|
|
140
|
+
* kind ∈ auth | provider | model | rate_limit | network | config | timeout | unknown
|
|
141
|
+
*/
|
|
142
|
+
function classifyClineError(rawMessage) {
|
|
143
|
+
const msg = redactSecrets(String(rawMessage || '').trim());
|
|
144
|
+
const low = msg.toLowerCase();
|
|
145
|
+
|
|
146
|
+
if (/unauthorized|authenticat|invalid.*(api[ _-]?key|token|credential)|\b401\b|\b403\b|re-authenticate|not signed in|no api key|api key (is )?(missing|required|not)/i.test(low)) {
|
|
147
|
+
return {
|
|
148
|
+
kind: 'auth',
|
|
149
|
+
userMessage:
|
|
150
|
+
'Cline is not authenticated. Configure an API key for this agent in the launcher, ' +
|
|
151
|
+
'or run `cline auth` to sign in. (' + truncate(msg, 160) + ')',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (/rate.?limit|too many requests|\b429\b|quota|overloaded/i.test(low)) {
|
|
155
|
+
return {
|
|
156
|
+
kind: 'rate_limit',
|
|
157
|
+
userMessage: 'The provider is rate-limiting or over quota. Please retry shortly. (' + truncate(msg, 160) + ')',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (/\bmodel\b.*(not found|unavailable|invalid|does not exist|unknown)|no such model|unsupported model/i.test(low)) {
|
|
161
|
+
return {
|
|
162
|
+
kind: 'model',
|
|
163
|
+
userMessage: 'The configured model is unavailable. Pick a valid model for this provider. (' + truncate(msg, 160) + ')',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (/provider.*(not|un)(available|configured|supported|known)|unknown provider|no provider/i.test(low)) {
|
|
167
|
+
return {
|
|
168
|
+
kind: 'provider',
|
|
169
|
+
userMessage: 'The selected provider is unavailable or unconfigured. Check the provider/model settings. (' + truncate(msg, 160) + ')',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (/timed?\s?out|timeout|deadline exceeded/i.test(low)) {
|
|
173
|
+
return { kind: 'timeout', userMessage: 'The task timed out. (' + truncate(msg, 160) + ')' };
|
|
174
|
+
}
|
|
175
|
+
if (/econn|enotfound|etimedout|network|fetch failed|socket|dns|tls|certificate|getaddrinfo/i.test(low)) {
|
|
176
|
+
return { kind: 'network', userMessage: 'A network error occurred reaching the provider. (' + truncate(msg, 160) + ')' };
|
|
177
|
+
}
|
|
178
|
+
if (/config|settings|providers\.json|parse|malformed|invalid json/i.test(low)) {
|
|
179
|
+
return { kind: 'config', userMessage: 'Cline configuration looks invalid. (' + truncate(msg, 160) + ')' };
|
|
180
|
+
}
|
|
181
|
+
return { kind: 'unknown', userMessage: msg ? 'Cline error: ' + truncate(msg, 200) : 'Cline returned an error.' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function truncate(s, n) {
|
|
185
|
+
s = String(s || '');
|
|
186
|
+
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Auth / config classification (from a parsed providers.json object)
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
// Field names that, when present and non-empty inside a provider's `settings`,
|
|
194
|
+
// indicate a stored credential. We only ever test for PRESENCE — values are
|
|
195
|
+
// never read into the return value or logged.
|
|
196
|
+
const CREDENTIAL_FIELDS = ['apiKey', 'apikey', 'token', 'accessToken', 'access_token', 'refreshToken', 'sessionToken'];
|
|
197
|
+
|
|
198
|
+
// Env vars Cline reads natively (a key here means the run can authenticate even
|
|
199
|
+
// when providers.json has no stored credential).
|
|
200
|
+
const CLINE_AUTH_ENV_VARS = [
|
|
201
|
+
'CLINE_API_KEY',
|
|
202
|
+
'ANTHROPIC_API_KEY',
|
|
203
|
+
'OPENAI_API_KEY',
|
|
204
|
+
'OPENROUTER_API_KEY',
|
|
205
|
+
'AI_GATEWAY_API_KEY',
|
|
206
|
+
'V0_API_KEY',
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
// Providers that authenticate WITHOUT an apiKey in providers.json (account /
|
|
210
|
+
// OAuth — the credential lives elsewhere). For these, a missing apiKey is NOT
|
|
211
|
+
// evidence of "no credentials" — we must report unknown, not no_credentials.
|
|
212
|
+
const ACCOUNT_PROVIDERS = new Set(['cline']);
|
|
213
|
+
|
|
214
|
+
function hasCredential(settings) {
|
|
215
|
+
if (!settings || typeof settings !== 'object') return false;
|
|
216
|
+
return CREDENTIAL_FIELDS.some(
|
|
217
|
+
(f) => typeof settings[f] === 'string' && settings[f].trim().length > 0,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Classify Cline auth/config from a parsed providers.json object and the agent
|
|
223
|
+
* env. Pure: never reads files, never exposes secret values.
|
|
224
|
+
*
|
|
225
|
+
* providers.json is a Cline-version-specific internal file, so this is a
|
|
226
|
+
* HEURISTIC, never an absolute auth verdict — the real authority is the run
|
|
227
|
+
* result. The classification is deliberately conservative:
|
|
228
|
+
* - `ready` only on a POSITIVE signal: a credential field is present,
|
|
229
|
+
* or a native env key is set.
|
|
230
|
+
* - `no_credentials` only when we can CONFIRM a key-based provider is selected
|
|
231
|
+
* with no credential anywhere (and it isn't an account/OAuth
|
|
232
|
+
* provider). Used for a softer hint, never to block startup.
|
|
233
|
+
* - `unknown` everything we cannot confirm: file missing, unparseable,
|
|
234
|
+
* unrecognized shape, an account/OAuth provider without a
|
|
235
|
+
* stored key, or no identifiable provider. Never reported as
|
|
236
|
+
* ready, and never blocks startup.
|
|
237
|
+
*
|
|
238
|
+
* @param {object|null} providersJson parsed providers.json, or null (missing),
|
|
239
|
+
* or a non-plain value (unparseable/changed)
|
|
240
|
+
* @param {object} env the agent env (checked for native key vars)
|
|
241
|
+
* @returns {{state: string, provider: string|null, model: string|null, detail: string}}
|
|
242
|
+
* state ∈ ready | no_credentials | unknown
|
|
243
|
+
*/
|
|
244
|
+
function classifyClineAuth(providersJson, env = {}) {
|
|
245
|
+
const envKey = CLINE_AUTH_ENV_VARS.find((k) => (env[k] || '').trim());
|
|
246
|
+
if (envKey) {
|
|
247
|
+
return { state: 'ready', provider: null, model: null, detail: `API key via ${envKey}` };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// No config file → cannot confirm anything (could be env/OAuth/custom data-dir).
|
|
251
|
+
if (providersJson === undefined || providersJson === null) {
|
|
252
|
+
return { state: 'unknown', provider: null, model: null, detail: 'No providers.json; auth undetermined' };
|
|
253
|
+
}
|
|
254
|
+
// Unparseable / not a recognizable object → undetermined, NOT a hard "no creds".
|
|
255
|
+
if (typeof providersJson !== 'object' || Array.isArray(providersJson) || providersJson.__parse_error) {
|
|
256
|
+
return { state: 'unknown', provider: null, model: null, detail: 'providers.json unparseable or unrecognized' };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const providers = providersJson.providers && typeof providersJson.providers === 'object' && !Array.isArray(providersJson.providers)
|
|
260
|
+
? providersJson.providers
|
|
261
|
+
: null;
|
|
262
|
+
if (!providers) {
|
|
263
|
+
return { state: 'unknown', provider: null, model: null, detail: 'providers.json shape unrecognized' };
|
|
264
|
+
}
|
|
265
|
+
const active = providersJson.lastUsedProvider || null;
|
|
266
|
+
|
|
267
|
+
// Positive signal: any provider carries a stored credential.
|
|
268
|
+
for (const id of Object.keys(providers)) {
|
|
269
|
+
if (hasCredential((providers[id] || {}).settings)) {
|
|
270
|
+
return { state: 'ready', provider: id, model: (providers[id].settings || {}).model || null, detail: 'Provider credential present' };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// No credential anywhere. Decide between no_credentials (confident) vs unknown.
|
|
275
|
+
const activeEntry = active && providers[active] ? providers[active] : null;
|
|
276
|
+
if (activeEntry && activeEntry.settings && !ACCOUNT_PROVIDERS.has(active)) {
|
|
277
|
+
// A key-based provider is selected with no stored credential → confident.
|
|
278
|
+
return { state: 'no_credentials', provider: active, model: activeEntry.settings.model || null, detail: 'Key-based provider selected but no credential stored' };
|
|
279
|
+
}
|
|
280
|
+
// Account/OAuth provider without a stored key, empty providers, or no active
|
|
281
|
+
// provider → cannot confirm; the credential may live outside providers.json.
|
|
282
|
+
return { state: 'unknown', provider: active, model: (activeEntry && activeEntry.settings && activeEntry.settings.model) || null, detail: 'No stored credential found; auth undetermined' };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Argument building (shell-safe — consumed as a spawn args array, never a string)
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Build the argv (after the binary) for a one-shot headless Cline run.
|
|
291
|
+
* The prompt is always the LAST positional so any leading dashes in user text
|
|
292
|
+
* cannot be parsed as flags. Returns a flat string array suitable for
|
|
293
|
+
* child_process.spawn(bin, args) — there is NO shell, so quotes/newlines/$()
|
|
294
|
+
* in the prompt are passed verbatim and safely.
|
|
295
|
+
*
|
|
296
|
+
* @param {object} o
|
|
297
|
+
* @param {string} o.prompt required — the task text (positional)
|
|
298
|
+
* @param {string} [o.cwd] working directory (-c)
|
|
299
|
+
* @param {string} [o.sessionId] resume an existing session (--id)
|
|
300
|
+
* @param {boolean} [o.planMode] plan mode (-p) vs default act+auto-approve
|
|
301
|
+
* @param {string} [o.provider] provider id (-P)
|
|
302
|
+
* @param {string} [o.model] model id (-m)
|
|
303
|
+
* @param {string} [o.apiKey] per-run API key (-k) — caller must keep out of logs
|
|
304
|
+
* @param {string} [o.thinking] reasoning level none|low|medium|high|xhigh (--thinking)
|
|
305
|
+
* @param {number} [o.timeoutSec] run timeout in seconds (-t); 0/undefined = no timeout
|
|
306
|
+
*/
|
|
307
|
+
function buildClineArgs(o) {
|
|
308
|
+
if (!o || typeof o.prompt !== 'string') {
|
|
309
|
+
throw new Error('buildClineArgs: prompt (string) is required');
|
|
310
|
+
}
|
|
311
|
+
const args = ['--json'];
|
|
312
|
+
if (o.cwd) args.push('-c', o.cwd);
|
|
313
|
+
if (o.sessionId) args.push('--id', o.sessionId);
|
|
314
|
+
// Default (no -p) is "act mode with auto-approve enabled" per `cline --help`.
|
|
315
|
+
// We make auto-approval explicit so a future change to Cline's default can't
|
|
316
|
+
// silently start blocking on tool prompts in our non-interactive context.
|
|
317
|
+
if (o.planMode) {
|
|
318
|
+
args.push('-p');
|
|
319
|
+
} else {
|
|
320
|
+
args.push('--auto-approve', 'true');
|
|
321
|
+
}
|
|
322
|
+
if (o.provider) args.push('-P', o.provider);
|
|
323
|
+
if (o.model) args.push('-m', o.model);
|
|
324
|
+
if (o.apiKey) args.push('-k', o.apiKey);
|
|
325
|
+
if (o.thinking) args.push('--thinking', o.thinking);
|
|
326
|
+
if (o.timeoutSec && Number(o.timeoutSec) > 0) args.push('-t', String(Math.floor(Number(o.timeoutSec))));
|
|
327
|
+
// Prompt LAST and positional so it is never mistaken for an option.
|
|
328
|
+
args.push(o.prompt);
|
|
329
|
+
return args;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Return a copy of an args array with the value after `-k` redacted, for logging. */
|
|
333
|
+
function redactArgs(args) {
|
|
334
|
+
const out = [];
|
|
335
|
+
for (let i = 0; i < args.length; i++) {
|
|
336
|
+
out.push(args[i]);
|
|
337
|
+
if (args[i] === '-k' || args[i] === '--key') {
|
|
338
|
+
out.push('«redacted»');
|
|
339
|
+
i++; // skip the real key
|
|
340
|
+
} else if (i === args.length - 1) {
|
|
341
|
+
// last positional is the prompt — don't echo user content to logs
|
|
342
|
+
out[out.length - 1] = '«prompt»';
|
|
343
|
+
} else {
|
|
344
|
+
out[out.length - 1] = redactSecrets(args[i]);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// NDJSON stream parser
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Incremental NDJSON parser. Handles:
|
|
356
|
+
* - chunked input where one JSON object is split across many chunks
|
|
357
|
+
* - a single chunk that contains several complete lines
|
|
358
|
+
* - mixed "\n" and "\r\n" line endings and a trailing incomplete line
|
|
359
|
+
* - stray non-JSON lines (skipped, surfaced via `garbage` for optional logging)
|
|
360
|
+
*
|
|
361
|
+
* Usage:
|
|
362
|
+
* const p = new ClineStreamParser();
|
|
363
|
+
* for (const chunk of chunks) for (const env of p.push(chunk)) handle(env);
|
|
364
|
+
* for (const env of p.flush()) handle(env); // trailing buffer at EOF
|
|
365
|
+
*/
|
|
366
|
+
class ClineStreamParser {
|
|
367
|
+
constructor() {
|
|
368
|
+
this._buf = '';
|
|
369
|
+
this.garbage = []; // lines that failed JSON.parse (for debugging only)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
push(chunk) {
|
|
373
|
+
if (chunk == null) return [];
|
|
374
|
+
this._buf += chunk.toString('utf-8');
|
|
375
|
+
const out = [];
|
|
376
|
+
let nl;
|
|
377
|
+
while ((nl = this._buf.indexOf('\n')) !== -1) {
|
|
378
|
+
const line = this._buf.slice(0, nl);
|
|
379
|
+
this._buf = this._buf.slice(nl + 1);
|
|
380
|
+
const env = this._parseLine(line);
|
|
381
|
+
if (env !== undefined) out.push(env);
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
flush() {
|
|
387
|
+
const out = [];
|
|
388
|
+
if (this._buf.trim()) {
|
|
389
|
+
const env = this._parseLine(this._buf);
|
|
390
|
+
if (env !== undefined) out.push(env);
|
|
391
|
+
}
|
|
392
|
+
this._buf = '';
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
_parseLine(line) {
|
|
397
|
+
const trimmed = line.replace(/\r$/, '').trim();
|
|
398
|
+
if (!trimmed) return undefined;
|
|
399
|
+
try {
|
|
400
|
+
return JSON.parse(trimmed);
|
|
401
|
+
} catch {
|
|
402
|
+
if (this.garbage.length < 50) this.garbage.push(trimmed.slice(0, 500));
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// Envelope → normalized event interpretation
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
// Tools that represent a question for the human rather than an autonomous action.
|
|
413
|
+
const ASK_TOOLS = new Set(['ask_question', 'ask_followup_question']);
|
|
414
|
+
|
|
415
|
+
/** Human-friendly verb for a Cline tool name (best-effort; unknown → the name). */
|
|
416
|
+
function friendlyToolLabel(toolName) {
|
|
417
|
+
switch (toolName) {
|
|
418
|
+
case 'editor':
|
|
419
|
+
case 'apply_patch':
|
|
420
|
+
return 'Editing file';
|
|
421
|
+
case 'read_files':
|
|
422
|
+
return 'Reading files';
|
|
423
|
+
case 'run_commands':
|
|
424
|
+
return 'Running command';
|
|
425
|
+
case 'search_codebase':
|
|
426
|
+
return 'Searching codebase';
|
|
427
|
+
case 'fetch_web_content':
|
|
428
|
+
return 'Fetching web content';
|
|
429
|
+
case 'skills':
|
|
430
|
+
return 'Using skill';
|
|
431
|
+
case 'spawn_agent':
|
|
432
|
+
return 'Spawning subagent';
|
|
433
|
+
default:
|
|
434
|
+
return toolName || 'Tool';
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Best-effort one-line preview of a tool's input, with secrets scrubbed. */
|
|
439
|
+
function toolInputPreview(input) {
|
|
440
|
+
if (input == null) return '';
|
|
441
|
+
if (typeof input === 'string') return truncate(redactSecrets(input), 160);
|
|
442
|
+
if (typeof input !== 'object') return truncate(String(input), 160);
|
|
443
|
+
const i = input;
|
|
444
|
+
let v =
|
|
445
|
+
(Array.isArray(i.commands) && i.commands.join('; ')) ||
|
|
446
|
+
i.command ||
|
|
447
|
+
(Array.isArray(i.file_paths) && i.file_paths.join(', ')) ||
|
|
448
|
+
i.file_path || i.path ||
|
|
449
|
+
(Array.isArray(i.queries) && i.queries.join(', ')) ||
|
|
450
|
+
i.query || i.pattern ||
|
|
451
|
+
(Array.isArray(i.requests) && i.requests.map((r) => r && r.url).filter(Boolean).join(', ')) ||
|
|
452
|
+
i.url || i.skill || i.question || '';
|
|
453
|
+
if (!v) {
|
|
454
|
+
try { v = JSON.stringify(redactValue(i)); } catch { v = ''; }
|
|
455
|
+
}
|
|
456
|
+
return truncate(redactSecrets(String(v)), 160);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Interpret one stream envelope into zero or more normalized events the adapter
|
|
461
|
+
* can act on. Stateless: relies on Cline emitting whole text/output in the
|
|
462
|
+
* `content_end` events (so we never double-count streamed deltas).
|
|
463
|
+
*
|
|
464
|
+
* Normalized event kinds:
|
|
465
|
+
* {kind:'text', text} final assistant text block for a turn
|
|
466
|
+
* {kind:'reasoning', text} a thinking/reasoning block
|
|
467
|
+
* {kind:'tool_start', toolName, toolCallId, label, preview, input}
|
|
468
|
+
* {kind:'tool_end', toolName, toolCallId, ok, error, durationMs}
|
|
469
|
+
* {kind:'ask', question, options, toolCallId}
|
|
470
|
+
* {kind:'notice', noticeType, reason, text}
|
|
471
|
+
* {kind:'error', message, recoverable} (from agent_event error)
|
|
472
|
+
* {kind:'done', reason, text} inner "done" event
|
|
473
|
+
* {kind:'result', finishReason, text, model, ok} terminal run_result
|
|
474
|
+
* {kind:'aborted', reason, text} run_aborted
|
|
475
|
+
* {kind:'session', taskId, agentId} hook_event agent_start (informational)
|
|
476
|
+
*/
|
|
477
|
+
function interpretClineEnvelope(env) {
|
|
478
|
+
if (!env || typeof env !== 'object') return [];
|
|
479
|
+
const type = env.type;
|
|
480
|
+
|
|
481
|
+
if (type === 'run_result') {
|
|
482
|
+
const reason = env.finishReason || 'unknown';
|
|
483
|
+
return [{
|
|
484
|
+
kind: 'result',
|
|
485
|
+
finishReason: reason,
|
|
486
|
+
ok: reason === 'completed',
|
|
487
|
+
text: typeof env.text === 'string' ? env.text : '',
|
|
488
|
+
model: env.model && env.model.id ? env.model.id : null,
|
|
489
|
+
}];
|
|
490
|
+
}
|
|
491
|
+
if (type === 'run_aborted') {
|
|
492
|
+
return [{ kind: 'aborted', reason: env.reason || 'aborted', text: env.message || '' }];
|
|
493
|
+
}
|
|
494
|
+
if (type === 'hook_event') {
|
|
495
|
+
if (env.hookEventName === 'agent_start') {
|
|
496
|
+
return [{ kind: 'session', taskId: env.taskId || null, agentId: env.agentId || null }];
|
|
497
|
+
}
|
|
498
|
+
return [];
|
|
499
|
+
}
|
|
500
|
+
if (type !== 'agent_event') return []; // run_start/team_event/unknown → no UI action
|
|
501
|
+
|
|
502
|
+
const ev = env.event;
|
|
503
|
+
if (!ev || typeof ev !== 'object') return [];
|
|
504
|
+
|
|
505
|
+
switch (ev.type) {
|
|
506
|
+
case 'content_end': {
|
|
507
|
+
// The whole, final content for a block. Deltas (content_start/_update)
|
|
508
|
+
// are intentionally ignored to avoid duplicate output.
|
|
509
|
+
if (ev.contentType === 'text') {
|
|
510
|
+
const t = (ev.text || '').trim();
|
|
511
|
+
return t ? [{ kind: 'text', text: t }] : [];
|
|
512
|
+
}
|
|
513
|
+
if (ev.contentType === 'reasoning') {
|
|
514
|
+
const t = (ev.reasoning || '').trim();
|
|
515
|
+
return t ? [{ kind: 'reasoning', text: t }] : [];
|
|
516
|
+
}
|
|
517
|
+
if (ev.contentType === 'tool') {
|
|
518
|
+
return [{
|
|
519
|
+
kind: 'tool_end',
|
|
520
|
+
toolName: ev.toolName || '',
|
|
521
|
+
toolCallId: ev.toolCallId || null,
|
|
522
|
+
ok: !ev.error,
|
|
523
|
+
error: ev.error ? redactSecrets(String(ev.error)) : null,
|
|
524
|
+
durationMs: typeof ev.durationMs === 'number' ? ev.durationMs : null,
|
|
525
|
+
}];
|
|
526
|
+
}
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
case 'content_start': {
|
|
530
|
+
// We only surface tool *starts* (text/reasoning starts are deltas we skip).
|
|
531
|
+
if (ev.contentType !== 'tool') return [];
|
|
532
|
+
const toolName = ev.toolName || '';
|
|
533
|
+
if (ASK_TOOLS.has(toolName)) {
|
|
534
|
+
const input = ev.input || {};
|
|
535
|
+
return [{
|
|
536
|
+
kind: 'ask',
|
|
537
|
+
toolCallId: ev.toolCallId || null,
|
|
538
|
+
question: typeof input.question === 'string' ? input.question : '',
|
|
539
|
+
options: Array.isArray(input.options) ? input.options.slice(0, 12) : [],
|
|
540
|
+
}];
|
|
541
|
+
}
|
|
542
|
+
return [{
|
|
543
|
+
kind: 'tool_start',
|
|
544
|
+
toolName,
|
|
545
|
+
toolCallId: ev.toolCallId || null,
|
|
546
|
+
label: friendlyToolLabel(toolName),
|
|
547
|
+
preview: toolInputPreview(ev.input),
|
|
548
|
+
input: ev.input,
|
|
549
|
+
}];
|
|
550
|
+
}
|
|
551
|
+
case 'error': {
|
|
552
|
+
const e = ev.error;
|
|
553
|
+
const message = e && typeof e === 'object' ? (e.message || '') : String(e || '');
|
|
554
|
+
return [{ kind: 'error', message: redactSecrets(message), recoverable: ev.recoverable === true }];
|
|
555
|
+
}
|
|
556
|
+
case 'notice': {
|
|
557
|
+
return [{
|
|
558
|
+
kind: 'notice',
|
|
559
|
+
noticeType: ev.noticeType || null,
|
|
560
|
+
reason: ev.reason || null,
|
|
561
|
+
text: redactSecrets(ev.message || ''),
|
|
562
|
+
}];
|
|
563
|
+
}
|
|
564
|
+
case 'done': {
|
|
565
|
+
return [{ kind: 'done', reason: ev.reason || 'completed', text: typeof ev.text === 'string' ? ev.text : '' }];
|
|
566
|
+
}
|
|
567
|
+
// content_update / iteration_start / iteration_end / usage → no UI action
|
|
568
|
+
default:
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// Session-id correlation (for --id resume)
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Cline does NOT emit a resumable session id in its `--json` stream — it only
|
|
579
|
+
* records sessions in `cline history --json`. We correlate the new session with
|
|
580
|
+
* a strict BEFORE/AFTER snapshot diff: the caller records the set of session ids
|
|
581
|
+
* that existed before spawning; this picks the run's session ONLY from records
|
|
582
|
+
* that are (a) new since then, (b) in the same working directory, and (c) within
|
|
583
|
+
* the run's time window. It binds ONLY when exactly one candidate remains —
|
|
584
|
+
* never guessing — so two concurrent runs in the same directory both decline
|
|
585
|
+
* rather than risk binding the wrong session.
|
|
586
|
+
*
|
|
587
|
+
* @param {Array} historyAfter parsed `cline history --json` taken AFTER the run
|
|
588
|
+
* @param {object} o
|
|
589
|
+
* @param {string} o.cwd the run's working directory (required to match)
|
|
590
|
+
* @param {Set|Array} [o.beforeIds] session ids that existed BEFORE the run (excluded)
|
|
591
|
+
* @param {number} [o.sinceMs] spawn timestamp (ms); only sessions started at/after count
|
|
592
|
+
* @param {string} [o.promptNeedle] a slice of the prompt; further narrows candidates
|
|
593
|
+
* @returns {string|null} the session id when exactly one candidate matches, else null
|
|
594
|
+
*/
|
|
595
|
+
function pickClineSessionId(historyAfter, o) {
|
|
596
|
+
if (!Array.isArray(historyAfter) || !o || !o.cwd) return null;
|
|
597
|
+
const before = o.beforeIds instanceof Set
|
|
598
|
+
? o.beforeIds
|
|
599
|
+
: new Set(Array.isArray(o.beforeIds) ? o.beforeIds : []);
|
|
600
|
+
const since = typeof o.sinceMs === 'number' ? o.sinceMs - 2000 : null; // small clock-skew slack
|
|
601
|
+
const candidates = historyAfter.filter((s) => {
|
|
602
|
+
if (!s || s.cwd !== o.cwd || !s.sessionId) return false;
|
|
603
|
+
if (before.has(s.sessionId)) return false; // only sessions NEW since the before-snapshot
|
|
604
|
+
if (since != null) {
|
|
605
|
+
const started = Date.parse(s.startedAt || s.updatedAt || '');
|
|
606
|
+
if (Number.isFinite(started) && started < since) return false;
|
|
607
|
+
}
|
|
608
|
+
if (o.promptNeedle && typeof s.prompt === 'string' && !s.prompt.includes(o.promptNeedle)) return false;
|
|
609
|
+
return true;
|
|
610
|
+
});
|
|
611
|
+
// Strict: bind only on a single unambiguous candidate; otherwise decline.
|
|
612
|
+
return candidates.length === 1 ? candidates[0].sessionId : null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
module.exports = {
|
|
616
|
+
MIN_CLINE_VERSION,
|
|
617
|
+
parseClineVersion,
|
|
618
|
+
compareVersions,
|
|
619
|
+
classifyClineVersion,
|
|
620
|
+
redactSecrets,
|
|
621
|
+
redactValue,
|
|
622
|
+
classifyClineError,
|
|
623
|
+
classifyClineAuth,
|
|
624
|
+
CLINE_AUTH_ENV_VARS,
|
|
625
|
+
buildClineArgs,
|
|
626
|
+
redactArgs,
|
|
627
|
+
ClineStreamParser,
|
|
628
|
+
interpretClineEnvelope,
|
|
629
|
+
friendlyToolLabel,
|
|
630
|
+
toolInputPreview,
|
|
631
|
+
pickClineSessionId,
|
|
632
|
+
};
|