orcasynth 1.4.3 → 1.4.5
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/advisor/mcpConfig.js +28 -0
- package/dist/advisor/service.js +74 -0
- package/dist/api/server.js +73 -1
- package/dist/cli/commands.js +26 -0
- package/dist/cli/index.js +9 -2
- package/dist/daemon/bootstrap.js +14 -1
- package/dist/mcp/server.js +34 -0
- package/dist/mcp/tools.js +17 -0
- package/dist/overseer/sessionInfo.js +7 -1
- package/dist/prompts/advisor.md +13 -0
- package/dist/shared/apiClient.js +23 -0
- package/dist/store/db.js +4 -0
- package/dist/store/schema.sql +3 -1
- package/dist/store/userStore.js +26 -1
- package/dist/tmux/driver.js +8 -0
- package/dist/tmux/fakeDriver.js +9 -0
- package/package.json +4 -2
- package/prompts/advisor.md +13 -0
- package/web-dist/.next/BUILD_ID +1 -1
- package/web-dist/.next/build-manifest.json +3 -3
- package/web-dist/.next/server/app/_global-error.html +1 -1
- package/web-dist/.next/server/app/_global-error.rsc +1 -1
- package/web-dist/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/_not-found/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/_not-found.html +1 -1
- package/web-dist/.next/server/app/_not-found.rsc +11 -11
- package/web-dist/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
- package/web-dist/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/_not-found.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/web-dist/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/web-dist/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/account/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/account/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/account/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/account.html +1 -1
- package/web-dist/.next/server/app/account.rsc +13 -13
- package/web-dist/.next/server/app/account.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/account.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/account.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/account.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/account.segments/account/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/account.segments/account.segment.rsc +3 -3
- package/web-dist/.next/server/app/dash/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/dash/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/dash/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/dash.html +1 -1
- package/web-dist/.next/server/app/dash.rsc +13 -13
- package/web-dist/.next/server/app/dash.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/dash.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/dash.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/dash.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/dash.segments/dash/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/dash.segments/dash.segment.rsc +3 -3
- package/web-dist/.next/server/app/escalations/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/escalations/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/escalations/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/escalations.html +1 -1
- package/web-dist/.next/server/app/escalations.rsc +13 -13
- package/web-dist/.next/server/app/escalations.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/escalations.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/escalations.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/escalations.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/escalations.segments/escalations/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/escalations.segments/escalations.segment.rsc +3 -3
- package/web-dist/.next/server/app/index.html +1 -1
- package/web-dist/.next/server/app/index.rsc +13 -13
- package/web-dist/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/index.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/index.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/kanban/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/kanban/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/kanban/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/kanban.html +1 -1
- package/web-dist/.next/server/app/kanban.rsc +13 -13
- package/web-dist/.next/server/app/kanban.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/kanban.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/kanban.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/kanban.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/kanban.segments/kanban/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/kanban.segments/kanban.segment.rsc +3 -3
- package/web-dist/.next/server/app/onboarding/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/onboarding/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/onboarding.html +1 -1
- package/web-dist/.next/server/app/onboarding.rsc +13 -13
- package/web-dist/.next/server/app/onboarding.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/onboarding.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/onboarding.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/onboarding.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/onboarding.segments/onboarding.segment.rsc +3 -3
- package/web-dist/.next/server/app/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/projects/page/react-loadable-manifest.json +8 -0
- package/web-dist/.next/server/app/projects/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/projects.html +1 -1
- package/web-dist/.next/server/app/projects.rsc +13 -13
- package/web-dist/.next/server/app/projects.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/projects.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/projects.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/projects.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/projects.segments/projects/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/projects.segments/projects.segment.rsc +3 -3
- package/web-dist/.next/server/app/sessions/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/sessions/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/sessions.html +1 -1
- package/web-dist/.next/server/app/sessions.rsc +13 -13
- package/web-dist/.next/server/app/sessions.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/sessions.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/sessions.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/sessions.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/sessions.segments/sessions/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/sessions.segments/sessions.segment.rsc +3 -3
- package/web-dist/.next/server/app/settings/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/settings/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/settings.html +1 -1
- package/web-dist/.next/server/app/settings.rsc +13 -13
- package/web-dist/.next/server/app/settings.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/settings.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/web-dist/.next/server/app/tasks/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/tasks/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/tasks.html +1 -1
- package/web-dist/.next/server/app/tasks.rsc +13 -13
- package/web-dist/.next/server/app/tasks.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/tasks.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
- package/web-dist/.next/server/app/timeline/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/timeline/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/timeline/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/timeline.html +1 -1
- package/web-dist/.next/server/app/timeline.rsc +13 -13
- package/web-dist/.next/server/app/timeline.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/timeline.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/timeline.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/timeline.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/timeline.segments/timeline/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/timeline.segments/timeline.segment.rsc +3 -3
- package/web-dist/.next/server/app/users/page/react-loadable-manifest.json +10 -1
- package/web-dist/.next/server/app/users/page.js.nft.json +1 -1
- package/web-dist/.next/server/app/users/page_client-reference-manifest.js +1 -1
- package/web-dist/.next/server/app/users.html +1 -1
- package/web-dist/.next/server/app/users.rsc +13 -13
- package/web-dist/.next/server/app/users.segments/_full.segment.rsc +13 -13
- package/web-dist/.next/server/app/users.segments/_head.segment.rsc +4 -4
- package/web-dist/.next/server/app/users.segments/_index.segment.rsc +6 -6
- package/web-dist/.next/server/app/users.segments/_tree.segment.rsc +2 -2
- package/web-dist/.next/server/app/users.segments/users/__PAGE__.segment.rsc +4 -4
- package/web-dist/.next/server/app/users.segments/users.segment.rsc +3 -3
- package/web-dist/.next/server/chunks/ssr/[root-of-the-server]__0yfatub._.js +3 -0
- package/web-dist/.next/server/chunks/ssr/_01rh28z._.js +3 -0
- package/web-dist/.next/server/chunks/ssr/_057a06r._.js +1 -1
- package/web-dist/.next/server/chunks/ssr/_081ml1k._.js +3 -0
- package/web-dist/.next/server/chunks/ssr/_0afhsmf._.js +3 -0
- package/web-dist/.next/server/chunks/ssr/{_1zscr7t._.js → _0m2j9hu._.js} +2 -2
- package/web-dist/.next/server/chunks/ssr/_0tzourm._.js +1 -1
- package/web-dist/.next/server/chunks/ssr/_0ysqykx._.js +3 -0
- package/web-dist/.next/server/chunks/ssr/_10ak-sh._.js +3 -0
- package/web-dist/.next/server/chunks/ssr/_13rgpyg._.js +3 -0
- package/web-dist/.next/server/chunks/ssr/_1fp8enw._.js +3 -0
- package/web-dist/.next/server/chunks/ssr/_1std18n._.js +3 -0
- package/web-dist/.next/server/chunks/ssr/_1zd7t3t._.js +3 -0
- package/web-dist/.next/server/chunks/ssr/app_tasks_page_tsx_1p6mxbw._.js +1 -1
- package/web-dist/.next/server/chunks/ssr/components_shell_Shell_tsx_1e5c27h._.js +1 -1
- package/web-dist/.next/server/chunks/ssr/{node_modules_0h91jdk._.js → node_modules_next_dist_client_components_0bew68i._.js} +2 -2
- package/web-dist/.next/server/middleware-build-manifest.js +3 -3
- package/web-dist/.next/server/pages/404.html +1 -1
- package/web-dist/.next/server/pages/500.html +1 -1
- package/web-dist/.next/static/chunks/09gkeu3bc4xo0.js +1 -0
- package/web-dist/.next/static/chunks/0c6iuw5yay1w0.js +1 -0
- package/web-dist/.next/static/chunks/0ccjus_sicyov.js +1 -0
- package/web-dist/.next/static/chunks/0gor0_p3jg67f.js +1 -0
- package/web-dist/.next/static/chunks/0kd16q0244sp5.js +1 -0
- package/web-dist/.next/static/chunks/0x0pwu4mealh1.js +1 -0
- package/web-dist/.next/static/chunks/0yg3wh0jczxoa.js +1 -0
- package/web-dist/.next/static/chunks/0zms_--zk-t3b.js +1 -0
- package/web-dist/.next/static/chunks/{18zkogw4aykzc.js → 14mmxdnhdicgy.js} +1 -1
- package/web-dist/.next/static/chunks/1bgv4d9v71ij7.js +1 -0
- package/web-dist/.next/static/chunks/1j2hh1hlkxrip.js +1 -0
- package/web-dist/.next/static/chunks/2c16uuyhfnhr9.js +1 -0
- package/web-dist/.next/static/chunks/2g_lldfaqo8bq.js +1 -0
- package/web-dist/.next/static/chunks/{3saus_snl5ri7.js → 2gak7jay2im1l.js} +1 -1
- package/web-dist/.next/static/chunks/2z02etl-1qi3g.js +1 -0
- package/web-dist/.next/static/chunks/2zutw3iy49kee.js +1 -0
- package/web-dist/.next/static/chunks/33tqcj2ra3wol.js +1 -0
- package/web-dist/.next/static/chunks/34b41uiwytome.css +2 -0
- package/web-dist/.next/static/chunks/3uf01y_a4cq8y.js +1 -0
- package/web-dist/.next/server/chunks/ssr/[root-of-the-server]__0i_7o4p._.js +0 -3
- package/web-dist/.next/server/chunks/ssr/_085pshu._.js +0 -3
- package/web-dist/.next/server/chunks/ssr/_0j9ppt0._.js +0 -3
- package/web-dist/.next/server/chunks/ssr/_0tc9z5_._.js +0 -3
- package/web-dist/.next/server/chunks/ssr/_12jhzvy._.js +0 -3
- package/web-dist/.next/server/chunks/ssr/_136wthy._.js +0 -3
- package/web-dist/.next/server/chunks/ssr/_193-v_i._.js +0 -3
- package/web-dist/.next/server/chunks/ssr/_1heytlk._.js +0 -3
- package/web-dist/.next/server/chunks/ssr/_1kom56q._.js +0 -3
- package/web-dist/.next/server/chunks/ssr/_1mjzb9s._.js +0 -3
- package/web-dist/.next/static/chunks/0l1t1fcd-_jj9.js +0 -1
- package/web-dist/.next/static/chunks/186xbnkxm5iu3.js +0 -1
- package/web-dist/.next/static/chunks/1c_1_ca4-vgc7.js +0 -1
- package/web-dist/.next/static/chunks/1f5f3qgbcjv4u.js +0 -1
- package/web-dist/.next/static/chunks/1t-dast4rat3s.css +0 -2
- package/web-dist/.next/static/chunks/1wsag7zex3h4_.js +0 -1
- package/web-dist/.next/static/chunks/24unfsl4do--1.js +0 -1
- package/web-dist/.next/static/chunks/2bfjq22q8xyfs.js +0 -1
- package/web-dist/.next/static/chunks/2kuzf7llj_291.js +0 -1
- package/web-dist/.next/static/chunks/34bew8owm40fg.js +0 -1
- package/web-dist/.next/static/chunks/3a7pgnw_6io2v.js +0 -1
- package/web-dist/.next/static/chunks/3fqd824e6lt4z.js +0 -1
- package/web-dist/.next/static/chunks/3k-swqzkcvlzm.js +0 -1
- package/web-dist/.next/static/chunks/3vmuunta80huz.js +0 -1
- /package/web-dist/.next/static/{-qI9ABgqZPR_Ri56jzVI5 → Vc91G_QEn5enhyqIYkLiV}/_buildManifest.js +0 -0
- /package/web-dist/.next/static/{-qI9ABgqZPR_Ri56jzVI5 → Vc91G_QEn5enhyqIYkLiV}/_clientMiddlewareManifest.js +0 -0
- /package/web-dist/.next/static/{-qI9ABgqZPR_Ri56jzVI5 → Vc91G_QEn5enhyqIYkLiV}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Write the per-program MCP config into the advisor session's cwd so the spawned CLI auto-connects
|
|
4
|
+
* to Orca's MCP server. Each CLI has its own config schema — claude reads `.mcp.json`, opencode reads
|
|
5
|
+
* `opencode.json`, codex reads a TOML config. The schemas are version-sensitive: VERIFY each against
|
|
6
|
+
* the installed CLI version's docs. The `orca api` CLI verb is the always-available fallback, so an
|
|
7
|
+
* imperfect MCP wiring for one program degrades gracefully rather than removing the advisor's reach. */
|
|
8
|
+
export function writeMcpConfig(program, cwd, token, mcpUrl) {
|
|
9
|
+
const auth = `Bearer ${token}`;
|
|
10
|
+
// The config carries the advisor's full-scope bearer token, so lock the file to the daemon user (0600).
|
|
11
|
+
const opts = { mode: 0o600 };
|
|
12
|
+
if (program.startsWith('claude')) {
|
|
13
|
+
writeFileSync(join(cwd, '.mcp.json'), JSON.stringify({
|
|
14
|
+
mcpServers: { orca: { type: 'http', url: mcpUrl, headers: { Authorization: auth } } },
|
|
15
|
+
}, null, 2), opts);
|
|
16
|
+
}
|
|
17
|
+
else if (program.startsWith('opencode')) {
|
|
18
|
+
writeFileSync(join(cwd, 'opencode.json'), JSON.stringify({
|
|
19
|
+
$schema: 'https://opencode.ai/config.json',
|
|
20
|
+
mcp: { orca: { type: 'remote', url: mcpUrl, headers: { Authorization: auth }, enabled: true } },
|
|
21
|
+
}, null, 2), opts);
|
|
22
|
+
}
|
|
23
|
+
else if (program.startsWith('codex')) {
|
|
24
|
+
// Codex reads MCP servers from its TOML config. Written project-local; VERIFY the exact key/path
|
|
25
|
+
// (and whether the installed codex needs a `--config`/`-c` flag to pick this up) for the version.
|
|
26
|
+
writeFileSync(join(cwd, '.codex-mcp.toml'), `[mcp_servers.orca]\nurl = "${mcpUrl}"\nbearer_token = "${token}"\n`, opts);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { resolveExecutor } from '../overseer/routing.js';
|
|
2
|
+
import { render } from '../prompts/index.js';
|
|
3
|
+
import { logger } from '../shared/logger.js';
|
|
4
|
+
const log = logger('advisor');
|
|
5
|
+
/** Per-user advisor lifecycle: a persistent `orca-advisor-<userId>` agent session that controls Orca
|
|
6
|
+
* on the user's behalf with a full-scope token. Chosen exec is remembered and auto-started on login. */
|
|
7
|
+
export class AdvisorService {
|
|
8
|
+
d;
|
|
9
|
+
constructor(d) {
|
|
10
|
+
this.d = d;
|
|
11
|
+
}
|
|
12
|
+
session(userId) { return `orca-advisor-${userId}`; }
|
|
13
|
+
/** An exec must be globally allowed AND (for a restricted non-admin) on the user's own allow-list. */
|
|
14
|
+
execAllowed(userId, exec) {
|
|
15
|
+
const u = this.d.users.get(userId);
|
|
16
|
+
if (!u)
|
|
17
|
+
return false;
|
|
18
|
+
if (!this.d.config.get().allowedExecs.includes(exec))
|
|
19
|
+
return false;
|
|
20
|
+
if (u.is_admin || u.allowed_execs.length === 0)
|
|
21
|
+
return true;
|
|
22
|
+
return u.allowed_execs.includes(exec);
|
|
23
|
+
}
|
|
24
|
+
async status(userId) {
|
|
25
|
+
const u = this.d.users.get(userId);
|
|
26
|
+
const name = this.session(userId);
|
|
27
|
+
const running = (await this.d.tmux.list()).includes(name);
|
|
28
|
+
return { running, exec: u?.advisor_exec ?? '', session: running ? name : null };
|
|
29
|
+
}
|
|
30
|
+
async start(userId, exec) {
|
|
31
|
+
if (!this.execAllowed(userId, exec))
|
|
32
|
+
throw new Error('exec not allowed for user');
|
|
33
|
+
const name = this.session(userId);
|
|
34
|
+
if ((await this.d.tmux.list()).includes(name))
|
|
35
|
+
return { session: name }; // already live — idempotent
|
|
36
|
+
this.d.users.setAdvisorExec(userId, exec); // remember the choice for autostart
|
|
37
|
+
const spec = resolveExecutor([`exec:${exec}`], this.d.fallback);
|
|
38
|
+
const token = this.d.users.ensureAdvisorToken(userId); // full-scope, reused across restarts
|
|
39
|
+
const cwd = this.d.advisorDir(userId);
|
|
40
|
+
await this.d.prepareMcp?.(spec.program, cwd, token, this.d.url);
|
|
41
|
+
const u = this.d.users.get(userId);
|
|
42
|
+
const rawPrompt = render('advisor', { userName: u.name || u.username });
|
|
43
|
+
// agentName `advisor-<id>` → SpawnService names the tmux session `orca-advisor-<id>`. The full
|
|
44
|
+
// advisor token overrides the daemon's agent service token via extraEnv, so the advisor acts with
|
|
45
|
+
// the user's own rights. The cwd is a neutral per-user dir, not a project checkout.
|
|
46
|
+
await this.d.spawn.launch({
|
|
47
|
+
projectId: this.d.projectId ?? 0,
|
|
48
|
+
projectPath: cwd,
|
|
49
|
+
taskId: name,
|
|
50
|
+
agentName: `advisor-${userId}`,
|
|
51
|
+
spec,
|
|
52
|
+
rawPrompt,
|
|
53
|
+
extraEnv: { ORCA_TOKEN: token, ORCA_URL: this.d.url },
|
|
54
|
+
});
|
|
55
|
+
log.info(`advisor started for user ${userId} (${spec.program}/${spec.model})`);
|
|
56
|
+
return { session: name };
|
|
57
|
+
}
|
|
58
|
+
async stop(userId) {
|
|
59
|
+
await this.d.tmux.kill(this.session(userId));
|
|
60
|
+
}
|
|
61
|
+
/** Bring the user's advisor back up after login, if they set one up and left autostart on. Never
|
|
62
|
+
* throws — a spawn failure must not block the login response. */
|
|
63
|
+
async ensureOnLogin(userId) {
|
|
64
|
+
const u = this.d.users.get(userId);
|
|
65
|
+
if (!u || !u.advisor_exec || !u.advisor_autostart)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
await this.start(userId, u.advisor_exec);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
log.error(`advisor autostart failed for user ${userId}`, e);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/api/server.js
CHANGED
|
@@ -21,6 +21,7 @@ import { RelayClient } from '../inference/client.js';
|
|
|
21
21
|
import { uniqueName } from '../daemon/uniqueName.js';
|
|
22
22
|
import { assembleMissionDetail } from '../store/missionDetail.js';
|
|
23
23
|
import { authMiddleware } from './auth.js';
|
|
24
|
+
import { handleMcpRequest } from '../mcp/server.js';
|
|
24
25
|
import { logger } from '../shared/logger.js';
|
|
25
26
|
import { shortId } from '../shared/id.js';
|
|
26
27
|
/** How many times an L3 mission auto-re-spawns a phase that the post-done review rejected before it
|
|
@@ -37,6 +38,8 @@ const ORCA_VERSION = (() => {
|
|
|
37
38
|
return '0.0.0';
|
|
38
39
|
}
|
|
39
40
|
})();
|
|
41
|
+
/** Port the daemon listens on — the MCP route reaches back into this same daemon's REST API at it. */
|
|
42
|
+
const ORCA_PORT = Number(process.env.ORCA_PORT ?? 4400);
|
|
40
43
|
export function createServer(d) {
|
|
41
44
|
const log = logger('api');
|
|
42
45
|
// Core reasoning stores are optional in deps for back-compat with existing call sites/tests; the
|
|
@@ -108,6 +111,12 @@ export function createServer(d) {
|
|
|
108
111
|
const p = c.req.path;
|
|
109
112
|
if (!GATED.some((g) => p === g || p.startsWith(g + '/')))
|
|
110
113
|
return next();
|
|
114
|
+
// An advisor session is per-user, not project-scoped: its access is governed by ownership in
|
|
115
|
+
// the route's own sessionAccessible check, so the project gate must not pre-empt it (the user
|
|
116
|
+
// need not be assigned to the daemon's project to reach their own advisor).
|
|
117
|
+
const sess = p.match(/^\/sessions\/([^/]+)/);
|
|
118
|
+
if (sess?.[1] && classifySession(decodeURIComponent(sess[1])).role === 'advisor')
|
|
119
|
+
return next();
|
|
111
120
|
if (users.count() === 0)
|
|
112
121
|
return next(); // setup mode — no users to gate yet
|
|
113
122
|
const u = c.get('user');
|
|
@@ -149,7 +158,9 @@ export function createServer(d) {
|
|
|
149
158
|
if (!user)
|
|
150
159
|
return c.json({ error: 'invalid credentials' }, 401);
|
|
151
160
|
loginHits.delete(ip); // a valid login clears the counter so an earlier typo streak can't lock the user out
|
|
152
|
-
|
|
161
|
+
const token = users.issueToken(user.id);
|
|
162
|
+
void d.advisor?.ensureOnLogin(user.id); // fire-and-forget: bring the user's advisor back up; never block login
|
|
163
|
+
return c.json({ token, user });
|
|
153
164
|
});
|
|
154
165
|
app.post('/auth/logout', (c) => { const t = c.get('token'); if (t)
|
|
155
166
|
users.revokeToken(t); return c.json({ ok: true }); });
|
|
@@ -428,6 +439,14 @@ export function createServer(d) {
|
|
|
428
439
|
if (!d.userProjects || !d.users)
|
|
429
440
|
return true; // open / single-user mode — no tenancy boundary
|
|
430
441
|
const u = c.get('user');
|
|
442
|
+
// An advisor session belongs to exactly one user: only its owner (or an admin) may reach it, and
|
|
443
|
+
// never via an agent-scoped token. It has no task row, so the project check below can't apply.
|
|
444
|
+
const info = classifySession(name);
|
|
445
|
+
if (info.role === 'advisor') {
|
|
446
|
+
if (c.get('tokenScope') === 'agent')
|
|
447
|
+
return false;
|
|
448
|
+
return !!u && (u.id === info.userId || d.userProjects.isAdmin(u.id));
|
|
449
|
+
}
|
|
431
450
|
// Admin sees every session — but NOT via an agent-scoped token (it's owned by the admin user yet
|
|
432
451
|
// must stay confined to its working set; fall through to the project check below).
|
|
433
452
|
if (c.get('tokenScope') !== 'agent' && u && d.userProjects.isAdmin(u.id))
|
|
@@ -1382,6 +1401,18 @@ export function createServer(d) {
|
|
|
1382
1401
|
await d.tmux.sendKeys(c.req.param('name'), keys);
|
|
1383
1402
|
return c.json({ ok: true });
|
|
1384
1403
|
});
|
|
1404
|
+
app.post('/sessions/:name/input', async (c) => {
|
|
1405
|
+
// Raw interactive input: the xterm `onData` bytes (printable chars, control codes, ESC sequences)
|
|
1406
|
+
// are forwarded verbatim to the pane via `send-keys -l`, so the advisor terminal behaves like a
|
|
1407
|
+
// real one. `-l` + `--` (in the driver) make a leading '-' safe, so no flag-token validation here.
|
|
1408
|
+
if (!sessionAccessible(c, c.req.param('name')))
|
|
1409
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
1410
|
+
const { data } = await c.req.json().catch(() => ({}));
|
|
1411
|
+
if (typeof data !== 'string' || data.length === 0)
|
|
1412
|
+
return c.json({ error: 'data must be a non-empty string' }, 400);
|
|
1413
|
+
await d.tmux.sendRaw(c.req.param('name'), data);
|
|
1414
|
+
return c.json({ ok: true });
|
|
1415
|
+
});
|
|
1385
1416
|
app.post('/sessions/:name/resize', async (c) => {
|
|
1386
1417
|
if (!sessionAccessible(c, c.req.param('name')))
|
|
1387
1418
|
return c.json({ error: 'forbidden' }, 403);
|
|
@@ -1424,6 +1455,47 @@ export function createServer(d) {
|
|
|
1424
1455
|
clear();
|
|
1425
1456
|
});
|
|
1426
1457
|
});
|
|
1458
|
+
// Per-user advisor lifecycle. Full-scope (non-agent) callers only — a spawned agent must not be able
|
|
1459
|
+
// to start/stop a human's advisor. Each acts on the caller's own session (`orca-advisor-<userId>`).
|
|
1460
|
+
app.get('/advisor/status', async (c) => {
|
|
1461
|
+
if (!d.advisor)
|
|
1462
|
+
return c.json({ running: false, exec: '', session: null });
|
|
1463
|
+
if (c.get('tokenScope') === 'agent')
|
|
1464
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
1465
|
+
return c.json(await d.advisor.status(c.get('user').id));
|
|
1466
|
+
});
|
|
1467
|
+
app.post('/advisor/start', async (c) => {
|
|
1468
|
+
if (!d.advisor)
|
|
1469
|
+
return c.json({ error: 'advisor unavailable' }, 503);
|
|
1470
|
+
if (c.get('tokenScope') === 'agent')
|
|
1471
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
1472
|
+
const { exec } = await c.req.json().catch(() => ({}));
|
|
1473
|
+
if (typeof exec !== 'string' || !exec)
|
|
1474
|
+
return c.json({ error: 'exec required' }, 400);
|
|
1475
|
+
try {
|
|
1476
|
+
return c.json(await d.advisor.start(c.get('user').id, exec), 201);
|
|
1477
|
+
}
|
|
1478
|
+
catch (e) {
|
|
1479
|
+
// A permission rejection is the user's fault (403); a spawn/tmux failure is ours (500).
|
|
1480
|
+
const msg = e.message;
|
|
1481
|
+
return c.json({ error: msg }, msg === 'exec not allowed for user' ? 403 : 500);
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
app.post('/advisor/stop', async (c) => {
|
|
1485
|
+
if (!d.advisor)
|
|
1486
|
+
return c.json({ ok: true });
|
|
1487
|
+
if (c.get('tokenScope') === 'agent')
|
|
1488
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
1489
|
+
await d.advisor.stop(c.get('user').id);
|
|
1490
|
+
return c.json({ ok: true });
|
|
1491
|
+
});
|
|
1492
|
+
// MCP endpoint: the advisor agent connects here to control Orca with native tools. Each request is
|
|
1493
|
+
// handled statelessly with the toolset bound to the caller's token, and every tool delegates to the
|
|
1494
|
+
// same `callOrcaApi` core as the `orca api` CLI verb — so a new REST endpoint needs zero edits here.
|
|
1495
|
+
app.all('/mcp', async (c) => {
|
|
1496
|
+
const token = c.get('token');
|
|
1497
|
+
return handleMcpRequest(c.req.raw, { url: `http://localhost:${ORCA_PORT}`, token });
|
|
1498
|
+
});
|
|
1427
1499
|
app.get('/missions', c => {
|
|
1428
1500
|
const allowed = accessibleProjects(c);
|
|
1429
1501
|
const live = d.missions.live();
|
package/dist/cli/commands.js
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { start as realStart, stop as realStop, status as realStatus } from './launcher.js';
|
|
2
2
|
import { update as realUpdate } from './update.js';
|
|
3
|
+
/** `orca api <METHOD> <path> [jsonBody]` — generic authenticated REST passthrough. Reads
|
|
4
|
+
* ORCA_URL/ORCA_TOKEN from the env the daemon injects into every spawned agent, so an agent can
|
|
5
|
+
* drive ANY endpoint without a per-endpoint CLI command (and a new endpoint needs zero CLI edits).
|
|
6
|
+
* Injectable for tests; returns a process exit code. */
|
|
7
|
+
export async function runApiCommand(args, env, deps) {
|
|
8
|
+
const [method, path, rawBody] = args;
|
|
9
|
+
if (!method || !path) {
|
|
10
|
+
deps.err('usage: orca api <METHOD> <path> [jsonBody]');
|
|
11
|
+
return 2;
|
|
12
|
+
}
|
|
13
|
+
let body;
|
|
14
|
+
if (rawBody !== undefined) {
|
|
15
|
+
try {
|
|
16
|
+
body = JSON.parse(rawBody);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
deps.err('api: body must be valid JSON');
|
|
20
|
+
return 2;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const url = env.ORCA_URL ?? 'http://localhost:4400';
|
|
24
|
+
const token = env.ORCA_TOKEN ?? '';
|
|
25
|
+
const res = await deps.call(method, path, body, { url, token });
|
|
26
|
+
deps.out(res.data !== undefined ? JSON.stringify(res.data, null, 2) : res.text);
|
|
27
|
+
return res.ok ? 0 : 1;
|
|
28
|
+
}
|
|
3
29
|
export function defaultLifecycleDeps(version) {
|
|
4
30
|
return {
|
|
5
31
|
version,
|
package/dist/cli/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import { readFileSync, realpathSync } from 'node:fs';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { OrcaClient } from './client.js';
|
|
7
|
-
import { defaultLifecycleDeps, runLifecycle } from './commands.js';
|
|
7
|
+
import { defaultLifecycleDeps, runLifecycle, runApiCommand } from './commands.js';
|
|
8
|
+
import { callOrcaApi } from '../shared/apiClient.js';
|
|
8
9
|
import { menu } from './menu.js';
|
|
9
10
|
const BASE = process.env.ORCA_URL ?? 'http://localhost:4400';
|
|
10
11
|
const USAGE = "usage: orca [command] [options] — run `orca --help` for the full command list";
|
|
@@ -35,6 +36,7 @@ TASKS
|
|
|
35
36
|
--outcome ok|fail record the outcome
|
|
36
37
|
|
|
37
38
|
AGENT-FACING (invoked by running agents — rarely needed by hand)
|
|
39
|
+
api <METHOD> <path> [body] generic authenticated REST call (needs ORCA_URL/ORCA_TOKEN)
|
|
38
40
|
plan submit --phases '<json>' submit an autopilot plan (needs ORCA_PLAN_JOB)
|
|
39
41
|
overseer poll wait for the next decision (needs ORCA_MISSION)
|
|
40
42
|
overseer decide --id <id> … resolve a decision: --approve | --escalate | --choice <optionId>
|
|
@@ -49,7 +51,7 @@ Docs & issues: https://github.com/dragocz1995/orcasynth`;
|
|
|
49
51
|
/** Commands that talk to the daemon API — only these justify auto-starting it. Everything else
|
|
50
52
|
* (help, unknown verbs) must NOT spawn a daemon: a stray detached daemon squats the port and starves
|
|
51
53
|
* the systemd-managed one into a restart loop. */
|
|
52
|
-
const API_COMMANDS = new Set(['ls', 'ready', 'sessions', 'close', 'plan', 'overseer']);
|
|
54
|
+
const API_COMMANDS = new Set(['ls', 'ready', 'sessions', 'close', 'plan', 'overseer', 'api']);
|
|
53
55
|
/** True only for verbs that need the daemon API up — the gate for ensureDaemon's auto-spawn. */
|
|
54
56
|
export function needsDaemon(cmd) {
|
|
55
57
|
return cmd !== undefined && API_COMMANDS.has(cmd);
|
|
@@ -102,6 +104,11 @@ export async function run(argv, c, env) {
|
|
|
102
104
|
case 'sessions':
|
|
103
105
|
console.log(JSON.stringify(await c.sessions(), null, 2));
|
|
104
106
|
break;
|
|
107
|
+
case 'api': {
|
|
108
|
+
const code = await runApiCommand(argv.slice(1), env, { call: callOrcaApi, out: (s) => console.log(s), err: (s) => console.error(s) });
|
|
109
|
+
process.exit(code);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
105
112
|
case 'close': {
|
|
106
113
|
if (!arg) {
|
|
107
114
|
console.error('usage: orca close <taskId> [--summary "<text>"] [--outcome ok|fail]');
|
package/dist/daemon/bootstrap.js
CHANGED
|
@@ -27,9 +27,12 @@ import { UserProjectStore } from '../store/userProjectStore.js';
|
|
|
27
27
|
import { RealGitReader } from '../git/gitReader.js';
|
|
28
28
|
import { uniqueName } from './uniqueName.js';
|
|
29
29
|
import { logger } from '../shared/logger.js';
|
|
30
|
+
import { AdvisorService } from '../advisor/service.js';
|
|
31
|
+
import { writeMcpConfig } from '../advisor/mcpConfig.js';
|
|
30
32
|
import { fileURLToPath } from 'node:url';
|
|
31
33
|
import { dirname, join } from 'node:path';
|
|
32
34
|
import { randomBytes } from 'node:crypto';
|
|
35
|
+
import { mkdirSync } from 'node:fs';
|
|
33
36
|
const log = logger('daemon');
|
|
34
37
|
/** Build the overseer-model prompt that turns a finished mission's phase results into a short,
|
|
35
38
|
* human-readable Czech summary shown on the epic in the dashboard. Kept terse so the relay returns
|
|
@@ -226,7 +229,17 @@ export function buildApp(opts) {
|
|
|
226
229
|
// Per-process secret for short-lived signed avatar URLs (finding W2) — keeps the long-lived session
|
|
227
230
|
// token out of <img> src query strings. Rotates on restart; links live ~5 min, so that's harmless.
|
|
228
231
|
const avatarSecret = randomBytes(32).toString('hex');
|
|
229
|
-
|
|
232
|
+
// Per-user advisor: a persistent assistant session controlling Orca on the user's behalf. Its cwd
|
|
233
|
+
// is a neutral per-user dir (alongside the DB, NOT a project checkout) so the per-program MCP config
|
|
234
|
+
// never pollutes a repo. Disabled for the in-memory DB (tests build their own AdvisorService).
|
|
235
|
+
const mcpUrl = `${orcaCli.url}/mcp`; // the daemon hosts the MCP server on its own /mcp route
|
|
236
|
+
const advisor = opts.dbPath === ':memory:' ? undefined : new AdvisorService({
|
|
237
|
+
spawn, tmux, users, config, fallback: { program: 'claude-code', model: 'sonnet' },
|
|
238
|
+
projectId: opts.project.id, url: orcaCli.url,
|
|
239
|
+
advisorDir: (id) => { const p = join(dirname(opts.dbPath), 'advisor', String(id)); mkdirSync(p, { recursive: true }); return p; },
|
|
240
|
+
prepareMcp: (program, cwd, token) => writeMcpConfig(program, cwd, token, mcpUrl),
|
|
241
|
+
});
|
|
242
|
+
const app = createServer({ tasks, readiness, missions, engine, spawn, tmux, bus, events, agents, project: opts.project, fallback: { program: 'claude-code', model: 'sonnet' }, clock: new SystemClock(), config, users, projects, userProjects, git, avatarsDir, avatarSecret, planJobs, decisionQueue, pilot, advisor });
|
|
230
243
|
// Root-cause recovery: after a daemon crash/restart, tasks left 'in_progress' whose tmux
|
|
231
244
|
// session is gone are zombies — revert them to 'open' so they can be picked up again. No grace
|
|
232
245
|
// or relaunch counter here: a restart isn't an agent death, so it shouldn't spend the budget.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { makeOrcaTools } from './tools.js';
|
|
5
|
+
/** Build an MCP server exposing the Orca toolset bound to one caller's token. Every tool delegates to
|
|
6
|
+
* `makeOrcaTools` → the shared `callOrcaApi` core, so there is no request logic here to maintain. */
|
|
7
|
+
function createOrcaMcpServer(deps) {
|
|
8
|
+
const tools = makeOrcaTools(deps);
|
|
9
|
+
const server = new McpServer({ name: 'orca', version: '1.0.0' });
|
|
10
|
+
const text = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data ?? null, null, 2) }] });
|
|
11
|
+
server.registerTool('orca_request', {
|
|
12
|
+
description: 'Call any Orca REST endpoint (full control). Generic escape hatch — every endpoint works without a dedicated tool.',
|
|
13
|
+
inputSchema: { method: z.string(), path: z.string(), body: z.unknown().optional() },
|
|
14
|
+
}, async (a) => text(await tools.orca_request({ method: a.method, path: a.path, body: a.body })));
|
|
15
|
+
server.registerTool('orca_tasks', { description: 'List all tasks.', inputSchema: {} }, async () => text(await tools.orca_tasks()));
|
|
16
|
+
server.registerTool('orca_create_task', {
|
|
17
|
+
description: 'Create a task.',
|
|
18
|
+
inputSchema: { title: z.string(), project_id: z.number().optional(), description: z.string().optional() },
|
|
19
|
+
}, async (a) => text(await tools.orca_create_task(a)));
|
|
20
|
+
server.registerTool('orca_plan', {
|
|
21
|
+
description: 'Plan a goal into an epic with phases (autopilot).',
|
|
22
|
+
inputSchema: { goal: z.string(), project_id: z.number().optional() },
|
|
23
|
+
}, async (a) => text(await tools.orca_plan(a)));
|
|
24
|
+
server.registerTool('orca_sessions', { description: 'List live agent sessions.', inputSchema: {} }, async () => text(await tools.orca_sessions()));
|
|
25
|
+
return server;
|
|
26
|
+
}
|
|
27
|
+
/** Stateless HTTP handler: a fresh server + transport per request, with the toolset bound to the
|
|
28
|
+
* request's bearer token, so each advisor connection acts with exactly its user's rights. */
|
|
29
|
+
export async function handleMcpRequest(req, deps) {
|
|
30
|
+
const server = createOrcaMcpServer(deps);
|
|
31
|
+
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
32
|
+
await server.connect(transport);
|
|
33
|
+
return transport.handleRequest(req);
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { callOrcaApi } from '../shared/apiClient.js';
|
|
2
|
+
export function makeOrcaTools(d) {
|
|
3
|
+
const call = d.call ?? callOrcaApi;
|
|
4
|
+
const req = async (method, path, body) => {
|
|
5
|
+
const r = await call(method, path, body, { url: d.url, token: d.token });
|
|
6
|
+
if (!r.ok)
|
|
7
|
+
throw new Error(`orca ${r.status}: ${r.text || JSON.stringify(r.data)}`);
|
|
8
|
+
return r.data;
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
orca_request: (a) => req(a.method, a.path, a.body),
|
|
12
|
+
orca_tasks: () => req('GET', '/tasks'),
|
|
13
|
+
orca_create_task: (a) => req('POST', '/tasks', a),
|
|
14
|
+
orca_plan: (a) => req('POST', '/tasks/plan', a),
|
|
15
|
+
orca_sessions: () => req('GET', '/sessions'),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
const ORCA = 'orca-';
|
|
2
2
|
const OVERSEER = 'overseer-';
|
|
3
3
|
const PILOT = 'pilot-';
|
|
4
|
+
const ADVISOR = 'advisor-';
|
|
4
5
|
/** Classify a live session name into its role + identity. Mirrors the spawn-time conventions:
|
|
5
|
-
* overseer → `orca-overseer-<missionId>`, pilot → `orca-pilot-<name>`,
|
|
6
|
+
* overseer → `orca-overseer-<missionId>`, pilot → `orca-pilot-<name>`, advisor → `orca-advisor-<userId>`,
|
|
7
|
+
* worker → `orca-<name>`. */
|
|
6
8
|
export function classifySession(name) {
|
|
7
9
|
const bare = name.startsWith(ORCA) ? name.slice(ORCA.length) : name;
|
|
8
10
|
if (bare.startsWith(OVERSEER))
|
|
9
11
|
return { name, role: 'overseer', agent: '', missionId: bare.slice(OVERSEER.length) };
|
|
10
12
|
if (bare.startsWith(PILOT))
|
|
11
13
|
return { name, role: 'pilot', agent: bare.slice(PILOT.length) };
|
|
14
|
+
if (bare.startsWith(ADVISOR)) {
|
|
15
|
+
const userId = Number(bare.slice(ADVISOR.length));
|
|
16
|
+
return { name, role: 'advisor', agent: '', userId: Number.isInteger(userId) ? userId : undefined };
|
|
17
|
+
}
|
|
12
18
|
return { name, role: 'agent', agent: bare };
|
|
13
19
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
You are {{userName}}'s personal Orca advisor — an always-available assistant that manages their Orca instance on their behalf. You run in an interactive terminal the user types into directly.
|
|
2
|
+
|
|
3
|
+
You have FULL control of Orca as this user. There are two equivalent ways to act, both authenticated by the ORCA_TOKEN already in your environment:
|
|
4
|
+
- The `orca_request` MCP tool (and the typed helpers orca_tasks / orca_create_task / orca_plan / orca_sessions) when MCP tools are available to you.
|
|
5
|
+
- The shell command `orca api <METHOD> <path> [jsonBody]`, which is always available. Examples:
|
|
6
|
+
orca api GET /tasks
|
|
7
|
+
orca api POST /tasks '{"title":"Fix the build","project_id":1}'
|
|
8
|
+
orca api POST /tasks/plan '{"goal":"Add dark mode","project_id":1}'
|
|
9
|
+
orca api GET /sessions
|
|
10
|
+
|
|
11
|
+
Both paths go through the same Orca REST API, so use whichever is handier.
|
|
12
|
+
|
|
13
|
+
Be proactive, concise, and friendly. Before any destructive action (deleting or cancelling tasks, killing sessions) confirm in plain language first. After you change something, briefly report what you did. Everything you do is scoped to this user's own projects and permissions.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function callOrcaApi(method, path, body, opts) {
|
|
2
|
+
const f = opts.fetchImpl ?? fetch;
|
|
3
|
+
const m = method.toUpperCase();
|
|
4
|
+
const headers = { authorization: `Bearer ${opts.token}` };
|
|
5
|
+
const hasBody = body !== undefined && m !== 'GET' && m !== 'HEAD';
|
|
6
|
+
if (hasBody)
|
|
7
|
+
headers['content-type'] = 'application/json';
|
|
8
|
+
const res = await f(`${opts.url}${path.startsWith('/') ? path : `/${path}`}`, {
|
|
9
|
+
method: m,
|
|
10
|
+
headers,
|
|
11
|
+
body: hasBody ? JSON.stringify(body) : undefined,
|
|
12
|
+
});
|
|
13
|
+
const text = await res.text();
|
|
14
|
+
let data;
|
|
15
|
+
// External/daemon response — parse defensively so a non-JSON body never throws here.
|
|
16
|
+
try {
|
|
17
|
+
data = text ? JSON.parse(text) : undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
data = undefined;
|
|
21
|
+
}
|
|
22
|
+
return { status: res.status, ok: res.ok, data, text };
|
|
23
|
+
}
|
package/dist/store/db.js
CHANGED
|
@@ -38,6 +38,10 @@ export function openDb(path) {
|
|
|
38
38
|
addColumn(db, 'users', 'email', "TEXT NOT NULL DEFAULT ''");
|
|
39
39
|
addColumn(db, 'users', 'avatar', "TEXT NOT NULL DEFAULT ''");
|
|
40
40
|
addColumn(db, 'users', 'default_exec', "TEXT NOT NULL DEFAULT ''");
|
|
41
|
+
// Per-user advisor: the remembered agent exec (empty = not set up yet) and whether it auto-starts
|
|
42
|
+
// on login. Additive so existing DBs gain them with sensible defaults (autostart on once chosen).
|
|
43
|
+
addColumn(db, 'users', 'advisor_exec', "TEXT NOT NULL DEFAULT ''");
|
|
44
|
+
addColumn(db, 'users', 'advisor_autostart', 'INTEGER NOT NULL DEFAULT 1');
|
|
41
45
|
// Token scope: spawned agents get a 'agent'-scoped token (worker/overseer/pilot verbs only),
|
|
42
46
|
// never the admin's full token. Pre-existing rows default to 'full' (interactive user sessions).
|
|
43
47
|
addColumn(db, 'auth_tokens', 'scope', "TEXT NOT NULL DEFAULT 'full'");
|
package/dist/store/schema.sql
CHANGED
|
@@ -32,7 +32,9 @@ CREATE TABLE IF NOT EXISTS users (
|
|
|
32
32
|
name TEXT NOT NULL DEFAULT '',
|
|
33
33
|
email TEXT NOT NULL DEFAULT '',
|
|
34
34
|
avatar TEXT NOT NULL DEFAULT '',
|
|
35
|
-
default_exec TEXT NOT NULL DEFAULT ''
|
|
35
|
+
default_exec TEXT NOT NULL DEFAULT '',
|
|
36
|
+
advisor_exec TEXT NOT NULL DEFAULT '',
|
|
37
|
+
advisor_autostart INTEGER NOT NULL DEFAULT 1
|
|
36
38
|
);
|
|
37
39
|
CREATE TABLE IF NOT EXISTS auth_tokens (
|
|
38
40
|
token TEXT PRIMARY KEY, user_id INTEGER NOT NULL,
|
package/dist/store/userStore.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
|
3
3
|
* own (e.g. tests). The live value comes from config.security.tokenTtlDays. */
|
|
4
4
|
const DEFAULT_TOKEN_TTL_DAYS = 30;
|
|
5
5
|
const ttlDays = (days) => typeof days === 'number' && Number.isFinite(days) && days >= 1 ? Math.floor(days) : DEFAULT_TOKEN_TTL_DAYS;
|
|
6
|
-
const mask = (r) => ({ id: r.id, username: r.username, created_at: r.created_at, is_admin: !!r.is_admin, allowed_execs: r.allowed_execs ? r.allowed_execs.split(',').filter(Boolean) : [], name: r.name ?? '', email: r.email ?? '', avatar: r.avatar ?? '', default_exec: r.default_exec ?? '' });
|
|
6
|
+
const mask = (r) => ({ id: r.id, username: r.username, created_at: r.created_at, is_admin: !!r.is_admin, allowed_execs: r.allowed_execs ? r.allowed_execs.split(',').filter(Boolean) : [], name: r.name ?? '', email: r.email ?? '', avatar: r.avatar ?? '', default_exec: r.default_exec ?? '', advisor_exec: r.advisor_exec ?? '', advisor_autostart: r.advisor_autostart === undefined ? true : !!r.advisor_autostart });
|
|
7
7
|
function hashPassword(password) {
|
|
8
8
|
const salt = randomBytes(16);
|
|
9
9
|
const hash = scryptSync(password, salt, 64);
|
|
@@ -69,6 +69,16 @@ export class UserStore {
|
|
|
69
69
|
this.db.prepare(`UPDATE users SET ${sets.join(', ')} WHERE id = @id`).run(p);
|
|
70
70
|
return this.get(id);
|
|
71
71
|
}
|
|
72
|
+
/** Remember which agent exec the user's advisor runs (chosen at first open). Empty = not set up. */
|
|
73
|
+
setAdvisorExec(id, exec) {
|
|
74
|
+
this.db.prepare('UPDATE users SET advisor_exec = ? WHERE id = ?').run(exec, id);
|
|
75
|
+
return this.get(id);
|
|
76
|
+
}
|
|
77
|
+
/** Toggle whether the advisor auto-starts on login. */
|
|
78
|
+
setAdvisorAutostart(id, on) {
|
|
79
|
+
this.db.prepare('UPDATE users SET advisor_autostart = ? WHERE id = ?').run(on ? 1 : 0, id);
|
|
80
|
+
return this.get(id);
|
|
81
|
+
}
|
|
72
82
|
/** Record the stored avatar filename (or '' to clear). */
|
|
73
83
|
setAvatar(id, filename) {
|
|
74
84
|
this.db.prepare('UPDATE users SET avatar = ? WHERE id = ?').run(filename, id);
|
|
@@ -150,6 +160,21 @@ export class UserStore {
|
|
|
150
160
|
return this.issueToken(userId, 'agent');
|
|
151
161
|
})();
|
|
152
162
|
}
|
|
163
|
+
/** The user's advisor token, reused across restarts. Stored under DB scope 'advisor' so it is
|
|
164
|
+
* isolated from login ('full') and worker ('agent') tokens — stopping/rotating the advisor never
|
|
165
|
+
* disturbs the user's web session. principalForToken maps any non-'agent' scope to full access, so
|
|
166
|
+
* the advisor acts with the user's own rights (mirrors ensureAgentToken's reuse-within-TTL shape). */
|
|
167
|
+
ensureAdvisorToken(userId, days) {
|
|
168
|
+
return this.db.transaction(() => {
|
|
169
|
+
const existing = this.db
|
|
170
|
+
.prepare(`SELECT token FROM auth_tokens WHERE user_id = ? AND scope = 'advisor' AND created_at > datetime('now', '-${ttlDays(days)} days') ORDER BY created_at DESC LIMIT 1`)
|
|
171
|
+
.get(userId);
|
|
172
|
+
if (existing?.token)
|
|
173
|
+
return existing.token;
|
|
174
|
+
this.db.prepare("DELETE FROM auth_tokens WHERE user_id = ? AND scope = 'advisor'").run(userId);
|
|
175
|
+
return this.issueToken(userId, 'advisor');
|
|
176
|
+
})();
|
|
177
|
+
}
|
|
153
178
|
revokeToken(token) {
|
|
154
179
|
this.db.prepare('DELETE FROM auth_tokens WHERE token = ?').run(token);
|
|
155
180
|
}
|
package/dist/tmux/driver.js
CHANGED
|
@@ -26,6 +26,14 @@ export class RealTmuxDriver {
|
|
|
26
26
|
throw new Error('sendKeys: keys must be a non-empty array of non-flag strings');
|
|
27
27
|
await run('tmux', ['send-keys', '-t', session, ...keys]);
|
|
28
28
|
}
|
|
29
|
+
/** Forward raw terminal bytes (xterm onData) to the pane verbatim. `-l` disables key-name lookup
|
|
30
|
+
* so control chars / ESC sequences pass through exactly as a real terminal would deliver them;
|
|
31
|
+
* `--` stops option parsing so `data` can safely begin with '-'. Powers the interactive advisor. */
|
|
32
|
+
async sendRaw(session, data) {
|
|
33
|
+
if (typeof data !== 'string' || data.length === 0)
|
|
34
|
+
return;
|
|
35
|
+
await run('tmux', ['send-keys', '-t', session, '-l', '--', data]);
|
|
36
|
+
}
|
|
29
37
|
async capturePane(session, tailLines) {
|
|
30
38
|
try {
|
|
31
39
|
const { stdout } = await run('tmux', ['capture-pane', '-p', '-t', session, '-S', `-${tailLines}`], { maxBuffer: 512 * 1024 });
|
package/dist/tmux/fakeDriver.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export class FakeTmuxDriver {
|
|
2
2
|
panes = new Map();
|
|
3
3
|
keys = new Map();
|
|
4
|
+
raw = new Map();
|
|
4
5
|
commands = new Map();
|
|
5
6
|
setPane(session, text) { this.panes.set(session, text); }
|
|
6
7
|
sentKeys(session) { return this.keys.get(session) ?? []; }
|
|
@@ -9,11 +10,19 @@ export class FakeTmuxDriver {
|
|
|
9
10
|
sizeFor(session) { return this.sizes.get(session); }
|
|
10
11
|
async spawn(session, opts) { this.panes.set(session, ''); this.commands.set(session, opts.command); }
|
|
11
12
|
async resize(session, cols, rows) { this.sizes.set(session, { cols, rows }); }
|
|
13
|
+
sentRaw(session) { return this.raw.get(session) ?? []; }
|
|
12
14
|
async sendKeys(session, keys) {
|
|
13
15
|
const arr = this.keys.get(session) ?? [];
|
|
14
16
|
arr.push(keys);
|
|
15
17
|
this.keys.set(session, arr);
|
|
16
18
|
}
|
|
19
|
+
async sendRaw(session, data) {
|
|
20
|
+
if (typeof data !== 'string' || data.length === 0)
|
|
21
|
+
return;
|
|
22
|
+
const arr = this.raw.get(session) ?? [];
|
|
23
|
+
arr.push(data);
|
|
24
|
+
this.raw.set(session, arr);
|
|
25
|
+
}
|
|
17
26
|
async capturePane(session, _tail) { return this.panes.get(session) ?? ''; }
|
|
18
27
|
async capturePaneAnsi(session, _tail) { return this.panes.get(session) ?? ''; }
|
|
19
28
|
async list() { return [...this.panes.keys()]; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orcasynth",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
4
4
|
"description": "ORCA — an autonomous coding-agent orchestrator with an autopilot overseer, daemon API and web UI. Install globally and run `orca`.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,8 +47,10 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@clack/prompts": "^1.6.0",
|
|
49
49
|
"@hono/node-server": "^1.13.0",
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
51
|
"better-sqlite3": "^11.0.0",
|
|
51
|
-
"hono": "^4.6.0"
|
|
52
|
+
"hono": "^4.6.0",
|
|
53
|
+
"zod": "^4.4.3"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@types/better-sqlite3": "^7.6.0",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
You are {{userName}}'s personal Orca advisor — an always-available assistant that manages their Orca instance on their behalf. You run in an interactive terminal the user types into directly.
|
|
2
|
+
|
|
3
|
+
You have FULL control of Orca as this user. There are two equivalent ways to act, both authenticated by the ORCA_TOKEN already in your environment:
|
|
4
|
+
- The `orca_request` MCP tool (and the typed helpers orca_tasks / orca_create_task / orca_plan / orca_sessions) when MCP tools are available to you.
|
|
5
|
+
- The shell command `orca api <METHOD> <path> [jsonBody]`, which is always available. Examples:
|
|
6
|
+
orca api GET /tasks
|
|
7
|
+
orca api POST /tasks '{"title":"Fix the build","project_id":1}'
|
|
8
|
+
orca api POST /tasks/plan '{"goal":"Add dark mode","project_id":1}'
|
|
9
|
+
orca api GET /sessions
|
|
10
|
+
|
|
11
|
+
Both paths go through the same Orca REST API, so use whichever is handier.
|
|
12
|
+
|
|
13
|
+
Be proactive, concise, and friendly. Before any destructive action (deleting or cancelling tasks, killing sessions) confirm in plain language first. After you change something, briefly report what you did. Everything you do is scoped to this user's own projects and permissions.
|
package/web-dist/.next/BUILD_ID
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Vc91G_QEn5enhyqIYkLiV
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
"static/chunks/0cz1d0mv5g_q7.js"
|
|
8
8
|
],
|
|
9
9
|
"lowPriorityFiles": [
|
|
10
|
-
"static
|
|
11
|
-
"static
|
|
12
|
-
"static
|
|
10
|
+
"static/Vc91G_QEn5enhyqIYkLiV/_buildManifest.js",
|
|
11
|
+
"static/Vc91G_QEn5enhyqIYkLiV/_ssgManifest.js",
|
|
12
|
+
"static/Vc91G_QEn5enhyqIYkLiV/_clientMiddlewareManifest.js"
|
|
13
13
|
],
|
|
14
14
|
"rootMainFiles": [
|
|
15
15
|
"static/chunks/310vm2bl3xxpt.js",
|