@rho-workspace/rho 0.5.5 → 0.5.7
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/agent-connect.d.ts +42 -0
- package/dist/agent-connect.js +68 -0
- package/dist/answer-board.js +3 -3
- package/dist/bridge.js +6 -1
- package/dist/browser-access.d.ts +1 -0
- package/dist/browser-access.js +5 -3
- package/dist/capabilities.d.ts +21 -5
- package/dist/capabilities.js +6 -4
- package/dist/cli.js +113 -18
- package/dist/composition.d.ts +1 -1
- package/dist/composition.js +13 -3
- package/dist/connect-cli.d.ts +50 -0
- package/dist/connect-cli.js +128 -0
- package/dist/contract.generated.d.ts +1437 -381
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +1160 -267
- package/dist/conversation-presentation.d.ts +9 -2
- package/dist/conversation-presentation.js +3 -1
- package/dist/conversations.d.ts +306 -7
- package/dist/conversations.js +208 -22
- package/dist/discovery.js +3 -3
- package/dist/extension-contract.d.ts +19 -0
- package/dist/extension-contract.js +15 -6
- package/dist/extension-markets.d.ts +74 -0
- package/dist/extension-markets.js +166 -0
- package/dist/extensions.d.ts +29 -1
- package/dist/extensions.js +13 -4
- package/dist/external-work.d.ts +22 -0
- package/dist/external-work.js +91 -0
- package/dist/plugin-cli.js +75 -3
- package/dist/plugin-inventory.d.ts +56 -0
- package/dist/plugin-inventory.js +41 -0
- package/dist/plugin-inventory.json +1581 -102
- package/dist/plugin-presentation.d.ts +105 -2
- package/dist/plugin-presentation.js +59 -5
- package/dist/plugins.d.ts +14 -3
- package/dist/plugins.js +12 -6
- package/dist/project-notes.js +1 -1
- package/dist/protocol.d.ts +7 -0
- package/dist/protocol.js +40 -15
- package/dist/replay.js +4 -0
- package/dist/response-contracts.js +26 -11
- package/dist/running.d.ts +10 -0
- package/dist/running.js +45 -0
- package/dist/server.d.ts +4 -10
- package/dist/server.js +140 -14
- package/dist/state.d.ts +8 -0
- package/dist/state.js +8 -0
- package/dist/studio-layout.js +3 -3
- package/dist/task-frame.d.ts +1 -1
- package/dist/timeline-events.d.ts +1 -0
- package/dist/timeline-events.js +8 -1
- package/dist/ui-context.d.ts +73 -0
- package/dist/ui-context.js +98 -0
- package/dist/ui-contract.d.ts +23 -0
- package/dist/web/.vite/manifest.json +135 -76
- package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
- package/dist/web/assets/choices-D8vGwsl2.js +1 -0
- package/dist/web/assets/{console-BSjLGe0T.js → console-Dyuo1q4N.js} +4 -4
- package/dist/web/assets/{dist-Tzoe_zm-.js → dist-CSiCyBml.js} +1 -1
- package/dist/web/assets/{dist-Cw8hRIlT.js → dist-D9FLb9Hm.js} +1 -1
- package/dist/web/assets/{dist-foH9IKc6.js → dist-FXbTBwML.js} +1 -1
- package/dist/web/assets/{dist-Buk0lwzI.js → dist-l3DUhbw5.js} +1 -1
- package/dist/web/assets/{explore-in-BVUy3ZW_.js → explore-in-BfSFGRh2.js} +1 -1
- package/dist/web/assets/graph-BU2_o5uA.js +2 -0
- package/dist/web/assets/index-C16mXVvk.css +1 -0
- package/dist/web/assets/index-CWg9XYXx.js +952 -0
- package/dist/web/assets/markdown.reader-Ct2-e2Zt.png +0 -0
- package/dist/web/assets/{notes-meaning-pVbbQUGP.js → notes-meaning-T2m5zQfv.js} +1 -1
- package/dist/web/assets/overview-PzfLJCo-.js +6 -0
- package/dist/web/assets/preview-BguEdsza.js +1 -0
- package/dist/web/assets/previews-pWNik-2g.js +3 -0
- package/dist/web/assets/{review-CMxQlQ-C.js → review-Dy_85lcH.js} +1 -1
- package/dist/web/assets/rho.editor-CCJeyHma.png +0 -0
- package/dist/web/assets/rho.flow-WkMj6fRS.png +0 -0
- package/dist/web/assets/rho.media-Cc6amodO.png +0 -0
- package/dist/web/assets/rho.notebook-6vsZKGDq.png +0 -0
- package/dist/web/assets/rho.object-BGoeXjte.png +0 -0
- package/dist/web/assets/rho.plots-yIiISCkb.png +0 -0
- package/dist/web/assets/rho.scatter-DwHxesCs.png +0 -0
- package/dist/web/assets/rho.servers-DKf_mnNq.png +0 -0
- package/dist/web/assets/rho.table-DWOGaWDa.png +0 -0
- package/dist/web/assets/rho.variables-CKcWczhQ.png +0 -0
- package/dist/web/assets/{skills-BFNSyrnD.js → skills-Dyz7mBlc.js} +1 -1
- package/dist/web/assets/{skills-work-BGPSnF_I.js → skills-work-BEGqlCkx.js} +1 -1
- package/dist/web/assets/studio-process-D-ykbC4D.js +2 -0
- package/dist/web/assets/timeline-events-DKTho9fU.js +1 -0
- package/dist/web/assets/work-turns-DSabCogR.js +1 -0
- package/dist/web/assets/{workspace-CWlkjLNx.js → workspace-DfjBL--r.js} +2 -2
- package/dist/web/composition.json +135 -80
- package/dist/web/index.html +3 -3
- package/dist/web/renderer.json +1 -1
- package/dist/web-assets.js +2 -2
- package/dist/work-graph.d.ts +11 -1
- package/dist/work-graph.js +16 -5
- package/dist/workspace.d.ts +2 -2
- package/dist/workspace.js +8 -7
- package/docs/FRONTEND.md +71 -21
- package/docs/HEADLESS.md +127 -24
- package/docs/THIRD-PARTY-PLUGINS.md +75 -3
- package/node_modules/@rho-workspace/plugin-agent/README.md +34 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +32 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +54 -11
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +29 -3
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +11 -5
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +12 -1
- package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
- package/node_modules/@rho-workspace/plugin-agent/package.json +2 -1
- package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
- package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
- package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
- package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
- package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
- package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
- package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
- package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
- package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
- package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
- package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
- package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
- package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
- package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
- package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
- package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
- package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
- package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
- package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
- package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
- package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
- package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
- package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
- package/node_modules/hono/dist/cjs/utils/url.js +1 -1
- package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
- package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
- package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
- package/node_modules/hono/dist/jsx/base.js +49 -32
- package/node_modules/hono/dist/jsx/components.js +2 -2
- package/node_modules/hono/dist/jsx/constants.js +2 -1
- package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
- package/node_modules/hono/dist/middleware/etag/index.js +3 -1
- package/node_modules/hono/dist/middleware/language/language.js +1 -1
- package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
- package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
- package/node_modules/hono/dist/utils/cookie.js +2 -2
- package/node_modules/hono/dist/utils/mime.js +1 -1
- package/node_modules/hono/dist/utils/url.js +1 -1
- package/node_modules/hono/package.json +1 -1
- package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
- package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
- package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
- package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
- package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
- package/node_modules/jose/package.json +1 -1
- package/package.json +6 -4
- package/protocol/index.json +106 -64
- package/protocol/operations/agent.sessions.list.json +57 -3
- package/protocol/operations/agent.sessions.read.json +61 -4
- package/protocol/operations/capability.list.json +382 -6
- package/protocol/operations/composition.apply.json +10 -2
- package/protocol/operations/composition.delete.json +845 -0
- package/protocol/operations/composition.read.json +10 -2
- package/protocol/operations/composition.undo.json +10 -2
- package/protocol/operations/conversation.agents.add.json +346 -0
- package/protocol/operations/conversation.agents.remove.json +340 -0
- package/protocol/operations/conversation.configure.json +29 -0
- package/protocol/operations/conversation.decide.json +225 -0
- package/protocol/operations/conversation.list.json +29 -0
- package/protocol/operations/conversation.permission.json +225 -0
- package/protocol/operations/conversation.read.json +225 -0
- package/protocol/operations/conversation.restore.json +29 -0
- package/protocol/operations/conversation.send.json +294 -0
- package/protocol/operations/conversation.settings.check.json +226 -416
- package/protocol/operations/conversation.settings.read.json +224 -411
- package/protocol/operations/conversation.settings.update.json +248 -475
- package/protocol/operations/conversation.start.json +313 -6
- package/protocol/operations/conversation.stop.json +225 -0
- package/protocol/operations/conversation.trash.json +29 -0
- package/protocol/operations/extension.describe.json +39 -0
- package/protocol/operations/extension.install.json +62 -0
- package/protocol/operations/extension.list.json +164 -1
- package/protocol/operations/extension.market.change.json +210 -0
- package/protocol/operations/figure.read.json +265 -0
- package/protocol/operations/figure.select.json +157 -0
- package/protocol/operations/kernel.interrupt.json +119 -0
- package/protocol/operations/kernel.read.json +119 -0
- package/protocol/operations/kernel.run.json +119 -0
- package/protocol/operations/observation.snapshot.json +445 -8
- package/protocol/operations/project.openFile.json +10 -2
- package/protocol/operations/session.grant.json +4 -0
- package/protocol/operations/session.instructions.json +43 -0
- package/protocol/operations/shared.json +1 -0
- package/protocol/operations/view.close.json +10 -2
- package/protocol/operations/view.protection.set.json +10 -2
- package/vendor/composition.json +174 -109
- package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
- package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
- package/web/theme.css +17 -0
- package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
- package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
- package/dist/web/assets/choices-b0XhrGQV.js +0 -1
- package/dist/web/assets/graph-1OdIJ9l7.js +0 -2
- package/dist/web/assets/index-CsC3sTRq.css +0 -1
- package/dist/web/assets/index-CxmItXqa.js +0 -891
- package/dist/web/assets/overview-B9UtVJ32.js +0 -6
- package/dist/web/assets/preview-CksD_5KS.js +0 -1
- package/dist/web/assets/studio-process-CsRoTHOh.js +0 -2
- package/dist/web/assets/timeline-events-BbuuFWHV.js +0 -1
- package/dist/web/assets/work-turns-BvJlIMmA.js +0 -1
- package/vendor/rho-workspace-plugin-agent-0.4.29.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.0.tgz +0 -0
package/dist/agent-connect.d.ts
CHANGED
|
@@ -12,6 +12,48 @@ export declare const connectLimits: {
|
|
|
12
12
|
};
|
|
13
13
|
export declare function mcpName(root: string): string;
|
|
14
14
|
export declare function mcpCommand(connectionFile: string): string[];
|
|
15
|
+
export declare function clientEntries(name: string, command: string[], root: string, env?: NodeJS.ProcessEnv): {
|
|
16
|
+
claude: string;
|
|
17
|
+
codex: string;
|
|
18
|
+
opencode: {
|
|
19
|
+
file: string;
|
|
20
|
+
merge: {
|
|
21
|
+
mcp: {
|
|
22
|
+
[name]: {
|
|
23
|
+
type: string;
|
|
24
|
+
command: string[];
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
dsh: {
|
|
31
|
+
file: string;
|
|
32
|
+
workspace: {
|
|
33
|
+
plugin: string;
|
|
34
|
+
minimumVersion: string;
|
|
35
|
+
guide: string;
|
|
36
|
+
mergeConfig: {
|
|
37
|
+
command: string;
|
|
38
|
+
args: string[];
|
|
39
|
+
connectionFiles: (string | undefined)[];
|
|
40
|
+
};
|
|
41
|
+
instructions: string;
|
|
42
|
+
};
|
|
43
|
+
append: string;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export declare const defaultStateDirectory: () => string;
|
|
47
|
+
export declare function launcher(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): Promise<string[]>;
|
|
48
|
+
export declare function commandLine(argv: string[], platform?: NodeJS.Platform): string;
|
|
49
|
+
export declare function connectionInstructions(project: {
|
|
50
|
+
id: string;
|
|
51
|
+
root: string;
|
|
52
|
+
}, stateDirectory: string): Promise<{
|
|
53
|
+
request: string;
|
|
54
|
+
prompt: string;
|
|
55
|
+
command: string[];
|
|
56
|
+
}>;
|
|
15
57
|
export declare function addToAgent(agent: OutsideAgent, binary: string, root: string, name: string, command: string[], limits?: {
|
|
16
58
|
timeoutMs: number;
|
|
17
59
|
outputBytes: number;
|
package/dist/agent-connect.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { Fault } from '@rho-workspace/plugin-contracts';
|
|
7
|
+
import { announced } from './running.js';
|
|
5
8
|
export const outsideLabel = { 'claude-code': 'Claude Code', codex: 'Codex' };
|
|
6
9
|
// The Agent's CLI is the protected resource: each `mcp` command runs for at most timeoutMs and
|
|
7
10
|
// keeps at most outputBytes of what it printed; past either, it is stopped and the step fails
|
|
@@ -13,6 +16,71 @@ export function mcpName(root) {
|
|
|
13
16
|
export function mcpCommand(connectionFile) {
|
|
14
17
|
return [process.execPath, fileURLToPath(new URL('./cli.js', import.meta.url)), '--mcp', connectionFile];
|
|
15
18
|
}
|
|
19
|
+
// Ready entries for the clients whose own mechanism has been used with Rho, so an Agent need not
|
|
20
|
+
// search its documentation for it: Claude Code's and Codex's `mcp add`, OpenCode's project
|
|
21
|
+
// configuration and DeepSeek Harness's profile patch (no `dsh mcp` command exists). Each holds
|
|
22
|
+
// the same name, command and arguments as `mcp`; any other client registers those itself.
|
|
23
|
+
export function clientEntries(name, command, root, env = process.env) {
|
|
24
|
+
const quote = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
25
|
+
const yaml = (value) => JSON.stringify(value);
|
|
26
|
+
return {
|
|
27
|
+
claude: ['claude', 'mcp', 'add', name, '--', ...command].map(quote).join(' '),
|
|
28
|
+
codex: ['codex', 'mcp', 'add', name, '--', ...command].map(quote).join(' '),
|
|
29
|
+
opencode: { file: path.join(root, '.opencode', 'opencode.json'), merge: { mcp: { [name]: { type: 'local', command, enabled: true } } } },
|
|
30
|
+
dsh: { file: path.join(env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'profiles', 'desktop', 'cordis.patch.yml'),
|
|
31
|
+
workspace: { plugin: 'dsh-rho', minimumVersion: '0.1.6', guide: 'https://yulab-smu.top/Rho/docs/reference/deepseek-harness.html',
|
|
32
|
+
mergeConfig: { command: command[0], args: command.slice(1, -2), connectionFiles: [command.at(-1)] },
|
|
33
|
+
instructions: 'For conversations and process views, use the dsh-rho plugin. Merge this config into its existing entry, appending the connection file without removing other projects. The append entry below supplies MCP tools only; it does not observe Harness conversations.' },
|
|
34
|
+
append: ['- insert:', ` - id: ${yaml('mcp-' + name)}`, ' name: "@deepseek-ai/dsh-mcp-client"', ' config:', ` serverName: ${yaml(name)}`, ' transport: "stdio"',
|
|
35
|
+
` command: ${yaml(command[0])}`, ' args:', ...command.slice(1).map(arg => ` - ${yaml(arg)}`)].join('\n') + '\n' },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// The state folder of a Rho started without --state-dir, which `rho --stop` and `rho connect`
|
|
39
|
+
// also take without it.
|
|
40
|
+
export const defaultStateDirectory = () => path.join(os.homedir(), 'Rho', 'state');
|
|
41
|
+
// How a shell runs this Rho: `rho` when the first `rho` on PATH is this installation (npm's
|
|
42
|
+
// link, or its .cmd shim on Windows), otherwise this Node with this cli.js.
|
|
43
|
+
export async function launcher(env = process.env, platform = process.platform) {
|
|
44
|
+
const cli = fileURLToPath(new URL('./cli.js', import.meta.url)), real = await fs.realpath(cli).catch(() => cli);
|
|
45
|
+
for (const dir of (env.PATH ?? '').split(path.delimiter).filter(Boolean)) {
|
|
46
|
+
const candidate = path.join(dir, platform === 'win32' ? 'rho.cmd' : 'rho');
|
|
47
|
+
let target;
|
|
48
|
+
try {
|
|
49
|
+
if (platform === 'win32') {
|
|
50
|
+
const shim = (await fs.readFile(candidate, 'utf8')).match(/"%dp0%\\([^"]*?cli\.js)"/i);
|
|
51
|
+
target = shim ? await fs.realpath(path.resolve(dir, shim[1])).catch(() => null) : null;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
target = await fs.realpath(candidate);
|
|
55
|
+
await fs.access(candidate, fs.constants.X_OK).catch(() => { target = null; });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
return target === real ? ['rho'] : [process.execPath, cli];
|
|
62
|
+
}
|
|
63
|
+
return [process.execPath, cli];
|
|
64
|
+
}
|
|
65
|
+
// A command line to read and paste: plain words as they are, anything else in double quotes.
|
|
66
|
+
export function commandLine(argv, platform = process.platform) {
|
|
67
|
+
return argv.map(arg => /^[A-Za-z0-9_./:@=+,-]+$/.test(arg) ? arg : platform === 'win32' ? `"${arg}"` : `"${arg.replace(/(["\\$`])/g, '\\$1')}"`).join(' ');
|
|
68
|
+
}
|
|
69
|
+
// The prompt names the project and one command; `rho connect` tells the Agent the rest. It names
|
|
70
|
+
// --state-dir only when `rho connect` would not find this Rho itself: a state folder other than
|
|
71
|
+
// ~/Rho/state that no running note points to (a service started without the CLI). Reading
|
|
72
|
+
// these instructions starts nothing, grants no access and includes no credential bytes.
|
|
73
|
+
export async function connectionInstructions(project, stateDirectory) {
|
|
74
|
+
const found = path.resolve(stateDirectory) === path.resolve(defaultStateDirectory()) || await announced(stateDirectory);
|
|
75
|
+
const state = found ? [] : ['--state-dir', stateDirectory];
|
|
76
|
+
const command = [...await launcher(), 'connect', project.root, ...state];
|
|
77
|
+
const request = `Connect yourself to my Rho project ${JSON.stringify(path.basename(project.root))} so we can work on it together.`;
|
|
78
|
+
const prompt = [request, '',
|
|
79
|
+
'Rho is already running on this machine. Run this command with a short, stable name for yourself in place of NAME, and follow its reply:',
|
|
80
|
+
'', commandLine([...command, '--as', 'NAME']),
|
|
81
|
+
].join('\n');
|
|
82
|
+
return { request, prompt, command };
|
|
83
|
+
}
|
|
16
84
|
function run(binary, args, cwd, limits) {
|
|
17
85
|
return new Promise((resolve, reject) => {
|
|
18
86
|
const child = spawn(binary, args, { cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
package/dist/answer-board.js
CHANGED
|
@@ -27,7 +27,7 @@ export function answerBoard(graph, turn, limits = boardLimits) {
|
|
|
27
27
|
const from = byId.get(e.from), to = byId.get(e.to);
|
|
28
28
|
if (!from || !to || to.kind !== 'file')
|
|
29
29
|
continue;
|
|
30
|
-
const made = (from.kind === 'question' && (e.label === 'wrote' || e.label === 'edited' || e.label === 'changed meanwhile')) || (from.kind === 'run' && e.label === 'changed while it ran');
|
|
30
|
+
const made = (from.kind === 'question' && (e.label === 'wrote' || e.label === 'edited' || e.label === 'changed meanwhile' || e.label === 'reported result')) || (from.kind === 'run' && e.label === 'changed while it ran');
|
|
31
31
|
// A file of no kind the project shows — an Agent's own settings, a cache — is no output of the work.
|
|
32
32
|
if (!made || !to.category)
|
|
33
33
|
continue;
|
|
@@ -48,7 +48,7 @@ export function answerBoard(graph, turn, limits = boardLimits) {
|
|
|
48
48
|
};
|
|
49
49
|
const madeBy = (o) => {
|
|
50
50
|
const from = byId.get(o.edge.from);
|
|
51
|
-
return from.kind === 'run' ? 'Changed while ' + quote(from.command ?? from.title) + ' ran' : o.edge.label === 'changed meanwhile' ? 'Changed on disk during this message' : o.edge.label === 'edited' ? 'Edited by the Agent' : 'Written by the Agent';
|
|
51
|
+
return from.kind === 'run' ? 'Changed while ' + quote(from.command ?? from.title) + ' ran' : o.edge.label === 'reported result' ? 'Reported in the Agent’s answer' : o.edge.label === 'changed meanwhile' ? 'Changed on disk during this message' : o.edge.label === 'edited' ? 'Edited by the Agent' : 'Written by the Agent';
|
|
52
52
|
};
|
|
53
53
|
// Results the answer names lead, then the newest; a figure answers before a table does.
|
|
54
54
|
const results = [...outputs.values()].filter(o => isResult(o.node))
|
|
@@ -80,7 +80,7 @@ export function answerBoard(graph, turn, limits = boardLimits) {
|
|
|
80
80
|
continue;
|
|
81
81
|
if (e.to === question.id && e.label === 'read')
|
|
82
82
|
inputs.set(from.id, { node: from, why: 'Read by the Agent' });
|
|
83
|
-
else if (e.label === 'named' && runs.has(e.to) && !inputs.has(from.id))
|
|
83
|
+
else if (e.label === 'named' && (runs.has(e.to) || own.some(link => link.label === 'reported result') && byId.get(e.to)?.kind === 'run') && !inputs.has(from.id))
|
|
84
84
|
inputs.set(from.id, { node: from, why: 'Named by ' + quote(byId.get(e.to).command ?? '') });
|
|
85
85
|
}
|
|
86
86
|
add('input', [...inputs.values()].sort((a, b) => Number(b.node.category === 'data') - Number(a.node.category === 'data') || a.node.at - b.node.at));
|
package/dist/bridge.js
CHANGED
|
@@ -59,10 +59,15 @@ const stopFile = (root) => path.join(root, '.rho', 'host.stop');
|
|
|
59
59
|
// Each argument reaches cmd as a variable inside quotes, where neither its expansion nor & ^ ( )
|
|
60
60
|
// apply, and a Windows path holds no quote; a backslash that would end up before the closing
|
|
61
61
|
// quote (a drive root) is doubled, so it does not escape it.
|
|
62
|
+
// R on Windows sets these locale categories from the environment, and Windows has no C.UTF-8: given
|
|
63
|
+
// one, as an Agent's Git Bash gives LC_CTYPE, a run's R warned at startup and printed "3 × 2" as
|
|
64
|
+
// "3 <U+00D7> 2". The Host's runs take the system's own locale instead, UTF-8 there.
|
|
65
|
+
const posixUtf8 = /^(C|POSIX)\.UTF-?8$/i, rLocale = /^LC_(ALL|CTYPE|COLLATE|TIME|MONETARY)$/;
|
|
62
66
|
function startOnWindows(binary, args, root) {
|
|
63
67
|
const values = [binary, ...args].map(value => value.endsWith('\\') ? value + '\\' : value);
|
|
64
68
|
const line = values.map((_, i) => `"%RHO_HOST_ARG${i}%"`).join(' ');
|
|
65
|
-
const
|
|
69
|
+
const inherited = Object.entries(process.env).filter(([name, value]) => !(rLocale.test(name.toUpperCase()) && posixUtf8.test(value ?? '')));
|
|
70
|
+
const env = { ...Object.fromEntries(inherited), RHO_CORE_STOP_FILE: stopFile(root), ...Object.fromEntries(values.map((value, i) => ['RHO_HOST_ARG' + i, value])) };
|
|
66
71
|
return spawn(path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'cmd.exe'), ['/d', '/v:off', '/s', '/c', `"${line}"`], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, windowsVerbatimArguments: true, env });
|
|
67
72
|
}
|
|
68
73
|
export class CoreBridge {
|
package/dist/browser-access.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare const localBrowserId = "browser:local";
|
|
|
12
12
|
export declare const consoleMethods: string[];
|
|
13
13
|
export declare const kernelMethods: string[];
|
|
14
14
|
export declare const skillsMethods: string[];
|
|
15
|
+
export declare const figureMethods: string[];
|
|
15
16
|
export declare function localBrowserPrincipal(root: string): Principal;
|
|
16
17
|
export declare class BrowserAccess {
|
|
17
18
|
private state;
|
package/dist/browser-access.js
CHANGED
|
@@ -23,7 +23,7 @@ export const browserMethods = ['protocol.describe', 'protocol.validate', 'projec
|
|
|
23
23
|
'action.submit', 'action.output', 'action.cancel',
|
|
24
24
|
// The person's own layout commands (split, move, close, divider, undo), conditional on the
|
|
25
25
|
// version they saw; protected views stay protected.
|
|
26
|
-
'composition.apply', 'composition.undo',
|
|
26
|
+
'composition.apply', 'composition.undo', 'composition.delete',
|
|
27
27
|
// A chart's points: the same rows data.page already reads, bounded and read-only.
|
|
28
28
|
'data.plot',
|
|
29
29
|
// A run's script, outputs and provenance written as ordinary files in a new project folder.
|
|
@@ -37,7 +37,7 @@ export const browserMethods = ['protocol.describe', 'protocol.validate', 'projec
|
|
|
37
37
|
// Only the person at this machine talks to a conversation Rho starts: it runs their own Claude
|
|
38
38
|
// Code with their model credentials. Only they add Rho to their own Agent's configuration. A
|
|
39
39
|
// narrower, expiring link never receives these.
|
|
40
|
-
export const conversationMethods = ['agent.sessions.list', 'agent.sessions.read', 'session.connect', 'conversation.start', 'conversation.send', 'conversation.stop', 'conversation.decide', 'conversation.read', 'conversation.list', 'conversation.trash', 'conversation.restore', 'conversation.delete', 'conversation.permission', 'conversation.configure', 'conversation.settings.read', 'conversation.settings.update', 'conversation.settings.check', 'project.work.read', 'evidence.preview', 'evidence.export'];
|
|
40
|
+
export const conversationMethods = ['agent.sessions.list', 'agent.sessions.read', 'session.instructions', 'session.connect', 'conversation.start', 'conversation.send', 'conversation.stop', 'conversation.decide', 'conversation.read', 'conversation.list', 'conversation.trash', 'conversation.restore', 'conversation.delete', 'conversation.permission', 'conversation.configure', 'conversation.settings.read', 'conversation.settings.update', 'conversation.settings.check', 'conversation.agents.add', 'conversation.agents.remove', 'project.work.read', 'evidence.preview', 'evidence.export'];
|
|
41
41
|
// The person at this machine. A tab this service served is authorized by the request itself:
|
|
42
42
|
// same origin, loopback only, no link, no code and no expiry. `--pair` stays an explicit,
|
|
43
43
|
// narrower grant for a shared or subfolder-scoped tab.
|
|
@@ -47,10 +47,12 @@ export const consoleMethods = ['console.run', 'console.list', 'console.events',
|
|
|
47
47
|
// The project's live R and Python sessions, which the person shares with their Agent.
|
|
48
48
|
export const kernelMethods = ['kernel.run', 'kernel.read', 'kernel.events', 'kernel.inspect', 'kernel.plot', 'kernel.interrupt', 'kernel.stop', 'kernel.places', 'kernel.place.set', 'kernel.place.check', 'kernel.place.list', 'kernel.place.copy', 'kernel.servers', 'kernel.server.skill'];
|
|
49
49
|
export const skillsMethods = ['skills.list', 'skills.read', 'skills.change', 'skills.undo'];
|
|
50
|
+
// The figures the live R session drew: the person reads them and keeps the marks they ask about.
|
|
51
|
+
export const figureMethods = ['figure.read', 'figure.select'];
|
|
50
52
|
export function localBrowserPrincipal(root) {
|
|
51
53
|
// The empty token hash can never match a request digest, so no header impersonates this
|
|
52
54
|
// identity: it is chosen because the request carried no credential at all.
|
|
53
|
-
return { id: localBrowserId, kind: 'browser', tokenHash: '', projectRoot: root, methods: [...extensionMethods, ...browserMethods, ...conversationMethods, ...consoleMethods, ...kernelMethods, ...skillsMethods] };
|
|
55
|
+
return { id: localBrowserId, kind: 'browser', tokenHash: '', projectRoot: root, methods: [...extensionMethods, ...browserMethods, ...conversationMethods, ...consoleMethods, ...kernelMethods, ...skillsMethods, ...figureMethods] };
|
|
54
56
|
}
|
|
55
57
|
export class BrowserAccess {
|
|
56
58
|
state;
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -157,7 +157,7 @@ export declare class Capabilities {
|
|
|
157
157
|
enabled: string[];
|
|
158
158
|
}, author: string): Promise<{
|
|
159
159
|
version: number;
|
|
160
|
-
enabled: ("media" | "r" | "agent" | "
|
|
160
|
+
enabled: ("media" | "r" | "files" | "agent" | "figures" | "skills" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel")[];
|
|
161
161
|
receipt: CapabilityReceipt;
|
|
162
162
|
}>;
|
|
163
163
|
private pluginStatus;
|
|
@@ -192,7 +192,7 @@ export declare class Capabilities {
|
|
|
192
192
|
}[];
|
|
193
193
|
configuration: {
|
|
194
194
|
version: number;
|
|
195
|
-
enabled: ("media" | "r" | "agent" | "
|
|
195
|
+
enabled: ("media" | "r" | "files" | "agent" | "figures" | "skills" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel")[];
|
|
196
196
|
retainedReceipts: number;
|
|
197
197
|
retainedFromVersion: number;
|
|
198
198
|
maxReceipts: number;
|
|
@@ -213,13 +213,23 @@ export declare class Capabilities {
|
|
|
213
213
|
effect?: "native" | "file" | "document" | "read" | "metadata" | undefined;
|
|
214
214
|
method: string;
|
|
215
215
|
}[];
|
|
216
|
-
skills: SkillDeclaration
|
|
216
|
+
skills: (SkillDeclaration & {
|
|
217
|
+
owner: string;
|
|
218
|
+
})[];
|
|
217
219
|
agent?: {
|
|
218
220
|
id: string;
|
|
219
221
|
transport: string;
|
|
220
222
|
modelLoop: string;
|
|
221
223
|
contextDelivery: string;
|
|
222
224
|
proactivePush: string;
|
|
225
|
+
mcp: {
|
|
226
|
+
entry: string;
|
|
227
|
+
transport: string;
|
|
228
|
+
tools: {
|
|
229
|
+
name: string;
|
|
230
|
+
description: string;
|
|
231
|
+
}[];
|
|
232
|
+
};
|
|
223
233
|
} | undefined;
|
|
224
234
|
version: string;
|
|
225
235
|
unavailableActions: {
|
|
@@ -237,6 +247,8 @@ export declare class Capabilities {
|
|
|
237
247
|
presentation?: import("@rho-workspace/plugin-contracts").EntryPresentation;
|
|
238
248
|
}[];
|
|
239
249
|
components: ({
|
|
250
|
+
uiPackage?: string | undefined;
|
|
251
|
+
uiPackageVersion?: string | undefined;
|
|
240
252
|
pluginId: string;
|
|
241
253
|
id: string;
|
|
242
254
|
version: string;
|
|
@@ -262,8 +274,12 @@ export declare class Capabilities {
|
|
|
262
274
|
})[];
|
|
263
275
|
capabilities: string[];
|
|
264
276
|
}>;
|
|
265
|
-
skills(principal: Principal): Promise<SkillDeclaration
|
|
266
|
-
|
|
277
|
+
skills(principal: Principal): Promise<(SkillDeclaration & {
|
|
278
|
+
owner: string;
|
|
279
|
+
})[]>;
|
|
280
|
+
readSkill(principal: Principal, skillId: string, expectedDigest?: string): Promise<SkillDeclaration & {
|
|
281
|
+
owner: string;
|
|
282
|
+
}>;
|
|
267
283
|
assertAvailable(method: string, principal: Principal): Promise<void>;
|
|
268
284
|
isAvailable(method: string, state?: CapabilityState): Promise<boolean>;
|
|
269
285
|
}
|
package/dist/capabilities.js
CHANGED
|
@@ -128,7 +128,9 @@ const analysisRunContent = [
|
|
|
128
128
|
' close it, and let the figure land in a file you name.',
|
|
129
129
|
'7. Write each file once, complete, and save it to disk before you rely on it. One script',
|
|
130
130
|
' written in full and then run costs less than the same script edited piece by piece,',
|
|
131
|
-
' and a file on disk is the only version that outlives a stop.',
|
|
131
|
+
' and a file on disk is the only version that outlives a stop. Run it in the session',
|
|
132
|
+
' with kernel.run (source() in R, exec() in Python), not from a shell: a figure file',
|
|
133
|
+
' stays readable mark by mark only while it holds the bytes a Notebook cell saved.',
|
|
132
134
|
'8. Check packages in one call, and do not install into a temporary library. Name what is',
|
|
133
135
|
' missing, and when you substitute a gene-set source or an annotation build, say so',
|
|
134
136
|
' beside the result, because that choice changes the numbers.',
|
|
@@ -140,7 +142,7 @@ const analysisRunContent = [
|
|
|
140
142
|
' that record rather than from a claim that the work is done.',
|
|
141
143
|
].join('\n');
|
|
142
144
|
const analysisRunSkill = {
|
|
143
|
-
id: 'rho.analysis-run', version: '1.
|
|
145
|
+
id: 'rho.analysis-run', version: '1.2', content: analysisRunContent,
|
|
144
146
|
digest: digest(analysisRunContent),
|
|
145
147
|
intent: 'Run a whole analysis through the live sessions so a stop never loses the work.',
|
|
146
148
|
requires: ['kernel.run', 'kernel.read', 'kernel.interrupt', 'resource.list'],
|
|
@@ -317,12 +319,12 @@ export class Capabilities {
|
|
|
317
319
|
}
|
|
318
320
|
async skills(principal) {
|
|
319
321
|
const state = await this.state();
|
|
320
|
-
const result = [rhoWorkspaceSkill, personFilesSkill, taskMapSkill, analysisRunSkill, screenCheckSkill];
|
|
322
|
+
const result = [rhoWorkspaceSkill, personFilesSkill, taskMapSkill, analysisRunSkill, screenCheckSkill].map(skill => ({ ...skill, owner: 'rho' }));
|
|
321
323
|
for (const descriptor of this.plugins.descriptors()) {
|
|
322
324
|
const status = this.pluginStatus(descriptor.id, state);
|
|
323
325
|
for (const skill of descriptor.skills ?? []) {
|
|
324
326
|
if (status.packagePresent && status.projectEnabled && status.dependenciesSatisfied && descriptor.operations.some(operation => this.authorized(principal, operation.method)))
|
|
325
|
-
result.push(skill);
|
|
327
|
+
result.push({ ...skill, owner: descriptor.id });
|
|
326
328
|
}
|
|
327
329
|
}
|
|
328
330
|
return result;
|
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import * as fs from 'node:fs/promises';
|
|
5
|
+
import { constants as fsConstants } from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
5
7
|
import { spawn } from 'node:child_process';
|
|
6
8
|
// A widened link is optional: the fragment carries a one-time code, which no request or Referer
|
|
7
9
|
// sends and which the page redeems and removes. Opening the plain url needs no code at all.
|
|
8
10
|
const link = (url, code) => url + '/#pair=' + code;
|
|
11
|
+
// What Ctrl+C does, asked by another program: Windows has no signal one process can send to Rho
|
|
12
|
+
// there, so `rho --stop` leaves this file in the state folder and the running launcher looks for it,
|
|
13
|
+
// as Rho asks its Host to stop on Windows.
|
|
14
|
+
const stopRequest = (stateDirectory) => path.join(stateDirectory, 'service.stop');
|
|
9
15
|
function openBrowser(target) {
|
|
10
16
|
const [command, args] = process.platform === 'darwin' ? ['open', [target]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', target]] : ['xdg-open', [target]];
|
|
11
17
|
spawn(command, args, { stdio: 'ignore', detached: true }).on('error', () => process.stderr.write('Could not open a browser. Open openUrl from the output instead.\n')).unref();
|
|
@@ -41,12 +47,17 @@ Start Rho on ~/Rho/projects and ~/Rho/state, and say where it is and what is mis
|
|
|
41
47
|
rho
|
|
42
48
|
Start Rho:
|
|
43
49
|
rho --root PROJECTS_DIRECTORY --state-dir SERVICE_STATE [--core ABSOLUTE_CORE_BINARY] [--rscript ABSOLUTE_RSCRIPT] [--python PYTHON] [--ssh SSH] [--serverctl SERVERCTL] [--claude CLAUDE_CODE_BINARY] [--codex CODEX_BINARY] [--agent-backend claude-acp|codex-acp|claude-cli|acp] [--acp-command JSON_ARGV] [--port PORT] [--limits JSON_FILE] [--plugins documents,files,...] [--example on|off] [--open]
|
|
50
|
+
Stop the Rho running for a state folder (~/Rho/state without --state-dir), as Ctrl+C in its window does:
|
|
51
|
+
rho --stop [--state-dir SERVICE_STATE]
|
|
44
52
|
A narrower browser link (optional):
|
|
45
53
|
rho --pair PRIVATE_CONNECTION_FILE [--root PROJECTS_RELATIVE_DIRECTORY] [--open]
|
|
46
|
-
|
|
54
|
+
Connect yourself, an Agent, to a project of the running Rho (what a setup prompt asks):
|
|
55
|
+
rho connect [PROJECT_FOLDER] --as NAME [--state-dir SERVICE_STATE] [--json]
|
|
56
|
+
Add Rho to your own Claude Code or Codex for one project:
|
|
47
57
|
rho --grant PRIVATE_CONNECTION_FILE --project PROJECTS_RELATIVE_DIRECTORY --agent claude-code|codex
|
|
48
|
-
Give
|
|
58
|
+
Give any MCP Agent access to one project and print its stdio MCP configuration:
|
|
49
59
|
rho --grant PRIVATE_CONNECTION_FILE --project PROJECTS_RELATIVE_DIRECTORY [--session NAME]
|
|
60
|
+
rho --grant PRIVATE_CONNECTION_FILE --project-id EXISTING_PROJECT_ID --session NAME
|
|
50
61
|
Serve that access to the Agent over MCP (stdio):
|
|
51
62
|
rho --mcp AGENT_CONNECTION_FILE
|
|
52
63
|
Observe an external ACP client (use this as its agent command):
|
|
@@ -60,8 +71,24 @@ narrower, expiring link for one folder.
|
|
|
60
71
|
PRIVATE_CONNECTION_FILE is the connectionFile the running service printed; it and every
|
|
61
72
|
connection file are credentials.
|
|
62
73
|
`;
|
|
74
|
+
// DeepSeek Harness speaks ACP through its `acp` profile (`dsh acp`). It is used only when both the
|
|
75
|
+
// launcher and that profile are on this computer; nothing is started to find them.
|
|
76
|
+
async function harnessAcp() {
|
|
77
|
+
const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
78
|
+
const profile = await fs.stat(path.join(home, 'profiles', 'acp', 'package.json')).then(s => s.isFile(), () => false);
|
|
79
|
+
if (!profile)
|
|
80
|
+
return undefined;
|
|
81
|
+
const names = process.platform === 'win32' ? ['dsh.cmd', 'dsh.exe', 'dsh'] : ['dsh'];
|
|
82
|
+
for (const dir of (process.env.PATH ?? '').split(path.delimiter).filter(Boolean))
|
|
83
|
+
for (const name of names) {
|
|
84
|
+
const file = path.join(dir, name);
|
|
85
|
+
if (await fs.access(file, fsConstants.X_OK).then(() => true, () => false))
|
|
86
|
+
return { file, args: ['acp'] };
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
63
90
|
const options = { example: { type: 'string' }, 'verify-evidence': { type: 'string' }, acp: { type: 'string' }, 'acp-agent': { type: 'string' }, root: { type: 'string' }, 'state-dir': { type: 'string' }, core: { type: 'string' }, rscript: { type: 'string' }, python: { type: 'string' }, ssh: { type: 'string' }, serverctl: { type: 'string' }, claude: { type: 'string' }, codex: { type: 'string' }, 'agent-backend': { type: 'string' }, 'acp-command': { type: 'string' }, port: { type: 'string' }, limits: { type: 'string' }, plugins: { type: 'string' }, pair: { type: 'string' }, open: { type: 'boolean' },
|
|
64
|
-
grant: { type: 'string' }, project: { type: 'string' }, session: { type: 'string' }, agent: { type: 'string' }, mcp: { type: 'string' }, help: { type: 'boolean', short: 'h' } };
|
|
91
|
+
grant: { type: 'string' }, project: { type: 'string' }, 'project-id': { type: 'string' }, session: { type: 'string' }, agent: { type: 'string' }, mcp: { type: 'string' }, stop: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } };
|
|
65
92
|
// A command that is refused says why and what to do. A service that is not running is the
|
|
66
93
|
// common case for --grant and --pair; anything without a known reason keeps its stack.
|
|
67
94
|
function refuse(error, url) {
|
|
@@ -83,6 +110,9 @@ if (process.argv[2] === 'plugin') {
|
|
|
83
110
|
process.exitCode = 1;
|
|
84
111
|
}
|
|
85
112
|
}
|
|
113
|
+
else if (process.argv[2] === 'connect') {
|
|
114
|
+
process.exitCode = await (await import('./connect-cli.js')).connectCli(process.argv.slice(3));
|
|
115
|
+
}
|
|
86
116
|
else {
|
|
87
117
|
let parsed;
|
|
88
118
|
try {
|
|
@@ -108,6 +138,55 @@ else {
|
|
|
108
138
|
process.stdout.write(usage);
|
|
109
139
|
process.exit(0);
|
|
110
140
|
}
|
|
141
|
+
if (values.stop) {
|
|
142
|
+
const { lease } = await import('./state.js');
|
|
143
|
+
const directory = path.resolve(values['state-dir'] ?? path.join(os.homedir(), 'Rho', 'state'));
|
|
144
|
+
// The state's writer lock says whether a service runs there: a lock this command can take is free.
|
|
145
|
+
// A folder without a connection file was never a Rho state, and gets no lock file of its own.
|
|
146
|
+
const running = () => lease(directory).then(async (release) => { await release(); return false; }, (e) => { if (e?.kind === 'conflict')
|
|
147
|
+
return true; throw e; });
|
|
148
|
+
const asked = () => fs.access(stopRequest(directory)).then(() => true, () => false);
|
|
149
|
+
const connection = await fs.readFile(path.join(directory, 'connection.json'), 'utf8').then(text => JSON.parse(text)).catch(() => null);
|
|
150
|
+
if (!connection || !await running()) {
|
|
151
|
+
process.stderr.write(`Rho is not running for ${directory}.\n`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
await fs.writeFile(stopRequest(directory), '');
|
|
155
|
+
// The launcher takes the request within a second and then stops as on Ctrl+C: its output ends
|
|
156
|
+
// with the {"shutdown":…} line, and the lock is free once its state is written.
|
|
157
|
+
const since = Date.now();
|
|
158
|
+
while (await running()) {
|
|
159
|
+
if (Date.now() - since > 10000 && await asked()) {
|
|
160
|
+
await fs.rm(stopRequest(directory), { force: true });
|
|
161
|
+
process.stderr.write(`The Rho running for ${directory} did not take the stop request: it is a version without --stop. Stop it with Ctrl+C in its own window (Ctrl+Break on Windows).\n`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
if (Date.now() - since > 120000) {
|
|
165
|
+
process.stderr.write(`Rho for ${directory} is still stopping after two minutes. Its output ends with a {"shutdown":…} line once it has stopped.\n`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
169
|
+
}
|
|
170
|
+
// The lock is released before the launcher prints its report and ends, and on Windows the log
|
|
171
|
+
// it writes stays open until then: a start right after would find that file in use.
|
|
172
|
+
const pid = Number.isInteger(connection.pid) ? connection.pid : 0;
|
|
173
|
+
const alive = () => { try {
|
|
174
|
+
process.kill(pid, 0);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
return e.code === 'EPERM';
|
|
179
|
+
} };
|
|
180
|
+
for (let i = 0; pid && alive(); i++) {
|
|
181
|
+
if (i === 40) {
|
|
182
|
+
process.stderr.write(`Rho for ${directory} has closed its state, but its process ${pid} has not ended after 10 s.\n`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
186
|
+
}
|
|
187
|
+
process.stdout.write(JSON.stringify({ stopped: { state: directory } }) + '\n');
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
111
190
|
// The Agent's side of a grant: its MCP server reads the connection file and speaks on stdio,
|
|
112
191
|
// so nothing else may write to stdout in this mode.
|
|
113
192
|
if (values.acp) {
|
|
@@ -126,8 +205,8 @@ else {
|
|
|
126
205
|
// Access comes from whoever runs Rho: the operator's connection grants one project to one
|
|
127
206
|
// Agent session, and the reply says how Claude Code starts that session's MCP server.
|
|
128
207
|
if (values.grant) {
|
|
129
|
-
if (
|
|
130
|
-
process.stderr.write('--grant needs --project
|
|
208
|
+
if (!!values.project === !!values['project-id']) {
|
|
209
|
+
process.stderr.write('--grant needs exactly one of --project (a folder relative to the service root) or --project-id (an existing project).\n\n' + usage);
|
|
131
210
|
process.exit(2);
|
|
132
211
|
}
|
|
133
212
|
if (values.agent !== undefined && !['claude-code', 'codex'].includes(values.agent)) {
|
|
@@ -141,7 +220,13 @@ else {
|
|
|
141
220
|
const { RhoClient } = await import('./client.js');
|
|
142
221
|
const client = await RhoClient.fromFile(path.resolve(values.grant)).catch(error => refuse(error));
|
|
143
222
|
try {
|
|
144
|
-
const project =
|
|
223
|
+
const project = values['project-id']
|
|
224
|
+
? (await client.call('project.list', {})).find(p => p.id === values['project-id'])
|
|
225
|
+
: await client.call('project.open', { path: values.project });
|
|
226
|
+
if (!project) {
|
|
227
|
+
process.stderr.write('Project is not registered or is outside this connection’s scope. Open the project in Rho and copy its setup prompt again.\n');
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
145
230
|
// The same as the project page's button: the grant that Agent keeps, then its own
|
|
146
231
|
// `mcp add`, recorded so the page shows it under that Agent.
|
|
147
232
|
if (values.agent) {
|
|
@@ -149,13 +234,8 @@ else {
|
|
|
149
234
|
process.stdout.write(JSON.stringify({ projectId: project.id, root: project.root, agent: values.agent, methods: connected.methods, granted: connected.granted, added: connected.added }) + '\n');
|
|
150
235
|
process.exit(0);
|
|
151
236
|
}
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const { mcpCommand, mcpName } = await import('./agent-connect.js');
|
|
155
|
-
const command = mcpCommand(granted.connectionFile), name = mcpName(project.root);
|
|
156
|
-
const quote = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
157
|
-
process.stdout.write(JSON.stringify({ projectId: project.id, session, connectionFile: granted.connectionFile, methods: granted.methods.length,
|
|
158
|
-
mcp: { name, command: command[0], args: command.slice(1) }, claude: ['claude', 'mcp', 'add', name, '--', ...command].map(quote).join(' ') }) + '\n');
|
|
237
|
+
const { grantReply } = await import('./connect-cli.js');
|
|
238
|
+
process.stdout.write(JSON.stringify(await grantReply(client, project, values.session ?? 'claude-code')) + '\n');
|
|
159
239
|
process.exit(0);
|
|
160
240
|
}
|
|
161
241
|
catch (error) {
|
|
@@ -203,7 +283,10 @@ else {
|
|
|
203
283
|
const agentBackend = values['agent-backend'] ?? process.env.RHO_AGENT_BACKEND ?? 'claude-acp';
|
|
204
284
|
if (!['claude-acp', 'codex-acp', 'claude-cli', 'acp'].includes(agentBackend))
|
|
205
285
|
throw new Error('Choose --agent-backend claude-acp, codex-acp, claude-cli or acp.');
|
|
206
|
-
|
|
286
|
+
// An ACP command named here, or else DeepSeek Harness's own ACP profile when it is installed, so
|
|
287
|
+
// sessions started in Harness continue in Rho too. Naming one does not make it the default Agent.
|
|
288
|
+
const named = values['acp-command'] ?? process.env.RHO_ACP_COMMAND;
|
|
289
|
+
const acpCommand = agentBackend === 'acp' || named ? parseAcpCommand(named) : await harnessAcp();
|
|
207
290
|
if (values.port !== undefined && !(/^\d+$/.test(values.port) && Number(values.port) <= 65535)) {
|
|
208
291
|
process.stderr.write('Choose --port 0 to 65535, or leave it out.\n');
|
|
209
292
|
process.exit(2);
|
|
@@ -221,6 +304,11 @@ else {
|
|
|
221
304
|
const running = typeof held === 'string' ? await fs.readFile(path.join(held, 'connection.json'), 'utf8').then(text => String(JSON.parse(text).url), () => '') : '';
|
|
222
305
|
return refuse(running ? Object.assign(error, { message: `Rho is already running for ${held} at ${running}. Open that url, or stop that service before starting it again.` }) : error);
|
|
223
306
|
});
|
|
307
|
+
// A request left by a --stop that this state's earlier service never took is not for this start.
|
|
308
|
+
await fs.rm(stopRequest(service.state.directory), { force: true });
|
|
309
|
+
// Where `rho connect` finds this Rho without being given its state folder.
|
|
310
|
+
const { announce, withdraw } = await import('./running.js');
|
|
311
|
+
await announce({ stateDirectory: service.state.directory, url: service.url, pid: process.pid }).catch(() => { });
|
|
224
312
|
// The narrow conclusion a person needs on one line: what was selected, where state
|
|
225
313
|
// lives, and how to open it. It stays inside `ready` so a parser reading line one is
|
|
226
314
|
// unaffected. Opening this url on this machine is the access; no link is minted.
|
|
@@ -252,9 +340,11 @@ else {
|
|
|
252
340
|
const claudePath = values.claude ?? process.env.RHO_CLAUDE, codexPath = values.codex ?? process.env.RHO_CODEX;
|
|
253
341
|
const env = { ...process.env, ...(claudePath ? { CLAUDE_CODE_EXECUTABLE: claudePath } : {}), ...(codexPath ? { CODEX_PATH: codexPath } : {}) };
|
|
254
342
|
// An ACP agent named with --acp-command is the operator's own command; it is named, not checked.
|
|
255
|
-
if (acpCommand)
|
|
343
|
+
if (acpCommand && agentBackend === 'acp')
|
|
256
344
|
say('Agent: ' + acpCommand.file + ', named with --acp-command.');
|
|
257
345
|
else {
|
|
346
|
+
if (acpCommand)
|
|
347
|
+
say('Also: ' + (acpCommand.args[0] === 'acp' && /dsh(\.[a-z]+)?$/i.test(acpCommand.file) ? 'DeepSeek Harness (' + acpCommand.file + ' acp)' : acpCommand.file) + ', so its sessions can continue here.');
|
|
258
348
|
const missing = agentCommand ? agentCommand.missingClient(agentBackend === 'codex-acp' ? 'codex' : 'claude', env) : null;
|
|
259
349
|
say(missing
|
|
260
350
|
? 'Agent: ' + missing + ' Until then folders can be opened and files read here; the Agent cannot run in them.'
|
|
@@ -271,11 +361,16 @@ else {
|
|
|
271
361
|
}
|
|
272
362
|
if (values.open)
|
|
273
363
|
openBrowser(service.url);
|
|
274
|
-
let closing = false;
|
|
364
|
+
let closing = false, asked;
|
|
365
|
+
const stop = async () => { if (closing)
|
|
366
|
+
return; closing = true; clearInterval(asked); await withdraw(service.state.directory).catch(() => { }); const reports = await service.close(); process.stdout.write(JSON.stringify({ shutdown: reports }) + '\n'); };
|
|
275
367
|
// Windows has no SIGTERM another process can send, and a console may pass on Ctrl+C turned off;
|
|
276
368
|
// there Ctrl+Break on Rho's console (SIGBREAK) stops it in order too.
|
|
277
369
|
for (const signal of ['SIGTERM', 'SIGINT', ...(process.platform === 'win32' ? ['SIGBREAK'] : [])])
|
|
278
|
-
process.on(signal,
|
|
279
|
-
|
|
370
|
+
process.on(signal, stop);
|
|
371
|
+
// A program with no console to share, such as an Agent that started Rho in the background on
|
|
372
|
+
// Windows, asks with rho --stop. Removing the request is what takes it.
|
|
373
|
+
asked = setInterval(() => { fs.rm(stopRequest(service.state.directory)).then(stop, () => { }); }, 500);
|
|
374
|
+
asked.unref();
|
|
280
375
|
}
|
|
281
376
|
}
|
package/dist/composition.d.ts
CHANGED
|
@@ -15,5 +15,5 @@ export declare class Compositions {
|
|
|
15
15
|
private ancestors;
|
|
16
16
|
private protect;
|
|
17
17
|
read(id: string): Promise<any>;
|
|
18
|
-
transact(body: any, author: string, kind?: 'apply' | 'undo' | 'protect'): Promise<any>;
|
|
18
|
+
transact(body: any, author: string, kind?: 'apply' | 'undo' | 'protect' | 'delete'): Promise<any>;
|
|
19
19
|
}
|
package/dist/composition.js
CHANGED
|
@@ -82,11 +82,11 @@ export class Compositions {
|
|
|
82
82
|
requireThat((current?.version ?? 0) === body.expectedVersion, 'stale', 'Composition version changed. Missing receipts do not authorize replay.', { actualVersion: current?.version ?? 0, retainedFromVersion: current?.retainedFromVersion });
|
|
83
83
|
const previous = current?.format === format ? current : { format, rootId: '', nodes: [] };
|
|
84
84
|
let next = structuredClone(previous);
|
|
85
|
-
const snapshot = (c) => ({ rootId: c.rootId, nodes: c.nodes });
|
|
85
|
+
const snapshot = (c) => ({ rootId: c.rootId, nodes: c.nodes, ...(c.deleted ? { deleted: true } : {}) });
|
|
86
86
|
let history = current?.history ?? [];
|
|
87
87
|
if (kind === 'undo') {
|
|
88
88
|
requireThat(history.length, 'condition', 'No retained layout transaction can be undone.');
|
|
89
|
-
next = { ...next, ...structuredClone(history.at(-1).snapshot) };
|
|
89
|
+
next = { ...next, deleted: false, ...structuredClone(history.at(-1).snapshot) };
|
|
90
90
|
history = history.slice(0, -1);
|
|
91
91
|
for (const n of next.nodes) {
|
|
92
92
|
const existing = previous.nodes.find((v) => v.id === n.id);
|
|
@@ -94,13 +94,23 @@ export class Compositions {
|
|
|
94
94
|
n.protected = existing?.protected ?? false;
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
+
else if (kind === 'delete') {
|
|
98
|
+
requireThat(current?.format === format && !current.deleted, 'condition', 'Choose an existing Studio workspace.');
|
|
99
|
+
requireThat(!body.compositionId.startsWith('conversation-'), 'condition', 'A conversation\'s Studio belongs to its conversation.');
|
|
100
|
+
requireThat(!current.nodes.some((n) => n.kind === 'view'), 'condition', 'Only an empty Studio workspace can be deleted. Close its views first; project files and sessions are separate.');
|
|
101
|
+
next.deleted = true;
|
|
102
|
+
history = [...history, { version: current.version, snapshot: snapshot(previous) }].slice(-this.limits.maxUndo);
|
|
103
|
+
}
|
|
97
104
|
else if (kind === 'protect') {
|
|
105
|
+
requireThat(!current?.deleted, 'condition', 'This Studio workspace was deleted.');
|
|
98
106
|
requireThat(typeof body.protected === 'boolean', 'invalid', 'Protection must be explicit.');
|
|
99
107
|
const view = next.nodes.find((n) => n.id === body.viewId && n.kind === 'view');
|
|
100
108
|
requireThat(view, 'not_found', 'View is not present.');
|
|
101
109
|
view.protected = body.protected;
|
|
102
110
|
}
|
|
103
111
|
else {
|
|
112
|
+
requireThat(!current?.deleted || body.operations?.length === 1 && body.operations[0].op === 'replace', 'condition', 'Recreate a deleted Studio workspace with one conditional replacement, or undo its deletion.');
|
|
113
|
+
next.deleted = false;
|
|
104
114
|
requireThat(Array.isArray(body.operations) && body.operations.length > 0 && body.operations.length <= this.limits.maxNodes, 'invalid', 'Supply a bounded atomic operation batch.');
|
|
105
115
|
for (const op of body.operations) {
|
|
106
116
|
const node = next.nodes.find((n) => n.id === op.id);
|
|
@@ -162,7 +172,7 @@ export class Compositions {
|
|
|
162
172
|
const availability = await this.validateViews(views, [...(previous.views ?? []), ...(kind === 'undo' ? current.history.flatMap((h) => h.snapshot.nodes.filter((n) => n.kind === 'view')) : [])]);
|
|
163
173
|
const receipt = { changeId: body.changeId, author, version: (current?.version ?? 0) + 1, requestDigest, at: now(), kind };
|
|
164
174
|
const receipts = [...(current?.receipts ?? []), receipt].slice(-this.limits.maxReceipts);
|
|
165
|
-
const result = { format, compositionId: body.compositionId, rootId: next.rootId, nodes: next.nodes, views, availability, version: receipt.version, author, acknowledgedAt: receipt.at, history, receipts, retainedFromVersion: receipts[0].version, undoRetainedFromVersion: history[0]?.version ?? receipt.version, receipt };
|
|
175
|
+
const result = { format, compositionId: body.compositionId, rootId: next.rootId, nodes: next.nodes, views, availability, ...(next.deleted ? { deleted: true } : {}), version: receipt.version, author, acknowledgedAt: receipt.at, history, receipts, retainedFromVersion: receipts[0].version, undoRetainedFromVersion: history[0]?.version ?? receipt.version, receipt };
|
|
166
176
|
requireThat(Buffer.byteLength(JSON.stringify(result) + '\n') <= this.limits.maxBytes, 'limit', 'Composition byte budget reached; existing state is preserved.', { limit: 'maxBytes', maximum: this.limits.maxBytes });
|
|
167
177
|
await atomicJson(file, result);
|
|
168
178
|
return result;
|