@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
@@ -0,0 +1,166 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { Ajv } from 'ajv';
5
+ import { atomicJson, Fault, requireThat } from '@rho-workspace/plugin-contracts';
6
+ export const marketLimits = { maxMarkets: 16, maxMarketBytes: 262144, maxMarketEntries: 128, marketTimeoutMs: 10000 };
7
+ const text = (maxLength) => ({ type: 'string', minLength: 1, maxLength });
8
+ const object = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false });
9
+ export const marketEntrySchema = object({
10
+ id: { ...text(96), pattern: '^[a-z0-9][a-z0-9._-]{0,95}$' }, title: text(120),
11
+ version: { ...text(64), pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$' },
12
+ description: text(2048), source: text(1024), repository: text(2048), needs: text(2048),
13
+ keywords: { type: 'array', items: text(64), maxItems: 16, uniqueItems: true },
14
+ }, ['id', 'title', 'version', 'description', 'source', 'repository', 'needs']);
15
+ export const marketIndexSchema = object({ apiVersion: { const: 1 }, title: text(120), plugins: { type: 'array', items: marketEntrySchema } });
16
+ const hash = (value) => createHash('sha256').update(value).digest('hex');
17
+ const validateIndex = new Ajv({ strict: false }).compile(marketIndexSchema);
18
+ function https(value) {
19
+ let url;
20
+ try {
21
+ url = new URL(value);
22
+ }
23
+ catch {
24
+ throw new Fault('invalid', 'Use an absolute HTTPS URL.');
25
+ }
26
+ requireThat(url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash, 'invalid', 'Use HTTPS without credentials, a query or a fragment.');
27
+ return url;
28
+ }
29
+ function indexUrl(value) {
30
+ let url;
31
+ try {
32
+ url = new URL(value);
33
+ }
34
+ catch {
35
+ throw new Fault('invalid', 'Use an HTTPS market index URL.');
36
+ }
37
+ const local = url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
38
+ requireThat((url.protocol === 'https:' || local) && !url.username && !url.password && !url.search && !url.hash, 'invalid', 'Market indexes need HTTPS without credentials, a query or a fragment. HTTP is allowed only on localhost.');
39
+ return url.href;
40
+ }
41
+ function validateSource(entry) {
42
+ if (entry.source.startsWith('git+https://')) {
43
+ const match = /^git\+(https:\/\/[^#]+)#([a-f0-9]{40})(?::([^:]+))?$/.exec(entry.source);
44
+ requireThat(match, 'invalid', 'Market Git packages must select a full 40-character commit and optional package path.');
45
+ https(match[1]);
46
+ requireThat(!match[3] || (!match[3].includes('\\') && match[3].split('/').every(p => p !== '' && p !== '.' && p !== '..')), 'invalid', 'Market package paths must stay inside the Git repository.');
47
+ }
48
+ else {
49
+ const match = /^((?:@[a-z0-9._-]+\/)?[a-z0-9._-]+)@(.+)$/.exec(entry.source);
50
+ requireThat(match && match[2] === entry.version, 'invalid', 'Market sources need a pinned Git commit or an npm package with the exact declared version.');
51
+ }
52
+ https(entry.repository);
53
+ }
54
+ // A configured index is a retained observation. Listing does not fetch it, and
55
+ // its package sources are only used after a separate, local install action.
56
+ export class ExtensionMarkets {
57
+ directory;
58
+ limits;
59
+ values = [];
60
+ constructor(directory, limits) {
61
+ this.directory = directory;
62
+ this.limits = limits;
63
+ }
64
+ async init() {
65
+ try {
66
+ this.values = JSON.parse(await fs.readFile(path.join(this.directory, 'markets.json'), 'utf8'));
67
+ }
68
+ catch (error) {
69
+ if (error.code !== 'ENOENT')
70
+ throw error;
71
+ }
72
+ }
73
+ list() { return structuredClone(this.values); }
74
+ async save(values) {
75
+ await atomicJson(path.join(this.directory, 'markets.json'), values);
76
+ this.values = values;
77
+ }
78
+ async fetch(url) {
79
+ const response = await fetch(url, { redirect: 'error', headers: { accept: 'application/json' }, signal: AbortSignal.timeout(this.limits.marketTimeoutMs) });
80
+ if (!response.ok || !response.body) {
81
+ await response.body?.cancel();
82
+ throw new Fault('unavailable', `Market index returned HTTP ${response.status}.`);
83
+ }
84
+ const reader = response.body.getReader();
85
+ const chunks = [];
86
+ let bytes = 0;
87
+ try {
88
+ requireThat(Number(response.headers.get('content-length') ?? 0) <= this.limits.maxMarketBytes, 'limit', `Market index exceeds maxMarketBytes (${this.limits.maxMarketBytes}).`);
89
+ for (;;) {
90
+ const chunk = await reader.read();
91
+ if (chunk.done)
92
+ break;
93
+ bytes += chunk.value.byteLength;
94
+ requireThat(bytes <= this.limits.maxMarketBytes, 'limit', `Market index exceeds maxMarketBytes (${this.limits.maxMarketBytes}).`);
95
+ chunks.push(chunk.value);
96
+ }
97
+ }
98
+ finally {
99
+ await reader.cancel().catch(() => { });
100
+ }
101
+ const raw = Buffer.concat(chunks);
102
+ let index;
103
+ try {
104
+ index = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(raw));
105
+ }
106
+ catch {
107
+ throw new Fault('invalid', 'Market index is not valid UTF-8 JSON.');
108
+ }
109
+ requireThat(validateIndex(index), 'invalid', 'Expected a Rho market index with apiVersion 1, title and plugin entries.');
110
+ requireThat(index.plugins.length <= this.limits.maxMarketEntries, 'limit', `Market index exceeds maxMarketEntries (${this.limits.maxMarketEntries}).`);
111
+ const seen = new Set();
112
+ for (const entry of index.plugins) {
113
+ requireThat(!seen.has(entry.id), 'invalid', 'A market index must contain one selected version per plugin id.');
114
+ seen.add(entry.id);
115
+ validateSource(entry);
116
+ }
117
+ return { title: index.title, plugins: index.plugins, digest: hash(raw) };
118
+ }
119
+ async observe(url) {
120
+ try {
121
+ return await this.fetch(url);
122
+ }
123
+ catch (error) {
124
+ if (error instanceof Fault)
125
+ throw error;
126
+ const reason = error.name === 'TimeoutError' || error.name === 'AbortError' ? `Market request exceeded marketTimeoutMs (${this.limits.marketTimeoutMs}).` : 'Could not read the market index. Check its URL and network connection; redirects are not followed.';
127
+ throw new Fault('unavailable', reason);
128
+ }
129
+ }
130
+ async change(input) {
131
+ if (input.action === 'add') {
132
+ const url = indexUrl(input.url), id = hash(url).slice(0, 24);
133
+ if (this.values.some(m => m.id === id))
134
+ return this.list();
135
+ requireThat(this.values.length < this.limits.maxMarkets, 'limit', `The maxMarkets limit (${this.limits.maxMarkets}) is reached. Existing indexes are retained.`);
136
+ const index = await this.observe(url), at = new Date().toISOString();
137
+ await this.save([...this.values, { id, url, ...index, revision: 1, fetchedAt: at, checkedAt: at, error: null }]);
138
+ }
139
+ else {
140
+ const current = this.values.find(m => m.id === input.id);
141
+ requireThat(current, 'not_found', 'Market source is no longer configured.');
142
+ requireThat(current.revision === input.expectedRevision, 'stale', 'Market source changed. Refresh the list before changing it.');
143
+ if (input.action === 'remove')
144
+ await this.save(this.values.filter(m => m.id !== input.id));
145
+ else {
146
+ const at = new Date().toISOString();
147
+ let next;
148
+ try {
149
+ next = { ...current, ...await this.observe(current.url), revision: current.revision + 1, fetchedAt: at, checkedAt: at, error: null };
150
+ }
151
+ catch (error) {
152
+ next = { ...current, revision: current.revision + 1, checkedAt: at, error: String(error.message).slice(0, 400) };
153
+ }
154
+ await this.save(this.values.map(m => m.id === input.id ? next : m));
155
+ }
156
+ }
157
+ return this.list();
158
+ }
159
+ select(selection, source) {
160
+ const market = this.values.find(m => m.id === selection.id);
161
+ requireThat(market && market.digest === selection.expectedDigest, 'stale', 'Market index changed or was removed. Refresh the list before installing.');
162
+ const entry = market.plugins.find(p => p.id === selection.pluginId);
163
+ requireThat(entry && entry.source === source, 'conflict', 'Selected package source does not match the retained market index.');
164
+ return { entry: structuredClone(entry), origin: { id: market.id, title: market.title, url: market.url, digest: market.digest, keywords: [...(entry.keywords ?? [])] } };
165
+ }
166
+ }
@@ -1,3 +1,4 @@
1
+ import { ExtensionMarkets, type MarketSelection, type MarketChange } from './extension-markets.js';
1
2
  export declare const extensionLimits: {
2
3
  maxPlugins: number;
3
4
  maxVersions: number;
@@ -10,6 +11,10 @@ export declare const extensionLimits: {
10
11
  maxDelegations: number;
11
12
  timeoutMs: number;
12
13
  installTimeoutMs: number;
14
+ maxMarkets: number;
15
+ maxMarketBytes: number;
16
+ maxMarketEntries: number;
17
+ marketTimeoutMs: number;
13
18
  };
14
19
  export interface ExtensionManifest {
15
20
  apiVersion: 1;
@@ -45,6 +50,13 @@ export interface InstalledExtension {
45
50
  digest: string;
46
51
  source: string;
47
52
  sourceCommit?: string;
53
+ market?: {
54
+ id: string;
55
+ title: string;
56
+ url: string;
57
+ digest: string;
58
+ keywords?: string[];
59
+ };
48
60
  enabled: boolean;
49
61
  installedAt: string;
50
62
  manifest: ExtensionManifest;
@@ -73,6 +85,7 @@ interface Receipt {
73
85
  export declare class Extensions {
74
86
  readonly directory: string;
75
87
  readonly limits: typeof extensionLimits;
88
+ readonly markets: ExtensionMarkets;
76
89
  private installed;
77
90
  private running;
78
91
  private workers;
@@ -92,6 +105,7 @@ export declare class Extensions {
92
105
  repository: string;
93
106
  needs: string;
94
107
  }[];
108
+ markets: import("./extension-markets.js").ExtensionMarket[];
95
109
  plugins: {
96
110
  id: string;
97
111
  name: string;
@@ -101,6 +115,13 @@ export declare class Extensions {
101
115
  digest: string;
102
116
  source: string;
103
117
  sourceCommit?: string;
118
+ market?: {
119
+ id: string;
120
+ title: string;
121
+ url: string;
122
+ digest: string;
123
+ keywords?: string[];
124
+ };
104
125
  enabled: boolean;
105
126
  installedAt: string;
106
127
  }[];
@@ -116,8 +137,15 @@ export declare class Extensions {
116
137
  maxDelegations: number;
117
138
  timeoutMs: number;
118
139
  installTimeoutMs: number;
140
+ maxMarkets: number;
141
+ maxMarketBytes: number;
142
+ maxMarketEntries: number;
143
+ marketTimeoutMs: number;
119
144
  };
120
145
  };
146
+ changeMarket(input: MarketChange): Promise<{
147
+ markets: import("./extension-markets.js").ExtensionMarket[];
148
+ }>;
121
149
  describe(id: string): InstalledExtension;
122
150
  private selected;
123
151
  private root;
@@ -137,7 +165,7 @@ export declare class Extensions {
137
165
  title: string;
138
166
  text: string;
139
167
  }>;
140
- install(source: string, replace?: string, expected?: string): Promise<InstalledExtension>;
168
+ install(source: string, replace?: string, expected?: string, market?: MarketSelection): Promise<InstalledExtension>;
141
169
  manage(id: string, expected: string, action: 'enable' | 'disable' | 'remove'): Promise<{
142
170
  pluginId: string;
143
171
  action: "remove" | "enable" | "disable";
@@ -1,11 +1,12 @@
1
1
  import { extensionDirectory } from './extension-directory.js';
2
+ import { ExtensionMarkets, marketLimits } from './extension-markets.js';
2
3
  import * as fs from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import { createHash } from 'node:crypto';
5
6
  import { fork, spawn } from 'node:child_process';
6
7
  import { Ajv } from 'ajv';
7
8
  import { atomicJson, canonical, digest, requireThat } from '@rho-workspace/plugin-contracts';
8
- export const extensionLimits = { maxPlugins: 32, maxVersions: 128, maxFiles: 20000, maxPackageBytes: 128 * 1024 * 1024, maxContentBytes: 512 * 1024, maxReceipts: 2000, maxResultBytes: 1024 * 1024, maxRunning: 4, maxDelegations: 32, timeoutMs: 30000, installTimeoutMs: 180000 };
9
+ export const extensionLimits = { ...marketLimits, maxPlugins: 32, maxVersions: 128, maxFiles: 20000, maxPackageBytes: 128 * 1024 * 1024, maxContentBytes: 512 * 1024, maxReceipts: 2000, maxResultBytes: 1024 * 1024, maxRunning: 4, maxDelegations: 32, timeoutMs: 30000, installTimeoutMs: 180000 };
9
10
  const ajv = new Ajv({ strict: false, allErrors: true });
10
11
  const identifier = /^[a-z0-9][a-z0-9._-]{0,95}$/;
11
12
  const relativeFile = (v) => typeof v === 'string' && v.length < 512 && !path.isAbsolute(v) && !v.includes('\\') && v.split('/').every(p => p !== '..' && p !== '.' && p !== '');
@@ -24,6 +25,7 @@ async function command(file, args, cwd, timeout) {
24
25
  export class Extensions {
25
26
  directory;
26
27
  limits;
28
+ markets;
27
29
  installed = [];
28
30
  running = new Map();
29
31
  workers = new Set();
@@ -34,10 +36,12 @@ export class Extensions {
34
36
  this.limits = { ...extensionLimits, ...limits };
35
37
  for (const [key, value] of Object.entries(this.limits))
36
38
  requireThat(Number.isSafeInteger(value) && value > 0, 'invalid', `Invalid extensions.${key}.`);
39
+ this.markets = new ExtensionMarkets(directory, this.limits);
37
40
  }
38
41
  async init() {
39
42
  await fs.mkdir(path.join(this.directory, 'versions'), { recursive: true });
40
43
  await fs.mkdir(path.join(this.directory, 'receipts'), { recursive: true });
44
+ await this.markets.init();
41
45
  try {
42
46
  this.installed = JSON.parse(await fs.readFile(path.join(this.directory, 'installed.json'), 'utf8'));
43
47
  }
@@ -56,7 +60,8 @@ export class Extensions {
56
60
  close() { for (const child of this.workers)
57
61
  child.kill(); }
58
62
  serial(fn) { const next = this.mutation.then(fn, fn); this.mutation = next.catch(() => { }); return next; }
59
- list() { return { directory: extensionDirectory, plugins: this.installed.map(({ manifest, ...info }) => info), limits: this.limits }; }
63
+ list() { return { directory: extensionDirectory, markets: this.markets.list(), plugins: this.installed.map(({ manifest, ...info }) => info), limits: this.limits }; }
64
+ changeMarket(input) { return this.serial(async () => ({ markets: await this.markets.change(input) })); }
60
65
  describe(id) { const value = this.installed.find(p => p.id === id); requireThat(value, 'not_found', 'Plugin is not installed.'); return structuredClone(value); }
61
66
  selected(id, expectedDigest) { const value = this.describe(id); requireThat(value.digest === expectedDigest, 'stale', 'The selected plugin changed. Read its descriptor again.'); return value; }
62
67
  root(p) { return path.join(this.directory, 'versions', p.digest, 'node_modules', p.name); }
@@ -134,8 +139,9 @@ export class Extensions {
134
139
  requireThat(entry, 'not_found', 'Plugin entry not found.');
135
140
  return { pluginId: id, packageDigest: p.digest, entryId, kind, title: entry.title, text: await fs.readFile(await this.file(this.root(p), entry.file), 'utf8') };
136
141
  }
137
- install(source, replace, expected) {
142
+ install(source, replace, expected, market) {
138
143
  return this.serial(async () => {
144
+ const selection = market ? this.markets.select(market, source) : undefined;
139
145
  requireThat(!!replace === !!expected, 'invalid', 'An update needs both replace and expectedDigest.');
140
146
  if (replace)
141
147
  this.selected(replace, expected ?? '');
@@ -181,11 +187,14 @@ export class Extensions {
181
187
  const pkg = JSON.parse(await fs.readFile(path.join(install, 'package.json'), 'utf8')), names = Object.keys(pkg.dependencies ?? {});
182
188
  requireThat(names.length === 1, 'invalid', 'Install exactly one plugin package.');
183
189
  const checked = await this.check(path.join(install, 'node_modules', names[0]));
190
+ requireThat(!selection || (checked.manifest.id === selection.entry.id && checked.version === selection.entry.version), 'conflict', 'Package identity or version does not match the selected market entry. The installed selection was kept.');
184
191
  requireThat(!replace || checked.manifest.id === replace, 'conflict', 'An update must keep the plugin identity.');
185
192
  requireThat(replace || !this.installed.some(p => p.id === checked.manifest.id), 'conflict', 'Plugin is already installed. Use update with its current digest.');
186
193
  await fs.rm(path.join(install, 'node_modules', '.package-lock.json'), { force: true });
187
194
  const sha = await this.hash(path.join(install, 'node_modules'));
188
- const selected = { id: checked.manifest.id, name: checked.name, version: checked.version, title: checked.manifest.title, description: checked.manifest.description, digest: sha, source, ...(sourceCommit ? { sourceCommit } : {}), enabled: replace ? this.describe(replace).enabled : true, installedAt: new Date().toISOString(), manifest: checked.manifest };
195
+ const previous = replace ? this.describe(replace) : undefined;
196
+ const origin = selection?.origin ?? (previous?.source === source && previous.version === checked.version ? previous.market : undefined);
197
+ const selected = { id: checked.manifest.id, name: checked.name, version: checked.version, title: checked.manifest.title, description: checked.manifest.description, digest: sha, source, ...(sourceCommit ? { sourceCommit } : {}), ...(origin ? { market: origin } : {}), enabled: previous?.enabled ?? true, installedAt: new Date().toISOString(), manifest: checked.manifest };
189
198
  const destination = path.join(this.directory, 'versions', sha);
190
199
  if (!versions.includes(sha))
191
200
  await fs.rename(install, destination);
@@ -0,0 +1,22 @@
1
+ import type { RhoOutput } from './contract.generated.js';
2
+ import { type GraphTurn, type PathScope } from './work-graph.js';
3
+ type Summary = RhoOutput<'agent.sessions.list'>['sessions'][number];
4
+ type Owned = NonNullable<RhoOutput<'observation.snapshot'>['conversations']>[number];
5
+ type Read = RhoOutput<'agent.sessions.read'>;
6
+ export interface WorkSession {
7
+ id: string;
8
+ title: string;
9
+ agent: string;
10
+ state: string;
11
+ lastAt: string;
12
+ version: string;
13
+ owned?: Owned;
14
+ external?: Summary;
15
+ }
16
+ export declare const workAgent: (name: string) => string;
17
+ export declare function workSessions(owned: readonly Owned[], outside: readonly Summary[]): WorkSession[];
18
+ export declare function externalTurnAt(turn: Read['turns'][number], at: number | null): Read['turns'][number] | null;
19
+ export declare function externalWork(read: Read, project: PathScope & {
20
+ id: string;
21
+ }): GraphTurn[];
22
+ export {};
@@ -0,0 +1,91 @@
1
+ import { projectPath } from './work-graph.js';
2
+ export const workAgent = (name) => name === 'dsh' ? 'DeepSeek Harness' : name === 'codex' ? 'Codex' : name === 'claude-code' ? 'Claude Code' : name === 'acp' ? 'Agent' : name;
3
+ // A native session continued here is one session, not an outside copy beside a Rho copy.
4
+ // Trashed conversations do not reappear through their native records.
5
+ export function workSessions(owned, outside) {
6
+ return [...owned.filter(c => !c.trashedAt).map(c => ({ id: c.conversationId, title: c.agentTitle || c.title, agent: workAgent(c.client), state: c.state, lastAt: c.lastAt, version: String(c.version), owned: c })),
7
+ ...outside.filter(s => !s.conversationId).map(s => ({ id: s.id, title: s.title, agent: workAgent(s.producer), state: s.state, lastAt: s.lastAt, version: [s.sourceCursor, s.lastAt, s.state].join(':'), external: s }))]
8
+ .sort((a, b) => b.lastAt.localeCompare(a.lastAt));
9
+ }
10
+ const json = (text) => { try {
11
+ const v = JSON.parse(text);
12
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : null;
13
+ }
14
+ catch {
15
+ return null;
16
+ } };
17
+ const bytes = (text) => new TextEncoder().encode(text).length;
18
+ const rhoCall = (name) => name === 'kernel.run' || /^mcp__[\w.-]+__rho_call$/.test(name);
19
+ export function externalTurnAt(turn, at) {
20
+ if (at === null)
21
+ return turn;
22
+ if (Date.parse(turn.at) > at)
23
+ return null;
24
+ if (turn.endedAt && Date.parse(turn.endedAt) <= at)
25
+ return turn;
26
+ return { ...turn, state: 'running', endedAt: undefined, reply: '', results: [], messages: turn.messages.filter(m => Date.parse(m.at) <= at),
27
+ tools: turn.tools.filter(t => Date.parse(t.at) <= at).map(t => ({ ...t, output: '', exitCode: undefined, outcome: t.outcome === 'requested' ? 'requested' : 'unknown' })) };
28
+ }
29
+ // A bounded tool reply can end in the middle of a cell's large plot. Only its complete,
30
+ // identifying prefix is read in that case; no outcome or omitted output is filled in.
31
+ function cellOf(name, detail, output) {
32
+ if (!rhoCall(name) || typeof detail?.code !== 'string' || !['r', 'python'].includes(detail?.language))
33
+ return null;
34
+ const cell = json(output)?.value?.cell;
35
+ if (cell?.changeId === detail.changeId && /^k-[a-f0-9]+$/.test(cell?.cellId))
36
+ return cell.cellId;
37
+ const prefix = /^\s*\{\s*"apiVersion"\s*:\s*"2\.0"\s*,\s*"ok"\s*:\s*true\s*,\s*"value"\s*:\s*\{\s*"cell"\s*:\s*\{\s*"cellId"\s*:\s*"(k-[a-f0-9]+)"\s*,\s*"changeId"\s*:\s*("(?:[^"\\]|\\.)*")/.exec(output);
38
+ return prefix && JSON.parse(prefix[2]) === detail.changeId ? prefix[1] : null;
39
+ }
40
+ // Presentation only: original native session/turn/call identities stay intact. This is never
41
+ // persisted or submitted as a Rho conversation. No native action becomes a managed run.
42
+ export function externalWork(read, project) {
43
+ return read.turns.map(t => {
44
+ const cells = [];
45
+ const steps = t.tools.map(tool => {
46
+ const input = json(tool.detail), cell = cellOf(tool.name, input, tool.output);
47
+ if (cell)
48
+ cells.push(cell);
49
+ const fileTool = /^(?:read|read_file|read_image|view_image|write|write_file|edit|apply_patch)$/i.test(tool.name);
50
+ const candidate = input?.file_path ?? input?.path ?? (fileTool ? tool.paths[0] : null);
51
+ const location = typeof candidate === 'string' ? projectPath(candidate, project) : null;
52
+ const toolName = /^(?:read_image|read_file)$/i.test(tool.name) ? 'read' : tool.name === 'write_file' ? 'write' : tool.name;
53
+ return { kind: 'tool', id: tool.id, at: tool.at, tool: cell ? 'mcp__rho__rho_call' : toolName,
54
+ // Older native projections kept the call time but not the result time. Do not
55
+ // invent a duration or show that result before the cut which retained it.
56
+ outcomeAt: t.endedAt ?? read.session.observedAt,
57
+ ...(cell || tool.name === 'kernel.run' ? { operation: 'kernel.run' } : {}),
58
+ ...(fileTool ? { detail: location ?? '' } : { detail: typeof input?.command === 'string' ? input.command : tool.detail }),
59
+ ...(input?.purpose && typeof input.purpose === 'string' ? { purpose: input.purpose } : {}),
60
+ outcome: tool.outcome === 'failed' ? 'returned' : tool.outcome, ...(tool.outcome === 'failed' ? { failed: true } : {}),
61
+ ...(tool.exitCode !== undefined ? { exitCode: tool.exitCode } : {}),
62
+ ...(tool.output ? { output: { text: tool.output, bytes: bytes(tool.output), lines: tool.output.split('\n').length, cut: t.omittedChars > 0 } } : {}) };
63
+ });
64
+ for (const m of t.messages)
65
+ steps.push({ kind: 'text', at: m.at, text: m.text });
66
+ if (t.reply)
67
+ steps.push({ kind: 'text', at: t.endedAt ?? read.session.lastAt, text: t.reply });
68
+ steps.sort((a, b) => a.at.localeCompare(b.at));
69
+ const turn = { id: t.id, by: read.session.producer, text: t.prompt, sentAt: t.at, startedAt: t.at, endedAt: t.endedAt ?? null,
70
+ state: t.state, model: null, stop: null, retry: null, result: null, problem: null, steps, omitted: t.omittedTools + t.omittedChars,
71
+ // This observation did not watch the disk during the turn. Today's mtime is not a
72
+ // historical file-change observation.
73
+ files: { changed: [], omitted: 0, partial: false } };
74
+ const results = new Map();
75
+ for (const r of t.results) {
76
+ const path = projectPath(r.path, project);
77
+ if (!path)
78
+ continue;
79
+ const exact = r.projectId === project.id && /^[a-f0-9]{64}$/.test(r.id ?? '') && /^[a-f0-9]{64}$/.test(r.version ?? '');
80
+ results.set(path, { path, at: t.endedAt ?? read.session.observedAt, ...(exact ? { resource: { id: r.id, version: r.version } } : {}) });
81
+ }
82
+ // Links in the final answer are reported results, not observations of file writes.
83
+ for (const m of t.reply.matchAll(/!?\[[^\]\n]*\]\((?:<([^>]+)>|([^\s)]+))\)/g)) {
84
+ const path = projectPath(m[1] ?? m[2], project);
85
+ if (path && /\.[A-Za-z0-9]{1,8}$/.test(path) && !/^[a-z][a-z0-9+.-]*:/i.test(path) && !results.has(path))
86
+ results.set(path, { path, at: t.endedAt ?? read.session.observedAt });
87
+ }
88
+ return { conversationId: read.session.id, conversationTitle: read.session.title, agent: workAgent(read.session.producer), turn,
89
+ external: true, kernelCells: cells, reportedResults: [...results.values()].slice(0, 64) };
90
+ });
91
+ }
@@ -11,6 +11,10 @@ const help = `Independent Rho plugins (API 1)
11
11
  rho plugin check DIRECTORY
12
12
  rho plugin add SOURCE [--connection FILE]
13
13
  rho plugin list
14
+ rho plugin market list
15
+ rho plugin market add URL
16
+ rho plugin market refresh|remove ID
17
+ rho plugin market install ID PLUGIN_ID
14
18
  rho plugin update ID [--source SOURCE]
15
19
  rho plugin enable|disable|remove ID
16
20
  rho plugin dev DIRECTORY
@@ -26,7 +30,7 @@ Remove preserves project files, live sessions, receipts and retained package rev
26
30
  `;
27
31
  export async function pluginCli(args) {
28
32
  const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { connection: { type: 'string' }, id: { type: 'string' }, source: { type: 'string' }, project: { type: 'string' }, input: { type: 'string' }, 'change-id': { type: 'string' }, help: { type: 'boolean' } } });
29
- const [command, target, operation] = positionals;
33
+ const [command, target, operation, marketPlugin] = positionals;
30
34
  if (values.help || !command) {
31
35
  process.stdout.write(help);
32
36
  return;
@@ -44,7 +48,52 @@ export async function pluginCli(args) {
44
48
  'rho-plugin.json': JSON.stringify(manifest, null, 2) + '\n',
45
49
  'host.mjs': `export async function run(operation, input, context) {\n if (operation === 'greet') return { greeting: 'Hello, ' + input.name + '!' };\n throw new Error('Unknown operation');\n}\n`,
46
50
  'SKILL.md': `# ${id}\n\nRead extension.describe for the installed digest and operation schemas. Call greet with a name. This operation reads only. Use a stable changeId for native operations; observe extension.read after a lost reply.\n`,
47
- 'view.html': `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${id}</title><style>body{font:16px system-ui;margin:24px;color:#24312b}input,button{font:inherit;padding:8px}output{display:block;margin-top:16px}</style><h1>${id}</h1><form><label>Your name <input name="name" value="World" maxlength="120"></label> <button>Greet</button></form><output aria-live="polite"></output><script>document.querySelector('form').onsubmit=async e=>{e.preventDefault();const out=document.querySelector('output');try{const result=await rho.call('greet',{name:e.target.elements.name.value});out.textContent=result.value?.greeting||result.error}catch(error){out.textContent=error.message}};</script></html>\n`,
51
+ 'view.html': `<!doctype html>
52
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${id}</title>
53
+ <style>
54
+ :root { color-scheme: light; --ink: #202927; --muted: #68736f; --line: #e1e6e3; --well: #f3f5f5; --accent: #1e6b52; --accent-soft: #e5f1e9; --failed: #b13f38; --failed-soft: #fbeeed; }
55
+ * { box-sizing: border-box; }
56
+ body { margin: 0; padding: 20px 22px 28px; font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--ink); background: #fff; }
57
+ header { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; }
58
+ .mark { flex: none; width: 38px; height: 38px; border-radius: 10px; background: var(--accent-soft); color: var(--accent); display: grid; place-content: center; font-weight: 600; font-size: 17px; }
59
+ h1 { margin: 0; font-size: 17px; line-height: 1.3; font-weight: 650; }
60
+ header p { margin: 2px 0 0; color: var(--muted); font-size: 12.5px; }
61
+ form { max-width: 520px; padding: 16px 18px; border: 1px solid var(--line); border-radius: 10px; }
62
+ label { display: block; margin-bottom: 6px; font-size: 13px; font-weight: 500; }
63
+ .row { display: flex; gap: 8px; }
64
+ input, button { min-height: 34px; padding: 6px 12px; border: 1px solid #cbd3ce; border-radius: 7px; font: inherit; color: inherit; background: #fff; }
65
+ input { flex: 1; min-width: 0; }
66
+ button { flex: none; font-weight: 500; cursor: pointer; color: #fff; background: var(--accent); border-color: var(--accent); }
67
+ button:disabled { opacity: .55; cursor: default; }
68
+ output { display: block; margin-top: 14px; padding: 11px 14px; border-radius: 8px; background: var(--well); white-space: pre-wrap; overflow-wrap: anywhere; }
69
+ output:empty { display: none; }
70
+ output.failed { color: var(--failed); background: var(--failed-soft); }
71
+ .note { margin: 14px 0 0; font-size: 12px; color: var(--muted); }
72
+ code { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
73
+ </style></head>
74
+ <body>
75
+ <header><span class="mark" aria-hidden="true">${id.charAt(0).toUpperCase()}</span><div><h1>${id}</h1><p>An independent Rho plugin. This page calls its own <code>greet</code> operation through Rho.</p></div></header>
76
+ <form>
77
+ <label for="name">Your name</label>
78
+ <div class="row"><input id="name" name="name" value="World" maxlength="120" required><button>Greet</button></div>
79
+ <output aria-live="polite"></output>
80
+ <p class="note">The reply comes from this package's <code>host.mjs</code> on this machine. Replace this page with your own; keep scripts and styles inline.</p>
81
+ </form>
82
+ <script>
83
+ const form = document.querySelector('form'), out = document.querySelector('output'), button = form.querySelector('button');
84
+ form.onsubmit = async event => {
85
+ event.preventDefault();
86
+ button.disabled = true; out.className = ''; out.textContent = 'Greeting…';
87
+ try {
88
+ const receipt = await rho.call('greet', { name: form.elements.name.value });
89
+ if (receipt.state !== 'completed') throw new Error(receipt.error || receipt.state);
90
+ out.textContent = receipt.value.greeting;
91
+ } catch (error) { out.className = 'failed'; out.textContent = error.message; }
92
+ finally { button.disabled = false; }
93
+ };
94
+ </script>
95
+ </body></html>
96
+ `,
48
97
  'README.md': `# ${id}\n\nDevelop: \`rho plugin dev .\`\nCheck: \`rho plugin check .\`\nDistribute: \`npm pack --ignore-scripts\`, then share the tarball, Git package source or publish to your own npm namespace.\n\nSee Rho's docs/THIRD-PARTY-PLUGINS.md for the host and iframe APIs, permissions and contribution process.\n`,
49
98
  };
50
99
  for (const [file, text] of Object.entries(files))
@@ -76,7 +125,30 @@ export async function pluginCli(args) {
76
125
  return result.value;
77
126
  };
78
127
  let result;
79
- if (command === 'list')
128
+ if (command === 'market') {
129
+ const catalog = await client.call('extension.list', {});
130
+ if (target === 'list')
131
+ result = catalog.markets;
132
+ else if (target === 'add' && operation)
133
+ result = await client.call('extension.market.change', { action: 'add', url: operation });
134
+ else if (['refresh', 'remove', 'install'].includes(target) && operation) {
135
+ const market = catalog.markets.find(m => m.id === operation);
136
+ if (!market)
137
+ throw new Error('Market is not configured: ' + operation);
138
+ if (target === 'install') {
139
+ const entry = market.plugins.find(p => p.id === marketPlugin);
140
+ if (!entry)
141
+ throw new Error('Name a plugin id from this market.');
142
+ const previous = catalog.plugins.find(p => p.id === entry.id);
143
+ result = await install({ source: entry.source, market: { id: market.id, expectedDigest: market.digest, pluginId: entry.id }, ...(previous ? { replace: previous.id, expectedDigest: previous.digest } : {}) });
144
+ }
145
+ else
146
+ result = await client.call('extension.market.change', { action: target, id: market.id, expectedRevision: market.revision });
147
+ }
148
+ else
149
+ throw new Error('Use rho plugin market list|add URL|refresh ID|remove ID|install ID PLUGIN_ID.');
150
+ }
151
+ else if (command === 'list')
80
152
  result = await list();
81
153
  else if (command === 'add') {
82
154
  if (!target)
@@ -0,0 +1,56 @@
1
+ import type { PluginDescriptor } from '@rho-workspace/plugin-contracts';
2
+ export declare function packageFacts(packageName: string): Promise<{
3
+ version?: string;
4
+ cli: string[];
5
+ }>;
6
+ export declare function pluginListing(plugin: PluginDescriptor, facts?: {
7
+ cli?: string[];
8
+ }): {
9
+ mcp?: {} | undefined;
10
+ claudeCode?: {
11
+ skills?: {
12
+ skill: string;
13
+ name: string;
14
+ description: string;
15
+ }[];
16
+ hooks?: {
17
+ id?: string;
18
+ event: string;
19
+ matcher: string;
20
+ entry: string;
21
+ timeoutSeconds?: number;
22
+ }[];
23
+ } | undefined;
24
+ cli?: string[] | undefined;
25
+ operations: {
26
+ method: string;
27
+ version: "2.0";
28
+ effect: "native" | "file" | "document" | "read" | "metadata";
29
+ requires: string[];
30
+ presentation: import("@rho-workspace/plugin-contracts").EntryPresentation | undefined;
31
+ }[];
32
+ actions: {
33
+ id: string;
34
+ version: string;
35
+ requires: string[];
36
+ presentation: import("@rho-workspace/plugin-contracts").EntryPresentation | undefined;
37
+ }[];
38
+ views: {
39
+ id: string;
40
+ version: string;
41
+ ui: string;
42
+ bindings: Record<string, string>;
43
+ presentation: import("@rho-workspace/plugin-contracts").EntryPresentation | undefined;
44
+ }[];
45
+ skills: {
46
+ id: string;
47
+ version: string;
48
+ digest: string;
49
+ presentation?: import("@rho-workspace/plugin-contracts").EntryPresentation;
50
+ intent?: string;
51
+ requires?: string[];
52
+ example?: import("@rho-workspace/plugin-contracts").SkillExample;
53
+ failureRecipes?: import("@rho-workspace/plugin-contracts").SkillFailureRecipe[];
54
+ verify?: import("@rho-workspace/plugin-contracts").SkillVerification;
55
+ }[];
56
+ };
@@ -0,0 +1,41 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import path from 'node:path';
4
+ // What the installed package itself records, beside the owner's declaration: its
5
+ // exact version and the command-line entries npm links for it. Read, not declared,
6
+ // so a package that ships no commands lists none.
7
+ export async function packageFacts(packageName) {
8
+ try {
9
+ let directory = path.dirname(fileURLToPath(import.meta.resolve(packageName)));
10
+ for (let depth = 0; depth < 4; depth++, directory = path.dirname(directory)) {
11
+ try {
12
+ const pkg = JSON.parse(await fs.readFile(path.join(directory, 'package.json'), 'utf8'));
13
+ if (pkg.name !== packageName)
14
+ continue;
15
+ const bin = typeof pkg.bin === 'string' ? { [packageName.split('/').at(-1)]: pkg.bin } : pkg.bin ?? {};
16
+ return { version: typeof pkg.version === 'string' ? pkg.version : undefined, cli: Object.keys(bin).filter(name => /^[a-z0-9][a-z0-9._-]{0,63}$/i.test(name)).slice(0, 16) };
17
+ }
18
+ catch (e) {
19
+ if (e.code !== 'ENOENT')
20
+ throw e;
21
+ }
22
+ }
23
+ }
24
+ catch { }
25
+ return { cli: [] };
26
+ }
27
+ // One descriptive projection for the live catalog and the retained build copy.
28
+ // It contains no Skill text, executable module or grant. Reduced installations
29
+ // can still explain the entries of an owner that is not loaded.
30
+ export function pluginListing(plugin, facts = {}) {
31
+ const native = plugin.claudeCode;
32
+ return {
33
+ operations: plugin.operations.map(({ method, version, effect, requires, presentation }) => ({ method, version, effect, requires, presentation })),
34
+ actions: (plugin.actions ?? []).map(({ id, version, requires, presentation }) => ({ id, version, requires, presentation })),
35
+ views: (plugin.components ?? []).map(({ id, version, ui, bindings, presentation }) => ({ id, version, ui, bindings, presentation })),
36
+ skills: plugin.skills.map(({ content, ...skill }) => skill),
37
+ ...(facts.cli?.length ? { cli: facts.cli } : {}),
38
+ ...(native ? { claudeCode: native } : {}),
39
+ ...(plugin.agent?.mcp ? { mcp: plugin.agent.mcp } : {}),
40
+ };
41
+ }