@myagentroam/node 0.1.8 → 0.9.2

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 (79) 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.d.ts +2 -0
  8. package/dist/connector.js +43 -14
  9. package/dist/database.d.ts +23 -0
  10. package/dist/database.js +84 -2
  11. package/dist/migrations/v004.d.ts +4 -0
  12. package/dist/migrations/v004.js +35 -0
  13. package/dist/native-session-history.js +72 -14
  14. package/dist/opencode-server.d.ts +1 -0
  15. package/dist/opencode-server.js +12 -0
  16. package/dist/runner/abstract-runner.d.ts +13 -3
  17. package/dist/runner/abstract-runner.js +78 -4
  18. package/dist/runner/claude/managed-run-controller.js +18 -7
  19. package/dist/runner/claude-code-runner.d.ts +1 -0
  20. package/dist/runner/claude-code-runner.js +17 -4
  21. package/dist/runner/codex/conversation-parser.d.ts +2 -2
  22. package/dist/runner/codex/conversation-parser.js +30 -12
  23. package/dist/runner/codex/managed-run-controller.js +11 -8
  24. package/dist/runner/codex-runner.d.ts +17 -3
  25. package/dist/runner/codex-runner.js +225 -12
  26. package/dist/runner/opencode/conversation-parser.d.ts +11 -7
  27. package/dist/runner/opencode/conversation-parser.js +15 -15
  28. package/dist/runner/opencode/managed-run-controller.d.ts +2 -1
  29. package/dist/runner/opencode/managed-run-controller.js +36 -12
  30. package/dist/runner/opencode-runner.d.ts +2 -1
  31. package/dist/runner/opencode-runner.js +23 -3
  32. package/dist/runner/runner-registry.d.ts +1 -1
  33. package/dist/runner/runner-registry.js +2 -2
  34. package/dist/runner-command-engine.js +1 -1
  35. package/dist/runner-profiles.js +29 -41
  36. package/dist/runner-usage.js +5 -5
  37. package/dist/runtime-command-detector.d.ts +3 -0
  38. package/dist/runtime-command-detector.js +78 -0
  39. package/dist/service/conversation-history-service.js +55 -24
  40. package/dist/service/conversation-segment-service.d.ts +20 -0
  41. package/dist/service/conversation-segment-service.js +155 -0
  42. package/dist/service/mcp-installation-verifier.d.ts +9 -0
  43. package/dist/service/mcp-installation-verifier.js +87 -0
  44. package/dist/service/mcp-node-operation-service.d.ts +11 -0
  45. package/dist/service/mcp-node-operation-service.js +90 -0
  46. package/dist/service/mcp-package-installer.d.ts +8 -0
  47. package/dist/service/mcp-package-installer.js +136 -0
  48. package/dist/service/native-session-watch-service.d.ts +1 -0
  49. package/dist/service/native-session-watch-service.js +21 -4
  50. package/dist/service/node-request-service.js +2 -1
  51. package/dist/service/run-attachment-service.d.ts +3 -2
  52. package/dist/service/run-attachment-service.js +21 -11
  53. package/dist/service/run-event-service.d.ts +1 -0
  54. package/dist/service/run-event-service.js +8 -2
  55. package/dist/service/runner-service.d.ts +2 -0
  56. package/dist/service/runner-service.js +5 -2
  57. package/dist/service/session-catalog-service.js +4 -4
  58. package/dist/service/session-lifecycle-service.js +7 -4
  59. package/dist/service/session-presentation-service.d.ts +1 -0
  60. package/dist/service/session-presentation-service.js +8 -1
  61. package/dist/service/session-query-service.d.ts +4 -0
  62. package/dist/service/session-query-service.js +6 -1
  63. package/dist/service/skill-directory-service.js +3 -26
  64. package/dist/service/skill-install-service.js +4 -74
  65. package/dist/service/skill-node-operation-service.js +2 -1
  66. package/dist/service/workbench-manifest-service.d.ts +2 -0
  67. package/dist/service/workspace-git-exclude-service.d.ts +4 -0
  68. package/dist/service/workspace-git-exclude-service.js +119 -0
  69. package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
  70. package/dist/service/workspace-queue-workbench-service.js +5 -3
  71. package/dist/service/workspace-service.js +2 -0
  72. package/dist/supervisor.js +2 -1
  73. package/dist/terminal.js +1 -1
  74. package/dist/util/node-operation-parsers.js +2 -2
  75. package/dist/util/runner-native-session-parsers.d.ts +2 -3
  76. package/dist/util/runner-native-session-parsers.js +5 -14
  77. package/dist/util/safe-error.d.ts +2 -0
  78. package/dist/util/safe-error.js +5 -0
  79. package/package.json +2 -2
@@ -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 {
@@ -66,7 +66,14 @@ export class ClaudeAgentSdkAdapter {
66
66
  settings: { showThinkingSummaries: true },
67
67
  abortController,
68
68
  canUseTool: input.onPermission,
69
- ...(channelEnabled ? { mcpServers: { myagentroam: channelMcpServer(input) } } : {})
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>;
@@ -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
  }
@@ -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;
@@ -27,6 +27,7 @@ export declare class NodeConnector {
27
27
  private readonly sessionIdentityService;
28
28
  private readonly nativeSessionProjectionService;
29
29
  private readonly conversationHistoryService;
30
+ private readonly conversationSegmentService;
30
31
  private readonly runnerInteractionService;
31
32
  private readonly runnerChannelMessages;
32
33
  private readonly managedRunnerSessions;
@@ -78,6 +79,7 @@ export declare class NodeConnector {
78
79
  private readonly handleMessage;
79
80
  private readonly emitRunEvent;
80
81
  private readonly emitConversationItem;
82
+ private readonly emitConversationTurn;
81
83
  private readonly emitWorkbenchEvent;
82
84
  private readonly discoverWorkspaceSessions;
83
85
  private readonly invalidateWorkspaceWatchTopics;
package/dist/connector.js CHANGED
@@ -19,6 +19,7 @@ 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
21
  import { SkillNodeOperationService } from './service/skill-node-operation-service.js';
22
+ import { McpNodeOperationService } from './service/mcp-node-operation-service.js';
22
23
  import { RunnerService } from './service/runner-service.js';
23
24
  import { TerminalService } from './service/terminal-service.js';
24
25
  import { WorkspaceService } from './service/workspace-service.js';
@@ -43,6 +44,7 @@ import { WorkspaceQueueWorkbenchService } from './service/workspace-queue-workbe
43
44
  import { SessionIdentityService } from './service/session-identity-service.js';
44
45
  import { NativeSessionProjectionService } from './service/native-session-projection-service.js';
45
46
  import { ConversationHistoryService, hasManagedActiveRun } from './service/conversation-history-service.js';
47
+ import { ConversationSegmentService, conversationSegmentIdentityForItem, conversationSegments } from './service/conversation-segment-service.js';
46
48
  import { RunnerInteractionService } from './service/runner-interaction-service.js';
47
49
  import { SessionLifecycleService } from './service/session-lifecycle-service.js';
48
50
  import { ExternalSessionResumeService } from './service/external-session-resume-service.js';
@@ -50,7 +52,7 @@ import { SessionMessageService } from './service/session-message-service.js';
50
52
  import { SessionCatalogService } from './service/session-catalog-service.js';
51
53
  import { SessionCommandService } from './service/session-command-service.js';
52
54
  import { SessionPresentationService } from './service/session-presentation-service.js';
53
- import { isRunnerImageAttachment, parseRunnerImageAttachments } from './runner/codex/conversation-parser.js';
55
+ import { isPlainRecord, isRunnerImageAttachment, parseRunnerImageAttachments } from './runner/codex/conversation-parser.js';
54
56
  export { nodeConnectEndpoint, validatedCapabilities } from './runner/codex/conversation-parser.js';
55
57
  import { NodeControlChannel } from './service/node-control-channel.js';
56
58
  import { NodeControlMessageService } from './service/node-control-message-service.js';
@@ -113,6 +115,7 @@ export class NodeConnector {
113
115
  sessionIdentityService;
114
116
  nativeSessionProjectionService;
115
117
  conversationHistoryService;
118
+ conversationSegmentService;
116
119
  runnerInteractionService;
117
120
  runnerChannelMessages;
118
121
  managedRunnerSessions;
@@ -172,7 +175,12 @@ export class NodeConnector {
172
175
  isBootstrapSession: (sessionId) => this.sessionIdentityService.bootstrapIds.has(sessionId),
173
176
  presentSession: (session) => this.sessionPresentationService.present(session),
174
177
  rejectBootstrap: (sessionId, code) => this.sessionIdentityService.reject(sessionId, code),
175
- emitWorkbench: (category, payload) => this.emitWorkbenchEvent(category, payload),
178
+ emitWorkbench: (category, payload) => {
179
+ if (category === 'conversation' && isPlainRecord(payload) && 'turn' in payload)
180
+ this.emitConversationTurn(payload.turn);
181
+ else
182
+ this.emitWorkbenchEvent(category, payload);
183
+ },
176
184
  cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
177
185
  expireImages: (runId) => this.runAttachmentService.expire(runId),
178
186
  scheduleContextRefresh: (sessionId) => this.sessionContextService.schedule(sessionId),
@@ -189,8 +197,9 @@ export class NodeConnector {
189
197
  refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
190
198
  present: (session) => this.sessionPresentationService.present(session),
191
199
  readPage: (session, limit) => this.conversationHistoryService.read(session, { limit }),
200
+ projectInitialPage: (session, page, _unit, limit) => this.conversationSegmentService.projectInitial(session, page, limit),
192
201
  emitSession: (session) => this.emitWorkbenchEvent('session', { session }),
193
- emitTurn: (turn) => this.emitWorkbenchEvent('conversation', { turn })
202
+ emitTurn: (turn) => this.emitConversationTurn(turn)
194
203
  });
195
204
  constructor(options = {}) {
196
205
  this.config = options.config;
@@ -229,8 +238,8 @@ export class NodeConnector {
229
238
  this.fakeRunner =
230
239
  options.fakeRunner === undefined ? undefined : new FakeTestRunner(options.fakeRunner);
231
240
  this.fakeScript = options.fakeScript;
232
- this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoot(session));
233
- this.codexClient = new CodexRunner(options.codexClient);
241
+ this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoots(session));
242
+ this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory);
234
243
  this.codexRunController = new CodexManagedRunController(this.codexClient, {
235
244
  available: () => this.capabilities.codex.available,
236
245
  intercepted: () => this.fakeRunner !== undefined,
@@ -251,7 +260,7 @@ export class NodeConnector {
251
260
  cancel: (runId, threadId, turnId, cwd, status) => this.runnerInteractionService.cancelCodexTurn(runId, threadId, turnId, cwd, status),
252
261
  createApproval: (input) => this.runtime.createApproval(input),
253
262
  publishApproval: (input) => this.runEventBridge.publishApproval(input),
254
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
263
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
255
264
  appendImages: (runId, images) => this.runAttachmentService.append(runId, images.filter(isRunnerImageAttachment)),
256
265
  captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
257
266
  syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
@@ -272,8 +281,9 @@ export class NodeConnector {
272
281
  intercepted: () => this.fakeRunner !== undefined,
273
282
  active: this.runState.active,
274
283
  parseAttachments: parseRunnerImageAttachments,
284
+ persistAttachments: (cwd, runId, attachments) => this.runAttachmentService.persistFiles(cwd, `${runId}-opencode-images`, attachments),
275
285
  emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
276
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
286
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
277
287
  adopt: (input) => this.runtime.adoptRun(input),
278
288
  promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
279
289
  rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
@@ -305,7 +315,7 @@ export class NodeConnector {
305
315
  rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
306
316
  promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
307
317
  send: (type, payload) => this.send(type, payload),
308
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
318
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
309
319
  syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
310
320
  setChannelToken: (sessionId, token) => this.runtime.setChannelToken(sessionId, token),
311
321
  commandState: (sessionId, commandId) => this.commandStates.get(sessionId, commandId),
@@ -329,7 +339,7 @@ export class NodeConnector {
329
339
  claude: () => this.claudeClient,
330
340
  runners: this.runners,
331
341
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
332
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
342
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
333
343
  captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
334
344
  approvalTimeoutMs: this.claudeApprovalTimeoutMs,
335
345
  publishApproval: (input) => this.runEventBridge.publishApproval(input)
@@ -367,6 +377,7 @@ export class NodeConnector {
367
377
  hasManagedActiveRun: (session) => hasManagedActiveRun(this.runtime, session),
368
378
  setExternalActivity: (sessionId, activity) => this.sessionState.externalActivity.set(sessionId, activity)
369
379
  });
380
+ this.conversationSegmentService = new ConversationSegmentService((session, input) => this.conversationHistoryService.read(session, input));
370
381
  this.sessionContextService = new SessionContextService({
371
382
  runners: this.runners,
372
383
  session: (sessionId) => this.runtime.getAgentSession(sessionId),
@@ -391,6 +402,7 @@ export class NodeConnector {
391
402
  resolve: (sessionId) => this.nativeSessionProjectionService.resolve(sessionId),
392
403
  present: (session) => this.sessionPresentationService.present(session),
393
404
  history: (session, input) => this.conversationHistoryService.read(session, input),
405
+ segments: (session, input) => this.conversationSegmentService.read(session, input),
394
406
  context: this.sessionContextService,
395
407
  discover: (runner, workspaceId) => this.nativeSessionProjectionService.discover(runner, workspaceId),
396
408
  workspaceIdForCwd: (cwd) => this.database?.listWorkspaces().find((candidate) => candidate.path === cwd)?.id
@@ -400,11 +412,12 @@ export class NodeConnector {
400
412
  emit: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
401
413
  clearImages: (runId) => this.runAttachmentService.clear(runId),
402
414
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
403
- emitConversation: (turn) => this.emitWorkbenchEvent('conversation', { turn }),
415
+ emitConversation: (turn) => this.emitConversationTurn(turn),
404
416
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
405
417
  stopped: () => this.stopped,
406
418
  markInterrupted: (runId) => this.runState.interruptRequested.add(runId),
407
- prepareInput: (runner, input, collaborationMode) => this.runners.require(runner).prepareMessageInput(input, collaborationMode)
419
+ prepareInput: (runner, input, collaborationMode) => this.runners.require(runner).prepareMessageInput(input, collaborationMode),
420
+ mcps: (workspaceId) => this.database?.effectiveMcpInstallations(workspaceId) ?? []
408
421
  });
409
422
  this.sessionIdentityService = new SessionIdentityService({
410
423
  runtime: this.runtime,
@@ -426,7 +439,7 @@ export class NodeConnector {
426
439
  emitSession: (session) => this.emitWorkbenchEvent('session', { session }),
427
440
  emitRun: (run) => this.emitWorkbenchEvent('run', { run }),
428
441
  emitQueue: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
429
- emitTurn: (turn) => this.emitWorkbenchEvent('conversation', { turn })
442
+ emitTurn: (turn) => this.emitConversationTurn(turn)
430
443
  });
431
444
  this.sessionLifecycleService = new SessionLifecycleService({
432
445
  database: () => {
@@ -594,7 +607,7 @@ export class NodeConnector {
594
607
  this.terminalChannel.disconnect();
595
608
  if (code === 4000 && reason === 'NODE_CONNECTION_REPLACED')
596
609
  nodeLog('node.connection.replaced', {
597
- message: '当前 Node 已被同一身份的新实例接管,请检查是否启动了重复实例。'
610
+ message: 'This Node connection was replaced by a newer instance with the same identity. Check for duplicate processes.'
598
611
  });
599
612
  if (code === 4003 || code === 1001)
600
613
  void this.terminalManager.shutdown();
@@ -714,6 +727,7 @@ export class NodeConnector {
714
727
  throw new Error('NODE_CONFIG_UNAVAILABLE');
715
728
  return this.config;
716
729
  }).operations());
730
+ this.operationRouter.registerAll(new McpNodeOperationService(requireDatabase).operations());
717
731
  this.operationRouter.register('node.metrics', () => ({
718
732
  metrics: {
719
733
  ...this.metrics.snapshot(),
@@ -778,7 +792,22 @@ export class NodeConnector {
778
792
  /** @internal Thin compatibility delegates; all behavior remains in the extracted services. */
779
793
  handleMessage = (raw) => this.controlMessages.handle(raw);
780
794
  emitRunEvent = (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status);
781
- emitConversationItem = (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item);
795
+ emitConversationItem = (runId, item) => {
796
+ this.runEventService.flushPendingText(runId);
797
+ const itemId = isPlainRecord(item) && typeof item.itemId === 'string' ? item.itemId : undefined;
798
+ const itemKind = isPlainRecord(item) && typeof item.kind === 'string' ? item.kind : undefined;
799
+ const turn = this.runtime.conversationTurnForRun(runId);
800
+ const segment = itemId === undefined || turn === undefined
801
+ ? undefined
802
+ : conversationSegmentIdentityForItem(turn, itemId, itemKind);
803
+ this.runEventBridge.emitRun(runId, 'conversation.item', segment === undefined || !isPlainRecord(item)
804
+ ? item
805
+ : { ...item, segmentId: segment.id, segmentIndex: segment.index });
806
+ };
807
+ emitConversationTurn = (turn) => {
808
+ const segments = isPlainRecord(turn) && Array.isArray(turn.items) ? conversationSegments(turn) : [];
809
+ this.emitWorkbenchEvent('conversation', { turn, segments });
810
+ };
782
811
  emitWorkbenchEvent = (category, payload) => this.runEventBridge.emitWorkbench(category, payload);
783
812
  discoverWorkspaceSessions = (workspaceId) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId);
784
813
  invalidateWorkspaceWatchTopics = (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId);
@@ -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, NODE_SCHEMA_VERSION_V3 } from './migrations/v003.js';
9
- const NODE_SCHEMA_VERSION = NODE_SCHEMA_VERSION_V3;
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'));
@@ -0,0 +1,4 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ export declare const NODE_SCHEMA_VERSION_V4 = 4;
3
+ export declare function migrateNodeSchemaV4(database: DatabaseSync): void;
4
+ export declare function assertNodeSchemaV4(database: DatabaseSync): void;
@@ -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
+ }