orquesta-agent 0.2.246 → 0.2.248
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/cli-auth-detect.js +1 -1
- package/dist/cli-auth-detect.js.map +1 -1
- package/dist/executor.d.ts +11 -4
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +273 -38
- package/dist/executor.js.map +1 -1
- package/dist/index.js +13 -8
- package/dist/index.js.map +1 -1
- package/dist/interactive-prompt.d.ts +65 -0
- package/dist/interactive-prompt.d.ts.map +1 -0
- package/dist/interactive-prompt.js +115 -0
- package/dist/interactive-prompt.js.map +1 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +5 -0
- package/dist/sandbox.js.map +1 -1
- package/dist/secret-files.d.ts +19 -0
- package/dist/secret-files.d.ts.map +1 -0
- package/dist/secret-files.js +77 -0
- package/dist/secret-files.js.map +1 -0
- package/dist/supabase.d.ts +13 -8
- package/dist/supabase.d.ts.map +1 -1
- package/dist/supabase.js +27 -1
- package/dist/supabase.js.map +1 -1
- package/dist/ui/onboarding.js +1 -1
- package/dist/ui/onboarding.js.map +1 -1
- package/dist/ui/public/app.js +2 -0
- package/dist/ui/public/index.html +1 -0
- package/dist/ui/public/style.css +1 -0
- package/dist/ui/public/welcome.html +2 -0
- package/dist/ui/server.d.ts +1 -1
- package/dist/ui/server.d.ts.map +1 -1
- package/dist/ui/server.js +2 -2
- package/dist/ui/server.js.map +1 -1
- package/dist/ui/types.d.ts +1 -1
- package/dist/ui/types.d.ts.map +1 -1
- package/dist/ws-client.d.ts +1 -1
- package/dist/ws-client.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/executor.js
CHANGED
|
@@ -119,6 +119,8 @@ import { fileURLToPath, pathToFileURL } from 'url';
|
|
|
119
119
|
import { createRequire } from 'module';
|
|
120
120
|
import * as logger from './logger.js';
|
|
121
121
|
import { redactSecrets } from './redact.js';
|
|
122
|
+
import { materializeSecretFiles } from './secret-files.js';
|
|
123
|
+
import { InteractiveStallDetector, describeInteractiveStall, stripAnsi as stripAnsiForResult } from './interactive-prompt.js';
|
|
122
124
|
// ESM require for resolving the bundled orquesta-cli entry on Windows (see the
|
|
123
125
|
// interactive-session spawn — node-pty can't launch the `.cmd` shim).
|
|
124
126
|
const nodeRequire = createRequire(import.meta.url);
|
|
@@ -155,6 +157,22 @@ function cleanKimiText(str) {
|
|
|
155
157
|
.map(line => line.replace(/^(\s*)•\s/, '$1'))
|
|
156
158
|
.join('\n');
|
|
157
159
|
}
|
|
160
|
+
// kiro-cli's headless `chat` output is a TUI transcript, not a plain answer:
|
|
161
|
+
// ANSI colour throughout, a trust banner when --trust-all-tools is on ("All
|
|
162
|
+
// tools are now trusted (!). …", "Agents can sometimes do unexpected things…",
|
|
163
|
+
// "Learn more at https://kiro.dev/docs/cli/chat/security/…"), the answer
|
|
164
|
+
// prefixed with the `> ` prompt marker, and a "▸ Credits: 0.05 • Time: 2s"
|
|
165
|
+
// footer. Strip the chrome so the timeline shows the answer.
|
|
166
|
+
function cleanKiroText(str) {
|
|
167
|
+
return stripAnsiCodes(str)
|
|
168
|
+
.split('\n')
|
|
169
|
+
.filter(line => !/^\s*All tools are now trusted/.test(line))
|
|
170
|
+
.filter(line => !/^\s*Agents can sometimes do unexpected things/.test(line))
|
|
171
|
+
.filter(line => !/^\s*Learn more at https:\/\/kiro\.dev\//.test(line))
|
|
172
|
+
.filter(line => !/^\s*▸?\s*Credits:\s*[\d.]+\s*•\s*Time:/.test(line))
|
|
173
|
+
.map(line => line.replace(/^>\s?/, ''))
|
|
174
|
+
.join('\n');
|
|
175
|
+
}
|
|
158
176
|
function stripAnsiCodes(str) {
|
|
159
177
|
// Remove OSC sequences first (ESC ] ... BEL or ESC ] ... ESC \)
|
|
160
178
|
// Windows Terminal / ConEmu emit OSC 9;4;<state>;<progress> for progress markers.
|
|
@@ -236,6 +254,18 @@ const promptCostCents = new Map();
|
|
|
236
254
|
// ever saw logs, and panels that read prompts.result (architecture extraction,
|
|
237
255
|
// auto-agent polling) found it empty.
|
|
238
256
|
const promptFinalResult = new Map();
|
|
257
|
+
// promptId -> failure text when a run was cut for waiting on a keyboard prompt
|
|
258
|
+
// (see interactive-prompt.ts). Read by the close handler.
|
|
259
|
+
const promptInteractiveStall = new Map();
|
|
260
|
+
// How long a prompt-looking line may sit with no further output before the
|
|
261
|
+
// run is killed as "waiting for input". 0 disables the watchdog.
|
|
262
|
+
function interactivePromptTimeoutMs() {
|
|
263
|
+
const raw = process.env.ORQUESTA_INTERACTIVE_PROMPT_TIMEOUT_MS;
|
|
264
|
+
if (raw === undefined || raw === '')
|
|
265
|
+
return 60_000;
|
|
266
|
+
const n = Number(raw);
|
|
267
|
+
return Number.isFinite(n) && n >= 0 ? n : 60_000;
|
|
268
|
+
}
|
|
239
269
|
// The CLI's own verdict for a run, from its `end` event. Kept per prompt because
|
|
240
270
|
// several prompts stream through the same process concurrently. See QA-76: an
|
|
241
271
|
// exit code is not evidence that the work succeeded.
|
|
@@ -343,6 +373,18 @@ export function setInjectedCredentials(credentials) {
|
|
|
343
373
|
export function getInjectedCredentials() {
|
|
344
374
|
return injectedCredentials;
|
|
345
375
|
}
|
|
376
|
+
// Write the project's secret files to disk (relative paths resolve against
|
|
377
|
+
// `cwd`) and return the credentials plus one `ORQUESTA_FILE_<NAME>` var per
|
|
378
|
+
// file — listed in the manifest too, so orquesta-cli's env whitelist lets the
|
|
379
|
+
// shell tools see them.
|
|
380
|
+
function withSecretFileEnv(credentials, cwd) {
|
|
381
|
+
const fileEnv = materializeSecretFiles(cwd);
|
|
382
|
+
const fileKeys = Object.keys(fileEnv);
|
|
383
|
+
if (fileKeys.length === 0)
|
|
384
|
+
return credentials;
|
|
385
|
+
const manifest = [credentials[INJECTED_ENV_MANIFEST], ...fileKeys].filter(Boolean).join(',');
|
|
386
|
+
return { ...credentials, ...fileEnv, [INJECTED_ENV_MANIFEST]: manifest };
|
|
387
|
+
}
|
|
346
388
|
let globalPermissionMode = 'auto';
|
|
347
389
|
// Agent instructions (from Orquesta dashboard)
|
|
348
390
|
let agentInstructions = null;
|
|
@@ -1037,6 +1079,87 @@ export function checkCursorAuth() {
|
|
|
1037
1079
|
return cache({ available: true, authenticated: true, method: null });
|
|
1038
1080
|
}
|
|
1039
1081
|
}
|
|
1082
|
+
// Resolve AWS Kiro's CLI (`kiro-cli`). Its installer drops the binary in
|
|
1083
|
+
// ~/.local/bin (alongside `kiro-cli-chat`/`kiro-cli-term`) and, like cursor's,
|
|
1084
|
+
// only reminds the user to put that on PATH — so probe the install dir too.
|
|
1085
|
+
// Order: KIRO_CLI_PATH → on PATH → ~/.local/bin/kiro-cli.
|
|
1086
|
+
// macOS + Linux only: Kiro ships no native Windows CLI, so on win32 this is
|
|
1087
|
+
// always null and the engine never appears as available there.
|
|
1088
|
+
let kiroBinaryCache = null;
|
|
1089
|
+
export function resolveKiroBinary() {
|
|
1090
|
+
const now = Date.now();
|
|
1091
|
+
if (kiroBinaryCache && now - kiroBinaryCache.at < CLI_DETECTION_TTL_MS)
|
|
1092
|
+
return kiroBinaryCache.path;
|
|
1093
|
+
let resolved = null;
|
|
1094
|
+
if (process.platform !== 'win32') {
|
|
1095
|
+
const envPath = process.env.KIRO_CLI_PATH;
|
|
1096
|
+
if (envPath) {
|
|
1097
|
+
try {
|
|
1098
|
+
fs.accessSync(envPath, fs.constants.X_OK);
|
|
1099
|
+
resolved = envPath;
|
|
1100
|
+
}
|
|
1101
|
+
catch { /* bad override */ }
|
|
1102
|
+
}
|
|
1103
|
+
if (!resolved) {
|
|
1104
|
+
try {
|
|
1105
|
+
execSync('kiro-cli --version', { stdio: 'pipe', timeout: 15000 });
|
|
1106
|
+
resolved = 'kiro-cli';
|
|
1107
|
+
}
|
|
1108
|
+
catch { /* not on PATH */ }
|
|
1109
|
+
}
|
|
1110
|
+
if (!resolved) {
|
|
1111
|
+
const candidate = path.join(os.homedir(), '.local', 'bin', 'kiro-cli');
|
|
1112
|
+
try {
|
|
1113
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
1114
|
+
resolved = candidate;
|
|
1115
|
+
}
|
|
1116
|
+
catch { /* not installed there */ }
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
kiroBinaryCache = { path: resolved, at: now };
|
|
1120
|
+
return resolved;
|
|
1121
|
+
}
|
|
1122
|
+
// Kiro CLI (the `kiro-cli` binary). Authenticated separately with
|
|
1123
|
+
// `kiro-cli login` against the user's own AWS Builder ID / IAM Identity Center
|
|
1124
|
+
// — Orquesta never sees that credential.
|
|
1125
|
+
export function isKiroCliAvailable() {
|
|
1126
|
+
return resolveKiroBinary() !== null;
|
|
1127
|
+
}
|
|
1128
|
+
// Is kiro-cli actually LOGGED IN, not just installed? Same contract as
|
|
1129
|
+
// checkCursorAuth: `kiro-cli whoami` exits 0 with the identity when logged in
|
|
1130
|
+
// and exits 1 printing "Not logged in" otherwise. Only that wording flips the
|
|
1131
|
+
// flag; anything else (timeout, older build) reports authenticated with a null
|
|
1132
|
+
// method, because execution never depends on this flag.
|
|
1133
|
+
let kiroAuthCache = null;
|
|
1134
|
+
export function checkKiroAuth() {
|
|
1135
|
+
const now = Date.now();
|
|
1136
|
+
if (kiroAuthCache && now - kiroAuthCache.at < CLI_DETECTION_TTL_MS)
|
|
1137
|
+
return kiroAuthCache.result;
|
|
1138
|
+
const cache = (result) => {
|
|
1139
|
+
kiroAuthCache = { result, at: now };
|
|
1140
|
+
return result;
|
|
1141
|
+
};
|
|
1142
|
+
const binary = resolveKiroBinary();
|
|
1143
|
+
if (!binary)
|
|
1144
|
+
return cache({ available: false, authenticated: false, method: null });
|
|
1145
|
+
try {
|
|
1146
|
+
const out = execSync(`"${binary}" whoami`, {
|
|
1147
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1148
|
+
timeout: 15000,
|
|
1149
|
+
encoding: 'utf-8',
|
|
1150
|
+
});
|
|
1151
|
+
if (isLoggedOutMessage(out))
|
|
1152
|
+
return cache({ available: true, authenticated: false, method: null });
|
|
1153
|
+
return cache({ available: true, authenticated: true, method: 'subscription' });
|
|
1154
|
+
}
|
|
1155
|
+
catch (error) {
|
|
1156
|
+
const err = error;
|
|
1157
|
+
const combined = `${err.stdout ?? ''}${err.stderr ?? ''}`;
|
|
1158
|
+
if (isLoggedOutMessage(combined))
|
|
1159
|
+
return cache({ available: true, authenticated: false, method: null });
|
|
1160
|
+
return cache({ available: true, authenticated: true, method: null });
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1040
1163
|
// Dedupe the fallback warning: selectCli() runs on every heartbeat (the agent
|
|
1041
1164
|
// reports activeCli), so logging unconditionally spammed thousands of identical
|
|
1042
1165
|
// WARN lines. Only log when the message actually changes.
|
|
@@ -1051,12 +1174,13 @@ function warnCliOnce(msg) {
|
|
|
1051
1174
|
// the pinned CLI turns out not to be installed. Table-driven rather than a nest
|
|
1052
1175
|
// of pairwise ifs: every CLI added here otherwise costs another N branches, and
|
|
1053
1176
|
// the pair that got missed is exactly how a preference silently no-ops.
|
|
1054
|
-
const CLI_AUTO_ORDER = ['orquesta', 'claude', 'kimi', 'cursor'];
|
|
1177
|
+
const CLI_AUTO_ORDER = ['orquesta', 'claude', 'kimi', 'cursor', 'kiro'];
|
|
1055
1178
|
const CLI_AUTO_REASON = {
|
|
1056
1179
|
orquesta: 'Auto: orquesta available (local LLM)',
|
|
1057
1180
|
claude: 'Auto: claude available (Anthropic API)',
|
|
1058
1181
|
kimi: 'Auto: kimi available',
|
|
1059
1182
|
cursor: 'Auto: cursor-agent available',
|
|
1183
|
+
kiro: 'Auto: kiro-cli available',
|
|
1060
1184
|
};
|
|
1061
1185
|
// Select which CLI to use based on availability and preference
|
|
1062
1186
|
export function selectCli() {
|
|
@@ -1065,6 +1189,7 @@ export function selectCli() {
|
|
|
1065
1189
|
claude: isClaudeCliAvailable(),
|
|
1066
1190
|
kimi: isKimiCliAvailable(),
|
|
1067
1191
|
cursor: isCursorCliAvailable(),
|
|
1192
|
+
kiro: isKiroCliAvailable(),
|
|
1068
1193
|
};
|
|
1069
1194
|
if (!CLI_AUTO_ORDER.some(cli => available[cli])) {
|
|
1070
1195
|
return { cli: null, reason: `No CLI found (${CLI_AUTO_ORDER.join(', ')})` };
|
|
@@ -2201,7 +2326,7 @@ ${userRequestBody}`;
|
|
|
2201
2326
|
// Build environment with authentication
|
|
2202
2327
|
const env = {
|
|
2203
2328
|
...process.env,
|
|
2204
|
-
...injectedCredentials, // Inject credentials from Orquesta
|
|
2329
|
+
...withSecretFileEnv(injectedCredentials, workingDirectory || process.cwd()), // Inject credentials from Orquesta
|
|
2205
2330
|
CI: 'true', // Ensure non-interactive mode
|
|
2206
2331
|
// Expose the current prompt id so Claude can link generated files back
|
|
2207
2332
|
// to this prompt via /api/agent/files (prompt_id=$ORQUESTA_PROMPT_ID).
|
|
@@ -2258,9 +2383,11 @@ ${userRequestBody}`;
|
|
|
2258
2383
|
? (resolveKimiBinary() || 'kimi')
|
|
2259
2384
|
: cliCommand === 'cursor'
|
|
2260
2385
|
? (resolveCursorBinary() || 'cursor-agent')
|
|
2261
|
-
: cliCommand === '
|
|
2262
|
-
? (
|
|
2263
|
-
:
|
|
2386
|
+
: cliCommand === 'kiro'
|
|
2387
|
+
? (resolveKiroBinary() || 'kiro-cli')
|
|
2388
|
+
: cliCommand === 'claude'
|
|
2389
|
+
? (resolveClaudeBinary() || 'claude')
|
|
2390
|
+
: (cliCommand || 'claude');
|
|
2264
2391
|
const cwd = workingDirectory || process.cwd();
|
|
2265
2392
|
// Which branch this prompt runs on — captured up front so it's persisted even
|
|
2266
2393
|
// if the run fails, and broadcast live below once sendSpecialEvent exists.
|
|
@@ -2272,16 +2399,16 @@ ${userRequestBody}`;
|
|
|
2272
2399
|
let detectedCommitSha = null;
|
|
2273
2400
|
let detectedCommitUrl = null;
|
|
2274
2401
|
logger.info(`CLI Selection: ${cliCommand} (${reason})`);
|
|
2275
|
-
logger.info(`Spawning: ${cliBinary} -p "${fullContent.slice(0, 50)}..." in ${cwd}${currentBranch ? ` on branch ${currentBranch}` : ''}`);
|
|
2402
|
+
logger.info(`Spawning: ${cliBinary} ${cliCommand === 'kiro' ? 'chat' : '-p'} "${fullContent.slice(0, 50)}..." in ${cwd}${currentBranch ? ` on branch ${currentBranch}` : ''}`);
|
|
2276
2403
|
// Hoist the static block into the CLI system prompt (claude/orquesta support
|
|
2277
2404
|
// --append-system-prompt-file; kimi has no such flag and stays inline). On any
|
|
2278
2405
|
// write failure we fall back to the inline fullContent, so a dispatched prompt
|
|
2279
2406
|
// can never regress to a worse state than before.
|
|
2280
2407
|
let promptContent = fullContent;
|
|
2281
2408
|
let sysPromptFilePath = null;
|
|
2282
|
-
// kimi
|
|
2283
|
-
// context stays inline in the prompt.
|
|
2284
|
-
if (cliCommand !== 'kimi' && cliCommand !== 'cursor') {
|
|
2409
|
+
// kimi, cursor-agent and kiro-cli have no --append-system-prompt-file, so
|
|
2410
|
+
// their static context stays inline in the prompt.
|
|
2411
|
+
if (cliCommand !== 'kimi' && cliCommand !== 'cursor' && cliCommand !== 'kiro') {
|
|
2285
2412
|
try {
|
|
2286
2413
|
const promptDir = process.platform === 'win32'
|
|
2287
2414
|
? os.tmpdir()
|
|
@@ -2314,22 +2441,29 @@ ${userRequestBody}`;
|
|
|
2314
2441
|
// below only understands Claude's and orquesta-cli's,
|
|
2315
2442
|
// so asking for it would give us an unparseable
|
|
2316
2443
|
// stream instead of an answer.
|
|
2317
|
-
: cliCommand === '
|
|
2318
|
-
? '--
|
|
2319
|
-
//
|
|
2320
|
-
//
|
|
2321
|
-
//
|
|
2322
|
-
|
|
2323
|
-
|
|
2444
|
+
: cliCommand === 'kiro'
|
|
2445
|
+
? '--no-interactive --wrap never' // Kiro: `chat` is interactive by default;
|
|
2446
|
+
// --no-interactive answers once and exits, --wrap
|
|
2447
|
+
// never stops it re-flowing the answer to the
|
|
2448
|
+
// (absent) terminal width.
|
|
2449
|
+
: cliCommand === 'kimi'
|
|
2450
|
+
? '--output-format text' // Kimi: plain text. NOTE: -p (one-shot) CANNOT be
|
|
2451
|
+
// combined with -y ("Cannot combine --prompt with
|
|
2452
|
+
// --yolo"), so dispatched kimi prompts respond as
|
|
2453
|
+
// chat (no autonomous tool use). Interactive kimi
|
|
2454
|
+
// sessions DO get -y (see ptyArgs below).
|
|
2455
|
+
: '--verbose --output-format stream-json'; // Claude: stream-json format
|
|
2324
2456
|
// Kimi has no --dangerously-skip-permissions, and -p forbids -y. Cursor spells
|
|
2325
|
-
// the same thing --force (alias --yolo).
|
|
2457
|
+
// the same thing --force (alias --yolo); kiro spells it --trust-all-tools.
|
|
2326
2458
|
const permFlags = cliCommand === 'kimi'
|
|
2327
2459
|
? ''
|
|
2328
2460
|
: mode !== 'auto'
|
|
2329
2461
|
? ''
|
|
2330
2462
|
: cliCommand === 'cursor'
|
|
2331
2463
|
? '--force'
|
|
2332
|
-
: '
|
|
2464
|
+
: cliCommand === 'kiro'
|
|
2465
|
+
? '--trust-all-tools'
|
|
2466
|
+
: '--dangerously-skip-permissions';
|
|
2333
2467
|
// Per-project endpoint override (orquesta CLI only). Single-quoted for the
|
|
2334
2468
|
// shell-wrapped (Linux) path; pushed as a separate arg on Win/macOS below.
|
|
2335
2469
|
const useEndpoint = cliCommand === 'orquesta' && !!globalCliEndpoint;
|
|
@@ -2341,7 +2475,10 @@ ${userRequestBody}`;
|
|
|
2341
2475
|
// Claude CLI's headless `-p` mode does not accept positional file args as
|
|
2342
2476
|
// attachments — image refs are surfaced inline in the prompt as absolute
|
|
2343
2477
|
// paths so Claude's Read tool can pick them up.
|
|
2344
|
-
|
|
2478
|
+
// kiro-cli has no `-p`: the one-shot form is `kiro-cli chat "<prompt>"` with
|
|
2479
|
+
// --no-interactive (baseFlags), the prompt as the verb's positional arg.
|
|
2480
|
+
const promptVerb = cliCommand === 'kiro' ? 'chat' : '-p';
|
|
2481
|
+
command = `${cliBinary} ${promptVerb} '${escapedContent}' ${permFlags} ${baseFlags} ${endpointFlag} ${sysPromptFlag}`;
|
|
2345
2482
|
if (imageReferences.length > 0) {
|
|
2346
2483
|
logger.info(`Image attachments referenced inline: ${imageReferences.length}`);
|
|
2347
2484
|
}
|
|
@@ -2354,7 +2491,7 @@ ${userRequestBody}`;
|
|
|
2354
2491
|
const isDarwin = process.platform === 'darwin';
|
|
2355
2492
|
let claude;
|
|
2356
2493
|
if (isWindows || isDarwin) {
|
|
2357
|
-
const args = [
|
|
2494
|
+
const args = [promptVerb];
|
|
2358
2495
|
// Windows cmd.exe caps the whole command line at ~8191 chars. The prompt is
|
|
2359
2496
|
// prefixed with large context blocks (capabilities, project CLAUDE.md,
|
|
2360
2497
|
// coordination…), so passing it as an argv overflows that limit and the CLI
|
|
@@ -2414,6 +2551,16 @@ ${userRequestBody}`;
|
|
|
2414
2551
|
let thinkingBuffer = '';
|
|
2415
2552
|
let textBuffer = '';
|
|
2416
2553
|
let toolInputBuffer = '';
|
|
2554
|
+
// Fallbacks for the stored result when the CLI dies without a result event
|
|
2555
|
+
// (killed, crashed, cut for an interactive prompt): the last prose the model
|
|
2556
|
+
// produced, else the tail of whatever raw output there was.
|
|
2557
|
+
let lastAssistantText = '';
|
|
2558
|
+
let rawOutputTail = '';
|
|
2559
|
+
const noteRawOutput = (text) => {
|
|
2560
|
+
const clean = stripAnsiForResult(text);
|
|
2561
|
+
if (clean.trim())
|
|
2562
|
+
rawOutputTail = (rawOutputTail + clean).slice(-4000);
|
|
2563
|
+
};
|
|
2417
2564
|
// Parse stream-json output format
|
|
2418
2565
|
// Returns { output: string, hadValidJson: boolean, logs: AgentLogEntry[] }
|
|
2419
2566
|
const parseStreamJson = (text) => {
|
|
@@ -2495,6 +2642,7 @@ ${userRequestBody}`;
|
|
|
2495
2642
|
output += `\n${content}\n`;
|
|
2496
2643
|
logs.push(createOutputLog(content, 'markdown', 'info', promptId));
|
|
2497
2644
|
promptFinalResult.set(promptId, String(content));
|
|
2645
|
+
lastAssistantText = String(content);
|
|
2498
2646
|
}
|
|
2499
2647
|
}
|
|
2500
2648
|
else if (json.event === 'error') {
|
|
@@ -2537,6 +2685,8 @@ ${userRequestBody}`;
|
|
|
2537
2685
|
// separate the model's prose from a preceding tool result
|
|
2538
2686
|
// (the blob otherwise leaves them adjacent with no delimiter).
|
|
2539
2687
|
output += `\n💬 ${block.text}\n`;
|
|
2688
|
+
if (typeof block.text === 'string' && block.text.trim())
|
|
2689
|
+
lastAssistantText = block.text;
|
|
2540
2690
|
// Create structured output log
|
|
2541
2691
|
if (block.text.trim()) {
|
|
2542
2692
|
logs.push(createOutputLog(block.text, 'markdown', 'info', promptId));
|
|
@@ -2639,6 +2789,7 @@ ${userRequestBody}`;
|
|
|
2639
2789
|
}
|
|
2640
2790
|
if (textBuffer.trim()) {
|
|
2641
2791
|
logs.push(createOutputLog(textBuffer, 'markdown', 'info', promptId));
|
|
2792
|
+
lastAssistantText = textBuffer;
|
|
2642
2793
|
textBuffer = '';
|
|
2643
2794
|
}
|
|
2644
2795
|
if (toolInputBuffer.trim() && currentToolName) {
|
|
@@ -2739,9 +2890,44 @@ ${userRequestBody}`;
|
|
|
2739
2890
|
sendSpecialEvent('git_branch', { branch: currentBranch });
|
|
2740
2891
|
}
|
|
2741
2892
|
// Stream stdout
|
|
2893
|
+
// Watchdog for a CLI (or a tool it ran with a real TTY) that stops to ask a
|
|
2894
|
+
// question. A headless run can never answer, so after `silenceMs` of quiet
|
|
2895
|
+
// following a prompt-looking line we kill it and record WHY — the old
|
|
2896
|
+
// behaviour was to sit until the dispatch timeout and store an empty
|
|
2897
|
+
// result over a log of TUI redraw garbage (`eas login`, 2026-09).
|
|
2898
|
+
const stallSilenceMs = interactivePromptTimeoutMs();
|
|
2899
|
+
const stallDetector = new InteractiveStallDetector({
|
|
2900
|
+
silenceMs: stallSilenceMs,
|
|
2901
|
+
onStall: (label, tail) => {
|
|
2902
|
+
if (!runningProcesses.has(commandId))
|
|
2903
|
+
return;
|
|
2904
|
+
const message = describeInteractiveStall(label, tail, stallSilenceMs);
|
|
2905
|
+
logger.warn(`Interactive prompt detected (${label}) with no output for ${Math.round(stallSilenceMs / 1000)}s — killing run`);
|
|
2906
|
+
promptInteractiveStall.set(promptId, message);
|
|
2907
|
+
addLog(promptId, 'error', 'system', message);
|
|
2908
|
+
sendOutput(channel, commandId, 'stderr', `\n❌ ${message}\n`);
|
|
2909
|
+
try {
|
|
2910
|
+
claude.kill('SIGTERM');
|
|
2911
|
+
}
|
|
2912
|
+
catch { /* already gone */ }
|
|
2913
|
+
setTimeout(() => {
|
|
2914
|
+
if (runningProcesses.has(commandId)) {
|
|
2915
|
+
try {
|
|
2916
|
+
claude.kill('SIGKILL');
|
|
2917
|
+
}
|
|
2918
|
+
catch { /* already gone */ }
|
|
2919
|
+
}
|
|
2920
|
+
}, 5000).unref?.();
|
|
2921
|
+
},
|
|
2922
|
+
});
|
|
2742
2923
|
if (claude.stdout) {
|
|
2743
2924
|
claude.stdout.on('data', (data) => {
|
|
2744
|
-
const text = cliCommand === 'kimi'
|
|
2925
|
+
const text = cliCommand === 'kimi'
|
|
2926
|
+
? cleanKimiText(data.toString())
|
|
2927
|
+
: cliCommand === 'kiro'
|
|
2928
|
+
? cleanKiroText(data.toString())
|
|
2929
|
+
: data.toString();
|
|
2930
|
+
stallDetector.feed(data.toString());
|
|
2745
2931
|
// Skip logging if it looks like base64/binary data (long strings without spaces)
|
|
2746
2932
|
const preview = text.slice(0, 150);
|
|
2747
2933
|
const looksLikeBinary = preview.length > 50 && !preview.includes(' ') && /^[A-Za-z0-9+/=]+$/.test(preview.replace(/\s/g, ''));
|
|
@@ -2756,6 +2942,8 @@ ${userRequestBody}`;
|
|
|
2756
2942
|
// Only fallback to raw text if it wasn't valid JSON
|
|
2757
2943
|
// If it was valid JSON but produced no output (e.g., system init), don't send anything
|
|
2758
2944
|
const outputToSend = hadValidJson ? parsed : text;
|
|
2945
|
+
if (!hadValidJson)
|
|
2946
|
+
noteRawOutput(text);
|
|
2759
2947
|
if (outputToSend.trim()) {
|
|
2760
2948
|
logger.output('stdout', outputToSend.slice(0, 200));
|
|
2761
2949
|
sendOutput(channel, commandId, 'stdout', outputToSend);
|
|
@@ -2867,9 +3055,11 @@ ${userRequestBody}`;
|
|
|
2867
3055
|
if (claude.stderr) {
|
|
2868
3056
|
claude.stderr.on('data', (data) => {
|
|
2869
3057
|
const raw = data.toString();
|
|
3058
|
+
stallDetector.feed(raw);
|
|
2870
3059
|
const text = stripAnsiCodes(raw);
|
|
2871
3060
|
if (!text.trim())
|
|
2872
3061
|
return;
|
|
3062
|
+
noteRawOutput(text);
|
|
2873
3063
|
logger.info(`[STDERR] ${text.slice(0, 100)}`);
|
|
2874
3064
|
logger.output('stderr', text);
|
|
2875
3065
|
sendOutput(channel, commandId, 'stderr', text);
|
|
@@ -2885,6 +3075,7 @@ ${userRequestBody}`;
|
|
|
2885
3075
|
// Handle completion
|
|
2886
3076
|
claude.on('close', async (code) => {
|
|
2887
3077
|
runningProcesses.delete(commandId);
|
|
3078
|
+
stallDetector.stop();
|
|
2888
3079
|
// Remove the hoisted system-prompt temp file now that the CLI has read it.
|
|
2889
3080
|
if (sysPromptFilePath) {
|
|
2890
3081
|
try {
|
|
@@ -2946,7 +3137,12 @@ ${userRequestBody}`;
|
|
|
2946
3137
|
if (cliReportedFailure && (code ?? 0) === 0) {
|
|
2947
3138
|
logger.warn('CLI reported a failed run but exited 0 — recording it as failed');
|
|
2948
3139
|
}
|
|
2949
|
-
|
|
3140
|
+
// A run cut for waiting on a keyboard prompt is a failure whatever the
|
|
3141
|
+
// signal-death exit code says (a SIGTERM'd child reports code null).
|
|
3142
|
+
const interactiveStall = promptId ? promptInteractiveStall.get(promptId) : undefined;
|
|
3143
|
+
if (promptId)
|
|
3144
|
+
promptInteractiveStall.delete(promptId);
|
|
3145
|
+
const exitCode = (claudeApiError || cliReportedFailure || interactiveStall) ? (code || 1) : (code ?? 0);
|
|
2950
3146
|
const duration = Date.now() - startTime;
|
|
2951
3147
|
const status = exitCode === 0 ? 'completed' : 'failed';
|
|
2952
3148
|
if (exitCode === 0) {
|
|
@@ -2994,8 +3190,25 @@ ${userRequestBody}`;
|
|
|
2994
3190
|
promptCostCents.delete(promptId);
|
|
2995
3191
|
}
|
|
2996
3192
|
// Update prompt status in database with summary and usage (backend fix - don't rely on frontend)
|
|
2997
|
-
|
|
3193
|
+
// Prefer the CLI's own result event; when the run died without one
|
|
3194
|
+
// (killed, crashed, cut for a prompt) still store SOMETHING readable —
|
|
3195
|
+
// the stall explanation, the last assistant prose, or the raw tail —
|
|
3196
|
+
// so the dashboard never shows an empty result over a dead run.
|
|
3197
|
+
let finalResult = promptFinalResult.get(promptId);
|
|
2998
3198
|
promptFinalResult.delete(promptId);
|
|
3199
|
+
if (!finalResult || !finalResult.trim()) {
|
|
3200
|
+
if (interactiveStall) {
|
|
3201
|
+
finalResult = lastAssistantText.trim()
|
|
3202
|
+
? `${interactiveStall}\n\nLast assistant message:\n${lastAssistantText.trim()}`
|
|
3203
|
+
: interactiveStall;
|
|
3204
|
+
}
|
|
3205
|
+
else if (lastAssistantText.trim()) {
|
|
3206
|
+
finalResult = lastAssistantText.trim();
|
|
3207
|
+
}
|
|
3208
|
+
else if (rawOutputTail.trim()) {
|
|
3209
|
+
finalResult = `${status === 'failed' ? `Run failed (exit code ${exitCode}) without a result. ` : ''}Last output:\n${rawOutputTail.trim().split('\n').slice(-40).join('\n')}`;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
2999
3212
|
// Record WHICH CLI + model executed it (timeline/history). Model is
|
|
3000
3213
|
// best-effort — the CLI/proxy pick the true upstream model internally
|
|
3001
3214
|
// (batuta-auto fans out per prompt), so we report the CLI's MODEL CONFIG:
|
|
@@ -3005,13 +3218,16 @@ ${userRequestBody}`;
|
|
|
3005
3218
|
? 'kimi-cli'
|
|
3006
3219
|
: cliCommand === 'cursor'
|
|
3007
3220
|
? 'cursor-cli'
|
|
3008
|
-
: '
|
|
3221
|
+
: cliCommand === 'kiro'
|
|
3222
|
+
? 'kiro-cli'
|
|
3223
|
+
: 'claude-cli';
|
|
3009
3224
|
let modelForDb;
|
|
3010
3225
|
if (cliCommand === 'kimi')
|
|
3011
3226
|
modelForDb = 'kimi-for-coding';
|
|
3012
3227
|
// cursor-agent picks the model from the user's own Cursor settings and does
|
|
3013
|
-
// not report it in text mode, so leave it unset rather than guess.
|
|
3014
|
-
|
|
3228
|
+
// not report it in text mode, so leave it unset rather than guess. Same for
|
|
3229
|
+
// kiro-cli (its default model is set in the user's Kiro agent config).
|
|
3230
|
+
else if (cliCommand === 'cursor' || cliCommand === 'kiro')
|
|
3015
3231
|
modelForDb = undefined;
|
|
3016
3232
|
else if (cliCommand === 'orquesta')
|
|
3017
3233
|
modelForDb = (globalCliEndpoint === 'batuta' || !globalCliEndpoint) ? 'batuta-auto' : globalCliEndpoint;
|
|
@@ -3038,6 +3254,7 @@ ${userRequestBody}`;
|
|
|
3038
3254
|
// Handle errors
|
|
3039
3255
|
claude.on('error', async (err) => {
|
|
3040
3256
|
runningProcesses.delete(commandId);
|
|
3257
|
+
stallDetector.stop();
|
|
3041
3258
|
if (promptId)
|
|
3042
3259
|
promptIdToCommandId.delete(promptId);
|
|
3043
3260
|
pendingSupervisionCallbacks.delete(commandId);
|
|
@@ -3858,7 +4075,7 @@ export async function startSession(options) {
|
|
|
3858
4075
|
// Build environment with authentication
|
|
3859
4076
|
const env = {
|
|
3860
4077
|
...process.env,
|
|
3861
|
-
...injectedCredentials,
|
|
4078
|
+
...withSecretFileEnv(injectedCredentials, cwd),
|
|
3862
4079
|
// Don't set CI=true for interactive mode - we want full TTY behavior
|
|
3863
4080
|
};
|
|
3864
4081
|
if (anthropicApiKey) {
|
|
@@ -3888,10 +4105,12 @@ export async function startSession(options) {
|
|
|
3888
4105
|
? (resolveKimiBinary() || 'kimi')
|
|
3889
4106
|
: cliCommand === 'cursor'
|
|
3890
4107
|
? (resolveCursorBinary() || 'cursor-agent')
|
|
3891
|
-
: cliCommand === '
|
|
3892
|
-
? (
|
|
3893
|
-
: cliCommand
|
|
3894
|
-
|
|
4108
|
+
: cliCommand === 'kiro'
|
|
4109
|
+
? (resolveKiroBinary() || 'kiro-cli')
|
|
4110
|
+
: cliCommand === 'claude'
|
|
4111
|
+
? (resolveClaudeBinary() || 'claude')
|
|
4112
|
+
: cliCommand;
|
|
4113
|
+
const resolvedByPath = cliCommand === 'kimi' || cliCommand === 'cursor' || cliCommand === 'kiro';
|
|
3895
4114
|
logger.info(`Interactive session CLI: ${cliCommand}${resolvedByPath ? ` (${cliBinary})` : ''}`);
|
|
3896
4115
|
// Best-effort model + cli label reported in session:started so the dashboard
|
|
3897
4116
|
// can tag the interactive prompt (same mapping dispatched prompts use on
|
|
@@ -3899,8 +4118,8 @@ export async function startSession(options) {
|
|
|
3899
4118
|
// back to ANTHROPIC_MODEL when set, else undefined.
|
|
3900
4119
|
const sessionModel = cliCommand === 'kimi'
|
|
3901
4120
|
? 'kimi-for-coding'
|
|
3902
|
-
: cliCommand === 'cursor'
|
|
3903
|
-
// cursor-agent
|
|
4121
|
+
: cliCommand === 'cursor' || cliCommand === 'kiro'
|
|
4122
|
+
// cursor-agent / kiro-cli use whatever model the user selected in their own
|
|
3904
4123
|
// account; it isn't knowable from the PTY, so report nothing rather than lie.
|
|
3905
4124
|
? undefined
|
|
3906
4125
|
: cliCommand === 'orquesta'
|
|
@@ -3909,7 +4128,8 @@ export async function startSession(options) {
|
|
|
3909
4128
|
const sessionCliType = cliCommand === 'orquesta' ? 'orquesta'
|
|
3910
4129
|
: cliCommand === 'kimi' ? 'kimi'
|
|
3911
4130
|
: cliCommand === 'cursor' ? 'cursor'
|
|
3912
|
-
: '
|
|
4131
|
+
: cliCommand === 'kiro' ? 'kiro'
|
|
4132
|
+
: 'claude';
|
|
3913
4133
|
// Filter undefined env values — node-pty requires Record<string, string>.
|
|
3914
4134
|
// In strict sandbox mode, sandboxEnv() first reduces to the allowlist so the
|
|
3915
4135
|
// interactive CLI can't read unrelated host secrets from its own env.
|
|
@@ -3997,7 +4217,7 @@ export async function startSession(options) {
|
|
|
3997
4217
|
// `ps aux`. Default-off because a file unreadable inside bwrap would silently
|
|
3998
4218
|
// strip the system prompt (degraded session, not a crash) — flip the default
|
|
3999
4219
|
// only after E2E confirms the sandbox reads it. Rollback = unset the var.
|
|
4000
|
-
const wantSysPromptFile = cliCommand !== 'kimi' && cliCommand !== 'cursor' && ((process.platform === 'win32' && cliCommand === 'orquesta') ||
|
|
4220
|
+
const wantSysPromptFile = cliCommand !== 'kimi' && cliCommand !== 'cursor' && cliCommand !== 'kiro' && ((process.platform === 'win32' && cliCommand === 'orquesta') ||
|
|
4001
4221
|
/^(1|true|yes|on)$/i.test(process.env.ORQUESTA_SYSPROMPT_FILE || ''));
|
|
4002
4222
|
if (wantSysPromptFile) {
|
|
4003
4223
|
try {
|
|
@@ -4016,7 +4236,22 @@ export async function startSession(options) {
|
|
|
4016
4236
|
}
|
|
4017
4237
|
}
|
|
4018
4238
|
let ptyArgs;
|
|
4019
|
-
if (cliCommand === '
|
|
4239
|
+
if (cliCommand === 'kiro') {
|
|
4240
|
+
// kiro-cli has no --append-system-prompt either; it reads AGENTS.md /
|
|
4241
|
+
// steering files from the working dir. `chat` is the interactive verb and
|
|
4242
|
+
// `--trust-all-tools` its --dangerously-skip-permissions. Resume flags go
|
|
4243
|
+
// AFTER the verb (see below) — the generic unshift would put them before it.
|
|
4244
|
+
ptyArgs = ['chat', '--trust-all-tools'];
|
|
4245
|
+
if (resumeSessionId) {
|
|
4246
|
+
ptyArgs.push('--resume-id', resumeSessionId);
|
|
4247
|
+
logger.info(`[Session] Resuming conversation ${resumeSessionId} (--resume-id) for kiro`);
|
|
4248
|
+
}
|
|
4249
|
+
else if (resume) {
|
|
4250
|
+
ptyArgs.push('--resume');
|
|
4251
|
+
logger.info('[Session] Resuming most recent conversation in this directory (--resume) for kiro');
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
else if (cliCommand === 'cursor') {
|
|
4020
4255
|
// cursor-agent has no --append-system-prompt; it reads AGENTS.md / CLAUDE.md
|
|
4021
4256
|
// from the working dir (which the agent already syncs), so the capabilities
|
|
4022
4257
|
// block isn't injected. `--force` is its --dangerously-skip-permissions.
|
|
@@ -4039,7 +4274,7 @@ export async function startSession(options) {
|
|
|
4039
4274
|
// cli.ts) both reattach the latest session non-interactively; kimi has no
|
|
4040
4275
|
// resume flag, so the option is silently ignored there. Prepended so it's an
|
|
4041
4276
|
// unambiguous standalone flag, separate from --append-system-prompt's value.
|
|
4042
|
-
if ((resume || resumeSessionId) && cliCommand !== 'kimi') {
|
|
4277
|
+
if ((resume || resumeSessionId) && cliCommand !== 'kimi' && cliCommand !== 'kiro') {
|
|
4043
4278
|
// cursor-agent spells these the same way claude does (--continue / --resume <id>).
|
|
4044
4279
|
// claude can target a SPECIFIC past conversation by id (--resume <id>);
|
|
4045
4280
|
// orquesta-cli only reattaches the latest (--continue). So when a specific
|
|
@@ -4183,7 +4418,7 @@ export async function startSession(options) {
|
|
|
4183
4418
|
startTime,
|
|
4184
4419
|
subAgentId,
|
|
4185
4420
|
subAgentName,
|
|
4186
|
-
cliType:
|
|
4421
|
+
cliType: sessionCliType,
|
|
4187
4422
|
isActive: true,
|
|
4188
4423
|
pendingPhoneBlock: '',
|
|
4189
4424
|
pendingPhoneTimer: null,
|