@rho-workspace/rho 0.5.4 → 0.5.6
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 +8 -0
- package/dist/agent-connect.js +15 -0
- package/dist/browser-access.js +3 -2
- package/dist/capabilities.d.ts +2 -2
- package/dist/capabilities.js +6 -1
- package/dist/cli.js +219 -176
- package/dist/contract.generated.d.ts +472 -4
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +455 -6
- package/dist/conversations.d.ts +157 -4
- package/dist/conversations.js +69 -6
- package/dist/discovery.d.ts +1 -0
- package/dist/discovery.js +3 -3
- package/dist/extension-contract.d.ts +99 -0
- package/dist/extension-contract.js +52 -0
- package/dist/extension-directory.d.ts +9 -0
- package/dist/extension-directory.js +11 -0
- package/dist/extension-worker.d.ts +1 -0
- package/dist/extension-worker.js +36 -0
- package/dist/extensions.d.ts +157 -0
- package/dist/extensions.js +349 -0
- package/dist/plugin-cli.d.ts +1 -0
- package/dist/plugin-cli.js +139 -0
- package/dist/plugin-inventory.json +18 -0
- package/dist/plugins.d.ts +3 -3
- package/dist/project-work.d.ts +3 -1
- package/dist/project-work.js +15 -2
- package/dist/protocol.d.ts +8 -0
- package/dist/protocol.js +16 -7
- package/dist/server.d.ts +5 -10
- package/dist/server.js +122 -12
- package/dist/web/.vite/manifest.json +67 -67
- package/dist/web/assets/{canvas-BXhC8RDZ.js → canvas-D8rDdfDz.js} +1 -1
- package/dist/web/assets/choices-D-YfnFP6.js +1 -0
- package/dist/web/assets/{console-Dbdo-5oO.js → console-Trqc0swD.js} +1 -1
- package/dist/web/assets/{dist-B2V0VDO0.js → dist-DZmszrjG.js} +2 -2
- package/dist/web/assets/dist-DpIHRk3B.js +1 -0
- package/dist/web/assets/{dist-D25Vn17i.js → dist-eMx6b8N1.js} +1 -1
- package/dist/web/assets/dist-pF902jHA.js +3 -0
- package/dist/web/assets/explore-in-BCHqjJ7B.js +1 -0
- package/dist/web/assets/graph-lENgfQNH.js +2 -0
- package/dist/web/assets/index-B3_OVy91.css +1 -0
- package/dist/web/assets/{index-BHQIaDsx.js → index-BpL_Kl_s.js} +87 -86
- package/dist/web/assets/{notes-meaning-DdPbtNzJ.js → notes-meaning-DcVGUBBY.js} +1 -1
- package/dist/web/assets/overview-CDnVKlOt.js +6 -0
- package/dist/web/assets/preview-BEC1E9MK.js +1 -0
- package/dist/web/assets/{review-CmWou4Iv.js → review-DYP0aCXb.js} +1 -1
- package/dist/web/assets/{skills-Dy7n3L9d.js → skills-1ec0Z-jx.js} +1 -1
- package/dist/web/assets/{skills-work-Cj0W6b-I.js → skills-work-DNexV4OJ.js} +1 -1
- package/dist/web/assets/{studio-process-QOY_n2_8.js → studio-process-EYQgikxq.js} +2 -2
- package/dist/web/assets/{timeline-events-DmX1BCh3.js → timeline-events-Chj2Gbx7.js} +1 -1
- package/dist/web/assets/{work-turns-Co0C71rJ.js → work-turns-CT9RVcWY.js} +1 -1
- package/dist/web/assets/workspace-COmCbGV3.js +5 -0
- package/dist/web/composition.json +65 -65
- package/dist/web/index.html +3 -3
- package/dist/web/renderer.json +1 -1
- package/dist/web-assets.d.ts +2 -1
- package/dist/web-assets.js +3 -2
- package/dist/work-contract.js +2 -2
- package/docs/FRONTEND.md +26 -18
- package/docs/HEADLESS.md +38 -28
- package/docs/PLUGIN-AUTHORING.md +4 -0
- package/docs/THIRD-PARTY-PLUGINS.md +190 -0
- package/node_modules/@rho-workspace/plugin-agent/README.md +68 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +9 -1
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +10 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.js +2 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.d.ts +6 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.js +174 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.d.ts +82 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +20 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +84 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.js +190 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +373 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.js +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/operations.d.ts +33 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/operations.js +3 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/presentation.d.ts +14 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/presentation.js +2 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.d.ts +15 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.js +37 -0
- package/node_modules/@rho-workspace/plugin-agent/package.json +6 -1
- package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
- package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
- package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
- package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
- package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
- package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
- package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
- package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
- package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
- package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
- package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
- package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
- package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
- package/node_modules/hono/dist/cjs/utils/url.js +1 -1
- package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
- package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
- package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
- package/node_modules/hono/dist/jsx/base.js +49 -32
- package/node_modules/hono/dist/jsx/components.js +2 -2
- package/node_modules/hono/dist/jsx/constants.js +2 -1
- package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
- package/node_modules/hono/dist/middleware/etag/index.js +3 -1
- package/node_modules/hono/dist/middleware/language/language.js +1 -1
- package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
- package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
- package/node_modules/hono/dist/utils/cookie.js +2 -2
- package/node_modules/hono/dist/utils/mime.js +1 -1
- package/node_modules/hono/dist/utils/url.js +1 -1
- package/node_modules/hono/package.json +1 -1
- package/package.json +5 -3
- package/protocol/index.json +93 -33
- package/protocol/operations/agent.sessions.list.json +146 -0
- package/protocol/operations/agent.sessions.read.json +315 -0
- package/protocol/operations/conversation.configure.json +29 -0
- package/protocol/operations/conversation.decide.json +29 -0
- package/protocol/operations/conversation.list.json +29 -0
- package/protocol/operations/conversation.permission.json +29 -0
- package/protocol/operations/conversation.read.json +29 -0
- package/protocol/operations/conversation.restore.json +29 -0
- package/protocol/operations/conversation.send.json +29 -0
- package/protocol/operations/conversation.settings.check.json +92 -0
- package/protocol/operations/conversation.settings.read.json +92 -0
- package/protocol/operations/conversation.settings.update.json +92 -0
- package/protocol/operations/conversation.start.json +46 -1
- package/protocol/operations/conversation.stop.json +29 -0
- package/protocol/operations/conversation.trash.json +29 -0
- package/protocol/operations/extension.call.json +132 -0
- package/protocol/operations/extension.content.json +78 -0
- package/protocol/operations/extension.describe.json +195 -0
- package/protocol/operations/extension.install.json +198 -0
- package/protocol/operations/extension.list.json +123 -0
- package/protocol/operations/extension.manage.json +55 -0
- package/protocol/operations/extension.read.json +112 -0
- package/protocol/operations/observation.snapshot.json +29 -0
- package/protocol/operations/project.work.read.json +3 -2
- package/protocol/operations/session.grant.json +9 -0
- package/protocol/operations/session.instructions.json +43 -0
- package/scripts/third-party-acceptance.mjs +77 -0
- package/vendor/composition.json +83 -83
- package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
- package/web/theme.css +12 -0
- package/dist/web/assets/choices-DU9kHlyQ.js +0 -1
- package/dist/web/assets/dist-BV3hWfn_.js +0 -3
- package/dist/web/assets/dist-CLNF2g-E.js +0 -1
- package/dist/web/assets/explore-in-8Q5ftt-E.js +0 -1
- package/dist/web/assets/graph-hS2kvHTI.js +0 -2
- package/dist/web/assets/index-Cj1_iqyK.css +0 -1
- package/dist/web/assets/overview-BV6ZO5TO.js +0 -6
- package/dist/web/assets/preview-n5lOOdjN.js +0 -1
- package/dist/web/assets/workspace-BKFefRfJ.js +0 -5
- package/vendor/rho-workspace-plugin-agent-0.4.28.tgz +0 -0
package/dist/agent-connect.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ export declare const connectLimits: {
|
|
|
12
12
|
};
|
|
13
13
|
export declare function mcpName(root: string): string;
|
|
14
14
|
export declare function mcpCommand(connectionFile: string): string[];
|
|
15
|
+
export declare function connectionInstructions(project: {
|
|
16
|
+
id: string;
|
|
17
|
+
root: string;
|
|
18
|
+
}, stateDirectory: string): {
|
|
19
|
+
request: string;
|
|
20
|
+
prompt: string;
|
|
21
|
+
command: string[];
|
|
22
|
+
};
|
|
15
23
|
export declare function addToAgent(agent: OutsideAgent, binary: string, root: string, name: string, command: string[], limits?: {
|
|
16
24
|
timeoutMs: number;
|
|
17
25
|
outputBytes: number;
|
package/dist/agent-connect.js
CHANGED
|
@@ -13,6 +13,21 @@ export function mcpName(root) {
|
|
|
13
13
|
export function mcpCommand(connectionFile) {
|
|
14
14
|
return [process.execPath, fileURLToPath(new URL('./cli.js', import.meta.url)), '--mcp', connectionFile];
|
|
15
15
|
}
|
|
16
|
+
// Paths are data in an argv array, never an interpolated shell command. Reading these
|
|
17
|
+
// instructions starts nothing, grants no access and includes no credential bytes.
|
|
18
|
+
export function connectionInstructions(project, stateDirectory) {
|
|
19
|
+
const command = [process.execPath, fileURLToPath(new URL('./cli.js', import.meta.url)), '--grant', path.join(stateDirectory, 'connection.json'), '--project-id', project.id];
|
|
20
|
+
const request = `Connect yourself to my existing Rho project ${JSON.stringify(path.basename(project.root))} so we can work on it together.`;
|
|
21
|
+
const prompt = [request, '',
|
|
22
|
+
`Project folder: ${JSON.stringify(project.root)}. Project ID: ${project.id}. Rho is already running on this machine.`,
|
|
23
|
+
'Configure your own client using its supported MCP setup mechanism; inspect your own help or documentation if needed. Keep unrelated configuration, models and credentials intact. Use private local configuration, scoped to this project where supported, and keep connection settings out of version-controlled files. If you cannot run a local stdio MCP server, explain that limitation.',
|
|
24
|
+
'Run this program and argument array, appending --session and a stable name identifying your own Agent (1–128 characters from A–Z, a–z, 0–9, dot, underscore, colon, @ or hyphen). Reuse that name when retrying:',
|
|
25
|
+
JSON.stringify(command),
|
|
26
|
+
'Parse the JSON reply. Register a stdio MCP server using mcp.name, mcp.command and mcp.args exactly as returned. The connection files contain credentials: do not print their contents or put them in the project. Do not start another Rho service.',
|
|
27
|
+
'Reload your MCP tools if supported, or tell me to open a new session in the project folder. Then call rho_index through your configured client tools and verify scopeHint.projectId matches the project ID above. A direct protocol probe is useful diagnostics but does not prove your client loaded its tools. Report configuration and a successful tool call separately; do not claim connected before the call succeeds. If configuration or verification fails, report the actual error and continue from the same connection.',
|
|
28
|
+
].join('\n');
|
|
29
|
+
return { request, prompt, command };
|
|
30
|
+
}
|
|
16
31
|
function run(binary, args, cwd, limits) {
|
|
17
32
|
return new Promise((resolve, reject) => {
|
|
18
33
|
const child = spawn(binary, args, { cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
package/dist/browser-access.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { extensionMethods } from './extension-contract.js';
|
|
1
2
|
import * as fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { randomBytes } from 'node:crypto';
|
|
@@ -36,7 +37,7 @@ export const browserMethods = ['protocol.describe', 'protocol.validate', 'projec
|
|
|
36
37
|
// Only the person at this machine talks to a conversation Rho starts: it runs their own Claude
|
|
37
38
|
// Code with their model credentials. Only they add Rho to their own Agent's configuration. A
|
|
38
39
|
// narrower, expiring link never receives these.
|
|
39
|
-
export const conversationMethods = ['session.connect', 'conversation.start', 'conversation.send', 'conversation.stop', 'conversation.decide', 'conversation.read', 'conversation.list', 'conversation.trash', 'conversation.restore', 'conversation.delete', 'conversation.permission', 'conversation.configure', 'conversation.settings.read', 'conversation.settings.update', 'conversation.settings.check', 'project.work.read', 'evidence.preview', 'evidence.export'];
|
|
40
|
+
export const conversationMethods = ['agent.sessions.list', 'agent.sessions.read', 'session.instructions', 'session.connect', 'conversation.start', 'conversation.send', 'conversation.stop', 'conversation.decide', 'conversation.read', 'conversation.list', 'conversation.trash', 'conversation.restore', 'conversation.delete', 'conversation.permission', 'conversation.configure', 'conversation.settings.read', 'conversation.settings.update', 'conversation.settings.check', 'project.work.read', 'evidence.preview', 'evidence.export'];
|
|
40
41
|
// The person at this machine. A tab this service served is authorized by the request itself:
|
|
41
42
|
// same origin, loopback only, no link, no code and no expiry. `--pair` stays an explicit,
|
|
42
43
|
// narrower grant for a shared or subfolder-scoped tab.
|
|
@@ -49,7 +50,7 @@ export const skillsMethods = ['skills.list', 'skills.read', 'skills.change', 'sk
|
|
|
49
50
|
export function localBrowserPrincipal(root) {
|
|
50
51
|
// The empty token hash can never match a request digest, so no header impersonates this
|
|
51
52
|
// identity: it is chosen because the request carried no credential at all.
|
|
52
|
-
return { id: localBrowserId, kind: 'browser', tokenHash: '', projectRoot: root, methods: [...browserMethods, ...conversationMethods, ...consoleMethods, ...kernelMethods, ...skillsMethods] };
|
|
53
|
+
return { id: localBrowserId, kind: 'browser', tokenHash: '', projectRoot: root, methods: [...extensionMethods, ...browserMethods, ...conversationMethods, ...consoleMethods, ...kernelMethods, ...skillsMethods] };
|
|
53
54
|
}
|
|
54
55
|
export class BrowserAccess {
|
|
55
56
|
state;
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -157,7 +157,7 @@ export declare class Capabilities {
|
|
|
157
157
|
enabled: string[];
|
|
158
158
|
}, author: string): Promise<{
|
|
159
159
|
version: number;
|
|
160
|
-
enabled: ("media" | "r" | "agent" | "files" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel"
|
|
160
|
+
enabled: ("media" | "r" | "agent" | "files" | "skills" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel")[];
|
|
161
161
|
receipt: CapabilityReceipt;
|
|
162
162
|
}>;
|
|
163
163
|
private pluginStatus;
|
|
@@ -192,7 +192,7 @@ export declare class Capabilities {
|
|
|
192
192
|
}[];
|
|
193
193
|
configuration: {
|
|
194
194
|
version: number;
|
|
195
|
-
enabled: ("media" | "r" | "agent" | "files" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel"
|
|
195
|
+
enabled: ("media" | "r" | "agent" | "files" | "skills" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel")[];
|
|
196
196
|
retainedReceipts: number;
|
|
197
197
|
retainedFromVersion: number;
|
|
198
198
|
maxReceipts: number;
|
package/dist/capabilities.js
CHANGED
|
@@ -10,6 +10,11 @@ const applicationContent = [
|
|
|
10
10
|
'Start with rho_index: select an owner or search, then rho_resolve only the operations',
|
|
11
11
|
'needed now. Use rho_call with the complete contractRef. The private connection supplies',
|
|
12
12
|
'the project; discovery never grants additional authority or starts a runtime.',
|
|
13
|
+
'Independent third-party plugins: extension.list shows installed packages, extension.describe',
|
|
14
|
+
'gives their digest, operations and Skill entries. Read extension.content (kind skill)',
|
|
15
|
+
'before extension.call. Keep expectedDigest and the original changeId for a native call;',
|
|
16
|
+
'extension.read observes pending or unknown outcomes without rerunning. Installation and',
|
|
17
|
+
'management belong to the local person; plugin code never expands this connection grant.',
|
|
13
18
|
'Large replies carry a result reference. Use rho_read to inspect its fields and fetch',
|
|
14
19
|
'only selected text ranges or array pages. Retain useful references; release finished',
|
|
15
20
|
'results explicitly. Never repeat an action just to retrieve a large reply.',
|
|
@@ -30,7 +35,7 @@ const applicationContent = [
|
|
|
30
35
|
'run or export it as files; a stopped run is not resubmitted under its own operationId.'
|
|
31
36
|
].join('\n');
|
|
32
37
|
const applicationSkill = {
|
|
33
|
-
id: 'rho.workspace', version: '1.
|
|
38
|
+
id: 'rho.workspace', version: '1.6', content: applicationContent,
|
|
34
39
|
intent: 'Work in this project through versioned operations, in a safe order.',
|
|
35
40
|
requires: ['capability.list', 'observation.snapshot', 'action.submit', 'composition.apply', 'context.read'],
|
|
36
41
|
example: { method: 'observation.snapshot', input: { projectId: 'the-open-project' } },
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import * as fs from 'node:fs/promises';
|
|
5
|
+
import { constants as fsConstants } from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
5
7
|
import { spawn } from 'node:child_process';
|
|
6
8
|
// A widened link is optional: the fragment carries a one-time code, which no request or Referer
|
|
7
9
|
// sends and which the page redeems and removes. Opening the plain url needs no code at all.
|
|
@@ -33,7 +35,9 @@ function parseAcpCommand(spec) {
|
|
|
33
35
|
throw new Error('--acp-command is empty.');
|
|
34
36
|
return { file: parts[0], args: parts.slice(1) };
|
|
35
37
|
}
|
|
36
|
-
const usage = `
|
|
38
|
+
const usage = `Develop, install and manage independent plugins:
|
|
39
|
+
rho plugin --help
|
|
40
|
+
Verify a read-only evidence package:
|
|
37
41
|
rho --verify-evidence PATH
|
|
38
42
|
Start Rho on ~/Rho/projects and ~/Rho/state, and say where it is and what is missing:
|
|
39
43
|
rho
|
|
@@ -41,10 +45,11 @@ Start Rho:
|
|
|
41
45
|
rho --root PROJECTS_DIRECTORY --state-dir SERVICE_STATE [--core ABSOLUTE_CORE_BINARY] [--rscript ABSOLUTE_RSCRIPT] [--python PYTHON] [--ssh SSH] [--serverctl SERVERCTL] [--claude CLAUDE_CODE_BINARY] [--codex CODEX_BINARY] [--agent-backend claude-acp|codex-acp|claude-cli|acp] [--acp-command JSON_ARGV] [--port PORT] [--limits JSON_FILE] [--plugins documents,files,...] [--example on|off] [--open]
|
|
42
46
|
A narrower browser link (optional):
|
|
43
47
|
rho --pair PRIVATE_CONNECTION_FILE [--root PROJECTS_RELATIVE_DIRECTORY] [--open]
|
|
44
|
-
Add Rho to your own Claude Code or Codex for one project
|
|
48
|
+
Add Rho to your own Claude Code or Codex for one project:
|
|
45
49
|
rho --grant PRIVATE_CONNECTION_FILE --project PROJECTS_RELATIVE_DIRECTORY --agent claude-code|codex
|
|
46
|
-
Give
|
|
50
|
+
Give any MCP Agent access to one project and print its stdio MCP configuration:
|
|
47
51
|
rho --grant PRIVATE_CONNECTION_FILE --project PROJECTS_RELATIVE_DIRECTORY [--session NAME]
|
|
52
|
+
rho --grant PRIVATE_CONNECTION_FILE --project-id EXISTING_PROJECT_ID --session NAME
|
|
48
53
|
Serve that access to the Agent over MCP (stdio):
|
|
49
54
|
rho --mcp AGENT_CONNECTION_FILE
|
|
50
55
|
Observe an external ACP client (use this as its agent command):
|
|
@@ -58,8 +63,24 @@ narrower, expiring link for one folder.
|
|
|
58
63
|
PRIVATE_CONNECTION_FILE is the connectionFile the running service printed; it and every
|
|
59
64
|
connection file are credentials.
|
|
60
65
|
`;
|
|
66
|
+
// DeepSeek Harness speaks ACP through its `acp` profile (`dsh acp`). It is used only when both the
|
|
67
|
+
// launcher and that profile are on this computer; nothing is started to find them.
|
|
68
|
+
async function harnessAcp() {
|
|
69
|
+
const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
70
|
+
const profile = await fs.stat(path.join(home, 'profiles', 'acp', 'package.json')).then(s => s.isFile(), () => false);
|
|
71
|
+
if (!profile)
|
|
72
|
+
return undefined;
|
|
73
|
+
const names = process.platform === 'win32' ? ['dsh.cmd', 'dsh.exe', 'dsh'] : ['dsh'];
|
|
74
|
+
for (const dir of (process.env.PATH ?? '').split(path.delimiter).filter(Boolean))
|
|
75
|
+
for (const name of names) {
|
|
76
|
+
const file = path.join(dir, name);
|
|
77
|
+
if (await fs.access(file, fsConstants.X_OK).then(() => true, () => false))
|
|
78
|
+
return { file, args: ['acp'] };
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
61
82
|
const options = { example: { type: 'string' }, 'verify-evidence': { type: 'string' }, acp: { type: 'string' }, 'acp-agent': { type: 'string' }, root: { type: 'string' }, 'state-dir': { type: 'string' }, core: { type: 'string' }, rscript: { type: 'string' }, python: { type: 'string' }, ssh: { type: 'string' }, serverctl: { type: 'string' }, claude: { type: 'string' }, codex: { type: 'string' }, 'agent-backend': { type: 'string' }, 'acp-command': { type: 'string' }, port: { type: 'string' }, limits: { type: 'string' }, plugins: { type: 'string' }, pair: { type: 'string' }, open: { type: 'boolean' },
|
|
62
|
-
grant: { type: 'string' }, project: { type: 'string' }, session: { type: 'string' }, agent: { type: 'string' }, mcp: { type: 'string' }, help: { type: 'boolean', short: 'h' } };
|
|
83
|
+
grant: { type: 'string' }, project: { type: 'string' }, 'project-id': { type: 'string' }, session: { type: 'string' }, agent: { type: 'string' }, mcp: { type: 'string' }, help: { type: 'boolean', short: 'h' } };
|
|
63
84
|
// A command that is refused says why and what to do. A service that is not running is the
|
|
64
85
|
// common case for --grant and --pair; anything without a known reason keeps its stack.
|
|
65
86
|
function refuse(error, url) {
|
|
@@ -72,197 +93,219 @@ function refuse(error, url) {
|
|
|
72
93
|
throw error;
|
|
73
94
|
process.exit(1);
|
|
74
95
|
}
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
parsed = parseArgs({ options });
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
process.stderr.write((error instanceof Error ? error.message : String(error)) + '\n\n' + usage);
|
|
81
|
-
process.exit(2);
|
|
82
|
-
}
|
|
83
|
-
const { values } = parsed;
|
|
84
|
-
if (values['verify-evidence']) {
|
|
96
|
+
if (process.argv[2] === 'plugin') {
|
|
85
97
|
try {
|
|
86
|
-
|
|
87
|
-
process.stdout.write(JSON.stringify(await verifyEvidence(path.resolve(values['verify-evidence']))) + '\n');
|
|
98
|
+
await (await import('./plugin-cli.js')).pluginCli(process.argv.slice(3));
|
|
88
99
|
}
|
|
89
|
-
catch (
|
|
90
|
-
process.stderr.write((
|
|
100
|
+
catch (error) {
|
|
101
|
+
process.stderr.write((error instanceof Error ? error.message : String(error)) + '\n');
|
|
91
102
|
process.exitCode = 1;
|
|
92
103
|
}
|
|
93
|
-
process.exit(process.exitCode ?? 0);
|
|
94
|
-
}
|
|
95
|
-
if (values.help) {
|
|
96
|
-
process.stdout.write(usage);
|
|
97
|
-
process.exit(0);
|
|
98
104
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
else {
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = parseArgs({ options });
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
process.stderr.write((error instanceof Error ? error.message : String(error)) + '\n\n' + usage);
|
|
104
112
|
process.exit(2);
|
|
105
113
|
}
|
|
106
|
-
const {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
await import('@rho-workspace/plugin-agent/mcp');
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
// Access comes from whoever runs Rho: the operator's connection grants one project to one
|
|
115
|
-
// Agent session, and the reply says how Claude Code starts that session's MCP server.
|
|
116
|
-
if (values.grant) {
|
|
117
|
-
if (!values.project) {
|
|
118
|
-
process.stderr.write('--grant needs --project, the project folder relative to the service root.\n\n' + usage);
|
|
119
|
-
process.exit(2);
|
|
114
|
+
const { values } = parsed;
|
|
115
|
+
if (values['verify-evidence']) {
|
|
116
|
+
try {
|
|
117
|
+
const { verifyEvidence } = await import('./evidence-export.js');
|
|
118
|
+
process.stdout.write(JSON.stringify(await verifyEvidence(path.resolve(values['verify-evidence']))) + '\n');
|
|
120
119
|
}
|
|
121
|
-
|
|
122
|
-
process.stderr.write(
|
|
123
|
-
process.
|
|
120
|
+
catch (e) {
|
|
121
|
+
process.stderr.write((e instanceof Error ? e.message : String(e)) + '\n');
|
|
122
|
+
process.exitCode = 1;
|
|
124
123
|
}
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
process.exit(process.exitCode ?? 0);
|
|
125
|
+
}
|
|
126
|
+
if (values.help) {
|
|
127
|
+
process.stdout.write(usage);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
// The Agent's side of a grant: its MCP server reads the connection file and speaks on stdio,
|
|
131
|
+
// so nothing else may write to stdout in this mode.
|
|
132
|
+
if (values.acp) {
|
|
133
|
+
if (!['claude', 'codex'].includes(values['acp-agent'] ?? '')) {
|
|
134
|
+
process.stderr.write('Select --acp-agent claude or codex.\n');
|
|
127
135
|
process.exit(2);
|
|
128
136
|
}
|
|
129
|
-
const {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
const { runExternalAcp } = await import('@rho-workspace/plugin-agent/acp-cli');
|
|
138
|
+
runExternalAcp(path.resolve(values.acp), values['acp-agent']);
|
|
139
|
+
}
|
|
140
|
+
else if (values.mcp) {
|
|
141
|
+
process.env.RHO_AGENT_CONNECTION = path.resolve(values.mcp);
|
|
142
|
+
await import('@rho-workspace/plugin-agent/mcp');
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Access comes from whoever runs Rho: the operator's connection grants one project to one
|
|
146
|
+
// Agent session, and the reply says how Claude Code starts that session's MCP server.
|
|
147
|
+
if (values.grant) {
|
|
148
|
+
if (!!values.project === !!values['project-id']) {
|
|
149
|
+
process.stderr.write('--grant needs exactly one of --project (a folder relative to the service root) or --project-id (an existing project).\n\n' + usage);
|
|
150
|
+
process.exit(2);
|
|
151
|
+
}
|
|
152
|
+
if (values.agent !== undefined && !['claude-code', 'codex'].includes(values.agent)) {
|
|
153
|
+
process.stderr.write('Choose --agent claude-code or codex.\n');
|
|
154
|
+
process.exit(2);
|
|
155
|
+
}
|
|
156
|
+
if (values.agent !== undefined && values.session !== undefined) {
|
|
157
|
+
process.stderr.write('--agent names the session itself; leave out --session.\n');
|
|
158
|
+
process.exit(2);
|
|
159
|
+
}
|
|
160
|
+
const { RhoClient } = await import('./client.js');
|
|
161
|
+
const client = await RhoClient.fromFile(path.resolve(values.grant)).catch(error => refuse(error));
|
|
162
|
+
try {
|
|
163
|
+
const project = values['project-id']
|
|
164
|
+
? (await client.call('project.list', {})).find(p => p.id === values['project-id'])
|
|
165
|
+
: await client.call('project.open', { path: values.project });
|
|
166
|
+
if (!project) {
|
|
167
|
+
process.stderr.write('Project is not registered or is outside this connection’s scope. Open the project in Rho and copy its setup prompt again.\n');
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
// The same as the project page's button: the grant that Agent keeps, then its own
|
|
171
|
+
// `mcp add`, recorded so the page shows it under that Agent.
|
|
172
|
+
if (values.agent) {
|
|
173
|
+
const connected = await client.call('session.connect', { projectId: project.id, agent: values.agent });
|
|
174
|
+
process.stdout.write(JSON.stringify({ projectId: project.id, root: project.root, agent: values.agent, methods: connected.methods, granted: connected.granted, added: connected.added }) + '\n');
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
const session = values.session ?? 'claude-code';
|
|
178
|
+
const granted = await client.call('session.grant', { projectId: project.id, externalSessionId: session });
|
|
179
|
+
const { mcpCommand, mcpName } = await import('./agent-connect.js');
|
|
180
|
+
const command = mcpCommand(granted.connectionFile), name = mcpName(project.root);
|
|
181
|
+
const quote = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
182
|
+
process.stdout.write(JSON.stringify({ projectId: project.id, session, connectionFile: granted.connectionFile, methods: granted.methods.length,
|
|
183
|
+
mcp: { name, command: command[0], args: command.slice(1) }, claude: ['claude', 'mcp', 'add', name, '--', ...command].map(quote).join(' ') }) + '\n');
|
|
138
184
|
process.exit(0);
|
|
139
185
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const command = mcpCommand(granted.connectionFile), name = mcpName(project.root);
|
|
144
|
-
const quote = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
145
|
-
process.stdout.write(JSON.stringify({ projectId: project.id, session, connectionFile: granted.connectionFile, methods: granted.methods.length,
|
|
146
|
-
mcp: { name, command: command[0], args: command.slice(1) }, claude: ['claude', 'mcp', 'add', name, '--', ...command].map(quote).join(' ') }) + '\n');
|
|
147
|
-
process.exit(0);
|
|
186
|
+
catch (error) {
|
|
187
|
+
refuse(error, client.connection.url);
|
|
188
|
+
}
|
|
148
189
|
}
|
|
149
|
-
|
|
150
|
-
|
|
190
|
+
if (values.pair) {
|
|
191
|
+
const { RhoClient } = await import('./client.js');
|
|
192
|
+
const client = await RhoClient.fromFile(path.resolve(values.pair)).catch(error => refuse(error));
|
|
193
|
+
try {
|
|
194
|
+
const pairing = await client.call('session.browser.pair', values.root ? { path: values.root } : {});
|
|
195
|
+
const openUrl = link(client.connection.url, pairing.code);
|
|
196
|
+
process.stdout.write(JSON.stringify({ url: client.connection.url, openUrl, pairingCode: pairing.code, pairingExpiresAt: pairing.expiresAt }) + '\n');
|
|
197
|
+
if (values.open)
|
|
198
|
+
openBrowser(openUrl);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
refuse(error, client.connection.url);
|
|
203
|
+
}
|
|
151
204
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
process.
|
|
160
|
-
if (values.open)
|
|
161
|
-
openBrowser(openUrl);
|
|
162
|
-
process.exit(0);
|
|
205
|
+
// A person who runs `npx @rho-workspace/rho` with no arguments wants the application, not a
|
|
206
|
+
// usage page: start it on their own folders, say in plain words where it is, and open it when
|
|
207
|
+
// this is an interactive terminal. Every flagged start is unchanged, so scripts and
|
|
208
|
+
// acceptance runs keep the output they read.
|
|
209
|
+
const quickStart = !values.root && !values['state-dir'];
|
|
210
|
+
if (!quickStart && (!values.root || !values['state-dir'])) {
|
|
211
|
+
process.stderr.write(usage);
|
|
212
|
+
process.exit(2);
|
|
163
213
|
}
|
|
164
|
-
|
|
165
|
-
|
|
214
|
+
let quickRoot = '', quickState = '';
|
|
215
|
+
if (quickStart) {
|
|
216
|
+
const { homedir } = await import('node:os');
|
|
217
|
+
quickRoot = path.join(homedir(), 'Rho', 'projects');
|
|
218
|
+
quickState = path.join(homedir(), 'Rho', 'state');
|
|
219
|
+
await fs.mkdir(quickRoot, { recursive: true });
|
|
220
|
+
await fs.mkdir(quickState, { recursive: true });
|
|
166
221
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
const projectsRoot = values.root ?? quickRoot, stateDirectory = values['state-dir'] ?? quickState;
|
|
223
|
+
const limits = values.limits ? JSON.parse(await fs.readFile(values.limits, 'utf8')) : {};
|
|
224
|
+
const allowed = new Set(['maxRequestBytes', 'maxEventBytes', 'eventRetention', 'maxSubscriptions', 'maxQueue', 'maxContexts', 'maxBindings', 'maxActivities', 'maxContextBytes', 'maxRetainedMetadataBytes', 'maxConcurrentActions', 'maxOperations', 'maxLogPageBytes', 'maxCodeBytes', 'maxReconciled', 'documents', 'resources', 'workspace', 'r', 'annotations', 'interactions', 'capabilityLimits', 'browser', 'nativeFolders', 'conversations', 'evidence', 'console']);
|
|
225
|
+
if (Object.keys(limits).some(k => !allowed.has(k)))
|
|
226
|
+
throw new Error('Unknown service budget in --limits file.');
|
|
227
|
+
const { RhoService } = await import('./server.js'), { runtime } = await import('./runtime.js');
|
|
228
|
+
const agentBackend = values['agent-backend'] ?? process.env.RHO_AGENT_BACKEND ?? 'claude-acp';
|
|
229
|
+
if (!['claude-acp', 'codex-acp', 'claude-cli', 'acp'].includes(agentBackend))
|
|
230
|
+
throw new Error('Choose --agent-backend claude-acp, codex-acp, claude-cli or acp.');
|
|
231
|
+
// An ACP command named here, or else DeepSeek Harness's own ACP profile when it is installed, so
|
|
232
|
+
// sessions started in Harness continue in Rho too. Naming one does not make it the default Agent.
|
|
233
|
+
const named = values['acp-command'] ?? process.env.RHO_ACP_COMMAND;
|
|
234
|
+
const acpCommand = agentBackend === 'acp' || named ? parseAcpCommand(named) : await harnessAcp();
|
|
235
|
+
if (values.port !== undefined && !(/^\d+$/.test(values.port) && Number(values.port) <= 65535)) {
|
|
236
|
+
process.stderr.write('Choose --port 0 to 65535, or leave it out.\n');
|
|
237
|
+
process.exit(2);
|
|
238
|
+
}
|
|
239
|
+
// A Core that cannot be used (none for this platform, a changed digest) is read by a person.
|
|
240
|
+
const native = await runtime(values.core, values.rscript).catch(e => { process.stderr.write((e instanceof Error ? e.message : String(e)) + '\n'); process.exit(1); });
|
|
241
|
+
// A person starting Rho sees the project it ships with; a composition that starts a service
|
|
242
|
+
// for its own work says so itself and is left alone.
|
|
243
|
+
if (values.example !== undefined && !['on', 'off'].includes(values.example))
|
|
244
|
+
throw new Error('Choose --example on or off.');
|
|
245
|
+
const example = values.example !== 'off';
|
|
246
|
+
const service = await new RhoService({ example, ...limits, root: path.resolve(projectsRoot), stateDir: path.resolve(stateDirectory), ...native, agentBackend: agentBackend, acpCommand, codex: values.codex ?? process.env.RHO_CODEX ?? 'codex', claude: values.claude ?? process.env.RHO_CLAUDE ?? 'claude', python: values.python ?? process.env.RHO_PYTHON, ssh: values.ssh ?? process.env.RHO_SSH, serverctl: values.serverctl ?? process.env.RHO_SERVERCTL, port: values.port === undefined ? undefined : Number(values.port), ...(values.plugins !== undefined ? { plugins: values.plugins ? values.plugins.split(',') : [] } : {}) }).start().catch(async (error) => {
|
|
247
|
+
// Starting Rho again on a state it already runs on is the common refusal: say where it runs.
|
|
248
|
+
const held = error?.details?.stateDirectory;
|
|
249
|
+
const running = typeof held === 'string' ? await fs.readFile(path.join(held, 'connection.json'), 'utf8').then(text => String(JSON.parse(text).url), () => '') : '';
|
|
250
|
+
return refuse(running ? Object.assign(error, { message: `Rho is already running for ${held} at ${running}. Open that url, or stop that service before starting it again.` }) : error);
|
|
251
|
+
});
|
|
252
|
+
// The narrow conclusion a person needs on one line: what was selected, where state
|
|
253
|
+
// lives, and how to open it. It stays inside `ready` so a parser reading line one is
|
|
254
|
+
// unaffected. Opening this url on this machine is the access; no link is minted.
|
|
255
|
+
const rscript = values.rscript ?? process.env.RHO_RSCRIPT ?? 'Rscript';
|
|
256
|
+
process.stdout.write(JSON.stringify({ ready: {
|
|
257
|
+
url: service.url, connectionFile: path.join(service.state.directory, 'connection.json'), apiVersion: '2.0',
|
|
258
|
+
openUrl: service.url,
|
|
259
|
+
startup: {
|
|
260
|
+
projects: path.resolve(projectsRoot), state: path.resolve(stateDirectory),
|
|
261
|
+
core: service.bridge.binary, rscript: service.bridge.rscript,
|
|
262
|
+
note: 'Open this url in a browser on this machine. Execution stays off until an Agent submits a run; project.doctor answers what is missing.'
|
|
263
|
+
}
|
|
264
|
+
} }) + '\n');
|
|
265
|
+
// What a person needs, after the machine line: where it is, and what is missing to work in it.
|
|
266
|
+
// A missing Agent or R does not stop the workspace; it decides what the person can do next.
|
|
267
|
+
if (quickStart) {
|
|
268
|
+
const say = (text) => process.stdout.write(text + '\n');
|
|
269
|
+
say('');
|
|
270
|
+
say('Rho is running. Open this address in a browser on this machine:');
|
|
271
|
+
say(' ' + service.url);
|
|
272
|
+
say('');
|
|
273
|
+
say('Projects folder: ' + quickRoot);
|
|
274
|
+
say('Service state: ' + quickState);
|
|
275
|
+
say('');
|
|
276
|
+
const agentCommand = await import('@rho-workspace/plugin-agent/acp-command').catch(() => null);
|
|
277
|
+
// The selection the service itself got: an explicit flag, then the environment, then the name
|
|
278
|
+
// on PATH. Reporting "installed" for a client the person pointed at a missing path is worse
|
|
279
|
+
// than saying nothing.
|
|
280
|
+
const claudePath = values.claude ?? process.env.RHO_CLAUDE, codexPath = values.codex ?? process.env.RHO_CODEX;
|
|
281
|
+
const env = { ...process.env, ...(claudePath ? { CLAUDE_CODE_EXECUTABLE: claudePath } : {}), ...(codexPath ? { CODEX_PATH: codexPath } : {}) };
|
|
282
|
+
// An ACP agent named with --acp-command is the operator's own command; it is named, not checked.
|
|
283
|
+
if (acpCommand && agentBackend === 'acp')
|
|
284
|
+
say('Agent: ' + acpCommand.file + ', named with --acp-command.');
|
|
285
|
+
else {
|
|
286
|
+
if (acpCommand)
|
|
287
|
+
say('Also: ' + (acpCommand.args[0] === 'acp' && /dsh(\.[a-z]+)?$/i.test(acpCommand.file) ? 'DeepSeek Harness (' + acpCommand.file + ' acp)' : acpCommand.file) + ', so its sessions can continue here.');
|
|
288
|
+
const missing = agentCommand ? agentCommand.missingClient(agentBackend === 'codex-acp' ? 'codex' : 'claude', env) : null;
|
|
289
|
+
say(missing
|
|
290
|
+
? 'Agent: ' + missing + ' Until then folders can be opened and files read here; the Agent cannot run in them.'
|
|
291
|
+
: 'Agent: ' + (agentBackend === 'codex-acp' ? 'Codex' : 'Claude Code') + ' is installed; a conversation can start in any project.');
|
|
223
292
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
say('');
|
|
233
|
-
say('Projects folder: ' + quickRoot);
|
|
234
|
-
say('Service state: ' + quickState);
|
|
235
|
-
say('');
|
|
236
|
-
const agentCommand = await import('@rho-workspace/plugin-agent/acp-command').catch(() => null);
|
|
237
|
-
// The selection the service itself got: an explicit flag, then the environment, then the name
|
|
238
|
-
// on PATH. Reporting "installed" for a client the person pointed at a missing path is worse
|
|
239
|
-
// than saying nothing.
|
|
240
|
-
const claudePath = values.claude ?? process.env.RHO_CLAUDE, codexPath = values.codex ?? process.env.RHO_CODEX;
|
|
241
|
-
const env = { ...process.env, ...(claudePath ? { CLAUDE_CODE_EXECUTABLE: claudePath } : {}), ...(codexPath ? { CODEX_PATH: codexPath } : {}) };
|
|
242
|
-
// An ACP agent named with --acp-command is the operator's own command; it is named, not checked.
|
|
243
|
-
if (acpCommand)
|
|
244
|
-
say('Agent: ' + acpCommand.file + ', named with --acp-command.');
|
|
245
|
-
else {
|
|
246
|
-
const missing = agentCommand ? agentCommand.missingClient(agentBackend === 'codex-acp' ? 'codex' : 'claude', env) : null;
|
|
247
|
-
say(missing
|
|
248
|
-
? 'Agent: ' + missing + ' Until then folders can be opened and files read here; the Agent cannot run in them.'
|
|
249
|
-
: 'Agent: ' + (agentBackend === 'codex-acp' ? 'Codex' : 'Claude Code') + ' is installed; a conversation can start in any project.');
|
|
293
|
+
const hasR = await fs.access(service.bridge.rscript).then(() => true, () => false);
|
|
294
|
+
say(hasR
|
|
295
|
+
? 'R: ' + service.bridge.rscript
|
|
296
|
+
: 'R: Rscript was not found. Reading, editing and viewing work; running R needs R installed (or --rscript).');
|
|
297
|
+
say('');
|
|
298
|
+
say('Stop it with Ctrl+C. Start it again with: npx @rho-workspace/rho');
|
|
299
|
+
if (process.stdout.isTTY)
|
|
300
|
+
openBrowser(service.url);
|
|
250
301
|
}
|
|
251
|
-
|
|
252
|
-
say(hasR
|
|
253
|
-
? 'R: ' + service.bridge.rscript
|
|
254
|
-
: 'R: Rscript was not found. Reading, editing and viewing work; running R needs R installed (or --rscript).');
|
|
255
|
-
say('');
|
|
256
|
-
say('Stop it with Ctrl+C. Start it again with: npx @rho-workspace/rho');
|
|
257
|
-
if (process.stdout.isTTY)
|
|
302
|
+
if (values.open)
|
|
258
303
|
openBrowser(service.url);
|
|
304
|
+
let closing = false;
|
|
305
|
+
// Windows has no SIGTERM another process can send, and a console may pass on Ctrl+C turned off;
|
|
306
|
+
// there Ctrl+Break on Rho's console (SIGBREAK) stops it in order too.
|
|
307
|
+
for (const signal of ['SIGTERM', 'SIGINT', ...(process.platform === 'win32' ? ['SIGBREAK'] : [])])
|
|
308
|
+
process.on(signal, async () => { if (closing)
|
|
309
|
+
return; closing = true; const reports = await service.close(); process.stdout.write(JSON.stringify({ shutdown: reports }) + '\n'); });
|
|
259
310
|
}
|
|
260
|
-
if (values.open)
|
|
261
|
-
openBrowser(service.url);
|
|
262
|
-
let closing = false;
|
|
263
|
-
// Windows has no SIGTERM another process can send, and a console may pass on Ctrl+C turned off;
|
|
264
|
-
// there Ctrl+Break on Rho's console (SIGBREAK) stops it in order too.
|
|
265
|
-
for (const signal of ['SIGTERM', 'SIGINT', ...(process.platform === 'win32' ? ['SIGBREAK'] : [])])
|
|
266
|
-
process.on(signal, async () => { if (closing)
|
|
267
|
-
return; closing = true; const reports = await service.close(); process.stdout.write(JSON.stringify({ shutdown: reports }) + '\n'); });
|
|
268
311
|
}
|