@rho-workspace/rho 0.5.6 → 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 +36 -2
- package/dist/agent-connect.js +64 -11
- 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 +76 -11
- 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 +1191 -278
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +845 -47
- package/dist/conversation-presentation.d.ts +9 -2
- package/dist/conversation-presentation.js +3 -1
- package/dist/conversations.d.ts +154 -8
- package/dist/conversations.js +147 -24
- package/dist/discovery.js +2 -2
- 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 +6 -0
- package/dist/protocol.js +30 -12
- 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 +2 -0
- package/dist/server.js +71 -8
- 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 +133 -74
- package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
- package/dist/web/assets/choices-D8vGwsl2.js +1 -0
- package/dist/web/assets/{console-Trqc0swD.js → console-Dyuo1q4N.js} +4 -4
- package/dist/web/assets/{dist-pF902jHA.js → dist-CSiCyBml.js} +1 -1
- package/dist/web/assets/{dist-DpIHRk3B.js → dist-D9FLb9Hm.js} +1 -1
- package/dist/web/assets/{dist-eMx6b8N1.js → dist-FXbTBwML.js} +1 -1
- package/dist/web/assets/{dist-DZmszrjG.js → dist-l3DUhbw5.js} +1 -1
- package/dist/web/assets/{explore-in-BCHqjJ7B.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-DcVGUBBY.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-DYP0aCXb.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-1ec0Z-jx.js → skills-Dyz7mBlc.js} +1 -1
- package/dist/web/assets/{skills-work-DNexV4OJ.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-COmCbGV3.js → workspace-DfjBL--r.js} +2 -2
- package/dist/web/composition.json +138 -83
- 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 +53 -11
- package/docs/HEADLESS.md +104 -11
- package/docs/THIRD-PARTY-PLUGINS.md +75 -3
- package/node_modules/@rho-workspace/plugin-agent/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +31 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +45 -10
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +19 -3
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +1 -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 +1 -1
- package/node_modules/@rho-workspace/plugin-agent/package.json +1 -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/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 +91 -55
- package/protocol/operations/agent.sessions.list.json +13 -5
- package/protocol/operations/agent.sessions.read.json +13 -5
- 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.decide.json +196 -0
- package/protocol/operations/conversation.permission.json +196 -0
- package/protocol/operations/conversation.read.json +196 -0
- package/protocol/operations/conversation.send.json +265 -0
- package/protocol/operations/conversation.settings.check.json +213 -495
- package/protocol/operations/conversation.settings.read.json +211 -490
- package/protocol/operations/conversation.settings.update.json +235 -554
- package/protocol/operations/conversation.start.json +267 -5
- package/protocol/operations/conversation.stop.json +196 -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 +416 -8
- package/protocol/operations/project.openFile.json +10 -2
- package/protocol/operations/session.grant.json +4 -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 +177 -112
- 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 +5 -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-D-YfnFP6.js +0 -1
- package/dist/web/assets/graph-lENgfQNH.js +0 -2
- package/dist/web/assets/index-B3_OVy91.css +0 -1
- package/dist/web/assets/index-BpL_Kl_s.js +0 -892
- package/dist/web/assets/overview-CDnVKlOt.js +0 -6
- package/dist/web/assets/preview-BEC1E9MK.js +0 -1
- package/dist/web/assets/studio-process-EYQgikxq.js +0 -2
- package/dist/web/assets/timeline-events-Chj2Gbx7.js +0 -1
- package/dist/web/assets/work-turns-CT9RVcWY.js +0 -1
- package/vendor/rho-workspace-plugin-agent-0.4.30.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,14 +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;
|
|
15
49
|
export declare function connectionInstructions(project: {
|
|
16
50
|
id: string;
|
|
17
51
|
root: string;
|
|
18
|
-
}, stateDirectory: string): {
|
|
52
|
+
}, stateDirectory: string): Promise<{
|
|
19
53
|
request: string;
|
|
20
54
|
prompt: string;
|
|
21
55
|
command: string[];
|
|
22
|
-
}
|
|
56
|
+
}>;
|
|
23
57
|
export declare function addToAgent(agent: OutsideAgent, binary: string, root: string, name: string, command: string[], limits?: {
|
|
24
58
|
timeoutMs: number;
|
|
25
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,18 +16,68 @@ 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
|
}
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.`;
|
|
21
78
|
const prompt = [request, '',
|
|
22
|
-
|
|
23
|
-
'
|
|
24
|
-
'Run this program and argument array, appending --session and a stable name identifying your own Agent (1–128 characters from A–Z, a–z, 0–9, dot, underscore, colon, @ or hyphen). Reuse that name when retrying:',
|
|
25
|
-
JSON.stringify(command),
|
|
26
|
-
'Parse the JSON reply. Register a stdio MCP server using mcp.name, mcp.command and mcp.args exactly as returned. The connection files contain credentials: do not print their contents or put them in the project. Do not start another Rho service.',
|
|
27
|
-
'Reload your MCP tools if supported, or tell me to open a new session in the project folder. Then call rho_index through your configured client tools and verify scopeHint.projectId matches the project ID above. A direct protocol probe is useful diagnostics but does not prove your client loaded its tools. Report configuration and a successful tool call separately; do not claim connected before the call succeeds. If configuration or verification fails, report the actual error and continue from the same connection.',
|
|
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']),
|
|
28
81
|
].join('\n');
|
|
29
82
|
return { request, prompt, command };
|
|
30
83
|
}
|
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.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', '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
|
@@ -8,6 +8,10 @@ import { spawn } from 'node:child_process';
|
|
|
8
8
|
// A widened link is optional: the fragment carries a one-time code, which no request or Referer
|
|
9
9
|
// sends and which the page redeems and removes. Opening the plain url needs no code at all.
|
|
10
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');
|
|
11
15
|
function openBrowser(target) {
|
|
12
16
|
const [command, args] = process.platform === 'darwin' ? ['open', [target]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', target]] : ['xdg-open', [target]];
|
|
13
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();
|
|
@@ -43,8 +47,12 @@ Start Rho on ~/Rho/projects and ~/Rho/state, and say where it is and what is mis
|
|
|
43
47
|
rho
|
|
44
48
|
Start Rho:
|
|
45
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]
|
|
46
52
|
A narrower browser link (optional):
|
|
47
53
|
rho --pair PRIVATE_CONNECTION_FILE [--root PROJECTS_RELATIVE_DIRECTORY] [--open]
|
|
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]
|
|
48
56
|
Add Rho to your own Claude Code or Codex for one project:
|
|
49
57
|
rho --grant PRIVATE_CONNECTION_FILE --project PROJECTS_RELATIVE_DIRECTORY --agent claude-code|codex
|
|
50
58
|
Give any MCP Agent access to one project and print its stdio MCP configuration:
|
|
@@ -80,7 +88,7 @@ async function harnessAcp() {
|
|
|
80
88
|
return undefined;
|
|
81
89
|
}
|
|
82
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' },
|
|
83
|
-
grant: { type: 'string' }, project: { type: 'string' }, 'project-id': { 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' } };
|
|
84
92
|
// A command that is refused says why and what to do. A service that is not running is the
|
|
85
93
|
// common case for --grant and --pair; anything without a known reason keeps its stack.
|
|
86
94
|
function refuse(error, url) {
|
|
@@ -102,6 +110,9 @@ if (process.argv[2] === 'plugin') {
|
|
|
102
110
|
process.exitCode = 1;
|
|
103
111
|
}
|
|
104
112
|
}
|
|
113
|
+
else if (process.argv[2] === 'connect') {
|
|
114
|
+
process.exitCode = await (await import('./connect-cli.js')).connectCli(process.argv.slice(3));
|
|
115
|
+
}
|
|
105
116
|
else {
|
|
106
117
|
let parsed;
|
|
107
118
|
try {
|
|
@@ -127,6 +138,55 @@ else {
|
|
|
127
138
|
process.stdout.write(usage);
|
|
128
139
|
process.exit(0);
|
|
129
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
|
+
}
|
|
130
190
|
// The Agent's side of a grant: its MCP server reads the connection file and speaks on stdio,
|
|
131
191
|
// so nothing else may write to stdout in this mode.
|
|
132
192
|
if (values.acp) {
|
|
@@ -174,13 +234,8 @@ else {
|
|
|
174
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');
|
|
175
235
|
process.exit(0);
|
|
176
236
|
}
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const { mcpCommand, mcpName } = await import('./agent-connect.js');
|
|
180
|
-
const command = mcpCommand(granted.connectionFile), name = mcpName(project.root);
|
|
181
|
-
const quote = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
182
|
-
process.stdout.write(JSON.stringify({ projectId: project.id, session, connectionFile: granted.connectionFile, methods: granted.methods.length,
|
|
183
|
-
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');
|
|
184
239
|
process.exit(0);
|
|
185
240
|
}
|
|
186
241
|
catch (error) {
|
|
@@ -249,6 +304,11 @@ else {
|
|
|
249
304
|
const running = typeof held === 'string' ? await fs.readFile(path.join(held, 'connection.json'), 'utf8').then(text => String(JSON.parse(text).url), () => '') : '';
|
|
250
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);
|
|
251
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(() => { });
|
|
252
312
|
// The narrow conclusion a person needs on one line: what was selected, where state
|
|
253
313
|
// lives, and how to open it. It stays inside `ready` so a parser reading line one is
|
|
254
314
|
// unaffected. Opening this url on this machine is the access; no link is minted.
|
|
@@ -301,11 +361,16 @@ else {
|
|
|
301
361
|
}
|
|
302
362
|
if (values.open)
|
|
303
363
|
openBrowser(service.url);
|
|
304
|
-
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'); };
|
|
305
367
|
// Windows has no SIGTERM another process can send, and a console may pass on Ctrl+C turned off;
|
|
306
368
|
// there Ctrl+Break on Rho's console (SIGBREAK) stops it in order too.
|
|
307
369
|
for (const signal of ['SIGTERM', 'SIGINT', ...(process.platform === 'win32' ? ['SIGBREAK'] : [])])
|
|
308
|
-
process.on(signal,
|
|
309
|
-
|
|
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();
|
|
310
375
|
}
|
|
311
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;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RhoClient } from './client.js';
|
|
2
|
+
export declare const connectUsage = "Connect yourself, an Agent, to a project of the Rho running on this machine:\n rho connect [PROJECT_FOLDER] --as NAME [--state-dir SERVICE_STATE] [--json]\nPROJECT_FOLDER is the project's folder (the current folder without it). NAME is a short, stable\nname for you, 1\u2013128 characters from A\u2013Z, a\u2013z, 0\u20139, dot, underscore, colon, @ or hyphen; use the\nsame name when you run this again. Without --state-dir, Rho finds the running Rho that holds the\nfolder; give --state-dir and its state folder when more than one does.\nThe reply says how to add Rho's MCP server to your client; --json prints it as JSON.\n";
|
|
3
|
+
type Client = Pick<RhoClient, 'call'>;
|
|
4
|
+
export type GrantReply = Awaited<ReturnType<typeof grantReply>>;
|
|
5
|
+
export declare function grantReply(client: Client, project: {
|
|
6
|
+
id: string;
|
|
7
|
+
root: string;
|
|
8
|
+
}, session: string): Promise<{
|
|
9
|
+
claude: string;
|
|
10
|
+
codex: string;
|
|
11
|
+
opencode: {
|
|
12
|
+
file: string;
|
|
13
|
+
merge: {
|
|
14
|
+
mcp: {
|
|
15
|
+
[x: string]: {
|
|
16
|
+
type: string;
|
|
17
|
+
command: string[];
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
dsh: {
|
|
24
|
+
file: string;
|
|
25
|
+
workspace: {
|
|
26
|
+
plugin: string;
|
|
27
|
+
minimumVersion: string;
|
|
28
|
+
guide: string;
|
|
29
|
+
mergeConfig: {
|
|
30
|
+
command: string;
|
|
31
|
+
args: string[];
|
|
32
|
+
connectionFiles: (string | undefined)[];
|
|
33
|
+
};
|
|
34
|
+
instructions: string;
|
|
35
|
+
};
|
|
36
|
+
append: string;
|
|
37
|
+
};
|
|
38
|
+
projectId: string;
|
|
39
|
+
session: string;
|
|
40
|
+
connectionFile: string;
|
|
41
|
+
methods: number;
|
|
42
|
+
mcp: {
|
|
43
|
+
name: string;
|
|
44
|
+
command: string;
|
|
45
|
+
args: string[];
|
|
46
|
+
};
|
|
47
|
+
}>;
|
|
48
|
+
export declare function connectText(reply: GrantReply, root: string): string;
|
|
49
|
+
export declare function connectCli(args: string[]): Promise<number>;
|
|
50
|
+
export {};
|