orquesta-agent 0.2.246 → 0.2.249
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/coordination.d.ts +21 -1
- package/dist/coordination.d.ts.map +1 -1
- package/dist/coordination.js +123 -0
- package/dist/coordination.js.map +1 -1
- package/dist/executor.d.ts +11 -4
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +279 -42
- 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,12 +119,14 @@ 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);
|
|
125
127
|
import { isSandboxAvailable, buildBwrapArgs, shQuote, ensureStrictProjectDirs, encodeClaudeProjectDir, findStaleBinds, criticalBindPaths } from './sandbox.js';
|
|
126
128
|
import { isLoggedOutMessage } from './cli-auth-detect.js';
|
|
127
|
-
import { parseCoordSpec, runCoordination } from './coordination.js';
|
|
129
|
+
import { parseCoordSpec, runCoordination, runVideoCall } from './coordination.js';
|
|
128
130
|
import { parseSudosudoInstallSpec, runSudosudoInstall } from './sudosudo.js';
|
|
129
131
|
import { sendOutput, sendComplete, sendError, sendSupervisionRequest, sendExecutionResumed, updatePromptStatus, persistOutputLogs, clearOutputBuffer, sendRequirement, persistRequirement, sendQAInstructions, persistQAInstructions, sendPlanItemsGenerated, sendSessionOutput, sendSessionStarted, sendSessionEnded, sendSessionError, sendSessionLog, reportAgentError } from './supabase.js';
|
|
130
132
|
import { startTranscriptTail } from './claude-transcript.js';
|
|
@@ -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(', ')})` };
|
|
@@ -2083,12 +2208,14 @@ export async function execute(options) {
|
|
|
2083
2208
|
// the spec is absent or malformed.
|
|
2084
2209
|
const coordSpec = parseCoordSpec(content);
|
|
2085
2210
|
if (coordSpec) {
|
|
2086
|
-
|
|
2211
|
+
const isCall = coordSpec.mode === 'call';
|
|
2212
|
+
addLog(promptId, 'info', 'system', `Native ${isCall ? 'video call' : 'coordination'} (${coordSpec.reason}) — channel ${coordSpec.channelId}`);
|
|
2087
2213
|
try {
|
|
2088
2214
|
// The agent token isn't in process.env — it's injected per-execution
|
|
2089
2215
|
// (index.ts sets ORQUESTA_TOKEN on injectedCredentials before execute()).
|
|
2090
2216
|
const orquestaToken = getInjectedCredentials()['ORQUESTA_TOKEN'] || process.env.ORQUESTA_TOKEN || '';
|
|
2091
|
-
const
|
|
2217
|
+
const runner = isCall ? runVideoCall : runCoordination;
|
|
2218
|
+
const summary = await runner(coordSpec, orquestaToken, (line) => {
|
|
2092
2219
|
addLog(promptId, 'info', 'output', line);
|
|
2093
2220
|
sendOutput(channel, commandId, 'stdout', line + '\n').catch(() => undefined);
|
|
2094
2221
|
});
|
|
@@ -2098,7 +2225,7 @@ export async function execute(options) {
|
|
|
2098
2225
|
await sendComplete(channel, commandId, 0, startTime);
|
|
2099
2226
|
}
|
|
2100
2227
|
catch (err) {
|
|
2101
|
-
const msg =
|
|
2228
|
+
const msg = `${coordSpec.mode === 'call' ? 'Video call' : 'Coordination'} failed: ${err.message}`;
|
|
2102
2229
|
addLog(promptId, 'error', 'error', msg);
|
|
2103
2230
|
await updatePromptStatus(promptId, 'failed', { text: msg, key_actions: [], files_modified: [] });
|
|
2104
2231
|
await sendComplete(channel, commandId, 1, startTime);
|
|
@@ -2201,7 +2328,7 @@ ${userRequestBody}`;
|
|
|
2201
2328
|
// Build environment with authentication
|
|
2202
2329
|
const env = {
|
|
2203
2330
|
...process.env,
|
|
2204
|
-
...injectedCredentials, // Inject credentials from Orquesta
|
|
2331
|
+
...withSecretFileEnv(injectedCredentials, workingDirectory || process.cwd()), // Inject credentials from Orquesta
|
|
2205
2332
|
CI: 'true', // Ensure non-interactive mode
|
|
2206
2333
|
// Expose the current prompt id so Claude can link generated files back
|
|
2207
2334
|
// to this prompt via /api/agent/files (prompt_id=$ORQUESTA_PROMPT_ID).
|
|
@@ -2258,9 +2385,11 @@ ${userRequestBody}`;
|
|
|
2258
2385
|
? (resolveKimiBinary() || 'kimi')
|
|
2259
2386
|
: cliCommand === 'cursor'
|
|
2260
2387
|
? (resolveCursorBinary() || 'cursor-agent')
|
|
2261
|
-
: cliCommand === '
|
|
2262
|
-
? (
|
|
2263
|
-
:
|
|
2388
|
+
: cliCommand === 'kiro'
|
|
2389
|
+
? (resolveKiroBinary() || 'kiro-cli')
|
|
2390
|
+
: cliCommand === 'claude'
|
|
2391
|
+
? (resolveClaudeBinary() || 'claude')
|
|
2392
|
+
: (cliCommand || 'claude');
|
|
2264
2393
|
const cwd = workingDirectory || process.cwd();
|
|
2265
2394
|
// Which branch this prompt runs on — captured up front so it's persisted even
|
|
2266
2395
|
// if the run fails, and broadcast live below once sendSpecialEvent exists.
|
|
@@ -2272,16 +2401,16 @@ ${userRequestBody}`;
|
|
|
2272
2401
|
let detectedCommitSha = null;
|
|
2273
2402
|
let detectedCommitUrl = null;
|
|
2274
2403
|
logger.info(`CLI Selection: ${cliCommand} (${reason})`);
|
|
2275
|
-
logger.info(`Spawning: ${cliBinary} -p "${fullContent.slice(0, 50)}..." in ${cwd}${currentBranch ? ` on branch ${currentBranch}` : ''}`);
|
|
2404
|
+
logger.info(`Spawning: ${cliBinary} ${cliCommand === 'kiro' ? 'chat' : '-p'} "${fullContent.slice(0, 50)}..." in ${cwd}${currentBranch ? ` on branch ${currentBranch}` : ''}`);
|
|
2276
2405
|
// Hoist the static block into the CLI system prompt (claude/orquesta support
|
|
2277
2406
|
// --append-system-prompt-file; kimi has no such flag and stays inline). On any
|
|
2278
2407
|
// write failure we fall back to the inline fullContent, so a dispatched prompt
|
|
2279
2408
|
// can never regress to a worse state than before.
|
|
2280
2409
|
let promptContent = fullContent;
|
|
2281
2410
|
let sysPromptFilePath = null;
|
|
2282
|
-
// kimi
|
|
2283
|
-
// context stays inline in the prompt.
|
|
2284
|
-
if (cliCommand !== 'kimi' && cliCommand !== 'cursor') {
|
|
2411
|
+
// kimi, cursor-agent and kiro-cli have no --append-system-prompt-file, so
|
|
2412
|
+
// their static context stays inline in the prompt.
|
|
2413
|
+
if (cliCommand !== 'kimi' && cliCommand !== 'cursor' && cliCommand !== 'kiro') {
|
|
2285
2414
|
try {
|
|
2286
2415
|
const promptDir = process.platform === 'win32'
|
|
2287
2416
|
? os.tmpdir()
|
|
@@ -2314,22 +2443,29 @@ ${userRequestBody}`;
|
|
|
2314
2443
|
// below only understands Claude's and orquesta-cli's,
|
|
2315
2444
|
// so asking for it would give us an unparseable
|
|
2316
2445
|
// stream instead of an answer.
|
|
2317
|
-
: cliCommand === '
|
|
2318
|
-
? '--
|
|
2319
|
-
//
|
|
2320
|
-
//
|
|
2321
|
-
//
|
|
2322
|
-
|
|
2323
|
-
|
|
2446
|
+
: cliCommand === 'kiro'
|
|
2447
|
+
? '--no-interactive --wrap never' // Kiro: `chat` is interactive by default;
|
|
2448
|
+
// --no-interactive answers once and exits, --wrap
|
|
2449
|
+
// never stops it re-flowing the answer to the
|
|
2450
|
+
// (absent) terminal width.
|
|
2451
|
+
: cliCommand === 'kimi'
|
|
2452
|
+
? '--output-format text' // Kimi: plain text. NOTE: -p (one-shot) CANNOT be
|
|
2453
|
+
// combined with -y ("Cannot combine --prompt with
|
|
2454
|
+
// --yolo"), so dispatched kimi prompts respond as
|
|
2455
|
+
// chat (no autonomous tool use). Interactive kimi
|
|
2456
|
+
// sessions DO get -y (see ptyArgs below).
|
|
2457
|
+
: '--verbose --output-format stream-json'; // Claude: stream-json format
|
|
2324
2458
|
// Kimi has no --dangerously-skip-permissions, and -p forbids -y. Cursor spells
|
|
2325
|
-
// the same thing --force (alias --yolo).
|
|
2459
|
+
// the same thing --force (alias --yolo); kiro spells it --trust-all-tools.
|
|
2326
2460
|
const permFlags = cliCommand === 'kimi'
|
|
2327
2461
|
? ''
|
|
2328
2462
|
: mode !== 'auto'
|
|
2329
2463
|
? ''
|
|
2330
2464
|
: cliCommand === 'cursor'
|
|
2331
2465
|
? '--force'
|
|
2332
|
-
: '
|
|
2466
|
+
: cliCommand === 'kiro'
|
|
2467
|
+
? '--trust-all-tools'
|
|
2468
|
+
: '--dangerously-skip-permissions';
|
|
2333
2469
|
// Per-project endpoint override (orquesta CLI only). Single-quoted for the
|
|
2334
2470
|
// shell-wrapped (Linux) path; pushed as a separate arg on Win/macOS below.
|
|
2335
2471
|
const useEndpoint = cliCommand === 'orquesta' && !!globalCliEndpoint;
|
|
@@ -2341,7 +2477,10 @@ ${userRequestBody}`;
|
|
|
2341
2477
|
// Claude CLI's headless `-p` mode does not accept positional file args as
|
|
2342
2478
|
// attachments — image refs are surfaced inline in the prompt as absolute
|
|
2343
2479
|
// paths so Claude's Read tool can pick them up.
|
|
2344
|
-
|
|
2480
|
+
// kiro-cli has no `-p`: the one-shot form is `kiro-cli chat "<prompt>"` with
|
|
2481
|
+
// --no-interactive (baseFlags), the prompt as the verb's positional arg.
|
|
2482
|
+
const promptVerb = cliCommand === 'kiro' ? 'chat' : '-p';
|
|
2483
|
+
command = `${cliBinary} ${promptVerb} '${escapedContent}' ${permFlags} ${baseFlags} ${endpointFlag} ${sysPromptFlag}`;
|
|
2345
2484
|
if (imageReferences.length > 0) {
|
|
2346
2485
|
logger.info(`Image attachments referenced inline: ${imageReferences.length}`);
|
|
2347
2486
|
}
|
|
@@ -2354,7 +2493,7 @@ ${userRequestBody}`;
|
|
|
2354
2493
|
const isDarwin = process.platform === 'darwin';
|
|
2355
2494
|
let claude;
|
|
2356
2495
|
if (isWindows || isDarwin) {
|
|
2357
|
-
const args = [
|
|
2496
|
+
const args = [promptVerb];
|
|
2358
2497
|
// Windows cmd.exe caps the whole command line at ~8191 chars. The prompt is
|
|
2359
2498
|
// prefixed with large context blocks (capabilities, project CLAUDE.md,
|
|
2360
2499
|
// coordination…), so passing it as an argv overflows that limit and the CLI
|
|
@@ -2414,6 +2553,16 @@ ${userRequestBody}`;
|
|
|
2414
2553
|
let thinkingBuffer = '';
|
|
2415
2554
|
let textBuffer = '';
|
|
2416
2555
|
let toolInputBuffer = '';
|
|
2556
|
+
// Fallbacks for the stored result when the CLI dies without a result event
|
|
2557
|
+
// (killed, crashed, cut for an interactive prompt): the last prose the model
|
|
2558
|
+
// produced, else the tail of whatever raw output there was.
|
|
2559
|
+
let lastAssistantText = '';
|
|
2560
|
+
let rawOutputTail = '';
|
|
2561
|
+
const noteRawOutput = (text) => {
|
|
2562
|
+
const clean = stripAnsiForResult(text);
|
|
2563
|
+
if (clean.trim())
|
|
2564
|
+
rawOutputTail = (rawOutputTail + clean).slice(-4000);
|
|
2565
|
+
};
|
|
2417
2566
|
// Parse stream-json output format
|
|
2418
2567
|
// Returns { output: string, hadValidJson: boolean, logs: AgentLogEntry[] }
|
|
2419
2568
|
const parseStreamJson = (text) => {
|
|
@@ -2495,6 +2644,7 @@ ${userRequestBody}`;
|
|
|
2495
2644
|
output += `\n${content}\n`;
|
|
2496
2645
|
logs.push(createOutputLog(content, 'markdown', 'info', promptId));
|
|
2497
2646
|
promptFinalResult.set(promptId, String(content));
|
|
2647
|
+
lastAssistantText = String(content);
|
|
2498
2648
|
}
|
|
2499
2649
|
}
|
|
2500
2650
|
else if (json.event === 'error') {
|
|
@@ -2537,6 +2687,8 @@ ${userRequestBody}`;
|
|
|
2537
2687
|
// separate the model's prose from a preceding tool result
|
|
2538
2688
|
// (the blob otherwise leaves them adjacent with no delimiter).
|
|
2539
2689
|
output += `\n💬 ${block.text}\n`;
|
|
2690
|
+
if (typeof block.text === 'string' && block.text.trim())
|
|
2691
|
+
lastAssistantText = block.text;
|
|
2540
2692
|
// Create structured output log
|
|
2541
2693
|
if (block.text.trim()) {
|
|
2542
2694
|
logs.push(createOutputLog(block.text, 'markdown', 'info', promptId));
|
|
@@ -2639,6 +2791,7 @@ ${userRequestBody}`;
|
|
|
2639
2791
|
}
|
|
2640
2792
|
if (textBuffer.trim()) {
|
|
2641
2793
|
logs.push(createOutputLog(textBuffer, 'markdown', 'info', promptId));
|
|
2794
|
+
lastAssistantText = textBuffer;
|
|
2642
2795
|
textBuffer = '';
|
|
2643
2796
|
}
|
|
2644
2797
|
if (toolInputBuffer.trim() && currentToolName) {
|
|
@@ -2739,9 +2892,44 @@ ${userRequestBody}`;
|
|
|
2739
2892
|
sendSpecialEvent('git_branch', { branch: currentBranch });
|
|
2740
2893
|
}
|
|
2741
2894
|
// Stream stdout
|
|
2895
|
+
// Watchdog for a CLI (or a tool it ran with a real TTY) that stops to ask a
|
|
2896
|
+
// question. A headless run can never answer, so after `silenceMs` of quiet
|
|
2897
|
+
// following a prompt-looking line we kill it and record WHY — the old
|
|
2898
|
+
// behaviour was to sit until the dispatch timeout and store an empty
|
|
2899
|
+
// result over a log of TUI redraw garbage (`eas login`, 2026-09).
|
|
2900
|
+
const stallSilenceMs = interactivePromptTimeoutMs();
|
|
2901
|
+
const stallDetector = new InteractiveStallDetector({
|
|
2902
|
+
silenceMs: stallSilenceMs,
|
|
2903
|
+
onStall: (label, tail) => {
|
|
2904
|
+
if (!runningProcesses.has(commandId))
|
|
2905
|
+
return;
|
|
2906
|
+
const message = describeInteractiveStall(label, tail, stallSilenceMs);
|
|
2907
|
+
logger.warn(`Interactive prompt detected (${label}) with no output for ${Math.round(stallSilenceMs / 1000)}s — killing run`);
|
|
2908
|
+
promptInteractiveStall.set(promptId, message);
|
|
2909
|
+
addLog(promptId, 'error', 'system', message);
|
|
2910
|
+
sendOutput(channel, commandId, 'stderr', `\n❌ ${message}\n`);
|
|
2911
|
+
try {
|
|
2912
|
+
claude.kill('SIGTERM');
|
|
2913
|
+
}
|
|
2914
|
+
catch { /* already gone */ }
|
|
2915
|
+
setTimeout(() => {
|
|
2916
|
+
if (runningProcesses.has(commandId)) {
|
|
2917
|
+
try {
|
|
2918
|
+
claude.kill('SIGKILL');
|
|
2919
|
+
}
|
|
2920
|
+
catch { /* already gone */ }
|
|
2921
|
+
}
|
|
2922
|
+
}, 5000).unref?.();
|
|
2923
|
+
},
|
|
2924
|
+
});
|
|
2742
2925
|
if (claude.stdout) {
|
|
2743
2926
|
claude.stdout.on('data', (data) => {
|
|
2744
|
-
const text = cliCommand === 'kimi'
|
|
2927
|
+
const text = cliCommand === 'kimi'
|
|
2928
|
+
? cleanKimiText(data.toString())
|
|
2929
|
+
: cliCommand === 'kiro'
|
|
2930
|
+
? cleanKiroText(data.toString())
|
|
2931
|
+
: data.toString();
|
|
2932
|
+
stallDetector.feed(data.toString());
|
|
2745
2933
|
// Skip logging if it looks like base64/binary data (long strings without spaces)
|
|
2746
2934
|
const preview = text.slice(0, 150);
|
|
2747
2935
|
const looksLikeBinary = preview.length > 50 && !preview.includes(' ') && /^[A-Za-z0-9+/=]+$/.test(preview.replace(/\s/g, ''));
|
|
@@ -2756,6 +2944,8 @@ ${userRequestBody}`;
|
|
|
2756
2944
|
// Only fallback to raw text if it wasn't valid JSON
|
|
2757
2945
|
// If it was valid JSON but produced no output (e.g., system init), don't send anything
|
|
2758
2946
|
const outputToSend = hadValidJson ? parsed : text;
|
|
2947
|
+
if (!hadValidJson)
|
|
2948
|
+
noteRawOutput(text);
|
|
2759
2949
|
if (outputToSend.trim()) {
|
|
2760
2950
|
logger.output('stdout', outputToSend.slice(0, 200));
|
|
2761
2951
|
sendOutput(channel, commandId, 'stdout', outputToSend);
|
|
@@ -2867,9 +3057,11 @@ ${userRequestBody}`;
|
|
|
2867
3057
|
if (claude.stderr) {
|
|
2868
3058
|
claude.stderr.on('data', (data) => {
|
|
2869
3059
|
const raw = data.toString();
|
|
3060
|
+
stallDetector.feed(raw);
|
|
2870
3061
|
const text = stripAnsiCodes(raw);
|
|
2871
3062
|
if (!text.trim())
|
|
2872
3063
|
return;
|
|
3064
|
+
noteRawOutput(text);
|
|
2873
3065
|
logger.info(`[STDERR] ${text.slice(0, 100)}`);
|
|
2874
3066
|
logger.output('stderr', text);
|
|
2875
3067
|
sendOutput(channel, commandId, 'stderr', text);
|
|
@@ -2885,6 +3077,7 @@ ${userRequestBody}`;
|
|
|
2885
3077
|
// Handle completion
|
|
2886
3078
|
claude.on('close', async (code) => {
|
|
2887
3079
|
runningProcesses.delete(commandId);
|
|
3080
|
+
stallDetector.stop();
|
|
2888
3081
|
// Remove the hoisted system-prompt temp file now that the CLI has read it.
|
|
2889
3082
|
if (sysPromptFilePath) {
|
|
2890
3083
|
try {
|
|
@@ -2946,7 +3139,12 @@ ${userRequestBody}`;
|
|
|
2946
3139
|
if (cliReportedFailure && (code ?? 0) === 0) {
|
|
2947
3140
|
logger.warn('CLI reported a failed run but exited 0 — recording it as failed');
|
|
2948
3141
|
}
|
|
2949
|
-
|
|
3142
|
+
// A run cut for waiting on a keyboard prompt is a failure whatever the
|
|
3143
|
+
// signal-death exit code says (a SIGTERM'd child reports code null).
|
|
3144
|
+
const interactiveStall = promptId ? promptInteractiveStall.get(promptId) : undefined;
|
|
3145
|
+
if (promptId)
|
|
3146
|
+
promptInteractiveStall.delete(promptId);
|
|
3147
|
+
const exitCode = (claudeApiError || cliReportedFailure || interactiveStall) ? (code || 1) : (code ?? 0);
|
|
2950
3148
|
const duration = Date.now() - startTime;
|
|
2951
3149
|
const status = exitCode === 0 ? 'completed' : 'failed';
|
|
2952
3150
|
if (exitCode === 0) {
|
|
@@ -2994,8 +3192,25 @@ ${userRequestBody}`;
|
|
|
2994
3192
|
promptCostCents.delete(promptId);
|
|
2995
3193
|
}
|
|
2996
3194
|
// Update prompt status in database with summary and usage (backend fix - don't rely on frontend)
|
|
2997
|
-
|
|
3195
|
+
// Prefer the CLI's own result event; when the run died without one
|
|
3196
|
+
// (killed, crashed, cut for a prompt) still store SOMETHING readable —
|
|
3197
|
+
// the stall explanation, the last assistant prose, or the raw tail —
|
|
3198
|
+
// so the dashboard never shows an empty result over a dead run.
|
|
3199
|
+
let finalResult = promptFinalResult.get(promptId);
|
|
2998
3200
|
promptFinalResult.delete(promptId);
|
|
3201
|
+
if (!finalResult || !finalResult.trim()) {
|
|
3202
|
+
if (interactiveStall) {
|
|
3203
|
+
finalResult = lastAssistantText.trim()
|
|
3204
|
+
? `${interactiveStall}\n\nLast assistant message:\n${lastAssistantText.trim()}`
|
|
3205
|
+
: interactiveStall;
|
|
3206
|
+
}
|
|
3207
|
+
else if (lastAssistantText.trim()) {
|
|
3208
|
+
finalResult = lastAssistantText.trim();
|
|
3209
|
+
}
|
|
3210
|
+
else if (rawOutputTail.trim()) {
|
|
3211
|
+
finalResult = `${status === 'failed' ? `Run failed (exit code ${exitCode}) without a result. ` : ''}Last output:\n${rawOutputTail.trim().split('\n').slice(-40).join('\n')}`;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
2999
3214
|
// Record WHICH CLI + model executed it (timeline/history). Model is
|
|
3000
3215
|
// best-effort — the CLI/proxy pick the true upstream model internally
|
|
3001
3216
|
// (batuta-auto fans out per prompt), so we report the CLI's MODEL CONFIG:
|
|
@@ -3005,13 +3220,16 @@ ${userRequestBody}`;
|
|
|
3005
3220
|
? 'kimi-cli'
|
|
3006
3221
|
: cliCommand === 'cursor'
|
|
3007
3222
|
? 'cursor-cli'
|
|
3008
|
-
: '
|
|
3223
|
+
: cliCommand === 'kiro'
|
|
3224
|
+
? 'kiro-cli'
|
|
3225
|
+
: 'claude-cli';
|
|
3009
3226
|
let modelForDb;
|
|
3010
3227
|
if (cliCommand === 'kimi')
|
|
3011
3228
|
modelForDb = 'kimi-for-coding';
|
|
3012
3229
|
// 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
|
-
|
|
3230
|
+
// not report it in text mode, so leave it unset rather than guess. Same for
|
|
3231
|
+
// kiro-cli (its default model is set in the user's Kiro agent config).
|
|
3232
|
+
else if (cliCommand === 'cursor' || cliCommand === 'kiro')
|
|
3015
3233
|
modelForDb = undefined;
|
|
3016
3234
|
else if (cliCommand === 'orquesta')
|
|
3017
3235
|
modelForDb = (globalCliEndpoint === 'batuta' || !globalCliEndpoint) ? 'batuta-auto' : globalCliEndpoint;
|
|
@@ -3038,6 +3256,7 @@ ${userRequestBody}`;
|
|
|
3038
3256
|
// Handle errors
|
|
3039
3257
|
claude.on('error', async (err) => {
|
|
3040
3258
|
runningProcesses.delete(commandId);
|
|
3259
|
+
stallDetector.stop();
|
|
3041
3260
|
if (promptId)
|
|
3042
3261
|
promptIdToCommandId.delete(promptId);
|
|
3043
3262
|
pendingSupervisionCallbacks.delete(commandId);
|
|
@@ -3858,7 +4077,7 @@ export async function startSession(options) {
|
|
|
3858
4077
|
// Build environment with authentication
|
|
3859
4078
|
const env = {
|
|
3860
4079
|
...process.env,
|
|
3861
|
-
...injectedCredentials,
|
|
4080
|
+
...withSecretFileEnv(injectedCredentials, cwd),
|
|
3862
4081
|
// Don't set CI=true for interactive mode - we want full TTY behavior
|
|
3863
4082
|
};
|
|
3864
4083
|
if (anthropicApiKey) {
|
|
@@ -3888,10 +4107,12 @@ export async function startSession(options) {
|
|
|
3888
4107
|
? (resolveKimiBinary() || 'kimi')
|
|
3889
4108
|
: cliCommand === 'cursor'
|
|
3890
4109
|
? (resolveCursorBinary() || 'cursor-agent')
|
|
3891
|
-
: cliCommand === '
|
|
3892
|
-
? (
|
|
3893
|
-
: cliCommand
|
|
3894
|
-
|
|
4110
|
+
: cliCommand === 'kiro'
|
|
4111
|
+
? (resolveKiroBinary() || 'kiro-cli')
|
|
4112
|
+
: cliCommand === 'claude'
|
|
4113
|
+
? (resolveClaudeBinary() || 'claude')
|
|
4114
|
+
: cliCommand;
|
|
4115
|
+
const resolvedByPath = cliCommand === 'kimi' || cliCommand === 'cursor' || cliCommand === 'kiro';
|
|
3895
4116
|
logger.info(`Interactive session CLI: ${cliCommand}${resolvedByPath ? ` (${cliBinary})` : ''}`);
|
|
3896
4117
|
// Best-effort model + cli label reported in session:started so the dashboard
|
|
3897
4118
|
// can tag the interactive prompt (same mapping dispatched prompts use on
|
|
@@ -3899,8 +4120,8 @@ export async function startSession(options) {
|
|
|
3899
4120
|
// back to ANTHROPIC_MODEL when set, else undefined.
|
|
3900
4121
|
const sessionModel = cliCommand === 'kimi'
|
|
3901
4122
|
? 'kimi-for-coding'
|
|
3902
|
-
: cliCommand === 'cursor'
|
|
3903
|
-
// cursor-agent
|
|
4123
|
+
: cliCommand === 'cursor' || cliCommand === 'kiro'
|
|
4124
|
+
// cursor-agent / kiro-cli use whatever model the user selected in their own
|
|
3904
4125
|
// account; it isn't knowable from the PTY, so report nothing rather than lie.
|
|
3905
4126
|
? undefined
|
|
3906
4127
|
: cliCommand === 'orquesta'
|
|
@@ -3909,7 +4130,8 @@ export async function startSession(options) {
|
|
|
3909
4130
|
const sessionCliType = cliCommand === 'orquesta' ? 'orquesta'
|
|
3910
4131
|
: cliCommand === 'kimi' ? 'kimi'
|
|
3911
4132
|
: cliCommand === 'cursor' ? 'cursor'
|
|
3912
|
-
: '
|
|
4133
|
+
: cliCommand === 'kiro' ? 'kiro'
|
|
4134
|
+
: 'claude';
|
|
3913
4135
|
// Filter undefined env values — node-pty requires Record<string, string>.
|
|
3914
4136
|
// In strict sandbox mode, sandboxEnv() first reduces to the allowlist so the
|
|
3915
4137
|
// interactive CLI can't read unrelated host secrets from its own env.
|
|
@@ -3997,7 +4219,7 @@ export async function startSession(options) {
|
|
|
3997
4219
|
// `ps aux`. Default-off because a file unreadable inside bwrap would silently
|
|
3998
4220
|
// strip the system prompt (degraded session, not a crash) — flip the default
|
|
3999
4221
|
// only after E2E confirms the sandbox reads it. Rollback = unset the var.
|
|
4000
|
-
const wantSysPromptFile = cliCommand !== 'kimi' && cliCommand !== 'cursor' && ((process.platform === 'win32' && cliCommand === 'orquesta') ||
|
|
4222
|
+
const wantSysPromptFile = cliCommand !== 'kimi' && cliCommand !== 'cursor' && cliCommand !== 'kiro' && ((process.platform === 'win32' && cliCommand === 'orquesta') ||
|
|
4001
4223
|
/^(1|true|yes|on)$/i.test(process.env.ORQUESTA_SYSPROMPT_FILE || ''));
|
|
4002
4224
|
if (wantSysPromptFile) {
|
|
4003
4225
|
try {
|
|
@@ -4016,7 +4238,22 @@ export async function startSession(options) {
|
|
|
4016
4238
|
}
|
|
4017
4239
|
}
|
|
4018
4240
|
let ptyArgs;
|
|
4019
|
-
if (cliCommand === '
|
|
4241
|
+
if (cliCommand === 'kiro') {
|
|
4242
|
+
// kiro-cli has no --append-system-prompt either; it reads AGENTS.md /
|
|
4243
|
+
// steering files from the working dir. `chat` is the interactive verb and
|
|
4244
|
+
// `--trust-all-tools` its --dangerously-skip-permissions. Resume flags go
|
|
4245
|
+
// AFTER the verb (see below) — the generic unshift would put them before it.
|
|
4246
|
+
ptyArgs = ['chat', '--trust-all-tools'];
|
|
4247
|
+
if (resumeSessionId) {
|
|
4248
|
+
ptyArgs.push('--resume-id', resumeSessionId);
|
|
4249
|
+
logger.info(`[Session] Resuming conversation ${resumeSessionId} (--resume-id) for kiro`);
|
|
4250
|
+
}
|
|
4251
|
+
else if (resume) {
|
|
4252
|
+
ptyArgs.push('--resume');
|
|
4253
|
+
logger.info('[Session] Resuming most recent conversation in this directory (--resume) for kiro');
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
else if (cliCommand === 'cursor') {
|
|
4020
4257
|
// cursor-agent has no --append-system-prompt; it reads AGENTS.md / CLAUDE.md
|
|
4021
4258
|
// from the working dir (which the agent already syncs), so the capabilities
|
|
4022
4259
|
// block isn't injected. `--force` is its --dangerously-skip-permissions.
|
|
@@ -4039,7 +4276,7 @@ export async function startSession(options) {
|
|
|
4039
4276
|
// cli.ts) both reattach the latest session non-interactively; kimi has no
|
|
4040
4277
|
// resume flag, so the option is silently ignored there. Prepended so it's an
|
|
4041
4278
|
// unambiguous standalone flag, separate from --append-system-prompt's value.
|
|
4042
|
-
if ((resume || resumeSessionId) && cliCommand !== 'kimi') {
|
|
4279
|
+
if ((resume || resumeSessionId) && cliCommand !== 'kimi' && cliCommand !== 'kiro') {
|
|
4043
4280
|
// cursor-agent spells these the same way claude does (--continue / --resume <id>).
|
|
4044
4281
|
// claude can target a SPECIFIC past conversation by id (--resume <id>);
|
|
4045
4282
|
// orquesta-cli only reattaches the latest (--continue). So when a specific
|
|
@@ -4183,7 +4420,7 @@ export async function startSession(options) {
|
|
|
4183
4420
|
startTime,
|
|
4184
4421
|
subAgentId,
|
|
4185
4422
|
subAgentName,
|
|
4186
|
-
cliType:
|
|
4423
|
+
cliType: sessionCliType,
|
|
4187
4424
|
isActive: true,
|
|
4188
4425
|
pendingPhoneBlock: '',
|
|
4189
4426
|
pendingPhoneTimer: null,
|