@rho-workspace/rho 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/dist/agent-connect.d.ts +42 -0
  2. package/dist/agent-connect.js +68 -0
  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 +113 -18
  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 +1437 -381
  15. package/dist/contract.generated.js +1 -1
  16. package/dist/contracts/owners.generated.js +1160 -267
  17. package/dist/conversation-presentation.d.ts +9 -2
  18. package/dist/conversation-presentation.js +3 -1
  19. package/dist/conversations.d.ts +306 -7
  20. package/dist/conversations.js +208 -22
  21. package/dist/discovery.js +3 -3
  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 +7 -0
  40. package/dist/protocol.js +40 -15
  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 +4 -10
  46. package/dist/server.js +140 -14
  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 +135 -76
  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-BSjLGe0T.js → console-Dyuo1q4N.js} +4 -4
  60. package/dist/web/assets/{dist-Tzoe_zm-.js → dist-CSiCyBml.js} +1 -1
  61. package/dist/web/assets/{dist-Cw8hRIlT.js → dist-D9FLb9Hm.js} +1 -1
  62. package/dist/web/assets/{dist-foH9IKc6.js → dist-FXbTBwML.js} +1 -1
  63. package/dist/web/assets/{dist-Buk0lwzI.js → dist-l3DUhbw5.js} +1 -1
  64. package/dist/web/assets/{explore-in-BVUy3ZW_.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-pVbbQUGP.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-CMxQlQ-C.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-BFNSyrnD.js → skills-Dyz7mBlc.js} +1 -1
  85. package/dist/web/assets/{skills-work-BGPSnF_I.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-CWlkjLNx.js → workspace-DfjBL--r.js} +2 -2
  90. package/dist/web/composition.json +135 -80
  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 +71 -21
  99. package/docs/HEADLESS.md +127 -24
  100. package/docs/THIRD-PARTY-PLUGINS.md +75 -3
  101. package/node_modules/@rho-workspace/plugin-agent/README.md +34 -0
  102. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +32 -0
  103. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +54 -11
  104. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
  105. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +29 -3
  106. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +11 -5
  107. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +12 -1
  108. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
  109. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
  110. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
  111. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
  112. package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
  113. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
  114. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
  115. package/node_modules/@rho-workspace/plugin-agent/package.json +2 -1
  116. package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
  117. package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
  118. package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
  119. package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
  120. package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
  121. package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
  122. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
  123. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
  124. package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
  125. package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
  126. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
  127. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
  128. package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
  129. package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
  130. package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
  131. package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
  132. package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
  133. package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
  134. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
  135. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
  136. package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
  137. package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
  138. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
  139. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
  140. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
  141. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
  142. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
  143. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
  144. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
  145. package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
  146. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
  147. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
  148. package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
  149. package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
  150. package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
  151. package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
  152. package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
  153. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
  154. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
  155. package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
  156. package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
  157. package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
  158. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
  159. package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
  160. package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
  161. package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
  162. package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
  163. package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
  164. package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
  165. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
  166. package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
  167. package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
  168. package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
  169. package/node_modules/hono/dist/cjs/utils/url.js +1 -1
  170. package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
  171. package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
  172. package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
  173. package/node_modules/hono/dist/jsx/base.js +49 -32
  174. package/node_modules/hono/dist/jsx/components.js +2 -2
  175. package/node_modules/hono/dist/jsx/constants.js +2 -1
  176. package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
  177. package/node_modules/hono/dist/middleware/etag/index.js +3 -1
  178. package/node_modules/hono/dist/middleware/language/language.js +1 -1
  179. package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
  180. package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
  181. package/node_modules/hono/dist/utils/cookie.js +2 -2
  182. package/node_modules/hono/dist/utils/mime.js +1 -1
  183. package/node_modules/hono/dist/utils/url.js +1 -1
  184. package/node_modules/hono/package.json +1 -1
  185. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
  186. package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
  187. package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
  188. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
  189. package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
  190. package/node_modules/jose/package.json +1 -1
  191. package/package.json +6 -4
  192. package/protocol/index.json +106 -64
  193. package/protocol/operations/agent.sessions.list.json +57 -3
  194. package/protocol/operations/agent.sessions.read.json +61 -4
  195. package/protocol/operations/capability.list.json +382 -6
  196. package/protocol/operations/composition.apply.json +10 -2
  197. package/protocol/operations/composition.delete.json +845 -0
  198. package/protocol/operations/composition.read.json +10 -2
  199. package/protocol/operations/composition.undo.json +10 -2
  200. package/protocol/operations/conversation.agents.add.json +346 -0
  201. package/protocol/operations/conversation.agents.remove.json +340 -0
  202. package/protocol/operations/conversation.configure.json +29 -0
  203. package/protocol/operations/conversation.decide.json +225 -0
  204. package/protocol/operations/conversation.list.json +29 -0
  205. package/protocol/operations/conversation.permission.json +225 -0
  206. package/protocol/operations/conversation.read.json +225 -0
  207. package/protocol/operations/conversation.restore.json +29 -0
  208. package/protocol/operations/conversation.send.json +294 -0
  209. package/protocol/operations/conversation.settings.check.json +226 -416
  210. package/protocol/operations/conversation.settings.read.json +224 -411
  211. package/protocol/operations/conversation.settings.update.json +248 -475
  212. package/protocol/operations/conversation.start.json +313 -6
  213. package/protocol/operations/conversation.stop.json +225 -0
  214. package/protocol/operations/conversation.trash.json +29 -0
  215. package/protocol/operations/extension.describe.json +39 -0
  216. package/protocol/operations/extension.install.json +62 -0
  217. package/protocol/operations/extension.list.json +164 -1
  218. package/protocol/operations/extension.market.change.json +210 -0
  219. package/protocol/operations/figure.read.json +265 -0
  220. package/protocol/operations/figure.select.json +157 -0
  221. package/protocol/operations/kernel.interrupt.json +119 -0
  222. package/protocol/operations/kernel.read.json +119 -0
  223. package/protocol/operations/kernel.run.json +119 -0
  224. package/protocol/operations/observation.snapshot.json +445 -8
  225. package/protocol/operations/project.openFile.json +10 -2
  226. package/protocol/operations/session.grant.json +4 -0
  227. package/protocol/operations/session.instructions.json +43 -0
  228. package/protocol/operations/shared.json +1 -0
  229. package/protocol/operations/view.close.json +10 -2
  230. package/protocol/operations/view.protection.set.json +10 -2
  231. package/vendor/composition.json +174 -109
  232. package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
  233. package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
  234. package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
  235. package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
  236. package/web/theme.css +17 -0
  237. package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
  238. package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
  239. package/dist/web/assets/choices-b0XhrGQV.js +0 -1
  240. package/dist/web/assets/graph-1OdIJ9l7.js +0 -2
  241. package/dist/web/assets/index-CsC3sTRq.css +0 -1
  242. package/dist/web/assets/index-CxmItXqa.js +0 -891
  243. package/dist/web/assets/overview-B9UtVJ32.js +0 -6
  244. package/dist/web/assets/preview-CksD_5KS.js +0 -1
  245. package/dist/web/assets/studio-process-CsRoTHOh.js +0 -2
  246. package/dist/web/assets/timeline-events-BbuuFWHV.js +0 -1
  247. package/dist/web/assets/work-turns-BvJlIMmA.js +0 -1
  248. package/vendor/rho-workspace-plugin-agent-0.4.29.tgz +0 -0
  249. package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
  250. package/vendor/rho-workspace-plugin-tables-0.5.0.tgz +0 -0
@@ -12,6 +12,48 @@ export declare const connectLimits: {
12
12
  };
13
13
  export declare function mcpName(root: string): string;
14
14
  export declare function mcpCommand(connectionFile: string): string[];
15
+ export declare function clientEntries(name: string, command: string[], root: string, env?: NodeJS.ProcessEnv): {
16
+ claude: string;
17
+ codex: string;
18
+ opencode: {
19
+ file: string;
20
+ merge: {
21
+ mcp: {
22
+ [name]: {
23
+ type: string;
24
+ command: string[];
25
+ enabled: boolean;
26
+ };
27
+ };
28
+ };
29
+ };
30
+ dsh: {
31
+ file: string;
32
+ workspace: {
33
+ plugin: string;
34
+ minimumVersion: string;
35
+ guide: string;
36
+ mergeConfig: {
37
+ command: string;
38
+ args: string[];
39
+ connectionFiles: (string | undefined)[];
40
+ };
41
+ instructions: string;
42
+ };
43
+ append: string;
44
+ };
45
+ };
46
+ export declare const defaultStateDirectory: () => string;
47
+ export declare function launcher(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): Promise<string[]>;
48
+ export declare function commandLine(argv: string[], platform?: NodeJS.Platform): string;
49
+ export declare function connectionInstructions(project: {
50
+ id: string;
51
+ root: string;
52
+ }, stateDirectory: string): Promise<{
53
+ request: string;
54
+ prompt: string;
55
+ command: string[];
56
+ }>;
15
57
  export declare function addToAgent(agent: OutsideAgent, binary: string, root: string, name: string, command: string[], limits?: {
16
58
  timeoutMs: number;
17
59
  outputBytes: number;
@@ -1,7 +1,10 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
2
4
  import path from 'node:path';
3
5
  import { fileURLToPath } from 'node:url';
4
6
  import { Fault } from '@rho-workspace/plugin-contracts';
7
+ import { announced } from './running.js';
5
8
  export const outsideLabel = { 'claude-code': 'Claude Code', codex: 'Codex' };
6
9
  // The Agent's CLI is the protected resource: each `mcp` command runs for at most timeoutMs and
7
10
  // keeps at most outputBytes of what it printed; past either, it is stopped and the step fails
@@ -13,6 +16,71 @@ export function mcpName(root) {
13
16
  export function mcpCommand(connectionFile) {
14
17
  return [process.execPath, fileURLToPath(new URL('./cli.js', import.meta.url)), '--mcp', connectionFile];
15
18
  }
19
+ // Ready entries for the clients whose own mechanism has been used with Rho, so an Agent need not
20
+ // search its documentation for it: Claude Code's and Codex's `mcp add`, OpenCode's project
21
+ // configuration and DeepSeek Harness's profile patch (no `dsh mcp` command exists). Each holds
22
+ // the same name, command and arguments as `mcp`; any other client registers those itself.
23
+ export function clientEntries(name, command, root, env = process.env) {
24
+ const quote = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
25
+ const yaml = (value) => JSON.stringify(value);
26
+ return {
27
+ claude: ['claude', 'mcp', 'add', name, '--', ...command].map(quote).join(' '),
28
+ codex: ['codex', 'mcp', 'add', name, '--', ...command].map(quote).join(' '),
29
+ opencode: { file: path.join(root, '.opencode', 'opencode.json'), merge: { mcp: { [name]: { type: 'local', command, enabled: true } } } },
30
+ dsh: { file: path.join(env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'profiles', 'desktop', 'cordis.patch.yml'),
31
+ workspace: { plugin: 'dsh-rho', minimumVersion: '0.1.6', guide: 'https://yulab-smu.top/Rho/docs/reference/deepseek-harness.html',
32
+ mergeConfig: { command: command[0], args: command.slice(1, -2), connectionFiles: [command.at(-1)] },
33
+ instructions: 'For conversations and process views, use the dsh-rho plugin. Merge this config into its existing entry, appending the connection file without removing other projects. The append entry below supplies MCP tools only; it does not observe Harness conversations.' },
34
+ append: ['- insert:', ` - id: ${yaml('mcp-' + name)}`, ' name: "@deepseek-ai/dsh-mcp-client"', ' config:', ` serverName: ${yaml(name)}`, ' transport: "stdio"',
35
+ ` command: ${yaml(command[0])}`, ' args:', ...command.slice(1).map(arg => ` - ${yaml(arg)}`)].join('\n') + '\n' },
36
+ };
37
+ }
38
+ // The state folder of a Rho started without --state-dir, which `rho --stop` and `rho connect`
39
+ // also take without it.
40
+ export const defaultStateDirectory = () => path.join(os.homedir(), 'Rho', 'state');
41
+ // How a shell runs this Rho: `rho` when the first `rho` on PATH is this installation (npm's
42
+ // link, or its .cmd shim on Windows), otherwise this Node with this cli.js.
43
+ export async function launcher(env = process.env, platform = process.platform) {
44
+ const cli = fileURLToPath(new URL('./cli.js', import.meta.url)), real = await fs.realpath(cli).catch(() => cli);
45
+ for (const dir of (env.PATH ?? '').split(path.delimiter).filter(Boolean)) {
46
+ const candidate = path.join(dir, platform === 'win32' ? 'rho.cmd' : 'rho');
47
+ let target;
48
+ try {
49
+ if (platform === 'win32') {
50
+ const shim = (await fs.readFile(candidate, 'utf8')).match(/"%dp0%\\([^"]*?cli\.js)"/i);
51
+ target = shim ? await fs.realpath(path.resolve(dir, shim[1])).catch(() => null) : null;
52
+ }
53
+ else {
54
+ target = await fs.realpath(candidate);
55
+ await fs.access(candidate, fs.constants.X_OK).catch(() => { target = null; });
56
+ }
57
+ }
58
+ catch {
59
+ continue;
60
+ }
61
+ return target === real ? ['rho'] : [process.execPath, cli];
62
+ }
63
+ return [process.execPath, cli];
64
+ }
65
+ // A command line to read and paste: plain words as they are, anything else in double quotes.
66
+ export function commandLine(argv, platform = process.platform) {
67
+ return argv.map(arg => /^[A-Za-z0-9_./:@=+,-]+$/.test(arg) ? arg : platform === 'win32' ? `"${arg}"` : `"${arg.replace(/(["\\$`])/g, '\\$1')}"`).join(' ');
68
+ }
69
+ // The prompt names the project and one command; `rho connect` tells the Agent the rest. It names
70
+ // --state-dir only when `rho connect` would not find this Rho itself: a state folder other than
71
+ // ~/Rho/state that no running note points to (a service started without the CLI). Reading
72
+ // these instructions starts nothing, grants no access and includes no credential bytes.
73
+ export async function connectionInstructions(project, stateDirectory) {
74
+ const found = path.resolve(stateDirectory) === path.resolve(defaultStateDirectory()) || await announced(stateDirectory);
75
+ const state = found ? [] : ['--state-dir', stateDirectory];
76
+ const command = [...await launcher(), 'connect', project.root, ...state];
77
+ const request = `Connect yourself to my Rho project ${JSON.stringify(path.basename(project.root))} so we can work on it together.`;
78
+ const prompt = [request, '',
79
+ 'Rho is already running on this machine. Run this command with a short, stable name for yourself in place of NAME, and follow its reply:',
80
+ '', commandLine([...command, '--as', 'NAME']),
81
+ ].join('\n');
82
+ return { request, prompt, command };
83
+ }
16
84
  function run(binary, args, cwd, limits) {
17
85
  return new Promise((resolve, reject) => {
18
86
  const child = spawn(binary, args, { cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
@@ -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.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
@@ -2,10 +2,16 @@
2
2
  import path from 'node:path';
3
3
  import { parseArgs } from 'node:util';
4
4
  import * as fs from 'node:fs/promises';
5
+ import { constants as fsConstants } from 'node:fs';
6
+ import os from 'node:os';
5
7
  import { spawn } from 'node:child_process';
6
8
  // A widened link is optional: the fragment carries a one-time code, which no request or Referer
7
9
  // sends and which the page redeems and removes. Opening the plain url needs no code at all.
8
10
  const link = (url, code) => url + '/#pair=' + code;
11
+ // What Ctrl+C does, asked by another program: Windows has no signal one process can send to Rho
12
+ // there, so `rho --stop` leaves this file in the state folder and the running launcher looks for it,
13
+ // as Rho asks its Host to stop on Windows.
14
+ const stopRequest = (stateDirectory) => path.join(stateDirectory, 'service.stop');
9
15
  function openBrowser(target) {
10
16
  const [command, args] = process.platform === 'darwin' ? ['open', [target]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', target]] : ['xdg-open', [target]];
11
17
  spawn(command, args, { stdio: 'ignore', detached: true }).on('error', () => process.stderr.write('Could not open a browser. Open openUrl from the output instead.\n')).unref();
@@ -41,12 +47,17 @@ Start Rho on ~/Rho/projects and ~/Rho/state, and say where it is and what is mis
41
47
  rho
42
48
  Start Rho:
43
49
  rho --root PROJECTS_DIRECTORY --state-dir SERVICE_STATE [--core ABSOLUTE_CORE_BINARY] [--rscript ABSOLUTE_RSCRIPT] [--python PYTHON] [--ssh SSH] [--serverctl SERVERCTL] [--claude CLAUDE_CODE_BINARY] [--codex CODEX_BINARY] [--agent-backend claude-acp|codex-acp|claude-cli|acp] [--acp-command JSON_ARGV] [--port PORT] [--limits JSON_FILE] [--plugins documents,files,...] [--example on|off] [--open]
50
+ Stop the Rho running for a state folder (~/Rho/state without --state-dir), as Ctrl+C in its window does:
51
+ rho --stop [--state-dir SERVICE_STATE]
44
52
  A narrower browser link (optional):
45
53
  rho --pair PRIVATE_CONNECTION_FILE [--root PROJECTS_RELATIVE_DIRECTORY] [--open]
46
- Add Rho to your own Claude Code or Codex for one project, as the project page's button does:
54
+ Connect yourself, an Agent, to a project of the running Rho (what a setup prompt asks):
55
+ rho connect [PROJECT_FOLDER] --as NAME [--state-dir SERVICE_STATE] [--json]
56
+ Add Rho to your own Claude Code or Codex for one project:
47
57
  rho --grant PRIVATE_CONNECTION_FILE --project PROJECTS_RELATIVE_DIRECTORY --agent claude-code|codex
48
- Give an Agent access to one project, and the command that adds it to Claude Code:
58
+ Give any MCP Agent access to one project and print its stdio MCP configuration:
49
59
  rho --grant PRIVATE_CONNECTION_FILE --project PROJECTS_RELATIVE_DIRECTORY [--session NAME]
60
+ rho --grant PRIVATE_CONNECTION_FILE --project-id EXISTING_PROJECT_ID --session NAME
50
61
  Serve that access to the Agent over MCP (stdio):
51
62
  rho --mcp AGENT_CONNECTION_FILE
52
63
  Observe an external ACP client (use this as its agent command):
@@ -60,8 +71,24 @@ narrower, expiring link for one folder.
60
71
  PRIVATE_CONNECTION_FILE is the connectionFile the running service printed; it and every
61
72
  connection file are credentials.
62
73
  `;
74
+ // DeepSeek Harness speaks ACP through its `acp` profile (`dsh acp`). It is used only when both the
75
+ // launcher and that profile are on this computer; nothing is started to find them.
76
+ async function harnessAcp() {
77
+ const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
78
+ const profile = await fs.stat(path.join(home, 'profiles', 'acp', 'package.json')).then(s => s.isFile(), () => false);
79
+ if (!profile)
80
+ return undefined;
81
+ const names = process.platform === 'win32' ? ['dsh.cmd', 'dsh.exe', 'dsh'] : ['dsh'];
82
+ for (const dir of (process.env.PATH ?? '').split(path.delimiter).filter(Boolean))
83
+ for (const name of names) {
84
+ const file = path.join(dir, name);
85
+ if (await fs.access(file, fsConstants.X_OK).then(() => true, () => false))
86
+ return { file, args: ['acp'] };
87
+ }
88
+ return undefined;
89
+ }
63
90
  const options = { example: { type: 'string' }, 'verify-evidence': { type: 'string' }, acp: { type: 'string' }, 'acp-agent': { type: 'string' }, root: { type: 'string' }, 'state-dir': { type: 'string' }, core: { type: 'string' }, rscript: { type: 'string' }, python: { type: 'string' }, ssh: { type: 'string' }, serverctl: { type: 'string' }, claude: { type: 'string' }, codex: { type: 'string' }, 'agent-backend': { type: 'string' }, 'acp-command': { type: 'string' }, port: { type: 'string' }, limits: { type: 'string' }, plugins: { type: 'string' }, pair: { type: 'string' }, open: { type: 'boolean' },
64
- grant: { type: 'string' }, project: { type: 'string' }, session: { type: 'string' }, agent: { type: 'string' }, mcp: { type: 'string' }, help: { type: 'boolean', short: 'h' } };
91
+ grant: { type: 'string' }, project: { type: 'string' }, 'project-id': { type: 'string' }, session: { type: 'string' }, agent: { type: 'string' }, mcp: { type: 'string' }, stop: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } };
65
92
  // A command that is refused says why and what to do. A service that is not running is the
66
93
  // common case for --grant and --pair; anything without a known reason keeps its stack.
67
94
  function refuse(error, url) {
@@ -83,6 +110,9 @@ if (process.argv[2] === 'plugin') {
83
110
  process.exitCode = 1;
84
111
  }
85
112
  }
113
+ else if (process.argv[2] === 'connect') {
114
+ process.exitCode = await (await import('./connect-cli.js')).connectCli(process.argv.slice(3));
115
+ }
86
116
  else {
87
117
  let parsed;
88
118
  try {
@@ -108,6 +138,55 @@ else {
108
138
  process.stdout.write(usage);
109
139
  process.exit(0);
110
140
  }
141
+ if (values.stop) {
142
+ const { lease } = await import('./state.js');
143
+ const directory = path.resolve(values['state-dir'] ?? path.join(os.homedir(), 'Rho', 'state'));
144
+ // The state's writer lock says whether a service runs there: a lock this command can take is free.
145
+ // A folder without a connection file was never a Rho state, and gets no lock file of its own.
146
+ const running = () => lease(directory).then(async (release) => { await release(); return false; }, (e) => { if (e?.kind === 'conflict')
147
+ return true; throw e; });
148
+ const asked = () => fs.access(stopRequest(directory)).then(() => true, () => false);
149
+ const connection = await fs.readFile(path.join(directory, 'connection.json'), 'utf8').then(text => JSON.parse(text)).catch(() => null);
150
+ if (!connection || !await running()) {
151
+ process.stderr.write(`Rho is not running for ${directory}.\n`);
152
+ process.exit(1);
153
+ }
154
+ await fs.writeFile(stopRequest(directory), '');
155
+ // The launcher takes the request within a second and then stops as on Ctrl+C: its output ends
156
+ // with the {"shutdown":…} line, and the lock is free once its state is written.
157
+ const since = Date.now();
158
+ while (await running()) {
159
+ if (Date.now() - since > 10000 && await asked()) {
160
+ await fs.rm(stopRequest(directory), { force: true });
161
+ process.stderr.write(`The Rho running for ${directory} did not take the stop request: it is a version without --stop. Stop it with Ctrl+C in its own window (Ctrl+Break on Windows).\n`);
162
+ process.exit(1);
163
+ }
164
+ if (Date.now() - since > 120000) {
165
+ process.stderr.write(`Rho for ${directory} is still stopping after two minutes. Its output ends with a {"shutdown":…} line once it has stopped.\n`);
166
+ process.exit(1);
167
+ }
168
+ await new Promise(resolve => setTimeout(resolve, 250));
169
+ }
170
+ // The lock is released before the launcher prints its report and ends, and on Windows the log
171
+ // it writes stays open until then: a start right after would find that file in use.
172
+ const pid = Number.isInteger(connection.pid) ? connection.pid : 0;
173
+ const alive = () => { try {
174
+ process.kill(pid, 0);
175
+ return true;
176
+ }
177
+ catch (e) {
178
+ return e.code === 'EPERM';
179
+ } };
180
+ for (let i = 0; pid && alive(); i++) {
181
+ if (i === 40) {
182
+ process.stderr.write(`Rho for ${directory} has closed its state, but its process ${pid} has not ended after 10 s.\n`);
183
+ process.exit(1);
184
+ }
185
+ await new Promise(resolve => setTimeout(resolve, 250));
186
+ }
187
+ process.stdout.write(JSON.stringify({ stopped: { state: directory } }) + '\n');
188
+ process.exit(0);
189
+ }
111
190
  // The Agent's side of a grant: its MCP server reads the connection file and speaks on stdio,
112
191
  // so nothing else may write to stdout in this mode.
113
192
  if (values.acp) {
@@ -126,8 +205,8 @@ else {
126
205
  // Access comes from whoever runs Rho: the operator's connection grants one project to one
127
206
  // Agent session, and the reply says how Claude Code starts that session's MCP server.
128
207
  if (values.grant) {
129
- if (!values.project) {
130
- process.stderr.write('--grant needs --project, the project folder relative to the service root.\n\n' + usage);
208
+ if (!!values.project === !!values['project-id']) {
209
+ process.stderr.write('--grant needs exactly one of --project (a folder relative to the service root) or --project-id (an existing project).\n\n' + usage);
131
210
  process.exit(2);
132
211
  }
133
212
  if (values.agent !== undefined && !['claude-code', 'codex'].includes(values.agent)) {
@@ -141,7 +220,13 @@ else {
141
220
  const { RhoClient } = await import('./client.js');
142
221
  const client = await RhoClient.fromFile(path.resolve(values.grant)).catch(error => refuse(error));
143
222
  try {
144
- const project = await client.call('project.open', { path: values.project });
223
+ const project = values['project-id']
224
+ ? (await client.call('project.list', {})).find(p => p.id === values['project-id'])
225
+ : await client.call('project.open', { path: values.project });
226
+ if (!project) {
227
+ process.stderr.write('Project is not registered or is outside this connection’s scope. Open the project in Rho and copy its setup prompt again.\n');
228
+ process.exit(1);
229
+ }
145
230
  // The same as the project page's button: the grant that Agent keeps, then its own
146
231
  // `mcp add`, recorded so the page shows it under that Agent.
147
232
  if (values.agent) {
@@ -149,13 +234,8 @@ else {
149
234
  process.stdout.write(JSON.stringify({ projectId: project.id, root: project.root, agent: values.agent, methods: connected.methods, granted: connected.granted, added: connected.added }) + '\n');
150
235
  process.exit(0);
151
236
  }
152
- const 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');
237
+ const { grantReply } = await import('./connect-cli.js');
238
+ process.stdout.write(JSON.stringify(await grantReply(client, project, values.session ?? 'claude-code')) + '\n');
159
239
  process.exit(0);
160
240
  }
161
241
  catch (error) {
@@ -203,7 +283,10 @@ else {
203
283
  const agentBackend = values['agent-backend'] ?? process.env.RHO_AGENT_BACKEND ?? 'claude-acp';
204
284
  if (!['claude-acp', 'codex-acp', 'claude-cli', 'acp'].includes(agentBackend))
205
285
  throw new Error('Choose --agent-backend claude-acp, codex-acp, claude-cli or acp.');
206
- const acpCommand = agentBackend === 'acp' ? parseAcpCommand(values['acp-command'] ?? process.env.RHO_ACP_COMMAND) : undefined;
286
+ // An ACP command named here, or else DeepSeek Harness's own ACP profile when it is installed, so
287
+ // sessions started in Harness continue in Rho too. Naming one does not make it the default Agent.
288
+ const named = values['acp-command'] ?? process.env.RHO_ACP_COMMAND;
289
+ const acpCommand = agentBackend === 'acp' || named ? parseAcpCommand(named) : await harnessAcp();
207
290
  if (values.port !== undefined && !(/^\d+$/.test(values.port) && Number(values.port) <= 65535)) {
208
291
  process.stderr.write('Choose --port 0 to 65535, or leave it out.\n');
209
292
  process.exit(2);
@@ -221,6 +304,11 @@ else {
221
304
  const running = typeof held === 'string' ? await fs.readFile(path.join(held, 'connection.json'), 'utf8').then(text => String(JSON.parse(text).url), () => '') : '';
222
305
  return refuse(running ? Object.assign(error, { message: `Rho is already running for ${held} at ${running}. Open that url, or stop that service before starting it again.` }) : error);
223
306
  });
307
+ // A request left by a --stop that this state's earlier service never took is not for this start.
308
+ await fs.rm(stopRequest(service.state.directory), { force: true });
309
+ // Where `rho connect` finds this Rho without being given its state folder.
310
+ const { announce, withdraw } = await import('./running.js');
311
+ await announce({ stateDirectory: service.state.directory, url: service.url, pid: process.pid }).catch(() => { });
224
312
  // The narrow conclusion a person needs on one line: what was selected, where state
225
313
  // lives, and how to open it. It stays inside `ready` so a parser reading line one is
226
314
  // unaffected. Opening this url on this machine is the access; no link is minted.
@@ -252,9 +340,11 @@ else {
252
340
  const claudePath = values.claude ?? process.env.RHO_CLAUDE, codexPath = values.codex ?? process.env.RHO_CODEX;
253
341
  const env = { ...process.env, ...(claudePath ? { CLAUDE_CODE_EXECUTABLE: claudePath } : {}), ...(codexPath ? { CODEX_PATH: codexPath } : {}) };
254
342
  // An ACP agent named with --acp-command is the operator's own command; it is named, not checked.
255
- if (acpCommand)
343
+ if (acpCommand && agentBackend === 'acp')
256
344
  say('Agent: ' + acpCommand.file + ', named with --acp-command.');
257
345
  else {
346
+ if (acpCommand)
347
+ say('Also: ' + (acpCommand.args[0] === 'acp' && /dsh(\.[a-z]+)?$/i.test(acpCommand.file) ? 'DeepSeek Harness (' + acpCommand.file + ' acp)' : acpCommand.file) + ', so its sessions can continue here.');
258
348
  const missing = agentCommand ? agentCommand.missingClient(agentBackend === 'codex-acp' ? 'codex' : 'claude', env) : null;
259
349
  say(missing
260
350
  ? 'Agent: ' + missing + ' Until then folders can be opened and files read here; the Agent cannot run in them.'
@@ -271,11 +361,16 @@ else {
271
361
  }
272
362
  if (values.open)
273
363
  openBrowser(service.url);
274
- let closing = false;
364
+ let closing = false, asked;
365
+ const stop = async () => { if (closing)
366
+ return; closing = true; clearInterval(asked); await withdraw(service.state.directory).catch(() => { }); const reports = await service.close(); process.stdout.write(JSON.stringify({ shutdown: reports }) + '\n'); };
275
367
  // Windows has no SIGTERM another process can send, and a console may pass on Ctrl+C turned off;
276
368
  // there Ctrl+Break on Rho's console (SIGBREAK) stops it in order too.
277
369
  for (const signal of ['SIGTERM', 'SIGINT', ...(process.platform === 'win32' ? ['SIGBREAK'] : [])])
278
- process.on(signal, async () => { if (closing)
279
- 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();
280
375
  }
281
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;