@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.
Files changed (102) hide show
  1. package/dist/browser-access.js +3 -2
  2. package/dist/capabilities.d.ts +2 -2
  3. package/dist/capabilities.js +6 -1
  4. package/dist/cli.js +186 -173
  5. package/dist/contract.generated.d.ts +329 -4
  6. package/dist/contract.generated.js +1 -1
  7. package/dist/contracts/owners.generated.js +360 -6
  8. package/dist/discovery.d.ts +1 -0
  9. package/dist/discovery.js +3 -3
  10. package/dist/extension-contract.d.ts +99 -0
  11. package/dist/extension-contract.js +52 -0
  12. package/dist/extension-directory.d.ts +9 -0
  13. package/dist/extension-directory.js +11 -0
  14. package/dist/extension-worker.d.ts +1 -0
  15. package/dist/extension-worker.js +36 -0
  16. package/dist/extensions.d.ts +157 -0
  17. package/dist/extensions.js +349 -0
  18. package/dist/plugin-cli.d.ts +1 -0
  19. package/dist/plugin-cli.js +139 -0
  20. package/dist/plugin-inventory.json +18 -0
  21. package/dist/plugins.d.ts +3 -3
  22. package/dist/project-work.d.ts +3 -1
  23. package/dist/project-work.js +15 -2
  24. package/dist/protocol.d.ts +7 -0
  25. package/dist/protocol.js +4 -2
  26. package/dist/server.d.ts +4 -1
  27. package/dist/server.js +55 -8
  28. package/dist/web/.vite/manifest.json +61 -61
  29. package/dist/web/assets/{canvas-BXhC8RDZ.js → canvas-D8rDdfDz.js} +1 -1
  30. package/dist/web/assets/choices-b0XhrGQV.js +1 -0
  31. package/dist/web/assets/{console-Dbdo-5oO.js → console-BSjLGe0T.js} +1 -1
  32. package/dist/web/assets/{dist-B2V0VDO0.js → dist-Buk0lwzI.js} +2 -2
  33. package/dist/web/assets/dist-Cw8hRIlT.js +1 -0
  34. package/dist/web/assets/dist-Tzoe_zm-.js +3 -0
  35. package/dist/web/assets/{dist-D25Vn17i.js → dist-foH9IKc6.js} +1 -1
  36. package/dist/web/assets/explore-in-BVUy3ZW_.js +1 -0
  37. package/dist/web/assets/graph-1OdIJ9l7.js +2 -0
  38. package/dist/web/assets/index-CsC3sTRq.css +1 -0
  39. package/dist/web/assets/{index-BHQIaDsx.js → index-CxmItXqa.js} +85 -85
  40. package/dist/web/assets/{notes-meaning-DdPbtNzJ.js → notes-meaning-pVbbQUGP.js} +1 -1
  41. package/dist/web/assets/overview-B9UtVJ32.js +6 -0
  42. package/dist/web/assets/preview-CksD_5KS.js +1 -0
  43. package/dist/web/assets/{review-CmWou4Iv.js → review-CMxQlQ-C.js} +1 -1
  44. package/dist/web/assets/{skills-Dy7n3L9d.js → skills-BFNSyrnD.js} +1 -1
  45. package/dist/web/assets/{skills-work-Cj0W6b-I.js → skills-work-BGPSnF_I.js} +1 -1
  46. package/dist/web/assets/{studio-process-QOY_n2_8.js → studio-process-CsRoTHOh.js} +2 -2
  47. package/dist/web/assets/{timeline-events-DmX1BCh3.js → timeline-events-BbuuFWHV.js} +1 -1
  48. package/dist/web/assets/{work-turns-Co0C71rJ.js → work-turns-BvJlIMmA.js} +1 -1
  49. package/dist/web/assets/workspace-CWlkjLNx.js +5 -0
  50. package/dist/web/composition.json +67 -67
  51. package/dist/web/index.html +3 -3
  52. package/dist/web/renderer.json +1 -1
  53. package/dist/web-assets.d.ts +2 -1
  54. package/dist/web-assets.js +3 -2
  55. package/dist/work-contract.js +2 -2
  56. package/docs/HEADLESS.md +1 -1
  57. package/docs/PLUGIN-AUTHORING.md +4 -0
  58. package/docs/THIRD-PARTY-PLUGINS.md +190 -0
  59. package/node_modules/@rho-workspace/plugin-agent/README.md +39 -0
  60. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.d.ts +1 -0
  61. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.js +2 -2
  62. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.d.ts +6 -0
  63. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.js +174 -0
  64. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.d.ts +82 -0
  65. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +15 -0
  66. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +73 -0
  67. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.js +190 -0
  68. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +373 -0
  69. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +1 -0
  70. package/node_modules/@rho-workspace/plugin-agent/dist/operations.d.ts +33 -0
  71. package/node_modules/@rho-workspace/plugin-agent/dist/operations.js +3 -0
  72. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.d.ts +14 -0
  73. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.js +2 -0
  74. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.d.ts +15 -0
  75. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.js +37 -0
  76. package/node_modules/@rho-workspace/plugin-agent/package.json +5 -1
  77. package/package.json +5 -3
  78. package/protocol/index.json +58 -4
  79. package/protocol/operations/agent.sessions.list.json +100 -0
  80. package/protocol/operations/agent.sessions.read.json +266 -0
  81. package/protocol/operations/extension.call.json +132 -0
  82. package/protocol/operations/extension.content.json +78 -0
  83. package/protocol/operations/extension.describe.json +195 -0
  84. package/protocol/operations/extension.install.json +198 -0
  85. package/protocol/operations/extension.list.json +123 -0
  86. package/protocol/operations/extension.manage.json +55 -0
  87. package/protocol/operations/extension.read.json +112 -0
  88. package/protocol/operations/project.work.read.json +3 -2
  89. package/protocol/operations/session.grant.json +9 -0
  90. package/scripts/third-party-acceptance.mjs +77 -0
  91. package/vendor/composition.json +85 -85
  92. package/vendor/rho-workspace-plugin-agent-0.4.29.tgz +0 -0
  93. package/dist/web/assets/choices-DU9kHlyQ.js +0 -1
  94. package/dist/web/assets/dist-BV3hWfn_.js +0 -3
  95. package/dist/web/assets/dist-CLNF2g-E.js +0 -1
  96. package/dist/web/assets/explore-in-8Q5ftt-E.js +0 -1
  97. package/dist/web/assets/graph-hS2kvHTI.js +0 -2
  98. package/dist/web/assets/index-Cj1_iqyK.css +0 -1
  99. package/dist/web/assets/overview-BV6ZO5TO.js +0 -6
  100. package/dist/web/assets/preview-n5lOOdjN.js +0 -1
  101. package/dist/web/assets/workspace-BKFefRfJ.js +0 -5
  102. package/vendor/rho-workspace-plugin-agent-0.4.28.tgz +0 -0
@@ -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;
@@ -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" | "skills")[];
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" | "skills")[];
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;
@@ -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.5', content: applicationContent,
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 = `Verify a read-only evidence package:
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
- let parsed;
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
- const { verifyEvidence } = await import('./evidence-export.js');
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 (e) {
90
- process.stderr.write((e instanceof Error ? e.message : String(e)) + '\n');
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
- // The Agent's side of a grant: its MCP server reads the connection file and speaks on stdio,
100
- // so nothing else may write to stdout in this mode.
101
- if (values.acp) {
102
- if (!['claude', 'codex'].includes(values['acp-agent'] ?? '')) {
103
- process.stderr.write('Select --acp-agent claude or codex.\n');
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 { runExternalAcp } = await import('@rho-workspace/plugin-agent/acp-cli');
107
- runExternalAcp(path.resolve(values.acp), values['acp-agent']);
108
- }
109
- else if (values.mcp) {
110
- process.env.RHO_AGENT_CONNECTION = path.resolve(values.mcp);
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
- if (values.agent !== undefined && !['claude-code', 'codex'].includes(values.agent)) {
122
- process.stderr.write('Choose --agent claude-code or codex.\n');
123
- process.exit(2);
101
+ catch (e) {
102
+ process.stderr.write((e instanceof Error ? e.message : String(e)) + '\n');
103
+ process.exitCode = 1;
124
104
  }
125
- if (values.agent !== undefined && values.session !== undefined) {
126
- process.stderr.write('--agent names the session itself; leave out --session.\n');
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 { RhoClient } = await import('./client.js');
130
- const client = await RhoClient.fromFile(path.resolve(values.grant)).catch(error => refuse(error));
131
- try {
132
- const project = await client.call('project.open', { path: values.project });
133
- // The same as the project page's button: the grant that Agent keeps, then its own
134
- // `mcp add`, recorded so the page shows it under that Agent.
135
- if (values.agent) {
136
- const connected = await client.call('session.connect', { projectId: project.id, agent: values.agent });
137
- process.stdout.write(JSON.stringify({ projectId: project.id, root: project.root, agent: values.agent, methods: connected.methods, granted: connected.granted, added: connected.added }) + '\n');
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
- const session = values.session ?? 'claude-code';
141
- const granted = await client.call('session.grant', { projectId: project.id, externalSessionId: session });
142
- const { mcpCommand, mcpName } = await import('./agent-connect.js');
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
- catch (error) {
150
- refuse(error, client.connection.url);
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
- if (values.pair) {
154
- const { RhoClient } = await import('./client.js');
155
- const client = await RhoClient.fromFile(path.resolve(values.pair)).catch(error => refuse(error));
156
- try {
157
- const pairing = await client.call('session.browser.pair', values.root ? { path: values.root } : {});
158
- const openUrl = link(client.connection.url, pairing.code);
159
- process.stdout.write(JSON.stringify({ url: client.connection.url, openUrl, pairingCode: pairing.code, pairingExpiresAt: pairing.expiresAt }) + '\n');
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
- catch (error) {
165
- refuse(error, client.connection.url);
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
- // A person who runs `npx @rho-workspace/rho` with no arguments wants the application, not a
169
- // usage page: start it on their own folders, say in plain words where it is, and open it when
170
- // this is an interactive terminal. Every flagged start is unchanged, so scripts and
171
- // acceptance runs keep the output they read.
172
- const quickStart = !values.root && !values['state-dir'];
173
- if (!quickStart && (!values.root || !values['state-dir'])) {
174
- process.stderr.write(usage);
175
- process.exit(2);
176
- }
177
- let quickRoot = '', quickState = '';
178
- if (quickStart) {
179
- const { homedir } = await import('node:os');
180
- quickRoot = path.join(homedir(), 'Rho', 'projects');
181
- quickState = path.join(homedir(), 'Rho', 'state');
182
- await fs.mkdir(quickRoot, { recursive: true });
183
- await fs.mkdir(quickState, { recursive: true });
184
- }
185
- const projectsRoot = values.root ?? quickRoot, stateDirectory = values['state-dir'] ?? quickState;
186
- const limits = values.limits ? JSON.parse(await fs.readFile(values.limits, 'utf8')) : {};
187
- 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']);
188
- if (Object.keys(limits).some(k => !allowed.has(k)))
189
- throw new Error('Unknown service budget in --limits file.');
190
- const { RhoService } = await import('./server.js'), { runtime } = await import('./runtime.js');
191
- const agentBackend = values['agent-backend'] ?? process.env.RHO_AGENT_BACKEND ?? 'claude-acp';
192
- if (!['claude-acp', 'codex-acp', 'claude-cli', 'acp'].includes(agentBackend))
193
- throw new Error('Choose --agent-backend claude-acp, codex-acp, claude-cli or acp.');
194
- const acpCommand = agentBackend === 'acp' ? parseAcpCommand(values['acp-command'] ?? process.env.RHO_ACP_COMMAND) : undefined;
195
- if (values.port !== undefined && !(/^\d+$/.test(values.port) && Number(values.port) <= 65535)) {
196
- process.stderr.write('Choose --port 0 to 65535, or leave it out.\n');
197
- process.exit(2);
198
- }
199
- // A Core that cannot be used (none for this platform, a changed digest) is read by a person.
200
- const native = await runtime(values.core, values.rscript).catch(e => { process.stderr.write((e instanceof Error ? e.message : String(e)) + '\n'); process.exit(1); });
201
- // A person starting Rho sees the project it ships with; a composition that starts a service
202
- // for its own work says so itself and is left alone.
203
- if (values.example !== undefined && !['on', 'off'].includes(values.example))
204
- throw new Error('Choose --example on or off.');
205
- const example = values.example !== 'off';
206
- 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) => {
207
- // Starting Rho again on a state it already runs on is the common refusal: say where it runs.
208
- const held = error?.details?.stateDirectory;
209
- const running = typeof held === 'string' ? await fs.readFile(path.join(held, 'connection.json'), 'utf8').then(text => String(JSON.parse(text).url), () => '') : '';
210
- 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);
211
- });
212
- // The narrow conclusion a person needs on one line: what was selected, where state
213
- // lives, and how to open it. It stays inside `ready` so a parser reading line one is
214
- // unaffected. Opening this url on this machine is the access; no link is minted.
215
- const rscript = values.rscript ?? process.env.RHO_RSCRIPT ?? 'Rscript';
216
- process.stdout.write(JSON.stringify({ ready: {
217
- url: service.url, connectionFile: path.join(service.state.directory, 'connection.json'), apiVersion: '2.0',
218
- openUrl: service.url,
219
- startup: {
220
- projects: path.resolve(projectsRoot), state: path.resolve(stateDirectory),
221
- core: service.bridge.binary, rscript: service.bridge.rscript,
222
- 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.'
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
- } }) + '\n');
225
- // What a person needs, after the machine line: where it is, and what is missing to work in it.
226
- // A missing Agent or R does not stop the workspace; it decides what the person can do next.
227
- if (quickStart) {
228
- const say = (text) => process.stdout.write(text + '\n');
229
- say('');
230
- say('Rho is running. Open this address in a browser on this machine:');
231
- say(' ' + service.url);
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
- const hasR = await fs.access(service.bridge.rscript).then(() => true, () => false);
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
  }