@askexenow/exe-os 0.8.38 → 0.8.39
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 -8
- package/dist/bin/backfill-conversations.js +46 -10
- package/dist/bin/backfill-responses.js +46 -10
- package/dist/bin/backfill-vectors.js +42 -8
- package/dist/bin/cleanup-stale-review-tasks.js +37 -8
- package/dist/bin/cli.js +281 -154
- package/dist/bin/exe-agent.js +19 -4
- package/dist/bin/exe-assign.js +39 -5
- package/dist/bin/exe-boot.js +237 -111
- package/dist/bin/exe-call.js +11 -6
- package/dist/bin/exe-cloud.js +99 -28
- package/dist/bin/exe-dispatch.js +1 -1
- package/dist/bin/exe-doctor.js +37 -8
- package/dist/bin/exe-export-behaviors.js +39 -10
- package/dist/bin/exe-forget.js +38 -9
- package/dist/bin/exe-gateway.js +109 -42
- package/dist/bin/exe-heartbeat.js +49 -20
- package/dist/bin/exe-kill.js +39 -10
- package/dist/bin/exe-launch-agent.js +58 -22
- package/dist/bin/exe-link.js +184 -85
- package/dist/bin/exe-new-employee.js +21 -7
- package/dist/bin/exe-pending-messages.js +46 -17
- package/dist/bin/exe-pending-notifications.js +37 -8
- package/dist/bin/exe-pending-reviews.js +47 -18
- package/dist/bin/exe-rename.js +21 -7
- package/dist/bin/exe-review.js +34 -5
- package/dist/bin/exe-search.js +47 -10
- package/dist/bin/exe-session-cleanup.js +56 -19
- package/dist/bin/exe-settings.js +63 -2
- package/dist/bin/exe-status.js +34 -5
- package/dist/bin/exe-team.js +34 -5
- package/dist/bin/git-sweep.js +38 -9
- package/dist/bin/graph-backfill.js +37 -8
- package/dist/bin/graph-export.js +37 -8
- package/dist/bin/install.js +1 -1
- package/dist/bin/scan-tasks.js +40 -11
- package/dist/bin/setup.js +58 -24
- package/dist/bin/shard-migrate.js +37 -8
- package/dist/bin/wiki-sync.js +39 -9
- package/dist/gateway/index.js +102 -37
- package/dist/hooks/bug-report-worker.js +62 -28
- package/dist/hooks/commit-complete.js +38 -9
- package/dist/hooks/error-recall.js +49 -8
- package/dist/hooks/exe-heartbeat-hook.js +3 -2
- package/dist/hooks/ingest-worker.js +151 -37
- package/dist/hooks/ingest.js +74 -28
- package/dist/hooks/instructions-loaded.js +39 -9
- package/dist/hooks/notification.js +37 -7
- package/dist/hooks/post-compact.js +37 -7
- package/dist/hooks/pre-compact.js +35 -6
- package/dist/hooks/pre-tool-use.js +52 -14
- package/dist/hooks/prompt-ingest-worker.js +56 -10
- package/dist/hooks/prompt-submit.js +61 -23
- package/dist/hooks/response-ingest-worker.js +57 -11
- package/dist/hooks/session-end.js +43 -10
- package/dist/hooks/session-start.js +46 -8
- package/dist/hooks/stop.js +37 -7
- package/dist/hooks/subagent-stop.js +37 -7
- package/dist/hooks/summary-worker.js +317 -99
- package/dist/index.js +87 -22
- package/dist/lib/cloud-sync.js +172 -78
- package/dist/lib/config.js +4 -1
- package/dist/lib/consolidation.js +5 -4
- package/dist/lib/database.js +1 -0
- package/dist/lib/device-registry.js +2 -1
- package/dist/lib/embedder.js +9 -1
- package/dist/lib/employees.js +11 -6
- package/dist/lib/exe-daemon-client.js +6 -1
- package/dist/lib/exe-daemon.js +71 -28
- package/dist/lib/hybrid-search.js +47 -10
- package/dist/lib/identity.js +1 -1
- package/dist/lib/keychain.js +2 -1
- package/dist/lib/license.js +13 -4
- package/dist/lib/messaging.js +1 -1
- package/dist/lib/reminders.js +2 -2
- package/dist/lib/schedules.js +37 -8
- package/dist/lib/skill-learning.js +1 -1
- package/dist/lib/store.js +37 -8
- package/dist/lib/tasks.js +1 -1
- package/dist/lib/tmux-routing.js +1 -1
- package/dist/mcp/server.js +97 -43
- package/dist/mcp/tools/complete-reminder.js +1 -1
- package/dist/mcp/tools/create-task.js +14 -6
- package/dist/mcp/tools/deactivate-behavior.js +2 -2
- package/dist/mcp/tools/list-reminders.js +1 -1
- package/dist/mcp/tools/list-tasks.js +1 -1
- package/dist/mcp/tools/send-message.js +1 -1
- package/dist/mcp/tools/update-task.js +1 -1
- package/dist/runtime/index.js +35 -6
- package/dist/tui/App.js +177 -95
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -461,6 +461,7 @@ async function ensureSchema() {
|
|
|
461
461
|
const client = getRawClient();
|
|
462
462
|
await client.execute("PRAGMA journal_mode = WAL");
|
|
463
463
|
await client.execute("PRAGMA busy_timeout = 30000");
|
|
464
|
+
await client.execute("PRAGMA wal_autocheckpoint = 1000");
|
|
464
465
|
try {
|
|
465
466
|
await client.execute("PRAGMA libsql_vector_search_ef = 128");
|
|
466
467
|
} catch {
|
|
@@ -1284,7 +1285,7 @@ __export(config_exports, {
|
|
|
1284
1285
|
migrateConfig: () => migrateConfig,
|
|
1285
1286
|
saveConfig: () => saveConfig
|
|
1286
1287
|
});
|
|
1287
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
1288
|
+
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
1288
1289
|
import { readFileSync as readFileSync3, existsSync as existsSync3, renameSync as renameSync2 } from "fs";
|
|
1289
1290
|
import path4 from "path";
|
|
1290
1291
|
import os4 from "os";
|
|
@@ -1410,6 +1411,9 @@ async function saveConfig(config2) {
|
|
|
1410
1411
|
await mkdir(dir, { recursive: true });
|
|
1411
1412
|
const configPath = path4.join(dir, "config.json");
|
|
1412
1413
|
await writeFile(configPath, JSON.stringify(config2, null, 2) + "\n");
|
|
1414
|
+
if (config2.cloud?.apiKey) {
|
|
1415
|
+
await chmod(configPath, 384);
|
|
1416
|
+
}
|
|
1413
1417
|
}
|
|
1414
1418
|
async function loadConfigFrom(configPath) {
|
|
1415
1419
|
const raw = await readFile(configPath, "utf-8");
|
|
@@ -3958,12 +3962,13 @@ var init_memory = __esm({
|
|
|
3958
3962
|
});
|
|
3959
3963
|
|
|
3960
3964
|
// src/lib/keychain.ts
|
|
3961
|
-
import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod } from "fs/promises";
|
|
3965
|
+
import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
|
|
3962
3966
|
import { existsSync as existsSync11 } from "fs";
|
|
3963
3967
|
import path15 from "path";
|
|
3968
|
+
import os7 from "os";
|
|
3964
3969
|
import crypto6 from "crypto";
|
|
3965
3970
|
function getKeyDir() {
|
|
3966
|
-
return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path15.join(
|
|
3971
|
+
return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path15.join(os7.homedir(), ".exe-os");
|
|
3967
3972
|
}
|
|
3968
3973
|
function getKeyPath() {
|
|
3969
3974
|
return path15.join(getKeyDir(), "master.key");
|
|
@@ -4264,6 +4269,28 @@ __export(store_exports, {
|
|
|
4264
4269
|
vectorToBlob: () => vectorToBlob,
|
|
4265
4270
|
writeMemory: () => writeMemory
|
|
4266
4271
|
});
|
|
4272
|
+
function isBusyError2(err) {
|
|
4273
|
+
if (err instanceof Error) {
|
|
4274
|
+
const msg = err.message.toLowerCase();
|
|
4275
|
+
return msg.includes("sqlite_busy") || msg.includes("database is locked");
|
|
4276
|
+
}
|
|
4277
|
+
return false;
|
|
4278
|
+
}
|
|
4279
|
+
async function retryOnBusy2(fn, label) {
|
|
4280
|
+
for (let attempt = 0; attempt <= INIT_MAX_RETRIES; attempt++) {
|
|
4281
|
+
try {
|
|
4282
|
+
return await fn();
|
|
4283
|
+
} catch (err) {
|
|
4284
|
+
if (!isBusyError2(err) || attempt === INIT_MAX_RETRIES) throw err;
|
|
4285
|
+
process.stderr.write(
|
|
4286
|
+
`[store] SQLITE_BUSY during ${label}, retry ${attempt + 1}/${INIT_MAX_RETRIES}
|
|
4287
|
+
`
|
|
4288
|
+
);
|
|
4289
|
+
await new Promise((r) => setTimeout(r, INIT_RETRY_DELAY_MS * (attempt + 1)));
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
throw new Error("unreachable");
|
|
4293
|
+
}
|
|
4267
4294
|
async function initStore(options) {
|
|
4268
4295
|
if (_flushTimer !== null) {
|
|
4269
4296
|
clearInterval(_flushTimer);
|
|
@@ -4292,14 +4319,17 @@ async function initStore(options) {
|
|
|
4292
4319
|
dbPath,
|
|
4293
4320
|
encryptionKey: hexKey
|
|
4294
4321
|
});
|
|
4295
|
-
await ensureSchema();
|
|
4322
|
+
await retryOnBusy2(() => ensureSchema(), "ensureSchema");
|
|
4296
4323
|
try {
|
|
4297
4324
|
const { initShardManager: initShardManager2 } = await Promise.resolve().then(() => (init_shard_manager(), shard_manager_exports));
|
|
4298
4325
|
initShardManager2(hexKey);
|
|
4299
4326
|
} catch {
|
|
4300
4327
|
}
|
|
4301
4328
|
const client = getClient();
|
|
4302
|
-
const vResult = await
|
|
4329
|
+
const vResult = await retryOnBusy2(
|
|
4330
|
+
() => client.execute("SELECT MAX(version) as max_v FROM memories"),
|
|
4331
|
+
"version-query"
|
|
4332
|
+
);
|
|
4303
4333
|
_nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
4304
4334
|
}
|
|
4305
4335
|
function classifyTier(record) {
|
|
@@ -4679,7 +4709,7 @@ async function getMemoryCardinality(agentId) {
|
|
|
4679
4709
|
return 0;
|
|
4680
4710
|
}
|
|
4681
4711
|
}
|
|
4682
|
-
var _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
|
|
4712
|
+
var INIT_MAX_RETRIES, INIT_RETRY_DELAY_MS, _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
|
|
4683
4713
|
var init_store = __esm({
|
|
4684
4714
|
"src/lib/store.ts"() {
|
|
4685
4715
|
"use strict";
|
|
@@ -4687,6 +4717,8 @@ var init_store = __esm({
|
|
|
4687
4717
|
init_database();
|
|
4688
4718
|
init_keychain();
|
|
4689
4719
|
init_config();
|
|
4720
|
+
INIT_MAX_RETRIES = 3;
|
|
4721
|
+
INIT_RETRY_DELAY_MS = 1e3;
|
|
4690
4722
|
_pendingRecords = [];
|
|
4691
4723
|
_batchSize = 20;
|
|
4692
4724
|
_flushIntervalMs = 1e4;
|
|
@@ -5079,7 +5111,8 @@ async function gqlRequest(query, variables) {
|
|
|
5079
5111
|
"Content-Type": "application/json",
|
|
5080
5112
|
Authorization: `Bearer ${config.apiToken}`
|
|
5081
5113
|
},
|
|
5082
|
-
body: JSON.stringify({ query, variables })
|
|
5114
|
+
body: JSON.stringify({ query, variables }),
|
|
5115
|
+
signal: AbortSignal.timeout(3e4)
|
|
5083
5116
|
});
|
|
5084
5117
|
if (!res.ok) {
|
|
5085
5118
|
throw new Error(`CRM GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
@@ -5307,6 +5340,10 @@ import path17 from "path";
|
|
|
5307
5340
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5308
5341
|
function handleData(chunk) {
|
|
5309
5342
|
_buffer += chunk.toString();
|
|
5343
|
+
if (_buffer.length > MAX_BUFFER) {
|
|
5344
|
+
_buffer = "";
|
|
5345
|
+
return;
|
|
5346
|
+
}
|
|
5310
5347
|
let newlineIdx;
|
|
5311
5348
|
while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
|
|
5312
5349
|
const line = _buffer.slice(0, newlineIdx).trim();
|
|
@@ -5614,7 +5651,7 @@ function disconnectClient() {
|
|
|
5614
5651
|
entry.resolve({ error: "Client disconnected" });
|
|
5615
5652
|
}
|
|
5616
5653
|
}
|
|
5617
|
-
var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _requestCount, HEALTH_CHECK_INTERVAL, _pending;
|
|
5654
|
+
var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _requestCount, HEALTH_CHECK_INTERVAL, _pending, MAX_BUFFER;
|
|
5618
5655
|
var init_exe_daemon_client = __esm({
|
|
5619
5656
|
"src/lib/exe-daemon-client.ts"() {
|
|
5620
5657
|
"use strict";
|
|
@@ -5631,6 +5668,7 @@ var init_exe_daemon_client = __esm({
|
|
|
5631
5668
|
_requestCount = 0;
|
|
5632
5669
|
HEALTH_CHECK_INTERVAL = 100;
|
|
5633
5670
|
_pending = /* @__PURE__ */ new Map();
|
|
5671
|
+
MAX_BUFFER = 1e7;
|
|
5634
5672
|
}
|
|
5635
5673
|
});
|
|
5636
5674
|
|
|
@@ -5720,12 +5758,30 @@ async function wikiFetch(config2, path20, method = "GET", body) {
|
|
|
5720
5758
|
const controller = new AbortController();
|
|
5721
5759
|
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
|
|
5722
5760
|
try {
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5761
|
+
let response;
|
|
5762
|
+
try {
|
|
5763
|
+
response = await fetch(url, {
|
|
5764
|
+
method,
|
|
5765
|
+
headers,
|
|
5766
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
5767
|
+
signal: controller.signal
|
|
5768
|
+
});
|
|
5769
|
+
} catch {
|
|
5770
|
+
clearTimeout(timeout);
|
|
5771
|
+
const retryController = new AbortController();
|
|
5772
|
+
const retryTimeout = setTimeout(() => retryController.abort(), REQUEST_TIMEOUT_MS2);
|
|
5773
|
+
try {
|
|
5774
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
5775
|
+
response = await fetch(url, {
|
|
5776
|
+
method,
|
|
5777
|
+
headers,
|
|
5778
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
5779
|
+
signal: retryController.signal
|
|
5780
|
+
});
|
|
5781
|
+
} finally {
|
|
5782
|
+
clearTimeout(retryTimeout);
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5729
5785
|
if (!response.ok) {
|
|
5730
5786
|
throw new Error(`Wiki API ${method} ${path20}: ${response.status} ${response.statusText}`);
|
|
5731
5787
|
}
|
|
@@ -9814,7 +9870,8 @@ var OllamaProvider = class {
|
|
|
9814
9870
|
const res = await fetch(`${this.host}/api/chat`, {
|
|
9815
9871
|
method: "POST",
|
|
9816
9872
|
headers: { "Content-Type": "application/json" },
|
|
9817
|
-
body: JSON.stringify(body)
|
|
9873
|
+
body: JSON.stringify(body),
|
|
9874
|
+
signal: AbortSignal.timeout(3e4)
|
|
9818
9875
|
});
|
|
9819
9876
|
if (!res.ok) {
|
|
9820
9877
|
throw new Error(`Ollama API error: ${res.status} ${await res.text()}`);
|
|
@@ -10242,7 +10299,7 @@ var SignalAdapter = class {
|
|
|
10242
10299
|
if (/^https?:\/\//i.test(trimmed)) {
|
|
10243
10300
|
return trimmed.replace(/\/+$/, "");
|
|
10244
10301
|
}
|
|
10245
|
-
return `
|
|
10302
|
+
return `https://${trimmed}`.replace(/\/+$/, "");
|
|
10246
10303
|
}
|
|
10247
10304
|
async connect(config2) {
|
|
10248
10305
|
this.baseUrl = this.normalizeBaseUrl(
|
|
@@ -10664,9 +10721,15 @@ var WebChatAdapter = class {
|
|
|
10664
10721
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
10665
10722
|
}
|
|
10666
10723
|
async handleChatRequest(req, res) {
|
|
10724
|
+
const MAX_BODY_SIZE = 1048576;
|
|
10667
10725
|
let body = "";
|
|
10668
10726
|
for await (const chunk of req) {
|
|
10669
10727
|
body += chunk;
|
|
10728
|
+
if (body.length > MAX_BODY_SIZE) {
|
|
10729
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
10730
|
+
res.end(JSON.stringify({ error: "Request body too large" }));
|
|
10731
|
+
return;
|
|
10732
|
+
}
|
|
10670
10733
|
}
|
|
10671
10734
|
let parsed;
|
|
10672
10735
|
try {
|
|
@@ -11146,12 +11209,12 @@ var SlackAdapter = class {
|
|
|
11146
11209
|
// src/gateway/adapters/imessage.ts
|
|
11147
11210
|
import { execFile } from "child_process";
|
|
11148
11211
|
import { promisify } from "util";
|
|
11149
|
-
import
|
|
11212
|
+
import os8 from "os";
|
|
11150
11213
|
import path18 from "path";
|
|
11151
11214
|
var execFileAsync = promisify(execFile);
|
|
11152
11215
|
var POLL_INTERVAL_MS = 5e3;
|
|
11153
11216
|
var MESSAGES_DB_PATH = path18.join(
|
|
11154
|
-
process.env.HOME ??
|
|
11217
|
+
process.env.HOME ?? os8.homedir(),
|
|
11155
11218
|
"Library/Messages/chat.db"
|
|
11156
11219
|
);
|
|
11157
11220
|
var IMessageAdapter = class {
|
|
@@ -11997,8 +12060,9 @@ async function ensureCRMContact(info) {
|
|
|
11997
12060
|
import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
|
|
11998
12061
|
import { randomUUID as randomUUID14 } from "crypto";
|
|
11999
12062
|
import path19 from "path";
|
|
12000
|
-
import
|
|
12001
|
-
var TRIGGERS_PATH = path19.join(
|
|
12063
|
+
import os9 from "os";
|
|
12064
|
+
var TRIGGERS_PATH = path19.join(os9.homedir(), ".exe-os", "triggers.json");
|
|
12065
|
+
var GRAPH_API_VERSION = "v21.0";
|
|
12002
12066
|
function substituteTemplate(template, record) {
|
|
12003
12067
|
return template.replace(
|
|
12004
12068
|
/\{\{(\w+(?:\.\w+)*)\}\}/g,
|
|
@@ -12076,7 +12140,7 @@ async function executeSendWhatsapp(params) {
|
|
|
12076
12140
|
const message = params.message ?? params.text;
|
|
12077
12141
|
if (!to || !message)
|
|
12078
12142
|
throw new Error("send_whatsapp requires 'to' and 'message' params");
|
|
12079
|
-
const url = `https://graph.facebook.com
|
|
12143
|
+
const url = `https://graph.facebook.com/${GRAPH_API_VERSION}/${account.phoneNumberId}/messages`;
|
|
12080
12144
|
const res = await fetch(url, {
|
|
12081
12145
|
method: "POST",
|
|
12082
12146
|
headers: {
|
|
@@ -12088,7 +12152,8 @@ async function executeSendWhatsapp(params) {
|
|
|
12088
12152
|
to,
|
|
12089
12153
|
type: "text",
|
|
12090
12154
|
text: { body: message }
|
|
12091
|
-
})
|
|
12155
|
+
}),
|
|
12156
|
+
signal: AbortSignal.timeout(3e4)
|
|
12092
12157
|
});
|
|
12093
12158
|
if (!res.ok) {
|
|
12094
12159
|
const errBody = await res.text();
|
package/dist/lib/cloud-sync.js
CHANGED
|
@@ -6,7 +6,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/lib/cloud-sync.ts
|
|
9
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync5, readdirSync, mkdirSync as mkdirSync2, appendFileSync } from "fs";
|
|
9
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync5, readdirSync, mkdirSync as mkdirSync2, appendFileSync, unlinkSync } from "fs";
|
|
10
10
|
import path5 from "path";
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
|
|
@@ -80,7 +80,7 @@ import { execSync } from "child_process";
|
|
|
80
80
|
import path2 from "path";
|
|
81
81
|
|
|
82
82
|
// src/lib/config.ts
|
|
83
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
83
|
+
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
84
84
|
import { readFileSync, existsSync, renameSync } from "fs";
|
|
85
85
|
import path from "path";
|
|
86
86
|
import os from "os";
|
|
@@ -187,15 +187,20 @@ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
|
|
|
187
187
|
await mkdir2(path2.dirname(employeesPath), { recursive: true });
|
|
188
188
|
await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
|
|
189
189
|
}
|
|
190
|
+
function findExeBin() {
|
|
191
|
+
try {
|
|
192
|
+
return execSync(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
190
197
|
function registerBinSymlinks(name) {
|
|
191
198
|
const created = [];
|
|
192
199
|
const skipped = [];
|
|
193
200
|
const errors = [];
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
} catch {
|
|
198
|
-
errors.push("Could not find 'exe' in PATH");
|
|
201
|
+
const exeBinPath = findExeBin();
|
|
202
|
+
if (!exeBinPath) {
|
|
203
|
+
errors.push("Could not find 'exe-os' in PATH");
|
|
199
204
|
return { created, skipped, errors };
|
|
200
205
|
}
|
|
201
206
|
const binDir = path2.dirname(exeBinPath);
|
|
@@ -232,6 +237,15 @@ var LICENSE_PATH = path3.join(EXE_AI_DIR, "license.key");
|
|
|
232
237
|
var CACHE_PATH = path3.join(EXE_AI_DIR, "license-cache.json");
|
|
233
238
|
var DEVICE_ID_PATH = path3.join(EXE_AI_DIR, "device-id");
|
|
234
239
|
var API_BASE = "https://askexe.com/cloud";
|
|
240
|
+
var RETRY_DELAY_MS = 500;
|
|
241
|
+
async function fetchRetry(url, init) {
|
|
242
|
+
try {
|
|
243
|
+
return await fetch(url, init);
|
|
244
|
+
} catch {
|
|
245
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
246
|
+
return fetch(url, { ...init, signal: AbortSignal.timeout(1e4) });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
235
249
|
var LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
236
250
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
|
|
237
251
|
4uj+UqeKCcvtgNHKmOK278HJaJcANe9xAeji8AFYu27q3WtzCi04pHudow==
|
|
@@ -323,7 +337,7 @@ function cacheResponse(token) {
|
|
|
323
337
|
async function validateLicense(apiKey, deviceId) {
|
|
324
338
|
const did = deviceId ?? loadDeviceId();
|
|
325
339
|
try {
|
|
326
|
-
const res = await
|
|
340
|
+
const res = await fetchRetry(`${API_BASE}/auth/activate`, {
|
|
327
341
|
method: "POST",
|
|
328
342
|
headers: { "Content-Type": "application/json" },
|
|
329
343
|
body: JSON.stringify({ apiKey, deviceId: did }),
|
|
@@ -418,16 +432,50 @@ function logError(msg) {
|
|
|
418
432
|
}
|
|
419
433
|
var LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
|
|
420
434
|
var FETCH_TIMEOUT_MS = 3e4;
|
|
435
|
+
var PUSH_BATCH_SIZE = 5e3;
|
|
436
|
+
var ROSTER_LOCK_PATH = path5.join(EXE_AI_DIR, "roster-merge.lock");
|
|
437
|
+
var LOCK_STALE_MS = 3e4;
|
|
438
|
+
async function withRosterLock(fn) {
|
|
439
|
+
if (existsSync5(ROSTER_LOCK_PATH)) {
|
|
440
|
+
try {
|
|
441
|
+
const ts = parseInt(readFileSync5(ROSTER_LOCK_PATH, "utf-8"), 10);
|
|
442
|
+
if (Date.now() - ts < LOCK_STALE_MS) {
|
|
443
|
+
throw new Error("Roster merge already in progress \u2014 another sync is running");
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
if (err instanceof Error && err.message.includes("already in progress")) throw err;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
writeFileSync2(ROSTER_LOCK_PATH, String(Date.now()));
|
|
450
|
+
try {
|
|
451
|
+
return await fn();
|
|
452
|
+
} finally {
|
|
453
|
+
try {
|
|
454
|
+
unlinkSync(ROSTER_LOCK_PATH);
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
421
459
|
async function fetchWithRetry(url, init) {
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
460
|
+
const MAX_RETRIES = 3;
|
|
461
|
+
const BASE_DELAY_MS = 200;
|
|
462
|
+
let lastError;
|
|
463
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
464
|
+
try {
|
|
465
|
+
const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
466
|
+
const resp = await fetch(url, { ...init, signal });
|
|
467
|
+
if (resp.status >= 500 && attempt < MAX_RETRIES) {
|
|
468
|
+
await new Promise((r) => setTimeout(r, BASE_DELAY_MS * Math.pow(2, attempt)));
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
return resp;
|
|
472
|
+
} catch (err) {
|
|
473
|
+
lastError = err;
|
|
474
|
+
if (attempt === MAX_RETRIES) throw err;
|
|
475
|
+
await new Promise((r) => setTimeout(r, BASE_DELAY_MS * Math.pow(2, attempt)));
|
|
476
|
+
}
|
|
429
477
|
}
|
|
430
|
-
|
|
478
|
+
throw lastError;
|
|
431
479
|
}
|
|
432
480
|
function assertSecureEndpoint(endpoint) {
|
|
433
481
|
if (endpoint.startsWith("https://")) return;
|
|
@@ -455,10 +503,15 @@ async function cloudPush(records, maxVersion, config) {
|
|
|
455
503
|
headers: {
|
|
456
504
|
Authorization: `Bearer ${config.apiKey}`,
|
|
457
505
|
"Content-Type": "application/json",
|
|
458
|
-
"X-Device-Id": loadDeviceId()
|
|
506
|
+
"X-Device-Id": loadDeviceId(),
|
|
507
|
+
"X-Expected-Version": String(maxVersion)
|
|
459
508
|
},
|
|
460
509
|
body: JSON.stringify({ version: maxVersion, blob })
|
|
461
510
|
});
|
|
511
|
+
if (resp.status === 409) {
|
|
512
|
+
logError("[cloud-sync] PUSH VERSION CONFLICT \u2014 re-pull required before next push");
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
462
515
|
return resp.ok;
|
|
463
516
|
} catch (err) {
|
|
464
517
|
logError(`[cloud-sync] PUSH FAILED: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -545,18 +598,21 @@ async function cloudSync(config) {
|
|
|
545
598
|
"SELECT value FROM sync_meta WHERE key = 'last_cloud_push_version'"
|
|
546
599
|
);
|
|
547
600
|
const lastPushVersion = pushMeta.rows.length > 0 ? Number(pushMeta.rows[0].value) : 0;
|
|
548
|
-
const recordsResult = await client.execute({
|
|
549
|
-
sql: `SELECT id, agent_id, agent_role, session_id, timestamp,
|
|
550
|
-
tool_name, project_name, has_error, raw_text, version,
|
|
551
|
-
author_device_id, scope
|
|
552
|
-
FROM memories
|
|
553
|
-
WHERE version > ?
|
|
554
|
-
AND (scope IS NULL OR scope != 'personal')
|
|
555
|
-
ORDER BY version ASC`,
|
|
556
|
-
args: [lastPushVersion]
|
|
557
|
-
});
|
|
558
601
|
let pushed = 0;
|
|
559
|
-
|
|
602
|
+
let batchCursor = lastPushVersion;
|
|
603
|
+
while (true) {
|
|
604
|
+
const recordsResult = await client.execute({
|
|
605
|
+
sql: `SELECT id, agent_id, agent_role, session_id, timestamp,
|
|
606
|
+
tool_name, project_name, has_error, raw_text, version,
|
|
607
|
+
author_device_id, scope
|
|
608
|
+
FROM memories
|
|
609
|
+
WHERE version > ?
|
|
610
|
+
AND (scope IS NULL OR scope != 'personal')
|
|
611
|
+
ORDER BY version ASC
|
|
612
|
+
LIMIT ?`,
|
|
613
|
+
args: [batchCursor, PUSH_BATCH_SIZE]
|
|
614
|
+
});
|
|
615
|
+
if (recordsResult.rows.length === 0) break;
|
|
560
616
|
const records = recordsResult.rows.map((row) => ({
|
|
561
617
|
id: row.id,
|
|
562
618
|
agent_id: row.agent_id,
|
|
@@ -573,13 +629,14 @@ async function cloudSync(config) {
|
|
|
573
629
|
}));
|
|
574
630
|
const maxVersion = Number(records[records.length - 1].version);
|
|
575
631
|
const pushOk = await cloudPush(records, maxVersion, config);
|
|
576
|
-
if (pushOk)
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
632
|
+
if (!pushOk) break;
|
|
633
|
+
await client.execute({
|
|
634
|
+
sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_push_version', ?)",
|
|
635
|
+
args: [String(maxVersion)]
|
|
636
|
+
});
|
|
637
|
+
pushed += records.length;
|
|
638
|
+
batchCursor = maxVersion;
|
|
639
|
+
if (recordsResult.rows.length < PUSH_BATCH_SIZE) break;
|
|
583
640
|
}
|
|
584
641
|
try {
|
|
585
642
|
await cloudPushRoster(config);
|
|
@@ -661,6 +718,28 @@ async function cloudSync(config) {
|
|
|
661
718
|
documents: documentsResult
|
|
662
719
|
};
|
|
663
720
|
}
|
|
721
|
+
var ROSTER_DELETIONS_PATH = path5.join(EXE_AI_DIR, "roster-deletions.json");
|
|
722
|
+
function recordRosterDeletion(name) {
|
|
723
|
+
let deletions = [];
|
|
724
|
+
try {
|
|
725
|
+
if (existsSync5(ROSTER_DELETIONS_PATH)) {
|
|
726
|
+
deletions = JSON.parse(readFileSync5(ROSTER_DELETIONS_PATH, "utf-8"));
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
730
|
+
if (!deletions.includes(name)) deletions.push(name);
|
|
731
|
+
writeFileSync2(ROSTER_DELETIONS_PATH, JSON.stringify(deletions));
|
|
732
|
+
}
|
|
733
|
+
function consumeRosterDeletions() {
|
|
734
|
+
try {
|
|
735
|
+
if (!existsSync5(ROSTER_DELETIONS_PATH)) return [];
|
|
736
|
+
const deletions = JSON.parse(readFileSync5(ROSTER_DELETIONS_PATH, "utf-8"));
|
|
737
|
+
writeFileSync2(ROSTER_DELETIONS_PATH, "[]");
|
|
738
|
+
return deletions;
|
|
739
|
+
} catch {
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
}
|
|
664
743
|
function buildRosterBlob(paths) {
|
|
665
744
|
const rosterPath = paths?.rosterPath ?? path5.join(EXE_AI_DIR, "exe-employees.json");
|
|
666
745
|
const identityDir = paths?.identityDir ?? path5.join(EXE_AI_DIR, "identity");
|
|
@@ -688,9 +767,10 @@ function buildRosterBlob(paths) {
|
|
|
688
767
|
} catch {
|
|
689
768
|
}
|
|
690
769
|
}
|
|
691
|
-
const
|
|
770
|
+
const deletedNames = consumeRosterDeletions();
|
|
771
|
+
const content = JSON.stringify({ roster, identities, config, deletedNames });
|
|
692
772
|
const hash = Buffer.from(content).length;
|
|
693
|
-
return { roster, identities, config, version: hash };
|
|
773
|
+
return { roster, identities, config, deletedNames, version: hash };
|
|
694
774
|
}
|
|
695
775
|
async function cloudPushRoster(config) {
|
|
696
776
|
assertSecureEndpoint(config.endpoint);
|
|
@@ -773,38 +853,50 @@ function mergeConfig(remoteConfig, configPath) {
|
|
|
773
853
|
writeFileSync2(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
774
854
|
}
|
|
775
855
|
async function mergeRosterFromRemote(remote, paths) {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
856
|
+
return withRosterLock(async () => {
|
|
857
|
+
const rosterPath = paths?.rosterPath ?? void 0;
|
|
858
|
+
const identityDir = paths?.identityDir ?? path5.join(EXE_AI_DIR, "identity");
|
|
859
|
+
const localEmployees = await loadEmployees(rosterPath);
|
|
860
|
+
const localNames = new Set(localEmployees.map((e) => e.name));
|
|
861
|
+
let added = 0;
|
|
862
|
+
for (const remoteEmp of remote.roster) {
|
|
863
|
+
if (localNames.has(remoteEmp.name)) continue;
|
|
864
|
+
localEmployees.push(remoteEmp);
|
|
865
|
+
localNames.add(remoteEmp.name);
|
|
866
|
+
added++;
|
|
867
|
+
if (remote.identities[`${remoteEmp.name}.md`]) {
|
|
868
|
+
if (!existsSync5(identityDir)) mkdirSync2(identityDir, { recursive: true });
|
|
869
|
+
const idPath = path5.join(identityDir, `${remoteEmp.name}.md`);
|
|
870
|
+
if (!existsSync5(idPath)) {
|
|
871
|
+
writeFileSync2(idPath, remote.identities[`${remoteEmp.name}.md`], "utf-8");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
try {
|
|
875
|
+
registerBinSymlinks(remoteEmp.name);
|
|
876
|
+
} catch {
|
|
791
877
|
}
|
|
792
878
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
879
|
+
let removed = 0;
|
|
880
|
+
if (remote.deletedNames && remote.deletedNames.length > 0) {
|
|
881
|
+
const toRemove = new Set(remote.deletedNames);
|
|
882
|
+
const filtered = localEmployees.filter((e) => !toRemove.has(e.name));
|
|
883
|
+
removed = localEmployees.length - filtered.length;
|
|
884
|
+
if (removed > 0) {
|
|
885
|
+
localEmployees.length = 0;
|
|
886
|
+
localEmployees.push(...filtered);
|
|
887
|
+
}
|
|
796
888
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
await saveEmployees(localEmployees, rosterPath);
|
|
800
|
-
}
|
|
801
|
-
if (remote.config && Object.keys(remote.config).length > 0) {
|
|
802
|
-
try {
|
|
803
|
-
mergeConfig(remote.config, paths?.configPath);
|
|
804
|
-
} catch {
|
|
889
|
+
if (added > 0 || removed > 0) {
|
|
890
|
+
await saveEmployees(localEmployees, rosterPath);
|
|
805
891
|
}
|
|
806
|
-
|
|
807
|
-
|
|
892
|
+
if (remote.config && Object.keys(remote.config).length > 0) {
|
|
893
|
+
try {
|
|
894
|
+
mergeConfig(remote.config, paths?.configPath);
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return { added };
|
|
899
|
+
});
|
|
808
900
|
}
|
|
809
901
|
async function cloudPushBlob(route, data, metaKey, config) {
|
|
810
902
|
if (data.length === 0) return { ok: true };
|
|
@@ -872,7 +964,7 @@ async function cloudPullBlob(route, config) {
|
|
|
872
964
|
}
|
|
873
965
|
async function cloudPushBehaviors(config) {
|
|
874
966
|
const client = getClient();
|
|
875
|
-
const result = await client.execute("SELECT * FROM behaviors");
|
|
967
|
+
const result = await client.execute("SELECT * FROM behaviors LIMIT 10000");
|
|
876
968
|
const rows = result.rows;
|
|
877
969
|
const { ok } = await cloudPushBlob(
|
|
878
970
|
"/sync/push-behaviors",
|
|
@@ -920,13 +1012,13 @@ async function cloudPullBehaviors(config) {
|
|
|
920
1012
|
async function cloudPushGraphRAG(config) {
|
|
921
1013
|
const client = getClient();
|
|
922
1014
|
const [entities, relationships, aliases, entityMems, relMems, hyperedges, hyperedgeNodes] = await Promise.all([
|
|
923
|
-
client.execute("SELECT * FROM entities"),
|
|
924
|
-
client.execute("SELECT * FROM relationships"),
|
|
925
|
-
client.execute("SELECT * FROM entity_aliases"),
|
|
926
|
-
client.execute("SELECT * FROM entity_memories"),
|
|
927
|
-
client.execute("SELECT * FROM relationship_memories"),
|
|
928
|
-
client.execute("SELECT * FROM hyperedges"),
|
|
929
|
-
client.execute("SELECT * FROM hyperedge_nodes")
|
|
1015
|
+
client.execute("SELECT * FROM entities LIMIT 50000"),
|
|
1016
|
+
client.execute("SELECT * FROM relationships LIMIT 50000"),
|
|
1017
|
+
client.execute("SELECT * FROM entity_aliases LIMIT 50000"),
|
|
1018
|
+
client.execute("SELECT * FROM entity_memories LIMIT 50000"),
|
|
1019
|
+
client.execute("SELECT * FROM relationship_memories LIMIT 50000"),
|
|
1020
|
+
client.execute("SELECT * FROM hyperedges LIMIT 50000"),
|
|
1021
|
+
client.execute("SELECT * FROM hyperedge_nodes LIMIT 50000")
|
|
930
1022
|
]);
|
|
931
1023
|
const blob = {
|
|
932
1024
|
entities: entities.rows,
|
|
@@ -1028,7 +1120,7 @@ async function cloudPullGraphRAG(config) {
|
|
|
1028
1120
|
}
|
|
1029
1121
|
async function cloudPushTasks(config) {
|
|
1030
1122
|
const client = getClient();
|
|
1031
|
-
const result = await client.execute("SELECT * FROM tasks");
|
|
1123
|
+
const result = await client.execute("SELECT * FROM tasks LIMIT 10000");
|
|
1032
1124
|
const rows = result.rows;
|
|
1033
1125
|
const { ok } = await cloudPushBlob(
|
|
1034
1126
|
"/sync/push-tasks",
|
|
@@ -1074,7 +1166,7 @@ async function cloudPullTasks(config) {
|
|
|
1074
1166
|
}
|
|
1075
1167
|
async function cloudPushConversations(config) {
|
|
1076
1168
|
const client = getClient();
|
|
1077
|
-
const result = await client.execute("SELECT * FROM conversations");
|
|
1169
|
+
const result = await client.execute("SELECT * FROM conversations LIMIT 50000");
|
|
1078
1170
|
const rows = result.rows;
|
|
1079
1171
|
const { ok } = await cloudPushBlob(
|
|
1080
1172
|
"/sync/push-conversations",
|
|
@@ -1124,8 +1216,8 @@ async function cloudPullConversations(config) {
|
|
|
1124
1216
|
async function cloudPushDocuments(config) {
|
|
1125
1217
|
const client = getClient();
|
|
1126
1218
|
const [workspaces, documents] = await Promise.all([
|
|
1127
|
-
client.execute("SELECT * FROM workspaces"),
|
|
1128
|
-
client.execute("SELECT * FROM documents")
|
|
1219
|
+
client.execute("SELECT * FROM workspaces LIMIT 1000"),
|
|
1220
|
+
client.execute("SELECT * FROM documents LIMIT 10000")
|
|
1129
1221
|
]);
|
|
1130
1222
|
const blob = {
|
|
1131
1223
|
workspaces: workspaces.rows,
|
|
@@ -1179,6 +1271,7 @@ async function cloudPullDocuments(config) {
|
|
|
1179
1271
|
return { pulled };
|
|
1180
1272
|
}
|
|
1181
1273
|
export {
|
|
1274
|
+
assertSecureEndpoint,
|
|
1182
1275
|
buildRosterBlob,
|
|
1183
1276
|
cloudPull,
|
|
1184
1277
|
cloudPullBehaviors,
|
|
@@ -1198,5 +1291,6 @@ export {
|
|
|
1198
1291
|
cloudPushTasks,
|
|
1199
1292
|
cloudSync,
|
|
1200
1293
|
mergeConfig,
|
|
1201
|
-
mergeRosterFromRemote
|
|
1294
|
+
mergeRosterFromRemote,
|
|
1295
|
+
recordRosterDeletion
|
|
1202
1296
|
};
|
package/dist/lib/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/lib/config.ts
|
|
2
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
2
|
+
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
3
3
|
import { readFileSync, existsSync, renameSync } from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import os from "os";
|
|
@@ -204,6 +204,9 @@ async function saveConfig(config) {
|
|
|
204
204
|
await mkdir(dir, { recursive: true });
|
|
205
205
|
const configPath = path.join(dir, "config.json");
|
|
206
206
|
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
207
|
+
if (config.cloud?.apiKey) {
|
|
208
|
+
await chmod(configPath, 384);
|
|
209
|
+
}
|
|
207
210
|
}
|
|
208
211
|
async function loadConfigFrom(configPath) {
|
|
209
212
|
const raw = await readFile(configPath, "utf-8");
|