@rho-workspace/rho 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-access.js +3 -2
- package/dist/capabilities.d.ts +2 -2
- package/dist/capabilities.js +6 -1
- package/dist/cli.js +186 -173
- package/dist/contract.generated.d.ts +329 -4
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +360 -6
- package/dist/discovery.d.ts +1 -0
- package/dist/discovery.js +3 -3
- package/dist/extension-contract.d.ts +99 -0
- package/dist/extension-contract.js +52 -0
- package/dist/extension-directory.d.ts +9 -0
- package/dist/extension-directory.js +11 -0
- package/dist/extension-worker.d.ts +1 -0
- package/dist/extension-worker.js +36 -0
- package/dist/extensions.d.ts +157 -0
- package/dist/extensions.js +349 -0
- package/dist/plugin-cli.d.ts +1 -0
- package/dist/plugin-cli.js +139 -0
- package/dist/plugin-inventory.json +18 -0
- package/dist/plugins.d.ts +3 -3
- package/dist/project-work.d.ts +3 -1
- package/dist/project-work.js +15 -2
- package/dist/protocol.d.ts +7 -0
- package/dist/protocol.js +4 -2
- package/dist/server.d.ts +4 -1
- package/dist/server.js +55 -8
- package/dist/web/.vite/manifest.json +61 -61
- package/dist/web/assets/{canvas-BXhC8RDZ.js → canvas-D8rDdfDz.js} +1 -1
- package/dist/web/assets/choices-b0XhrGQV.js +1 -0
- package/dist/web/assets/{console-Dbdo-5oO.js → console-BSjLGe0T.js} +1 -1
- package/dist/web/assets/{dist-B2V0VDO0.js → dist-Buk0lwzI.js} +2 -2
- package/dist/web/assets/dist-Cw8hRIlT.js +1 -0
- package/dist/web/assets/dist-Tzoe_zm-.js +3 -0
- package/dist/web/assets/{dist-D25Vn17i.js → dist-foH9IKc6.js} +1 -1
- package/dist/web/assets/explore-in-BVUy3ZW_.js +1 -0
- package/dist/web/assets/graph-1OdIJ9l7.js +2 -0
- package/dist/web/assets/index-CsC3sTRq.css +1 -0
- package/dist/web/assets/{index-BHQIaDsx.js → index-CxmItXqa.js} +85 -85
- package/dist/web/assets/{notes-meaning-DdPbtNzJ.js → notes-meaning-pVbbQUGP.js} +1 -1
- package/dist/web/assets/overview-B9UtVJ32.js +6 -0
- package/dist/web/assets/preview-CksD_5KS.js +1 -0
- package/dist/web/assets/{review-CmWou4Iv.js → review-CMxQlQ-C.js} +1 -1
- package/dist/web/assets/{skills-Dy7n3L9d.js → skills-BFNSyrnD.js} +1 -1
- package/dist/web/assets/{skills-work-Cj0W6b-I.js → skills-work-BGPSnF_I.js} +1 -1
- package/dist/web/assets/{studio-process-QOY_n2_8.js → studio-process-CsRoTHOh.js} +2 -2
- package/dist/web/assets/{timeline-events-DmX1BCh3.js → timeline-events-BbuuFWHV.js} +1 -1
- package/dist/web/assets/{work-turns-Co0C71rJ.js → work-turns-BvJlIMmA.js} +1 -1
- package/dist/web/assets/workspace-CWlkjLNx.js +5 -0
- package/dist/web/composition.json +67 -67
- package/dist/web/index.html +3 -3
- package/dist/web/renderer.json +1 -1
- package/dist/web-assets.d.ts +2 -1
- package/dist/web-assets.js +3 -2
- package/dist/work-contract.js +2 -2
- package/docs/HEADLESS.md +1 -1
- package/docs/PLUGIN-AUTHORING.md +4 -0
- package/docs/THIRD-PARTY-PLUGINS.md +190 -0
- package/node_modules/@rho-workspace/plugin-agent/README.md +39 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.js +2 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.d.ts +6 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.js +174 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.d.ts +82 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +15 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +73 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.js +190 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +373 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.js +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/operations.d.ts +33 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/operations.js +3 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/presentation.d.ts +14 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/presentation.js +2 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.d.ts +15 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.js +37 -0
- package/node_modules/@rho-workspace/plugin-agent/package.json +5 -1
- package/package.json +5 -3
- package/protocol/index.json +58 -4
- package/protocol/operations/agent.sessions.list.json +100 -0
- package/protocol/operations/agent.sessions.read.json +266 -0
- package/protocol/operations/extension.call.json +132 -0
- package/protocol/operations/extension.content.json +78 -0
- package/protocol/operations/extension.describe.json +195 -0
- package/protocol/operations/extension.install.json +198 -0
- package/protocol/operations/extension.list.json +123 -0
- package/protocol/operations/extension.manage.json +55 -0
- package/protocol/operations/extension.read.json +112 -0
- package/protocol/operations/project.work.read.json +3 -2
- package/protocol/operations/session.grant.json +9 -0
- package/scripts/third-party-acceptance.mjs +77 -0
- package/vendor/composition.json +85 -85
- package/vendor/rho-workspace-plugin-agent-0.4.29.tgz +0 -0
- package/dist/web/assets/choices-DU9kHlyQ.js +0 -1
- package/dist/web/assets/dist-BV3hWfn_.js +0 -3
- package/dist/web/assets/dist-CLNF2g-E.js +0 -1
- package/dist/web/assets/explore-in-8Q5ftt-E.js +0 -1
- package/dist/web/assets/graph-hS2kvHTI.js +0 -2
- package/dist/web/assets/index-Cj1_iqyK.css +0 -1
- package/dist/web/assets/overview-BV6ZO5TO.js +0 -6
- package/dist/web/assets/preview-n5lOOdjN.js +0 -1
- package/dist/web/assets/workspace-BKFefRfJ.js +0 -5
- package/vendor/rho-workspace-plugin-agent-0.4.28.tgz +0 -0
package/dist/browser-access.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { extensionMethods } from './extension-contract.js';
|
|
1
2
|
import * as fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { randomBytes } from 'node:crypto';
|
|
@@ -36,7 +37,7 @@ export const browserMethods = ['protocol.describe', 'protocol.validate', 'projec
|
|
|
36
37
|
// Only the person at this machine talks to a conversation Rho starts: it runs their own Claude
|
|
37
38
|
// Code with their model credentials. Only they add Rho to their own Agent's configuration. A
|
|
38
39
|
// narrower, expiring link never receives these.
|
|
39
|
-
export const conversationMethods = ['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.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
41
|
// The person at this machine. A tab this service served is authorized by the request itself:
|
|
41
42
|
// same origin, loopback only, no link, no code and no expiry. `--pair` stays an explicit,
|
|
42
43
|
// narrower grant for a shared or subfolder-scoped tab.
|
|
@@ -49,7 +50,7 @@ export const skillsMethods = ['skills.list', 'skills.read', 'skills.change', 'sk
|
|
|
49
50
|
export function localBrowserPrincipal(root) {
|
|
50
51
|
// The empty token hash can never match a request digest, so no header impersonates this
|
|
51
52
|
// identity: it is chosen because the request carried no credential at all.
|
|
52
|
-
return { id: localBrowserId, kind: 'browser', tokenHash: '', projectRoot: root, methods: [...browserMethods, ...conversationMethods, ...consoleMethods, ...kernelMethods, ...skillsMethods] };
|
|
53
|
+
return { id: localBrowserId, kind: 'browser', tokenHash: '', projectRoot: root, methods: [...extensionMethods, ...browserMethods, ...conversationMethods, ...consoleMethods, ...kernelMethods, ...skillsMethods] };
|
|
53
54
|
}
|
|
54
55
|
export class BrowserAccess {
|
|
55
56
|
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" | "files" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel"
|
|
160
|
+
enabled: ("media" | "r" | "agent" | "files" | "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" | "files" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel"
|
|
195
|
+
enabled: ("media" | "r" | "agent" | "files" | "skills" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel")[];
|
|
196
196
|
retainedReceipts: number;
|
|
197
197
|
retainedFromVersion: number;
|
|
198
198
|
maxReceipts: number;
|
package/dist/capabilities.js
CHANGED
|
@@ -10,6 +10,11 @@ const applicationContent = [
|
|
|
10
10
|
'Start with rho_index: select an owner or search, then rho_resolve only the operations',
|
|
11
11
|
'needed now. Use rho_call with the complete contractRef. The private connection supplies',
|
|
12
12
|
'the project; discovery never grants additional authority or starts a runtime.',
|
|
13
|
+
'Independent third-party plugins: extension.list shows installed packages, extension.describe',
|
|
14
|
+
'gives their digest, operations and Skill entries. Read extension.content (kind skill)',
|
|
15
|
+
'before extension.call. Keep expectedDigest and the original changeId for a native call;',
|
|
16
|
+
'extension.read observes pending or unknown outcomes without rerunning. Installation and',
|
|
17
|
+
'management belong to the local person; plugin code never expands this connection grant.',
|
|
13
18
|
'Large replies carry a result reference. Use rho_read to inspect its fields and fetch',
|
|
14
19
|
'only selected text ranges or array pages. Retain useful references; release finished',
|
|
15
20
|
'results explicitly. Never repeat an action just to retrieve a large reply.',
|
|
@@ -30,7 +35,7 @@ const applicationContent = [
|
|
|
30
35
|
'run or export it as files; a stopped run is not resubmitted under its own operationId.'
|
|
31
36
|
].join('\n');
|
|
32
37
|
const applicationSkill = {
|
|
33
|
-
id: 'rho.workspace', version: '1.
|
|
38
|
+
id: 'rho.workspace', version: '1.6', content: applicationContent,
|
|
34
39
|
intent: 'Work in this project through versioned operations, in a safe order.',
|
|
35
40
|
requires: ['capability.list', 'observation.snapshot', 'action.submit', 'composition.apply', 'context.read'],
|
|
36
41
|
example: { method: 'observation.snapshot', input: { projectId: 'the-open-project' } },
|
package/dist/cli.js
CHANGED
|
@@ -33,7 +33,9 @@ function parseAcpCommand(spec) {
|
|
|
33
33
|
throw new Error('--acp-command is empty.');
|
|
34
34
|
return { file: parts[0], args: parts.slice(1) };
|
|
35
35
|
}
|
|
36
|
-
const usage = `
|
|
36
|
+
const usage = `Develop, install and manage independent plugins:
|
|
37
|
+
rho plugin --help
|
|
38
|
+
Verify a read-only evidence package:
|
|
37
39
|
rho --verify-evidence PATH
|
|
38
40
|
Start Rho on ~/Rho/projects and ~/Rho/state, and say where it is and what is missing:
|
|
39
41
|
rho
|
|
@@ -72,197 +74,208 @@ function refuse(error, url) {
|
|
|
72
74
|
throw error;
|
|
73
75
|
process.exit(1);
|
|
74
76
|
}
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
parsed = parseArgs({ options });
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
process.stderr.write((error instanceof Error ? error.message : String(error)) + '\n\n' + usage);
|
|
81
|
-
process.exit(2);
|
|
82
|
-
}
|
|
83
|
-
const { values } = parsed;
|
|
84
|
-
if (values['verify-evidence']) {
|
|
77
|
+
if (process.argv[2] === 'plugin') {
|
|
85
78
|
try {
|
|
86
|
-
|
|
87
|
-
process.stdout.write(JSON.stringify(await verifyEvidence(path.resolve(values['verify-evidence']))) + '\n');
|
|
79
|
+
await (await import('./plugin-cli.js')).pluginCli(process.argv.slice(3));
|
|
88
80
|
}
|
|
89
|
-
catch (
|
|
90
|
-
process.stderr.write((
|
|
81
|
+
catch (error) {
|
|
82
|
+
process.stderr.write((error instanceof Error ? error.message : String(error)) + '\n');
|
|
91
83
|
process.exitCode = 1;
|
|
92
84
|
}
|
|
93
|
-
process.exit(process.exitCode ?? 0);
|
|
94
|
-
}
|
|
95
|
-
if (values.help) {
|
|
96
|
-
process.stdout.write(usage);
|
|
97
|
-
process.exit(0);
|
|
98
85
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
86
|
+
else {
|
|
87
|
+
let parsed;
|
|
88
|
+
try {
|
|
89
|
+
parsed = parseArgs({ options });
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
process.stderr.write((error instanceof Error ? error.message : String(error)) + '\n\n' + usage);
|
|
104
93
|
process.exit(2);
|
|
105
94
|
}
|
|
106
|
-
const {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
await import('@rho-workspace/plugin-agent/mcp');
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
// Access comes from whoever runs Rho: the operator's connection grants one project to one
|
|
115
|
-
// Agent session, and the reply says how Claude Code starts that session's MCP server.
|
|
116
|
-
if (values.grant) {
|
|
117
|
-
if (!values.project) {
|
|
118
|
-
process.stderr.write('--grant needs --project, the project folder relative to the service root.\n\n' + usage);
|
|
119
|
-
process.exit(2);
|
|
95
|
+
const { values } = parsed;
|
|
96
|
+
if (values['verify-evidence']) {
|
|
97
|
+
try {
|
|
98
|
+
const { verifyEvidence } = await import('./evidence-export.js');
|
|
99
|
+
process.stdout.write(JSON.stringify(await verifyEvidence(path.resolve(values['verify-evidence']))) + '\n');
|
|
120
100
|
}
|
|
121
|
-
|
|
122
|
-
process.stderr.write(
|
|
123
|
-
process.
|
|
101
|
+
catch (e) {
|
|
102
|
+
process.stderr.write((e instanceof Error ? e.message : String(e)) + '\n');
|
|
103
|
+
process.exitCode = 1;
|
|
124
104
|
}
|
|
125
|
-
|
|
126
|
-
|
|
105
|
+
process.exit(process.exitCode ?? 0);
|
|
106
|
+
}
|
|
107
|
+
if (values.help) {
|
|
108
|
+
process.stdout.write(usage);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
// The Agent's side of a grant: its MCP server reads the connection file and speaks on stdio,
|
|
112
|
+
// so nothing else may write to stdout in this mode.
|
|
113
|
+
if (values.acp) {
|
|
114
|
+
if (!['claude', 'codex'].includes(values['acp-agent'] ?? '')) {
|
|
115
|
+
process.stderr.write('Select --acp-agent claude or codex.\n');
|
|
127
116
|
process.exit(2);
|
|
128
117
|
}
|
|
129
|
-
const {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
118
|
+
const { runExternalAcp } = await import('@rho-workspace/plugin-agent/acp-cli');
|
|
119
|
+
runExternalAcp(path.resolve(values.acp), values['acp-agent']);
|
|
120
|
+
}
|
|
121
|
+
else if (values.mcp) {
|
|
122
|
+
process.env.RHO_AGENT_CONNECTION = path.resolve(values.mcp);
|
|
123
|
+
await import('@rho-workspace/plugin-agent/mcp');
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Access comes from whoever runs Rho: the operator's connection grants one project to one
|
|
127
|
+
// Agent session, and the reply says how Claude Code starts that session's MCP server.
|
|
128
|
+
if (values.grant) {
|
|
129
|
+
if (!values.project) {
|
|
130
|
+
process.stderr.write('--grant needs --project, the project folder relative to the service root.\n\n' + usage);
|
|
131
|
+
process.exit(2);
|
|
132
|
+
}
|
|
133
|
+
if (values.agent !== undefined && !['claude-code', 'codex'].includes(values.agent)) {
|
|
134
|
+
process.stderr.write('Choose --agent claude-code or codex.\n');
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
if (values.agent !== undefined && values.session !== undefined) {
|
|
138
|
+
process.stderr.write('--agent names the session itself; leave out --session.\n');
|
|
139
|
+
process.exit(2);
|
|
140
|
+
}
|
|
141
|
+
const { RhoClient } = await import('./client.js');
|
|
142
|
+
const client = await RhoClient.fromFile(path.resolve(values.grant)).catch(error => refuse(error));
|
|
143
|
+
try {
|
|
144
|
+
const project = await client.call('project.open', { path: values.project });
|
|
145
|
+
// The same as the project page's button: the grant that Agent keeps, then its own
|
|
146
|
+
// `mcp add`, recorded so the page shows it under that Agent.
|
|
147
|
+
if (values.agent) {
|
|
148
|
+
const connected = await client.call('session.connect', { projectId: project.id, agent: values.agent });
|
|
149
|
+
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
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
const session = values.session ?? 'claude-code';
|
|
153
|
+
const granted = await client.call('session.grant', { projectId: project.id, externalSessionId: session });
|
|
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');
|
|
138
159
|
process.exit(0);
|
|
139
160
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const command = mcpCommand(granted.connectionFile), name = mcpName(project.root);
|
|
144
|
-
const quote = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
145
|
-
process.stdout.write(JSON.stringify({ projectId: project.id, session, connectionFile: granted.connectionFile, methods: granted.methods.length,
|
|
146
|
-
mcp: { name, command: command[0], args: command.slice(1) }, claude: ['claude', 'mcp', 'add', name, '--', ...command].map(quote).join(' ') }) + '\n');
|
|
147
|
-
process.exit(0);
|
|
161
|
+
catch (error) {
|
|
162
|
+
refuse(error, client.connection.url);
|
|
163
|
+
}
|
|
148
164
|
}
|
|
149
|
-
|
|
150
|
-
|
|
165
|
+
if (values.pair) {
|
|
166
|
+
const { RhoClient } = await import('./client.js');
|
|
167
|
+
const client = await RhoClient.fromFile(path.resolve(values.pair)).catch(error => refuse(error));
|
|
168
|
+
try {
|
|
169
|
+
const pairing = await client.call('session.browser.pair', values.root ? { path: values.root } : {});
|
|
170
|
+
const openUrl = link(client.connection.url, pairing.code);
|
|
171
|
+
process.stdout.write(JSON.stringify({ url: client.connection.url, openUrl, pairingCode: pairing.code, pairingExpiresAt: pairing.expiresAt }) + '\n');
|
|
172
|
+
if (values.open)
|
|
173
|
+
openBrowser(openUrl);
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
refuse(error, client.connection.url);
|
|
178
|
+
}
|
|
151
179
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
process.
|
|
160
|
-
if (values.open)
|
|
161
|
-
openBrowser(openUrl);
|
|
162
|
-
process.exit(0);
|
|
180
|
+
// A person who runs `npx @rho-workspace/rho` with no arguments wants the application, not a
|
|
181
|
+
// usage page: start it on their own folders, say in plain words where it is, and open it when
|
|
182
|
+
// this is an interactive terminal. Every flagged start is unchanged, so scripts and
|
|
183
|
+
// acceptance runs keep the output they read.
|
|
184
|
+
const quickStart = !values.root && !values['state-dir'];
|
|
185
|
+
if (!quickStart && (!values.root || !values['state-dir'])) {
|
|
186
|
+
process.stderr.write(usage);
|
|
187
|
+
process.exit(2);
|
|
163
188
|
}
|
|
164
|
-
|
|
165
|
-
|
|
189
|
+
let quickRoot = '', quickState = '';
|
|
190
|
+
if (quickStart) {
|
|
191
|
+
const { homedir } = await import('node:os');
|
|
192
|
+
quickRoot = path.join(homedir(), 'Rho', 'projects');
|
|
193
|
+
quickState = path.join(homedir(), 'Rho', 'state');
|
|
194
|
+
await fs.mkdir(quickRoot, { recursive: true });
|
|
195
|
+
await fs.mkdir(quickState, { recursive: true });
|
|
166
196
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
await
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
process.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
197
|
+
const projectsRoot = values.root ?? quickRoot, stateDirectory = values['state-dir'] ?? quickState;
|
|
198
|
+
const limits = values.limits ? JSON.parse(await fs.readFile(values.limits, 'utf8')) : {};
|
|
199
|
+
const allowed = new Set(['maxRequestBytes', 'maxEventBytes', 'eventRetention', 'maxSubscriptions', 'maxQueue', 'maxContexts', 'maxBindings', 'maxActivities', 'maxContextBytes', 'maxRetainedMetadataBytes', 'maxConcurrentActions', 'maxOperations', 'maxLogPageBytes', 'maxCodeBytes', 'maxReconciled', 'documents', 'resources', 'workspace', 'r', 'annotations', 'interactions', 'capabilityLimits', 'browser', 'nativeFolders', 'conversations', 'evidence', 'console']);
|
|
200
|
+
if (Object.keys(limits).some(k => !allowed.has(k)))
|
|
201
|
+
throw new Error('Unknown service budget in --limits file.');
|
|
202
|
+
const { RhoService } = await import('./server.js'), { runtime } = await import('./runtime.js');
|
|
203
|
+
const agentBackend = values['agent-backend'] ?? process.env.RHO_AGENT_BACKEND ?? 'claude-acp';
|
|
204
|
+
if (!['claude-acp', 'codex-acp', 'claude-cli', 'acp'].includes(agentBackend))
|
|
205
|
+
throw new Error('Choose --agent-backend claude-acp, codex-acp, claude-cli or acp.');
|
|
206
|
+
const acpCommand = agentBackend === 'acp' ? parseAcpCommand(values['acp-command'] ?? process.env.RHO_ACP_COMMAND) : undefined;
|
|
207
|
+
if (values.port !== undefined && !(/^\d+$/.test(values.port) && Number(values.port) <= 65535)) {
|
|
208
|
+
process.stderr.write('Choose --port 0 to 65535, or leave it out.\n');
|
|
209
|
+
process.exit(2);
|
|
210
|
+
}
|
|
211
|
+
// A Core that cannot be used (none for this platform, a changed digest) is read by a person.
|
|
212
|
+
const native = await runtime(values.core, values.rscript).catch(e => { process.stderr.write((e instanceof Error ? e.message : String(e)) + '\n'); process.exit(1); });
|
|
213
|
+
// A person starting Rho sees the project it ships with; a composition that starts a service
|
|
214
|
+
// for its own work says so itself and is left alone.
|
|
215
|
+
if (values.example !== undefined && !['on', 'off'].includes(values.example))
|
|
216
|
+
throw new Error('Choose --example on or off.');
|
|
217
|
+
const example = values.example !== 'off';
|
|
218
|
+
const service = await new RhoService({ example, ...limits, root: path.resolve(projectsRoot), stateDir: path.resolve(stateDirectory), ...native, agentBackend: agentBackend, acpCommand, codex: values.codex ?? process.env.RHO_CODEX ?? 'codex', claude: values.claude ?? process.env.RHO_CLAUDE ?? 'claude', python: values.python ?? process.env.RHO_PYTHON, ssh: values.ssh ?? process.env.RHO_SSH, serverctl: values.serverctl ?? process.env.RHO_SERVERCTL, port: values.port === undefined ? undefined : Number(values.port), ...(values.plugins !== undefined ? { plugins: values.plugins ? values.plugins.split(',') : [] } : {}) }).start().catch(async (error) => {
|
|
219
|
+
// Starting Rho again on a state it already runs on is the common refusal: say where it runs.
|
|
220
|
+
const held = error?.details?.stateDirectory;
|
|
221
|
+
const running = typeof held === 'string' ? await fs.readFile(path.join(held, 'connection.json'), 'utf8').then(text => String(JSON.parse(text).url), () => '') : '';
|
|
222
|
+
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
|
+
});
|
|
224
|
+
// The narrow conclusion a person needs on one line: what was selected, where state
|
|
225
|
+
// lives, and how to open it. It stays inside `ready` so a parser reading line one is
|
|
226
|
+
// unaffected. Opening this url on this machine is the access; no link is minted.
|
|
227
|
+
const rscript = values.rscript ?? process.env.RHO_RSCRIPT ?? 'Rscript';
|
|
228
|
+
process.stdout.write(JSON.stringify({ ready: {
|
|
229
|
+
url: service.url, connectionFile: path.join(service.state.directory, 'connection.json'), apiVersion: '2.0',
|
|
230
|
+
openUrl: service.url,
|
|
231
|
+
startup: {
|
|
232
|
+
projects: path.resolve(projectsRoot), state: path.resolve(stateDirectory),
|
|
233
|
+
core: service.bridge.binary, rscript: service.bridge.rscript,
|
|
234
|
+
note: 'Open this url in a browser on this machine. Execution stays off until an Agent submits a run; project.doctor answers what is missing.'
|
|
235
|
+
}
|
|
236
|
+
} }) + '\n');
|
|
237
|
+
// What a person needs, after the machine line: where it is, and what is missing to work in it.
|
|
238
|
+
// A missing Agent or R does not stop the workspace; it decides what the person can do next.
|
|
239
|
+
if (quickStart) {
|
|
240
|
+
const say = (text) => process.stdout.write(text + '\n');
|
|
241
|
+
say('');
|
|
242
|
+
say('Rho is running. Open this address in a browser on this machine:');
|
|
243
|
+
say(' ' + service.url);
|
|
244
|
+
say('');
|
|
245
|
+
say('Projects folder: ' + quickRoot);
|
|
246
|
+
say('Service state: ' + quickState);
|
|
247
|
+
say('');
|
|
248
|
+
const agentCommand = await import('@rho-workspace/plugin-agent/acp-command').catch(() => null);
|
|
249
|
+
// The selection the service itself got: an explicit flag, then the environment, then the name
|
|
250
|
+
// on PATH. Reporting "installed" for a client the person pointed at a missing path is worse
|
|
251
|
+
// than saying nothing.
|
|
252
|
+
const claudePath = values.claude ?? process.env.RHO_CLAUDE, codexPath = values.codex ?? process.env.RHO_CODEX;
|
|
253
|
+
const env = { ...process.env, ...(claudePath ? { CLAUDE_CODE_EXECUTABLE: claudePath } : {}), ...(codexPath ? { CODEX_PATH: codexPath } : {}) };
|
|
254
|
+
// An ACP agent named with --acp-command is the operator's own command; it is named, not checked.
|
|
255
|
+
if (acpCommand)
|
|
256
|
+
say('Agent: ' + acpCommand.file + ', named with --acp-command.');
|
|
257
|
+
else {
|
|
258
|
+
const missing = agentCommand ? agentCommand.missingClient(agentBackend === 'codex-acp' ? 'codex' : 'claude', env) : null;
|
|
259
|
+
say(missing
|
|
260
|
+
? 'Agent: ' + missing + ' Until then folders can be opened and files read here; the Agent cannot run in them.'
|
|
261
|
+
: 'Agent: ' + (agentBackend === 'codex-acp' ? 'Codex' : 'Claude Code') + ' is installed; a conversation can start in any project.');
|
|
223
262
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
say('');
|
|
233
|
-
say('Projects folder: ' + quickRoot);
|
|
234
|
-
say('Service state: ' + quickState);
|
|
235
|
-
say('');
|
|
236
|
-
const agentCommand = await import('@rho-workspace/plugin-agent/acp-command').catch(() => null);
|
|
237
|
-
// The selection the service itself got: an explicit flag, then the environment, then the name
|
|
238
|
-
// on PATH. Reporting "installed" for a client the person pointed at a missing path is worse
|
|
239
|
-
// than saying nothing.
|
|
240
|
-
const claudePath = values.claude ?? process.env.RHO_CLAUDE, codexPath = values.codex ?? process.env.RHO_CODEX;
|
|
241
|
-
const env = { ...process.env, ...(claudePath ? { CLAUDE_CODE_EXECUTABLE: claudePath } : {}), ...(codexPath ? { CODEX_PATH: codexPath } : {}) };
|
|
242
|
-
// An ACP agent named with --acp-command is the operator's own command; it is named, not checked.
|
|
243
|
-
if (acpCommand)
|
|
244
|
-
say('Agent: ' + acpCommand.file + ', named with --acp-command.');
|
|
245
|
-
else {
|
|
246
|
-
const missing = agentCommand ? agentCommand.missingClient(agentBackend === 'codex-acp' ? 'codex' : 'claude', env) : null;
|
|
247
|
-
say(missing
|
|
248
|
-
? 'Agent: ' + missing + ' Until then folders can be opened and files read here; the Agent cannot run in them.'
|
|
249
|
-
: 'Agent: ' + (agentBackend === 'codex-acp' ? 'Codex' : 'Claude Code') + ' is installed; a conversation can start in any project.');
|
|
263
|
+
const hasR = await fs.access(service.bridge.rscript).then(() => true, () => false);
|
|
264
|
+
say(hasR
|
|
265
|
+
? 'R: ' + service.bridge.rscript
|
|
266
|
+
: 'R: Rscript was not found. Reading, editing and viewing work; running R needs R installed (or --rscript).');
|
|
267
|
+
say('');
|
|
268
|
+
say('Stop it with Ctrl+C. Start it again with: npx @rho-workspace/rho');
|
|
269
|
+
if (process.stdout.isTTY)
|
|
270
|
+
openBrowser(service.url);
|
|
250
271
|
}
|
|
251
|
-
|
|
252
|
-
say(hasR
|
|
253
|
-
? 'R: ' + service.bridge.rscript
|
|
254
|
-
: 'R: Rscript was not found. Reading, editing and viewing work; running R needs R installed (or --rscript).');
|
|
255
|
-
say('');
|
|
256
|
-
say('Stop it with Ctrl+C. Start it again with: npx @rho-workspace/rho');
|
|
257
|
-
if (process.stdout.isTTY)
|
|
272
|
+
if (values.open)
|
|
258
273
|
openBrowser(service.url);
|
|
274
|
+
let closing = false;
|
|
275
|
+
// Windows has no SIGTERM another process can send, and a console may pass on Ctrl+C turned off;
|
|
276
|
+
// there Ctrl+Break on Rho's console (SIGBREAK) stops it in order too.
|
|
277
|
+
for (const signal of ['SIGTERM', 'SIGINT', ...(process.platform === 'win32' ? ['SIGBREAK'] : [])])
|
|
278
|
+
process.on(signal, async () => { if (closing)
|
|
279
|
+
return; closing = true; const reports = await service.close(); process.stdout.write(JSON.stringify({ shutdown: reports }) + '\n'); });
|
|
259
280
|
}
|
|
260
|
-
if (values.open)
|
|
261
|
-
openBrowser(service.url);
|
|
262
|
-
let closing = false;
|
|
263
|
-
// Windows has no SIGTERM another process can send, and a console may pass on Ctrl+C turned off;
|
|
264
|
-
// there Ctrl+Break on Rho's console (SIGBREAK) stops it in order too.
|
|
265
|
-
for (const signal of ['SIGTERM', 'SIGINT', ...(process.platform === 'win32' ? ['SIGBREAK'] : [])])
|
|
266
|
-
process.on(signal, async () => { if (closing)
|
|
267
|
-
return; closing = true; const reports = await service.close(); process.stdout.write(JSON.stringify({ shutdown: reports }) + '\n'); });
|
|
268
281
|
}
|