@myagentroam/node 0.1.7 → 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.
- package/dist/capabilities.js +6 -3
- package/dist/claude-agent-sdk.d.ts +1 -0
- package/dist/claude-agent-sdk.js +8 -1
- package/dist/codex-app-server.d.ts +6 -0
- package/dist/codex-app-server.js +6 -0
- package/dist/config.d.ts +3 -5
- package/dist/config.js +9 -20
- package/dist/connector/node-connector-options.d.ts +1 -0
- package/dist/connector.js +12 -3
- package/dist/database.d.ts +23 -0
- package/dist/database.js +84 -2
- package/dist/main.js +4 -1
- package/dist/migrations/v004.d.ts +4 -0
- package/dist/migrations/v004.js +35 -0
- package/dist/opencode-server.d.ts +1 -0
- package/dist/opencode-server.js +13 -1
- package/dist/operational.js +4 -1
- package/dist/rotating-log.d.ts +17 -0
- package/dist/rotating-log.js +67 -0
- package/dist/runner/abstract-runner.d.ts +12 -2
- package/dist/runner/abstract-runner.js +76 -2
- package/dist/runner/claude/managed-run-controller.js +2 -1
- package/dist/runner/claude-code-runner.d.ts +1 -0
- package/dist/runner/claude-code-runner.js +14 -0
- package/dist/runner/codex/managed-run-controller.js +5 -3
- package/dist/runner/codex-runner.d.ts +16 -2
- package/dist/runner/codex-runner.js +215 -4
- package/dist/runner/opencode/managed-run-controller.js +13 -3
- package/dist/runner/opencode-runner.d.ts +2 -1
- package/dist/runner/opencode-runner.js +23 -3
- package/dist/runner/runner-registry.d.ts +1 -1
- package/dist/runner/runner-registry.js +2 -2
- package/dist/runner-profiles.js +5 -25
- package/dist/runtime-command-detector.d.ts +3 -0
- package/dist/runtime-command-detector.js +78 -0
- package/dist/service/mcp-installation-verifier.d.ts +9 -0
- package/dist/service/mcp-installation-verifier.js +87 -0
- package/dist/service/mcp-node-operation-service.d.ts +11 -0
- package/dist/service/mcp-node-operation-service.js +90 -0
- package/dist/service/mcp-package-installer.d.ts +8 -0
- package/dist/service/mcp-package-installer.js +136 -0
- package/dist/service/node-connection-lifecycle-service.js +1 -2
- package/dist/service/runner-service.d.ts +4 -2
- package/dist/service/runner-service.js +5 -2
- package/dist/service/session-lifecycle-service.js +7 -4
- package/dist/service/skill-directory-service.d.ts +17 -0
- package/dist/service/skill-directory-service.js +203 -0
- package/dist/service/skill-install-service.d.ts +21 -0
- package/dist/service/skill-install-service.js +504 -0
- package/dist/service/skill-node-operation-service.d.ts +44 -0
- package/dist/service/skill-node-operation-service.js +203 -0
- package/dist/service/workbench-manifest-service.d.ts +4 -2
- package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
- package/dist/service/workspace-queue-workbench-service.js +2 -1
- package/dist/supervisor.js +1 -20
- package/package.json +2 -2
package/dist/capabilities.js
CHANGED
|
@@ -3,6 +3,7 @@ import nodePackage from '../package.json' with { type: 'json' };
|
|
|
3
3
|
import { probeClaudeCode, probeClaudeCodeAsync } from './claude-agent-sdk.js';
|
|
4
4
|
import { probeCodex, probeCodexAsync } from './codex-app-server.js';
|
|
5
5
|
import { probeOpenCode, probeOpenCodeAsync } from './opencode-runtime.js';
|
|
6
|
+
import { detectRuntimeCommands } from './runtime-command-detector.js';
|
|
6
7
|
const nodeAppVersion = typeof nodePackage.version === 'string' ? nodePackage.version : 'unknown';
|
|
7
8
|
function reportedPlatform() {
|
|
8
9
|
return process.platform === 'win32' ? 'windows' : 'linux';
|
|
@@ -59,10 +60,11 @@ export async function detectCapabilitiesAsync(commands = {}) {
|
|
|
59
60
|
if (process.platform !== 'win32' && process.platform !== 'linux') {
|
|
60
61
|
throw new Error('NODE_PLATFORM_UNSUPPORTED');
|
|
61
62
|
}
|
|
62
|
-
const [codex, claudeCode, openCode] = await Promise.all([
|
|
63
|
+
const [codex, claudeCode, openCode, runtimeCommands] = await Promise.all([
|
|
63
64
|
probeCodexAsync(commands.codex),
|
|
64
65
|
probeClaudeCodeAsync(commands.claudeCode),
|
|
65
|
-
probeOpenCodeAsync(commands.openCode)
|
|
66
|
+
probeOpenCodeAsync(commands.openCode),
|
|
67
|
+
detectRuntimeCommands()
|
|
66
68
|
]);
|
|
67
69
|
return {
|
|
68
70
|
platform: reportedPlatform(),
|
|
@@ -71,7 +73,8 @@ export async function detectCapabilitiesAsync(commands = {}) {
|
|
|
71
73
|
nodeVersion: process.version,
|
|
72
74
|
metadata: {
|
|
73
75
|
osRelease: release(),
|
|
74
|
-
osVersion: version()
|
|
76
|
+
osVersion: version(),
|
|
77
|
+
runtimeCommands
|
|
75
78
|
},
|
|
76
79
|
codex: {
|
|
77
80
|
available: codex.available && codex.compatible,
|
|
@@ -22,6 +22,7 @@ export interface ClaudeQueryInput {
|
|
|
22
22
|
readonly onMessage: (message: SDKMessage) => void;
|
|
23
23
|
readonly onChannelReply?: (content: string) => void;
|
|
24
24
|
readonly environment?: Readonly<Record<string, string>>;
|
|
25
|
+
readonly mcpServers?: NonNullable<Options['mcpServers']>;
|
|
25
26
|
}
|
|
26
27
|
/** Image blocks accepted by the Claude Agent SDK message input. */
|
|
27
28
|
export interface ClaudeImageAttachment {
|
package/dist/claude-agent-sdk.js
CHANGED
|
@@ -66,7 +66,14 @@ export class ClaudeAgentSdkAdapter {
|
|
|
66
66
|
settings: { showThinkingSummaries: true },
|
|
67
67
|
abortController,
|
|
68
68
|
canUseTool: input.onPermission,
|
|
69
|
-
...(
|
|
69
|
+
...(input.mcpServers !== undefined || channelEnabled
|
|
70
|
+
? {
|
|
71
|
+
mcpServers: {
|
|
72
|
+
...(input.mcpServers ?? {}),
|
|
73
|
+
...(channelEnabled ? { myagentroam: channelMcpServer(input) } : {})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
: {})
|
|
70
77
|
}
|
|
71
78
|
});
|
|
72
79
|
const completed = consume(stream, input.onMessage);
|
|
@@ -49,6 +49,7 @@ export interface CodexRunConfiguration {
|
|
|
49
49
|
readonly access?: string | null;
|
|
50
50
|
readonly serviceTier?: 'fast';
|
|
51
51
|
readonly collaborationMode?: 'default' | 'plan';
|
|
52
|
+
readonly mcpServers?: unknown;
|
|
52
53
|
}
|
|
53
54
|
/** Public App Server turn inputs supported by the Workbench Composer. */
|
|
54
55
|
export type CodexComposerInput = {
|
|
@@ -84,6 +85,11 @@ export declare class CodexAppServerClient {
|
|
|
84
85
|
startThread(cwd: string, configuration?: CodexRunConfiguration): Promise<unknown>;
|
|
85
86
|
listThreads(): Promise<unknown>;
|
|
86
87
|
listCollaborationModes(): Promise<unknown>;
|
|
88
|
+
listModels(params: {
|
|
89
|
+
readonly cursor: string | null;
|
|
90
|
+
readonly limit: number;
|
|
91
|
+
readonly includeHidden: boolean;
|
|
92
|
+
}): Promise<unknown>;
|
|
87
93
|
readThread(threadId: string, includeTurns?: boolean): Promise<unknown>;
|
|
88
94
|
forkThread(threadId: string, cwd: string, lastTurnId: string, configuration?: CodexRunConfiguration): Promise<unknown>;
|
|
89
95
|
setThreadName(threadId: string, name: string): Promise<unknown>;
|
package/dist/codex-app-server.js
CHANGED
|
@@ -170,6 +170,9 @@ export class CodexAppServerClient {
|
|
|
170
170
|
listCollaborationModes() {
|
|
171
171
|
return this.request('collaborationMode/list', {});
|
|
172
172
|
}
|
|
173
|
+
listModels(params) {
|
|
174
|
+
return this.request('model/list', params);
|
|
175
|
+
}
|
|
173
176
|
readThread(threadId, includeTurns = true) {
|
|
174
177
|
return this.request('thread/read', { threadId, includeTurns });
|
|
175
178
|
}
|
|
@@ -263,6 +266,9 @@ export class CodexAppServerClient {
|
|
|
263
266
|
function codexThreadConfiguration(cwd, configuration) {
|
|
264
267
|
return {
|
|
265
268
|
...modelOption(configuration.model),
|
|
269
|
+
...(configuration.mcpServers === undefined
|
|
270
|
+
? {}
|
|
271
|
+
: { config: { mcp_servers: configuration.mcpServers } }),
|
|
266
272
|
...codexPermissionConfiguration(cwd, configuration.access, 'thread')
|
|
267
273
|
};
|
|
268
274
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -2,19 +2,17 @@ export interface NodeConfig {
|
|
|
2
2
|
readonly serverUrl: string;
|
|
3
3
|
/** Explicit roots or the platform default resolved while loading the config. */
|
|
4
4
|
readonly allowedRoots: readonly string[];
|
|
5
|
-
/** Absolute or config-relative
|
|
6
|
-
readonly
|
|
5
|
+
/** Absolute or config-relative root for SQLite, upgrades and other private Node data. */
|
|
6
|
+
readonly dataDirectory?: string;
|
|
7
7
|
readonly nodeId?: string;
|
|
8
8
|
readonly credential?: string;
|
|
9
9
|
readonly registrationToken?: string;
|
|
10
|
-
readonly upgrade?: {
|
|
11
|
-
readonly registryUrl?: string;
|
|
12
|
-
};
|
|
13
10
|
}
|
|
14
11
|
export declare function nodeConfigPath(): string;
|
|
15
12
|
export declare function loadNodeConfig(path?: string): Promise<NodeConfig>;
|
|
16
13
|
/** Uses the Node service account's scope when no Workspace roots are configured. */
|
|
17
14
|
export declare function defaultAllowedRoots(platform?: NodeJS.Platform, homePath?: string, currentDirectory?: string): readonly string[];
|
|
18
15
|
export declare function nodeDatabasePath(config: NodeConfig, configPath?: string): string;
|
|
16
|
+
export declare function nodeDataDirectory(config: NodeConfig, configPath?: string): string;
|
|
19
17
|
/** Credentials are always written with owner-only POSIX permissions where supported. */
|
|
20
18
|
export declare function saveNodeConfig(config: NodeConfig, path?: string): Promise<void>;
|
package/dist/config.js
CHANGED
|
@@ -10,6 +10,7 @@ export async function loadNodeConfig(path = nodeConfigPath()) {
|
|
|
10
10
|
parsed === null ||
|
|
11
11
|
!('serverUrl' in parsed) ||
|
|
12
12
|
typeof parsed.serverUrl !== 'string' ||
|
|
13
|
+
'databasePath' in parsed ||
|
|
13
14
|
('allowedRoots' in parsed &&
|
|
14
15
|
(!Array.isArray(parsed.allowedRoots) ||
|
|
15
16
|
!parsed.allowedRoots.every((root) => typeof root === 'string')))) {
|
|
@@ -20,15 +21,7 @@ export async function loadNodeConfig(path = nodeConfigPath()) {
|
|
|
20
21
|
const nodeId = optionalString('nodeId');
|
|
21
22
|
const credential = optionalString('credential');
|
|
22
23
|
const registrationToken = optionalString('registrationToken');
|
|
23
|
-
const
|
|
24
|
-
const upgradeInput = input['upgrade'];
|
|
25
|
-
if (upgradeInput !== undefined &&
|
|
26
|
-
(upgradeInput === null ||
|
|
27
|
-
typeof upgradeInput !== 'object' ||
|
|
28
|
-
Array.isArray(upgradeInput) ||
|
|
29
|
-
('registryUrl' in upgradeInput &&
|
|
30
|
-
typeof upgradeInput['registryUrl'] !== 'string')))
|
|
31
|
-
throw new Error('NODE_CONFIG_INVALID');
|
|
24
|
+
const dataDirectory = optionalString('dataDirectory');
|
|
32
25
|
const allowedRoots = Array.isArray(input['allowedRoots'])
|
|
33
26
|
? input['allowedRoots']
|
|
34
27
|
: defaultAllowedRoots();
|
|
@@ -38,16 +31,7 @@ export async function loadNodeConfig(path = nodeConfigPath()) {
|
|
|
38
31
|
...(nodeId === undefined ? {} : { nodeId }),
|
|
39
32
|
...(credential === undefined ? {} : { credential }),
|
|
40
33
|
...(registrationToken === undefined ? {} : { registrationToken }),
|
|
41
|
-
...(
|
|
42
|
-
...(upgradeInput === undefined
|
|
43
|
-
? {}
|
|
44
|
-
: {
|
|
45
|
-
upgrade: {
|
|
46
|
-
...(upgradeInput['registryUrl'] === undefined
|
|
47
|
-
? {}
|
|
48
|
-
: { registryUrl: upgradeInput['registryUrl'] })
|
|
49
|
-
}
|
|
50
|
-
})
|
|
34
|
+
...(dataDirectory === undefined ? {} : { dataDirectory })
|
|
51
35
|
};
|
|
52
36
|
}
|
|
53
37
|
/** Uses the Node service account's scope when no Workspace roots are configured. */
|
|
@@ -55,7 +39,12 @@ export function defaultAllowedRoots(platform = process.platform, homePath = home
|
|
|
55
39
|
return platform === 'win32' ? [win32.parse(currentDirectory).root] : [homePath];
|
|
56
40
|
}
|
|
57
41
|
export function nodeDatabasePath(config, configPath = nodeConfigPath()) {
|
|
58
|
-
return
|
|
42
|
+
return resolve(nodeDataDirectory(config, configPath), 'node-runtime.sqlite');
|
|
43
|
+
}
|
|
44
|
+
export function nodeDataDirectory(config, configPath = nodeConfigPath()) {
|
|
45
|
+
if (config.dataDirectory !== undefined)
|
|
46
|
+
return resolve(dirname(configPath), config.dataDirectory);
|
|
47
|
+
return resolve(dirname(configPath));
|
|
59
48
|
}
|
|
60
49
|
/** Credentials are always written with owner-only POSIX permissions where supported. */
|
|
61
50
|
export async function saveNodeConfig(config, path = nodeConfigPath()) {
|
|
@@ -16,6 +16,7 @@ export interface NodeConnectorOptions {
|
|
|
16
16
|
readonly fakeRunner?: FakeRunner;
|
|
17
17
|
readonly fakeScript?: (input: unknown, runId: string) => readonly FakeRunnerStep[];
|
|
18
18
|
readonly codexClient?: CodexAppServerClient;
|
|
19
|
+
readonly codexProfileClientFactory?: () => CodexAppServerClient;
|
|
19
20
|
readonly claudeClient?: ClaudeAgentSdkAdapter;
|
|
20
21
|
readonly openCodeClient?: OpenCodeServerClient;
|
|
21
22
|
readonly database?: NodeDatabase;
|
package/dist/connector.js
CHANGED
|
@@ -18,6 +18,8 @@ import { RunnerRegistry } from './runner/runner-registry.js';
|
|
|
18
18
|
import { OpenCodeRunner } from './runner/opencode-runner.js';
|
|
19
19
|
import { OpenCodeManagedRunController } from './runner/opencode/managed-run-controller.js';
|
|
20
20
|
import { NodeOperationRouter } from './connector/node-operation-router.js';
|
|
21
|
+
import { SkillNodeOperationService } from './service/skill-node-operation-service.js';
|
|
22
|
+
import { McpNodeOperationService } from './service/mcp-node-operation-service.js';
|
|
21
23
|
import { RunnerService } from './service/runner-service.js';
|
|
22
24
|
import { TerminalService } from './service/terminal-service.js';
|
|
23
25
|
import { WorkspaceService } from './service/workspace-service.js';
|
|
@@ -229,7 +231,7 @@ export class NodeConnector {
|
|
|
229
231
|
options.fakeRunner === undefined ? undefined : new FakeTestRunner(options.fakeRunner);
|
|
230
232
|
this.fakeScript = options.fakeScript;
|
|
231
233
|
this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoot(session));
|
|
232
|
-
this.codexClient = new CodexRunner(options.codexClient);
|
|
234
|
+
this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory);
|
|
233
235
|
this.codexRunController = new CodexManagedRunController(this.codexClient, {
|
|
234
236
|
available: () => this.capabilities.codex.available,
|
|
235
237
|
intercepted: () => this.fakeRunner !== undefined,
|
|
@@ -313,9 +315,9 @@ export class NodeConnector {
|
|
|
313
315
|
commandStates: (sessionId) => this.commandStates.list(sessionId)
|
|
314
316
|
});
|
|
315
317
|
this.runners = new RunnerRegistry([
|
|
318
|
+
this.openCodeClient,
|
|
316
319
|
this.codexClient,
|
|
317
320
|
this.claudeClient,
|
|
318
|
-
this.openCodeClient,
|
|
319
321
|
...(this.fakeRunner === undefined ? [] : [this.fakeRunner])
|
|
320
322
|
]);
|
|
321
323
|
this.managedRunnerSessions = new ManagedRunnerSessionService(this.runtime, this.runners, (session) => this.emitWorkbenchEvent('session', {
|
|
@@ -403,7 +405,8 @@ export class NodeConnector {
|
|
|
403
405
|
control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
|
|
404
406
|
stopped: () => this.stopped,
|
|
405
407
|
markInterrupted: (runId) => this.runState.interruptRequested.add(runId),
|
|
406
|
-
prepareInput: (runner, input, collaborationMode) => this.runners.require(runner).prepareMessageInput(input, collaborationMode)
|
|
408
|
+
prepareInput: (runner, input, collaborationMode) => this.runners.require(runner).prepareMessageInput(input, collaborationMode),
|
|
409
|
+
mcps: (workspaceId) => this.database?.effectiveMcpInstallations(workspaceId) ?? []
|
|
407
410
|
});
|
|
408
411
|
this.sessionIdentityService = new SessionIdentityService({
|
|
409
412
|
runtime: this.runtime,
|
|
@@ -708,6 +711,12 @@ export class NodeConnector {
|
|
|
708
711
|
this.operationRouter.registerAll(this.sessionCatalogService.operations());
|
|
709
712
|
this.operationRouter.registerAll(this.sessionCommandService.operations());
|
|
710
713
|
this.operationRouter.registerAll(this.workspaceQueueWorkbenchService.operations());
|
|
714
|
+
this.operationRouter.registerAll(new SkillNodeOperationService(requireDatabase, () => {
|
|
715
|
+
if (this.config === undefined)
|
|
716
|
+
throw new Error('NODE_CONFIG_UNAVAILABLE');
|
|
717
|
+
return this.config;
|
|
718
|
+
}).operations());
|
|
719
|
+
this.operationRouter.registerAll(new McpNodeOperationService(requireDatabase).operations());
|
|
711
720
|
this.operationRouter.register('node.metrics', () => ({
|
|
712
721
|
metrics: {
|
|
713
722
|
...this.metrics.snapshot(),
|
package/dist/database.d.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import { type AgentRunStatus, type RunnerDefaultConfiguration, type RunnerName } from '@myagentroam/protocol';
|
|
3
|
+
import { type McpCatalogEntry, type McpInstallationConfiguration, type McpRuntimeDescriptor } from '@myagentroam/protocol';
|
|
3
4
|
import type { NativeSessionHistory } from './native-session-history.js';
|
|
5
|
+
export interface NodeMcpInstallation {
|
|
6
|
+
readonly targetKind: 'NODE' | 'WORKSPACE';
|
|
7
|
+
readonly targetId: string;
|
|
8
|
+
readonly catalogEntryId: string;
|
|
9
|
+
readonly serverName: string;
|
|
10
|
+
readonly displayName: string;
|
|
11
|
+
readonly version: string;
|
|
12
|
+
readonly enabled: boolean;
|
|
13
|
+
readonly runtime: McpRuntimeDescriptor;
|
|
14
|
+
readonly configuration: McpInstallationConfiguration;
|
|
15
|
+
readonly installedAt: number;
|
|
16
|
+
}
|
|
4
17
|
export type RunRecoveryState = 'ACTIVE' | 'SUSPECT' | 'LOST';
|
|
5
18
|
export type ApprovalStatus = 'PENDING' | 'APPROVED' | 'REJECTED' | 'EXPIRED';
|
|
6
19
|
/** Whether a native session ID was created by this Node or merely discovered. */
|
|
@@ -153,6 +166,16 @@ export declare class NodeDatabase {
|
|
|
153
166
|
close(): void;
|
|
154
167
|
getRunnerDefaults(): readonly RunnerDefaultConfiguration[];
|
|
155
168
|
saveRunnerDefaults(input: RunnerDefaultConfiguration): RunnerDefaultConfiguration;
|
|
169
|
+
listMcpInstallations(targetKind: 'NODE' | 'WORKSPACE', targetId?: string): readonly NodeMcpInstallation[];
|
|
170
|
+
saveMcpInstallation(input: {
|
|
171
|
+
readonly targetKind: 'NODE' | 'WORKSPACE';
|
|
172
|
+
readonly targetId?: string;
|
|
173
|
+
readonly entry: McpCatalogEntry;
|
|
174
|
+
readonly enabled: boolean;
|
|
175
|
+
readonly configuration: NodeMcpInstallation['configuration'];
|
|
176
|
+
}): NodeMcpInstallation;
|
|
177
|
+
removeMcpInstallation(targetKind: 'NODE' | 'WORKSPACE', targetId: string, serverName: string): boolean;
|
|
178
|
+
effectiveMcpInstallations(workspaceId: string): readonly NodeMcpInstallation[];
|
|
156
179
|
/**
|
|
157
180
|
* Stores only terminal lifecycle metadata. PTY input/output must remain in
|
|
158
181
|
* the Node-local bounded replay buffer and must never be passed here.
|
package/dist/database.js
CHANGED
|
@@ -3,10 +3,12 @@ import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
|
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
5
|
import { runnerDefaultConfigurationSchema } from '@myagentroam/protocol';
|
|
6
|
+
import { mcpCatalogEntrySchema, mcpInstallationConfigurationSchema, mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
|
|
6
7
|
import { assertNodeSchema, NODE_SCHEMA_V1 } from './migrations/v001.js';
|
|
7
8
|
import { assertNodeSchemaV2, migrateNodeSchemaV2 } from './migrations/v002.js';
|
|
8
|
-
import { assertNodeSchemaV3, migrateNodeSchemaV3
|
|
9
|
-
|
|
9
|
+
import { assertNodeSchemaV3, migrateNodeSchemaV3 } from './migrations/v003.js';
|
|
10
|
+
import { assertNodeSchemaV4, migrateNodeSchemaV4, NODE_SCHEMA_VERSION_V4 } from './migrations/v004.js';
|
|
11
|
+
const NODE_SCHEMA_VERSION = NODE_SCHEMA_VERSION_V4;
|
|
10
12
|
export class NodeDatabase {
|
|
11
13
|
now;
|
|
12
14
|
activeWorkspaceLeaseCount() {
|
|
@@ -63,6 +65,57 @@ export class NodeDatabase {
|
|
|
63
65
|
.run(input.runner, input.model, input.effort, input.access);
|
|
64
66
|
return input;
|
|
65
67
|
}
|
|
68
|
+
listMcpInstallations(targetKind, targetId = '') {
|
|
69
|
+
const rows = this.raw
|
|
70
|
+
.prepare(`SELECT target_kind,target_id,catalog_entry_id,server_name,display_name,version,
|
|
71
|
+
enabled,runtime,configuration,installed_at
|
|
72
|
+
FROM mcp_installations WHERE target_kind=? AND target_id=? ORDER BY display_name,server_name`)
|
|
73
|
+
.all(targetKind, targetKind === 'NODE' ? '' : targetId);
|
|
74
|
+
return rows.map(mcpInstallation);
|
|
75
|
+
}
|
|
76
|
+
saveMcpInstallation(input) {
|
|
77
|
+
const entry = mcpCatalogEntrySchema.parse(input.entry);
|
|
78
|
+
const configuration = mcpInstallationConfigurationSchema.parse(input.configuration);
|
|
79
|
+
const targetId = input.targetKind === 'NODE' ? '' : (input.targetId ?? '');
|
|
80
|
+
if (input.targetKind === 'WORKSPACE' && this.getWorkspace(targetId) === undefined)
|
|
81
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
82
|
+
const installedAt = this.now();
|
|
83
|
+
this.raw
|
|
84
|
+
.prepare(`INSERT INTO mcp_installations
|
|
85
|
+
(target_kind,target_id,catalog_entry_id,server_name,display_name,version,enabled,runtime,configuration,installed_at)
|
|
86
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
87
|
+
ON CONFLICT(target_kind,target_id,server_name) DO UPDATE SET
|
|
88
|
+
catalog_entry_id=excluded.catalog_entry_id,display_name=excluded.display_name,
|
|
89
|
+
version=excluded.version,enabled=excluded.enabled,runtime=excluded.runtime,
|
|
90
|
+
configuration=excluded.configuration,installed_at=excluded.installed_at`)
|
|
91
|
+
.run(input.targetKind, targetId, entry.id, entry.serverName, entry.displayName, entry.version, input.enabled ? 1 : 0, JSON.stringify(entry.runtime), JSON.stringify(configuration), installedAt);
|
|
92
|
+
return {
|
|
93
|
+
targetKind: input.targetKind,
|
|
94
|
+
targetId,
|
|
95
|
+
catalogEntryId: entry.id,
|
|
96
|
+
serverName: entry.serverName,
|
|
97
|
+
displayName: entry.displayName,
|
|
98
|
+
version: entry.version,
|
|
99
|
+
enabled: input.enabled,
|
|
100
|
+
runtime: entry.runtime,
|
|
101
|
+
configuration,
|
|
102
|
+
installedAt
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
removeMcpInstallation(targetKind, targetId, serverName) {
|
|
106
|
+
return (Number(this.raw
|
|
107
|
+
.prepare('DELETE FROM mcp_installations WHERE target_kind=? AND target_id=? AND server_name=?')
|
|
108
|
+
.run(targetKind, targetKind === 'NODE' ? '' : targetId, serverName).changes) > 0);
|
|
109
|
+
}
|
|
110
|
+
effectiveMcpInstallations(workspaceId) {
|
|
111
|
+
const effective = new Map(this.listMcpInstallations('NODE').map((installation) => [
|
|
112
|
+
installation.serverName,
|
|
113
|
+
installation
|
|
114
|
+
]));
|
|
115
|
+
for (const installation of this.listMcpInstallations('WORKSPACE', workspaceId))
|
|
116
|
+
effective.set(installation.serverName, installation);
|
|
117
|
+
return [...effective.values()].filter((installation) => installation.enabled);
|
|
118
|
+
}
|
|
66
119
|
/**
|
|
67
120
|
* Stores only terminal lifecycle metadata. PTY input/output must remain in
|
|
68
121
|
* the Node-local bounded replay buffer and must never be passed here.
|
|
@@ -138,6 +191,9 @@ export class NodeDatabase {
|
|
|
138
191
|
// record itself never represents a filesystem deletion.
|
|
139
192
|
this.raw.prepare('DELETE FROM agent_sessions WHERE workspace_id = ?').run(id);
|
|
140
193
|
this.raw.prepare('DELETE FROM legacy_agent_sessions WHERE workspace_id = ?').run(id);
|
|
194
|
+
this.raw
|
|
195
|
+
.prepare("DELETE FROM mcp_installations WHERE target_kind='WORKSPACE' AND target_id = ?")
|
|
196
|
+
.run(id);
|
|
141
197
|
this.raw.prepare('DELETE FROM workspaces WHERE id = ?').run(id);
|
|
142
198
|
this.raw.exec('COMMIT');
|
|
143
199
|
return current;
|
|
@@ -1028,6 +1084,8 @@ function migrate(database) {
|
|
|
1028
1084
|
database.exec('COMMIT');
|
|
1029
1085
|
migrateNodeSchemaV3(database);
|
|
1030
1086
|
assertNodeSchemaV3(database);
|
|
1087
|
+
migrateNodeSchemaV4(database);
|
|
1088
|
+
assertNodeSchemaV4(database);
|
|
1031
1089
|
return;
|
|
1032
1090
|
}
|
|
1033
1091
|
catch (error) {
|
|
@@ -1066,6 +1124,8 @@ function migrate(database) {
|
|
|
1066
1124
|
assertNodeSchemaV2(database);
|
|
1067
1125
|
migrateNodeSchemaV3(database);
|
|
1068
1126
|
assertNodeSchemaV3(database);
|
|
1127
|
+
migrateNodeSchemaV4(database);
|
|
1128
|
+
assertNodeSchemaV4(database);
|
|
1069
1129
|
return;
|
|
1070
1130
|
}
|
|
1071
1131
|
if (current === 0) {
|
|
@@ -1092,7 +1152,29 @@ function migrate(database) {
|
|
|
1092
1152
|
.get();
|
|
1093
1153
|
if (numberValue(afterV2, 'version') === 2)
|
|
1094
1154
|
migrateNodeSchemaV3(database);
|
|
1155
|
+
const afterV3 = database
|
|
1156
|
+
.prepare('SELECT MAX(version) AS version FROM node_schema_migrations')
|
|
1157
|
+
.get();
|
|
1158
|
+
if (numberValue(afterV3, 'version') === 3)
|
|
1159
|
+
migrateNodeSchemaV4(database);
|
|
1095
1160
|
assertNodeSchemaV3(database);
|
|
1161
|
+
assertNodeSchemaV4(database);
|
|
1162
|
+
}
|
|
1163
|
+
function mcpInstallation(row) {
|
|
1164
|
+
const runtime = mcpRuntimeDescriptorSchema.parse(JSON.parse(stringValue(row, 'runtime')));
|
|
1165
|
+
const configuration = mcpInstallationConfigurationSchema.parse(JSON.parse(stringValue(row, 'configuration')));
|
|
1166
|
+
return {
|
|
1167
|
+
targetKind: stringValue(row, 'target_kind'),
|
|
1168
|
+
targetId: stringValue(row, 'target_id'),
|
|
1169
|
+
catalogEntryId: stringValue(row, 'catalog_entry_id'),
|
|
1170
|
+
serverName: stringValue(row, 'server_name'),
|
|
1171
|
+
displayName: stringValue(row, 'display_name'),
|
|
1172
|
+
version: stringValue(row, 'version'),
|
|
1173
|
+
enabled: numberValue(row, 'enabled') === 1,
|
|
1174
|
+
runtime,
|
|
1175
|
+
configuration,
|
|
1176
|
+
installedAt: numberValue(row, 'installed_at')
|
|
1177
|
+
};
|
|
1096
1178
|
}
|
|
1097
1179
|
function sessionMetadata(row) {
|
|
1098
1180
|
const raw = JSON.parse(stringValue(row, 'metadata'));
|
package/dist/main.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { NodeConnector } from './connector.js';
|
|
4
|
-
import { loadNodeConfig, nodeConfigPath, nodeDatabasePath } from './config.js';
|
|
4
|
+
import { loadNodeConfig, nodeConfigPath, nodeDatabasePath, nodeDataDirectory } from './config.js';
|
|
5
5
|
import { getNodeHealth } from './health.js';
|
|
6
6
|
import { removeServiceDefinition, writeServiceDefinition } from './service.js';
|
|
7
7
|
import { superviseNode } from './supervisor.js';
|
|
8
|
+
import { configureNodeLog } from './rotating-log.js';
|
|
8
9
|
const [command = 'status', ...args] = process.argv.slice(2);
|
|
9
10
|
const config = readOption(args, '--config') ?? nodeConfigPath();
|
|
10
11
|
const output = readOption(args, '--output') ?? defaultServicePath();
|
|
11
12
|
if (command === 'run') {
|
|
13
|
+
configureNodeLog(nodeDataDirectory(await loadNodeConfig(config), config));
|
|
12
14
|
const connector = new NodeConnector({ configPath: config });
|
|
13
15
|
await connector.start();
|
|
14
16
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
@@ -18,6 +20,7 @@ if (command === 'run') {
|
|
|
18
20
|
else if (command === 'supervise') {
|
|
19
21
|
if (process.argv[1] === undefined)
|
|
20
22
|
throw new Error('NODE_ENTRYPOINT_UNAVAILABLE');
|
|
23
|
+
configureNodeLog(nodeDataDirectory(await loadNodeConfig(config), config));
|
|
21
24
|
await superviseNode(config, process.argv[1]);
|
|
22
25
|
}
|
|
23
26
|
else if (command === 'install') {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const NODE_SCHEMA_VERSION_V4 = 4;
|
|
2
|
+
export function migrateNodeSchemaV4(database) {
|
|
3
|
+
database.exec('BEGIN IMMEDIATE');
|
|
4
|
+
try {
|
|
5
|
+
database.exec(`CREATE TABLE mcp_installations (
|
|
6
|
+
target_kind TEXT NOT NULL CHECK (target_kind IN ('NODE','WORKSPACE')),
|
|
7
|
+
target_id TEXT NOT NULL,
|
|
8
|
+
catalog_entry_id TEXT NOT NULL,
|
|
9
|
+
server_name TEXT NOT NULL,
|
|
10
|
+
display_name TEXT NOT NULL,
|
|
11
|
+
version TEXT NOT NULL,
|
|
12
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),
|
|
13
|
+
runtime TEXT NOT NULL CHECK (json_valid(runtime)),
|
|
14
|
+
configuration TEXT NOT NULL CHECK (json_valid(configuration)),
|
|
15
|
+
installed_at INTEGER NOT NULL,
|
|
16
|
+
PRIMARY KEY (target_kind,target_id,server_name),
|
|
17
|
+
CHECK ((target_kind='NODE' AND target_id='') OR (target_kind='WORKSPACE' AND target_id<>''))
|
|
18
|
+
) STRICT;`);
|
|
19
|
+
database
|
|
20
|
+
.prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
|
|
21
|
+
.run(NODE_SCHEMA_VERSION_V4, Date.now());
|
|
22
|
+
database.exec('COMMIT');
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
database.exec('ROLLBACK');
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function assertNodeSchemaV4(database) {
|
|
30
|
+
const row = database
|
|
31
|
+
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='mcp_installations'")
|
|
32
|
+
.get();
|
|
33
|
+
if (typeof row?.sql !== 'string' || !row.sql.includes("target_kind IN ('NODE','WORKSPACE')"))
|
|
34
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID:mcp_installations');
|
|
35
|
+
}
|
|
@@ -16,6 +16,7 @@ export declare class OpenCodeServerClient {
|
|
|
16
16
|
start(): Promise<void>;
|
|
17
17
|
private startServer;
|
|
18
18
|
providers(directory: string): Promise<OpenCodeProviderCatalog>;
|
|
19
|
+
configureMcp(directory: string, mcp: unknown): Promise<void>;
|
|
19
20
|
sessions(directory: string, limit?: number): Promise<readonly unknown[]>;
|
|
20
21
|
sessionStatus(directory: string): Promise<Readonly<Record<string, unknown>>>;
|
|
21
22
|
createSession(directory: string, title?: string): Promise<unknown>;
|
package/dist/opencode-server.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { createServer } from 'node:net';
|
|
4
|
-
import { createOpencodeClient } from '@opencode-ai/sdk/v2/client';
|
|
5
4
|
import { minimalRunnerEnvironment, platformCliInvocation } from './operational.js';
|
|
6
5
|
import { terminateProcessTree } from './process-tree.js';
|
|
7
6
|
const START_TIMEOUT_MS = 10_000;
|
|
@@ -35,6 +34,7 @@ export class OpenCodeServerClient {
|
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
36
|
async startServer() {
|
|
37
|
+
const { createOpencodeClient } = await import('@opencode-ai/sdk/v2/client');
|
|
38
38
|
const port = await availablePort();
|
|
39
39
|
const password = randomBytes(24).toString('base64url');
|
|
40
40
|
const invocation = platformCliInvocation(this.options.command ?? 'opencode', [
|
|
@@ -77,6 +77,18 @@ export class OpenCodeServerClient {
|
|
|
77
77
|
const client = await this.requireClient();
|
|
78
78
|
return unwrap(await client.provider.list({ directory }));
|
|
79
79
|
}
|
|
80
|
+
async configureMcp(directory, mcp) {
|
|
81
|
+
const client = await this.requireClient();
|
|
82
|
+
if (typeof mcp !== 'object' || mcp === null || Array.isArray(mcp))
|
|
83
|
+
return;
|
|
84
|
+
for (const [name, config] of Object.entries(mcp)) {
|
|
85
|
+
await client.mcp.add({
|
|
86
|
+
directory,
|
|
87
|
+
name,
|
|
88
|
+
config: config
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
80
92
|
async sessions(directory, limit = 100) {
|
|
81
93
|
const client = await this.requireClient();
|
|
82
94
|
return unwrap(await client.session.list({ directory, limit }));
|
package/dist/operational.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { writeNodeLog } from './rotating-log.js';
|
|
2
3
|
const secretKey = /(?:password|credential|token|cookie|authorization|secret|api[_-]?key)/i;
|
|
3
4
|
export function runRunnerProbe(command, args, options) {
|
|
4
5
|
return new Promise((resolve) => {
|
|
@@ -115,5 +116,7 @@ export function platformCliInvocation(command, args, platform = process.platform
|
|
|
115
116
|
}
|
|
116
117
|
export function nodeLog(event, fields = {}) {
|
|
117
118
|
const safeFields = redactLogValue(fields);
|
|
118
|
-
|
|
119
|
+
const line = `${JSON.stringify({ timestamp: new Date().toISOString(), level: 'info', component: 'mar-node', event, ...safeFields })}\n`;
|
|
120
|
+
process.stdout.write(line);
|
|
121
|
+
writeNodeLog(line);
|
|
119
122
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function configureNodeLog(dataDirectory: string): void;
|
|
2
|
+
export declare function writeNodeLog(content: string): void;
|
|
3
|
+
export declare class RotatingNodeLog {
|
|
4
|
+
private readonly maxBytes;
|
|
5
|
+
private readonly retentionMs;
|
|
6
|
+
private readonly now;
|
|
7
|
+
private readonly activePath;
|
|
8
|
+
private bytes;
|
|
9
|
+
private day;
|
|
10
|
+
private rotation;
|
|
11
|
+
private lastCleanup;
|
|
12
|
+
constructor(directory: string, maxBytes?: number, retentionMs?: number, now?: () => number);
|
|
13
|
+
write(content: string): void;
|
|
14
|
+
private currentDay;
|
|
15
|
+
private rotate;
|
|
16
|
+
private cleanup;
|
|
17
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
4
|
+
let sink;
|
|
5
|
+
export function configureNodeLog(dataDirectory) {
|
|
6
|
+
sink = new RotatingNodeLog(resolve(dataDirectory, 'logs'));
|
|
7
|
+
}
|
|
8
|
+
export function writeNodeLog(content) {
|
|
9
|
+
sink?.write(content);
|
|
10
|
+
}
|
|
11
|
+
export class RotatingNodeLog {
|
|
12
|
+
maxBytes;
|
|
13
|
+
retentionMs;
|
|
14
|
+
now;
|
|
15
|
+
activePath;
|
|
16
|
+
bytes = 0;
|
|
17
|
+
day = '';
|
|
18
|
+
rotation = 0;
|
|
19
|
+
lastCleanup = 0;
|
|
20
|
+
constructor(directory, maxBytes = 100 * 1024 * 1024, retentionMs = 7 * DAY_MS, now = Date.now) {
|
|
21
|
+
this.maxBytes = maxBytes;
|
|
22
|
+
this.retentionMs = retentionMs;
|
|
23
|
+
this.now = now;
|
|
24
|
+
mkdirSync(directory, { recursive: true });
|
|
25
|
+
this.activePath = resolve(directory, 'node.log');
|
|
26
|
+
try {
|
|
27
|
+
const stat = statSync(this.activePath);
|
|
28
|
+
this.bytes = stat.size;
|
|
29
|
+
this.day = new Date(stat.mtimeMs).toISOString().slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
this.day = this.currentDay();
|
|
33
|
+
}
|
|
34
|
+
this.cleanup();
|
|
35
|
+
}
|
|
36
|
+
write(content) {
|
|
37
|
+
const length = Buffer.byteLength(content);
|
|
38
|
+
const day = this.currentDay();
|
|
39
|
+
if (this.bytes > 0 && (this.bytes + length > this.maxBytes || day !== this.day))
|
|
40
|
+
this.rotate();
|
|
41
|
+
appendFileSync(this.activePath, content, { encoding: 'utf8', mode: 0o600 });
|
|
42
|
+
this.bytes += length;
|
|
43
|
+
this.day = day;
|
|
44
|
+
if (this.now() - this.lastCleanup >= DAY_MS)
|
|
45
|
+
this.cleanup();
|
|
46
|
+
}
|
|
47
|
+
currentDay() {
|
|
48
|
+
return new Date(this.now()).toISOString().slice(0, 10);
|
|
49
|
+
}
|
|
50
|
+
rotate() {
|
|
51
|
+
const suffix = `${this.day}-${this.now()}-${String(this.rotation++).padStart(3, '0')}`;
|
|
52
|
+
renameSync(this.activePath, resolve(this.activePath, '..', `node-${suffix}.log`));
|
|
53
|
+
this.bytes = 0;
|
|
54
|
+
}
|
|
55
|
+
cleanup() {
|
|
56
|
+
const directory = resolve(this.activePath, '..');
|
|
57
|
+
const cutoff = this.now() - this.retentionMs;
|
|
58
|
+
for (const entry of readdirSync(directory)) {
|
|
59
|
+
if (!entry.startsWith('node-') || !entry.endsWith('.log'))
|
|
60
|
+
continue;
|
|
61
|
+
const path = resolve(directory, entry);
|
|
62
|
+
if (statSync(path).mtimeMs < cutoff)
|
|
63
|
+
unlinkSync(path);
|
|
64
|
+
}
|
|
65
|
+
this.lastCleanup = this.now();
|
|
66
|
+
}
|
|
67
|
+
}
|