@rho-workspace/rho 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-connect.d.ts +36 -2
- package/dist/agent-connect.js +64 -11
- package/dist/answer-board.js +3 -3
- package/dist/bridge.js +6 -1
- package/dist/browser-access.d.ts +1 -0
- package/dist/browser-access.js +5 -3
- package/dist/capabilities.d.ts +21 -5
- package/dist/capabilities.js +6 -4
- package/dist/cli.js +76 -11
- package/dist/composition.d.ts +1 -1
- package/dist/composition.js +13 -3
- package/dist/connect-cli.d.ts +50 -0
- package/dist/connect-cli.js +128 -0
- package/dist/contract.generated.d.ts +1191 -278
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +845 -47
- package/dist/conversation-presentation.d.ts +9 -2
- package/dist/conversation-presentation.js +3 -1
- package/dist/conversations.d.ts +154 -8
- package/dist/conversations.js +147 -24
- package/dist/discovery.js +2 -2
- package/dist/extension-contract.d.ts +19 -0
- package/dist/extension-contract.js +15 -6
- package/dist/extension-markets.d.ts +74 -0
- package/dist/extension-markets.js +166 -0
- package/dist/extensions.d.ts +29 -1
- package/dist/extensions.js +13 -4
- package/dist/external-work.d.ts +22 -0
- package/dist/external-work.js +91 -0
- package/dist/plugin-cli.js +75 -3
- package/dist/plugin-inventory.d.ts +56 -0
- package/dist/plugin-inventory.js +41 -0
- package/dist/plugin-inventory.json +1581 -102
- package/dist/plugin-presentation.d.ts +105 -2
- package/dist/plugin-presentation.js +59 -5
- package/dist/plugins.d.ts +14 -3
- package/dist/plugins.js +12 -6
- package/dist/project-notes.js +1 -1
- package/dist/protocol.d.ts +6 -0
- package/dist/protocol.js +30 -12
- package/dist/replay.js +4 -0
- package/dist/response-contracts.js +26 -11
- package/dist/running.d.ts +10 -0
- package/dist/running.js +45 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +71 -8
- package/dist/state.d.ts +8 -0
- package/dist/state.js +8 -0
- package/dist/studio-layout.js +3 -3
- package/dist/task-frame.d.ts +1 -1
- package/dist/timeline-events.d.ts +1 -0
- package/dist/timeline-events.js +8 -1
- package/dist/ui-context.d.ts +73 -0
- package/dist/ui-context.js +98 -0
- package/dist/ui-contract.d.ts +23 -0
- package/dist/web/.vite/manifest.json +133 -74
- package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
- package/dist/web/assets/choices-D8vGwsl2.js +1 -0
- package/dist/web/assets/{console-Trqc0swD.js → console-Dyuo1q4N.js} +4 -4
- package/dist/web/assets/{dist-pF902jHA.js → dist-CSiCyBml.js} +1 -1
- package/dist/web/assets/{dist-DpIHRk3B.js → dist-D9FLb9Hm.js} +1 -1
- package/dist/web/assets/{dist-eMx6b8N1.js → dist-FXbTBwML.js} +1 -1
- package/dist/web/assets/{dist-DZmszrjG.js → dist-l3DUhbw5.js} +1 -1
- package/dist/web/assets/{explore-in-BCHqjJ7B.js → explore-in-BfSFGRh2.js} +1 -1
- package/dist/web/assets/graph-BU2_o5uA.js +2 -0
- package/dist/web/assets/index-C16mXVvk.css +1 -0
- package/dist/web/assets/index-CWg9XYXx.js +952 -0
- package/dist/web/assets/markdown.reader-Ct2-e2Zt.png +0 -0
- package/dist/web/assets/{notes-meaning-DcVGUBBY.js → notes-meaning-T2m5zQfv.js} +1 -1
- package/dist/web/assets/overview-PzfLJCo-.js +6 -0
- package/dist/web/assets/preview-BguEdsza.js +1 -0
- package/dist/web/assets/previews-pWNik-2g.js +3 -0
- package/dist/web/assets/{review-DYP0aCXb.js → review-Dy_85lcH.js} +1 -1
- package/dist/web/assets/rho.editor-CCJeyHma.png +0 -0
- package/dist/web/assets/rho.flow-WkMj6fRS.png +0 -0
- package/dist/web/assets/rho.media-Cc6amodO.png +0 -0
- package/dist/web/assets/rho.notebook-6vsZKGDq.png +0 -0
- package/dist/web/assets/rho.object-BGoeXjte.png +0 -0
- package/dist/web/assets/rho.plots-yIiISCkb.png +0 -0
- package/dist/web/assets/rho.scatter-DwHxesCs.png +0 -0
- package/dist/web/assets/rho.servers-DKf_mnNq.png +0 -0
- package/dist/web/assets/rho.table-DWOGaWDa.png +0 -0
- package/dist/web/assets/rho.variables-CKcWczhQ.png +0 -0
- package/dist/web/assets/{skills-1ec0Z-jx.js → skills-Dyz7mBlc.js} +1 -1
- package/dist/web/assets/{skills-work-DNexV4OJ.js → skills-work-BEGqlCkx.js} +1 -1
- package/dist/web/assets/studio-process-D-ykbC4D.js +2 -0
- package/dist/web/assets/timeline-events-DKTho9fU.js +1 -0
- package/dist/web/assets/work-turns-DSabCogR.js +1 -0
- package/dist/web/assets/{workspace-COmCbGV3.js → workspace-DfjBL--r.js} +2 -2
- package/dist/web/composition.json +138 -83
- package/dist/web/index.html +3 -3
- package/dist/web/renderer.json +1 -1
- package/dist/web-assets.js +2 -2
- package/dist/work-graph.d.ts +11 -1
- package/dist/work-graph.js +16 -5
- package/dist/workspace.d.ts +2 -2
- package/dist/workspace.js +8 -7
- package/docs/FRONTEND.md +53 -11
- package/docs/HEADLESS.md +104 -11
- package/docs/THIRD-PARTY-PLUGINS.md +75 -3
- package/node_modules/@rho-workspace/plugin-agent/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +31 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +45 -10
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +19 -3
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +1 -1
- package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +1 -1
- package/node_modules/@rho-workspace/plugin-agent/package.json +1 -1
- package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
- package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
- package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
- package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
- package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
- package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
- package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
- package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
- package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
- package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
- package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
- package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
- package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
- package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
- package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
- package/node_modules/jose/package.json +1 -1
- package/package.json +6 -4
- package/protocol/index.json +91 -55
- package/protocol/operations/agent.sessions.list.json +13 -5
- package/protocol/operations/agent.sessions.read.json +13 -5
- package/protocol/operations/capability.list.json +382 -6
- package/protocol/operations/composition.apply.json +10 -2
- package/protocol/operations/composition.delete.json +845 -0
- package/protocol/operations/composition.read.json +10 -2
- package/protocol/operations/composition.undo.json +10 -2
- package/protocol/operations/conversation.agents.add.json +346 -0
- package/protocol/operations/conversation.agents.remove.json +340 -0
- package/protocol/operations/conversation.decide.json +196 -0
- package/protocol/operations/conversation.permission.json +196 -0
- package/protocol/operations/conversation.read.json +196 -0
- package/protocol/operations/conversation.send.json +265 -0
- package/protocol/operations/conversation.settings.check.json +213 -495
- package/protocol/operations/conversation.settings.read.json +211 -490
- package/protocol/operations/conversation.settings.update.json +235 -554
- package/protocol/operations/conversation.start.json +267 -5
- package/protocol/operations/conversation.stop.json +196 -0
- package/protocol/operations/extension.describe.json +39 -0
- package/protocol/operations/extension.install.json +62 -0
- package/protocol/operations/extension.list.json +164 -1
- package/protocol/operations/extension.market.change.json +210 -0
- package/protocol/operations/figure.read.json +265 -0
- package/protocol/operations/figure.select.json +157 -0
- package/protocol/operations/kernel.interrupt.json +119 -0
- package/protocol/operations/kernel.read.json +119 -0
- package/protocol/operations/kernel.run.json +119 -0
- package/protocol/operations/observation.snapshot.json +416 -8
- package/protocol/operations/project.openFile.json +10 -2
- package/protocol/operations/session.grant.json +4 -0
- package/protocol/operations/shared.json +1 -0
- package/protocol/operations/view.close.json +10 -2
- package/protocol/operations/view.protection.set.json +10 -2
- package/vendor/composition.json +177 -112
- package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
- package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
- package/web/theme.css +5 -0
- package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
- package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
- package/dist/web/assets/choices-D-YfnFP6.js +0 -1
- package/dist/web/assets/graph-lENgfQNH.js +0 -2
- package/dist/web/assets/index-B3_OVy91.css +0 -1
- package/dist/web/assets/index-BpL_Kl_s.js +0 -892
- package/dist/web/assets/overview-CDnVKlOt.js +0 -6
- package/dist/web/assets/preview-BEC1E9MK.js +0 -1
- package/dist/web/assets/studio-process-EYQgikxq.js +0 -2
- package/dist/web/assets/timeline-events-Chj2Gbx7.js +0 -1
- package/dist/web/assets/work-turns-CT9RVcWY.js +0 -1
- package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
- 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
|
+
}
|
package/dist/extensions.d.ts
CHANGED
|
@@ -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";
|
package/dist/extensions.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/dist/plugin-cli.js
CHANGED
|
@@ -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
|
|
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 === '
|
|
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
|
+
}
|