@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.
- package/README.md +17 -3
- package/dist/cli.js +6 -0
- package/dist/commands/domain/options.d.ts +10 -0
- package/dist/commands/domain/options.js +75 -0
- package/dist/commands/domain/run.d.ts +24 -0
- package/dist/commands/domain/run.js +137 -0
- package/dist/commands/gateway/index.d.ts +4 -0
- package/dist/commands/gateway/index.js +300 -0
- package/dist/commands/notes/actions.js +47 -18
- package/dist/commands/notes/createFromTemplate.d.ts +35 -0
- package/dist/commands/notes/createFromTemplate.js +58 -0
- package/dist/commands/notes/descriptors.js +9 -4
- package/dist/commands/notes/repo/read.js +4 -23
- package/dist/commands/notes/repo/write.d.ts +2 -0
- package/dist/commands/notes/repo/write.js +1 -1
- package/dist/commands/properties/index.d.ts +16 -0
- package/dist/commands/properties/index.js +640 -0
- package/dist/commands/properties/repo.d.ts +150 -0
- package/dist/commands/properties/repo.js +272 -0
- package/dist/commands/savedViews/fields.d.ts +28 -0
- package/dist/commands/savedViews/fields.js +87 -0
- package/dist/commands/savedViews/index.js +203 -164
- package/dist/commands/savedViews/model.d.ts +27 -69
- package/dist/commands/savedViews/model.js +63 -279
- package/dist/commands/savedViews/repo.d.ts +64 -62
- package/dist/commands/savedViews/repo.js +309 -428
- package/dist/commands/schema/renderSkills.js +33 -5
- package/dist/commands/schema/spec.js +6 -0
- package/dist/commands/templates/index.d.ts +4 -0
- package/dist/commands/templates/index.js +290 -0
- package/dist/commands/templates/model.d.ts +12 -34
- package/dist/commands/templates/model.js +39 -83
- package/dist/commands/templates/repo.d.ts +53 -0
- package/dist/commands/templates/repo.js +78 -0
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +1 -1
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.js +31 -0
- package/dist/gateway/adapters/acp.d.ts +42 -0
- package/dist/gateway/adapters/acp.js +285 -0
- package/dist/gateway/adapters/echo.d.ts +12 -0
- package/dist/gateway/adapters/echo.js +51 -0
- package/dist/gateway/adapters/index.d.ts +9 -0
- package/dist/gateway/adapters/index.js +30 -0
- package/dist/gateway/assistantProjection.d.ts +32 -0
- package/dist/gateway/assistantProjection.js +193 -0
- package/dist/gateway/config.d.ts +50 -0
- package/dist/gateway/config.js +253 -0
- package/dist/gateway/detect.d.ts +40 -0
- package/dist/gateway/detect.js +145 -0
- package/dist/gateway/deviceHost.d.ts +82 -0
- package/dist/gateway/deviceHost.js +530 -0
- package/dist/gateway/mcpServer.d.ts +4 -0
- package/dist/gateway/mcpServer.js +118 -0
- package/dist/gateway/protocol.d.ts +29 -0
- package/dist/gateway/protocol.js +127 -0
- package/dist/gateway/runExecutor.d.ts +67 -0
- package/dist/gateway/runExecutor.js +316 -0
- package/dist/gateway/service.d.ts +26 -0
- package/dist/gateway/service.js +82 -0
- package/dist/gateway/streamPublisher.d.ts +33 -0
- package/dist/gateway/streamPublisher.js +93 -0
- package/dist/gateway/threadStore.d.ts +157 -0
- package/dist/gateway/threadStore.js +430 -0
- package/dist/gateway/types.d.ts +117 -0
- package/dist/gateway/types.js +9 -0
- package/dist/skills/registry.js +7 -0
- package/dist/utils/errorCodes.d.ts +212 -0
- package/dist/utils/errorCodes.js +53 -0
- package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
- package/dist/vendor/electron/domain/note-templates.js +232 -0
- package/dist/vendor/electron/domain/properties.d.ts +10 -0
- package/dist/vendor/electron/domain/properties.js +1549 -0
- package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
- package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
- package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
- package/dist/vendor/electron/domain/saved-views.js +1120 -0
- package/dist/vendor/electron/rpc.d.ts +22 -0
- package/dist/vendor/electron/rpc.js +27 -0
- package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
- package/dist/vendor/electron/shared/ai-errors.js +107 -0
- package/dist/vendor/electron/shared/external-types.d.ts +20 -0
- package/dist/vendor/electron/shared/external-types.js +1 -0
- package/dist/vendor/electron/shared/index.d.ts +9 -0
- package/dist/vendor/electron/shared/index.js +7 -0
- package/dist/vendor/electron/shared/note-properties.d.ts +328 -0
- package/dist/vendor/electron/shared/note-properties.js +1027 -0
- package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
- package/dist/vendor/electron/shared/note-templates.js +84 -0
- package/dist/vendor/electron/shared/powersync-read-models.d.ts +1716 -0
- package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
- package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
- package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
- package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
- package/dist/vendor/electron/shared/saved-views.js +981 -0
- package/dist/vendor/electron/sql-utils.d.ts +5 -0
- package/dist/vendor/electron/sql-utils.js +32 -0
- package/package.json +10 -4
- package/dist/commands/savedViews/semantics.d.ts +0 -46
- package/dist/commands/savedViews/semantics.js +0 -738
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { withPowerSyncWriteTransaction } from '../powersync/runtime.js';
|
|
2
|
+
const THREAD_CHAT_TYPE = 'thread';
|
|
3
|
+
const MESSAGE_ENTRY_TYPE = 'message';
|
|
4
|
+
const LEGACY_MESSAGE_ENTRY_TYPE = 'thread';
|
|
5
|
+
const CONTEXT_RESET_ENTRY_TYPE = 'internal';
|
|
6
|
+
const CHAT_MESSAGE_VERSION = 'v1';
|
|
7
|
+
const ORDER_INDEX_STEP = 1024;
|
|
8
|
+
const RUN_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
9
|
+
const MESSAGE_ENTRY_FILTER = '(entry_type IS NULL OR entry_type = \'\' OR entry_type = ? OR entry_type = ?)';
|
|
10
|
+
export const TERMINAL_RUN_STATUSES = new Set([
|
|
11
|
+
'succeeded',
|
|
12
|
+
'failed',
|
|
13
|
+
'cancelled',
|
|
14
|
+
'interrupted',
|
|
15
|
+
'expired',
|
|
16
|
+
]);
|
|
17
|
+
function safeJson(value) {
|
|
18
|
+
return JSON.stringify(value ?? null);
|
|
19
|
+
}
|
|
20
|
+
function parseJson(value) {
|
|
21
|
+
if (typeof value !== 'string' || !value)
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(value);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function toOrderIndex(value) {
|
|
31
|
+
const parsed = typeof value === 'string' ? Number(value) : value;
|
|
32
|
+
return typeof parsed === 'number' && Number.isSafeInteger(parsed) ? parsed : null;
|
|
33
|
+
}
|
|
34
|
+
/** Mirrors desktop `withRunMetadata`: the UI reads `metadata.status` for streaming state. */
|
|
35
|
+
export function withRunMetadata(message, runId, status) {
|
|
36
|
+
const uiStatus = status === 'succeeded' || status === 'waiting_approval'
|
|
37
|
+
? 'done'
|
|
38
|
+
: TERMINAL_RUN_STATUSES.has(status)
|
|
39
|
+
? 'error'
|
|
40
|
+
: 'streaming';
|
|
41
|
+
return { ...message, metadata: { ...(message.metadata ?? {}), runId, status: uiStatus } };
|
|
42
|
+
}
|
|
43
|
+
function withQueueMarker(message, marker) {
|
|
44
|
+
const { queue: _previous, ...metadata } = (message.metadata ?? {});
|
|
45
|
+
return { ...message, metadata: marker ? { ...metadata, queue: marker } : metadata };
|
|
46
|
+
}
|
|
47
|
+
/** The answering run merged this turn into a later one; it never gets its own reply. */
|
|
48
|
+
export function withMergedInto(message, mergedIntoRunId) {
|
|
49
|
+
return { ...message, metadata: { ...(message.metadata ?? {}), mergedIntoRunId } };
|
|
50
|
+
}
|
|
51
|
+
function messageCreatedAt(message, fallback) {
|
|
52
|
+
const raw = message.metadata?.createTime;
|
|
53
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
54
|
+
return new Date(raw).toISOString();
|
|
55
|
+
if (typeof raw === 'string' && raw.trim())
|
|
56
|
+
return raw;
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
/** Next `order_index` after `parentId` (midpoint before its next sibling) or at the end. */
|
|
60
|
+
export async function resolveNextOrderIndex(tx, input) {
|
|
61
|
+
const row = await tx.getOptional(`SELECT MAX(order_index) AS max_order_index, COUNT(*) AS message_count,
|
|
62
|
+
(SELECT order_index FROM c_chat_message
|
|
63
|
+
WHERE id = ? AND type = ? AND source_id = ? AND deleted_at IS NULL AND ${MESSAGE_ENTRY_FILTER}
|
|
64
|
+
LIMIT 1) AS parent_order_index
|
|
65
|
+
FROM c_chat_message
|
|
66
|
+
WHERE type = ? AND source_id = ? AND deleted_at IS NULL AND ${MESSAGE_ENTRY_FILTER}`, [
|
|
67
|
+
input.parentId ?? '',
|
|
68
|
+
THREAD_CHAT_TYPE,
|
|
69
|
+
input.threadId,
|
|
70
|
+
MESSAGE_ENTRY_TYPE,
|
|
71
|
+
LEGACY_MESSAGE_ENTRY_TYPE,
|
|
72
|
+
THREAD_CHAT_TYPE,
|
|
73
|
+
input.threadId,
|
|
74
|
+
MESSAGE_ENTRY_TYPE,
|
|
75
|
+
LEGACY_MESSAGE_ENTRY_TYPE,
|
|
76
|
+
]);
|
|
77
|
+
const maxOrderIndex = toOrderIndex(row?.max_order_index);
|
|
78
|
+
const parentOrderIndex = toOrderIndex(row?.parent_order_index);
|
|
79
|
+
const countBased = Math.max(0, Number(row?.message_count ?? 0)) * ORDER_INDEX_STEP;
|
|
80
|
+
if (parentOrderIndex != null) {
|
|
81
|
+
const next = await tx.getOptional(`SELECT MIN(order_index) AS next_order_index FROM c_chat_message
|
|
82
|
+
WHERE type = ? AND source_id = ? AND deleted_at IS NULL AND order_index > ? AND ${MESSAGE_ENTRY_FILTER}`, [THREAD_CHAT_TYPE, input.threadId, parentOrderIndex, MESSAGE_ENTRY_TYPE, LEGACY_MESSAGE_ENTRY_TYPE]);
|
|
83
|
+
const nextOrderIndex = toOrderIndex(next?.next_order_index);
|
|
84
|
+
if (nextOrderIndex != null && nextOrderIndex - parentOrderIndex > 1) {
|
|
85
|
+
return parentOrderIndex + Math.floor((nextOrderIndex - parentOrderIndex) / 2);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return Math.max(maxOrderIndex ?? 0, parentOrderIndex ?? 0, countBased) + ORDER_INDEX_STEP;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The thread's last message, by transcript order. A new turn is appended after
|
|
92
|
+
* it: what the client believed was last can be stale or out of order (another
|
|
93
|
+
* device, a lagging sync, a display glitch), and trusting it would splice the
|
|
94
|
+
* turn into the middle of the conversation.
|
|
95
|
+
*/
|
|
96
|
+
async function resolveThreadTail(tx, threadId) {
|
|
97
|
+
const row = await tx.getOptional(`SELECT id FROM c_chat_message
|
|
98
|
+
WHERE type = ? AND source_id = ? AND deleted_at IS NULL AND ${MESSAGE_ENTRY_FILTER}
|
|
99
|
+
ORDER BY order_index DESC, created_at DESC, id DESC LIMIT 1`, [THREAD_CHAT_TYPE, threadId, MESSAGE_ENTRY_TYPE, LEGACY_MESSAGE_ENTRY_TYPE]);
|
|
100
|
+
return row?.id ?? null;
|
|
101
|
+
}
|
|
102
|
+
export async function threadExists(db, threadId, userId) {
|
|
103
|
+
const row = await db.getOptional('SELECT id FROM c_thread WHERE id = ? AND user_id = ? AND is_del = 0 LIMIT 1', [threadId, userId]);
|
|
104
|
+
return Boolean(row?.id);
|
|
105
|
+
}
|
|
106
|
+
/** Creates (or revives) the thread a remote run targets. Returns true when a row was written. */
|
|
107
|
+
async function ensureThread(tx, input, now) {
|
|
108
|
+
const existing = await tx.getOptional('SELECT id, is_del FROM c_thread WHERE id = ? LIMIT 1', [input.id]);
|
|
109
|
+
if (existing?.id && Number(existing.is_del) !== 1)
|
|
110
|
+
return false;
|
|
111
|
+
const name = (input.name ?? '').trim().slice(0, 120) || 'Untitled';
|
|
112
|
+
const values = [
|
|
113
|
+
name,
|
|
114
|
+
input.kind || 'chat',
|
|
115
|
+
input.userId,
|
|
116
|
+
input.projectId ?? null,
|
|
117
|
+
now,
|
|
118
|
+
now,
|
|
119
|
+
safeJson(input.metaData ?? {}),
|
|
120
|
+
input.cmdId ?? null,
|
|
121
|
+
safeJson(input.config ?? {}),
|
|
122
|
+
];
|
|
123
|
+
if (existing?.id) {
|
|
124
|
+
await tx.execute(`UPDATE c_thread SET name = ?, kind = ?, user_id = ?, project_id = ?, last_opened_at = ?, updated_at = ?,
|
|
125
|
+
meta_data = ?, cmd_id = ?, config = ?, filter_condition = '{}', snapshot = '{}', memory = '{}',
|
|
126
|
+
is_del = 0, robot_session_id = NULL, compact_context = NULL
|
|
127
|
+
WHERE id = ?`, [...values, input.id]);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
await tx.execute(`INSERT INTO c_thread (id, name, kind, user_id, project_id, last_opened_at, updated_at, meta_data, cmd_id, config,
|
|
131
|
+
created_at, messages, pinned_at, filter_condition, snapshot, memory, is_del, robot_session_id, compact_context)
|
|
132
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '[]', NULL, '{}', '{}', '{}', 0, NULL, NULL)`, [input.id, ...values, now]);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
async function writeChatMessage(tx, input) {
|
|
136
|
+
const existing = await tx.getOptional('SELECT id, order_index FROM c_chat_message WHERE id = ? LIMIT 1', [input.message.id]);
|
|
137
|
+
const orderIndex = toOrderIndex(existing?.order_index) ??
|
|
138
|
+
(await resolveNextOrderIndex(tx, { threadId: input.threadId, parentId: input.parentId }));
|
|
139
|
+
if (existing?.id) {
|
|
140
|
+
await tx.execute(`UPDATE c_chat_message SET role = ?, type = ?, entry_type = ?, source_id = ?, parent_id = ?, content = ?,
|
|
141
|
+
updated_at = ?, order_index = ?, version = ?, user_id = ?, deleted_at = NULL
|
|
142
|
+
WHERE id = ?`, [
|
|
143
|
+
input.message.role,
|
|
144
|
+
THREAD_CHAT_TYPE,
|
|
145
|
+
MESSAGE_ENTRY_TYPE,
|
|
146
|
+
input.threadId,
|
|
147
|
+
input.parentId,
|
|
148
|
+
safeJson(input.message),
|
|
149
|
+
input.updatedAt,
|
|
150
|
+
orderIndex,
|
|
151
|
+
CHAT_MESSAGE_VERSION,
|
|
152
|
+
input.userId,
|
|
153
|
+
input.message.id,
|
|
154
|
+
]);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await tx.execute(`INSERT INTO c_chat_message (id, role, type, entry_type, source_id, parent_id, content, created_at, updated_at,
|
|
158
|
+
order_index, meta_data, version, user_id, deleted_at)
|
|
159
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, NULL)`, [
|
|
160
|
+
input.message.id,
|
|
161
|
+
input.message.role,
|
|
162
|
+
THREAD_CHAT_TYPE,
|
|
163
|
+
MESSAGE_ENTRY_TYPE,
|
|
164
|
+
input.threadId,
|
|
165
|
+
input.parentId,
|
|
166
|
+
safeJson(input.message),
|
|
167
|
+
input.createdAt,
|
|
168
|
+
input.updatedAt,
|
|
169
|
+
orderIndex,
|
|
170
|
+
CHAT_MESSAGE_VERSION,
|
|
171
|
+
input.userId,
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
/** Persists the accepted turn atomically: thread (if new), user message, queued run. */
|
|
175
|
+
export async function enqueueRun(db, input) {
|
|
176
|
+
const now = new Date().toISOString();
|
|
177
|
+
return withPowerSyncWriteTransaction(db, async (tx) => {
|
|
178
|
+
const threadCreated = input.createThread
|
|
179
|
+
? await ensureThread(tx, { ...input.createThread, userId: input.userId }, now)
|
|
180
|
+
: false;
|
|
181
|
+
const createdAt = messageCreatedAt(input.userMessage, now);
|
|
182
|
+
const parentId = input.queuedAfterMessageId ?? (await resolveThreadTail(tx, input.threadId)) ?? input.parentMessageId;
|
|
183
|
+
const userMessage = input.queued
|
|
184
|
+
? withQueueMarker(input.userMessage, { state: 'queued', runId: input.runId, executor: 'gateway', queuedAt: now })
|
|
185
|
+
: input.userMessage;
|
|
186
|
+
await writeChatMessage(tx, {
|
|
187
|
+
threadId: input.threadId,
|
|
188
|
+
userId: input.userId,
|
|
189
|
+
message: userMessage,
|
|
190
|
+
parentId,
|
|
191
|
+
createdAt,
|
|
192
|
+
updatedAt: createdAt,
|
|
193
|
+
});
|
|
194
|
+
await tx.execute(`INSERT INTO local_thread_run (id, thread_id, user_id, status, trigger, user_message_id, assistant_message_id,
|
|
195
|
+
parent_message_id, queued_at, permission_mode, request_payload_json, assistant_snapshot_json, queue_index,
|
|
196
|
+
created_at, updated_at)
|
|
197
|
+
VALUES (?, ?, ?, 'queued', 'submit-message', ?, ?, ?, ?, 'default', ?, ?, ?, ?, ?)`, [
|
|
198
|
+
input.runId,
|
|
199
|
+
input.threadId,
|
|
200
|
+
input.userId,
|
|
201
|
+
input.userMessage.id,
|
|
202
|
+
input.assistantMessage.id,
|
|
203
|
+
parentId,
|
|
204
|
+
now,
|
|
205
|
+
safeJson({ ...input.requestPayload, metadata: { remoteDevice: { requestId: input.requestId } } }),
|
|
206
|
+
safeJson(withRunMetadata(input.assistantMessage, input.runId, 'queued')),
|
|
207
|
+
input.queueIndex,
|
|
208
|
+
now,
|
|
209
|
+
now,
|
|
210
|
+
]);
|
|
211
|
+
return { threadCreated };
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/** Live progress for `run.status` recovery; never touches synced tables. */
|
|
215
|
+
export async function updateRunSnapshot(db, input) {
|
|
216
|
+
const now = new Date().toISOString();
|
|
217
|
+
await db.execute(`UPDATE local_thread_run SET status = ?, assistant_snapshot_json = ?, updated_at = ?, last_heartbeat_at = ?,
|
|
218
|
+
started_at = COALESCE(started_at, ?)
|
|
219
|
+
WHERE id = ? AND status NOT IN ('succeeded', 'failed', 'cancelled', 'interrupted', 'expired')`, [input.status, safeJson(withRunMetadata(input.assistant, input.runId, input.status)), now, now, now, input.runId]);
|
|
220
|
+
}
|
|
221
|
+
/** Ends the run and writes the final assistant message into the synced transcript. */
|
|
222
|
+
export async function commitRunTerminal(db, input) {
|
|
223
|
+
const finishedAt = new Date().toISOString();
|
|
224
|
+
const assistant = withRunMetadata(input.assistant, input.runId, input.status);
|
|
225
|
+
await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
226
|
+
await writeChatMessage(tx, {
|
|
227
|
+
threadId: input.threadId,
|
|
228
|
+
userId: input.userId,
|
|
229
|
+
message: assistant,
|
|
230
|
+
parentId: input.userMessageId,
|
|
231
|
+
createdAt: messageCreatedAt(assistant, finishedAt),
|
|
232
|
+
updatedAt: finishedAt,
|
|
233
|
+
});
|
|
234
|
+
await tx.execute(`UPDATE local_thread_run SET status = ?, error_text = ?, finished_at = ?, updated_at = ?, expires_at = NULL,
|
|
235
|
+
cleanup_after_at = ?, assistant_snapshot_json = ?
|
|
236
|
+
WHERE id = ?`, [
|
|
237
|
+
input.status,
|
|
238
|
+
input.errorText,
|
|
239
|
+
finishedAt,
|
|
240
|
+
finishedAt,
|
|
241
|
+
new Date(Date.parse(finishedAt) + RUN_RETENTION_MS).toISOString(),
|
|
242
|
+
safeJson(assistant),
|
|
243
|
+
input.runId,
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
export async function findRun(db, input) {
|
|
248
|
+
const row = await db.getOptional(`SELECT id, thread_id, status, request_payload_json, assistant_snapshot_json, updated_at
|
|
249
|
+
FROM local_thread_run WHERE id = ? AND user_id = ? LIMIT 1`, [input.runId, input.userId]);
|
|
250
|
+
if (!row)
|
|
251
|
+
return null;
|
|
252
|
+
const payload = parseJson(row.request_payload_json);
|
|
253
|
+
const requestId = payload?.metadata?.remoteDevice?.requestId;
|
|
254
|
+
return {
|
|
255
|
+
id: row.id,
|
|
256
|
+
threadId: row.thread_id,
|
|
257
|
+
status: row.status,
|
|
258
|
+
requestId: typeof requestId === 'string' ? requestId : null,
|
|
259
|
+
assistant: parseJson(row.assistant_snapshot_json),
|
|
260
|
+
updatedAt: row.updated_at,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** Rewrites a user message's queue marker in place; a missing message is ignored. */
|
|
264
|
+
async function writeQueueMarker(tx, messageId, marker, extra = {}) {
|
|
265
|
+
const row = await tx.getOptional('SELECT content FROM c_chat_message WHERE id = ? LIMIT 1', [messageId]);
|
|
266
|
+
const message = parseJson(row?.content);
|
|
267
|
+
if (!message)
|
|
268
|
+
return;
|
|
269
|
+
const now = new Date().toISOString();
|
|
270
|
+
await tx.execute(`UPDATE c_chat_message SET content = ?, updated_at = ?${extra.deletedAt ? ', deleted_at = ?' : ''} WHERE id = ?`, extra.deletedAt
|
|
271
|
+
? [safeJson(withQueueMarker(message, marker)), now, extra.deletedAt, messageId]
|
|
272
|
+
: [safeJson(withQueueMarker(message, marker)), now, messageId]);
|
|
273
|
+
}
|
|
274
|
+
/** The queued turns start now: their messages stop showing as queued. */
|
|
275
|
+
export async function startQueuedRuns(db, userMessageIds) {
|
|
276
|
+
if (userMessageIds.length === 0)
|
|
277
|
+
return;
|
|
278
|
+
await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
279
|
+
for (const messageId of userMessageIds)
|
|
280
|
+
await writeQueueMarker(tx, messageId, null);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/** Ends a queued turn that was merged into a later one: no reply of its own is written. */
|
|
284
|
+
export async function finishMergedRun(db, input) {
|
|
285
|
+
const now = new Date().toISOString();
|
|
286
|
+
const assistant = withRunMetadata(withMergedInto(input.assistant, input.mergedIntoRunId), input.runId, 'succeeded');
|
|
287
|
+
await db.execute(`UPDATE local_thread_run SET status = 'succeeded', finished_at = ?, updated_at = ?, assistant_snapshot_json = ?,
|
|
288
|
+
cleanup_after_at = ?
|
|
289
|
+
WHERE id = ?`, [now, now, safeJson(assistant), new Date(Date.parse(now) + RUN_RETENTION_MS).toISOString(), input.runId]);
|
|
290
|
+
return assistant;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The user took a queued turn back before it ran: the run is cancelled and its
|
|
294
|
+
* message removed from the transcript (marked `withdrawn` so other devices can
|
|
295
|
+
* tell a withdrawal from a deletion).
|
|
296
|
+
*/
|
|
297
|
+
export async function withdrawQueuedRun(db, input) {
|
|
298
|
+
const now = new Date().toISOString();
|
|
299
|
+
const assistant = withRunMetadata(input.assistant, input.runId, 'cancelled');
|
|
300
|
+
await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
301
|
+
await writeQueueMarker(tx, input.userMessageId, { state: 'withdrawn', runId: input.runId, executor: 'gateway', queuedAt: now }, { deletedAt: now });
|
|
302
|
+
await tx.execute(`UPDATE local_thread_run SET status = 'cancelled', finished_at = ?, updated_at = ?, assistant_snapshot_json = ?,
|
|
303
|
+
error_text = 'withdrawn by user', cleanup_after_at = ?
|
|
304
|
+
WHERE id = ?`, [now, now, safeJson(assistant), new Date(Date.parse(now) + RUN_RETENTION_MS).toISOString(), input.runId]);
|
|
305
|
+
});
|
|
306
|
+
return assistant;
|
|
307
|
+
}
|
|
308
|
+
/** Runs left non-terminal by a previous process can never finish; mark them interrupted. */
|
|
309
|
+
export async function interruptOrphanedRuns(db, userId) {
|
|
310
|
+
const now = new Date().toISOString();
|
|
311
|
+
const rows = await db.getAll(`SELECT id, user_message_id FROM local_thread_run
|
|
312
|
+
WHERE user_id = ? AND status NOT IN ('succeeded', 'failed', 'cancelled', 'interrupted', 'expired')
|
|
313
|
+
AND request_payload_json LIKE '%"gatewayId"%'`, [userId]);
|
|
314
|
+
for (const row of rows) {
|
|
315
|
+
await db.execute(`UPDATE local_thread_run SET status = 'interrupted', error_text = ?, finished_at = ?, updated_at = ? WHERE id = ?`, ['gateway host restarted', now, now, row.id]);
|
|
316
|
+
// A turn that was still queued will never run: it must not show as queued forever.
|
|
317
|
+
if (row.user_message_id)
|
|
318
|
+
await writeQueueMarker(db, row.user_message_id, null);
|
|
319
|
+
}
|
|
320
|
+
return rows.length;
|
|
321
|
+
}
|
|
322
|
+
export async function readSessionBinding(db, threadId) {
|
|
323
|
+
const row = await db.getOptional('SELECT robot_session_id FROM c_thread WHERE id = ? LIMIT 1', [threadId]);
|
|
324
|
+
const parsed = parseJson(row?.robot_session_id);
|
|
325
|
+
if (parsed?.v !== 1 || typeof parsed.sessionId !== 'string' || typeof parsed.gatewayId !== 'string')
|
|
326
|
+
return null;
|
|
327
|
+
return {
|
|
328
|
+
sessionId: parsed.sessionId,
|
|
329
|
+
topicId: typeof parsed.topicId === 'string' ? parsed.topicId : null,
|
|
330
|
+
gatewayId: parsed.gatewayId,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
export async function writeSessionBinding(db, threadId, binding) {
|
|
334
|
+
await db.execute('UPDATE c_thread SET robot_session_id = ?, updated_at = ? WHERE id = ?', [
|
|
335
|
+
safeJson({ v: 1, ...binding }),
|
|
336
|
+
new Date().toISOString(),
|
|
337
|
+
threadId,
|
|
338
|
+
]);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The thread's current topic: the latest "new topic" marker the app wrote
|
|
342
|
+
* (`entry_type = 'internal'`, `meta_data.kind = 'context_reset'`). A new topic
|
|
343
|
+
* starts a fresh agent session.
|
|
344
|
+
*/
|
|
345
|
+
export async function readCurrentTopicId(db, threadId) {
|
|
346
|
+
const rows = await db.getAll(`SELECT id, meta_data FROM c_chat_message
|
|
347
|
+
WHERE type = ? AND source_id = ? AND entry_type = ? AND deleted_at IS NULL
|
|
348
|
+
ORDER BY julianday(created_at) DESC, id DESC LIMIT 20`, [THREAD_CHAT_TYPE, threadId, CONTEXT_RESET_ENTRY_TYPE]);
|
|
349
|
+
for (const row of rows) {
|
|
350
|
+
if (parseJson(row.meta_data)?.kind === 'context_reset')
|
|
351
|
+
return row.id;
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
/** File parts resolve only to resources this user owns with an https storage URL. */
|
|
356
|
+
export async function resolveTrustedResource(db, input) {
|
|
357
|
+
const row = await db.getOptional(`SELECT storage_url, name, mime_type FROM c_resource
|
|
358
|
+
WHERE id = ? AND user_id = ? AND (is_del IS NULL OR is_del = 0) AND deleted_at IS NULL LIMIT 1`, [input.resourceId, input.userId]);
|
|
359
|
+
if (!row?.storage_url?.startsWith('https://'))
|
|
360
|
+
return null;
|
|
361
|
+
return { url: row.storage_url, name: row.name ?? 'attachment', mediaType: row.mime_type };
|
|
362
|
+
}
|
|
363
|
+
function robotMetaData(input) {
|
|
364
|
+
return JSON.stringify({ gateway: { machineId: input.machineId, ...(input.agentId ? { agentId: input.agentId } : {}) } });
|
|
365
|
+
}
|
|
366
|
+
export async function upsertRobot(db, input) {
|
|
367
|
+
const now = new Date().toISOString();
|
|
368
|
+
// The backend rejects NULL c_robot columns, and PowerSync omits NULLs only from inserts.
|
|
369
|
+
const cwd = input.cwd ?? '';
|
|
370
|
+
const metaData = robotMetaData(input);
|
|
371
|
+
const existing = await db.getOptional('SELECT name, device_id, device_name, agent_type, cwd, meta_data, is_del FROM c_robot WHERE id = ? LIMIT 1', [
|
|
372
|
+
input.id,
|
|
373
|
+
]);
|
|
374
|
+
if (existing) {
|
|
375
|
+
const unchanged = existing.name === input.name
|
|
376
|
+
&& existing.device_id === input.deviceId
|
|
377
|
+
&& existing.device_name === input.deviceName
|
|
378
|
+
&& existing.agent_type === input.agentType
|
|
379
|
+
&& (existing.cwd ?? '') === cwd
|
|
380
|
+
&& sameJson(existing.meta_data, metaData)
|
|
381
|
+
&& !existing.is_del;
|
|
382
|
+
// Every daemon start re-registers; skip no-op writes so they do not queue uploads.
|
|
383
|
+
if (unchanged)
|
|
384
|
+
return;
|
|
385
|
+
// A removed gateway that is added again revives its robot (ids are deterministic).
|
|
386
|
+
await db.execute(`UPDATE c_robot SET name = ?, device_id = ?, device_name = ?, agent_type = ?, cwd = ?, meta_data = ?,
|
|
387
|
+
updated_at = ?, is_del = 0
|
|
388
|
+
WHERE id = ?`, [input.name, input.deviceId, input.deviceName, input.agentType, cwd, metaData, now, input.id]);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
await db.execute(`INSERT INTO c_robot (id, name, device_id, device_name, agent_type, cwd, config, meta_data, created_at, updated_at,
|
|
392
|
+
user_id, is_del)
|
|
393
|
+
VALUES (?, ?, ?, ?, ?, ?, '{}', ?, ?, ?, ?, 0)`, [input.id, input.name, input.deviceId, input.deviceName, input.agentType, cwd, metaData, now, now, input.userId]);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Soft-deletes this machine's gateway robots that are no longer configured
|
|
397
|
+
* (removed, or replaced before ids were deterministic), so the app never
|
|
398
|
+
* offers an agent that nothing hosts. Robots from other machines are untouched.
|
|
399
|
+
*/
|
|
400
|
+
export async function retireRemovedRobots(db, input) {
|
|
401
|
+
const rows = await db.getAll(`SELECT id, meta_data FROM c_robot WHERE user_id = ? AND COALESCE(is_del, 0) = 0`, [input.userId]);
|
|
402
|
+
const keep = new Set(input.keepIds);
|
|
403
|
+
const retired = rows
|
|
404
|
+
.filter((row) => !keep.has(row.id) && robotMachineId(row.meta_data) === input.machineId)
|
|
405
|
+
.map((row) => row.id);
|
|
406
|
+
for (const id of retired)
|
|
407
|
+
await softDeleteRobot(db, id);
|
|
408
|
+
return retired;
|
|
409
|
+
}
|
|
410
|
+
/** The backend re-serializes JSON columns, so compare them parsed. */
|
|
411
|
+
function sameJson(stored, expected) {
|
|
412
|
+
try {
|
|
413
|
+
return JSON.stringify(JSON.parse(stored ?? '')) === expected;
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function robotMachineId(metaData) {
|
|
420
|
+
try {
|
|
421
|
+
const parsed = JSON.parse(metaData ?? '');
|
|
422
|
+
return typeof parsed?.gateway?.machineId === 'string' ? parsed.gateway.machineId : null;
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
export async function softDeleteRobot(db, robotId) {
|
|
429
|
+
await db.execute('UPDATE c_robot SET is_del = 1, updated_at = ? WHERE id = ?', [new Date().toISOString(), robotId]);
|
|
430
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dinox agent gateway: hosts external agent runtimes (Hermes, OpenClaw, pi,
|
|
3
|
+
* Claude Code, Codex, ... over ACP) as remote devices the Dinox app can drive.
|
|
4
|
+
*
|
|
5
|
+
* The app sees each gateway as one `dinox.remote-device.v2` device. The agent
|
|
6
|
+
* owns its conversation context (gateway-session mode): each turn sends only
|
|
7
|
+
* the new user message, while Dinox keeps a display/sync copy of the transcript.
|
|
8
|
+
*/
|
|
9
|
+
export type GatewayKind = 'acp' | 'echo';
|
|
10
|
+
/** Local-only gateway definition (never synced: it may carry commands and env). */
|
|
11
|
+
export type GatewayDefinition = {
|
|
12
|
+
/** Stable id; also the `c_robot.id` the app binds agents to. */
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
kind: GatewayKind;
|
|
16
|
+
/** Detected agent type (`cursor`, `opencode`, ...); one gateway per type per machine. */
|
|
17
|
+
agentId?: string;
|
|
18
|
+
/** NATS device id the app addresses (`[a-z0-9][a-z0-9_-]{0,63}`). */
|
|
19
|
+
deviceId: string;
|
|
20
|
+
/** ACP: executable started over stdio, e.g. `hermes` with args `["acp"]`. */
|
|
21
|
+
command?: string;
|
|
22
|
+
args?: string[];
|
|
23
|
+
/** Working directory handed to the agent's sessions. */
|
|
24
|
+
cwd?: string;
|
|
25
|
+
/** Extra environment for the agent process (secrets stay on this machine). */
|
|
26
|
+
env?: Record<string, string>;
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
/** Offer the read-only Dinox knowledge tools (MCP) to the agent; default true. */
|
|
29
|
+
dinoxTools?: boolean;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
};
|
|
32
|
+
export type GatewayToolStatus = 'pending' | 'running' | 'completed' | 'failed';
|
|
33
|
+
/** Normalized agent output, independent of the adapter protocol. */
|
|
34
|
+
export type GatewayEvent = {
|
|
35
|
+
type: 'text-delta';
|
|
36
|
+
text: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: 'reasoning-delta';
|
|
39
|
+
text: string;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'tool';
|
|
42
|
+
toolCallId: string;
|
|
43
|
+
title?: string;
|
|
44
|
+
status?: GatewayToolStatus;
|
|
45
|
+
input?: unknown;
|
|
46
|
+
output?: unknown;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'approval-request';
|
|
49
|
+
approvalId: string;
|
|
50
|
+
toolCallId: string;
|
|
51
|
+
toolName: string;
|
|
52
|
+
input?: unknown;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'approval-resolved';
|
|
55
|
+
approvalId: string;
|
|
56
|
+
approved: boolean;
|
|
57
|
+
} | {
|
|
58
|
+
type: 'usage';
|
|
59
|
+
inputTokens?: number | null;
|
|
60
|
+
outputTokens?: number | null;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* A choice the agent exposes per session (an ACP select config option), e.g.
|
|
64
|
+
* its model or mode. Values are the agent's own ids; `group` is set when the
|
|
65
|
+
* agent groups its values (OpenCode groups models by provider).
|
|
66
|
+
*/
|
|
67
|
+
export type GatewayConfigOption = {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
/** ACP semantic category: `model`, `mode`, `thought_level`, ... (UX only). */
|
|
71
|
+
category: string | null;
|
|
72
|
+
description: string | null;
|
|
73
|
+
currentValue: string;
|
|
74
|
+
values: Array<{
|
|
75
|
+
value: string;
|
|
76
|
+
name: string;
|
|
77
|
+
description: string | null;
|
|
78
|
+
group: string | null;
|
|
79
|
+
}>;
|
|
80
|
+
};
|
|
81
|
+
export type GatewayStopReason = 'end_turn' | 'cancelled' | 'max_tokens' | 'refusal' | 'error';
|
|
82
|
+
export type GatewayTurnInput = {
|
|
83
|
+
/** Agent session to continue; absent starts a new one. */
|
|
84
|
+
sessionId: string | null;
|
|
85
|
+
prompt: string;
|
|
86
|
+
/** Attachments the user sent with this turn (trusted, https). */
|
|
87
|
+
resources: Array<{
|
|
88
|
+
uri: string;
|
|
89
|
+
name: string;
|
|
90
|
+
mediaType?: string | null;
|
|
91
|
+
}>;
|
|
92
|
+
signal: AbortSignal;
|
|
93
|
+
/** Option id → value to apply to the session before the prompt; unknown entries are skipped. */
|
|
94
|
+
config?: Readonly<Record<string, string>>;
|
|
95
|
+
onEvent: (event: GatewayEvent) => void;
|
|
96
|
+
/** Resolves with the user's decision for an `approval-request` event. */
|
|
97
|
+
awaitApproval: (approvalId: string) => Promise<boolean>;
|
|
98
|
+
};
|
|
99
|
+
export type GatewayTurnResult = {
|
|
100
|
+
sessionId: string;
|
|
101
|
+
stopReason: GatewayStopReason;
|
|
102
|
+
error?: string;
|
|
103
|
+
};
|
|
104
|
+
export type GatewayAdapterInfo = {
|
|
105
|
+
kind: GatewayKind;
|
|
106
|
+
agentName: string | null;
|
|
107
|
+
agentVersion: string | null;
|
|
108
|
+
};
|
|
109
|
+
/** One agent runtime. Implementations keep their own process/connection state. */
|
|
110
|
+
export interface GatewayAdapter {
|
|
111
|
+
readonly kind: GatewayKind;
|
|
112
|
+
info(): GatewayAdapterInfo;
|
|
113
|
+
runTurn(input: GatewayTurnInput): Promise<GatewayTurnResult>;
|
|
114
|
+
/** The agent's session settings (models, modes); absent when it has none. */
|
|
115
|
+
describeConfig?(): Promise<GatewayConfigOption[]>;
|
|
116
|
+
dispose(): Promise<void>;
|
|
117
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dinox agent gateway: hosts external agent runtimes (Hermes, OpenClaw, pi,
|
|
3
|
+
* Claude Code, Codex, ... over ACP) as remote devices the Dinox app can drive.
|
|
4
|
+
*
|
|
5
|
+
* The app sees each gateway as one `dinox.remote-device.v2` device. The agent
|
|
6
|
+
* owns its conversation context (gateway-session mode): each turn sends only
|
|
7
|
+
* the new user message, while Dinox keeps a display/sync copy of the transcript.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/skills/registry.js
CHANGED
|
@@ -18,6 +18,13 @@ const REGISTRY_ENTRIES = [
|
|
|
18
18
|
name: 'dino-manage-boxes',
|
|
19
19
|
generated: [commands('SKILL.md', 'skill:dino-manage-boxes')],
|
|
20
20
|
},
|
|
21
|
+
{
|
|
22
|
+
name: 'dino-manage-properties',
|
|
23
|
+
generated: [
|
|
24
|
+
commands('SKILL.md', 'skill:dino-manage-properties'),
|
|
25
|
+
commands('references/templates.md', 'skill:dino-manage-properties:templates'),
|
|
26
|
+
],
|
|
27
|
+
},
|
|
21
28
|
{
|
|
22
29
|
name: 'dino-manage-prompts',
|
|
23
30
|
generated: [commands('SKILL.md', 'skill:dino-manage-prompts')],
|