@myagentroam/node 0.1.8 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/capabilities.js +6 -3
  2. package/dist/claude-agent-sdk.d.ts +1 -0
  3. package/dist/claude-agent-sdk.js +8 -1
  4. package/dist/codex-app-server.d.ts +6 -0
  5. package/dist/codex-app-server.js +6 -0
  6. package/dist/connector/node-connector-options.d.ts +1 -0
  7. package/dist/connector.js +5 -2
  8. package/dist/database.d.ts +23 -0
  9. package/dist/database.js +84 -2
  10. package/dist/migrations/v004.d.ts +4 -0
  11. package/dist/migrations/v004.js +35 -0
  12. package/dist/opencode-server.d.ts +1 -0
  13. package/dist/opencode-server.js +12 -0
  14. package/dist/runner/abstract-runner.d.ts +12 -2
  15. package/dist/runner/abstract-runner.js +76 -2
  16. package/dist/runner/claude/managed-run-controller.js +2 -1
  17. package/dist/runner/claude-code-runner.d.ts +1 -0
  18. package/dist/runner/claude-code-runner.js +14 -0
  19. package/dist/runner/codex/managed-run-controller.js +5 -3
  20. package/dist/runner/codex-runner.d.ts +16 -2
  21. package/dist/runner/codex-runner.js +215 -4
  22. package/dist/runner/opencode/managed-run-controller.js +13 -3
  23. package/dist/runner/opencode-runner.d.ts +2 -1
  24. package/dist/runner/opencode-runner.js +23 -3
  25. package/dist/runner/runner-registry.d.ts +1 -1
  26. package/dist/runner/runner-registry.js +2 -2
  27. package/dist/runner-profiles.js +5 -25
  28. package/dist/runtime-command-detector.d.ts +3 -0
  29. package/dist/runtime-command-detector.js +78 -0
  30. package/dist/service/mcp-installation-verifier.d.ts +9 -0
  31. package/dist/service/mcp-installation-verifier.js +87 -0
  32. package/dist/service/mcp-node-operation-service.d.ts +11 -0
  33. package/dist/service/mcp-node-operation-service.js +90 -0
  34. package/dist/service/mcp-package-installer.d.ts +8 -0
  35. package/dist/service/mcp-package-installer.js +136 -0
  36. package/dist/service/runner-service.d.ts +2 -0
  37. package/dist/service/runner-service.js +5 -2
  38. package/dist/service/session-lifecycle-service.js +7 -4
  39. package/dist/service/workbench-manifest-service.d.ts +2 -0
  40. package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
  41. package/dist/service/workspace-queue-workbench-service.js +2 -1
  42. package/package.json +2 -2
@@ -0,0 +1,90 @@
1
+ import { mcpCatalogEntrySchema, mcpInstallationInputSchema, mcpTargetInspectionSchema } from '@myagentroam/protocol';
2
+ import { homedir } from 'node:os';
3
+ import { McpInstallationVerifier } from './mcp-installation-verifier.js';
4
+ export class McpNodeOperationService {
5
+ requireDatabase;
6
+ verifier;
7
+ constructor(requireDatabase, verifier = new McpInstallationVerifier()) {
8
+ this.requireDatabase = requireDatabase;
9
+ this.verifier = verifier;
10
+ }
11
+ operations() {
12
+ return {
13
+ 'node.mcps.inspect': () => this.inspect('NODE', ''),
14
+ 'node.mcps.install': (data) => this.install('NODE', '', data),
15
+ 'node.mcps.remove': (data) => this.remove('NODE', '', data),
16
+ 'workspace.mcps.inspect': (data) => this.inspect('WORKSPACE', workspaceId(data)),
17
+ 'workspace.mcps.install': (data) => this.install('WORKSPACE', workspaceId(data), data),
18
+ 'workspace.mcps.remove': (data) => this.remove('WORKSPACE', workspaceId(data), data)
19
+ };
20
+ }
21
+ inspect(targetKind, targetId) {
22
+ const database = this.requireDatabase();
23
+ const own = database.listMcpInstallations(targetKind, targetId);
24
+ const ownNames = new Set(own.map((installation) => installation.serverName));
25
+ const installations = [
26
+ ...(targetKind === 'WORKSPACE'
27
+ ? database
28
+ .listMcpInstallations('NODE')
29
+ .filter((installation) => !ownNames.has(installation.serverName))
30
+ : []),
31
+ ...own
32
+ ].map((installation) => ({
33
+ catalogEntryId: installation.catalogEntryId,
34
+ serverName: installation.serverName,
35
+ displayName: installation.displayName,
36
+ version: installation.version,
37
+ origin: installation.targetKind,
38
+ enabled: installation.enabled,
39
+ status: 'CURRENT',
40
+ installedAt: installation.installedAt
41
+ }));
42
+ return mcpTargetInspectionSchema.parse({ targetKind, installations });
43
+ }
44
+ async install(targetKind, targetId, raw) {
45
+ if (!isRecord(raw))
46
+ throw new Error('MCP_INSTALL_INPUT_INVALID');
47
+ const entry = mcpCatalogEntrySchema.parse(raw['entry']);
48
+ const input = mcpInstallationInputSchema.parse({
49
+ catalogEntryId: entry.id,
50
+ enabled: raw['enabled'] ?? true,
51
+ configuration: raw['configuration'] ?? {}
52
+ });
53
+ const database = this.requireDatabase();
54
+ const secretEnvironment = isRecord(raw['secretEnvironment'])
55
+ ? Object.fromEntries(Object.entries(raw['secretEnvironment']).filter((entry) => typeof entry[1] === 'string'))
56
+ : {};
57
+ const cwd = targetKind === 'WORKSPACE' ? database.getWorkspace(targetId)?.path : homedir();
58
+ if (cwd === undefined)
59
+ throw new Error('WORKSPACE_NOT_FOUND');
60
+ if (input.enabled)
61
+ await this.verifier.verify({
62
+ entry,
63
+ configuration: input.configuration,
64
+ secrets: secretEnvironment,
65
+ cwd
66
+ });
67
+ database.saveMcpInstallation({
68
+ targetKind,
69
+ ...(targetKind === 'WORKSPACE' ? { targetId } : {}),
70
+ entry,
71
+ enabled: input.enabled,
72
+ configuration: input.configuration
73
+ });
74
+ return this.inspect(targetKind, targetId);
75
+ }
76
+ remove(targetKind, targetId, raw) {
77
+ if (!isRecord(raw) || typeof raw['serverName'] !== 'string')
78
+ throw new Error('MCP_INSTALL_INPUT_INVALID');
79
+ this.requireDatabase().removeMcpInstallation(targetKind, targetId, raw['serverName']);
80
+ return this.inspect(targetKind, targetId);
81
+ }
82
+ }
83
+ function workspaceId(raw) {
84
+ if (!isRecord(raw) || typeof raw['workspaceId'] !== 'string' || raw['workspaceId'] === '')
85
+ throw new Error('WORKSPACE_INVALID');
86
+ return raw['workspaceId'];
87
+ }
88
+ function isRecord(value) {
89
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
90
+ }
@@ -0,0 +1,8 @@
1
+ import type { McpCatalogEntry } from '@myagentroam/protocol';
2
+ export declare class McpPackageInstaller {
3
+ private readonly dataDirectory;
4
+ constructor(dataDirectory: string | (() => string));
5
+ prepare(entry: McpCatalogEntry): Promise<McpCatalogEntry>;
6
+ private install;
7
+ private prepareRuntimeEnvironment;
8
+ }
@@ -0,0 +1,136 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ const execute = promisify(execFile);
7
+ const installs = new Map();
8
+ export class McpPackageInstaller {
9
+ dataDirectory;
10
+ constructor(dataDirectory) {
11
+ this.dataDirectory = dataDirectory;
12
+ }
13
+ prepare(entry) {
14
+ if (entry.runtime.transport !== 'STDIO')
15
+ return Promise.resolve(entry);
16
+ const request = packageRequest(entry.runtime.command, entry.runtime.args);
17
+ if (request === undefined)
18
+ return Promise.resolve(entry);
19
+ const key = `${request.spec}\0${entry.serverName}`;
20
+ const active = installs.get(key);
21
+ if (active !== undefined)
22
+ return active;
23
+ const operation = this.install(entry, request).finally(() => installs.delete(key));
24
+ installs.set(key, operation);
25
+ return operation;
26
+ }
27
+ async install(entry, request) {
28
+ const dataDirectory = typeof this.dataDirectory === 'string' ? this.dataDirectory : this.dataDirectory();
29
+ if (entry.runtime.transport !== 'STDIO')
30
+ return entry;
31
+ const runtimeEnvironment = entry.runtime.environment;
32
+ const digest = createHash('sha256').update(request.spec).digest('hex');
33
+ const root = resolve(dataDirectory, 'mcp-packages', digest);
34
+ const metadataPath = resolve(root, 'installation.json');
35
+ let executable = await installedExecutable(metadataPath, request.spec);
36
+ if (executable === undefined) {
37
+ const staging = `${root}.install-${randomUUID()}`;
38
+ await mkdir(dirname(root), { recursive: true, mode: 0o700 });
39
+ try {
40
+ await mkdir(staging, { recursive: true, mode: 0o700 });
41
+ await runNpmInstall(staging, request.spec);
42
+ executable = await resolvePackageExecutable(staging, request.spec);
43
+ await writeFile(resolve(staging, 'installation.json'), `${JSON.stringify({ spec: request.spec, executable: executable.slice(staging.length) })}\n`, { mode: 0o600 });
44
+ await rm(root, { recursive: true, force: true });
45
+ await rename(staging, root);
46
+ executable = resolve(root, `.${executable.slice(staging.length)}`);
47
+ }
48
+ catch (error) {
49
+ await rm(staging, { recursive: true, force: true });
50
+ throw packageError(error);
51
+ }
52
+ }
53
+ const prefixArgs = await this.prepareRuntimeEnvironment(entry.serverName, root, dataDirectory);
54
+ return {
55
+ ...entry,
56
+ runtime: {
57
+ transport: 'STDIO',
58
+ command: process.execPath,
59
+ args: [...prefixArgs, executable, ...request.serverArgs],
60
+ ...(runtimeEnvironment === undefined ? {} : { environment: runtimeEnvironment })
61
+ }
62
+ };
63
+ }
64
+ async prepareRuntimeEnvironment(serverName, packageRoot, dataDirectory) {
65
+ if (serverName !== 'io.github.microsoft/playwright-mcp')
66
+ return [];
67
+ const browserRoot = resolve(dataDirectory, 'mcp-browsers', 'playwright');
68
+ await mkdir(browserRoot, { recursive: true, mode: 0o700 });
69
+ const preload = resolve(packageRoot, 'playwright-environment.cjs');
70
+ await writeFile(preload, `process.env.PLAYWRIGHT_BROWSERS_PATH = ${JSON.stringify(browserRoot)};\n`, { mode: 0o600 });
71
+ const playwright = resolve(packageRoot, 'node_modules', 'playwright', 'cli.js');
72
+ await run(process.execPath, [playwright, 'install', 'chromium'], {
73
+ PLAYWRIGHT_BROWSERS_PATH: browserRoot
74
+ });
75
+ const modulePath = resolve(packageRoot, 'node_modules', 'playwright');
76
+ await run(process.execPath, [
77
+ '-e',
78
+ `const {chromium}=require(${JSON.stringify(modulePath)});chromium.launch({headless:true}).then(b=>b.close())`
79
+ ], { PLAYWRIGHT_BROWSERS_PATH: browserRoot });
80
+ return ['--require', preload];
81
+ }
82
+ }
83
+ function packageRequest(command, args) {
84
+ if (command !== 'npx')
85
+ return undefined;
86
+ const index = args.findIndex((argument) => !argument.startsWith('-'));
87
+ const spec = index < 0 ? '' : args[index];
88
+ if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+@\d[0-9A-Za-z.+-]*$/u.test(spec))
89
+ throw new Error('MCP_PACKAGE_SPEC_INVALID');
90
+ return { spec, serverArgs: args.slice(index + 1) };
91
+ }
92
+ async function runNpmInstall(root, spec) {
93
+ await run(process.platform === 'win32' ? 'npm.cmd' : 'npm', [
94
+ 'install',
95
+ '--prefix',
96
+ root,
97
+ '--no-save',
98
+ '--package-lock=false',
99
+ '--ignore-scripts',
100
+ spec
101
+ ]);
102
+ }
103
+ async function run(command, args, environment = {}) {
104
+ await execute(command, [...args], {
105
+ env: { ...process.env, ...environment },
106
+ timeout: 300_000,
107
+ maxBuffer: 1024 * 1024,
108
+ ...(process.platform === 'win32' && command.endsWith('.cmd') ? { shell: true } : {})
109
+ });
110
+ }
111
+ async function resolvePackageExecutable(root, spec) {
112
+ const packageName = spec.slice(0, spec.lastIndexOf('@'));
113
+ const packageRoot = resolve(root, 'node_modules', ...packageName.split('/'));
114
+ const manifest = JSON.parse(await readFile(resolve(packageRoot, 'package.json'), 'utf8'));
115
+ const candidates = typeof manifest.bin === 'string' ? [manifest.bin] : Object.values(manifest.bin ?? {});
116
+ if (candidates.length !== 1)
117
+ throw new Error('MCP_PACKAGE_EXECUTABLE_INVALID');
118
+ return resolve(packageRoot, candidates[0]);
119
+ }
120
+ async function installedExecutable(metadataPath, spec) {
121
+ try {
122
+ const metadata = JSON.parse(await readFile(metadataPath, 'utf8'));
123
+ if (metadata['spec'] !== spec || typeof metadata['executable'] !== 'string')
124
+ return undefined;
125
+ return resolve(dirname(metadataPath), `.${metadata['executable']}`);
126
+ }
127
+ catch {
128
+ return undefined;
129
+ }
130
+ }
131
+ function packageError(error) {
132
+ const message = error instanceof Error ? error.message : '';
133
+ if (message.startsWith('MCP_'))
134
+ return new Error(message);
135
+ return new Error('MCP_PACKAGE_INSTALL_FAILED');
136
+ }
@@ -17,6 +17,8 @@ export declare class RunnerService {
17
17
  id: string;
18
18
  label: string;
19
19
  supportedEfforts: string[];
20
+ isDefault?: boolean | undefined;
21
+ defaultEffort?: string | undefined;
20
22
  }[];
21
23
  accessOptions: {
22
24
  id: string;
@@ -54,14 +54,17 @@ export class RunnerService {
54
54
  profiles: await this.refreshProfiles(workspaceId, environment),
55
55
  defaults: this.database()
56
56
  .getRunnerDefaults()
57
- .filter((value) => this.runners.supportsConfiguration(value))
57
+ .filter((value) => this.runners.supportsConfiguration(value, undefined, environment))
58
58
  };
59
59
  }
60
60
  saveDefaults(data) {
61
+ const environment = parseSecretEnvironment(typeof data === 'object' && data !== null
62
+ ? data.secretEnvironment
63
+ : undefined);
61
64
  const parsed = runnerDefaultConfigurationSchema.safeParse(data);
62
65
  if (!parsed.success)
63
66
  throw new Error('RUNNER_DEFAULT_INVALID');
64
- if (!this.runners.supportsConfiguration(parsed.data))
67
+ if (!this.runners.supportsConfiguration(parsed.data, undefined, environment))
65
68
  throw new Error('RUNNER_CONFIGURATION_UNSUPPORTED');
66
69
  return { defaults: this.database().saveRunnerDefaults(parsed.data) };
67
70
  }
@@ -1,5 +1,6 @@
1
1
  import { validSessionEffort, validSessionOption, validSessionTitle } from '../util/node-operation-parsers.js';
2
2
  import { isPlainRecord } from '../util/node-operation-parsers.js';
3
+ import { parseSecretEnvironment } from '../util/secret-environment.js';
3
4
  export class SessionLifecycleService {
4
5
  options;
5
6
  starts = new Map();
@@ -75,18 +76,19 @@ export class SessionLifecycleService {
75
76
  const workspace = database.getWorkspace(input.workspaceId);
76
77
  if (workspace === undefined)
77
78
  throw new Error('WORKSPACE_NOT_FOUND');
79
+ const environment = parseSecretEnvironment(input.secretEnvironment);
78
80
  const saved = database
79
81
  .getRunnerDefaults()
80
82
  .find((value) => value.runner === runner.name &&
81
- this.options.runners.supportsConfiguration(value, workspace));
82
- const fallback = runner.defaultConfiguration(workspace);
83
+ this.options.runners.supportsConfiguration(value, workspace, environment));
84
+ const fallback = runner.defaultConfiguration(workspace, environment);
83
85
  const configuration = {
84
86
  runner: runner.name,
85
87
  model: typeof input.model === 'string' ? input.model : (saved?.model ?? fallback.model),
86
88
  effort: typeof input.effort === 'string' ? input.effort : (saved?.effort ?? fallback.effort),
87
89
  access: typeof input.access === 'string' ? input.access : (saved?.access ?? fallback.access)
88
90
  };
89
- if (!this.options.runners.supportsConfiguration(configuration, workspace))
91
+ if (!this.options.runners.supportsConfiguration(configuration, workspace, environment))
90
92
  throw new Error('RUNNER_CONFIGURATION_UNSUPPORTED');
91
93
  const session = this.options.runtime.createAgentSession({
92
94
  workspaceId: input.workspaceId,
@@ -125,8 +127,9 @@ export class SessionLifecycleService {
125
127
  access: input.access
126
128
  };
127
129
  const workspace = this.options.database().getWorkspace(current.workspaceId);
130
+ const environment = parseSecretEnvironment(input.secretEnvironment);
128
131
  if (workspace === undefined ||
129
- !this.options.runners.supportsConfiguration(configuration, workspace))
132
+ !this.options.runners.supportsConfiguration(configuration, workspace, environment))
130
133
  throw new Error('RUNNER_CONFIGURATION_UNSUPPORTED');
131
134
  const session = this.options.runtime.updateAgentSessionConfiguration({
132
135
  sessionId: current.id,
@@ -33,6 +33,8 @@ export declare class WorkbenchManifestService {
33
33
  id: string;
34
34
  label: string;
35
35
  supportedEfforts: string[];
36
+ isDefault?: boolean | undefined;
37
+ defaultEffort?: string | undefined;
36
38
  }[];
37
39
  accessOptions: {
38
40
  id: string;
@@ -1,6 +1,7 @@
1
1
  import type { NodeOperationHandler } from '../connector/node-operation-router.js';
2
2
  import type { NodeRuntimeState } from '../runtime-state.js';
3
3
  import type { RunnerName } from '@myagentroam/protocol';
4
+ import type { NodeMcpInstallation } from '../database.js';
4
5
  export interface QueuedRunStart<TAttachment = unknown> {
5
6
  readonly runId: string;
6
7
  readonly sessionId: string;
@@ -28,6 +29,7 @@ export interface WorkspaceQueueWorkbenchServiceOptions {
28
29
  readonly stopped: () => boolean;
29
30
  readonly markInterrupted: (runId: string) => void;
30
31
  readonly prepareInput: (runner: RunnerName, input: string, collaborationMode: 'default' | 'plan') => string;
32
+ readonly mcps: (workspaceId: string) => readonly NodeMcpInstallation[];
31
33
  }
32
34
  export declare class WorkspaceQueueWorkbenchService<TAttachment> {
33
35
  private readonly options;
@@ -178,7 +178,8 @@ export class WorkspaceQueueWorkbenchService {
178
178
  ...(pending.access === null ? {} : { access: pending.access }),
179
179
  collaborationMode: pending.collaborationMode,
180
180
  ...(pending.serviceTier === undefined ? {} : { serviceTier: pending.serviceTier }),
181
- secretEnvironment: pending.secretEnvironment
181
+ secretEnvironment: pending.secretEnvironment,
182
+ mcpInstallations: this.options.mcps(pending.workspaceId)
182
183
  });
183
184
  }
184
185
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.1.8",
3
+ "version": "0.9.0",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "type": "module",
6
6
  "files": [
@@ -24,7 +24,7 @@
24
24
  "node-pty": "1.1.0",
25
25
  "ws": "^8.21.3",
26
26
  "zod": "4.4.3",
27
- "@myagentroam/protocol": "^0.1.8"
27
+ "@myagentroam/protocol": "^0.9.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/ws": "^8.18.1"