@dinoxx/dinox-cli 2.14.0 → 2.16.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 (97) hide show
  1. package/README.md +2 -0
  2. package/dist/ai/ragxClient.d.ts +15 -0
  3. package/dist/ai/ragxClient.js +71 -0
  4. package/dist/cli.js +27 -3
  5. package/dist/commands/batch.d.ts +35 -0
  6. package/dist/commands/batch.js +202 -0
  7. package/dist/commands/boxes/index.js +21 -21
  8. package/dist/commands/mcp.d.ts +6 -0
  9. package/dist/commands/mcp.js +47 -0
  10. package/dist/commands/notes/actions.js +18 -2
  11. package/dist/commands/notes/descriptors.js +111 -5
  12. package/dist/commands/notes/detailContent.d.ts +19 -0
  13. package/dist/commands/notes/detailContent.js +62 -0
  14. package/dist/commands/notes/export.js +4 -0
  15. package/dist/commands/notes/repo/search.d.ts +2 -1
  16. package/dist/commands/notes/repo/search.js +1 -1
  17. package/dist/commands/notes/repo/semantic.d.ts +14 -0
  18. package/dist/commands/notes/repo/semantic.js +31 -0
  19. package/dist/commands/notes/searchResponse.d.ts +3 -0
  20. package/dist/commands/notes/searchResponse.js +1 -0
  21. package/dist/commands/notes/semantic.d.ts +116 -0
  22. package/dist/commands/notes/semantic.js +332 -0
  23. package/dist/commands/prompt/index.js +7 -5
  24. package/dist/commands/savedViews/index.js +12 -3
  25. package/dist/commands/schema/globalOptions.d.ts +6 -0
  26. package/dist/commands/schema/globalOptions.js +6 -0
  27. package/dist/commands/schema/renderSkills.js +5 -2
  28. package/dist/commands/schema/spec.js +18 -7
  29. package/dist/commands/storage/index.js +7 -5
  30. package/dist/commands/sync.js +3 -3
  31. package/dist/commands/tags/index.js +21 -21
  32. package/dist/commands/todo/index.js +22 -2
  33. package/dist/commands/todo/repo.d.ts +5 -0
  34. package/dist/commands/todo/repo.js +7 -2
  35. package/dist/commands/writeReceipt.d.ts +13 -0
  36. package/dist/commands/writeReceipt.js +12 -0
  37. package/dist/config/serviceEndpoints.d.ts +2 -0
  38. package/dist/config/serviceEndpoints.js +2 -0
  39. package/dist/daemon/client.d.ts +2 -0
  40. package/dist/daemon/client.js +22 -2
  41. package/dist/daemon/forward.d.ts +4 -1
  42. package/dist/daemon/forward.js +13 -4
  43. package/dist/daemon/server.d.ts +1 -1
  44. package/dist/daemon/server.js +17 -1
  45. package/dist/daemon/types.d.ts +6 -0
  46. package/dist/dinox.js +12 -10
  47. package/dist/gateway/adapters/acp.d.ts +5 -1
  48. package/dist/gateway/adapters/acp.js +21 -9
  49. package/dist/gateway/adapters/echo.js +7 -4
  50. package/dist/gateway/adapters/index.d.ts +1 -0
  51. package/dist/gateway/adapters/index.js +5 -3
  52. package/dist/gateway/deviceHost.d.ts +3 -1
  53. package/dist/gateway/deviceHost.js +75 -5
  54. package/dist/gateway/mcpCommandTools.d.ts +15 -5
  55. package/dist/gateway/mcpCommandTools.js +30 -19
  56. package/dist/gateway/mcpEnv.d.ts +4 -0
  57. package/dist/gateway/mcpEnv.js +4 -0
  58. package/dist/gateway/mcpGroups.d.ts +3 -0
  59. package/dist/gateway/mcpGroups.js +4 -0
  60. package/dist/gateway/mcpServer.d.ts +10 -1
  61. package/dist/gateway/mcpServer.js +26 -5
  62. package/dist/gateway/protocol.d.ts +31 -2
  63. package/dist/gateway/protocol.js +15 -3
  64. package/dist/gateway/runExecutor.js +23 -12
  65. package/dist/gateway/service.d.ts +2 -0
  66. package/dist/gateway/service.js +29 -4
  67. package/dist/gateway/threadStore.d.ts +2 -0
  68. package/dist/gateway/threadStore.js +13 -0
  69. package/dist/gateway/types.d.ts +5 -4
  70. package/dist/gateway/workspace.d.ts +5 -0
  71. package/dist/gateway/workspace.js +30 -0
  72. package/dist/gateway/workspaceFolders.d.ts +5 -3
  73. package/dist/gateway/workspaceFolders.js +20 -7
  74. package/dist/gateway/workspaceListing.d.ts +5 -0
  75. package/dist/gateway/workspaceListing.js +78 -0
  76. package/dist/powersync/syncPass.js +13 -1
  77. package/dist/powersync/tokenIndex.d.ts +2 -0
  78. package/dist/powersync/tokenIndex.js +1 -0
  79. package/dist/requests/execute.d.ts +11 -0
  80. package/dist/requests/execute.js +26 -0
  81. package/dist/skills/commandRefs.js +2 -10
  82. package/dist/utils/cursor.d.ts +9 -0
  83. package/dist/utils/cursor.js +61 -0
  84. package/dist/utils/defaultFormat.d.ts +8 -0
  85. package/dist/utils/defaultFormat.js +38 -0
  86. package/dist/utils/errorCodes.d.ts +4 -0
  87. package/dist/utils/errorCodes.js +1 -0
  88. package/dist/utils/output.d.ts +2 -0
  89. package/dist/utils/output.js +41 -3
  90. package/dist/utils/progress.d.ts +25 -0
  91. package/dist/utils/progress.js +59 -0
  92. package/dist/utils/requestContext.d.ts +8 -0
  93. package/dist/utils/select.d.ts +20 -0
  94. package/dist/utils/select.js +92 -0
  95. package/dist/utils/usageError.d.ts +37 -0
  96. package/dist/utils/usageError.js +127 -0
  97. package/package.json +1 -1
@@ -140,7 +140,7 @@ export class AcpGatewayAdapter {
140
140
  };
141
141
  input.signal.addEventListener('abort', onAbort, { once: true });
142
142
  try {
143
- const text = created && input.sessionPreamble ? `${input.sessionPreamble}\n\n${input.prompt}` : input.prompt;
143
+ const text = await input.prompt({ created });
144
144
  const prompt = [{ type: 'text', text }];
145
145
  for (const resource of input.resources) {
146
146
  prompt.push({ type: 'resource_link', uri: resource.uri, name: resource.name, mimeType: resource.mediaType ?? null });
@@ -170,16 +170,23 @@ export class AcpGatewayAdapter {
170
170
  turn.sent += 1;
171
171
  return turn.sendFile(file);
172
172
  }
173
+ catalogExpiresAt = 0;
174
+ catalogRevision = 0;
175
+ probing = null;
173
176
  async describeConfig() {
174
- if (this.catalog)
177
+ if (this.catalog && Date.now() < this.catalogExpiresAt)
175
178
  return this.catalog;
179
+ if (!this.probing)
180
+ this.probing = this.probeConfig().finally(() => { this.probing = null; });
181
+ return this.probing;
182
+ }
183
+ async probeConfig() {
176
184
  const agent = await this.ensureProcess();
177
- if (this.catalog)
178
- return this.catalog;
179
185
  // No session yet: open one only to read the agent's choices. It never runs a turn,
180
186
  // so close it where the agent allows, or it shows up in the user's session list.
187
+ const revision = this.catalogRevision;
181
188
  const created = await agent.connection.newSession({ cwd: this.gateway.cwd ?? os.homedir(), mcpServers: [] });
182
- this.rememberConfig(agent, created.sessionId, created.configOptions);
189
+ this.rememberConfig(agent, created.sessionId, created.configOptions, revision === this.catalogRevision);
183
190
  if (agent.init.agentCapabilities?.sessionCapabilities?.close) {
184
191
  await agent.connection.closeSession({ sessionId: created.sessionId }).catch((error) => this.log('closing the settings session failed', { error: String(error) }));
185
192
  }
@@ -221,7 +228,7 @@ export class AcpGatewayAdapter {
221
228
  try {
222
229
  // No turn is registered yet, so the history the agent replays is ignored.
223
230
  const token = crypto.randomBytes(18).toString('base64url');
224
- const loaded = await agent.connection.loadSession({ sessionId, cwd, mcpServers: this.mcpServers(token) });
231
+ const loaded = await agent.connection.loadSession({ sessionId, cwd, mcpServers: this.mcpServers(token, cwd) });
225
232
  this.sessionTokens.set(token, sessionId);
226
233
  agent.liveSessions.add(sessionId);
227
234
  this.rememberConfig(agent, sessionId, loaded.configOptions);
@@ -232,7 +239,7 @@ export class AcpGatewayAdapter {
232
239
  }
233
240
  }
234
241
  const token = crypto.randomBytes(18).toString('base64url');
235
- const created = await agent.connection.newSession({ cwd, mcpServers: this.mcpServers(token) });
242
+ const created = await agent.connection.newSession({ cwd, mcpServers: this.mcpServers(token, cwd) });
236
243
  this.sessionTokens.set(token, created.sessionId);
237
244
  agent.liveSessions.add(created.sessionId);
238
245
  this.rememberConfig(agent, created.sessionId, created.configOptions);
@@ -260,12 +267,17 @@ export class AcpGatewayAdapter {
260
267
  }
261
268
  }
262
269
  }
263
- rememberConfig(agent, sessionId, options) {
270
+ rememberConfig(agent, sessionId, options, updateCatalog = true) {
264
271
  if (!options)
265
272
  return;
266
273
  const normalized = normalizeConfigOptions(options);
267
274
  agent.sessionConfig.set(sessionId, normalized);
268
- this.catalog = normalized;
275
+ // A slow probe must not overwrite a newer live session update.
276
+ if (updateCatalog) {
277
+ this.catalog = normalized;
278
+ this.catalogExpiresAt = Date.now() + 5 * 60_000;
279
+ this.catalogRevision += 1;
280
+ }
269
281
  }
270
282
  ensureProcess() {
271
283
  if (this.process)
@@ -24,18 +24,21 @@ export class EchoGatewayAdapter {
24
24
  async runTurn(input) {
25
25
  const sessionId = input.sessionId ?? `echo-${generateId()}`;
26
26
  try {
27
- if (input.prompt.includes('/approve')) {
27
+ const text = await input.prompt({ created: input.sessionId === null });
28
+ // Echo the user text while exercising the full prompt factory.
29
+ const prompt = text.replace(/<dinox_(delivery|knowledge|agent_instructions)>[\s\S]*?<\/dinox_\1>\n\n/g, '');
30
+ if (prompt.includes('/approve')) {
28
31
  const toolCallId = `tool-${generateId()}`;
29
32
  const approvalId = `approval-${generateId()}`;
30
- input.onEvent({ type: 'tool', toolCallId, title: 'echo.write', status: 'pending', input: { text: input.prompt } });
31
- input.onEvent({ type: 'approval-request', approvalId, toolCallId, toolName: 'echo.write', input: { text: input.prompt } });
33
+ input.onEvent({ type: 'tool', toolCallId, title: 'echo.write', status: 'pending', input: { text: prompt } });
34
+ input.onEvent({ type: 'approval-request', approvalId, toolCallId, toolName: 'echo.write', input: { text: prompt } });
32
35
  const approved = await input.awaitApproval(approvalId);
33
36
  input.onEvent({ type: 'approval-resolved', approvalId, approved });
34
37
  input.onEvent(approved
35
38
  ? { type: 'tool', toolCallId, status: 'completed', output: 'approved' }
36
39
  : { type: 'tool', toolCallId, status: 'failed', output: 'denied by user' });
37
40
  }
38
- for (const word of `Echo: ${input.prompt}`.split(/(\s+)/)) {
41
+ for (const word of `Echo: ${prompt}`.split(/(\s+)/)) {
39
42
  await delay(CHUNK_DELAY_MS, input.signal);
40
43
  input.onEvent({ type: 'text-delta', text: word });
41
44
  }
@@ -8,5 +8,6 @@ import type { GatewayAdapter, GatewayDefinition } from '../types.js';
8
8
  export declare function dinoxMcpServerConfig(options: {
9
9
  sessionToken: string;
10
10
  knowledgeTools: boolean;
11
+ cwd?: string;
11
12
  }): McpServer | null;
12
13
  export declare function createGatewayAdapter(gateway: GatewayDefinition, log: (message: string, details?: Record<string, unknown>) => void): GatewayAdapter;
@@ -1,4 +1,4 @@
1
- import { DINOX_MCP_SERVER_NAME, GATEWAY_KNOWLEDGE_TOOLS_ENV, GATEWAY_SESSION_TOKEN_ENV } from '../mcpEnv.js';
1
+ import { DINOX_MCP_SERVER_NAME, GATEWAY_SESSION_CWD_ENV, GATEWAY_KNOWLEDGE_TOOLS_ENV, GATEWAY_SESSION_TOKEN_ENV } from '../mcpEnv.js';
2
2
  import { AcpGatewayAdapter } from './acp.js';
3
3
  import { EchoGatewayAdapter } from './echo.js';
4
4
  /**
@@ -12,6 +12,7 @@ export function dinoxMcpServerConfig(options) {
12
12
  return null;
13
13
  const inherited = Object.entries(process.env).filter((entry) => entry[0].startsWith('DINOX_') &&
14
14
  entry[0] !== GATEWAY_SESSION_TOKEN_ENV &&
15
+ entry[0] !== GATEWAY_SESSION_CWD_ENV &&
15
16
  entry[0] !== GATEWAY_KNOWLEDGE_TOOLS_ENV &&
16
17
  typeof entry[1] === 'string');
17
18
  return {
@@ -20,6 +21,7 @@ export function dinoxMcpServerConfig(options) {
20
21
  args: [...process.execArgv, entrypoint, 'gateway', 'mcp'],
21
22
  env: [
22
23
  ...inherited,
24
+ [GATEWAY_SESSION_CWD_ENV, options.cwd ?? process.cwd()],
23
25
  [GATEWAY_SESSION_TOKEN_ENV, options.sessionToken],
24
26
  [GATEWAY_KNOWLEDGE_TOOLS_ENV, options.knowledgeTools ? '1' : '0'],
25
27
  ].map(([name, value]) => ({ name, value })),
@@ -30,8 +32,8 @@ export function createGatewayAdapter(gateway, log) {
30
32
  case 'acp':
31
33
  // Read per session, so turning the knowledge tools off applies to the next new conversation.
32
34
  // `send_file` stays offered either way: it is how the agent hands the user a file.
33
- return new AcpGatewayAdapter(gateway, log, (sessionToken) => {
34
- const tools = dinoxMcpServerConfig({ sessionToken, knowledgeTools: gateway.dinoxTools !== false });
35
+ return new AcpGatewayAdapter(gateway, log, (sessionToken, cwd) => {
36
+ const tools = dinoxMcpServerConfig({ sessionToken, cwd, knowledgeTools: gateway.dinoxTools !== false });
35
37
  return tools ? [tools] : [];
36
38
  });
37
39
  case 'echo':
@@ -77,8 +77,10 @@ export declare class GatewayDeviceHost {
77
77
  private publishRunEvent;
78
78
  private flushPendingEvents;
79
79
  private handleDiscover;
80
- /** Only the gateway's own queries are served; desktop queries (skills, workspace) are not advertised. */
80
+ /** Every reply-bearing query gets a result, including unsupported extensions. */
81
81
  private handleQuery;
82
+ private dispatchQuery;
83
+ private prepareFolder;
82
84
  private settingsResult;
83
85
  private changeSettings;
84
86
  private listFolders;
@@ -1,15 +1,17 @@
1
1
  import os from 'node:os';
2
+ import { listGatewayWorkspace } from './workspaceListing.js';
2
3
  import { credsAuthenticator, wsconnect, } from '@nats-io/nats-core';
4
+ import { AI_SERVICE_URL } from '../config/serviceEndpoints.js';
3
5
  import { toAuthorizationHeaderValue } from '../utils/authToken.js';
4
6
  import { REMOTE_DEVICE_PRESENCE_INTERVAL_MS, REMOTE_DEVICE_PROTOCOL_NAME, REMOTE_DEVICE_PROTOCOL_VERSION, remoteDeviceSubject, remoteDiscoverSubject, remotePresenceSubject, remoteRunStreamSubject, } from '../vendor/electron/shared/remote-device-agent.js';
5
7
  import { projectPendingApprovals } from './assistantProjection.js';
6
- import { isAgentConfigQuery, isAgentFoldersQuery, isAgentSettingsQuery, isCancelRequest, isRunStatusRequest, readAgentSettingsUpdate, reconnectDelayMs, validateApprovalRequest, validateRunRequest, } from './protocol.js';
8
+ import { isAgentConfigQuery, isAgentFolderPrepareQuery, isAgentFoldersQuery, isAgentSettingsQuery, isCancelRequest, isRunStatusRequest, readAgentSettingsUpdate, reconnectDelayMs, validateApprovalRequest, validateRunRequest, } from './protocol.js';
7
9
  import { GatewayRunExecutor, MAX_QUEUED_TURNS, QueueFullError } from './runExecutor.js';
8
10
  import { RunStreamPublisher } from './streamPublisher.js';
9
- import { findRun, readRecentSessionFolders, resolveTrustedResource, TERMINAL_RUN_STATUSES, threadExists, } from './threadStore.js';
11
+ import { findRun, readRecentSessionFolders, readSessionBinding, resolveTrustedResource, TERMINAL_RUN_STATUSES, threadExists, } from './threadStore.js';
10
12
  import { uploadToOfficialStorage } from './fileUpload.js';
11
13
  import { agentHomeFolder, ensureAgentHomeFolder, existingWorkspaceFolders, listWorkspaceFolders, resolveWorkspaceFolder, WorkspaceFolderError, } from './workspaceFolders.js';
12
- export const DEFAULT_NATS_CREDENTIALS_URL = 'https://aisdk.chatgo.pro/api/nats/credentials';
14
+ export const DEFAULT_NATS_CREDENTIALS_URL = `${AI_SERVICE_URL}/api/nats/credentials`;
13
15
  const CREDENTIALS_TIMEOUT_MS = 10_000;
14
16
  const CREDENTIALS_REFRESH_LEAD_MS = 60_000;
15
17
  const ACK_CACHE_TTL_MS = 10 * 60 * 1000;
@@ -239,6 +241,7 @@ export class GatewayDeviceHost {
239
241
  connectedAt: this.connectedAt ?? new Date().toISOString(),
240
242
  busyThreadIds: this.executor.busyThreadIds(),
241
243
  capabilities: {
244
+ workspace: { list: true, scope: 'thread', filePreview: false },
242
245
  threadRun: true,
243
246
  cancel: true,
244
247
  stream: 'assistant-snapshot',
@@ -249,6 +252,7 @@ export class GatewayDeviceHost {
249
252
  // Additive v2 extension: tells clients this device is an external agent
250
253
  // (the agent owns context; Dinox model/tool settings do not apply).
251
254
  info.capabilities.executor = {
255
+ agentWorkspace: true,
252
256
  kind: 'gateway',
253
257
  gatewayId: this.deps.gateway.id,
254
258
  runtime: this.deps.gateway.kind,
@@ -333,17 +337,55 @@ export class GatewayDeviceHost {
333
337
  return;
334
338
  msg.respond(encode({ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION, type: 'device.discovered', requestId, device: this.deviceInfo() }));
335
339
  }
336
- /** Only the gateway's own queries are served; desktop queries (skills, workspace) are not advertised. */
340
+ /** Every reply-bearing query gets a result, including unsupported extensions. */
337
341
  async handleQuery(msg) {
338
342
  const { value, byteLength } = decode(msg);
339
343
  if (!msg.reply)
340
344
  return;
345
+ try {
346
+ await this.dispatchQuery(msg, value, byteLength);
347
+ }
348
+ catch (error) {
349
+ const requestId = value?.requestId;
350
+ if (typeof requestId !== 'string')
351
+ return;
352
+ msg.respond(encode({
353
+ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION,
354
+ type: 'query.error', requestId, ok: false,
355
+ error: { code: 'query_failed', message: error instanceof Error ? error.message : String(error) },
356
+ }));
357
+ }
358
+ }
359
+ async dispatchQuery(msg, value, byteLength) {
360
+ const raw = value;
361
+ if (raw?.type === 'query.cancel')
362
+ return;
363
+ if (raw?.type === 'agent.folder.prepare' && typeof raw.requestId === 'string') {
364
+ msg.respond(encode(await this.prepareFolder(value, raw.requestId, byteLength)));
365
+ return;
366
+ }
367
+ if (raw?.type === 'workspace.list' && typeof raw.requestId === 'string') {
368
+ msg.respond(encode(await listGatewayWorkspace(this.deps.db, this.deps.userId, this.deps.gateway, value)));
369
+ return;
370
+ }
371
+ if ((raw?.type === 'workspace.file.read' || raw?.type === 'skills.list') && typeof raw.requestId === 'string') {
372
+ const base = { protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION, requestId: raw.requestId, threadId: String(raw.threadId ?? ''), ok: false, fetchedAt: new Date().toISOString(), error: { code: 'unsupported', message: 'This gateway does not support this query' } };
373
+ const result = raw.type === 'skills.list'
374
+ ? { ...base, type: 'skills.list.result', skills: [] }
375
+ : { ...base, type: 'workspace.file.read.result', path: String(raw.path ?? ''), preview: null, size: null };
376
+ msg.respond(encode(result));
377
+ return;
378
+ }
341
379
  if (isAgentConfigQuery(value)) {
342
380
  msg.respond(encode(await this.agentConfig(value)));
343
381
  return;
344
382
  }
345
383
  if (isAgentSettingsQuery(value)) {
346
- msg.respond(encode(this.settingsResult(value.requestId)));
384
+ const result = this.settingsResult(value.requestId);
385
+ if (value.threadId && await threadExists(this.deps.db, value.threadId, this.deps.userId)) {
386
+ result.threadCwd = (await readSessionBinding(this.deps.db, value.threadId, this.deps.gateway.id))?.cwd ?? null;
387
+ }
388
+ msg.respond(encode(result));
347
389
  return;
348
390
  }
349
391
  if (isAgentFoldersQuery(value)) {
@@ -356,7 +398,34 @@ export class GatewayDeviceHost {
356
398
  msg.respond(encode('error' in update
357
399
  ? this.settingsResult(requestId, { code: 'invalid_request', message: update.error })
358
400
  : await this.changeSettings(update.update)));
401
+ return;
402
+ }
403
+ if (typeof raw?.requestId === 'string')
404
+ msg.respond(encode({ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION, type: 'query.error', requestId: raw.requestId, ok: false, error: { code: 'unsupported', message: 'Unsupported gateway query' } }));
405
+ }
406
+ async prepareFolder(value, requestId, byteLength) {
407
+ const result = {
408
+ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION,
409
+ type: 'agent.folder.result',
410
+ requestId,
411
+ ok: false,
412
+ path: null,
413
+ home: os.homedir(),
414
+ fetchedAt: new Date().toISOString(),
415
+ };
416
+ if (!isAgentFolderPrepareQuery(value, byteLength)) {
417
+ return { ...result, error: { code: 'invalid_request', message: 'Choose exactly one path or agent name' } };
418
+ }
419
+ try {
420
+ result.path = value.path !== undefined
421
+ ? await resolveWorkspaceFolder(value.path)
422
+ : await ensureAgentHomeFolder(value.agentName, undefined, value.agentId);
423
+ result.ok = true;
424
+ }
425
+ catch (error) {
426
+ result.error = settingsError(error);
359
427
  }
428
+ return result;
360
429
  }
361
430
  settingsResult(requestId, error) {
362
431
  const gateway = this.deps.gateway;
@@ -365,6 +434,7 @@ export class GatewayDeviceHost {
365
434
  type: 'agent.settings.result',
366
435
  requestId,
367
436
  ok: !error,
437
+ threadCwd: null,
368
438
  settings: {
369
439
  cwd: gateway.cwd ?? null,
370
440
  approval: gateway.approval ?? 'ask',
@@ -3,16 +3,26 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { z } from 'zod';
4
4
  import type { CommandSchema } from '../commands/schema/types.js';
5
5
  import { tryForwardToDaemon } from '../daemon/forward.js';
6
+ import { MCP_COMMAND_GROUPS, type McpCommandGroup } from './mcpGroups.js';
6
7
  /**
7
8
  * One MCP tool per command group, generated from the command schemas. Each runs
8
9
  * the real CLI as a child process without a shell, preserving the CLI's own
9
10
  * validation, dry-run and confirmation guards. Approval belongs to the agent's
10
11
  * permission flow, guided by the tool annotations.
11
12
  */
12
- /** Command groups exposed as MCP tools. */
13
- export declare const MCP_COMMAND_GROUPS: readonly ["note", "tag", "box", "todo", "view", "prop", "template", "prompt", "graph", "storage", "meta.stats"];
13
+ export { MCP_COMMAND_GROUPS, type McpCommandGroup };
14
14
  /** Commands deliberately excluded from the MCP surface. */
15
- export declare const MCP_COMMAND_EXCLUSIONS: readonly ["auth.*", "config.*", "daemon.*", "gateway.*", "doctor", "skills.*", "update", "sync", "info", "schema", "meta.schema"];
15
+ export declare const MCP_COMMAND_EXCLUSIONS: readonly ["auth.*", "config.*", "daemon.*", "gateway.*", "doctor", "skills.*", "update", "sync", "info", "schema", "meta.schema", "batch", "mcp"];
16
+ export type CommandToolOptions = {
17
+ /** Only commands whose risk is `read`; groups left without any are dropped. */
18
+ readOnly?: boolean;
19
+ /** Subset of MCP_COMMAND_GROUPS to expose; default all. */
20
+ groups?: readonly McpCommandGroup[];
21
+ /** The curated search_notes/read_note/... tools are registered alongside. */
22
+ knowledgeTools?: boolean;
23
+ };
24
+ /** The tool name a command group is published under. */
25
+ export declare function commandToolName(group: McpCommandGroup): string;
16
26
  /** Runs a CLI invocation and captures its output. */
17
27
  export type CommandRunner = (argv: string[]) => Promise<{
18
28
  stdout: string;
@@ -30,7 +40,7 @@ export declare const runCliCommand: CommandRunner;
30
40
  /** Validates tool inputs and builds CLI arguments in schema order. */
31
41
  export declare function commandArgv(schema: CommandSchema, input: Record<string, unknown>): string[];
32
42
  /** Builds grouped MCP tool definitions from the command schemas. */
33
- export declare function commandToolDefinitions(): {
43
+ export declare function commandToolDefinitions(options?: CommandToolOptions): {
34
44
  name: string;
35
45
  commands: CommandSchema[];
36
46
  config: {
@@ -49,4 +59,4 @@ export declare function commandToolDefinitions(): {
49
59
  };
50
60
  }[];
51
61
  /** Registers command tools using the real CLI or an injected runner. */
52
- export declare function registerCommandTools(server: McpServer, runner?: CommandRunner): void;
62
+ export declare function registerCommandTools(server: McpServer, runner?: CommandRunner, options?: CommandToolOptions): void;
@@ -3,6 +3,7 @@ import { promisify } from 'node:util';
3
3
  import { z } from 'zod';
4
4
  import { listCommandSchemas } from '../commands/schema/spec.js';
5
5
  import { tryForwardToDaemon } from '../daemon/forward.js';
6
+ import { MCP_COMMAND_GROUPS } from './mcpGroups.js';
6
7
  import { requestContext } from '../utils/requestContext.js';
7
8
  /**
8
9
  * One MCP tool per command group, generated from the command schemas. Each runs
@@ -10,14 +11,19 @@ import { requestContext } from '../utils/requestContext.js';
10
11
  * validation, dry-run and confirmation guards. Approval belongs to the agent's
11
12
  * permission flow, guided by the tool annotations.
12
13
  */
13
- /** Command groups exposed as MCP tools. */
14
- export const MCP_COMMAND_GROUPS = [
15
- 'note', 'tag', 'box', 'todo', 'view', 'prop', 'template', 'prompt', 'graph', 'storage', 'meta.stats',
16
- ];
14
+ export { MCP_COMMAND_GROUPS };
17
15
  /** Commands deliberately excluded from the MCP surface. */
18
16
  export const MCP_COMMAND_EXCLUSIONS = [
19
17
  'auth.*', 'config.*', 'daemon.*', 'gateway.*', 'doctor', 'skills.*', 'update', 'sync', 'info', 'schema', 'meta.schema',
18
+ // MCP clients issue several tool calls themselves; batch is the shell's equivalent.
19
+ 'batch',
20
+ // The MCP server itself.
21
+ 'mcp',
20
22
  ];
23
+ /** The tool name a command group is published under. */
24
+ export function commandToolName(group) {
25
+ return group === 'meta.stats' ? 'dinox_stats' : `dinox_${group}`;
26
+ }
21
27
  const execute = promisify(execFile);
22
28
  /** Runs the current CLI entry point without a shell. */
23
29
  export function createCliCommandRunner(overrides = {}) {
@@ -169,28 +175,33 @@ export function commandArgv(schema, input) {
169
175
  return [...schema.path.split('.'), ...flags, '--format', 'json', '--', ...positionals];
170
176
  }
171
177
  /** Builds grouped MCP tool definitions from the command schemas. */
172
- export function commandToolDefinitions() {
173
- return MCP_COMMAND_GROUPS.map((group) => {
174
- const commands = listCommandSchemas().filter((schema) => group === 'meta.stats' ? schema.path === group : schema.path.startsWith(`${group}.`));
178
+ export function commandToolDefinitions(options = {}) {
179
+ const { readOnly = false, groups = MCP_COMMAND_GROUPS, knowledgeTools = true } = options;
180
+ return MCP_COMMAND_GROUPS.filter((group) => groups.includes(group)).flatMap((group) => {
181
+ const commands = listCommandSchemas().filter((schema) => (group === 'meta.stats' ? schema.path === group : schema.path.startsWith(`${group}.`))
182
+ && (!readOnly || schema.risk === 'read'));
183
+ if (commands.length === 0)
184
+ return [];
175
185
  const actions = commands.map(actionName);
176
- const description = 'Dinox CLI commands. search_notes/read_note/search_todos/related_notes are the fast daemon-backed path for plain search/read. ' +
186
+ const description = 'Dinox CLI commands. ' +
187
+ (knowledgeTools ? 'search_notes/read_note/search_todos/related_notes are the fast daemon-backed path for plain search/read. ' : '') +
177
188
  'High-risk writes need dryRun: true first, then the CLI confirmation options listed below (e.g. confirm/expectedCount); ' +
178
189
  'CLI guards enforce them. Input keys are camelCase.\n' +
179
190
  commands.map(describeAction).join('\n');
180
- return {
181
- name: group === 'meta.stats' ? 'dinox_stats' : `dinox_${group}`,
182
- commands,
183
- config: {
184
- description,
185
- annotations: { ...toolAnnotations(commands), openWorldHint: group === 'storage' },
186
- inputSchema: { action: z.enum(actions), input: z.record(z.string(), z.unknown()).optional() },
187
- },
188
- };
191
+ return [{
192
+ name: commandToolName(group),
193
+ commands,
194
+ config: {
195
+ description,
196
+ annotations: { ...toolAnnotations(commands), openWorldHint: group === 'storage' },
197
+ inputSchema: { action: z.enum(actions), input: z.record(z.string(), z.unknown()).optional() },
198
+ },
199
+ }];
189
200
  });
190
201
  }
191
202
  /** Registers command tools using the real CLI or an injected runner. */
192
- export function registerCommandTools(server, runner = runCliCommand) {
193
- for (const tool of commandToolDefinitions()) {
203
+ export function registerCommandTools(server, runner = runCliCommand, options = {}) {
204
+ for (const tool of commandToolDefinitions(options)) {
194
205
  server.registerTool(tool.name, tool.config, async ({ action, input }) => {
195
206
  try {
196
207
  const schema = tool.commands.find((command) => actionName(command) === action);
@@ -5,3 +5,7 @@ export declare const GATEWAY_SESSION_TOKEN_ENV = "DINOX_GATEWAY_SESSION_TOKEN";
5
5
  export declare const GATEWAY_KNOWLEDGE_TOOLS_ENV = "DINOX_GATEWAY_KNOWLEDGE_TOOLS";
6
6
  /** Must not collide with MCP servers the user already registered with the agent. */
7
7
  export declare const DINOX_MCP_SERVER_NAME = "dinox-gateway";
8
+ /** `dino mcp`, the server users register with their own MCP clients. */
9
+ export declare const DINOX_STANDALONE_MCP_SERVER_NAME = "dinox";
10
+ /** Session folder for resolving relative send_file paths. */
11
+ export declare const GATEWAY_SESSION_CWD_ENV = "DINOX_GATEWAY_SESSION_CWD";
@@ -5,3 +5,7 @@ export const GATEWAY_SESSION_TOKEN_ENV = 'DINOX_GATEWAY_SESSION_TOKEN';
5
5
  export const GATEWAY_KNOWLEDGE_TOOLS_ENV = 'DINOX_GATEWAY_KNOWLEDGE_TOOLS';
6
6
  /** Must not collide with MCP servers the user already registered with the agent. */
7
7
  export const DINOX_MCP_SERVER_NAME = 'dinox-gateway';
8
+ /** `dino mcp`, the server users register with their own MCP clients. */
9
+ export const DINOX_STANDALONE_MCP_SERVER_NAME = 'dinox';
10
+ /** Session folder for resolving relative send_file paths. */
11
+ export const GATEWAY_SESSION_CWD_ENV = 'DINOX_GATEWAY_SESSION_CWD';
@@ -0,0 +1,3 @@
1
+ /** Command groups exposed as MCP tools. Kept free of imports: command descriptors read it at load time. */
2
+ export declare const MCP_COMMAND_GROUPS: readonly ["note", "tag", "box", "todo", "view", "prop", "template", "prompt", "graph", "storage", "meta.stats"];
3
+ export type McpCommandGroup = (typeof MCP_COMMAND_GROUPS)[number];
@@ -0,0 +1,4 @@
1
+ /** Command groups exposed as MCP tools. Kept free of imports: command descriptors read it at load time. */
2
+ export const MCP_COMMAND_GROUPS = [
3
+ 'note', 'tag', 'box', 'todo', 'view', 'prop', 'template', 'prompt', 'graph', 'storage', 'meta.stats',
4
+ ];
@@ -1,4 +1,4 @@
1
- import { type CommandRunner } from './mcpCommandTools.js';
1
+ import { type CommandRunner, type CommandToolOptions } from './mcpCommandTools.js';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  export type DaemonCall = (method: string, params?: unknown, options?: {
4
4
  timeoutMs?: number;
@@ -13,4 +13,13 @@ export type DinoxMcpOptions = {
13
13
  cwd?: string;
14
14
  };
15
15
  export declare function createDinoxMcpServer(call?: DaemonCall, options?: DinoxMcpOptions): McpServer;
16
+ /**
17
+ * The Dinox MCP server for any MCP client (`dino mcp`). Only the generated
18
+ * command tools: they reach the daemon when it is available and otherwise run
19
+ * the CLI in a child process, so no daemon is required.
20
+ */
21
+ export declare function createStandaloneMcpServer(options?: Omit<CommandToolOptions, 'knowledgeTools'> & {
22
+ commandRunner?: CommandRunner;
23
+ }): McpServer;
16
24
  export declare function serveDinoxMcpOverStdio(): Promise<void>;
25
+ export declare function serveStandaloneMcpOverStdio(options: Omit<CommandToolOptions, 'knowledgeTools'>): Promise<void>;
@@ -4,7 +4,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
5
  import { z } from 'zod';
6
6
  import { sendDaemonRequest } from '../daemon/client.js';
7
- import { DINOX_MCP_SERVER_NAME, GATEWAY_KNOWLEDGE_TOOLS_ENV, GATEWAY_SESSION_TOKEN_ENV } from './mcpEnv.js';
7
+ import { DINOX_MCP_SERVER_NAME, DINOX_STANDALONE_MCP_SERVER_NAME, GATEWAY_SESSION_CWD_ENV, GATEWAY_KNOWLEDGE_TOOLS_ENV, GATEWAY_SESSION_TOKEN_ENV } from './mcpEnv.js';
8
8
  import { READ_CONVERSATION_MAX_LIMIT } from './conversationContext.js';
9
9
  import { getPackageVersion } from '../utils/version.js';
10
10
  /**
@@ -183,11 +183,32 @@ function registerKnowledgeTools(server, call) {
183
183
  }
184
184
  });
185
185
  }
186
+ const STANDALONE_INSTRUCTIONS = "Tools for the user's Dinox knowledge base (notes, todos, tags, card boxes, views, properties, templates, prompts, " +
187
+ 'the link graph and storage). Each dinox_* tool groups the CLI commands of one area: pick an `action` and pass its ' +
188
+ 'camelCase `input`. Prefer note search for keywords and note semantic/context for meaning-based questions. Results ' +
189
+ 'carry `_notice` (stale cache, pending upload) worth relaying. High-risk writes need dryRun: true first, then the ' +
190
+ 'confirmation options the action lists.';
191
+ /**
192
+ * The Dinox MCP server for any MCP client (`dino mcp`). Only the generated
193
+ * command tools: they reach the daemon when it is available and otherwise run
194
+ * the CLI in a child process, so no daemon is required.
195
+ */
196
+ export function createStandaloneMcpServer(options = {}) {
197
+ const server = new McpServer({ name: DINOX_STANDALONE_MCP_SERVER_NAME, version: getPackageVersion() }, { instructions: STANDALONE_INSTRUCTIONS + (options.readOnly ? ' This server is read-only.' : '') });
198
+ registerCommandTools(server, options.commandRunner, { ...options, knowledgeTools: false });
199
+ return server;
200
+ }
201
+ async function serveOverStdio(server) {
202
+ await server.connect(new StdioServerTransport());
203
+ await new Promise((resolve) => process.stdin.once('close', resolve));
204
+ }
186
205
  export async function serveDinoxMcpOverStdio() {
187
- const server = createDinoxMcpServer(undefined, {
206
+ await serveOverStdio(createDinoxMcpServer(undefined, {
207
+ cwd: process.env[GATEWAY_SESSION_CWD_ENV] || process.cwd(),
188
208
  knowledgeTools: process.env[GATEWAY_KNOWLEDGE_TOOLS_ENV] !== '0',
189
209
  sessionToken: process.env[GATEWAY_SESSION_TOKEN_ENV]?.trim() || undefined,
190
- });
191
- await server.connect(new StdioServerTransport());
192
- await new Promise((resolve) => process.stdin.once('close', resolve));
210
+ }));
211
+ }
212
+ export async function serveStandaloneMcpOverStdio(options) {
213
+ await serveOverStdio(createStandaloneMcpServer(options));
193
214
  }
@@ -20,8 +20,11 @@ export type AgentConfigResult = {
20
20
  };
21
21
  };
22
22
  export declare function isAgentConfigQuery(value: unknown): value is AgentConfigQuery;
23
+ export declare const MAX_PATH_LENGTH = 4096;
24
+ /** Host-side workspace choice, never forwarded as an ACP session option. */
25
+ export declare const AGENT_CWD_CONFIG_KEY = "dinox.cwd";
23
26
  export type AgentHostSettings = {
24
- /** Folder new conversations start in; null until chosen (the home folder is used). */
27
+ /** Fallback when neither the conversation nor its agent has chosen a folder. */
25
28
  cwd: string | null;
26
29
  approval: GatewayApprovalMode;
27
30
  /** Online but refusing new turns. */
@@ -32,7 +35,7 @@ export type AgentHostSettings = {
32
35
  export type AgentSettingsChange = {
33
36
  /** An existing folder (absolute or `~/...`), checked on this computer. */
34
37
  cwd?: string;
35
- /** Creates and uses the agent's own folder (`~/Dinox Agents/<name>`). */
38
+ /** Creates and uses the agent's own folder (`~/dinox-agents/<name>`). */
36
39
  useAgentFolder?: true;
37
40
  approval?: GatewayApprovalMode;
38
41
  paused?: boolean;
@@ -41,6 +44,7 @@ export type AgentSettingsChange = {
41
44
  export type AgentSettingsQuery = {
42
45
  protocolVersion: typeof REMOTE_DEVICE_PROTOCOL_VERSION;
43
46
  type: 'agent.settings.get';
47
+ threadId?: string;
44
48
  requestId: string;
45
49
  clientDeviceId: string;
46
50
  };
@@ -58,6 +62,8 @@ export type AgentSettingsResult = {
58
62
  requestId: string;
59
63
  ok: boolean;
60
64
  settings: AgentHostSettings;
65
+ /** The conversation's folder, even after its topic has changed. */
66
+ threadCwd: string | null;
61
67
  /** For showing paths as `~/...`. */
62
68
  home: string;
63
69
  /** Where `useAgentFolder` puts the agent's own folder. */
@@ -99,6 +105,29 @@ export type AgentFoldersResult = {
99
105
  message: string;
100
106
  };
101
107
  };
108
+ export type AgentFolderPrepareQuery = {
109
+ protocolVersion: typeof REMOTE_DEVICE_PROTOCOL_VERSION;
110
+ type: 'agent.folder.prepare';
111
+ requestId: string;
112
+ clientDeviceId: string;
113
+ path?: string;
114
+ agentName?: string;
115
+ agentId?: string;
116
+ };
117
+ export type AgentFolderPrepareResult = {
118
+ protocolVersion: typeof REMOTE_DEVICE_PROTOCOL_VERSION;
119
+ type: 'agent.folder.result';
120
+ requestId: string;
121
+ ok: boolean;
122
+ path: string | null;
123
+ home: string;
124
+ fetchedAt: string;
125
+ error?: {
126
+ code: AgentSettingsErrorCode;
127
+ message: string;
128
+ };
129
+ };
130
+ export declare function isAgentFolderPrepareQuery(value: unknown, byteLength: number): value is AgentFolderPrepareQuery;
102
131
  export declare function isAgentSettingsQuery(value: unknown): value is AgentSettingsQuery;
103
132
  export declare function isAgentFoldersQuery(value: unknown): value is AgentFoldersQuery;
104
133
  /** The change an `agent.settings.set` asks for, or why it is malformed. */
@@ -76,9 +76,21 @@ export function isAgentConfigQuery(value) {
76
76
  * `agent.settings.set`, and picks a working folder with `agent.folders.list`.
77
77
  */
78
78
  const MAX_SETTINGS_REQUEST_BYTES = 16 * 1024;
79
- const MAX_PATH_LENGTH = 4096;
79
+ export const MAX_PATH_LENGTH = 4096;
80
+ /** Host-side workspace choice, never forwarded as an ACP session option. */
81
+ export const AGENT_CWD_CONFIG_KEY = 'dinox.cwd';
82
+ export function isAgentFolderPrepareQuery(value, byteLength) {
83
+ if (!isMessageOfType(value, 'agent.folder.prepare') || !isDeviceId(value.clientDeviceId) || byteLength > MAX_SETTINGS_REQUEST_BYTES)
84
+ return false;
85
+ if ((value.path === undefined) === (value.agentName === undefined))
86
+ return false;
87
+ if (value.agentId !== undefined && (value.agentName === undefined || typeof value.agentId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(value.agentId)))
88
+ return false;
89
+ const text = value.path ?? value.agentName;
90
+ return typeof text === 'string' && !!text.trim() && text.length <= (value.path === undefined ? MAX_AGENT_CONFIG_TEXT : MAX_PATH_LENGTH);
91
+ }
80
92
  export function isAgentSettingsQuery(value) {
81
- return isMessageOfType(value, 'agent.settings.get') && isDeviceId(value.clientDeviceId);
93
+ return isMessageOfType(value, 'agent.settings.get') && isDeviceId(value.clientDeviceId) && (value.threadId === undefined || isSubjectId(value.threadId));
82
94
  }
83
95
  export function isAgentFoldersQuery(value) {
84
96
  if (!isMessageOfType(value, 'agent.folders.list') || !isDeviceId(value.clientDeviceId))
@@ -139,7 +151,7 @@ export function readAgentConfig(request) {
139
151
  for (const [id, value] of entries) {
140
152
  if (!id || id.length > MAX_AGENT_CONFIG_TEXT)
141
153
  return 'invalid';
142
- if (typeof value !== 'string' || !value || value.length > MAX_AGENT_CONFIG_TEXT)
154
+ if (typeof value !== 'string' || !value || value.length > (id === AGENT_CWD_CONFIG_KEY ? MAX_PATH_LENGTH : MAX_AGENT_CONFIG_TEXT))
143
155
  return 'invalid';
144
156
  config[id] = value;
145
157
  }