@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,82 @@
1
+ import { wsconnect } from '@nats-io/nats-core';
2
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
3
+ import type { GatewayAdapter, GatewayDefinition } from './types.js';
4
+ export declare const DEFAULT_NATS_CREDENTIALS_URL = "https://aisdk.chatgo.pro/api/nats/credentials";
5
+ type Logger = (message: string, details?: Record<string, unknown>) => void;
6
+ export type DeviceHostState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'error';
7
+ export type DeviceHostStatus = {
8
+ gatewayId: string;
9
+ name: string;
10
+ deviceId: string;
11
+ kind: string;
12
+ state: DeviceHostState;
13
+ connectedAt: string | null;
14
+ lastError: string | null;
15
+ busyThreadIds: string[];
16
+ agentName: string | null;
17
+ };
18
+ export type DeviceHostDeps = {
19
+ gateway: GatewayDefinition;
20
+ db: AbstractPowerSyncDatabase;
21
+ userId: string;
22
+ authorization: string;
23
+ adapter: GatewayAdapter;
24
+ appVersion: string;
25
+ log: Logger;
26
+ credentialsUrl?: string;
27
+ connect?: typeof wsconnect;
28
+ fetchImpl?: typeof fetch;
29
+ };
30
+ /**
31
+ * Hosts one gateway as a `dinox.remote-device.v2` device over NATS, the same
32
+ * contract Dinox desktop serves, so the app drives it with no client changes.
33
+ * Each gateway is its own device id (the credential service scopes a desktop
34
+ * credential to a single device subtree).
35
+ */
36
+ export declare class GatewayDeviceHost {
37
+ private readonly deps;
38
+ private readonly executor;
39
+ private connection;
40
+ private subscriptions;
41
+ private userNo;
42
+ private state;
43
+ private connectedAt;
44
+ private lastError;
45
+ private generation;
46
+ private attempt;
47
+ private presenceSequence;
48
+ private presenceTimer;
49
+ private refreshTimer;
50
+ private reconnectTimer;
51
+ private stopped;
52
+ private retryBlocked;
53
+ private readonly ackCache;
54
+ /** Latest event per run while offline; the snapshot model makes older ones redundant. */
55
+ private readonly pendingEvents;
56
+ constructor(deps: DeviceHostDeps);
57
+ start(): void;
58
+ stop(): Promise<void>;
59
+ status(): DeviceHostStatus;
60
+ private connect;
61
+ private watchConnection;
62
+ private scheduleReconnect;
63
+ /** Short-lived credentials: reconnect with fresh ones before they expire (runs keep going). */
64
+ private scheduleCredentialRefresh;
65
+ private disconnect;
66
+ private fetchCredentials;
67
+ private deviceInfo;
68
+ private subscribe;
69
+ private publishPresence;
70
+ private publishRunEvent;
71
+ private flushPendingEvents;
72
+ private handleDiscover;
73
+ /** Only the gateway's own query is served; desktop queries (skills, workspace) are not advertised. */
74
+ private handleQuery;
75
+ private handleInfo;
76
+ private respondAck;
77
+ private cacheAck;
78
+ private handleRunRequest;
79
+ private handleCancel;
80
+ private handleApprove;
81
+ }
82
+ export {};
@@ -0,0 +1,530 @@
1
+ import os from 'node:os';
2
+ import { credsAuthenticator, wsconnect, } from '@nats-io/nats-core';
3
+ import { toAuthorizationHeaderValue } from '../utils/authToken.js';
4
+ 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
+ import { projectPendingApprovals } from './assistantProjection.js';
6
+ import { isAgentConfigQuery, isCancelRequest, isRunStatusRequest, reconnectDelayMs, validateApprovalRequest, validateRunRequest, } from './protocol.js';
7
+ import { GatewayRunExecutor, MAX_QUEUED_TURNS, QueueFullError } from './runExecutor.js';
8
+ import { RunStreamPublisher } from './streamPublisher.js';
9
+ import { findRun, resolveTrustedResource, TERMINAL_RUN_STATUSES, threadExists } from './threadStore.js';
10
+ export const DEFAULT_NATS_CREDENTIALS_URL = 'https://aisdk.chatgo.pro/api/nats/credentials';
11
+ const CREDENTIALS_TIMEOUT_MS = 10_000;
12
+ const CREDENTIALS_REFRESH_LEAD_MS = 60_000;
13
+ const ACK_CACHE_TTL_MS = 10 * 60 * 1000;
14
+ const ACK_CACHE_MAX = 1000;
15
+ const DRAIN_TIMEOUT_MS = 3_000;
16
+ class CredentialsHttpError extends Error {
17
+ status;
18
+ constructor(status, message) {
19
+ super(message);
20
+ this.status = status;
21
+ }
22
+ }
23
+ const encoder = new TextEncoder();
24
+ function encode(value) {
25
+ return encoder.encode(JSON.stringify(value));
26
+ }
27
+ function decode(msg) {
28
+ try {
29
+ return { value: msg.json(), byteLength: msg.data.byteLength };
30
+ }
31
+ catch {
32
+ return { value: null, byteLength: msg.data.byteLength };
33
+ }
34
+ }
35
+ /**
36
+ * Hosts one gateway as a `dinox.remote-device.v2` device over NATS, the same
37
+ * contract Dinox desktop serves, so the app drives it with no client changes.
38
+ * Each gateway is its own device id (the credential service scopes a desktop
39
+ * credential to a single device subtree).
40
+ */
41
+ export class GatewayDeviceHost {
42
+ deps;
43
+ executor;
44
+ connection = null;
45
+ subscriptions = [];
46
+ userNo = null;
47
+ state = 'idle';
48
+ connectedAt = null;
49
+ lastError = null;
50
+ generation = 0;
51
+ attempt = 0;
52
+ presenceSequence = 0;
53
+ presenceTimer = null;
54
+ refreshTimer = null;
55
+ reconnectTimer = null;
56
+ stopped = false;
57
+ retryBlocked = false;
58
+ ackCache = new Map();
59
+ /** Latest event per run while offline; the snapshot model makes older ones redundant. */
60
+ pendingEvents = new Map();
61
+ constructor(deps) {
62
+ this.deps = deps;
63
+ this.executor = new GatewayRunExecutor({
64
+ db: deps.db,
65
+ userId: deps.userId,
66
+ gateway: deps.gateway,
67
+ adapter: deps.adapter,
68
+ log: deps.log,
69
+ });
70
+ }
71
+ start() {
72
+ this.stopped = false;
73
+ void this.connect();
74
+ }
75
+ async stop() {
76
+ this.stopped = true;
77
+ this.executor.cancelAll();
78
+ await this.disconnect({ publishOffline: true });
79
+ await this.deps.adapter.dispose();
80
+ this.state = 'idle';
81
+ }
82
+ status() {
83
+ return {
84
+ gatewayId: this.deps.gateway.id,
85
+ name: this.deps.gateway.name,
86
+ deviceId: this.deps.gateway.deviceId,
87
+ kind: this.deps.gateway.kind,
88
+ state: this.state,
89
+ connectedAt: this.connectedAt,
90
+ lastError: this.lastError,
91
+ busyThreadIds: this.executor.busyThreadIds(),
92
+ agentName: this.deps.adapter.info().agentName,
93
+ };
94
+ }
95
+ // ---- connection lifecycle -------------------------------------------------
96
+ async connect() {
97
+ if (this.stopped || this.retryBlocked)
98
+ return;
99
+ const generation = ++this.generation;
100
+ this.state = this.attempt > 0 ? 'reconnecting' : 'connecting';
101
+ try {
102
+ const credentials = await this.fetchCredentials();
103
+ if (generation !== this.generation || this.stopped)
104
+ return;
105
+ const connection = await (this.deps.connect ?? wsconnect)({
106
+ servers: credentials.url,
107
+ authenticator: credsAuthenticator(encoder.encode(credentials.credentials)),
108
+ inboxPrefix: credentials.inboxPrefix,
109
+ name: `dinox-gateway-${this.deps.gateway.deviceId}`,
110
+ noEcho: true,
111
+ maxReconnectAttempts: -1,
112
+ reconnectDelayHandler: () => reconnectDelayMs(this.attempt++),
113
+ timeout: CREDENTIALS_TIMEOUT_MS,
114
+ });
115
+ if (generation !== this.generation || this.stopped) {
116
+ await connection.close();
117
+ return;
118
+ }
119
+ this.connection = connection;
120
+ this.userNo = credentials.userNo;
121
+ this.attempt = 0;
122
+ this.state = 'connected';
123
+ this.connectedAt = new Date().toISOString();
124
+ this.lastError = null;
125
+ this.subscribe(connection, credentials.userNo);
126
+ this.publishPresence('online');
127
+ this.flushPendingEvents();
128
+ this.presenceTimer = setInterval(() => this.publishPresence('online'), REMOTE_DEVICE_PRESENCE_INTERVAL_MS);
129
+ this.scheduleCredentialRefresh(credentials.expiresAt, generation);
130
+ void this.watchConnection(connection, generation);
131
+ this.deps.log('gateway device online', { gateway: this.deps.gateway.name, deviceId: this.deps.gateway.deviceId });
132
+ }
133
+ catch (error) {
134
+ if (generation !== this.generation)
135
+ return;
136
+ this.state = 'error';
137
+ this.lastError = error instanceof Error ? error.message : String(error);
138
+ if (error instanceof CredentialsHttpError && error.status !== 429 && error.status < 500) {
139
+ // Auth or request errors will not heal on retry; wait for a reload.
140
+ this.retryBlocked = true;
141
+ this.deps.log('gateway credentials rejected', { status: error.status, error: this.lastError });
142
+ return;
143
+ }
144
+ this.scheduleReconnect();
145
+ }
146
+ }
147
+ async watchConnection(connection, generation) {
148
+ const error = await connection.closed();
149
+ if (generation !== this.generation || this.stopped)
150
+ return;
151
+ this.lastError = error instanceof Error ? error.message : 'connection closed';
152
+ await this.disconnect({ publishOffline: false });
153
+ this.scheduleReconnect();
154
+ }
155
+ scheduleReconnect() {
156
+ if (this.stopped || this.reconnectTimer)
157
+ return;
158
+ this.state = 'reconnecting';
159
+ this.reconnectTimer = setTimeout(() => {
160
+ this.reconnectTimer = null;
161
+ void this.connect();
162
+ }, reconnectDelayMs(this.attempt++));
163
+ }
164
+ /** Short-lived credentials: reconnect with fresh ones before they expire (runs keep going). */
165
+ scheduleCredentialRefresh(expiresAt, generation) {
166
+ const expiresAtMs = Date.parse(expiresAt);
167
+ const delay = Number.isFinite(expiresAtMs) ? Math.max(1_000, expiresAtMs - Date.now() - CREDENTIALS_REFRESH_LEAD_MS) : 60_000;
168
+ this.refreshTimer = setTimeout(() => {
169
+ if (generation !== this.generation || this.stopped)
170
+ return;
171
+ void this.disconnect({ publishOffline: false }).then(() => this.connect());
172
+ }, delay);
173
+ }
174
+ async disconnect(options) {
175
+ this.generation += 1;
176
+ for (const timer of [this.presenceTimer, this.refreshTimer, this.reconnectTimer]) {
177
+ if (timer)
178
+ clearTimeout(timer);
179
+ }
180
+ this.presenceTimer = this.refreshTimer = this.reconnectTimer = null;
181
+ const connection = this.connection;
182
+ this.connection = null;
183
+ for (const subscription of this.subscriptions)
184
+ subscription.unsubscribe();
185
+ this.subscriptions = [];
186
+ if (!connection)
187
+ return;
188
+ if (options.publishOffline)
189
+ this.publishPresence('offline', connection);
190
+ await Promise.race([
191
+ connection.drain().catch(() => undefined),
192
+ new Promise((resolve) => setTimeout(resolve, DRAIN_TIMEOUT_MS)),
193
+ ]);
194
+ await connection.close().catch(() => undefined);
195
+ }
196
+ async fetchCredentials() {
197
+ const fetchImpl = this.deps.fetchImpl ?? fetch;
198
+ const response = await fetchImpl(this.deps.credentialsUrl ?? DEFAULT_NATS_CREDENTIALS_URL, {
199
+ method: 'POST',
200
+ headers: {
201
+ Authorization: toAuthorizationHeaderValue(this.deps.authorization),
202
+ 'Content-Type': 'application/json',
203
+ Accept: 'application/json',
204
+ },
205
+ body: JSON.stringify({ deviceId: this.deps.gateway.deviceId, role: 'desktop' }),
206
+ signal: AbortSignal.timeout(CREDENTIALS_TIMEOUT_MS),
207
+ });
208
+ const envelope = (await response.json().catch(() => null));
209
+ if (!response.ok || envelope?.code !== '000000' || !envelope.data) {
210
+ throw new CredentialsHttpError(response.status, envelope?.msg ?? `credentials request failed (${response.status})`);
211
+ }
212
+ const data = envelope.data;
213
+ if (data.role !== 'desktop' || data.deviceId !== this.deps.gateway.deviceId) {
214
+ throw new Error('credentials do not match this gateway device');
215
+ }
216
+ if (data.userNo !== this.deps.userId.trim().toLowerCase()) {
217
+ throw new CredentialsHttpError(403, 'credentials belong to another account');
218
+ }
219
+ return data;
220
+ }
221
+ // ---- protocol ---------------------------------------------------------------
222
+ deviceInfo() {
223
+ const adapterInfo = this.deps.adapter.info();
224
+ const info = {
225
+ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION,
226
+ protocol: REMOTE_DEVICE_PROTOCOL_NAME,
227
+ deviceId: this.deps.gateway.deviceId,
228
+ deviceName: `${this.deps.gateway.name} · ${os.hostname()}`.slice(0, 64),
229
+ platform: process.platform,
230
+ appVersion: this.deps.appVersion,
231
+ connectedAt: this.connectedAt ?? new Date().toISOString(),
232
+ busyThreadIds: this.executor.busyThreadIds(),
233
+ capabilities: {
234
+ threadRun: true,
235
+ cancel: true,
236
+ stream: 'assistant-snapshot',
237
+ toolApproval: 'desktop',
238
+ remoteToolApproval: { respond: true, decisions: ['approve_once', 'deny'], recovery: 'run-status' },
239
+ },
240
+ };
241
+ // Additive v2 extension: tells clients this device is an external agent
242
+ // (the agent owns context; Dinox model/tool settings do not apply).
243
+ info.capabilities.executor = {
244
+ kind: 'gateway',
245
+ gatewayId: this.deps.gateway.id,
246
+ runtime: this.deps.gateway.kind,
247
+ agentName: adapterInfo.agentName,
248
+ agentVersion: adapterInfo.agentVersion,
249
+ context: 'agent-session',
250
+ // Gateway extension: `agent.config.get` lists the agent's models and modes.
251
+ config: Boolean(this.deps.adapter.describeConfig),
252
+ // Gateway extension: a busy thread queues turns instead of refusing them;
253
+ // waiting turns run together as one, and cancelling a waiting turn withdraws it.
254
+ queue: { maxQueued: MAX_QUEUED_TURNS, merge: true },
255
+ };
256
+ return info;
257
+ }
258
+ subscribe(connection, userNo) {
259
+ const deviceId = this.deps.gateway.deviceId;
260
+ const route = (subject, handler) => {
261
+ const subscription = connection.subscribe(subject);
262
+ this.subscriptions.push(subscription);
263
+ void (async () => {
264
+ for await (const msg of subscription) {
265
+ try {
266
+ await handler(msg);
267
+ }
268
+ catch (error) {
269
+ this.deps.log('gateway handler failed', { subject, error: String(error) });
270
+ }
271
+ }
272
+ })();
273
+ };
274
+ route(remoteDiscoverSubject(userNo), (msg) => this.handleDiscover(msg));
275
+ route(remoteDeviceSubject(userNo, deviceId, 'info'), (msg) => this.handleInfo(msg));
276
+ route(remoteDeviceSubject(userNo, deviceId, 'request'), (msg) => this.handleRunRequest(msg));
277
+ route(remoteDeviceSubject(userNo, deviceId, 'cancel'), (msg) => this.handleCancel(msg));
278
+ route(remoteDeviceSubject(userNo, deviceId, 'approve'), (msg) => this.handleApprove(msg));
279
+ route(remoteDeviceSubject(userNo, deviceId, 'query'), (msg) => this.handleQuery(msg));
280
+ }
281
+ publishPresence(status, connection = this.connection) {
282
+ if (!connection || !this.userNo)
283
+ return;
284
+ this.presenceSequence += 1;
285
+ try {
286
+ connection.publish(remotePresenceSubject(this.userNo, this.deps.gateway.deviceId), encode({
287
+ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION,
288
+ type: 'device.presence',
289
+ status,
290
+ sequence: this.presenceSequence,
291
+ emittedAt: new Date().toISOString(),
292
+ device: this.deviceInfo(),
293
+ }));
294
+ }
295
+ catch (error) {
296
+ this.deps.log('presence publish failed', { error: String(error) });
297
+ }
298
+ }
299
+ publishRunEvent(subject, event) {
300
+ if (!this.connection) {
301
+ this.pendingEvents.set(event.runId, { subject, event });
302
+ return;
303
+ }
304
+ try {
305
+ this.connection.publish(subject, encode(event));
306
+ }
307
+ catch {
308
+ this.pendingEvents.set(event.runId, { subject, event });
309
+ }
310
+ }
311
+ flushPendingEvents() {
312
+ const pending = [...this.pendingEvents.values()];
313
+ this.pendingEvents.clear();
314
+ for (const { subject, event } of pending)
315
+ this.publishRunEvent(subject, event);
316
+ }
317
+ handleDiscover(msg) {
318
+ const { value } = decode(msg);
319
+ const requestId = value?.requestId;
320
+ if (typeof requestId !== 'string' || !msg.reply)
321
+ return;
322
+ msg.respond(encode({ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION, type: 'device.discovered', requestId, device: this.deviceInfo() }));
323
+ }
324
+ /** Only the gateway's own query is served; desktop queries (skills, workspace) are not advertised. */
325
+ async handleQuery(msg) {
326
+ const { value } = decode(msg);
327
+ if (!msg.reply || !isAgentConfigQuery(value))
328
+ return;
329
+ const result = {
330
+ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION,
331
+ type: 'agent.config.result',
332
+ requestId: value.requestId,
333
+ ok: false,
334
+ options: [],
335
+ fetchedAt: new Date().toISOString(),
336
+ };
337
+ const describe = this.deps.adapter.describeConfig?.bind(this.deps.adapter);
338
+ if (!describe) {
339
+ result.error = { code: 'unsupported', message: 'This agent has no session settings' };
340
+ }
341
+ else {
342
+ try {
343
+ result.options = await describe();
344
+ result.ok = true;
345
+ }
346
+ catch (error) {
347
+ result.error = { code: 'query_failed', message: error instanceof Error ? error.message : String(error) };
348
+ }
349
+ }
350
+ msg.respond(encode(result));
351
+ }
352
+ async handleInfo(msg) {
353
+ const { value } = decode(msg);
354
+ if (!msg.reply)
355
+ return;
356
+ if (isRunStatusRequest(value)) {
357
+ const run = await findRun(this.deps.db, { runId: value.runId, userId: this.deps.userId });
358
+ const found = Boolean(run && run.threadId === value.threadId);
359
+ msg.respond(encode({
360
+ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION,
361
+ type: 'run.status.result',
362
+ requestId: value.requestId,
363
+ runId: value.runId,
364
+ threadId: value.threadId,
365
+ found,
366
+ ...(found && run
367
+ ? {
368
+ status: run.status,
369
+ updatedAt: run.updatedAt ?? undefined,
370
+ ...(run.assistant ? { assistant: { message: run.assistant } } : {}),
371
+ pendingApprovals: run.status === 'waiting_approval'
372
+ ? projectPendingApprovals(run.assistant, { runId: run.id, threadId: run.threadId })
373
+ : [],
374
+ }
375
+ : {}),
376
+ }));
377
+ return;
378
+ }
379
+ const requestId = value?.requestId;
380
+ if (typeof requestId !== 'string')
381
+ return;
382
+ msg.respond(encode({ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION, type: 'device.info.result', requestId, device: this.deviceInfo() }));
383
+ }
384
+ respondAck(msg, request, code, extra = {}) {
385
+ const ack = {
386
+ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION,
387
+ type: 'run.ack',
388
+ requestId: request.requestId,
389
+ runId: request.runId,
390
+ threadId: request.threadId,
391
+ accepted: code === 'accepted',
392
+ code,
393
+ ...extra,
394
+ };
395
+ if (msg.reply)
396
+ msg.respond(encode(ack));
397
+ return ack;
398
+ }
399
+ cacheAck(ack) {
400
+ const now = Date.now();
401
+ for (const [key, entry] of this.ackCache) {
402
+ if (entry.expiresAt <= now || this.ackCache.size >= ACK_CACHE_MAX)
403
+ this.ackCache.delete(key);
404
+ }
405
+ this.ackCache.set(ack.requestId, { ack, expiresAt: now + ACK_CACHE_TTL_MS });
406
+ }
407
+ async handleRunRequest(msg) {
408
+ const { value, byteLength } = decode(msg);
409
+ const invalid = validateRunRequest(value, byteLength);
410
+ const raw = (value ?? {});
411
+ const ids = { requestId: String(raw.requestId ?? ''), runId: String(raw.runId ?? ''), threadId: String(raw.threadId ?? '') };
412
+ if (invalid) {
413
+ this.respondAck(msg, ids, 'invalid_request', { error: invalid });
414
+ return;
415
+ }
416
+ const request = value;
417
+ // Idempotent re-delivery: replay the ack, or recover it from the persisted run.
418
+ const cached = this.ackCache.get(request.requestId);
419
+ if (cached && cached.expiresAt > Date.now()) {
420
+ const sameRun = cached.ack.runId === request.runId && cached.ack.threadId === request.threadId;
421
+ if (msg.reply)
422
+ msg.respond(encode(sameRun ? cached.ack : { ...cached.ack, accepted: false, code: 'invalid_request' }));
423
+ return;
424
+ }
425
+ const existing = await findRun(this.deps.db, { runId: request.runId, userId: this.deps.userId });
426
+ if (existing) {
427
+ if (existing.requestId === request.requestId && existing.threadId === request.threadId) {
428
+ this.respondAck(msg, request, 'accepted', { status: existing.status });
429
+ }
430
+ else {
431
+ this.respondAck(msg, request, 'invalid_request', { error: 'runId already used by another request' });
432
+ }
433
+ return;
434
+ }
435
+ if (!this.deps.gateway.enabled) {
436
+ this.respondAck(msg, request, 'disabled');
437
+ return;
438
+ }
439
+ if (!request.createThread && !(await threadExists(this.deps.db, request.threadId, this.deps.userId))) {
440
+ this.respondAck(msg, request, 'thread_not_found');
441
+ return;
442
+ }
443
+ for (const part of request.userMessage.parts) {
444
+ if (part.type !== 'file')
445
+ continue;
446
+ const resourceId = part.providerMetadata?.dinox?.resourceId;
447
+ const resource = resourceId
448
+ ? await resolveTrustedResource(this.deps.db, { resourceId, userId: this.deps.userId })
449
+ : null;
450
+ if (!resource) {
451
+ this.respondAck(msg, request, 'attachment_unavailable');
452
+ return;
453
+ }
454
+ }
455
+ const subject = remoteRunStreamSubject(this.userNo ?? this.deps.userId, request.threadId, request.runId);
456
+ const publisher = new RunStreamPublisher(request, (event) => this.publishRunEvent(subject, event));
457
+ try {
458
+ const { threadCreated, queued } = await this.executor.accept(request, (snapshot) => {
459
+ publisher.push(snapshot);
460
+ if (TERMINAL_RUN_STATUSES.has(snapshot.status)) {
461
+ publisher.dispose();
462
+ this.publishPresence('online');
463
+ }
464
+ });
465
+ const ack = this.respondAck(msg, request, 'accepted', {
466
+ status: 'queued',
467
+ // Gateway extension: the turn waits behind the thread's active one.
468
+ ...(queued ? { waiting: true } : {}),
469
+ ...(request.createThread
470
+ ? { threadDisposition: threadCreated ? 'created_desktop_local' : 'already_present_desktop_local' }
471
+ : {}),
472
+ });
473
+ this.cacheAck(ack);
474
+ this.publishPresence('online');
475
+ }
476
+ catch (error) {
477
+ publisher.dispose();
478
+ if (error instanceof QueueFullError) {
479
+ this.respondAck(msg, request, 'busy', { error: error.message });
480
+ return;
481
+ }
482
+ this.respondAck(msg, request, 'enqueue_failed', { error: error instanceof Error ? error.message : String(error) });
483
+ }
484
+ }
485
+ handleCancel(msg) {
486
+ const { value } = decode(msg);
487
+ if (!isCancelRequest(value))
488
+ return;
489
+ const cancelled = this.executor.cancel(value.runId, value.threadId);
490
+ msg.respond(encode({
491
+ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION,
492
+ type: 'run.cancel.ack',
493
+ requestId: value.requestId,
494
+ runId: value.runId,
495
+ threadId: value.threadId,
496
+ cancelled,
497
+ ...(cancelled ? {} : { error: 'run is not active on this gateway' }),
498
+ }));
499
+ }
500
+ async handleApprove(msg) {
501
+ const { value, byteLength } = decode(msg);
502
+ const invalid = validateApprovalRequest(value, byteLength);
503
+ const raw = (value ?? {});
504
+ const base = {
505
+ protocolVersion: REMOTE_DEVICE_PROTOCOL_VERSION,
506
+ type: 'tool.approval.ack',
507
+ requestId: String(raw.requestId ?? ''),
508
+ runId: String(raw.runId ?? ''),
509
+ threadId: String(raw.threadId ?? ''),
510
+ approvalId: String(raw.approvalId ?? ''),
511
+ };
512
+ if (invalid) {
513
+ msg.respond(encode({ ...base, accepted: false, code: 'invalid_request', error: invalid }));
514
+ return;
515
+ }
516
+ const code = this.executor.respondApproval({
517
+ runId: base.runId,
518
+ threadId: base.threadId,
519
+ approvalId: base.approvalId,
520
+ approved: raw.approved === true,
521
+ });
522
+ const run = await findRun(this.deps.db, { runId: base.runId, userId: this.deps.userId });
523
+ msg.respond(encode({
524
+ ...base,
525
+ accepted: code === 'applied' || code === 'already_applied',
526
+ code,
527
+ ...(run ? { status: run.status, pendingApprovals: projectPendingApprovals(run.assistant, base) } : {}),
528
+ }));
529
+ }
530
+ }
@@ -0,0 +1,4 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export type DaemonCall = (method: string, params?: unknown) => Promise<unknown>;
3
+ export declare function createDinoxMcpServer(call?: DaemonCall): McpServer;
4
+ export declare function serveDinoxMcpOverStdio(): Promise<void>;