@dinoxx/dinox-cli 2.2.0 → 2.4.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 (102) hide show
  1. package/README.md +17 -3
  2. package/dist/cli.js +6 -0
  3. package/dist/commands/domain/options.d.ts +10 -0
  4. package/dist/commands/domain/options.js +75 -0
  5. package/dist/commands/domain/run.d.ts +24 -0
  6. package/dist/commands/domain/run.js +137 -0
  7. package/dist/commands/gateway/index.d.ts +4 -0
  8. package/dist/commands/gateway/index.js +300 -0
  9. package/dist/commands/notes/actions.js +47 -18
  10. package/dist/commands/notes/createFromTemplate.d.ts +35 -0
  11. package/dist/commands/notes/createFromTemplate.js +58 -0
  12. package/dist/commands/notes/descriptors.js +9 -4
  13. package/dist/commands/notes/repo/read.js +4 -23
  14. package/dist/commands/notes/repo/write.d.ts +2 -0
  15. package/dist/commands/notes/repo/write.js +1 -1
  16. package/dist/commands/properties/index.d.ts +16 -0
  17. package/dist/commands/properties/index.js +640 -0
  18. package/dist/commands/properties/repo.d.ts +150 -0
  19. package/dist/commands/properties/repo.js +272 -0
  20. package/dist/commands/savedViews/fields.d.ts +28 -0
  21. package/dist/commands/savedViews/fields.js +87 -0
  22. package/dist/commands/savedViews/index.js +203 -164
  23. package/dist/commands/savedViews/model.d.ts +27 -69
  24. package/dist/commands/savedViews/model.js +63 -279
  25. package/dist/commands/savedViews/repo.d.ts +64 -62
  26. package/dist/commands/savedViews/repo.js +309 -428
  27. package/dist/commands/schema/renderSkills.js +33 -5
  28. package/dist/commands/schema/spec.js +6 -0
  29. package/dist/commands/templates/index.d.ts +4 -0
  30. package/dist/commands/templates/index.js +290 -0
  31. package/dist/commands/templates/model.d.ts +12 -34
  32. package/dist/commands/templates/model.js +39 -83
  33. package/dist/commands/templates/repo.d.ts +53 -0
  34. package/dist/commands/templates/repo.js +78 -0
  35. package/dist/config/paths.d.ts +1 -0
  36. package/dist/config/paths.js +1 -1
  37. package/dist/daemon/server.d.ts +1 -0
  38. package/dist/daemon/server.js +31 -0
  39. package/dist/gateway/adapters/acp.d.ts +42 -0
  40. package/dist/gateway/adapters/acp.js +285 -0
  41. package/dist/gateway/adapters/echo.d.ts +12 -0
  42. package/dist/gateway/adapters/echo.js +51 -0
  43. package/dist/gateway/adapters/index.d.ts +9 -0
  44. package/dist/gateway/adapters/index.js +30 -0
  45. package/dist/gateway/assistantProjection.d.ts +32 -0
  46. package/dist/gateway/assistantProjection.js +193 -0
  47. package/dist/gateway/config.d.ts +50 -0
  48. package/dist/gateway/config.js +253 -0
  49. package/dist/gateway/detect.d.ts +40 -0
  50. package/dist/gateway/detect.js +145 -0
  51. package/dist/gateway/deviceHost.d.ts +82 -0
  52. package/dist/gateway/deviceHost.js +530 -0
  53. package/dist/gateway/mcpServer.d.ts +4 -0
  54. package/dist/gateway/mcpServer.js +118 -0
  55. package/dist/gateway/protocol.d.ts +29 -0
  56. package/dist/gateway/protocol.js +127 -0
  57. package/dist/gateway/runExecutor.d.ts +67 -0
  58. package/dist/gateway/runExecutor.js +316 -0
  59. package/dist/gateway/service.d.ts +26 -0
  60. package/dist/gateway/service.js +82 -0
  61. package/dist/gateway/streamPublisher.d.ts +33 -0
  62. package/dist/gateway/streamPublisher.js +93 -0
  63. package/dist/gateway/threadStore.d.ts +157 -0
  64. package/dist/gateway/threadStore.js +430 -0
  65. package/dist/gateway/types.d.ts +117 -0
  66. package/dist/gateway/types.js +9 -0
  67. package/dist/skills/registry.js +7 -0
  68. package/dist/utils/errorCodes.d.ts +212 -0
  69. package/dist/utils/errorCodes.js +53 -0
  70. package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
  71. package/dist/vendor/electron/domain/note-templates.js +232 -0
  72. package/dist/vendor/electron/domain/properties.d.ts +10 -0
  73. package/dist/vendor/electron/domain/properties.js +1549 -0
  74. package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
  75. package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
  76. package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
  77. package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
  78. package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
  79. package/dist/vendor/electron/domain/saved-views.js +1120 -0
  80. package/dist/vendor/electron/rpc.d.ts +22 -0
  81. package/dist/vendor/electron/rpc.js +27 -0
  82. package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
  83. package/dist/vendor/electron/shared/ai-errors.js +107 -0
  84. package/dist/vendor/electron/shared/external-types.d.ts +20 -0
  85. package/dist/vendor/electron/shared/external-types.js +1 -0
  86. package/dist/vendor/electron/shared/index.d.ts +9 -0
  87. package/dist/vendor/electron/shared/index.js +7 -0
  88. package/dist/vendor/electron/shared/note-properties.d.ts +328 -0
  89. package/dist/vendor/electron/shared/note-properties.js +1027 -0
  90. package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
  91. package/dist/vendor/electron/shared/note-templates.js +84 -0
  92. package/dist/vendor/electron/shared/powersync-read-models.d.ts +1716 -0
  93. package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
  94. package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
  95. package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
  96. package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
  97. package/dist/vendor/electron/shared/saved-views.js +981 -0
  98. package/dist/vendor/electron/sql-utils.d.ts +5 -0
  99. package/dist/vendor/electron/sql-utils.js +32 -0
  100. package/package.json +10 -4
  101. package/dist/commands/savedViews/semantics.d.ts +0 -46
  102. package/dist/commands/savedViews/semantics.js +0 -738
@@ -0,0 +1,78 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ import { fetchDomainReadModel, previewInRolledBackTransaction, runDomainCommand, } from '../domain/run.js';
3
+ import { mapNoteTemplateRow } from './model.js';
4
+ function nowIso() {
5
+ return new Date().toISOString();
6
+ }
7
+ function notFound(id) {
8
+ return new DinoxError(`Note template not found: ${id}`, {
9
+ code: 'NOTE_TEMPLATE_NOT_FOUND',
10
+ details: { templateId: id },
11
+ suggestion: 'Run `dino template list --format json` to see template ids.',
12
+ });
13
+ }
14
+ export async function listNoteTemplates(db, input) {
15
+ const rows = await fetchDomainReadModel(db, 'noteTemplates.catalog', {
16
+ userId: input.userId,
17
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
18
+ });
19
+ return rows.map(mapNoteTemplateRow);
20
+ }
21
+ export async function getNoteTemplate(db, input) {
22
+ const row = await db.getOptional(`
23
+ SELECT id, user_id, name, content, priority, is_show, extra_data, property_def,
24
+ insert_at, update_at, is_del
25
+ FROM c_note_template
26
+ WHERE id = ? AND user_id = ? AND (is_del IS NULL OR is_del = 0)
27
+ LIMIT 1
28
+ `, [input.id.trim(), input.userId]);
29
+ if (!row)
30
+ throw notFound(input.id);
31
+ return mapNoteTemplateRow(row);
32
+ }
33
+ async function runCreate(db, input) {
34
+ const { result: id } = await runDomainCommand(db, 'noteTemplates.create', {
35
+ userId: input.userId,
36
+ name: input.name,
37
+ propertyKeys: input.propertyKeys ?? [],
38
+ ...(input.content === undefined ? {} : { content: input.content }),
39
+ updatedAt: nowIso(),
40
+ });
41
+ return getNoteTemplate(db, { userId: input.userId, id });
42
+ }
43
+ async function runUpdate(db, input) {
44
+ await getNoteTemplate(db, input);
45
+ await runDomainCommand(db, 'noteTemplates.update', {
46
+ id: input.id,
47
+ userId: input.userId,
48
+ ...(input.name === undefined ? {} : { name: input.name }),
49
+ ...(input.propertyKeys === undefined ? {} : { propertyKeys: input.propertyKeys }),
50
+ ...(input.content === undefined ? {} : { content: input.content }),
51
+ updatedAt: nowIso(),
52
+ });
53
+ return getNoteTemplate(db, input);
54
+ }
55
+ async function runDelete(db, input) {
56
+ const before = await getNoteTemplate(db, input);
57
+ const { result: changed } = await runDomainCommand(db, 'noteTemplates.softDelete', {
58
+ id: input.id,
59
+ userId: input.userId,
60
+ updatedAt: nowIso(),
61
+ });
62
+ if (!changed)
63
+ throw notFound(input.id);
64
+ return before;
65
+ }
66
+ export const createNoteTemplate = runCreate;
67
+ export const updateNoteTemplate = runUpdate;
68
+ export const deleteNoteTemplate = runDelete;
69
+ export async function previewCreateNoteTemplate(db, input) {
70
+ return previewInRolledBackTransaction(db, (tx) => runCreate(tx, input), async (_tx, after) => ({ before: null, after }));
71
+ }
72
+ export async function previewUpdateNoteTemplate(db, input) {
73
+ const before = await getNoteTemplate(db, input);
74
+ return previewInRolledBackTransaction(db, (tx) => runUpdate(tx, input), async (_tx, after) => ({ before, after }));
75
+ }
76
+ export async function previewDeleteNoteTemplate(db, input) {
77
+ return previewInRolledBackTransaction(db, (tx) => runDelete(tx, input), async (_tx, before) => ({ before, after: null }));
78
+ }
@@ -1,3 +1,4 @@
1
+ export declare function normalizeUserPathSegment(userId: string): string;
1
2
  export declare function getConfigDir(): string;
2
3
  export declare function getConfigFilePath(): string;
3
4
  export declare function getDataDir(): string;
@@ -1,7 +1,7 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  const POWERSYNC_DB_FILENAME = 'dinox-cli.sqlite';
4
- function normalizeUserPathSegment(userId) {
4
+ export function normalizeUserPathSegment(userId) {
5
5
  const normalized = userId.trim();
6
6
  const encoded = encodeURIComponent(normalized);
7
7
  if (encoded === '.' || encoded === '..') {
@@ -12,6 +12,7 @@ export declare class DaemonServer {
12
12
  private ownsArtifacts;
13
13
  private releaseOwner;
14
14
  private stopPromise;
15
+ private gateways;
15
16
  initialize(): Promise<void>;
16
17
  handleRequest(req: DaemonRequest): Promise<DaemonResponse>;
17
18
  private isCacheStale;
@@ -6,6 +6,7 @@ import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve
6
6
  import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
7
7
  import { closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot, } from '../powersync/runtime.js';
8
8
  import { executeNoteSearch } from '../commands/notes/repo.js';
9
+ import { getNoteDetail } from '../commands/notes/repo/read.js';
9
10
  import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from '../commands/graph/repo.js';
10
11
  import { executeTodoSearch } from '../commands/todo/repo.js';
11
12
  import { ensureDaemonDir, getDaemonSocketPath, isProcessRunning, readPid, removePid, savePid, } from './pid.js';
@@ -13,6 +14,7 @@ import { getPackageVersion } from '../utils/version.js';
13
14
  import { syncNoteTokenIndex } from '../powersync/tokenIndex.js';
14
15
  import { buildStructuredErrorPayload, DinoxError } from '../utils/errors.js';
15
16
  import { getDaemonInfo } from './client.js';
17
+ import { GatewayService } from '../gateway/service.js';
16
18
  const MAX_DAEMON_REQUEST_BYTES = 1 * 1024 * 1024;
17
19
  const DAEMON_PROCESS_VERSION = getPackageVersion();
18
20
  async function readJsonBody(req) {
@@ -68,6 +70,7 @@ export class DaemonServer {
68
70
  ownsArtifacts = false;
69
71
  releaseOwner = null;
70
72
  stopPromise = null;
73
+ gateways = null;
71
74
  async initialize() {
72
75
  const baseConfig = resolveConfig(await loadConfig());
73
76
  const { config } = await hydrateResolvedAuthIdentity(baseConfig, { timeoutMs: baseConfig.sync.timeoutMs });
@@ -83,6 +86,19 @@ export class DaemonServer {
83
86
  });
84
87
  this.db = db;
85
88
  this.releaseOwner = releaseOwner;
89
+ if (config.auth.userId && config.auth.authorization) {
90
+ // Agent gateways share the daemon's PowerSync connection; a gateway
91
+ // failure must never take the daemon down.
92
+ this.gateways = new GatewayService({
93
+ db,
94
+ userId: config.auth.userId,
95
+ authorization: config.auth.authorization,
96
+ credentialsUrl: process.env.DINOX_NATS_CREDENTIALS_URL || undefined,
97
+ });
98
+ await this.gateways.start().catch((error) => {
99
+ process.stderr.write(`[dinox-gateway] start failed: ${String(error)}\n`);
100
+ });
101
+ }
86
102
  // PowerSync keeps the local cache in sync continuously while connected,
87
103
  // so no background polling loop is needed here.
88
104
  }
@@ -147,6 +163,18 @@ export class DaemonServer {
147
163
  case 'graph.stats':
148
164
  const graphStats = await getGraphStats(this.db);
149
165
  return { id: req.id, ok: true, result: { ...graphStats, stale: this.isCacheStale() } };
166
+ case 'note.detail': {
167
+ const note = await getNoteDetail(this.db, String(params?.id ?? ''));
168
+ return { id: req.id, ok: true, result: { note, stale: this.isCacheStale() } };
169
+ }
170
+ case 'gateway.status':
171
+ return { id: req.id, ok: true, result: { gateways: this.gateways?.status() ?? [] } };
172
+ case 'gateway.reload': {
173
+ if (!this.gateways) {
174
+ throw new DinoxError('Gateways need a signed-in daemon', { code: 'AUTH_REQUIRED' });
175
+ }
176
+ return { id: req.id, ok: true, result: { gateways: await this.gateways.reload() } };
177
+ }
150
178
  case 'todo.search': {
151
179
  // Same executor as the local `dino todo search` path — no option/result drift.
152
180
  const result = await executeTodoSearch(this.db, params);
@@ -258,6 +286,9 @@ export class DaemonServer {
258
286
  }
259
287
  }
260
288
  async stopInternal() {
289
+ const gateways = this.gateways;
290
+ this.gateways = null;
291
+ await gateways?.stop().catch(() => undefined);
261
292
  const db = this.db;
262
293
  const releaseOwner = this.releaseOwner;
263
294
  if (db) {
@@ -0,0 +1,42 @@
1
+ import { type McpServer, type SessionConfigOption } from '@agentclientprotocol/sdk';
2
+ import type { GatewayAdapter, GatewayAdapterInfo, GatewayConfigOption, GatewayDefinition, GatewayTurnInput, GatewayTurnResult } from '../types.js';
3
+ type Logger = (message: string, details?: Record<string, unknown>) => void;
4
+ /** Select options only; boolean toggles have no use in the app yet. */
5
+ export declare function normalizeConfigOptions(options: SessionConfigOption[] | null | undefined): GatewayConfigOption[];
6
+ /**
7
+ * Drives any Agent Client Protocol agent over stdio (`hermes acp`, `openclaw acp`,
8
+ * `claude-code-acp`, `codex-acp`, `pi-acp`, ...). One long-lived process per
9
+ * gateway hosts every thread's session; the agent keeps the conversation, so
10
+ * each turn sends only the new prompt.
11
+ */
12
+ export declare class AcpGatewayAdapter implements GatewayAdapter {
13
+ private readonly gateway;
14
+ private readonly log;
15
+ /** MCP servers offered to every session, e.g. the Dinox knowledge tools. */
16
+ private readonly mcpServers;
17
+ readonly kind: "acp";
18
+ private process;
19
+ private starting;
20
+ private readonly turns;
21
+ /** The latest settings any session reported: the agent-wide choices the app lists. */
22
+ private catalog;
23
+ constructor(gateway: GatewayDefinition, log?: Logger,
24
+ /** MCP servers offered to every session, e.g. the Dinox knowledge tools. */
25
+ mcpServers?: McpServer[]);
26
+ info(): GatewayAdapterInfo;
27
+ runTurn(input: GatewayTurnInput): Promise<GatewayTurnResult>;
28
+ describeConfig(): Promise<GatewayConfigOption[]>;
29
+ dispose(): Promise<void>;
30
+ private resolveSession;
31
+ /**
32
+ * Brings the session to the requested settings before its turn. Entries the
33
+ * agent does not offer are skipped, so a stale choice never fails the run.
34
+ */
35
+ private applyConfig;
36
+ private rememberConfig;
37
+ private ensureProcess;
38
+ private startProcess;
39
+ private handleSessionUpdate;
40
+ private handlePermission;
41
+ }
42
+ export {};
@@ -0,0 +1,285 @@
1
+ import { spawn } from 'node:child_process';
2
+ import os from 'node:os';
3
+ import { Readable, Writable } from 'node:stream';
4
+ import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from '@agentclientprotocol/sdk';
5
+ import { generateId } from '../../utils/id.js';
6
+ import { getPackageVersion } from '../../utils/version.js';
7
+ const TOOL_STATUS = {
8
+ pending: 'pending',
9
+ in_progress: 'running',
10
+ completed: 'completed',
11
+ failed: 'failed',
12
+ };
13
+ const STOP_REASON = {
14
+ end_turn: 'end_turn',
15
+ max_tokens: 'max_tokens',
16
+ max_turn_requests: 'max_tokens',
17
+ refusal: 'refusal',
18
+ cancelled: 'cancelled',
19
+ };
20
+ function toolContentText(content) {
21
+ const text = (content ?? [])
22
+ .map((item) => (item.type === 'content' && item.content.type === 'text' ? item.content.text : ''))
23
+ .filter(Boolean)
24
+ .join('\n');
25
+ return text || undefined;
26
+ }
27
+ /** Select options only; boolean toggles have no use in the app yet. */
28
+ export function normalizeConfigOptions(options) {
29
+ const normalized = [];
30
+ for (const option of options ?? []) {
31
+ if (option.type !== 'select')
32
+ continue;
33
+ const values = [];
34
+ for (const entry of option.options) {
35
+ if ('group' in entry) {
36
+ for (const value of entry.options) {
37
+ values.push({ value: value.value, name: value.name, description: value.description ?? null, group: entry.name });
38
+ }
39
+ }
40
+ else {
41
+ values.push({ value: entry.value, name: entry.name, description: entry.description ?? null, group: null });
42
+ }
43
+ }
44
+ normalized.push({
45
+ id: option.id,
46
+ name: option.name,
47
+ category: option.category ?? null,
48
+ description: option.description ?? null,
49
+ currentValue: option.currentValue,
50
+ values,
51
+ });
52
+ }
53
+ return normalized;
54
+ }
55
+ function pickOption(options, approved) {
56
+ const preferred = approved ? ['allow_once', 'allow_always'] : ['reject_once', 'reject_always'];
57
+ for (const kind of preferred) {
58
+ const option = options.find((item) => item.kind === kind);
59
+ if (option)
60
+ return option;
61
+ }
62
+ return undefined;
63
+ }
64
+ /**
65
+ * Drives any Agent Client Protocol agent over stdio (`hermes acp`, `openclaw acp`,
66
+ * `claude-code-acp`, `codex-acp`, `pi-acp`, ...). One long-lived process per
67
+ * gateway hosts every thread's session; the agent keeps the conversation, so
68
+ * each turn sends only the new prompt.
69
+ */
70
+ export class AcpGatewayAdapter {
71
+ gateway;
72
+ log;
73
+ mcpServers;
74
+ kind = 'acp';
75
+ process = null;
76
+ starting = null;
77
+ turns = new Map();
78
+ /** The latest settings any session reported: the agent-wide choices the app lists. */
79
+ catalog = null;
80
+ constructor(gateway, log = () => { },
81
+ /** MCP servers offered to every session, e.g. the Dinox knowledge tools. */
82
+ mcpServers = []) {
83
+ this.gateway = gateway;
84
+ this.log = log;
85
+ this.mcpServers = mcpServers;
86
+ }
87
+ info() {
88
+ const agentInfo = this.process?.init.agentInfo;
89
+ return { kind: this.kind, agentName: agentInfo?.name ?? null, agentVersion: agentInfo?.version ?? null };
90
+ }
91
+ async runTurn(input) {
92
+ const agent = await this.ensureProcess();
93
+ const sessionId = await this.resolveSession(agent, input.sessionId);
94
+ await this.applyConfig(agent, sessionId, input.config);
95
+ this.turns.set(sessionId, { onEvent: input.onEvent, awaitApproval: input.awaitApproval });
96
+ const onAbort = () => {
97
+ void agent.connection.cancel({ sessionId }).catch(() => undefined);
98
+ };
99
+ input.signal.addEventListener('abort', onAbort, { once: true });
100
+ try {
101
+ const prompt = [{ type: 'text', text: input.prompt }];
102
+ for (const resource of input.resources) {
103
+ prompt.push({ type: 'resource_link', uri: resource.uri, name: resource.name, mimeType: resource.mediaType ?? null });
104
+ }
105
+ const response = await agent.connection.prompt({ sessionId, prompt });
106
+ return { sessionId, stopReason: STOP_REASON[response.stopReason] ?? 'end_turn' };
107
+ }
108
+ catch (error) {
109
+ if (input.signal.aborted)
110
+ return { sessionId, stopReason: 'cancelled' };
111
+ return { sessionId, stopReason: 'error', error: error instanceof Error ? error.message : String(error) };
112
+ }
113
+ finally {
114
+ input.signal.removeEventListener('abort', onAbort);
115
+ this.turns.delete(sessionId);
116
+ }
117
+ }
118
+ async describeConfig() {
119
+ if (this.catalog)
120
+ return this.catalog;
121
+ const agent = await this.ensureProcess();
122
+ if (this.catalog)
123
+ return this.catalog;
124
+ // No session yet: open one only to read the agent's choices. It never runs a turn.
125
+ const created = await agent.connection.newSession({ cwd: this.gateway.cwd ?? os.homedir(), mcpServers: [] });
126
+ this.rememberConfig(agent, created.sessionId, created.configOptions);
127
+ return this.catalog ?? [];
128
+ }
129
+ async dispose() {
130
+ const agent = this.process;
131
+ this.process = null;
132
+ this.starting = null;
133
+ if (!agent)
134
+ return;
135
+ agent.child.stdin.end();
136
+ agent.child.kill('SIGTERM');
137
+ }
138
+ async resolveSession(agent, sessionId) {
139
+ const cwd = this.gateway.cwd ?? os.homedir();
140
+ if (sessionId && agent.liveSessions.has(sessionId))
141
+ return sessionId;
142
+ if (sessionId && agent.init.agentCapabilities?.loadSession) {
143
+ try {
144
+ // No turn is registered yet, so the history the agent replays is ignored.
145
+ const loaded = await agent.connection.loadSession({ sessionId, cwd, mcpServers: this.mcpServers });
146
+ agent.liveSessions.add(sessionId);
147
+ this.rememberConfig(agent, sessionId, loaded.configOptions);
148
+ return sessionId;
149
+ }
150
+ catch (error) {
151
+ this.log('session/load failed; starting a new session', { sessionId, error: String(error) });
152
+ }
153
+ }
154
+ const created = await agent.connection.newSession({ cwd, mcpServers: this.mcpServers });
155
+ agent.liveSessions.add(created.sessionId);
156
+ this.rememberConfig(agent, created.sessionId, created.configOptions);
157
+ return created.sessionId;
158
+ }
159
+ /**
160
+ * Brings the session to the requested settings before its turn. Entries the
161
+ * agent does not offer are skipped, so a stale choice never fails the run.
162
+ */
163
+ async applyConfig(agent, sessionId, config) {
164
+ for (const [configId, value] of Object.entries(config ?? {})) {
165
+ const option = agent.sessionConfig.get(sessionId)?.find((item) => item.id === configId);
166
+ if (!option || option.currentValue === value)
167
+ continue;
168
+ if (!option.values.some((item) => item.value === value)) {
169
+ this.log('agent setting not offered; skipped', { configId, value });
170
+ continue;
171
+ }
172
+ try {
173
+ const response = await agent.connection.setSessionConfigOption({ sessionId, configId, value });
174
+ this.rememberConfig(agent, sessionId, response.configOptions);
175
+ }
176
+ catch (error) {
177
+ this.log('agent setting failed; continuing with the current one', { configId, error: String(error) });
178
+ }
179
+ }
180
+ }
181
+ rememberConfig(agent, sessionId, options) {
182
+ if (!options)
183
+ return;
184
+ const normalized = normalizeConfigOptions(options);
185
+ agent.sessionConfig.set(sessionId, normalized);
186
+ this.catalog = normalized;
187
+ }
188
+ ensureProcess() {
189
+ if (this.process)
190
+ return Promise.resolve(this.process);
191
+ this.starting ??= this.startProcess().finally(() => {
192
+ this.starting = null;
193
+ });
194
+ return this.starting;
195
+ }
196
+ async startProcess() {
197
+ const command = this.gateway.command;
198
+ if (!command)
199
+ throw new Error(`Gateway ${this.gateway.name} has no ACP command`);
200
+ const child = spawn(command, this.gateway.args ?? [], {
201
+ cwd: this.gateway.cwd ?? os.homedir(),
202
+ env: { ...process.env, ...(this.gateway.env ?? {}) },
203
+ stdio: ['pipe', 'pipe', 'pipe'],
204
+ });
205
+ child.stderr.on('data', (chunk) => this.log('agent stderr', { text: chunk.toString('utf8').slice(0, 2000) }));
206
+ const exited = new Promise((_, reject) => {
207
+ child.once('error', reject);
208
+ child.once('exit', (code, signal) => reject(new Error(`agent exited (code ${code}, signal ${signal})`)));
209
+ });
210
+ child.once('exit', () => {
211
+ if (this.process?.child === child)
212
+ this.process = null;
213
+ });
214
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
215
+ const connection = new ClientSideConnection(() => ({
216
+ sessionUpdate: (params) => this.handleSessionUpdate(params),
217
+ requestPermission: (params) => this.handlePermission(params),
218
+ }), stream);
219
+ const init = await Promise.race([
220
+ connection.initialize({
221
+ protocolVersion: PROTOCOL_VERSION,
222
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
223
+ clientInfo: { name: 'dinox-gateway', version: getPackageVersion() },
224
+ }),
225
+ exited,
226
+ ]);
227
+ exited.catch(() => undefined);
228
+ const agent = { child, connection, init, liveSessions: new Set(), sessionConfig: new Map() };
229
+ this.process = agent;
230
+ this.log('agent connected', { agent: init.agentInfo?.name ?? command, protocol: init.protocolVersion });
231
+ return agent;
232
+ }
233
+ handleSessionUpdate(params) {
234
+ const update = params.update;
235
+ if (update.sessionUpdate === 'config_option_update' && this.process) {
236
+ this.rememberConfig(this.process, params.sessionId, update.configOptions);
237
+ return;
238
+ }
239
+ const turn = this.turns.get(params.sessionId);
240
+ if (!turn)
241
+ return;
242
+ switch (update.sessionUpdate) {
243
+ case 'agent_message_chunk':
244
+ if (update.content.type === 'text')
245
+ turn.onEvent({ type: 'text-delta', text: update.content.text });
246
+ break;
247
+ case 'agent_thought_chunk':
248
+ if (update.content.type === 'text')
249
+ turn.onEvent({ type: 'reasoning-delta', text: update.content.text });
250
+ break;
251
+ case 'tool_call':
252
+ case 'tool_call_update':
253
+ turn.onEvent({
254
+ type: 'tool',
255
+ toolCallId: update.toolCallId,
256
+ ...(update.title ? { title: update.title } : {}),
257
+ ...(update.status ? { status: TOOL_STATUS[update.status] } : {}),
258
+ ...(update.rawInput !== undefined ? { input: update.rawInput } : {}),
259
+ ...(update.rawOutput !== undefined || update.content
260
+ ? { output: update.rawOutput ?? toolContentText(update.content) }
261
+ : {}),
262
+ });
263
+ break;
264
+ default:
265
+ break;
266
+ }
267
+ }
268
+ async handlePermission(params) {
269
+ const turn = this.turns.get(params.sessionId);
270
+ if (!turn)
271
+ return { outcome: { outcome: 'cancelled' } };
272
+ const approvalId = `acp-${generateId()}`;
273
+ turn.onEvent({
274
+ type: 'approval-request',
275
+ approvalId,
276
+ toolCallId: params.toolCall.toolCallId,
277
+ toolName: params.toolCall.title || params.toolCall.name || 'tool',
278
+ input: params.toolCall.rawInput,
279
+ });
280
+ const approved = await turn.awaitApproval(approvalId);
281
+ turn.onEvent({ type: 'approval-resolved', approvalId, approved });
282
+ const option = pickOption(params.options, approved);
283
+ return option ? { outcome: { outcome: 'selected', optionId: option.optionId } } : { outcome: { outcome: 'cancelled' } };
284
+ }
285
+ }
@@ -0,0 +1,12 @@
1
+ import type { GatewayAdapter, GatewayAdapterInfo, GatewayTurnInput, GatewayTurnResult } from '../types.js';
2
+ /**
3
+ * Loopback agent for end-to-end checks of the gateway path (app ⇄ NATS ⇄ CLI ⇄
4
+ * PowerSync) without a real runtime. Echoes the prompt; a prompt containing
5
+ * `/approve` exercises the tool-approval round trip first.
6
+ */
7
+ export declare class EchoGatewayAdapter implements GatewayAdapter {
8
+ readonly kind: "echo";
9
+ info(): GatewayAdapterInfo;
10
+ runTurn(input: GatewayTurnInput): Promise<GatewayTurnResult>;
11
+ dispose(): Promise<void>;
12
+ }
@@ -0,0 +1,51 @@
1
+ import { generateId } from '../../utils/id.js';
2
+ const CHUNK_DELAY_MS = 40;
3
+ function delay(ms, signal) {
4
+ return new Promise((resolve, reject) => {
5
+ if (signal.aborted)
6
+ return reject(signal.reason);
7
+ const timer = setTimeout(resolve, ms);
8
+ signal.addEventListener('abort', () => {
9
+ clearTimeout(timer);
10
+ reject(signal.reason);
11
+ }, { once: true });
12
+ });
13
+ }
14
+ /**
15
+ * Loopback agent for end-to-end checks of the gateway path (app ⇄ NATS ⇄ CLI ⇄
16
+ * PowerSync) without a real runtime. Echoes the prompt; a prompt containing
17
+ * `/approve` exercises the tool-approval round trip first.
18
+ */
19
+ export class EchoGatewayAdapter {
20
+ kind = 'echo';
21
+ info() {
22
+ return { kind: this.kind, agentName: 'echo', agentVersion: null };
23
+ }
24
+ async runTurn(input) {
25
+ const sessionId = input.sessionId ?? `echo-${generateId()}`;
26
+ try {
27
+ if (input.prompt.includes('/approve')) {
28
+ const toolCallId = `tool-${generateId()}`;
29
+ 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 } });
32
+ const approved = await input.awaitApproval(approvalId);
33
+ input.onEvent({ type: 'approval-resolved', approvalId, approved });
34
+ input.onEvent(approved
35
+ ? { type: 'tool', toolCallId, status: 'completed', output: 'approved' }
36
+ : { type: 'tool', toolCallId, status: 'failed', output: 'denied by user' });
37
+ }
38
+ for (const word of `Echo: ${input.prompt}`.split(/(\s+)/)) {
39
+ await delay(CHUNK_DELAY_MS, input.signal);
40
+ input.onEvent({ type: 'text-delta', text: word });
41
+ }
42
+ return { sessionId, stopReason: 'end_turn' };
43
+ }
44
+ catch (error) {
45
+ if (input.signal.aborted)
46
+ return { sessionId, stopReason: 'cancelled' };
47
+ throw error;
48
+ }
49
+ }
50
+ async dispose() { }
51
+ }
@@ -0,0 +1,9 @@
1
+ import type { McpServer } from '@agentclientprotocol/sdk';
2
+ import type { GatewayAdapter, GatewayDefinition } from '../types.js';
3
+ /**
4
+ * The Dinox knowledge tools as a stdio MCP server: this same CLI, run as
5
+ * `dino gateway mcp`, reading through the daemon. DINOX_* variables are passed
6
+ * on so the child finds the same config, data dir and daemon socket.
7
+ */
8
+ export declare function dinoxMcpServerConfig(): McpServer | null;
9
+ export declare function createGatewayAdapter(gateway: GatewayDefinition, log: (message: string, details?: Record<string, unknown>) => void): GatewayAdapter;
@@ -0,0 +1,30 @@
1
+ import { AcpGatewayAdapter } from './acp.js';
2
+ import { EchoGatewayAdapter } from './echo.js';
3
+ /**
4
+ * The Dinox knowledge tools as a stdio MCP server: this same CLI, run as
5
+ * `dino gateway mcp`, reading through the daemon. DINOX_* variables are passed
6
+ * on so the child finds the same config, data dir and daemon socket.
7
+ */
8
+ export function dinoxMcpServerConfig() {
9
+ const entrypoint = process.argv[1];
10
+ if (!entrypoint)
11
+ return null;
12
+ return {
13
+ name: 'dinox',
14
+ command: process.execPath,
15
+ args: [...process.execArgv, entrypoint, 'gateway', 'mcp'],
16
+ env: Object.entries(process.env)
17
+ .filter((entry) => entry[0].startsWith('DINOX_') && typeof entry[1] === 'string')
18
+ .map(([name, value]) => ({ name, value })),
19
+ };
20
+ }
21
+ export function createGatewayAdapter(gateway, log) {
22
+ switch (gateway.kind) {
23
+ case 'acp': {
24
+ const dinoxTools = gateway.dinoxTools === false ? null : dinoxMcpServerConfig();
25
+ return new AcpGatewayAdapter(gateway, log, dinoxTools ? [dinoxTools] : []);
26
+ }
27
+ case 'echo':
28
+ return new EchoGatewayAdapter();
29
+ }
30
+ }
@@ -0,0 +1,32 @@
1
+ import type { ChatUIMessage } from '../vendor/electron/shared/external-types.js';
2
+ import type { RemoteToolApprovalPrompt } from '../vendor/electron/shared/remote-device-agent.js';
3
+ import type { GatewayEvent } from './types.js';
4
+ export declare class AssistantProjection {
5
+ private readonly base;
6
+ private readonly parts;
7
+ private readonly toolIndex;
8
+ private readonly approvals;
9
+ private revisionCounter;
10
+ constructor(base: ChatUIMessage);
11
+ get revision(): number;
12
+ apply(event: GatewayEvent): void;
13
+ /** Closes streaming parts; called once when the turn ends. */
14
+ finish(): void;
15
+ snapshot(): ChatUIMessage;
16
+ private appendText;
17
+ private ensureTool;
18
+ private findToolByApproval;
19
+ private applyTool;
20
+ }
21
+ /** Same projection the desktop host sends: open approval requests found in the snapshot. */
22
+ /**
23
+ * The reply as it is kept in the synced transcript. A computer agent's
24
+ * reasoning and tool calls are working state: they stream to the client while
25
+ * the turn runs and stay in the local run snapshot, but only the answer is
26
+ * stored in `c_chat_message`.
27
+ */
28
+ export declare function toTranscriptMessage(message: ChatUIMessage): ChatUIMessage;
29
+ export declare function projectPendingApprovals(message: ChatUIMessage | null | undefined, ids: {
30
+ runId: string;
31
+ threadId: string;
32
+ }): RemoteToolApprovalPrompt[];