orquesta-agent 0.2.234 → 0.2.238

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.
Files changed (43) hide show
  1. package/README.md +45 -0
  2. package/dist/cli-auth-detect.d.ts +17 -0
  3. package/dist/cli-auth-detect.d.ts.map +1 -0
  4. package/dist/cli-auth-detect.js +31 -0
  5. package/dist/cli-auth-detect.js.map +1 -0
  6. package/dist/executor.d.ts +13 -4
  7. package/dist/executor.d.ts.map +1 -1
  8. package/dist/executor.js +257 -68
  9. package/dist/executor.js.map +1 -1
  10. package/dist/hook.d.ts +26 -1
  11. package/dist/hook.d.ts.map +1 -1
  12. package/dist/hook.js +170 -21
  13. package/dist/hook.js.map +1 -1
  14. package/dist/index.js +28 -14
  15. package/dist/index.js.map +1 -1
  16. package/dist/init.d.ts +42 -0
  17. package/dist/init.d.ts.map +1 -1
  18. package/dist/init.js +100 -28
  19. package/dist/init.js.map +1 -1
  20. package/dist/sandbox.d.ts.map +1 -1
  21. package/dist/sandbox.js +4 -0
  22. package/dist/sandbox.js.map +1 -1
  23. package/dist/supabase.d.ts +18 -8
  24. package/dist/supabase.d.ts.map +1 -1
  25. package/dist/supabase.js +36 -2
  26. package/dist/supabase.js.map +1 -1
  27. package/dist/types/hook-types.d.ts +49 -1
  28. package/dist/types/hook-types.d.ts.map +1 -1
  29. package/dist/ui/onboarding.js +1 -1
  30. package/dist/ui/onboarding.js.map +1 -1
  31. package/dist/ui/public/app.js +11 -1
  32. package/dist/ui/public/index.html +1 -0
  33. package/dist/ui/public/style.css +1 -0
  34. package/dist/ui/public/welcome.html +2 -0
  35. package/dist/ui/server.d.ts +1 -1
  36. package/dist/ui/server.d.ts.map +1 -1
  37. package/dist/ui/server.js +2 -2
  38. package/dist/ui/server.js.map +1 -1
  39. package/dist/ui/types.d.ts +1 -1
  40. package/dist/ui/types.d.ts.map +1 -1
  41. package/dist/ws-client.d.ts +1 -1
  42. package/dist/ws-client.d.ts.map +1 -1
  43. package/package.json +1 -1
package/dist/executor.js CHANGED
@@ -122,11 +122,12 @@ import * as logger from './logger.js';
122
122
  // interactive-session spawn — node-pty can't launch the `.cmd` shim).
123
123
  const nodeRequire = createRequire(import.meta.url);
124
124
  import { isSandboxAvailable, buildBwrapArgs, shQuote, ensureStrictProjectDirs, encodeClaudeProjectDir } from './sandbox.js';
125
+ import { isLoggedOutMessage } from './cli-auth-detect.js';
125
126
  import { parseCoordSpec, runCoordination } from './coordination.js';
126
127
  import { parseSudosudoInstallSpec, runSudosudoInstall } from './sudosudo.js';
127
128
  import { sendOutput, sendComplete, sendError, sendSupervisionRequest, sendExecutionResumed, updatePromptStatus, persistOutputLogs, clearOutputBuffer, sendRequirement, persistRequirement, sendQAInstructions, persistQAInstructions, sendPlanItemsGenerated, sendSessionOutput, sendSessionStarted, sendSessionEnded, sendSessionError, sendSessionLog, reportAgentError } from './supabase.js';
128
129
  import { startTranscriptTail } from './claude-transcript.js';
129
- import { createThinkingLog, createToolCallLog, createToolResultLog, createOutputLog, createErrorLog, createSystemLog, } from './types/agent-logs.js';
130
+ import { createThinkingLog, createToolCallLog, createToolResultLog, createOutputLog, createErrorLog, createSystemLog, createProgressLog, } from './types/agent-logs.js';
130
131
  /**
131
132
  * Build args for the `script` PTY wrapper, accounting for BSD vs util-linux differences.
132
133
  * - util-linux (Linux): `script -q -c "<cmd>" /dev/null`
@@ -874,6 +875,101 @@ export function resolveKimiBinary() {
874
875
  export function isKimiCliAvailable() {
875
876
  return resolveKimiBinary() !== null;
876
877
  }
878
+ // Resolve Cursor's headless CLI (`cursor-agent`). Its installer
879
+ // (`curl https://cursor.com/install -fsS | bash`) drops the binary in
880
+ // ~/.local/bin and only PRINTS a reminder to add that to PATH — so on a machine
881
+ // where the user never edited their shell profile, a bare `cursor-agent` lookup
882
+ // fails while the CLI is in fact installed. Same trap as kimi.
883
+ // Order: CURSOR_AGENT_PATH → on PATH → ~/.local/bin/cursor-agent.
884
+ // On Windows the PowerShell installer does put it on PATH, so the PATH probe is
885
+ // the working path there.
886
+ let cursorBinaryCache = null;
887
+ export function resolveCursorBinary() {
888
+ const now = Date.now();
889
+ if (cursorBinaryCache && now - cursorBinaryCache.at < CLI_DETECTION_TTL_MS)
890
+ return cursorBinaryCache.path;
891
+ let resolved = null;
892
+ const envPath = process.env.CURSOR_AGENT_PATH;
893
+ if (envPath) {
894
+ try {
895
+ fs.accessSync(envPath, fs.constants.X_OK);
896
+ resolved = envPath;
897
+ }
898
+ catch { /* bad override */ }
899
+ }
900
+ if (!resolved) {
901
+ try {
902
+ execSync('cursor-agent --version', { stdio: 'pipe', timeout: 15000 });
903
+ resolved = 'cursor-agent';
904
+ }
905
+ catch { /* not on PATH */ }
906
+ }
907
+ if (!resolved) {
908
+ const candidate = path.join(os.homedir(), '.local', 'bin', 'cursor-agent');
909
+ try {
910
+ fs.accessSync(candidate, fs.constants.X_OK);
911
+ resolved = candidate;
912
+ }
913
+ catch { /* not installed there */ }
914
+ }
915
+ cursorBinaryCache = { path: resolved, at: now };
916
+ return resolved;
917
+ }
918
+ // Cursor CLI (the `cursor-agent` binary). Authenticated separately with
919
+ // `cursor-agent login` against the user's own Cursor subscription — Orquesta
920
+ // never sees that credential.
921
+ export function isCursorCliAvailable() {
922
+ return resolveCursorBinary() !== null;
923
+ }
924
+ // Is cursor-agent actually LOGGED IN, not just installed?
925
+ //
926
+ // Installed-but-logged-out is a real state and used to be invisible: the agent
927
+ // reported `authenticated: available`, so the dashboard showed a green tick, the
928
+ // user pinned Cursor, and every prompt failed at execution with cursor's own
929
+ // auth error. `cursor-agent status` is the documented way to ask.
930
+ //
931
+ // Deliberately conservative: it reports NOT authenticated only when cursor says
932
+ // so in words. Any other outcome (an older build with no `status` subcommand, a
933
+ // timeout, an unparseable answer) reports authenticated with a null method —
934
+ // crying wolf on a working machine is worse than the tick we had before, and
935
+ // nothing about EXECUTION depends on this flag; selectCli() still keys purely on
936
+ // the binary existing.
937
+ let cursorAuthCache = null;
938
+ export function checkCursorAuth() {
939
+ const now = Date.now();
940
+ if (cursorAuthCache && now - cursorAuthCache.at < CLI_DETECTION_TTL_MS)
941
+ return cursorAuthCache.result;
942
+ const cache = (result) => {
943
+ cursorAuthCache = { result, at: now };
944
+ return result;
945
+ };
946
+ const binary = resolveCursorBinary();
947
+ if (!binary)
948
+ return cache({ available: false, authenticated: false, method: null });
949
+ // An API key in the environment authenticates cursor-agent on its own, and is
950
+ // what CI machines use — no point spawning a process to confirm it.
951
+ if (process.env.CURSOR_API_KEY)
952
+ return cache({ available: true, authenticated: true, method: 'api_key' });
953
+ try {
954
+ const out = execSync(`"${binary}" status`, {
955
+ stdio: ['ignore', 'pipe', 'pipe'],
956
+ timeout: 15000,
957
+ encoding: 'utf-8',
958
+ });
959
+ if (isLoggedOutMessage(out))
960
+ return cache({ available: true, authenticated: false, method: null });
961
+ return cache({ available: true, authenticated: true, method: 'subscription' });
962
+ }
963
+ catch (error) {
964
+ // Non-zero exit covers both "logged out" and "this build has no `status`",
965
+ // so the OUTPUT decides, not the exit code.
966
+ const err = error;
967
+ const combined = `${err.stdout ?? ''}${err.stderr ?? ''}`;
968
+ if (isLoggedOutMessage(combined))
969
+ return cache({ available: true, authenticated: false, method: null });
970
+ return cache({ available: true, authenticated: true, method: null });
971
+ }
972
+ }
877
973
  // Dedupe the fallback warning: selectCli() runs on every heartbeat (the agent
878
974
  // reports activeCli), so logging unconditionally spammed thousands of identical
879
975
  // WARN lines. Only log when the message actually changes.
@@ -884,53 +980,48 @@ function warnCliOnce(msg) {
884
980
  lastCliWarn = msg;
885
981
  logger.warn(msg);
886
982
  }
983
+ // Preference order when nothing is pinned, and the order a fallback walks when
984
+ // the pinned CLI turns out not to be installed. Table-driven rather than a nest
985
+ // of pairwise ifs: every CLI added here otherwise costs another N branches, and
986
+ // the pair that got missed is exactly how a preference silently no-ops.
987
+ const CLI_AUTO_ORDER = ['orquesta', 'claude', 'kimi', 'cursor'];
988
+ const CLI_AUTO_REASON = {
989
+ orquesta: 'Auto: orquesta available (local LLM)',
990
+ claude: 'Auto: claude available (Anthropic API)',
991
+ kimi: 'Auto: kimi available',
992
+ cursor: 'Auto: cursor-agent available',
993
+ };
887
994
  // Select which CLI to use based on availability and preference
888
995
  export function selectCli() {
889
- const hasOrquesta = isOrquestaCliAvailable();
890
- const hasClaude = isClaudeCliAvailable();
891
- const hasKimi = isKimiCliAvailable();
892
- // If none available, return null
893
- if (!hasOrquesta && !hasClaude && !hasKimi) {
894
- return { cli: null, reason: 'No CLI found (orquesta, claude, kimi)' };
996
+ const available = {
997
+ orquesta: isOrquestaCliAvailable(),
998
+ claude: isClaudeCliAvailable(),
999
+ kimi: isKimiCliAvailable(),
1000
+ cursor: isCursorCliAvailable(),
1001
+ };
1002
+ if (!CLI_AUTO_ORDER.some(cli => available[cli])) {
1003
+ return { cli: null, reason: `No CLI found (${CLI_AUTO_ORDER.join(', ')})` };
895
1004
  }
1005
+ const preferred = globalCliPreference;
896
1006
  // Explicit preference, available → use it.
897
- if (globalCliPreference === 'orquesta' && hasOrquesta)
898
- return { cli: 'orquesta', reason: 'User preference: orquesta' };
899
- if (globalCliPreference === 'claude' && hasClaude)
900
- return { cli: 'claude', reason: 'User preference: claude' };
901
- if (globalCliPreference === 'kimi' && hasKimi)
902
- return { cli: 'kimi', reason: 'User preference: kimi' };
903
- // Explicit preference, NOT available → warn once + fall back (orquesta → claude → kimi).
904
- if (globalCliPreference === 'kimi' && !hasKimi) {
905
- warnCliOnce('Kimi CLI preferred but not available, falling back');
906
- if (hasOrquesta)
907
- return { cli: 'orquesta', reason: 'Fallback: kimi not available' };
908
- if (hasClaude)
909
- return { cli: 'claude', reason: 'Fallback: kimi not available' };
910
- }
911
- if (globalCliPreference === 'orquesta' && !hasOrquesta) {
912
- warnCliOnce('Orquesta CLI preferred but not available, falling back');
913
- if (hasClaude)
914
- return { cli: 'claude', reason: 'Fallback: orquesta not available' };
915
- if (hasKimi)
916
- return { cli: 'kimi', reason: 'Fallback: orquesta not available' };
917
- }
918
- if (globalCliPreference === 'claude' && !hasClaude) {
919
- warnCliOnce('Claude CLI preferred but not available, falling back');
920
- if (hasOrquesta)
921
- return { cli: 'orquesta', reason: 'Fallback: claude not available' };
922
- if (hasKimi)
923
- return { cli: 'kimi', reason: 'Fallback: claude not available' };
924
- }
925
- // Auto mode: prefer orquesta → claude → kimi.
926
- if (hasOrquesta) {
1007
+ if (preferred !== 'auto' && available[preferred]) {
1008
+ lastCliWarn = null;
1009
+ return { cli: preferred, reason: `User preference: ${preferred}` };
1010
+ }
1011
+ // Explicit preference, NOT available → warn once, then fall through to the
1012
+ // auto order skipping the one that isn't there.
1013
+ if (preferred !== 'auto') {
1014
+ warnCliOnce(`${preferred} CLI preferred but not available, falling back`);
1015
+ const fallback = CLI_AUTO_ORDER.find(cli => cli !== preferred && available[cli]);
1016
+ if (fallback)
1017
+ return { cli: fallback, reason: `Fallback: ${preferred} not available` };
1018
+ return { cli: null, reason: 'No CLI found' };
1019
+ }
1020
+ const auto = CLI_AUTO_ORDER.find(cli => available[cli]);
1021
+ if (auto) {
927
1022
  lastCliWarn = null;
928
- return { cli: 'orquesta', reason: 'Auto: orquesta available (local LLM)' };
1023
+ return { cli: auto, reason: CLI_AUTO_REASON[auto] };
929
1024
  }
930
- if (hasClaude)
931
- return { cli: 'claude', reason: 'Auto: claude available (Anthropic API)' };
932
- if (hasKimi)
933
- return { cli: 'kimi', reason: 'Auto: kimi available' };
934
1025
  return { cli: null, reason: 'No CLI found' };
935
1026
  }
936
1027
  // Check if Claude CLI is authenticated
@@ -1762,6 +1853,36 @@ export function getCurrentGitBranch(cwd) {
1762
1853
  return null;
1763
1854
  }
1764
1855
  }
1856
+ // Args for a file-editing tool, trimmed so the live stream can show WHAT
1857
+ // changed without carrying a whole file through the socket. Returns null for
1858
+ // anything that isn't a file edit — those args stay dropped when oversized.
1859
+ const EDIT_SIDE_CAP = 800;
1860
+ export function compactFileEditArgs(args) {
1861
+ if (!args || typeof args !== 'object')
1862
+ return null;
1863
+ const a = args;
1864
+ const path = a.file_path ?? a.filePath ?? a.path;
1865
+ if (typeof path !== 'string' || !path)
1866
+ return null;
1867
+ const trim = (v) => {
1868
+ if (typeof v !== 'string')
1869
+ return undefined;
1870
+ return v.length > EDIT_SIDE_CAP ? v.slice(0, EDIT_SIDE_CAP) + '\n…[truncated]' : v;
1871
+ };
1872
+ const out = { file_path: path };
1873
+ const oldStr = trim(a.old_string ?? a.oldString);
1874
+ const newStr = trim(a.new_string ?? a.newString ?? a.content);
1875
+ if (oldStr !== undefined)
1876
+ out.old_string = oldStr;
1877
+ if (newStr !== undefined)
1878
+ out.new_string = newStr;
1879
+ if (typeof a.reason === 'string')
1880
+ out.reason = a.reason.slice(0, 200);
1881
+ // A bare path (read/list) carries no diff — not worth a payload.
1882
+ if (oldStr === undefined && newStr === undefined)
1883
+ return null;
1884
+ return JSON.stringify(out, null, 2);
1885
+ }
1765
1886
  export async function execute(options) {
1766
1887
  const { commandId, promptId, content, workingDirectory, channel, permissionMode, attachments } = options;
1767
1888
  const startTime = Date.now();
@@ -1959,13 +2080,21 @@ ${userRequestBody}`;
1959
2080
  // (cliCommand stays the TYPE for flag decisions.)
1960
2081
  const cliBinary = cliCommand === 'kimi'
1961
2082
  ? (resolveKimiBinary() || 'kimi')
1962
- : cliCommand === 'claude'
1963
- ? (resolveClaudeBinary() || 'claude')
1964
- : (cliCommand || 'claude');
2083
+ : cliCommand === 'cursor'
2084
+ ? (resolveCursorBinary() || 'cursor-agent')
2085
+ : cliCommand === 'claude'
2086
+ ? (resolveClaudeBinary() || 'claude')
2087
+ : (cliCommand || 'claude');
1965
2088
  const cwd = workingDirectory || process.cwd();
1966
2089
  // Which branch this prompt runs on — captured up front so it's persisted even
1967
2090
  // if the run fails, and broadcast live below once sendSpecialEvent exists.
1968
2091
  const currentBranch = getCurrentGitBranch(cwd);
2092
+ // Last commit this run produced. Detected from the CLI's own output further
2093
+ // down; it used to be broadcast live and then dropped on the floor, so a run
2094
+ // that committed AND pushed still left prompts.git_commit_sha NULL and the UI
2095
+ // had no way to link the change it just shipped.
2096
+ let detectedCommitSha = null;
2097
+ let detectedCommitUrl = null;
1969
2098
  logger.info(`CLI Selection: ${cliCommand} (${reason})`);
1970
2099
  logger.info(`Spawning: ${cliBinary} -p "${fullContent.slice(0, 50)}..." in ${cwd}${currentBranch ? ` on branch ${currentBranch}` : ''}`);
1971
2100
  // Hoist the static block into the CLI system prompt (claude/orquesta support
@@ -1974,7 +2103,9 @@ ${userRequestBody}`;
1974
2103
  // can never regress to a worse state than before.
1975
2104
  let promptContent = fullContent;
1976
2105
  let sysPromptFilePath = null;
1977
- if (cliCommand !== 'kimi') {
2106
+ // kimi and cursor-agent have no --append-system-prompt-file, so their static
2107
+ // context stays inline in the prompt.
2108
+ if (cliCommand !== 'kimi' && cliCommand !== 'cursor') {
1978
2109
  try {
1979
2110
  const promptDir = process.platform === 'win32'
1980
2111
  ? os.tmpdir()
@@ -2001,15 +2132,28 @@ ${userRequestBody}`;
2001
2132
  // Orquesta CLI doesn't need special format flags - it outputs directly
2002
2133
  const baseFlags = cliCommand === 'orquesta'
2003
2134
  ? '--verbose' // Orquesta: simple verbose output
2004
- : cliCommand === 'kimi'
2005
- ? '--output-format text' // Kimi: plain text. NOTE: -p (one-shot) CANNOT be
2006
- // combined with -y ("Cannot combine --prompt with
2007
- // --yolo"), so dispatched kimi prompts respond as
2008
- // chat (no autonomous tool use). Interactive kimi
2009
- // sessions DO get -y (see ptyArgs below).
2010
- : '--verbose --output-format stream-json'; // Claude: stream-json format
2011
- // Kimi has no --dangerously-skip-permissions, and -p forbids -y.
2012
- const permFlags = cliCommand === 'kimi' ? '' : (mode === 'auto' ? '--dangerously-skip-permissions' : '');
2135
+ : cliCommand === 'cursor'
2136
+ ? '--output-format text' // Cursor: plain text. Its stream-json events are a
2137
+ // different shape from Claude's, and parseStreamJson
2138
+ // below only understands Claude's and orquesta-cli's,
2139
+ // so asking for it would give us an unparseable
2140
+ // stream instead of an answer.
2141
+ : cliCommand === 'kimi'
2142
+ ? '--output-format text' // Kimi: plain text. NOTE: -p (one-shot) CANNOT be
2143
+ // combined with -y ("Cannot combine --prompt with
2144
+ // --yolo"), so dispatched kimi prompts respond as
2145
+ // chat (no autonomous tool use). Interactive kimi
2146
+ // sessions DO get -y (see ptyArgs below).
2147
+ : '--verbose --output-format stream-json'; // Claude: stream-json format
2148
+ // Kimi has no --dangerously-skip-permissions, and -p forbids -y. Cursor spells
2149
+ // the same thing --force (alias --yolo).
2150
+ const permFlags = cliCommand === 'kimi'
2151
+ ? ''
2152
+ : mode !== 'auto'
2153
+ ? ''
2154
+ : cliCommand === 'cursor'
2155
+ ? '--force'
2156
+ : '--dangerously-skip-permissions';
2013
2157
  // Per-project endpoint override (orquesta CLI only). Single-quoted for the
2014
2158
  // shell-wrapped (Linux) path; pushed as a separate arg on Win/macOS below.
2015
2159
  const useEndpoint = cliCommand === 'orquesta' && !!globalCliEndpoint;
@@ -2130,6 +2274,13 @@ ${userRequestBody}`;
2130
2274
  else if (action === 'failed') {
2131
2275
  output += `\n❌ Failed: ${title}\n`;
2132
2276
  }
2277
+ // Persist the transition too. These used to be marker text ONLY, so a
2278
+ // replayed run had zero progress rows and rendered as a different run
2279
+ // than the one the user watched live (no plan, no checklist). `phase`
2280
+ // carries the action so a reader can rebuild the checklist state.
2281
+ if (action && title) {
2282
+ logs.push(createProgressLog(String(title), undefined, action === 'failed' ? 'error' : 'info', promptId, undefined, undefined, String(action)));
2283
+ }
2133
2284
  }
2134
2285
  else if (json.event === 'tool_call') {
2135
2286
  const { tool, args } = json.data;
@@ -2138,6 +2289,15 @@ ${userRequestBody}`;
2138
2289
  if (argsStr.length < 500) {
2139
2290
  output += `${argsStr}\n`;
2140
2291
  }
2292
+ else {
2293
+ // A file edit is the one thing the user actually wants to SEE, and
2294
+ // its args are almost always over the cap (old+new string), so the
2295
+ // live stream showed nothing while the DB row had the whole diff.
2296
+ // Emit a trimmed edit payload instead of dropping it entirely.
2297
+ const compact = compactFileEditArgs(args);
2298
+ if (compact)
2299
+ output += `${compact}\n`;
2300
+ }
2141
2301
  logs.push(createToolCallLog(tool, args, 'info', promptId));
2142
2302
  currentToolName = tool;
2143
2303
  }
@@ -2448,6 +2608,8 @@ ${userRequestBody}`;
2448
2608
  catch {
2449
2609
  // Ignore errors getting git remote
2450
2610
  }
2611
+ detectedCommitSha = commitMatch[1];
2612
+ detectedCommitUrl = commitUrl;
2451
2613
  sendSpecialEvent('git_commit', {
2452
2614
  commitSha: commitMatch[1],
2453
2615
  message: commitMatch[2] || '',
@@ -2628,15 +2790,25 @@ ${userRequestBody}`;
2628
2790
  // Record WHICH CLI + model executed it (timeline/history). Model is
2629
2791
  // best-effort — the CLI/proxy pick the true upstream model internally
2630
2792
  // (batuta-auto fans out per prompt), so we report the CLI's MODEL CONFIG:
2631
- const cliTypeForDb = cliCommand === 'orquesta' ? 'orquesta-cli' : cliCommand === 'kimi' ? 'kimi-cli' : 'claude-cli';
2793
+ const cliTypeForDb = cliCommand === 'orquesta'
2794
+ ? 'orquesta-cli'
2795
+ : cliCommand === 'kimi'
2796
+ ? 'kimi-cli'
2797
+ : cliCommand === 'cursor'
2798
+ ? 'cursor-cli'
2799
+ : 'claude-cli';
2632
2800
  let modelForDb;
2633
2801
  if (cliCommand === 'kimi')
2634
2802
  modelForDb = 'kimi-for-coding';
2803
+ // cursor-agent picks the model from the user's own Cursor settings and does
2804
+ // not report it in text mode, so leave it unset rather than guess.
2805
+ else if (cliCommand === 'cursor')
2806
+ modelForDb = undefined;
2635
2807
  else if (cliCommand === 'orquesta')
2636
2808
  modelForDb = (globalCliEndpoint === 'batuta' || !globalCliEndpoint) ? 'batuta-auto' : globalCliEndpoint;
2637
2809
  else if (cliCommand === 'claude')
2638
2810
  modelForDb = process.env.ANTHROPIC_MODEL || undefined;
2639
- updatePromptStatus(promptId, status, summary || undefined, Object.keys(usage).length > 0 ? usage : undefined, finalResult, { cliType: cliTypeForDb, model: modelForDb, gitBranch: currentBranch || undefined }).catch(err => {
2811
+ updatePromptStatus(promptId, status, summary || undefined, Object.keys(usage).length > 0 ? usage : undefined, finalResult, { cliType: cliTypeForDb, model: modelForDb, gitBranch: currentBranch || undefined, gitCommitSha: detectedCommitSha || undefined, gitCommitUrl: detectedCommitUrl || undefined }).catch(err => {
2640
2812
  logger.error(`Failed to update prompt status: ${err}`);
2641
2813
  });
2642
2814
  // Auto-publish any documents/assets the run generated so the operator
@@ -3490,20 +3662,30 @@ export async function startSession(options) {
3490
3662
  // inside bwrap (/ is ro-bound). cliCommand stays the TYPE.
3491
3663
  const cliBinary = cliCommand === 'kimi'
3492
3664
  ? (resolveKimiBinary() || 'kimi')
3493
- : cliCommand === 'claude'
3494
- ? (resolveClaudeBinary() || 'claude')
3495
- : cliCommand;
3496
- logger.info(`Interactive session CLI: ${cliCommand}${cliCommand === 'kimi' ? ` (${cliBinary})` : ''}`);
3665
+ : cliCommand === 'cursor'
3666
+ ? (resolveCursorBinary() || 'cursor-agent')
3667
+ : cliCommand === 'claude'
3668
+ ? (resolveClaudeBinary() || 'claude')
3669
+ : cliCommand;
3670
+ const resolvedByPath = cliCommand === 'kimi' || cliCommand === 'cursor';
3671
+ logger.info(`Interactive session CLI: ${cliCommand}${resolvedByPath ? ` (${cliBinary})` : ''}`);
3497
3672
  // Best-effort model + cli label reported in session:started so the dashboard
3498
3673
  // can tag the interactive prompt (same mapping dispatched prompts use on
3499
3674
  // completion). claude's exact model isn't knowable from a raw PTY, so it falls
3500
3675
  // back to ANTHROPIC_MODEL when set, else undefined.
3501
3676
  const sessionModel = cliCommand === 'kimi'
3502
3677
  ? 'kimi-for-coding'
3503
- : cliCommand === 'orquesta'
3504
- ? ((globalCliEndpoint === 'batuta' || !globalCliEndpoint) ? 'batuta-auto' : globalCliEndpoint)
3505
- : (process.env.ANTHROPIC_MODEL || undefined);
3506
- const sessionCliType = cliCommand === 'orquesta' ? 'orquesta' : cliCommand === 'kimi' ? 'kimi' : 'claude';
3678
+ : cliCommand === 'cursor'
3679
+ // cursor-agent uses whatever model the user selected in their own Cursor
3680
+ // account; it isn't knowable from the PTY, so report nothing rather than lie.
3681
+ ? undefined
3682
+ : cliCommand === 'orquesta'
3683
+ ? ((globalCliEndpoint === 'batuta' || !globalCliEndpoint) ? 'batuta-auto' : globalCliEndpoint)
3684
+ : (process.env.ANTHROPIC_MODEL || undefined);
3685
+ const sessionCliType = cliCommand === 'orquesta' ? 'orquesta'
3686
+ : cliCommand === 'kimi' ? 'kimi'
3687
+ : cliCommand === 'cursor' ? 'cursor'
3688
+ : 'claude';
3507
3689
  // Filter undefined env values — node-pty requires Record<string, string>.
3508
3690
  // In strict sandbox mode, sandboxEnv() first reduces to the allowlist so the
3509
3691
  // interactive CLI can't read unrelated host secrets from its own env.
@@ -3583,7 +3765,7 @@ export async function startSession(options) {
3583
3765
  // `ps aux`. Default-off because a file unreadable inside bwrap would silently
3584
3766
  // strip the system prompt (degraded session, not a crash) — flip the default
3585
3767
  // only after E2E confirms the sandbox reads it. Rollback = unset the var.
3586
- const wantSysPromptFile = cliCommand !== 'kimi' && ((process.platform === 'win32' && cliCommand === 'orquesta') ||
3768
+ const wantSysPromptFile = cliCommand !== 'kimi' && cliCommand !== 'cursor' && ((process.platform === 'win32' && cliCommand === 'orquesta') ||
3587
3769
  /^(1|true|yes|on)$/i.test(process.env.ORQUESTA_SYSPROMPT_FILE || ''));
3588
3770
  if (wantSysPromptFile) {
3589
3771
  try {
@@ -3602,7 +3784,13 @@ export async function startSession(options) {
3602
3784
  }
3603
3785
  }
3604
3786
  let ptyArgs;
3605
- if (cliCommand === 'kimi') {
3787
+ if (cliCommand === 'cursor') {
3788
+ // cursor-agent has no --append-system-prompt; it reads AGENTS.md / CLAUDE.md
3789
+ // from the working dir (which the agent already syncs), so the capabilities
3790
+ // block isn't injected. `--force` is its --dangerously-skip-permissions.
3791
+ ptyArgs = ['--force'];
3792
+ }
3793
+ else if (cliCommand === 'kimi') {
3606
3794
  // Kimi has neither --dangerously-skip-permissions nor --append-system-prompt.
3607
3795
  // `-y` auto-approves the interactive session; it reads project conventions from
3608
3796
  // CLAUDE.md / AGENTS.md in the working dir (which the agent already syncs), so
@@ -3620,10 +3808,11 @@ export async function startSession(options) {
3620
3808
  // resume flag, so the option is silently ignored there. Prepended so it's an
3621
3809
  // unambiguous standalone flag, separate from --append-system-prompt's value.
3622
3810
  if ((resume || resumeSessionId) && cliCommand !== 'kimi') {
3811
+ // cursor-agent spells these the same way claude does (--continue / --resume <id>).
3623
3812
  // claude can target a SPECIFIC past conversation by id (--resume <id>);
3624
3813
  // orquesta-cli only reattaches the latest (--continue). So when a specific
3625
3814
  // id is requested and we're on claude, use --resume; otherwise --continue.
3626
- if (resumeSessionId && cliCommand === 'claude') {
3815
+ if (resumeSessionId && (cliCommand === 'claude' || cliCommand === 'cursor')) {
3627
3816
  ptyArgs.unshift('--resume', resumeSessionId);
3628
3817
  logger.info(`[Session] Resuming conversation ${resumeSessionId} (--resume) for ${cliCommand}`);
3629
3818
  }