@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.
Files changed (211) hide show
  1. package/dist/agent-connect.d.ts +36 -2
  2. package/dist/agent-connect.js +64 -11
  3. package/dist/answer-board.js +3 -3
  4. package/dist/bridge.js +6 -1
  5. package/dist/browser-access.d.ts +1 -0
  6. package/dist/browser-access.js +5 -3
  7. package/dist/capabilities.d.ts +21 -5
  8. package/dist/capabilities.js +6 -4
  9. package/dist/cli.js +76 -11
  10. package/dist/composition.d.ts +1 -1
  11. package/dist/composition.js +13 -3
  12. package/dist/connect-cli.d.ts +50 -0
  13. package/dist/connect-cli.js +128 -0
  14. package/dist/contract.generated.d.ts +1191 -278
  15. package/dist/contract.generated.js +1 -1
  16. package/dist/contracts/owners.generated.js +845 -47
  17. package/dist/conversation-presentation.d.ts +9 -2
  18. package/dist/conversation-presentation.js +3 -1
  19. package/dist/conversations.d.ts +154 -8
  20. package/dist/conversations.js +147 -24
  21. package/dist/discovery.js +2 -2
  22. package/dist/extension-contract.d.ts +19 -0
  23. package/dist/extension-contract.js +15 -6
  24. package/dist/extension-markets.d.ts +74 -0
  25. package/dist/extension-markets.js +166 -0
  26. package/dist/extensions.d.ts +29 -1
  27. package/dist/extensions.js +13 -4
  28. package/dist/external-work.d.ts +22 -0
  29. package/dist/external-work.js +91 -0
  30. package/dist/plugin-cli.js +75 -3
  31. package/dist/plugin-inventory.d.ts +56 -0
  32. package/dist/plugin-inventory.js +41 -0
  33. package/dist/plugin-inventory.json +1581 -102
  34. package/dist/plugin-presentation.d.ts +105 -2
  35. package/dist/plugin-presentation.js +59 -5
  36. package/dist/plugins.d.ts +14 -3
  37. package/dist/plugins.js +12 -6
  38. package/dist/project-notes.js +1 -1
  39. package/dist/protocol.d.ts +6 -0
  40. package/dist/protocol.js +30 -12
  41. package/dist/replay.js +4 -0
  42. package/dist/response-contracts.js +26 -11
  43. package/dist/running.d.ts +10 -0
  44. package/dist/running.js +45 -0
  45. package/dist/server.d.ts +2 -0
  46. package/dist/server.js +71 -8
  47. package/dist/state.d.ts +8 -0
  48. package/dist/state.js +8 -0
  49. package/dist/studio-layout.js +3 -3
  50. package/dist/task-frame.d.ts +1 -1
  51. package/dist/timeline-events.d.ts +1 -0
  52. package/dist/timeline-events.js +8 -1
  53. package/dist/ui-context.d.ts +73 -0
  54. package/dist/ui-context.js +98 -0
  55. package/dist/ui-contract.d.ts +23 -0
  56. package/dist/web/.vite/manifest.json +133 -74
  57. package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
  58. package/dist/web/assets/choices-D8vGwsl2.js +1 -0
  59. package/dist/web/assets/{console-Trqc0swD.js → console-Dyuo1q4N.js} +4 -4
  60. package/dist/web/assets/{dist-pF902jHA.js → dist-CSiCyBml.js} +1 -1
  61. package/dist/web/assets/{dist-DpIHRk3B.js → dist-D9FLb9Hm.js} +1 -1
  62. package/dist/web/assets/{dist-eMx6b8N1.js → dist-FXbTBwML.js} +1 -1
  63. package/dist/web/assets/{dist-DZmszrjG.js → dist-l3DUhbw5.js} +1 -1
  64. package/dist/web/assets/{explore-in-BCHqjJ7B.js → explore-in-BfSFGRh2.js} +1 -1
  65. package/dist/web/assets/graph-BU2_o5uA.js +2 -0
  66. package/dist/web/assets/index-C16mXVvk.css +1 -0
  67. package/dist/web/assets/index-CWg9XYXx.js +952 -0
  68. package/dist/web/assets/markdown.reader-Ct2-e2Zt.png +0 -0
  69. package/dist/web/assets/{notes-meaning-DcVGUBBY.js → notes-meaning-T2m5zQfv.js} +1 -1
  70. package/dist/web/assets/overview-PzfLJCo-.js +6 -0
  71. package/dist/web/assets/preview-BguEdsza.js +1 -0
  72. package/dist/web/assets/previews-pWNik-2g.js +3 -0
  73. package/dist/web/assets/{review-DYP0aCXb.js → review-Dy_85lcH.js} +1 -1
  74. package/dist/web/assets/rho.editor-CCJeyHma.png +0 -0
  75. package/dist/web/assets/rho.flow-WkMj6fRS.png +0 -0
  76. package/dist/web/assets/rho.media-Cc6amodO.png +0 -0
  77. package/dist/web/assets/rho.notebook-6vsZKGDq.png +0 -0
  78. package/dist/web/assets/rho.object-BGoeXjte.png +0 -0
  79. package/dist/web/assets/rho.plots-yIiISCkb.png +0 -0
  80. package/dist/web/assets/rho.scatter-DwHxesCs.png +0 -0
  81. package/dist/web/assets/rho.servers-DKf_mnNq.png +0 -0
  82. package/dist/web/assets/rho.table-DWOGaWDa.png +0 -0
  83. package/dist/web/assets/rho.variables-CKcWczhQ.png +0 -0
  84. package/dist/web/assets/{skills-1ec0Z-jx.js → skills-Dyz7mBlc.js} +1 -1
  85. package/dist/web/assets/{skills-work-DNexV4OJ.js → skills-work-BEGqlCkx.js} +1 -1
  86. package/dist/web/assets/studio-process-D-ykbC4D.js +2 -0
  87. package/dist/web/assets/timeline-events-DKTho9fU.js +1 -0
  88. package/dist/web/assets/work-turns-DSabCogR.js +1 -0
  89. package/dist/web/assets/{workspace-COmCbGV3.js → workspace-DfjBL--r.js} +2 -2
  90. package/dist/web/composition.json +138 -83
  91. package/dist/web/index.html +3 -3
  92. package/dist/web/renderer.json +1 -1
  93. package/dist/web-assets.js +2 -2
  94. package/dist/work-graph.d.ts +11 -1
  95. package/dist/work-graph.js +16 -5
  96. package/dist/workspace.d.ts +2 -2
  97. package/dist/workspace.js +8 -7
  98. package/docs/FRONTEND.md +53 -11
  99. package/docs/HEADLESS.md +104 -11
  100. package/docs/THIRD-PARTY-PLUGINS.md +75 -3
  101. package/node_modules/@rho-workspace/plugin-agent/README.md +5 -0
  102. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +31 -0
  103. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +45 -10
  104. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +19 -3
  105. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +3 -2
  106. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +1 -1
  107. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
  108. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
  109. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
  110. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
  111. package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
  112. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +1 -1
  113. package/node_modules/@rho-workspace/plugin-agent/package.json +1 -1
  114. package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
  115. package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
  116. package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
  117. package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
  118. package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
  119. package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
  120. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
  121. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
  122. package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
  123. package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
  124. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
  125. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
  126. package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
  127. package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
  128. package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
  129. package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
  130. package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
  131. package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
  132. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
  133. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
  134. package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
  135. package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
  136. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
  137. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
  138. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
  139. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
  140. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
  141. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
  142. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
  143. package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
  144. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
  145. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
  146. package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
  147. package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
  148. package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
  149. package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
  150. package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
  151. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
  152. package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
  153. package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
  154. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
  155. package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
  156. package/node_modules/jose/package.json +1 -1
  157. package/package.json +6 -4
  158. package/protocol/index.json +91 -55
  159. package/protocol/operations/agent.sessions.list.json +13 -5
  160. package/protocol/operations/agent.sessions.read.json +13 -5
  161. package/protocol/operations/capability.list.json +382 -6
  162. package/protocol/operations/composition.apply.json +10 -2
  163. package/protocol/operations/composition.delete.json +845 -0
  164. package/protocol/operations/composition.read.json +10 -2
  165. package/protocol/operations/composition.undo.json +10 -2
  166. package/protocol/operations/conversation.agents.add.json +346 -0
  167. package/protocol/operations/conversation.agents.remove.json +340 -0
  168. package/protocol/operations/conversation.decide.json +196 -0
  169. package/protocol/operations/conversation.permission.json +196 -0
  170. package/protocol/operations/conversation.read.json +196 -0
  171. package/protocol/operations/conversation.send.json +265 -0
  172. package/protocol/operations/conversation.settings.check.json +213 -495
  173. package/protocol/operations/conversation.settings.read.json +211 -490
  174. package/protocol/operations/conversation.settings.update.json +235 -554
  175. package/protocol/operations/conversation.start.json +267 -5
  176. package/protocol/operations/conversation.stop.json +196 -0
  177. package/protocol/operations/extension.describe.json +39 -0
  178. package/protocol/operations/extension.install.json +62 -0
  179. package/protocol/operations/extension.list.json +164 -1
  180. package/protocol/operations/extension.market.change.json +210 -0
  181. package/protocol/operations/figure.read.json +265 -0
  182. package/protocol/operations/figure.select.json +157 -0
  183. package/protocol/operations/kernel.interrupt.json +119 -0
  184. package/protocol/operations/kernel.read.json +119 -0
  185. package/protocol/operations/kernel.run.json +119 -0
  186. package/protocol/operations/observation.snapshot.json +416 -8
  187. package/protocol/operations/project.openFile.json +10 -2
  188. package/protocol/operations/session.grant.json +4 -0
  189. package/protocol/operations/shared.json +1 -0
  190. package/protocol/operations/view.close.json +10 -2
  191. package/protocol/operations/view.protection.set.json +10 -2
  192. package/vendor/composition.json +177 -112
  193. package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
  194. package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
  195. package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
  196. package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
  197. package/web/theme.css +5 -0
  198. package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
  199. package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
  200. package/dist/web/assets/choices-D-YfnFP6.js +0 -1
  201. package/dist/web/assets/graph-lENgfQNH.js +0 -2
  202. package/dist/web/assets/index-B3_OVy91.css +0 -1
  203. package/dist/web/assets/index-BpL_Kl_s.js +0 -892
  204. package/dist/web/assets/overview-CDnVKlOt.js +0 -6
  205. package/dist/web/assets/preview-BEC1E9MK.js +0 -1
  206. package/dist/web/assets/studio-process-EYQgikxq.js +0 -2
  207. package/dist/web/assets/timeline-events-Chj2Gbx7.js +0 -1
  208. package/dist/web/assets/work-turns-CT9RVcWY.js +0 -1
  209. package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
  210. package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
  211. package/vendor/rho-workspace-plugin-tables-0.5.0.tgz +0 -0
@@ -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;
@@ -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
- // Paths are data in an argv array, never an interpolated shell command. Reading these
17
- // instructions starts nothing, grants no access and includes no credential bytes.
18
- export function connectionInstructions(project, stateDirectory) {
19
- const command = [process.execPath, fileURLToPath(new URL('./cli.js', import.meta.url)), '--grant', path.join(stateDirectory, 'connection.json'), '--project-id', project.id];
20
- const request = `Connect yourself to my existing Rho project ${JSON.stringify(path.basename(project.root))} so we can work on it together.`;
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
- `Project folder: ${JSON.stringify(project.root)}. Project ID: ${project.id}. Rho is already running on this machine.`,
23
- 'Configure your own client using its supported MCP setup mechanism; inspect your own help or documentation if needed. Keep unrelated configuration, models and credentials intact. Use private local configuration, scoped to this project where supported, and keep connection settings out of version-controlled files. If you cannot run a local stdio MCP server, explain that limitation.',
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
  }
@@ -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 env = { ...process.env, RHO_CORE_STOP_FILE: stopFile(root), ...Object.fromEntries(values.map((value, i) => ['RHO_HOST_ARG' + i, value])) };
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 {
@@ -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;
@@ -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;
@@ -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" | "skills" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel")[];
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" | "files" | "skills" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel")[];
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
- readSkill(principal: Principal, skillId: string, expectedDigest?: string): Promise<SkillDeclaration>;
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
  }
@@ -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.1', content: analysisRunContent,
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 session = values.session ?? 'claude-code';
178
- const granted = await client.call('session.grant', { projectId: project.id, externalSessionId: session });
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, async () => { if (closing)
309
- return; closing = true; const reports = await service.close(); process.stdout.write(JSON.stringify({ shutdown: reports }) + '\n'); });
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
  }
@@ -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
  }
@@ -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 {};