orquesta-agent 0.2.238 → 0.2.240

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.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Read-only file browsing for the dashboard's Code Browser.
3
+ *
4
+ * A project bought as "Solo Orquesta" has no GitHub repository and no SSH
5
+ * credentials — the two sources the browser knew about — so the panel was
6
+ * permanently blocked even though the agent is connected and the files are
7
+ * right there on the machine. This answers `fs:list` / `fs:read` from the
8
+ * agent's own working directory. QA-68.
9
+ *
10
+ * Read-only on purpose, and confined to the working directory: it resolves
11
+ * every requested path and refuses anything that lands outside, symlinks
12
+ * included, so a crafted path cannot be used to read the rest of the disk.
13
+ */
14
+ export interface BrowseItem {
15
+ name: string;
16
+ path: string;
17
+ type: 'file' | 'dir';
18
+ size: number;
19
+ }
20
+ export declare function listDirectory(root: string, requestedPath?: string): {
21
+ path: string;
22
+ items: BrowseItem[];
23
+ truncated: boolean;
24
+ } | {
25
+ error: string;
26
+ };
27
+ export declare function readFileForBrowser(root: string, requestedPath?: string): {
28
+ path: string;
29
+ name: string;
30
+ content: string;
31
+ size: number;
32
+ } | {
33
+ error: string;
34
+ };
35
+ //# sourceMappingURL=fs-browse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-browse.d.ts","sourceRoot":"","sources":["../src/fs-browse.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,KAAK,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;CACb;AAsCD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG;IACnE,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;CACnB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAyCpB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CA2BpB"}
@@ -0,0 +1,106 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import * as logger from './logger.js';
4
+ /** A listing is a directory pane — long enough to be useful, short enough to send. */
5
+ const MAX_ENTRIES = 1000;
6
+ /** Above this a file stops being something you read in a browser pane. */
7
+ const MAX_FILE_BYTES = 1024 * 1024;
8
+ /**
9
+ * Resolve `rel` beneath `root`, or null when it escapes.
10
+ *
11
+ * Both the lexical path and its realpath are checked: `..` is caught by the
12
+ * first, a symlink pointing out of the tree only by the second.
13
+ */
14
+ function resolveInside(root, rel) {
15
+ const base = path.resolve(root);
16
+ const cleaned = (rel || '').replace(/^[/\\]+/, '');
17
+ const target = path.resolve(base, cleaned === '.' ? '' : cleaned);
18
+ const contains = (p) => p === base || p.startsWith(base + path.sep);
19
+ if (!contains(target))
20
+ return null;
21
+ try {
22
+ const real = fs.realpathSync(target);
23
+ const realBase = fs.realpathSync(base);
24
+ if (real !== realBase && !real.startsWith(realBase + path.sep))
25
+ return null;
26
+ }
27
+ catch {
28
+ // Does not exist yet / cannot be resolved — the caller's stat will report it.
29
+ }
30
+ return target;
31
+ }
32
+ /** Path as the dashboard addresses it: relative to the root, forward slashes. */
33
+ function toRelative(root, absolute) {
34
+ const rel = path.relative(path.resolve(root), absolute);
35
+ return rel.split(path.sep).join('/');
36
+ }
37
+ export function listDirectory(root, requestedPath) {
38
+ const target = resolveInside(root, requestedPath);
39
+ if (!target)
40
+ return { error: 'Path is outside the working directory' };
41
+ let entries;
42
+ try {
43
+ const stat = fs.statSync(target);
44
+ if (!stat.isDirectory())
45
+ return { error: 'Not a directory' };
46
+ entries = fs.readdirSync(target, { withFileTypes: true });
47
+ }
48
+ catch (err) {
49
+ return { error: err instanceof Error ? err.message : 'Could not read directory' };
50
+ }
51
+ // `.git` is megabytes of objects nobody browses, and the GitHub source does
52
+ // not show it either — keeping it out keeps the two sources looking alike.
53
+ const visible = entries.filter(e => e.name !== '.git');
54
+ const truncated = visible.length > MAX_ENTRIES;
55
+ const items = [];
56
+ for (const entry of visible.slice(0, MAX_ENTRIES)) {
57
+ const absolute = path.join(target, entry.name);
58
+ let size = 0;
59
+ let isDir = entry.isDirectory();
60
+ try {
61
+ const stat = fs.statSync(absolute); // follows symlinks, so a linked dir reads as a dir
62
+ size = stat.isFile() ? stat.size : 0;
63
+ isDir = stat.isDirectory();
64
+ }
65
+ catch {
66
+ // Broken symlink or a race with a delete — list it as an empty file.
67
+ }
68
+ items.push({
69
+ name: entry.name,
70
+ path: toRelative(root, absolute),
71
+ type: isDir ? 'dir' : 'file',
72
+ size,
73
+ });
74
+ }
75
+ items.sort((a, b) => (a.type !== b.type ? (a.type === 'dir' ? -1 : 1) : a.name.localeCompare(b.name)));
76
+ return { path: toRelative(root, target), items, truncated };
77
+ }
78
+ export function readFileForBrowser(root, requestedPath) {
79
+ const target = resolveInside(root, requestedPath);
80
+ if (!target)
81
+ return { error: 'Path is outside the working directory' };
82
+ try {
83
+ const stat = fs.statSync(target);
84
+ if (!stat.isFile())
85
+ return { error: 'Not a file' };
86
+ if (stat.size > MAX_FILE_BYTES) {
87
+ return { error: `File is too large to open here (${Math.round(stat.size / 1024)} KB)` };
88
+ }
89
+ const buffer = fs.readFileSync(target);
90
+ // A NUL byte early on is the usual, cheap "this is not text" signal.
91
+ if (buffer.subarray(0, 8000).includes(0)) {
92
+ return { error: 'Binary file' };
93
+ }
94
+ return {
95
+ path: toRelative(root, target),
96
+ name: path.basename(target),
97
+ content: buffer.toString('utf-8'),
98
+ size: stat.size,
99
+ };
100
+ }
101
+ catch (err) {
102
+ logger.debug(`[fs-browse] read failed: ${err instanceof Error ? err.message : err}`);
103
+ return { error: err instanceof Error ? err.message : 'Could not read file' };
104
+ }
105
+ }
106
+ //# sourceMappingURL=fs-browse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-browse.js","sourceRoot":"","sources":["../src/fs-browse.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -29,12 +29,13 @@ import * as path from 'path';
29
29
  import * as os from 'os';
30
30
  import { fileURLToPath } from 'url';
31
31
  import * as logger from './logger.js';
32
+ import { listDirectory, readFileForBrowser } from './fs-browse.js';
32
33
  import { startDaemon, isDaemonChild, acquireSingleInstanceLock } from './daemon.js';
33
34
  import { getCliTokenHash } from './cli-config.js';
34
35
  import { validateToken, markDisconnected, subscribeToChannel, setAgentToken, reportAgentError, reportAgentProblem, persistAgentSettings, startHeartbeat, stopHeartbeat, setPermissionModeUpdater, setCliPreferenceUpdater, setCliEndpointUpdater, setBrowserEnabledUpdater, setSandboxUpdater, setSandboxAccessUpdater, setCurrentProjectId, getCurrentProjectId, fetchCredentials, fetchSkills, syncSkillsToDirectory, uploadClaudeMd, setClaudeAuthChecker, setOrquestaCliChecker, setKimiCliChecker, setCursorCliChecker, setActiveCliResolver, setEffectiveConfigResolver, setAgentVersion, setAgentName, } from './supabase.js';
35
36
  import { createSocketIOConnection, createExecutionChannel, setupSocketIOListeners, } from './socketio-transport.js';
36
37
  import { execSync, spawn } from 'child_process';
37
- import { execute, evaluate, cancel, cancelAll, hasRunningProcesses, checkClaudeAuth, configureAuth, gitClone, gitPull, setPermissionMode, setSandboxConfig, setSandboxAccess, setAgentInstructions, setProjectSettings, handleSupervisionResponse, setInjectedCredentials, generatePlanItems, isOrquestaCliAvailable, isClaudeCliAvailable, setCliPreference, setCliEndpoint, setBrowserEnabled, selectCli, isKimiCliAvailable, isCursorCliAvailable, checkCursorAuth,
38
+ import { execute, evaluate, cancel, cancelAll, hasRunningProcesses, checkClaudeAuth, configureAuth, gitClone, gitPull, gitCommitAndPush, gitNumstatSince, getCurrentGitBranch, setPermissionMode, setSandboxConfig, setSandboxAccess, setAgentInstructions, setProjectSettings, handleSupervisionResponse, setInjectedCredentials, generatePlanItems, isOrquestaCliAvailable, isClaudeCliAvailable, setCliPreference, setCliEndpoint, setBrowserEnabled, selectCli, isKimiCliAvailable, isCursorCliAvailable, checkCursorAuth,
38
39
  // Interactive session functions
39
40
  startSession, sendSessionInput, endSession, terminateSession, hasActiveSession, hasActiveSessionFor, getEffectiveRuntimeConfig, getSessionStatus, resizeSession, setOnSessionEnded, listResumableSessions, } from './executor.js';
40
41
  import { startBrowserSession, endBrowserSession, tunnelInbound, openBrowserTunnel, browserRpc, runBrowserGoal, pauseBrowserGoal, resumeBrowserGoal, cancelBrowserGoal, shutdownAllBrowserSessions, } from './browser.js';
@@ -690,6 +691,48 @@ async function connect(options) {
690
691
  return promptQueue.some(q => q.promptId === promptId) ||
691
692
  [...runningExecutions.values()].some(e => e.promptId === promptId);
692
693
  }
694
+ /**
695
+ * Commit the assisted item's work and hand the server the raw facts.
696
+ *
697
+ * Facts only: the commit sha, the branch, and `git diff --numstat` text.
698
+ * The server evaluates the gate. That split is the point — a gate computed
699
+ * on the machine being gated protects nobody, and this machine belongs to
700
+ * the customer.
701
+ *
702
+ * Best-effort throughout: a failure here must not fail the prompt, which
703
+ * has already produced real work. The reviewer sees the item stuck
704
+ * in_progress and can nudge the run instead.
705
+ */
706
+ async function reportAssistedResult(payload, execDir, baseBranch) {
707
+ const assisted = payload.assisted;
708
+ try {
709
+ const commit = await gitCommitAndPush(execDir, `assisted: ${payload.content.split('\n')[0].slice(0, 120)}`);
710
+ // Diff against the branch we started from when we know it; otherwise
711
+ // the last commit only, which is what the item just produced anyway.
712
+ const base = baseBranch && baseBranch !== assisted.branch ? baseBranch : 'HEAD~1';
713
+ const numstat = commit.sha ? gitNumstatSince(execDir, base) : '';
714
+ const apiUrl = process.env.ORQUESTA_API_URL || 'https://getorquesta.com';
715
+ const res = await fetch(`${apiUrl}/api/agent/assisted-report`, {
716
+ method: 'POST',
717
+ headers: { 'Content-Type': 'application/json' },
718
+ body: JSON.stringify({
719
+ token,
720
+ promptId: payload.promptId,
721
+ commitSha: commit.sha,
722
+ branch: assisted.branch,
723
+ numstat,
724
+ }),
725
+ });
726
+ if (!res.ok) {
727
+ logger.warn(`[assisted] report rejected (${res.status}) for item ${assisted.itemId}`);
728
+ return;
729
+ }
730
+ logger.success(`[assisted] reported ${commit.sha ? commit.sha.slice(0, 8) : 'no commit'} on ${assisted.branch}`);
731
+ }
732
+ catch (err) {
733
+ logger.warn(`[assisted] report failed: ${err}`);
734
+ }
735
+ }
693
736
  async function executePrompt(payload) {
694
737
  const execId = payload.id;
695
738
  runningExecutions.set(execId, { promptId: payload.promptId, commandId: execId, subAgentId: payload.subAgentId });
@@ -717,6 +760,14 @@ async function connect(options) {
717
760
  catch {
718
761
  // Non-fatal: continue with existing credentials
719
762
  }
763
+ // The branch we came FROM, captured before any checkout below moves
764
+ // HEAD. Auto-merge used to read it afterwards, from `workingDirectory`
765
+ // — which for the common case (execDir === workingDirectory) is the
766
+ // sub-agent branch itself, so `subCfg.branch !== mainBranch` was false
767
+ // and the merge silently never ran. Capture first, merge into this.
768
+ const baseBranch = fs.existsSync(path.join(execDir, '.git'))
769
+ ? getCurrentGitBranch(execDir)
770
+ : null;
720
771
  // Branch checkout if sub-agent has a configured branch
721
772
  if (subCfg?.branch && fs.existsSync(path.join(execDir, '.git'))) {
722
773
  try {
@@ -742,15 +793,28 @@ async function connect(options) {
742
793
  channel: channel,
743
794
  permissionMode: currentPermissionMode,
744
795
  attachments: payload.attachments,
796
+ assisted: payload.assisted,
745
797
  });
798
+ // Assisted run: commit what the CLI produced and report the FACTS to
799
+ // the server, which computes the gate. The agent deliberately makes no
800
+ // judgement about the diff — it does not decide whether the change is
801
+ // too big, and it never merges or deploys. autoMerge is false on these
802
+ // payloads for the same reason.
803
+ if (payload.assisted && fs.existsSync(path.join(execDir, '.git'))) {
804
+ await reportAssistedResult(payload, execDir, baseBranch);
805
+ }
746
806
  // Auto-merge if configured
747
807
  if (subCfg?.autoMerge && subCfg.branch && fs.existsSync(path.join(execDir, '.git'))) {
748
808
  try {
749
- const mainBranch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: workingDirectory, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
750
- if (subCfg.branch !== mainBranch) {
751
- execSync(`git checkout ${mainBranch} && git merge ${subCfg.branch} --no-edit`, { cwd: execDir, stdio: 'pipe' });
809
+ const mainBranch = baseBranch;
810
+ if (mainBranch && subCfg.branch !== mainBranch) {
811
+ execSync(`git checkout ${mainBranch}`, { cwd: execDir, stdio: 'pipe' });
812
+ execSync(`git merge ${subCfg.branch} --no-edit`, { cwd: execDir, stdio: 'pipe' });
752
813
  logger.success(`${label} Auto-merged ${subCfg.branch} into ${mainBranch}`);
753
814
  }
815
+ else if (!mainBranch) {
816
+ logger.warn(`${label} Auto-merge skipped: could not determine the base branch`);
817
+ }
754
818
  }
755
819
  catch (err) {
756
820
  logger.warn(`${label} Auto-merge failed (resolve manually): ${err}`);
@@ -820,12 +884,24 @@ async function connect(options) {
820
884
  }
821
885
  else {
822
886
  logger.info(`[Heartbeat] Dispatching missed execute for prompt ${cmd.promptId}`);
887
+ // Carry the branch envelope through the fallback too. Without it an
888
+ // assisted item delivered by heartbeat runs on whatever branch is
889
+ // currently checked out, which is exactly the isolation the run
890
+ // depends on.
891
+ const queued = {
892
+ id: cmd.commandId,
893
+ promptId: cmd.promptId,
894
+ content: cmd.content,
895
+ workingDirectory,
896
+ subAgentConfig: cmd.subAgentConfig,
897
+ assisted: cmd.assisted,
898
+ };
823
899
  if (isDefaultSlotBusy()) {
824
900
  logger.warn(`[Heartbeat] Default slot busy - queuing missed prompt: ${cmd.promptId}`);
825
- promptQueue.push({ id: cmd.commandId, promptId: cmd.promptId, content: cmd.content, workingDirectory });
901
+ promptQueue.push(queued);
826
902
  }
827
903
  else {
828
- executePrompt({ id: cmd.commandId, promptId: cmd.promptId, content: cmd.content, workingDirectory });
904
+ executePrompt(queued);
829
905
  }
830
906
  }
831
907
  });
@@ -1288,7 +1364,14 @@ async function connect(options) {
1288
1364
  },
1289
1365
  // Resumable past conversations for the dashboard Resume menu. Defaults to
1290
1366
  // the project working dir when the request omits one.
1291
- (payload) => listResumableSessions(payload?.workingDirectory || workingDirectory));
1367
+ (payload) => listResumableSessions(payload?.workingDirectory || workingDirectory),
1368
+ // Read-only file browsing, rooted at the working directory. This is the
1369
+ // only file source a project with no GitHub repo and no SSH host has, and
1370
+ // the browser panel is blocked without it. QA-68.
1371
+ {
1372
+ onFsList: (payload) => listDirectory(workingDirectory, payload?.path),
1373
+ onFsRead: (payload) => readFileForBrowser(workingDirectory, payload?.path),
1374
+ });
1292
1375
  }
1293
1376
  catch (err) {
1294
1377
  const message = err instanceof Error ? err.message : 'Unknown error';