orquesta-agent 0.2.211 → 0.2.213

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/executor.js CHANGED
@@ -30,6 +30,7 @@ async function loadPtySpawn() {
30
30
  import * as fs from 'fs';
31
31
  import * as path from 'path';
32
32
  import * as os from 'os';
33
+ import { randomUUID } from 'crypto';
33
34
  import { fileURLToPath } from 'url';
34
35
  import { createRequire } from 'module';
35
36
  import * as logger from './logger.js';
@@ -39,7 +40,8 @@ const nodeRequire = createRequire(import.meta.url);
39
40
  import { isSandboxAvailable, buildBwrapArgs, shQuote, ensureStrictProjectDirs, encodeClaudeProjectDir } from './sandbox.js';
40
41
  import { parseCoordSpec, runCoordination } from './coordination.js';
41
42
  import { parseSudosudoInstallSpec, runSudosudoInstall } from './sudosudo.js';
42
- import { sendOutput, sendComplete, sendError, sendSupervisionRequest, sendExecutionResumed, updatePromptStatus, persistOutputLogs, clearOutputBuffer, sendRequirement, persistRequirement, sendQAInstructions, persistQAInstructions, sendPlanItemsGenerated, sendSessionOutput, sendSessionStarted, sendSessionEnded, sendSessionError, reportAgentError } from './supabase.js';
43
+ import { sendOutput, sendComplete, sendError, sendSupervisionRequest, sendExecutionResumed, updatePromptStatus, persistOutputLogs, clearOutputBuffer, sendRequirement, persistRequirement, sendQAInstructions, persistQAInstructions, sendPlanItemsGenerated, sendSessionOutput, sendSessionStarted, sendSessionEnded, sendSessionError, sendSessionLog, reportAgentError } from './supabase.js';
44
+ import { startTranscriptTail } from './claude-transcript.js';
43
45
  import { createThinkingLog, createToolCallLog, createToolResultLog, createOutputLog, createErrorLog, createSystemLog, } from './types/agent-logs.js';
44
46
  /**
45
47
  * Build args for the `script` PTY wrapper, accounting for BSD vs util-linux differences.
@@ -668,17 +670,65 @@ export function isOrquestaCliAvailable() {
668
670
  orquestaCliCache = { available, at: now };
669
671
  return available;
670
672
  }
673
+ // Resolve a claude binary that ACTUALLY runs — not one that merely exists on
674
+ // PATH. The claude-code npm package's `bin/claude` launcher can be broken: a
675
+ // Windows `claude.exe` shim (ASCII, no +x) landed on a Linux box, so a bare
676
+ // `claude` resolves to it and every spawn dies instantly (exit 126, or exit 0 /
677
+ // no output) while the REAL native binary ships fine as an optional platform
678
+ // subpackage (@anthropic-ai/claude-code-<plat>-<arch>/claude). The old detection
679
+ // just ran `claude --version` and, when it (briefly) passed, reported the CLI as
680
+ // available+authenticated — so the dashboard showed "authenticated" while
681
+ // interactive sessions "exited early". Field report: EHive DLM server,
682
+ // 2026-06-15. Resolve a binary that runs `--version`, preferring PATH, then the
683
+ // platform subpackage; return null if none works (honest "no CLI").
684
+ let claudeBinaryCache = null;
685
+ export function resolveClaudeBinary() {
686
+ const now = Date.now();
687
+ if (claudeBinaryCache && now - claudeBinaryCache.at < CLI_DETECTION_TTL_MS)
688
+ return claudeBinaryCache.path;
689
+ let resolved = null;
690
+ // 1. `claude` on PATH that actually executes (the broken launcher exists but
691
+ // exit-126s, which --version surfaces — so this rejects it).
692
+ try {
693
+ execSync('claude --version', { stdio: 'pipe', timeout: 15000 });
694
+ resolved = 'claude';
695
+ }
696
+ catch { /* missing or broken launcher */ }
697
+ // 2. PATH claude missing/broken → run the native binary claude-code ships as an
698
+ // optional platform subpackage, directly.
699
+ if (!resolved) {
700
+ const exe = process.platform === 'win32' ? 'claude.exe' : 'claude';
701
+ const sub = `claude-code-${process.platform}-${process.arch}`;
702
+ const roots = [];
703
+ try {
704
+ const p = execSync('npm root -g', { stdio: 'pipe', timeout: 5000, encoding: 'utf-8' }).trim();
705
+ if (p)
706
+ roots.push(p);
707
+ }
708
+ catch { /* npm slow/absent */ }
709
+ roots.push(path.join(os.homedir(), '.npm-global', 'lib', 'node_modules'), '/usr/local/lib/node_modules', '/usr/lib/node_modules');
710
+ for (const root of roots) {
711
+ const cand = path.join(root, '@anthropic-ai', 'claude-code', 'node_modules', '@anthropic-ai', sub, exe);
712
+ try {
713
+ fs.accessSync(cand, fs.constants.X_OK);
714
+ execSync(`"${cand}" --version`, { stdio: 'pipe', timeout: 15000 });
715
+ resolved = cand;
716
+ break;
717
+ }
718
+ catch { /* not under this root / not runnable */ }
719
+ }
720
+ if (resolved)
721
+ warnCliOnce(`claude on PATH is broken or wrong-platform; using native binary ${resolved}. Reinstall to fix: npm i -g @anthropic-ai/claude-code`);
722
+ }
723
+ claudeBinaryCache = { path: resolved, at: now };
724
+ return resolved;
725
+ }
671
726
  export function isClaudeCliAvailable() {
672
727
  const now = Date.now();
673
728
  if (claudeCliCache && now - claudeCliCache.at < CLI_DETECTION_TTL_MS) {
674
729
  return claudeCliCache.available;
675
730
  }
676
- let available = false;
677
- try {
678
- execSync('claude --version', { stdio: 'pipe', timeout: 15000 });
679
- available = true;
680
- }
681
- catch { /* not installed or detection slow */ }
731
+ const available = resolveClaudeBinary() !== null;
682
732
  claudeCliCache = { available, at: now };
683
733
  return available;
684
734
  }
@@ -797,9 +847,11 @@ export function checkClaudeAuth() {
797
847
  // Preferred: ask the CLI directly. Claude Code v2.x stores credentials
798
848
  // in the OS keychain (libsecret on Linux, Keychain on macOS) and the
799
849
  // legacy ~/.claude/.credentials.json file may not exist. `claude auth
800
- // status --json` resolves auth regardless of storage backend.
850
+ // status --json` resolves auth regardless of storage backend. Use the
851
+ // resolved binary (PATH `claude` may be the broken launcher).
801
852
  try {
802
- const out = execSync('claude auth status --json', {
853
+ const claudeBin = resolveClaudeBinary() || 'claude';
854
+ const out = execSync(`"${claudeBin}" auth status --json`, {
803
855
  stdio: ['ignore', 'pipe', 'ignore'],
804
856
  timeout: 5000,
805
857
  encoding: 'utf-8',
@@ -1687,9 +1739,14 @@ ${userRequestBody}`;
1687
1739
  }
1688
1740
  const cliCommand = selectedCli;
1689
1741
  // The string used to actually INVOKE the CLI. kimi usually isn't on PATH
1690
- // (installed at ~/.kimi-code/bin), so resolve its absolute path; claude/orquesta
1691
- // are invoked by name as before. (cliCommand stays the TYPE for flag decisions.)
1692
- const cliBinary = cliCommand === 'kimi' ? (resolveKimiBinary() || 'kimi') : (cliCommand || 'claude');
1742
+ // (installed at ~/.kimi-code/bin) and claude's PATH launcher may be broken, so
1743
+ // resolve their real absolute paths; orquesta is invoked by name as before.
1744
+ // (cliCommand stays the TYPE for flag decisions.)
1745
+ const cliBinary = cliCommand === 'kimi'
1746
+ ? (resolveKimiBinary() || 'kimi')
1747
+ : cliCommand === 'claude'
1748
+ ? (resolveClaudeBinary() || 'claude')
1749
+ : (cliCommand || 'claude');
1693
1750
  const cwd = workingDirectory || process.cwd();
1694
1751
  logger.info(`CLI Selection: ${cliCommand} (${reason})`);
1695
1752
  logger.info(`Spawning: ${cliBinary} -p "${fullContent.slice(0, 50)}..." in ${cwd}`);
@@ -3187,9 +3244,14 @@ export async function startSession(options) {
3187
3244
  const { cli: selectedCli } = selectCli();
3188
3245
  const cliCommand = selectedCli || 'claude';
3189
3246
  // Actual invocation string — kimi usually isn't on PATH (installed at
3190
- // ~/.kimi-code/bin), so resolve its absolute path. The absolute path also
3191
- // resolves identically inside bwrap (/ is ro-bound). cliCommand stays the TYPE.
3192
- const cliBinary = cliCommand === 'kimi' ? (resolveKimiBinary() || 'kimi') : cliCommand;
3247
+ // ~/.kimi-code/bin) and claude's PATH launcher may be broken/wrong-platform, so
3248
+ // resolve their real absolute paths. The absolute path also resolves identically
3249
+ // inside bwrap (/ is ro-bound). cliCommand stays the TYPE.
3250
+ const cliBinary = cliCommand === 'kimi'
3251
+ ? (resolveKimiBinary() || 'kimi')
3252
+ : cliCommand === 'claude'
3253
+ ? (resolveClaudeBinary() || 'claude')
3254
+ : cliCommand;
3193
3255
  logger.info(`Interactive session CLI: ${cliCommand}${cliCommand === 'kimi' ? ` (${cliBinary})` : ''}`);
3194
3256
  // Best-effort model + cli label reported in session:started so the dashboard
3195
3257
  // can tag the interactive prompt (same mapping dispatched prompts use on
@@ -3323,6 +3385,23 @@ export async function startSession(options) {
3323
3385
  logger.info(`[Session] Resuming most recent conversation (--continue) for ${cliCommand}`);
3324
3386
  }
3325
3387
  }
3388
+ // Pin the transcript file name so the structured-log tailer
3389
+ // (claude-transcript.ts) knows exactly which ~/.claude/projects/<cwd>/<id>.jsonl
3390
+ // to follow. claude only:
3391
+ // - --resume <id>: claude appends to that same <id>.jsonl → tail it directly.
3392
+ // - fresh session: mint a uuid + pass --session-id (can't combine with resume).
3393
+ // - plain --continue: claude reattaches its OWN prior id (unknown to us) → null,
3394
+ // and the tailer adopts the newest transcript touched since startup.
3395
+ let claudeTranscriptId = null;
3396
+ if (cliCommand === 'claude') {
3397
+ if (resumeSessionId) {
3398
+ claudeTranscriptId = resumeSessionId;
3399
+ }
3400
+ else if (!resume) {
3401
+ claudeTranscriptId = randomUUID();
3402
+ ptyArgs.push('--session-id', claudeTranscriptId);
3403
+ }
3404
+ }
3326
3405
  // Per-project endpoint override (only the orquesta CLI understands --endpoint).
3327
3406
  if (cliCommand === 'orquesta' && globalCliEndpoint) {
3328
3407
  ptyArgs.push('--endpoint', globalCliEndpoint);
@@ -3407,6 +3486,7 @@ export async function startSession(options) {
3407
3486
  pendingPhoneTimer: null,
3408
3487
  lastPtyOutputAt: Date.now(),
3409
3488
  rogerthatWatcher: null,
3489
+ transcriptTail: null,
3410
3490
  };
3411
3491
  sessions.set(sessionId, session);
3412
3492
  ensureSessionReaper(); // reap this session if it's later abandoned without session:end
@@ -3414,6 +3494,25 @@ export async function startSession(options) {
3414
3494
  // the PTY as if the operator had typed it, so they can hold a full
3415
3495
  // conversation from the phone without touching the dashboard.
3416
3496
  session.rogerthatWatcher = startRogerthatMonitor(sessionId);
3497
+ // Tail claude's on-disk transcript and broadcast STRUCTURED logs (tool_call /
3498
+ // tool_result / output / thinking) so the dashboard persists a rich timeline
3499
+ // instead of scraped terminal chrome. claude only: orquesta-cli self-reports
3500
+ // via its prompt-reporter, and kimi has no structured transcript yet.
3501
+ if (cliCommand === 'claude') {
3502
+ session.transcriptTail = startTranscriptTail({
3503
+ sessionFileId: claudeTranscriptId,
3504
+ startedAtMs: startTime,
3505
+ // The agent has no per-turn promptId (the dashboard owns it), so we tag the
3506
+ // broadcast with none — the dashboard attaches each entry to its active prompt.
3507
+ getPromptId: () => undefined,
3508
+ onLog: (entry) => {
3509
+ if (!sessions.get(sessionId)?.isActive)
3510
+ return;
3511
+ void sendSessionLog(channel, sessionId, entry);
3512
+ },
3513
+ log: (msg) => logger.debug(`[Session ${sessionId}] ${msg}`),
3514
+ });
3515
+ }
3417
3516
  // Send session:started only after Claude produces its first output.
3418
3517
  // This ensures the UI doesn't activate the input field before Claude is
3419
3518
  // actually ready — early input would otherwise be silently ignored.
@@ -3499,6 +3598,10 @@ export async function startSession(options) {
3499
3598
  exitingSession.rogerthatWatcher?.close();
3500
3599
  }
3501
3600
  catch { /* already closed */ }
3601
+ try {
3602
+ exitingSession.transcriptTail?.stop();
3603
+ }
3604
+ catch { /* already stopped */ }
3502
3605
  if (exitingSession.pendingPhoneTimer) {
3503
3606
  clearTimeout(exitingSession.pendingPhoneTimer);
3504
3607
  exitingSession.pendingPhoneTimer = null;
@@ -3822,6 +3925,10 @@ export function terminateSession(sessionId) {
3822
3925
  s.rogerthatWatcher?.close();
3823
3926
  }
3824
3927
  catch { /* already closed */ }
3928
+ try {
3929
+ s.transcriptTail?.stop();
3930
+ }
3931
+ catch { /* already stopped */ }
3825
3932
  if (s.pendingPhoneTimer) {
3826
3933
  clearTimeout(s.pendingPhoneTimer);
3827
3934
  s.pendingPhoneTimer = null;