@askexenow/exe-os 0.9.7 → 0.9.9
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/dist/bin/backfill-conversations.js +953 -105
- package/dist/bin/backfill-responses.js +952 -104
- package/dist/bin/backfill-vectors.js +956 -108
- package/dist/bin/cleanup-stale-review-tasks.js +802 -58
- package/dist/bin/cli.js +2292 -1070
- package/dist/bin/exe-agent-config.js +157 -101
- package/dist/bin/exe-agent.js +55 -29
- package/dist/bin/exe-assign.js +940 -92
- package/dist/bin/exe-boot.js +1424 -442
- package/dist/bin/exe-call.js +240 -141
- package/dist/bin/exe-cloud.js +198 -70
- package/dist/bin/exe-dispatch.js +951 -192
- package/dist/bin/exe-doctor.js +791 -51
- package/dist/bin/exe-export-behaviors.js +790 -42
- package/dist/bin/exe-forget.js +771 -31
- package/dist/bin/exe-gateway.js +1592 -521
- package/dist/bin/exe-heartbeat.js +850 -109
- package/dist/bin/exe-kill.js +783 -35
- package/dist/bin/exe-launch-agent.js +1030 -107
- package/dist/bin/exe-link.js +916 -110
- package/dist/bin/exe-new-employee.js +526 -217
- package/dist/bin/exe-pending-messages.js +1046 -62
- package/dist/bin/exe-pending-notifications.js +1318 -111
- package/dist/bin/exe-pending-reviews.js +1040 -72
- package/dist/bin/exe-rename.js +772 -59
- package/dist/bin/exe-review.js +772 -32
- package/dist/bin/exe-search.js +982 -128
- package/dist/bin/exe-session-cleanup.js +1180 -306
- package/dist/bin/exe-settings.js +185 -105
- package/dist/bin/exe-start-codex.js +886 -132
- package/dist/bin/exe-start-opencode.js +873 -119
- package/dist/bin/exe-status.js +803 -59
- package/dist/bin/exe-team.js +772 -32
- package/dist/bin/git-sweep.js +1046 -223
- package/dist/bin/graph-backfill.js +779 -31
- package/dist/bin/graph-export.js +785 -37
- package/dist/bin/install.js +632 -200
- package/dist/bin/scan-tasks.js +1055 -232
- package/dist/bin/setup.js +1419 -320
- package/dist/bin/shard-migrate.js +783 -35
- package/dist/bin/update.js +138 -49
- package/dist/bin/wiki-sync.js +782 -34
- package/dist/gateway/index.js +1444 -449
- package/dist/hooks/bug-report-worker.js +1141 -269
- package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
- package/dist/hooks/commit-complete.js +1044 -221
- package/dist/hooks/error-recall.js +989 -135
- package/dist/hooks/exe-heartbeat-hook.js +99 -75
- package/dist/hooks/ingest-worker.js +4176 -3226
- package/dist/hooks/ingest.js +920 -168
- package/dist/hooks/instructions-loaded.js +874 -70
- package/dist/hooks/notification.js +860 -56
- package/dist/hooks/post-compact.js +881 -73
- package/dist/hooks/pre-compact.js +1050 -227
- package/dist/hooks/pre-tool-use.js +1084 -159
- package/dist/hooks/prompt-ingest-worker.js +1089 -164
- package/dist/hooks/prompt-submit.js +1469 -515
- package/dist/hooks/response-ingest-worker.js +1104 -179
- package/dist/hooks/session-end.js +1085 -251
- package/dist/hooks/session-start.js +1241 -231
- package/dist/hooks/stop.js +935 -109
- package/dist/hooks/subagent-stop.js +881 -73
- package/dist/hooks/summary-worker.js +1323 -307
- package/dist/index.js +1449 -452
- package/dist/lib/agent-config.js +28 -6
- package/dist/lib/cloud-sync.js +909 -115
- package/dist/lib/config.js +30 -10
- package/dist/lib/consolidation.js +42 -9
- package/dist/lib/database.js +739 -33
- package/dist/lib/db-daemon-client.js +73 -19
- package/dist/lib/db.js +2359 -0
- package/dist/lib/device-registry.js +760 -47
- package/dist/lib/embedder.js +201 -73
- package/dist/lib/employee-templates.js +30 -4
- package/dist/lib/employees.js +290 -86
- package/dist/lib/exe-daemon-client.js +187 -83
- package/dist/lib/exe-daemon.js +1696 -616
- package/dist/lib/hybrid-search.js +982 -128
- package/dist/lib/identity.js +43 -13
- package/dist/lib/license.js +133 -48
- package/dist/lib/messaging.js +167 -80
- package/dist/lib/reminders.js +35 -5
- package/dist/lib/schedules.js +772 -32
- package/dist/lib/skill-learning.js +54 -7
- package/dist/lib/store.js +779 -31
- package/dist/lib/task-router.js +94 -73
- package/dist/lib/tasks.js +298 -225
- package/dist/lib/tmux-routing.js +246 -172
- package/dist/lib/token-spend.js +52 -14
- package/dist/mcp/server.js +2893 -850
- package/dist/mcp/tools/complete-reminder.js +35 -5
- package/dist/mcp/tools/create-reminder.js +35 -5
- package/dist/mcp/tools/create-task.js +507 -323
- package/dist/mcp/tools/deactivate-behavior.js +40 -10
- package/dist/mcp/tools/list-reminders.js +35 -5
- package/dist/mcp/tools/list-tasks.js +277 -104
- package/dist/mcp/tools/send-message.js +129 -56
- package/dist/mcp/tools/update-task.js +1864 -188
- package/dist/runtime/index.js +1083 -259
- package/dist/tui/App.js +1501 -434
- package/package.json +3 -2
package/dist/lib/cloud-sync.js
CHANGED
|
@@ -63,9 +63,34 @@ var init_db_retry = __esm({
|
|
|
63
63
|
}
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
// src/lib/secure-files.ts
|
|
67
|
+
import { chmodSync, existsSync, mkdirSync } from "fs";
|
|
68
|
+
import { chmod, mkdir } from "fs/promises";
|
|
69
|
+
function ensurePrivateDirSync(dirPath) {
|
|
70
|
+
mkdirSync(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
71
|
+
try {
|
|
72
|
+
chmodSync(dirPath, PRIVATE_DIR_MODE);
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function enforcePrivateFileSync(filePath) {
|
|
77
|
+
try {
|
|
78
|
+
if (existsSync(filePath)) chmodSync(filePath, PRIVATE_FILE_MODE);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
|
|
83
|
+
var init_secure_files = __esm({
|
|
84
|
+
"src/lib/secure-files.ts"() {
|
|
85
|
+
"use strict";
|
|
86
|
+
PRIVATE_DIR_MODE = 448;
|
|
87
|
+
PRIVATE_FILE_MODE = 384;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
66
91
|
// src/lib/config.ts
|
|
67
|
-
import { readFile, writeFile
|
|
68
|
-
import { readFileSync, existsSync, renameSync } from "fs";
|
|
92
|
+
import { readFile, writeFile } from "fs/promises";
|
|
93
|
+
import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
|
|
69
94
|
import path from "path";
|
|
70
95
|
import os from "os";
|
|
71
96
|
function resolveDataDir() {
|
|
@@ -73,7 +98,7 @@ function resolveDataDir() {
|
|
|
73
98
|
if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
|
|
74
99
|
const newDir = path.join(os.homedir(), ".exe-os");
|
|
75
100
|
const legacyDir = path.join(os.homedir(), ".exe-mem");
|
|
76
|
-
if (!
|
|
101
|
+
if (!existsSync2(newDir) && existsSync2(legacyDir)) {
|
|
77
102
|
try {
|
|
78
103
|
renameSync(legacyDir, newDir);
|
|
79
104
|
process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
|
|
@@ -88,6 +113,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
|
|
|
88
113
|
var init_config = __esm({
|
|
89
114
|
"src/lib/config.ts"() {
|
|
90
115
|
"use strict";
|
|
116
|
+
init_secure_files();
|
|
91
117
|
EXE_AI_DIR = resolveDataDir();
|
|
92
118
|
DB_PATH = path.join(EXE_AI_DIR, "memories.db");
|
|
93
119
|
MODELS_DIR = path.join(EXE_AI_DIR, "models");
|
|
@@ -156,7 +182,7 @@ var init_config = __esm({
|
|
|
156
182
|
|
|
157
183
|
// src/lib/employees.ts
|
|
158
184
|
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
159
|
-
import { existsSync as
|
|
185
|
+
import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
|
|
160
186
|
import { execSync } from "child_process";
|
|
161
187
|
import path2 from "path";
|
|
162
188
|
import os2 from "os";
|
|
@@ -173,7 +199,7 @@ function getCoordinatorName(employees = loadEmployeesSync()) {
|
|
|
173
199
|
return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
|
|
174
200
|
}
|
|
175
201
|
async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
|
|
176
|
-
if (!
|
|
202
|
+
if (!existsSync3(employeesPath)) {
|
|
177
203
|
return [];
|
|
178
204
|
}
|
|
179
205
|
const raw = await readFile2(employeesPath, "utf-8");
|
|
@@ -188,7 +214,7 @@ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
|
|
|
188
214
|
await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
|
|
189
215
|
}
|
|
190
216
|
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
191
|
-
if (!
|
|
217
|
+
if (!existsSync3(employeesPath)) return [];
|
|
192
218
|
try {
|
|
193
219
|
return JSON.parse(readFileSync2(employeesPath, "utf-8"));
|
|
194
220
|
} catch {
|
|
@@ -222,7 +248,7 @@ function registerBinSymlinks(name) {
|
|
|
222
248
|
for (const suffix of ["", "-opencode"]) {
|
|
223
249
|
const linkName = `${name}${suffix}`;
|
|
224
250
|
const linkPath = path2.join(binDir, linkName);
|
|
225
|
-
if (
|
|
251
|
+
if (existsSync3(linkPath)) {
|
|
226
252
|
skipped.push(linkName);
|
|
227
253
|
continue;
|
|
228
254
|
}
|
|
@@ -235,7 +261,7 @@ function registerBinSymlinks(name) {
|
|
|
235
261
|
}
|
|
236
262
|
return { created, skipped, errors };
|
|
237
263
|
}
|
|
238
|
-
var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE;
|
|
264
|
+
var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, IDENTITY_DIR;
|
|
239
265
|
var init_employees = __esm({
|
|
240
266
|
"src/lib/employees.ts"() {
|
|
241
267
|
"use strict";
|
|
@@ -243,16 +269,638 @@ var init_employees = __esm({
|
|
|
243
269
|
EMPLOYEES_PATH = path2.join(EXE_AI_DIR, "exe-employees.json");
|
|
244
270
|
DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
|
|
245
271
|
COORDINATOR_ROLE = "COO";
|
|
272
|
+
IDENTITY_DIR = path2.join(EXE_AI_DIR, "identity");
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// src/lib/database-adapter.ts
|
|
277
|
+
import os3 from "os";
|
|
278
|
+
import path3 from "path";
|
|
279
|
+
import { createRequire } from "module";
|
|
280
|
+
import { pathToFileURL } from "url";
|
|
281
|
+
function quotedIdentifier(identifier) {
|
|
282
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
283
|
+
}
|
|
284
|
+
function unqualifiedTableName(name) {
|
|
285
|
+
const raw = name.trim().replace(/^"|"$/g, "");
|
|
286
|
+
const parts = raw.split(".");
|
|
287
|
+
return parts[parts.length - 1].replace(/^"|"$/g, "").toLowerCase();
|
|
288
|
+
}
|
|
289
|
+
function stripTrailingSemicolon(sql) {
|
|
290
|
+
return sql.trim().replace(/;+\s*$/u, "");
|
|
291
|
+
}
|
|
292
|
+
function appendClause(sql, clause) {
|
|
293
|
+
const trimmed = stripTrailingSemicolon(sql);
|
|
294
|
+
const returningMatch = /\sRETURNING\b[\s\S]*$/iu.exec(trimmed);
|
|
295
|
+
if (!returningMatch) {
|
|
296
|
+
return `${trimmed}${clause}`;
|
|
297
|
+
}
|
|
298
|
+
const idx = returningMatch.index;
|
|
299
|
+
return `${trimmed.slice(0, idx)}${clause}${trimmed.slice(idx)}`;
|
|
300
|
+
}
|
|
301
|
+
function normalizeStatement(stmt) {
|
|
302
|
+
if (typeof stmt === "string") {
|
|
303
|
+
return { kind: "positional", sql: stmt, args: [] };
|
|
304
|
+
}
|
|
305
|
+
const sql = stmt.sql;
|
|
306
|
+
if (Array.isArray(stmt.args) || stmt.args === void 0) {
|
|
307
|
+
return { kind: "positional", sql, args: stmt.args ?? [] };
|
|
308
|
+
}
|
|
309
|
+
return { kind: "named", sql, args: stmt.args };
|
|
310
|
+
}
|
|
311
|
+
function rewriteBooleanLiterals(sql) {
|
|
312
|
+
let out = sql;
|
|
313
|
+
for (const column of BOOLEAN_COLUMN_NAMES) {
|
|
314
|
+
const scoped = `((?:\\b[a-z_][a-z0-9_]*\\.)?${column})`;
|
|
315
|
+
out = out.replace(new RegExp(`${scoped}\\s*=\\s*0\\b`, "giu"), "$1 = FALSE");
|
|
316
|
+
out = out.replace(new RegExp(`${scoped}\\s*=\\s*1\\b`, "giu"), "$1 = TRUE");
|
|
317
|
+
out = out.replace(new RegExp(`${scoped}\\s*!=\\s*0\\b`, "giu"), "$1 != FALSE");
|
|
318
|
+
out = out.replace(new RegExp(`${scoped}\\s*!=\\s*1\\b`, "giu"), "$1 != TRUE");
|
|
319
|
+
out = out.replace(new RegExp(`${scoped}\\s*<>\\s*0\\b`, "giu"), "$1 <> FALSE");
|
|
320
|
+
out = out.replace(new RegExp(`${scoped}\\s*<>\\s*1\\b`, "giu"), "$1 <> TRUE");
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
function rewriteInsertOrIgnore(sql) {
|
|
325
|
+
if (!/^\s*INSERT\s+OR\s+IGNORE\s+INTO\b/iu.test(sql)) {
|
|
326
|
+
return sql;
|
|
327
|
+
}
|
|
328
|
+
const replaced = sql.replace(/^\s*INSERT\s+OR\s+IGNORE\s+INTO\b/iu, "INSERT INTO");
|
|
329
|
+
return /\bON\s+CONFLICT\b/iu.test(replaced) ? replaced : appendClause(replaced, " ON CONFLICT DO NOTHING");
|
|
330
|
+
}
|
|
331
|
+
function rewriteInsertOrReplace(sql) {
|
|
332
|
+
const match = /^\s*INSERT\s+OR\s+REPLACE\s+INTO\s+([A-Za-z0-9_."]+)\s*\(([^)]+)\)([\s\S]*)$/iu.exec(sql);
|
|
333
|
+
if (!match) {
|
|
334
|
+
return sql;
|
|
335
|
+
}
|
|
336
|
+
const rawTable = match[1];
|
|
337
|
+
const rawColumns = match[2];
|
|
338
|
+
const remainder = match[3];
|
|
339
|
+
const tableName = unqualifiedTableName(rawTable);
|
|
340
|
+
const conflictKeys = UPSERT_KEYS[tableName];
|
|
341
|
+
if (!conflictKeys?.length) {
|
|
342
|
+
return sql;
|
|
343
|
+
}
|
|
344
|
+
const columns = rawColumns.split(",").map((col) => col.trim().replace(/^"|"$/g, ""));
|
|
345
|
+
const updateColumns = columns.filter((col) => !conflictKeys.includes(col));
|
|
346
|
+
const conflictTarget = conflictKeys.map(quotedIdentifier).join(", ");
|
|
347
|
+
const updateClause = updateColumns.length === 0 ? " DO NOTHING" : ` DO UPDATE SET ${updateColumns.map((col) => `${quotedIdentifier(col)} = EXCLUDED.${quotedIdentifier(col)}`).join(", ")}`;
|
|
348
|
+
return `INSERT INTO ${rawTable} (${rawColumns})${appendClause(remainder, ` ON CONFLICT (${conflictTarget})${updateClause}`)}`;
|
|
349
|
+
}
|
|
350
|
+
function rewriteSql(sql) {
|
|
351
|
+
let out = sql;
|
|
352
|
+
out = out.replace(/\bdatetime\(\s*['"]now['"]\s*\)/giu, "CURRENT_TIMESTAMP");
|
|
353
|
+
out = out.replace(/\bvector32\s*\(\s*\?\s*\)/giu, "?");
|
|
354
|
+
out = rewriteBooleanLiterals(out);
|
|
355
|
+
out = rewriteInsertOrReplace(out);
|
|
356
|
+
out = rewriteInsertOrIgnore(out);
|
|
357
|
+
return stripTrailingSemicolon(out);
|
|
358
|
+
}
|
|
359
|
+
function toBoolean(value) {
|
|
360
|
+
if (value === null || value === void 0) return value;
|
|
361
|
+
if (typeof value === "boolean") return value;
|
|
362
|
+
if (typeof value === "number") return value !== 0;
|
|
363
|
+
if (typeof value === "bigint") return value !== 0n;
|
|
364
|
+
if (typeof value === "string") {
|
|
365
|
+
const normalized = value.trim().toLowerCase();
|
|
366
|
+
if (normalized === "0" || normalized === "false") return false;
|
|
367
|
+
if (normalized === "1" || normalized === "true") return true;
|
|
368
|
+
}
|
|
369
|
+
return Boolean(value);
|
|
370
|
+
}
|
|
371
|
+
function countQuestionMarks(sql, end) {
|
|
372
|
+
let count = 0;
|
|
373
|
+
let inSingle = false;
|
|
374
|
+
let inDouble = false;
|
|
375
|
+
let inLineComment = false;
|
|
376
|
+
let inBlockComment = false;
|
|
377
|
+
for (let i = 0; i < end; i++) {
|
|
378
|
+
const ch = sql[i];
|
|
379
|
+
const next = sql[i + 1];
|
|
380
|
+
if (inLineComment) {
|
|
381
|
+
if (ch === "\n") inLineComment = false;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (inBlockComment) {
|
|
385
|
+
if (ch === "*" && next === "/") {
|
|
386
|
+
inBlockComment = false;
|
|
387
|
+
i += 1;
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (!inSingle && !inDouble && ch === "-" && next === "-") {
|
|
392
|
+
inLineComment = true;
|
|
393
|
+
i += 1;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (!inSingle && !inDouble && ch === "/" && next === "*") {
|
|
397
|
+
inBlockComment = true;
|
|
398
|
+
i += 1;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (!inDouble && ch === "'" && sql[i - 1] !== "\\") {
|
|
402
|
+
inSingle = !inSingle;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (!inSingle && ch === '"' && sql[i - 1] !== "\\") {
|
|
406
|
+
inDouble = !inDouble;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (!inSingle && !inDouble && ch === "?") {
|
|
410
|
+
count += 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return count;
|
|
414
|
+
}
|
|
415
|
+
function findBooleanPlaceholderIndexes(sql) {
|
|
416
|
+
const indexes = /* @__PURE__ */ new Set();
|
|
417
|
+
for (const column of BOOLEAN_COLUMN_NAMES) {
|
|
418
|
+
const pattern = new RegExp(`(?:\\b[a-z_][a-z0-9_]*\\.)?${column}\\s*=\\s*\\?`, "giu");
|
|
419
|
+
for (const match of sql.matchAll(pattern)) {
|
|
420
|
+
const matchText = match[0];
|
|
421
|
+
const qIndex = match.index + matchText.lastIndexOf("?");
|
|
422
|
+
indexes.add(countQuestionMarks(sql, qIndex + 1));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return indexes;
|
|
426
|
+
}
|
|
427
|
+
function coerceInsertBooleanArgs(sql, args) {
|
|
428
|
+
const match = /^\s*INSERT(?:\s+OR\s+(?:IGNORE|REPLACE))?\s+INTO\s+([A-Za-z0-9_."]+)\s*\(([^)]+)\)/iu.exec(sql);
|
|
429
|
+
if (!match) return;
|
|
430
|
+
const rawTable = match[1];
|
|
431
|
+
const rawColumns = match[2];
|
|
432
|
+
const boolColumns = BOOLEAN_COLUMNS_BY_TABLE[unqualifiedTableName(rawTable)];
|
|
433
|
+
if (!boolColumns?.size) return;
|
|
434
|
+
const columns = rawColumns.split(",").map((col) => col.trim().replace(/^"|"$/g, ""));
|
|
435
|
+
for (const [index, column] of columns.entries()) {
|
|
436
|
+
if (boolColumns.has(column) && index < args.length) {
|
|
437
|
+
args[index] = toBoolean(args[index]);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function coerceUpdateBooleanArgs(sql, args) {
|
|
442
|
+
const match = /^\s*UPDATE\s+([A-Za-z0-9_."]+)\s+SET\s+([\s\S]+?)(?:\s+WHERE\b|$)/iu.exec(sql);
|
|
443
|
+
if (!match) return;
|
|
444
|
+
const rawTable = match[1];
|
|
445
|
+
const setClause = match[2];
|
|
446
|
+
const boolColumns = BOOLEAN_COLUMNS_BY_TABLE[unqualifiedTableName(rawTable)];
|
|
447
|
+
if (!boolColumns?.size) return;
|
|
448
|
+
const assignments = setClause.split(",");
|
|
449
|
+
let placeholderIndex = 0;
|
|
450
|
+
for (const assignment of assignments) {
|
|
451
|
+
if (!assignment.includes("?")) continue;
|
|
452
|
+
placeholderIndex += 1;
|
|
453
|
+
const colMatch = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*\.)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\?/iu.exec(assignment);
|
|
454
|
+
if (colMatch && boolColumns.has(colMatch[1])) {
|
|
455
|
+
args[placeholderIndex - 1] = toBoolean(args[placeholderIndex - 1]);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function coerceBooleanArgs(sql, args) {
|
|
460
|
+
const nextArgs = [...args];
|
|
461
|
+
coerceInsertBooleanArgs(sql, nextArgs);
|
|
462
|
+
coerceUpdateBooleanArgs(sql, nextArgs);
|
|
463
|
+
const placeholderIndexes = findBooleanPlaceholderIndexes(sql);
|
|
464
|
+
for (const index of placeholderIndexes) {
|
|
465
|
+
if (index > 0 && index <= nextArgs.length) {
|
|
466
|
+
nextArgs[index - 1] = toBoolean(nextArgs[index - 1]);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return nextArgs;
|
|
470
|
+
}
|
|
471
|
+
function convertQuestionMarksToDollarParams(sql) {
|
|
472
|
+
let out = "";
|
|
473
|
+
let placeholder = 0;
|
|
474
|
+
let inSingle = false;
|
|
475
|
+
let inDouble = false;
|
|
476
|
+
let inLineComment = false;
|
|
477
|
+
let inBlockComment = false;
|
|
478
|
+
for (let i = 0; i < sql.length; i++) {
|
|
479
|
+
const ch = sql[i];
|
|
480
|
+
const next = sql[i + 1];
|
|
481
|
+
if (inLineComment) {
|
|
482
|
+
out += ch;
|
|
483
|
+
if (ch === "\n") inLineComment = false;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (inBlockComment) {
|
|
487
|
+
out += ch;
|
|
488
|
+
if (ch === "*" && next === "/") {
|
|
489
|
+
out += next;
|
|
490
|
+
inBlockComment = false;
|
|
491
|
+
i += 1;
|
|
492
|
+
}
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (!inSingle && !inDouble && ch === "-" && next === "-") {
|
|
496
|
+
out += ch + next;
|
|
497
|
+
inLineComment = true;
|
|
498
|
+
i += 1;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (!inSingle && !inDouble && ch === "/" && next === "*") {
|
|
502
|
+
out += ch + next;
|
|
503
|
+
inBlockComment = true;
|
|
504
|
+
i += 1;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (!inDouble && ch === "'" && sql[i - 1] !== "\\") {
|
|
508
|
+
inSingle = !inSingle;
|
|
509
|
+
out += ch;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (!inSingle && ch === '"' && sql[i - 1] !== "\\") {
|
|
513
|
+
inDouble = !inDouble;
|
|
514
|
+
out += ch;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (!inSingle && !inDouble && ch === "?") {
|
|
518
|
+
placeholder += 1;
|
|
519
|
+
out += `$${placeholder}`;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
out += ch;
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
function translateStatementForPostgres(stmt) {
|
|
527
|
+
const normalized = normalizeStatement(stmt);
|
|
528
|
+
if (normalized.kind === "named") {
|
|
529
|
+
throw new Error("Named SQL parameters are not supported by the Prisma adapter.");
|
|
530
|
+
}
|
|
531
|
+
const rewrittenSql = rewriteSql(normalized.sql);
|
|
532
|
+
const coercedArgs = coerceBooleanArgs(rewrittenSql, normalized.args);
|
|
533
|
+
return {
|
|
534
|
+
sql: convertQuestionMarksToDollarParams(rewrittenSql),
|
|
535
|
+
args: coercedArgs
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function shouldBypassPostgres(stmt) {
|
|
539
|
+
const normalized = normalizeStatement(stmt);
|
|
540
|
+
if (normalized.kind === "named") {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
return IMMEDIATE_FALLBACK_PATTERNS.some((pattern) => pattern.test(normalized.sql));
|
|
544
|
+
}
|
|
545
|
+
function shouldFallbackOnError(error) {
|
|
546
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
547
|
+
return /42P01|42883|42601|does not exist|syntax error|not supported|Named SQL parameters are not supported/iu.test(message);
|
|
548
|
+
}
|
|
549
|
+
function isReadQuery(sql) {
|
|
550
|
+
const trimmed = sql.trimStart();
|
|
551
|
+
return /^(SELECT|WITH|SHOW|EXPLAIN|VALUES)\b/iu.test(trimmed) || /\bRETURNING\b/iu.test(trimmed);
|
|
552
|
+
}
|
|
553
|
+
function buildRow(row, columns) {
|
|
554
|
+
const values = columns.map((column) => row[column]);
|
|
555
|
+
return Object.assign(values, row);
|
|
556
|
+
}
|
|
557
|
+
function buildResultSet(rows, rowsAffected = 0) {
|
|
558
|
+
const columns = rows[0] ? Object.keys(rows[0]) : [];
|
|
559
|
+
const resultRows = rows.map((row) => buildRow(row, columns));
|
|
560
|
+
return {
|
|
561
|
+
columns,
|
|
562
|
+
columnTypes: columns.map(() => ""),
|
|
563
|
+
rows: resultRows,
|
|
564
|
+
rowsAffected,
|
|
565
|
+
lastInsertRowid: void 0,
|
|
566
|
+
toJSON() {
|
|
567
|
+
return {
|
|
568
|
+
columns,
|
|
569
|
+
columnTypes: columns.map(() => ""),
|
|
570
|
+
rows,
|
|
571
|
+
rowsAffected,
|
|
572
|
+
lastInsertRowid: void 0
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
async function loadPrismaClient() {
|
|
578
|
+
if (!prismaClientPromise) {
|
|
579
|
+
prismaClientPromise = (async () => {
|
|
580
|
+
const explicitPath = process.env.EXE_OS_PRISMA_CLIENT_PATH;
|
|
581
|
+
if (explicitPath) {
|
|
582
|
+
const module2 = await import(pathToFileURL(explicitPath).href);
|
|
583
|
+
const PrismaClient2 = module2.PrismaClient ?? module2.default?.PrismaClient;
|
|
584
|
+
if (!PrismaClient2) {
|
|
585
|
+
throw new Error(`No PrismaClient export found at ${explicitPath}`);
|
|
586
|
+
}
|
|
587
|
+
return new PrismaClient2();
|
|
588
|
+
}
|
|
589
|
+
const exeDbRoot = process.env.EXE_DB_ROOT ?? path3.join(os3.homedir(), "exe-db");
|
|
590
|
+
const requireFromExeDb = createRequire(path3.join(exeDbRoot, "package.json"));
|
|
591
|
+
const prismaEntry = requireFromExeDb.resolve("@prisma/client");
|
|
592
|
+
const module = await import(pathToFileURL(prismaEntry).href);
|
|
593
|
+
const PrismaClient = module.PrismaClient ?? module.default?.PrismaClient;
|
|
594
|
+
if (!PrismaClient) {
|
|
595
|
+
throw new Error(`No PrismaClient export found in ${prismaEntry}`);
|
|
596
|
+
}
|
|
597
|
+
return new PrismaClient();
|
|
598
|
+
})();
|
|
599
|
+
}
|
|
600
|
+
return prismaClientPromise;
|
|
601
|
+
}
|
|
602
|
+
async function ensureCompatibilityViews(prisma) {
|
|
603
|
+
if (!compatibilityBootstrapPromise) {
|
|
604
|
+
compatibilityBootstrapPromise = (async () => {
|
|
605
|
+
for (const mapping of VIEW_MAPPINGS) {
|
|
606
|
+
const relation = mapping.source.replace(/"/g, "");
|
|
607
|
+
const rows = await prisma.$queryRawUnsafe(
|
|
608
|
+
"SELECT to_regclass($1) AS regclass",
|
|
609
|
+
relation
|
|
610
|
+
);
|
|
611
|
+
if (!rows[0]?.regclass) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
await prisma.$executeRawUnsafe(
|
|
615
|
+
`CREATE OR REPLACE VIEW public.${quotedIdentifier(mapping.view)} AS SELECT * FROM ${mapping.source}`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
})();
|
|
619
|
+
}
|
|
620
|
+
return compatibilityBootstrapPromise;
|
|
621
|
+
}
|
|
622
|
+
async function executeOnPrisma(executor, stmt) {
|
|
623
|
+
const translated = translateStatementForPostgres(stmt);
|
|
624
|
+
if (isReadQuery(translated.sql)) {
|
|
625
|
+
const rows = await executor.$queryRawUnsafe(
|
|
626
|
+
translated.sql,
|
|
627
|
+
...translated.args
|
|
628
|
+
);
|
|
629
|
+
return buildResultSet(rows, /\bRETURNING\b/iu.test(translated.sql) ? rows.length : 0);
|
|
630
|
+
}
|
|
631
|
+
const rowsAffected = await executor.$executeRawUnsafe(translated.sql, ...translated.args);
|
|
632
|
+
return buildResultSet([], rowsAffected);
|
|
633
|
+
}
|
|
634
|
+
function splitSqlStatements(sql) {
|
|
635
|
+
const parts = [];
|
|
636
|
+
let current = "";
|
|
637
|
+
let inSingle = false;
|
|
638
|
+
let inDouble = false;
|
|
639
|
+
let inLineComment = false;
|
|
640
|
+
let inBlockComment = false;
|
|
641
|
+
for (let i = 0; i < sql.length; i++) {
|
|
642
|
+
const ch = sql[i];
|
|
643
|
+
const next = sql[i + 1];
|
|
644
|
+
if (inLineComment) {
|
|
645
|
+
current += ch;
|
|
646
|
+
if (ch === "\n") inLineComment = false;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (inBlockComment) {
|
|
650
|
+
current += ch;
|
|
651
|
+
if (ch === "*" && next === "/") {
|
|
652
|
+
current += next;
|
|
653
|
+
inBlockComment = false;
|
|
654
|
+
i += 1;
|
|
655
|
+
}
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (!inSingle && !inDouble && ch === "-" && next === "-") {
|
|
659
|
+
current += ch + next;
|
|
660
|
+
inLineComment = true;
|
|
661
|
+
i += 1;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (!inSingle && !inDouble && ch === "/" && next === "*") {
|
|
665
|
+
current += ch + next;
|
|
666
|
+
inBlockComment = true;
|
|
667
|
+
i += 1;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (!inDouble && ch === "'" && sql[i - 1] !== "\\") {
|
|
671
|
+
inSingle = !inSingle;
|
|
672
|
+
current += ch;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (!inSingle && ch === '"' && sql[i - 1] !== "\\") {
|
|
676
|
+
inDouble = !inDouble;
|
|
677
|
+
current += ch;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (!inSingle && !inDouble && ch === ";") {
|
|
681
|
+
if (current.trim()) {
|
|
682
|
+
parts.push(current.trim());
|
|
683
|
+
}
|
|
684
|
+
current = "";
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
current += ch;
|
|
688
|
+
}
|
|
689
|
+
if (current.trim()) {
|
|
690
|
+
parts.push(current.trim());
|
|
691
|
+
}
|
|
692
|
+
return parts;
|
|
693
|
+
}
|
|
694
|
+
async function createPrismaDbAdapter(fallbackClient) {
|
|
695
|
+
const prisma = await loadPrismaClient();
|
|
696
|
+
await ensureCompatibilityViews(prisma);
|
|
697
|
+
let closed = false;
|
|
698
|
+
let adapter;
|
|
699
|
+
const fallbackExecute = async (stmt, error) => {
|
|
700
|
+
if (!fallbackClient) {
|
|
701
|
+
if (error) throw error;
|
|
702
|
+
throw new Error("No fallback SQLite client is available for this Prisma-routed query.");
|
|
703
|
+
}
|
|
704
|
+
if (error) {
|
|
705
|
+
process.stderr.write(
|
|
706
|
+
`[database-adapter] Falling back to SQLite: ${error instanceof Error ? error.message : String(error)}
|
|
707
|
+
`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
return fallbackClient.execute(stmt);
|
|
711
|
+
};
|
|
712
|
+
adapter = {
|
|
713
|
+
async execute(stmt) {
|
|
714
|
+
if (shouldBypassPostgres(stmt)) {
|
|
715
|
+
return fallbackExecute(stmt);
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
return await executeOnPrisma(prisma, stmt);
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (shouldFallbackOnError(error)) {
|
|
721
|
+
return fallbackExecute(stmt, error);
|
|
722
|
+
}
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
async batch(stmts, mode) {
|
|
727
|
+
if (stmts.some((stmt) => shouldBypassPostgres(stmt))) {
|
|
728
|
+
if (!fallbackClient) {
|
|
729
|
+
throw new Error("Cannot batch unsupported SQLite-only statements without a fallback client.");
|
|
730
|
+
}
|
|
731
|
+
return fallbackClient.batch(stmts, mode);
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
if (prisma.$transaction) {
|
|
735
|
+
return await prisma.$transaction(async (tx) => {
|
|
736
|
+
const results2 = [];
|
|
737
|
+
for (const stmt of stmts) {
|
|
738
|
+
results2.push(await executeOnPrisma(tx, stmt));
|
|
739
|
+
}
|
|
740
|
+
return results2;
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
const results = [];
|
|
744
|
+
for (const stmt of stmts) {
|
|
745
|
+
results.push(await executeOnPrisma(prisma, stmt));
|
|
746
|
+
}
|
|
747
|
+
return results;
|
|
748
|
+
} catch (error) {
|
|
749
|
+
if (fallbackClient && shouldFallbackOnError(error)) {
|
|
750
|
+
process.stderr.write(
|
|
751
|
+
`[database-adapter] Falling back batch to SQLite: ${error instanceof Error ? error.message : String(error)}
|
|
752
|
+
`
|
|
753
|
+
);
|
|
754
|
+
return fallbackClient.batch(stmts, mode);
|
|
755
|
+
}
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
async migrate(stmts) {
|
|
760
|
+
if (fallbackClient) {
|
|
761
|
+
return fallbackClient.migrate(stmts);
|
|
762
|
+
}
|
|
763
|
+
return adapter.batch(stmts, "deferred");
|
|
764
|
+
},
|
|
765
|
+
async transaction(mode) {
|
|
766
|
+
if (!fallbackClient) {
|
|
767
|
+
throw new Error("Interactive transactions are only supported on the SQLite fallback client.");
|
|
768
|
+
}
|
|
769
|
+
return fallbackClient.transaction(mode);
|
|
770
|
+
},
|
|
771
|
+
async executeMultiple(sql) {
|
|
772
|
+
if (fallbackClient && shouldBypassPostgres(sql)) {
|
|
773
|
+
return fallbackClient.executeMultiple(sql);
|
|
774
|
+
}
|
|
775
|
+
for (const statement of splitSqlStatements(sql)) {
|
|
776
|
+
await adapter.execute(statement);
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
async sync() {
|
|
780
|
+
if (fallbackClient) {
|
|
781
|
+
return fallbackClient.sync();
|
|
782
|
+
}
|
|
783
|
+
return { frame_no: 0, frames_synced: 0 };
|
|
784
|
+
},
|
|
785
|
+
close() {
|
|
786
|
+
closed = true;
|
|
787
|
+
prismaClientPromise = null;
|
|
788
|
+
compatibilityBootstrapPromise = null;
|
|
789
|
+
void prisma.$disconnect?.();
|
|
790
|
+
},
|
|
791
|
+
get closed() {
|
|
792
|
+
return closed;
|
|
793
|
+
},
|
|
794
|
+
get protocol() {
|
|
795
|
+
return "prisma-postgres";
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
return adapter;
|
|
799
|
+
}
|
|
800
|
+
var VIEW_MAPPINGS, UPSERT_KEYS, BOOLEAN_COLUMNS_BY_TABLE, BOOLEAN_COLUMN_NAMES, IMMEDIATE_FALLBACK_PATTERNS, prismaClientPromise, compatibilityBootstrapPromise;
|
|
801
|
+
var init_database_adapter = __esm({
|
|
802
|
+
"src/lib/database-adapter.ts"() {
|
|
803
|
+
"use strict";
|
|
804
|
+
VIEW_MAPPINGS = [
|
|
805
|
+
{ view: "memories", source: "memory.memory_records" },
|
|
806
|
+
{ view: "tasks", source: "memory.tasks" },
|
|
807
|
+
{ view: "behaviors", source: "memory.behaviors" },
|
|
808
|
+
{ view: "entities", source: "memory.entities" },
|
|
809
|
+
{ view: "relationships", source: "memory.relationships" },
|
|
810
|
+
{ view: "entity_memories", source: "memory.entity_memories" },
|
|
811
|
+
{ view: "entity_aliases", source: "memory.entity_aliases" },
|
|
812
|
+
{ view: "notifications", source: "memory.notifications" },
|
|
813
|
+
{ view: "messages", source: "memory.messages" },
|
|
814
|
+
{ view: "users", source: "wiki.users" },
|
|
815
|
+
{ view: "workspaces", source: "wiki.workspaces" },
|
|
816
|
+
{ view: "workspace_users", source: "wiki.workspace_users" },
|
|
817
|
+
{ view: "documents", source: "wiki.workspace_documents" },
|
|
818
|
+
{ view: "chats", source: "wiki.workspace_chats" }
|
|
819
|
+
];
|
|
820
|
+
UPSERT_KEYS = {
|
|
821
|
+
memories: ["id"],
|
|
822
|
+
tasks: ["id"],
|
|
823
|
+
behaviors: ["id"],
|
|
824
|
+
entities: ["id"],
|
|
825
|
+
relationships: ["id"],
|
|
826
|
+
entity_aliases: ["alias"],
|
|
827
|
+
notifications: ["id"],
|
|
828
|
+
messages: ["id"],
|
|
829
|
+
users: ["id"],
|
|
830
|
+
workspaces: ["id"],
|
|
831
|
+
workspace_users: ["id"],
|
|
832
|
+
documents: ["id"],
|
|
833
|
+
chats: ["id"]
|
|
834
|
+
};
|
|
835
|
+
BOOLEAN_COLUMNS_BY_TABLE = {
|
|
836
|
+
memories: /* @__PURE__ */ new Set(["has_error", "draft"]),
|
|
837
|
+
behaviors: /* @__PURE__ */ new Set(["active"]),
|
|
838
|
+
notifications: /* @__PURE__ */ new Set(["read"]),
|
|
839
|
+
users: /* @__PURE__ */ new Set(["has_personal_memory"])
|
|
840
|
+
};
|
|
841
|
+
BOOLEAN_COLUMN_NAMES = new Set(
|
|
842
|
+
Object.values(BOOLEAN_COLUMNS_BY_TABLE).flatMap((cols) => [...cols])
|
|
843
|
+
);
|
|
844
|
+
IMMEDIATE_FALLBACK_PATTERNS = [
|
|
845
|
+
/\bPRAGMA\b/i,
|
|
846
|
+
/\bsqlite_master\b/i,
|
|
847
|
+
/(?:^|[.\s])(?:memories|conversations|entities)_fts\b/i,
|
|
848
|
+
/\bMATCH\b/i,
|
|
849
|
+
/\bvector_distance_cos\s*\(/i,
|
|
850
|
+
/\bjson_extract\s*\(/i,
|
|
851
|
+
/\bjulianday\s*\(/i,
|
|
852
|
+
/\bstrftime\s*\(/i,
|
|
853
|
+
/\blast_insert_rowid\s*\(/i
|
|
854
|
+
];
|
|
855
|
+
prismaClientPromise = null;
|
|
856
|
+
compatibilityBootstrapPromise = null;
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// src/lib/daemon-auth.ts
|
|
861
|
+
import crypto from "crypto";
|
|
862
|
+
import path4 from "path";
|
|
863
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
864
|
+
function normalizeToken(token) {
|
|
865
|
+
if (!token) return null;
|
|
866
|
+
const trimmed = token.trim();
|
|
867
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
868
|
+
}
|
|
869
|
+
function readDaemonToken() {
|
|
870
|
+
try {
|
|
871
|
+
if (!existsSync4(DAEMON_TOKEN_PATH)) return null;
|
|
872
|
+
return normalizeToken(readFileSync3(DAEMON_TOKEN_PATH, "utf8"));
|
|
873
|
+
} catch {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function ensureDaemonToken(seed) {
|
|
878
|
+
const existing = readDaemonToken();
|
|
879
|
+
if (existing) return existing;
|
|
880
|
+
const token = normalizeToken(seed) ?? crypto.randomBytes(32).toString("hex");
|
|
881
|
+
ensurePrivateDirSync(EXE_AI_DIR);
|
|
882
|
+
writeFileSync2(DAEMON_TOKEN_PATH, `${token}
|
|
883
|
+
`, "utf8");
|
|
884
|
+
enforcePrivateFileSync(DAEMON_TOKEN_PATH);
|
|
885
|
+
return token;
|
|
886
|
+
}
|
|
887
|
+
var DAEMON_TOKEN_PATH;
|
|
888
|
+
var init_daemon_auth = __esm({
|
|
889
|
+
"src/lib/daemon-auth.ts"() {
|
|
890
|
+
"use strict";
|
|
891
|
+
init_config();
|
|
892
|
+
init_secure_files();
|
|
893
|
+
DAEMON_TOKEN_PATH = path4.join(EXE_AI_DIR, "exed.token");
|
|
246
894
|
}
|
|
247
895
|
});
|
|
248
896
|
|
|
249
897
|
// src/lib/exe-daemon-client.ts
|
|
250
898
|
import net from "net";
|
|
251
|
-
import
|
|
899
|
+
import os4 from "os";
|
|
252
900
|
import { spawn } from "child_process";
|
|
253
901
|
import { randomUUID } from "crypto";
|
|
254
|
-
import { existsSync as
|
|
255
|
-
import
|
|
902
|
+
import { existsSync as existsSync5, unlinkSync as unlinkSync2, readFileSync as readFileSync4, openSync, closeSync, statSync } from "fs";
|
|
903
|
+
import path5 from "path";
|
|
256
904
|
import { fileURLToPath } from "url";
|
|
257
905
|
function handleData(chunk) {
|
|
258
906
|
_buffer += chunk.toString();
|
|
@@ -280,9 +928,9 @@ function handleData(chunk) {
|
|
|
280
928
|
}
|
|
281
929
|
}
|
|
282
930
|
function cleanupStaleFiles() {
|
|
283
|
-
if (
|
|
931
|
+
if (existsSync5(PID_PATH)) {
|
|
284
932
|
try {
|
|
285
|
-
const pid = parseInt(
|
|
933
|
+
const pid = parseInt(readFileSync4(PID_PATH, "utf8").trim(), 10);
|
|
286
934
|
if (pid > 0) {
|
|
287
935
|
try {
|
|
288
936
|
process.kill(pid, 0);
|
|
@@ -303,17 +951,17 @@ function cleanupStaleFiles() {
|
|
|
303
951
|
}
|
|
304
952
|
}
|
|
305
953
|
function findPackageRoot() {
|
|
306
|
-
let dir =
|
|
307
|
-
const { root } =
|
|
954
|
+
let dir = path5.dirname(fileURLToPath(import.meta.url));
|
|
955
|
+
const { root } = path5.parse(dir);
|
|
308
956
|
while (dir !== root) {
|
|
309
|
-
if (
|
|
310
|
-
dir =
|
|
957
|
+
if (existsSync5(path5.join(dir, "package.json"))) return dir;
|
|
958
|
+
dir = path5.dirname(dir);
|
|
311
959
|
}
|
|
312
960
|
return null;
|
|
313
961
|
}
|
|
314
962
|
function spawnDaemon() {
|
|
315
|
-
const freeGB =
|
|
316
|
-
const totalGB =
|
|
963
|
+
const freeGB = os4.freemem() / (1024 * 1024 * 1024);
|
|
964
|
+
const totalGB = os4.totalmem() / (1024 * 1024 * 1024);
|
|
317
965
|
if (totalGB <= 8) {
|
|
318
966
|
process.stderr.write(
|
|
319
967
|
`[exed-client] SKIP: ${totalGB.toFixed(0)}GB system \u2014 embedding daemon disabled. Using keyword search only. Minimum 16GB recommended for vector search.
|
|
@@ -333,16 +981,17 @@ function spawnDaemon() {
|
|
|
333
981
|
process.stderr.write("[exed-client] WARN: cannot find package root\n");
|
|
334
982
|
return;
|
|
335
983
|
}
|
|
336
|
-
const daemonPath =
|
|
337
|
-
if (!
|
|
984
|
+
const daemonPath = path5.join(pkgRoot, "dist", "lib", "exe-daemon.js");
|
|
985
|
+
if (!existsSync5(daemonPath)) {
|
|
338
986
|
process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
|
|
339
987
|
`);
|
|
340
988
|
return;
|
|
341
989
|
}
|
|
342
990
|
const resolvedPath = daemonPath;
|
|
991
|
+
const daemonToken = ensureDaemonToken(process.env[DAEMON_TOKEN_ENV] ?? null);
|
|
343
992
|
process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
|
|
344
993
|
`);
|
|
345
|
-
const logPath =
|
|
994
|
+
const logPath = path5.join(path5.dirname(SOCKET_PATH), "exed.log");
|
|
346
995
|
let stderrFd = "ignore";
|
|
347
996
|
try {
|
|
348
997
|
stderrFd = openSync(logPath, "a");
|
|
@@ -360,7 +1009,8 @@ function spawnDaemon() {
|
|
|
360
1009
|
TMUX_PANE: void 0,
|
|
361
1010
|
// Prevents resolveExeSession() from scoping to one session
|
|
362
1011
|
EXE_DAEMON_SOCK: SOCKET_PATH,
|
|
363
|
-
EXE_DAEMON_PID: PID_PATH
|
|
1012
|
+
EXE_DAEMON_PID: PID_PATH,
|
|
1013
|
+
[DAEMON_TOKEN_ENV]: daemonToken
|
|
364
1014
|
}
|
|
365
1015
|
});
|
|
366
1016
|
child.unref();
|
|
@@ -467,13 +1117,14 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
467
1117
|
return;
|
|
468
1118
|
}
|
|
469
1119
|
const id = randomUUID();
|
|
1120
|
+
const token = process.env[DAEMON_TOKEN_ENV] ?? readDaemonToken();
|
|
470
1121
|
const timer = setTimeout(() => {
|
|
471
1122
|
_pending.delete(id);
|
|
472
1123
|
resolve({ error: "Request timeout" });
|
|
473
1124
|
}, timeoutMs);
|
|
474
1125
|
_pending.set(id, { resolve, timer });
|
|
475
1126
|
try {
|
|
476
|
-
_socket.write(JSON.stringify({ id, ...payload }) + "\n");
|
|
1127
|
+
_socket.write(JSON.stringify({ id, token, ...payload }) + "\n");
|
|
477
1128
|
} catch {
|
|
478
1129
|
clearTimeout(timer);
|
|
479
1130
|
_pending.delete(id);
|
|
@@ -484,17 +1135,19 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
484
1135
|
function isClientConnected() {
|
|
485
1136
|
return _connected;
|
|
486
1137
|
}
|
|
487
|
-
var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
|
|
1138
|
+
var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, DAEMON_TOKEN_ENV, _socket, _connected, _buffer, _pending, MAX_BUFFER;
|
|
488
1139
|
var init_exe_daemon_client = __esm({
|
|
489
1140
|
"src/lib/exe-daemon-client.ts"() {
|
|
490
1141
|
"use strict";
|
|
491
1142
|
init_config();
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1143
|
+
init_daemon_auth();
|
|
1144
|
+
SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path5.join(EXE_AI_DIR, "exed.sock");
|
|
1145
|
+
PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path5.join(EXE_AI_DIR, "exed.pid");
|
|
1146
|
+
SPAWN_LOCK_PATH = path5.join(EXE_AI_DIR, "exed-spawn.lock");
|
|
495
1147
|
SPAWN_LOCK_STALE_MS = 3e4;
|
|
496
1148
|
CONNECT_TIMEOUT_MS = 15e3;
|
|
497
1149
|
REQUEST_TIMEOUT_MS = 3e4;
|
|
1150
|
+
DAEMON_TOKEN_ENV = "EXE_DAEMON_TOKEN";
|
|
498
1151
|
_socket = null;
|
|
499
1152
|
_connected = false;
|
|
500
1153
|
_buffer = "";
|
|
@@ -573,7 +1226,7 @@ __export(db_daemon_client_exports, {
|
|
|
573
1226
|
createDaemonDbClient: () => createDaemonDbClient,
|
|
574
1227
|
initDaemonDbClient: () => initDaemonDbClient
|
|
575
1228
|
});
|
|
576
|
-
function
|
|
1229
|
+
function normalizeStatement2(stmt) {
|
|
577
1230
|
if (typeof stmt === "string") {
|
|
578
1231
|
return { sql: stmt, args: [] };
|
|
579
1232
|
}
|
|
@@ -597,7 +1250,7 @@ function createDaemonDbClient(fallbackClient) {
|
|
|
597
1250
|
if (!_useDaemon || !isClientConnected()) {
|
|
598
1251
|
return fallbackClient.execute(stmt);
|
|
599
1252
|
}
|
|
600
|
-
const { sql, args } =
|
|
1253
|
+
const { sql, args } = normalizeStatement2(stmt);
|
|
601
1254
|
const response = await sendDaemonRequest({
|
|
602
1255
|
type: "db-execute",
|
|
603
1256
|
sql,
|
|
@@ -622,7 +1275,7 @@ function createDaemonDbClient(fallbackClient) {
|
|
|
622
1275
|
if (!_useDaemon || !isClientConnected()) {
|
|
623
1276
|
return fallbackClient.batch(stmts, mode);
|
|
624
1277
|
}
|
|
625
|
-
const statements = stmts.map(
|
|
1278
|
+
const statements = stmts.map(normalizeStatement2);
|
|
626
1279
|
const response = await sendDaemonRequest({
|
|
627
1280
|
type: "db-batch",
|
|
628
1281
|
statements,
|
|
@@ -717,6 +1370,18 @@ __export(database_exports, {
|
|
|
717
1370
|
});
|
|
718
1371
|
import { createClient } from "@libsql/client";
|
|
719
1372
|
async function initDatabase(config) {
|
|
1373
|
+
if (_walCheckpointTimer) {
|
|
1374
|
+
clearInterval(_walCheckpointTimer);
|
|
1375
|
+
_walCheckpointTimer = null;
|
|
1376
|
+
}
|
|
1377
|
+
if (_daemonClient) {
|
|
1378
|
+
_daemonClient.close();
|
|
1379
|
+
_daemonClient = null;
|
|
1380
|
+
}
|
|
1381
|
+
if (_adapterClient && _adapterClient !== _resilientClient) {
|
|
1382
|
+
_adapterClient.close();
|
|
1383
|
+
}
|
|
1384
|
+
_adapterClient = null;
|
|
720
1385
|
if (_client) {
|
|
721
1386
|
_client.close();
|
|
722
1387
|
_client = null;
|
|
@@ -730,6 +1395,7 @@ async function initDatabase(config) {
|
|
|
730
1395
|
}
|
|
731
1396
|
_client = createClient(opts);
|
|
732
1397
|
_resilientClient = wrapWithRetry(_client);
|
|
1398
|
+
_adapterClient = _resilientClient;
|
|
733
1399
|
_client.execute("PRAGMA busy_timeout = 30000").catch(() => {
|
|
734
1400
|
});
|
|
735
1401
|
_client.execute("PRAGMA journal_mode = WAL").catch(() => {
|
|
@@ -740,14 +1406,20 @@ async function initDatabase(config) {
|
|
|
740
1406
|
});
|
|
741
1407
|
}, 3e4);
|
|
742
1408
|
_walCheckpointTimer.unref();
|
|
1409
|
+
if (process.env.DATABASE_URL) {
|
|
1410
|
+
_adapterClient = await createPrismaDbAdapter(_resilientClient);
|
|
1411
|
+
}
|
|
743
1412
|
}
|
|
744
1413
|
function isInitialized() {
|
|
745
|
-
return _client !== null;
|
|
1414
|
+
return _adapterClient !== null || _client !== null;
|
|
746
1415
|
}
|
|
747
1416
|
function getClient() {
|
|
748
|
-
if (!
|
|
1417
|
+
if (!_adapterClient) {
|
|
749
1418
|
throw new Error("Database client not initialized. Call initDatabase() first.");
|
|
750
1419
|
}
|
|
1420
|
+
if (process.env.DATABASE_URL) {
|
|
1421
|
+
return _adapterClient;
|
|
1422
|
+
}
|
|
751
1423
|
if (process.env.EXE_IS_DAEMON === "1") {
|
|
752
1424
|
return _resilientClient;
|
|
753
1425
|
}
|
|
@@ -757,6 +1429,7 @@ function getClient() {
|
|
|
757
1429
|
return _resilientClient;
|
|
758
1430
|
}
|
|
759
1431
|
async function initDaemonClient() {
|
|
1432
|
+
if (process.env.DATABASE_URL) return;
|
|
760
1433
|
if (process.env.EXE_IS_DAEMON === "1") return;
|
|
761
1434
|
if (!_resilientClient) return;
|
|
762
1435
|
try {
|
|
@@ -1053,6 +1726,7 @@ async function ensureSchema() {
|
|
|
1053
1726
|
project TEXT NOT NULL,
|
|
1054
1727
|
summary TEXT NOT NULL,
|
|
1055
1728
|
task_file TEXT,
|
|
1729
|
+
session_scope TEXT,
|
|
1056
1730
|
read INTEGER NOT NULL DEFAULT 0,
|
|
1057
1731
|
created_at TEXT NOT NULL
|
|
1058
1732
|
);
|
|
@@ -1061,7 +1735,7 @@ async function ensureSchema() {
|
|
|
1061
1735
|
ON notifications(read);
|
|
1062
1736
|
|
|
1063
1737
|
CREATE INDEX IF NOT EXISTS idx_notifications_agent
|
|
1064
|
-
ON notifications(agent_id);
|
|
1738
|
+
ON notifications(agent_id, session_scope);
|
|
1065
1739
|
|
|
1066
1740
|
CREATE INDEX IF NOT EXISTS idx_notifications_task_file
|
|
1067
1741
|
ON notifications(task_file);
|
|
@@ -1099,6 +1773,7 @@ async function ensureSchema() {
|
|
|
1099
1773
|
target_agent TEXT NOT NULL,
|
|
1100
1774
|
target_project TEXT,
|
|
1101
1775
|
target_device TEXT NOT NULL DEFAULT 'local',
|
|
1776
|
+
session_scope TEXT,
|
|
1102
1777
|
content TEXT NOT NULL,
|
|
1103
1778
|
priority TEXT DEFAULT 'normal',
|
|
1104
1779
|
status TEXT DEFAULT 'pending',
|
|
@@ -1112,10 +1787,31 @@ async function ensureSchema() {
|
|
|
1112
1787
|
);
|
|
1113
1788
|
|
|
1114
1789
|
CREATE INDEX IF NOT EXISTS idx_messages_target
|
|
1115
|
-
ON messages(target_agent, status);
|
|
1790
|
+
ON messages(target_agent, session_scope, status);
|
|
1116
1791
|
|
|
1117
1792
|
CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
|
|
1118
|
-
ON messages(target_agent, from_agent, server_seq);
|
|
1793
|
+
ON messages(target_agent, session_scope, from_agent, server_seq);
|
|
1794
|
+
`);
|
|
1795
|
+
try {
|
|
1796
|
+
await client.execute({
|
|
1797
|
+
sql: `ALTER TABLE notifications ADD COLUMN session_scope TEXT`,
|
|
1798
|
+
args: []
|
|
1799
|
+
});
|
|
1800
|
+
} catch {
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
await client.execute({
|
|
1804
|
+
sql: `ALTER TABLE messages ADD COLUMN session_scope TEXT`,
|
|
1805
|
+
args: []
|
|
1806
|
+
});
|
|
1807
|
+
} catch {
|
|
1808
|
+
}
|
|
1809
|
+
await client.executeMultiple(`
|
|
1810
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_agent_scope_read
|
|
1811
|
+
ON notifications(agent_id, session_scope, read, created_at);
|
|
1812
|
+
|
|
1813
|
+
CREATE INDEX IF NOT EXISTS idx_messages_target_scope_status
|
|
1814
|
+
ON messages(target_agent, session_scope, status, created_at);
|
|
1119
1815
|
`);
|
|
1120
1816
|
try {
|
|
1121
1817
|
await client.execute({
|
|
@@ -1699,28 +2395,45 @@ async function ensureSchema() {
|
|
|
1699
2395
|
} catch {
|
|
1700
2396
|
}
|
|
1701
2397
|
}
|
|
2398
|
+
try {
|
|
2399
|
+
await client.execute({
|
|
2400
|
+
sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
|
|
2401
|
+
args: []
|
|
2402
|
+
});
|
|
2403
|
+
} catch {
|
|
2404
|
+
}
|
|
1702
2405
|
}
|
|
1703
2406
|
async function disposeDatabase() {
|
|
2407
|
+
if (_walCheckpointTimer) {
|
|
2408
|
+
clearInterval(_walCheckpointTimer);
|
|
2409
|
+
_walCheckpointTimer = null;
|
|
2410
|
+
}
|
|
1704
2411
|
if (_daemonClient) {
|
|
1705
2412
|
_daemonClient.close();
|
|
1706
2413
|
_daemonClient = null;
|
|
1707
2414
|
}
|
|
2415
|
+
if (_adapterClient && _adapterClient !== _resilientClient) {
|
|
2416
|
+
_adapterClient.close();
|
|
2417
|
+
}
|
|
2418
|
+
_adapterClient = null;
|
|
1708
2419
|
if (_client) {
|
|
1709
2420
|
_client.close();
|
|
1710
2421
|
_client = null;
|
|
1711
2422
|
_resilientClient = null;
|
|
1712
2423
|
}
|
|
1713
2424
|
}
|
|
1714
|
-
var _client, _resilientClient, _walCheckpointTimer, _daemonClient, initTurso, disposeTurso;
|
|
2425
|
+
var _client, _resilientClient, _walCheckpointTimer, _daemonClient, _adapterClient, initTurso, disposeTurso;
|
|
1715
2426
|
var init_database = __esm({
|
|
1716
2427
|
"src/lib/database.ts"() {
|
|
1717
2428
|
"use strict";
|
|
1718
2429
|
init_db_retry();
|
|
1719
2430
|
init_employees();
|
|
2431
|
+
init_database_adapter();
|
|
1720
2432
|
_client = null;
|
|
1721
2433
|
_resilientClient = null;
|
|
1722
2434
|
_walCheckpointTimer = null;
|
|
1723
2435
|
_daemonClient = null;
|
|
2436
|
+
_adapterClient = null;
|
|
1724
2437
|
initTurso = initDatabase;
|
|
1725
2438
|
disposeTurso = disposeDatabase;
|
|
1726
2439
|
}
|
|
@@ -1745,8 +2458,8 @@ __export(crdt_sync_exports, {
|
|
|
1745
2458
|
rebuildFromDb: () => rebuildFromDb
|
|
1746
2459
|
});
|
|
1747
2460
|
import * as Y from "yjs";
|
|
1748
|
-
import { readFileSync as
|
|
1749
|
-
import
|
|
2461
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
2462
|
+
import path7 from "path";
|
|
1750
2463
|
import { homedir } from "os";
|
|
1751
2464
|
function getStatePath() {
|
|
1752
2465
|
return _statePathOverride ?? DEFAULT_STATE_PATH;
|
|
@@ -1758,9 +2471,9 @@ function initCrdtDoc() {
|
|
|
1758
2471
|
if (doc) return doc;
|
|
1759
2472
|
doc = new Y.Doc();
|
|
1760
2473
|
const sp = getStatePath();
|
|
1761
|
-
if (
|
|
2474
|
+
if (existsSync7(sp)) {
|
|
1762
2475
|
try {
|
|
1763
|
-
const state =
|
|
2476
|
+
const state = readFileSync6(sp);
|
|
1764
2477
|
Y.applyUpdate(doc, new Uint8Array(state));
|
|
1765
2478
|
} catch {
|
|
1766
2479
|
console.warn("[crdt-sync] WARN: corrupted state file, rebuilding from DB");
|
|
@@ -1902,10 +2615,10 @@ function persistState() {
|
|
|
1902
2615
|
if (!doc) return;
|
|
1903
2616
|
try {
|
|
1904
2617
|
const sp = getStatePath();
|
|
1905
|
-
const dir =
|
|
1906
|
-
if (!
|
|
2618
|
+
const dir = path7.dirname(sp);
|
|
2619
|
+
if (!existsSync7(dir)) mkdirSync3(dir, { recursive: true });
|
|
1907
2620
|
const state = Y.encodeStateAsUpdate(doc);
|
|
1908
|
-
|
|
2621
|
+
writeFileSync4(sp, Buffer.from(state));
|
|
1909
2622
|
} catch {
|
|
1910
2623
|
}
|
|
1911
2624
|
}
|
|
@@ -1946,7 +2659,7 @@ var DEFAULT_STATE_PATH, _statePathOverride, doc;
|
|
|
1946
2659
|
var init_crdt_sync = __esm({
|
|
1947
2660
|
"src/lib/crdt-sync.ts"() {
|
|
1948
2661
|
"use strict";
|
|
1949
|
-
DEFAULT_STATE_PATH =
|
|
2662
|
+
DEFAULT_STATE_PATH = path7.join(homedir(), ".exe-os", "crdt-state.bin");
|
|
1950
2663
|
_statePathOverride = null;
|
|
1951
2664
|
doc = null;
|
|
1952
2665
|
}
|
|
@@ -1962,14 +2675,14 @@ __export(keychain_exports, {
|
|
|
1962
2675
|
setMasterKey: () => setMasterKey
|
|
1963
2676
|
});
|
|
1964
2677
|
import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
|
|
1965
|
-
import { existsSync as
|
|
1966
|
-
import
|
|
1967
|
-
import
|
|
2678
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2679
|
+
import path8 from "path";
|
|
2680
|
+
import os6 from "os";
|
|
1968
2681
|
function getKeyDir() {
|
|
1969
|
-
return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ??
|
|
2682
|
+
return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path8.join(os6.homedir(), ".exe-os");
|
|
1970
2683
|
}
|
|
1971
2684
|
function getKeyPath() {
|
|
1972
|
-
return
|
|
2685
|
+
return path8.join(getKeyDir(), "master.key");
|
|
1973
2686
|
}
|
|
1974
2687
|
async function tryKeytar() {
|
|
1975
2688
|
try {
|
|
@@ -1990,9 +2703,9 @@ async function getMasterKey() {
|
|
|
1990
2703
|
}
|
|
1991
2704
|
}
|
|
1992
2705
|
const keyPath = getKeyPath();
|
|
1993
|
-
if (!
|
|
2706
|
+
if (!existsSync8(keyPath)) {
|
|
1994
2707
|
process.stderr.write(
|
|
1995
|
-
`[keychain] Key not found at ${keyPath} (HOME=${
|
|
2708
|
+
`[keychain] Key not found at ${keyPath} (HOME=${os6.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
|
|
1996
2709
|
`
|
|
1997
2710
|
);
|
|
1998
2711
|
return null;
|
|
@@ -2033,7 +2746,7 @@ async function deleteMasterKey() {
|
|
|
2033
2746
|
}
|
|
2034
2747
|
}
|
|
2035
2748
|
const keyPath = getKeyPath();
|
|
2036
|
-
if (
|
|
2749
|
+
if (existsSync8(keyPath)) {
|
|
2037
2750
|
await unlink(keyPath);
|
|
2038
2751
|
}
|
|
2039
2752
|
}
|
|
@@ -2077,13 +2790,13 @@ var init_keychain = __esm({
|
|
|
2077
2790
|
|
|
2078
2791
|
// src/lib/cloud-sync.ts
|
|
2079
2792
|
init_database();
|
|
2080
|
-
import { readFileSync as
|
|
2081
|
-
import
|
|
2082
|
-
import
|
|
2793
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync9, readdirSync, mkdirSync as mkdirSync4, appendFileSync, unlinkSync as unlinkSync4, openSync as openSync2, closeSync as closeSync2 } from "fs";
|
|
2794
|
+
import crypto3 from "crypto";
|
|
2795
|
+
import path9 from "path";
|
|
2083
2796
|
import { homedir as homedir2 } from "os";
|
|
2084
2797
|
|
|
2085
2798
|
// src/lib/crypto.ts
|
|
2086
|
-
import
|
|
2799
|
+
import crypto2 from "crypto";
|
|
2087
2800
|
var ALGORITHM = "aes-256-gcm";
|
|
2088
2801
|
var IV_LENGTH = 12;
|
|
2089
2802
|
var TAG_LENGTH = 16;
|
|
@@ -2094,7 +2807,7 @@ function initSyncCrypto(masterKey) {
|
|
|
2094
2807
|
throw new Error(`Master key must be 32 bytes, got ${masterKey.length}`);
|
|
2095
2808
|
}
|
|
2096
2809
|
_syncKey = Buffer.from(
|
|
2097
|
-
|
|
2810
|
+
crypto2.hkdfSync("sha256", masterKey, "", SYNC_HKDF_INFO, 32)
|
|
2098
2811
|
);
|
|
2099
2812
|
}
|
|
2100
2813
|
function isSyncCryptoInitialized() {
|
|
@@ -2108,8 +2821,8 @@ function requireSyncKey() {
|
|
|
2108
2821
|
}
|
|
2109
2822
|
function encryptSyncBlob(data) {
|
|
2110
2823
|
const key = requireSyncKey();
|
|
2111
|
-
const iv =
|
|
2112
|
-
const cipher =
|
|
2824
|
+
const iv = crypto2.randomBytes(IV_LENGTH);
|
|
2825
|
+
const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
|
|
2113
2826
|
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
2114
2827
|
const tag = cipher.getAuthTag();
|
|
2115
2828
|
return Buffer.concat([iv, encrypted, tag]).toString("base64");
|
|
@@ -2123,7 +2836,7 @@ function decryptSyncBlob(ciphertext) {
|
|
|
2123
2836
|
const iv = combined.subarray(0, IV_LENGTH);
|
|
2124
2837
|
const tag = combined.subarray(combined.length - TAG_LENGTH);
|
|
2125
2838
|
const encrypted = combined.subarray(IV_LENGTH, combined.length - TAG_LENGTH);
|
|
2126
|
-
const decipher =
|
|
2839
|
+
const decipher = crypto2.createDecipheriv(ALGORITHM, key, iv);
|
|
2127
2840
|
decipher.setAuthTag(tag);
|
|
2128
2841
|
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
2129
2842
|
}
|
|
@@ -2145,32 +2858,35 @@ function decompress(input) {
|
|
|
2145
2858
|
|
|
2146
2859
|
// src/lib/license.ts
|
|
2147
2860
|
init_config();
|
|
2148
|
-
import { readFileSync as
|
|
2861
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
|
|
2149
2862
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2150
|
-
import
|
|
2863
|
+
import { createRequire as createRequire2 } from "module";
|
|
2864
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
2865
|
+
import os5 from "os";
|
|
2866
|
+
import path6 from "path";
|
|
2151
2867
|
import { jwtVerify, importSPKI } from "jose";
|
|
2152
|
-
var LICENSE_PATH =
|
|
2153
|
-
var CACHE_PATH =
|
|
2154
|
-
var DEVICE_ID_PATH =
|
|
2868
|
+
var LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
|
|
2869
|
+
var CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
|
|
2870
|
+
var DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
|
|
2155
2871
|
function loadDeviceId() {
|
|
2156
|
-
const deviceJsonPath =
|
|
2872
|
+
const deviceJsonPath = path6.join(EXE_AI_DIR, "device.json");
|
|
2157
2873
|
try {
|
|
2158
|
-
if (
|
|
2159
|
-
const data = JSON.parse(
|
|
2874
|
+
if (existsSync6(deviceJsonPath)) {
|
|
2875
|
+
const data = JSON.parse(readFileSync5(deviceJsonPath, "utf8"));
|
|
2160
2876
|
if (data.deviceId) return data.deviceId;
|
|
2161
2877
|
}
|
|
2162
2878
|
} catch {
|
|
2163
2879
|
}
|
|
2164
2880
|
try {
|
|
2165
|
-
if (
|
|
2166
|
-
const id2 =
|
|
2881
|
+
if (existsSync6(DEVICE_ID_PATH)) {
|
|
2882
|
+
const id2 = readFileSync5(DEVICE_ID_PATH, "utf8").trim();
|
|
2167
2883
|
if (id2) return id2;
|
|
2168
2884
|
}
|
|
2169
2885
|
} catch {
|
|
2170
2886
|
}
|
|
2171
2887
|
const id = randomUUID2();
|
|
2172
|
-
|
|
2173
|
-
|
|
2888
|
+
mkdirSync2(EXE_AI_DIR, { recursive: true });
|
|
2889
|
+
writeFileSync3(DEVICE_ID_PATH, id, "utf8");
|
|
2174
2890
|
return id;
|
|
2175
2891
|
}
|
|
2176
2892
|
|
|
@@ -2178,12 +2894,13 @@ function loadDeviceId() {
|
|
|
2178
2894
|
init_config();
|
|
2179
2895
|
init_crdt_sync();
|
|
2180
2896
|
init_employees();
|
|
2897
|
+
init_secure_files();
|
|
2181
2898
|
function sqlSafe(v) {
|
|
2182
2899
|
return v === void 0 ? null : v;
|
|
2183
2900
|
}
|
|
2184
2901
|
function logError(msg) {
|
|
2185
2902
|
try {
|
|
2186
|
-
const logPath =
|
|
2903
|
+
const logPath = path9.join(homedir2(), ".exe-os", "workers.log");
|
|
2187
2904
|
appendFileSync(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
2188
2905
|
`);
|
|
2189
2906
|
} catch {
|
|
@@ -2192,24 +2909,93 @@ function logError(msg) {
|
|
|
2192
2909
|
var LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
|
|
2193
2910
|
var FETCH_TIMEOUT_MS = 3e4;
|
|
2194
2911
|
var PUSH_BATCH_SIZE = 5e3;
|
|
2195
|
-
var ROSTER_LOCK_PATH =
|
|
2912
|
+
var ROSTER_LOCK_PATH = path9.join(EXE_AI_DIR, "roster-merge.lock");
|
|
2196
2913
|
var LOCK_STALE_MS = 3e4;
|
|
2914
|
+
var _pgPromise = null;
|
|
2915
|
+
var _pgFailed = false;
|
|
2916
|
+
function loadPgClient() {
|
|
2917
|
+
if (_pgFailed) return null;
|
|
2918
|
+
const postgresUrl = process.env.DATABASE_URL;
|
|
2919
|
+
const configPath = path9.join(EXE_AI_DIR, "config.json");
|
|
2920
|
+
let cloudPostgresUrl;
|
|
2921
|
+
try {
|
|
2922
|
+
if (existsSync9(configPath)) {
|
|
2923
|
+
const cfg = JSON.parse(readFileSync7(configPath, "utf8"));
|
|
2924
|
+
cloudPostgresUrl = cfg.cloud?.postgresUrl;
|
|
2925
|
+
if (cfg.cloud?.syncToPostgres === false) {
|
|
2926
|
+
_pgFailed = true;
|
|
2927
|
+
return null;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
} catch {
|
|
2931
|
+
}
|
|
2932
|
+
const url = postgresUrl || cloudPostgresUrl;
|
|
2933
|
+
if (!url) {
|
|
2934
|
+
_pgFailed = true;
|
|
2935
|
+
return null;
|
|
2936
|
+
}
|
|
2937
|
+
if (!_pgPromise) {
|
|
2938
|
+
_pgPromise = (async () => {
|
|
2939
|
+
const { createRequire: createRequire3 } = await import("module");
|
|
2940
|
+
const { pathToFileURL: pathToFileURL3 } = await import("url");
|
|
2941
|
+
const exeDbRoot = process.env.EXE_DB_ROOT ?? path9.join(homedir2(), "exe-db");
|
|
2942
|
+
const req = createRequire3(path9.join(exeDbRoot, "package.json"));
|
|
2943
|
+
const entry = req.resolve("@prisma/client");
|
|
2944
|
+
const mod = await import(pathToFileURL3(entry).href);
|
|
2945
|
+
const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
|
|
2946
|
+
if (!Ctor) throw new Error("No PrismaClient");
|
|
2947
|
+
return new Ctor();
|
|
2948
|
+
})().catch(() => {
|
|
2949
|
+
_pgFailed = true;
|
|
2950
|
+
_pgPromise = null;
|
|
2951
|
+
throw new Error("pg_unavailable");
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
return _pgPromise;
|
|
2955
|
+
}
|
|
2956
|
+
async function pushToPostgres(records) {
|
|
2957
|
+
const loader = loadPgClient();
|
|
2958
|
+
if (!loader) return 0;
|
|
2959
|
+
let prisma;
|
|
2960
|
+
try {
|
|
2961
|
+
prisma = await loader;
|
|
2962
|
+
} catch {
|
|
2963
|
+
return 0;
|
|
2964
|
+
}
|
|
2965
|
+
let inserted = 0;
|
|
2966
|
+
for (const rec of records) {
|
|
2967
|
+
try {
|
|
2968
|
+
await prisma.$executeRawUnsafe(
|
|
2969
|
+
`INSERT INTO raw.raw_events (id, source, source_id, event_type, payload, metadata, timestamp)
|
|
2970
|
+
VALUES (gen_random_uuid(), 'cloud_sync', $1, 'memory', $2::jsonb, $3::jsonb, $4)
|
|
2971
|
+
ON CONFLICT (source, source_id, event_type) DO NOTHING`,
|
|
2972
|
+
String(rec.id ?? ""),
|
|
2973
|
+
JSON.stringify(rec),
|
|
2974
|
+
JSON.stringify({ agent_id: rec.agent_id, project_name: rec.project_name, tool_name: rec.tool_name }),
|
|
2975
|
+
rec.timestamp ? new Date(String(rec.timestamp)) : /* @__PURE__ */ new Date()
|
|
2976
|
+
);
|
|
2977
|
+
inserted++;
|
|
2978
|
+
} catch {
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
return inserted;
|
|
2982
|
+
}
|
|
2197
2983
|
async function withRosterLock(fn) {
|
|
2198
2984
|
try {
|
|
2199
2985
|
const fd = openSync2(ROSTER_LOCK_PATH, "wx");
|
|
2200
2986
|
closeSync2(fd);
|
|
2201
|
-
|
|
2987
|
+
writeFileSync5(ROSTER_LOCK_PATH, String(Date.now()));
|
|
2202
2988
|
} catch (err) {
|
|
2203
2989
|
if (err.code === "EEXIST") {
|
|
2204
2990
|
try {
|
|
2205
|
-
const ts = parseInt(
|
|
2991
|
+
const ts = parseInt(readFileSync7(ROSTER_LOCK_PATH, "utf-8"), 10);
|
|
2206
2992
|
if (Date.now() - ts < LOCK_STALE_MS) {
|
|
2207
2993
|
throw new Error("Roster merge already in progress \u2014 another sync is running");
|
|
2208
2994
|
}
|
|
2209
2995
|
unlinkSync4(ROSTER_LOCK_PATH);
|
|
2210
2996
|
const fd = openSync2(ROSTER_LOCK_PATH, "wx");
|
|
2211
2997
|
closeSync2(fd);
|
|
2212
|
-
|
|
2998
|
+
writeFileSync5(ROSTER_LOCK_PATH, String(Date.now()));
|
|
2213
2999
|
} catch (retryErr) {
|
|
2214
3000
|
if (retryErr instanceof Error && retryErr.message.includes("already in progress")) throw retryErr;
|
|
2215
3001
|
throw new Error("Roster merge already in progress \u2014 another sync is running");
|
|
@@ -2479,6 +3265,10 @@ async function cloudSync(config) {
|
|
|
2479
3265
|
const maxVersion = Number(records[records.length - 1].version);
|
|
2480
3266
|
const pushOk = await cloudPush(records, maxVersion, config);
|
|
2481
3267
|
if (!pushOk) break;
|
|
3268
|
+
try {
|
|
3269
|
+
await pushToPostgres(records);
|
|
3270
|
+
} catch {
|
|
3271
|
+
}
|
|
2482
3272
|
await client.execute({
|
|
2483
3273
|
sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_push_version', ?)",
|
|
2484
3274
|
args: [String(maxVersion)]
|
|
@@ -2583,8 +3373,8 @@ async function cloudSync(config) {
|
|
|
2583
3373
|
try {
|
|
2584
3374
|
const employees = await loadEmployees();
|
|
2585
3375
|
rosterResult.employees = employees.length;
|
|
2586
|
-
const idDir =
|
|
2587
|
-
if (
|
|
3376
|
+
const idDir = path9.join(EXE_AI_DIR, "identity");
|
|
3377
|
+
if (existsSync9(idDir)) {
|
|
2588
3378
|
rosterResult.identities = readdirSync(idDir).filter((f) => f.endsWith(".md")).length;
|
|
2589
3379
|
}
|
|
2590
3380
|
} catch {
|
|
@@ -2602,66 +3392,66 @@ async function cloudSync(config) {
|
|
|
2602
3392
|
roster: rosterResult
|
|
2603
3393
|
};
|
|
2604
3394
|
}
|
|
2605
|
-
var ROSTER_DELETIONS_PATH =
|
|
3395
|
+
var ROSTER_DELETIONS_PATH = path9.join(EXE_AI_DIR, "roster-deletions.json");
|
|
2606
3396
|
function recordRosterDeletion(name) {
|
|
2607
3397
|
let deletions = [];
|
|
2608
3398
|
try {
|
|
2609
|
-
if (
|
|
2610
|
-
deletions = JSON.parse(
|
|
3399
|
+
if (existsSync9(ROSTER_DELETIONS_PATH)) {
|
|
3400
|
+
deletions = JSON.parse(readFileSync7(ROSTER_DELETIONS_PATH, "utf-8"));
|
|
2611
3401
|
}
|
|
2612
3402
|
} catch {
|
|
2613
3403
|
}
|
|
2614
3404
|
if (!deletions.includes(name)) deletions.push(name);
|
|
2615
|
-
|
|
3405
|
+
writeFileSync5(ROSTER_DELETIONS_PATH, JSON.stringify(deletions));
|
|
2616
3406
|
}
|
|
2617
3407
|
function consumeRosterDeletions() {
|
|
2618
3408
|
try {
|
|
2619
|
-
if (!
|
|
2620
|
-
const deletions = JSON.parse(
|
|
2621
|
-
|
|
3409
|
+
if (!existsSync9(ROSTER_DELETIONS_PATH)) return [];
|
|
3410
|
+
const deletions = JSON.parse(readFileSync7(ROSTER_DELETIONS_PATH, "utf-8"));
|
|
3411
|
+
writeFileSync5(ROSTER_DELETIONS_PATH, "[]");
|
|
2622
3412
|
return deletions;
|
|
2623
3413
|
} catch {
|
|
2624
3414
|
return [];
|
|
2625
3415
|
}
|
|
2626
3416
|
}
|
|
2627
3417
|
function buildRosterBlob(paths) {
|
|
2628
|
-
const rosterPath = paths?.rosterPath ??
|
|
2629
|
-
const identityDir = paths?.identityDir ??
|
|
2630
|
-
const configPath = paths?.configPath ??
|
|
3418
|
+
const rosterPath = paths?.rosterPath ?? path9.join(EXE_AI_DIR, "exe-employees.json");
|
|
3419
|
+
const identityDir = paths?.identityDir ?? path9.join(EXE_AI_DIR, "identity");
|
|
3420
|
+
const configPath = paths?.configPath ?? path9.join(EXE_AI_DIR, "config.json");
|
|
2631
3421
|
let roster = [];
|
|
2632
|
-
if (
|
|
3422
|
+
if (existsSync9(rosterPath)) {
|
|
2633
3423
|
try {
|
|
2634
|
-
roster = JSON.parse(
|
|
3424
|
+
roster = JSON.parse(readFileSync7(rosterPath, "utf-8"));
|
|
2635
3425
|
} catch {
|
|
2636
3426
|
}
|
|
2637
3427
|
}
|
|
2638
3428
|
const identities = {};
|
|
2639
|
-
if (
|
|
3429
|
+
if (existsSync9(identityDir)) {
|
|
2640
3430
|
for (const file of readdirSync(identityDir).filter((f) => f.endsWith(".md"))) {
|
|
2641
3431
|
try {
|
|
2642
|
-
identities[file] =
|
|
3432
|
+
identities[file] = readFileSync7(path9.join(identityDir, file), "utf-8");
|
|
2643
3433
|
} catch {
|
|
2644
3434
|
}
|
|
2645
3435
|
}
|
|
2646
3436
|
}
|
|
2647
3437
|
let config;
|
|
2648
|
-
if (
|
|
3438
|
+
if (existsSync9(configPath)) {
|
|
2649
3439
|
try {
|
|
2650
|
-
config = JSON.parse(
|
|
3440
|
+
config = JSON.parse(readFileSync7(configPath, "utf-8"));
|
|
2651
3441
|
} catch {
|
|
2652
3442
|
}
|
|
2653
3443
|
}
|
|
2654
3444
|
let agentConfig;
|
|
2655
|
-
const agentConfigPath =
|
|
2656
|
-
if (
|
|
3445
|
+
const agentConfigPath = path9.join(EXE_AI_DIR, "agent-config.json");
|
|
3446
|
+
if (existsSync9(agentConfigPath)) {
|
|
2657
3447
|
try {
|
|
2658
|
-
agentConfig = JSON.parse(
|
|
3448
|
+
agentConfig = JSON.parse(readFileSync7(agentConfigPath, "utf-8"));
|
|
2659
3449
|
} catch {
|
|
2660
3450
|
}
|
|
2661
3451
|
}
|
|
2662
3452
|
const deletedNames = consumeRosterDeletions();
|
|
2663
3453
|
const content = JSON.stringify({ roster, identities, config, agentConfig, deletedNames });
|
|
2664
|
-
const hash =
|
|
3454
|
+
const hash = crypto3.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
2665
3455
|
return { roster, identities, config, agentConfig, deletedNames, version: hash };
|
|
2666
3456
|
}
|
|
2667
3457
|
async function cloudPushRoster(config) {
|
|
@@ -2731,23 +3521,24 @@ async function cloudPullRoster(config) {
|
|
|
2731
3521
|
}
|
|
2732
3522
|
}
|
|
2733
3523
|
function mergeConfig(remoteConfig, configPath) {
|
|
2734
|
-
const cfgPath = configPath ??
|
|
3524
|
+
const cfgPath = configPath ?? path9.join(EXE_AI_DIR, "config.json");
|
|
2735
3525
|
let local = {};
|
|
2736
|
-
if (
|
|
3526
|
+
if (existsSync9(cfgPath)) {
|
|
2737
3527
|
try {
|
|
2738
|
-
local = JSON.parse(
|
|
3528
|
+
local = JSON.parse(readFileSync7(cfgPath, "utf-8"));
|
|
2739
3529
|
} catch {
|
|
2740
3530
|
}
|
|
2741
3531
|
}
|
|
2742
3532
|
const merged = { ...remoteConfig, ...local };
|
|
2743
|
-
const dir =
|
|
2744
|
-
|
|
2745
|
-
|
|
3533
|
+
const dir = path9.dirname(cfgPath);
|
|
3534
|
+
ensurePrivateDirSync(dir);
|
|
3535
|
+
writeFileSync5(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
3536
|
+
enforcePrivateFileSync(cfgPath);
|
|
2746
3537
|
}
|
|
2747
3538
|
async function mergeRosterFromRemote(remote, paths) {
|
|
2748
3539
|
return withRosterLock(async () => {
|
|
2749
3540
|
const rosterPath = paths?.rosterPath ?? void 0;
|
|
2750
|
-
const identityDir = paths?.identityDir ??
|
|
3541
|
+
const identityDir = paths?.identityDir ?? path9.join(EXE_AI_DIR, "identity");
|
|
2751
3542
|
const localEmployees = await loadEmployees(rosterPath);
|
|
2752
3543
|
const localNames = new Set(localEmployees.map((e) => e.name));
|
|
2753
3544
|
let added = 0;
|
|
@@ -2768,15 +3559,15 @@ async function mergeRosterFromRemote(remote, paths) {
|
|
|
2768
3559
|
) ?? lookupKey;
|
|
2769
3560
|
const remoteIdentity = remote.identities[matchedKey];
|
|
2770
3561
|
if (remoteIdentity) {
|
|
2771
|
-
if (!
|
|
2772
|
-
const idPath =
|
|
3562
|
+
if (!existsSync9(identityDir)) mkdirSync4(identityDir, { recursive: true });
|
|
3563
|
+
const idPath = path9.join(identityDir, `${remoteEmp.name}.md`);
|
|
2773
3564
|
let localIdentity = null;
|
|
2774
3565
|
try {
|
|
2775
|
-
localIdentity =
|
|
3566
|
+
localIdentity = existsSync9(idPath) ? readFileSync7(idPath, "utf-8") : null;
|
|
2776
3567
|
} catch {
|
|
2777
3568
|
}
|
|
2778
3569
|
if (localIdentity !== remoteIdentity) {
|
|
2779
|
-
|
|
3570
|
+
writeFileSync5(idPath, remoteIdentity, "utf-8");
|
|
2780
3571
|
identitiesUpdated++;
|
|
2781
3572
|
}
|
|
2782
3573
|
}
|
|
@@ -2802,16 +3593,18 @@ async function mergeRosterFromRemote(remote, paths) {
|
|
|
2802
3593
|
}
|
|
2803
3594
|
if (remote.agentConfig && Object.keys(remote.agentConfig).length > 0) {
|
|
2804
3595
|
try {
|
|
2805
|
-
const agentConfigPath =
|
|
3596
|
+
const agentConfigPath = path9.join(EXE_AI_DIR, "agent-config.json");
|
|
2806
3597
|
let local = {};
|
|
2807
|
-
if (
|
|
3598
|
+
if (existsSync9(agentConfigPath)) {
|
|
2808
3599
|
try {
|
|
2809
|
-
local = JSON.parse(
|
|
3600
|
+
local = JSON.parse(readFileSync7(agentConfigPath, "utf-8"));
|
|
2810
3601
|
} catch {
|
|
2811
3602
|
}
|
|
2812
3603
|
}
|
|
2813
3604
|
const merged = { ...remote.agentConfig, ...local };
|
|
2814
|
-
|
|
3605
|
+
ensurePrivateDirSync(path9.dirname(agentConfigPath));
|
|
3606
|
+
writeFileSync5(agentConfigPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
3607
|
+
enforcePrivateFileSync(agentConfigPath);
|
|
2815
3608
|
} catch {
|
|
2816
3609
|
}
|
|
2817
3610
|
}
|
|
@@ -3259,5 +4052,6 @@ export {
|
|
|
3259
4052
|
cloudSync,
|
|
3260
4053
|
mergeConfig,
|
|
3261
4054
|
mergeRosterFromRemote,
|
|
4055
|
+
pushToPostgres,
|
|
3262
4056
|
recordRosterDeletion
|
|
3263
4057
|
};
|