@askexenow/exe-os 0.9.61 → 0.9.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/deploy/stack-manifests/v0.9.json +62 -13
  2. package/dist/bin/backfill-conversations.js +210 -2
  3. package/dist/bin/backfill-responses.js +210 -2
  4. package/dist/bin/backfill-vectors.js +47 -2
  5. package/dist/bin/cleanup-stale-review-tasks.js +210 -2
  6. package/dist/bin/cli.js +488 -25
  7. package/dist/bin/exe-agent-config.js +2 -2
  8. package/dist/bin/exe-agent.js +2 -2
  9. package/dist/bin/exe-assign.js +210 -2
  10. package/dist/bin/exe-boot.js +47 -2
  11. package/dist/bin/exe-call.js +2 -2
  12. package/dist/bin/exe-cloud.js +2 -2
  13. package/dist/bin/exe-dispatch.js +210 -2
  14. package/dist/bin/exe-doctor.js +2691 -1212
  15. package/dist/bin/exe-export-behaviors.js +210 -2
  16. package/dist/bin/exe-forget.js +264 -2
  17. package/dist/bin/exe-gateway.js +210 -2
  18. package/dist/bin/exe-heartbeat.js +212 -4
  19. package/dist/bin/exe-kill.js +210 -2
  20. package/dist/bin/exe-launch-agent.js +210 -2
  21. package/dist/bin/exe-link.js +47 -2
  22. package/dist/bin/exe-new-employee.js +113 -3
  23. package/dist/bin/exe-pending-messages.js +210 -2
  24. package/dist/bin/exe-pending-notifications.js +210 -2
  25. package/dist/bin/exe-pending-reviews.js +210 -2
  26. package/dist/bin/exe-rename.js +210 -2
  27. package/dist/bin/exe-review.js +210 -2
  28. package/dist/bin/exe-search.js +234 -3
  29. package/dist/bin/exe-session-cleanup.js +210 -2
  30. package/dist/bin/exe-settings.js +2 -2
  31. package/dist/bin/exe-start-codex.js +322 -4
  32. package/dist/bin/exe-start-opencode.js +336 -4
  33. package/dist/bin/exe-status.js +210 -2
  34. package/dist/bin/exe-team.js +210 -2
  35. package/dist/bin/git-sweep.js +210 -2
  36. package/dist/bin/graph-backfill.js +210 -2
  37. package/dist/bin/graph-export.js +210 -2
  38. package/dist/bin/install.js +113 -3
  39. package/dist/bin/intercom-check.js +210 -2
  40. package/dist/bin/scan-tasks.js +210 -2
  41. package/dist/bin/setup.js +47 -2
  42. package/dist/bin/shard-migrate.js +210 -2
  43. package/dist/bin/stack-update.js +158 -13
  44. package/dist/bin/update.js +2 -2
  45. package/dist/gateway/index.js +210 -2
  46. package/dist/hooks/bug-report-worker.js +321 -44
  47. package/dist/hooks/codex-stop-task-finalizer.js +210 -2
  48. package/dist/hooks/commit-complete.js +210 -2
  49. package/dist/hooks/error-recall.js +234 -3
  50. package/dist/hooks/exe-heartbeat-hook.js +2 -2
  51. package/dist/hooks/ingest-worker.js +2 -2
  52. package/dist/hooks/ingest.js +210 -2
  53. package/dist/hooks/instructions-loaded.js +210 -2
  54. package/dist/hooks/notification.js +210 -2
  55. package/dist/hooks/post-compact.js +210 -2
  56. package/dist/hooks/post-tool-combined.js +234 -3
  57. package/dist/hooks/pre-compact.js +446 -111
  58. package/dist/hooks/pre-tool-use.js +210 -2
  59. package/dist/hooks/prompt-submit.js +234 -3
  60. package/dist/hooks/session-end.js +347 -124
  61. package/dist/hooks/session-start.js +236 -5
  62. package/dist/hooks/stop.js +210 -2
  63. package/dist/hooks/subagent-stop.js +210 -2
  64. package/dist/hooks/summary-worker.js +171 -74
  65. package/dist/index.js +210 -2
  66. package/dist/lib/agent-config.js +2 -2
  67. package/dist/lib/cloud-sync.js +47 -2
  68. package/dist/lib/config.js +2 -2
  69. package/dist/lib/consolidation.js +2 -2
  70. package/dist/lib/database.js +47 -2
  71. package/dist/lib/db-daemon-client.js +2 -2
  72. package/dist/lib/db.js +47 -2
  73. package/dist/lib/device-registry.js +47 -2
  74. package/dist/lib/embedder.js +2 -2
  75. package/dist/lib/employee-templates.js +2 -2
  76. package/dist/lib/employees.js +2 -2
  77. package/dist/lib/exe-daemon-client.js +2 -2
  78. package/dist/lib/exe-daemon.js +592 -10
  79. package/dist/lib/hybrid-search.js +234 -3
  80. package/dist/lib/identity.js +2 -2
  81. package/dist/lib/license.js +2 -2
  82. package/dist/lib/messaging.js +2 -2
  83. package/dist/lib/reminders.js +2 -2
  84. package/dist/lib/schedules.js +47 -2
  85. package/dist/lib/skill-learning.js +2 -2
  86. package/dist/lib/store.js +210 -2
  87. package/dist/lib/task-router.js +2 -2
  88. package/dist/lib/tasks.js +2 -2
  89. package/dist/lib/tmux-routing.js +2 -2
  90. package/dist/lib/token-spend.js +2 -2
  91. package/dist/mcp/server.js +608 -10
  92. package/dist/mcp/tools/complete-reminder.js +2 -2
  93. package/dist/mcp/tools/create-reminder.js +2 -2
  94. package/dist/mcp/tools/create-task.js +2 -2
  95. package/dist/mcp/tools/deactivate-behavior.js +2 -2
  96. package/dist/mcp/tools/list-reminders.js +2 -2
  97. package/dist/mcp/tools/list-tasks.js +2 -2
  98. package/dist/mcp/tools/send-message.js +2 -2
  99. package/dist/mcp/tools/update-task.js +2 -2
  100. package/dist/runtime/index.js +210 -2
  101. package/dist/tui/App.js +210 -2
  102. package/package.json +3 -2
  103. package/stack.release.json +23 -7
@@ -256,8 +256,8 @@ var init_config = __esm({
256
256
  rerankerAutoTrigger: {
257
257
  enabled: true,
258
258
  broadQueryMinCardinality: 5e4,
259
- fetchTopK: 150,
260
- returnTopK: 5
259
+ fetchTopK: 200,
260
+ returnTopK: 20
261
261
  }
262
262
  },
263
263
  graphRagEnabled: true,
@@ -2506,6 +2506,51 @@ async function ensureSchema() {
2506
2506
  VALUES (new.rowid, new.content_text, new.sender_name, new.agent_response);
2507
2507
  END;
2508
2508
  `);
2509
+ await client.executeMultiple(`
2510
+ CREATE TABLE IF NOT EXISTS memory_cards (
2511
+ id TEXT PRIMARY KEY,
2512
+ memory_id TEXT NOT NULL,
2513
+ agent_id TEXT NOT NULL,
2514
+ session_id TEXT NOT NULL,
2515
+ project_name TEXT,
2516
+ timestamp TEXT NOT NULL,
2517
+ card_type TEXT NOT NULL,
2518
+ subject TEXT,
2519
+ predicate TEXT,
2520
+ object TEXT,
2521
+ content TEXT NOT NULL,
2522
+ source_ref TEXT,
2523
+ confidence REAL DEFAULT 0.6,
2524
+ active INTEGER DEFAULT 1,
2525
+ created_at TEXT NOT NULL
2526
+ );
2527
+
2528
+ CREATE INDEX IF NOT EXISTS idx_memory_cards_agent
2529
+ ON memory_cards(agent_id, active, timestamp);
2530
+
2531
+ CREATE INDEX IF NOT EXISTS idx_memory_cards_memory
2532
+ ON memory_cards(memory_id);
2533
+
2534
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_cards_fts
2535
+ USING fts5(content, subject, predicate, object, content='memory_cards', content_rowid='rowid');
2536
+
2537
+ CREATE TRIGGER IF NOT EXISTS memory_cards_fts_ai AFTER INSERT ON memory_cards BEGIN
2538
+ INSERT INTO memory_cards_fts(rowid, content, subject, predicate, object)
2539
+ VALUES (new.rowid, new.content, new.subject, new.predicate, new.object);
2540
+ END;
2541
+
2542
+ CREATE TRIGGER IF NOT EXISTS memory_cards_fts_ad AFTER DELETE ON memory_cards BEGIN
2543
+ INSERT INTO memory_cards_fts(memory_cards_fts, rowid, content, subject, predicate, object)
2544
+ VALUES('delete', old.rowid, old.content, old.subject, old.predicate, old.object);
2545
+ END;
2546
+
2547
+ CREATE TRIGGER IF NOT EXISTS memory_cards_fts_au AFTER UPDATE ON memory_cards BEGIN
2548
+ INSERT INTO memory_cards_fts(memory_cards_fts, rowid, content, subject, predicate, object)
2549
+ VALUES('delete', old.rowid, old.content, old.subject, old.predicate, old.object);
2550
+ INSERT INTO memory_cards_fts(rowid, content, subject, predicate, object)
2551
+ VALUES (new.rowid, new.content, new.subject, new.predicate, new.object);
2552
+ END;
2553
+ `);
2509
2554
  try {
2510
2555
  await client.execute({
2511
2556
  sql: `ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3`,
@@ -3323,6 +3368,164 @@ ${p.content}`).join("\n\n");
3323
3368
  }
3324
3369
  });
3325
3370
 
3371
+ // src/lib/memory-cards.ts
3372
+ var memory_cards_exports = {};
3373
+ __export(memory_cards_exports, {
3374
+ extractMemoryCards: () => extractMemoryCards,
3375
+ insertMemoryCardsForBatch: () => insertMemoryCardsForBatch,
3376
+ searchMemoryCards: () => searchMemoryCards
3377
+ });
3378
+ import { createHash as createHash2 } from "crypto";
3379
+ function stableId(memoryId, type, content) {
3380
+ return createHash2("sha256").update(`${memoryId}:${type}:${content}`).digest("hex").slice(0, 32);
3381
+ }
3382
+ function cleanText(text) {
3383
+ return text.replace(/```[\s\S]*?```/g, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
3384
+ }
3385
+ function splitSentences(text) {
3386
+ return cleanText(text).split(/(?<=[.!?])\s+|\n+/).map((s) => s.trim()).filter((s) => s.length >= 24 && s.length <= MAX_SENTENCE_CHARS);
3387
+ }
3388
+ function inferCardType(sentence, toolName) {
3389
+ const lower = sentence.toLowerCase();
3390
+ if (toolName === "store_decision" || /\b(decided|decision|adr|approved|rejected)\b/.test(lower)) return "decision";
3391
+ if (/\b(prefers|preference|likes|dislikes|wants|doesn't want|does not want)\b/.test(lower)) return "preference";
3392
+ if (/\b(changed|updated|replaced|now|no longer|instead|supersedes)\b/.test(lower)) return "belief_update";
3393
+ if (toolName && ["Read", "Write", "Edit", "Bash"].includes(toolName)) return "code";
3394
+ if (/\b(meeting|deadline|shipped|launched|completed|failed|blocked|assigned|created)\b/.test(lower)) return "event";
3395
+ return "fact";
3396
+ }
3397
+ function extractSubject(sentence, agentId) {
3398
+ const explicit = sentence.match(/\b([A-Z][a-zA-Z0-9_-]{2,}(?:\s+[A-Z][a-zA-Z0-9_-]{2,})?)\b/);
3399
+ return explicit?.[1] ?? agentId;
3400
+ }
3401
+ function predicateFor(type) {
3402
+ switch (type) {
3403
+ case "preference":
3404
+ return "prefers";
3405
+ case "belief_update":
3406
+ return "updated";
3407
+ case "decision":
3408
+ return "decided";
3409
+ case "event":
3410
+ return "happened";
3411
+ case "code":
3412
+ return "implemented";
3413
+ default:
3414
+ return "states";
3415
+ }
3416
+ }
3417
+ function extractMemoryCards(row) {
3418
+ const sentences = splitSentences(row.raw_text);
3419
+ const cards = [];
3420
+ for (const sentence of sentences) {
3421
+ const type = inferCardType(sentence, row.tool_name);
3422
+ const subject = extractSubject(sentence, row.agent_id);
3423
+ const content = sentence.length > MAX_SENTENCE_CHARS ? `${sentence.slice(0, MAX_SENTENCE_CHARS - 1)}\u2026` : sentence;
3424
+ cards.push({
3425
+ id: stableId(row.id, type, content),
3426
+ memory_id: row.id,
3427
+ agent_id: row.agent_id,
3428
+ session_id: row.session_id,
3429
+ project_name: row.project_name ?? null,
3430
+ timestamp: row.timestamp,
3431
+ card_type: type,
3432
+ subject,
3433
+ predicate: predicateFor(type),
3434
+ object: content,
3435
+ content,
3436
+ source_ref: row.id,
3437
+ confidence: type === "fact" ? 0.55 : 0.65
3438
+ });
3439
+ if (cards.length >= MAX_CARDS_PER_MEMORY) break;
3440
+ }
3441
+ return cards;
3442
+ }
3443
+ async function insertMemoryCardsForBatch(rows) {
3444
+ const cards = rows.flatMap(extractMemoryCards);
3445
+ if (cards.length === 0) return 0;
3446
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3447
+ const client = getClient();
3448
+ const stmts = cards.map((card) => ({
3449
+ sql: `INSERT OR IGNORE INTO memory_cards
3450
+ (id, memory_id, agent_id, session_id, project_name, timestamp, card_type,
3451
+ subject, predicate, object, content, source_ref, confidence, active, created_at)
3452
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
3453
+ args: [
3454
+ card.id,
3455
+ card.memory_id,
3456
+ card.agent_id,
3457
+ card.session_id,
3458
+ card.project_name,
3459
+ card.timestamp,
3460
+ card.card_type,
3461
+ card.subject,
3462
+ card.predicate,
3463
+ card.object,
3464
+ card.content,
3465
+ card.source_ref,
3466
+ card.confidence,
3467
+ now
3468
+ ]
3469
+ }));
3470
+ await client.batch(stmts, "write");
3471
+ return cards.length;
3472
+ }
3473
+ function buildMatchExpr(queryText) {
3474
+ const terms = queryText.toLowerCase().split(/\s+/).filter((t) => t.length >= 3).map((t) => t.replace(/[^a-z0-9_]/g, "")).filter((t) => t.length >= 3).slice(0, 12);
3475
+ if (terms.length === 0) return null;
3476
+ return terms.map((t) => `${t}*`).join(terms.length >= 3 ? " AND " : " OR ");
3477
+ }
3478
+ async function searchMemoryCards(queryText, agentId, options) {
3479
+ const limit = options?.limit ?? 10;
3480
+ const matchExpr = buildMatchExpr(queryText);
3481
+ if (!matchExpr) return [];
3482
+ let sql = `SELECT c.id, c.memory_id, c.agent_id, c.session_id, c.project_name,
3483
+ c.timestamp, c.card_type, c.content, c.source_ref, c.confidence
3484
+ FROM memory_cards c
3485
+ JOIN memory_cards_fts fts ON c.rowid = fts.rowid
3486
+ WHERE memory_cards_fts MATCH ?
3487
+ AND c.agent_id = ?
3488
+ AND COALESCE(c.active, 1) = 1`;
3489
+ const args = [matchExpr, agentId];
3490
+ if (options?.projectName) {
3491
+ sql += ` AND c.project_name = ?`;
3492
+ args.push(options.projectName);
3493
+ }
3494
+ if (options?.since) {
3495
+ sql += ` AND c.timestamp >= ?`;
3496
+ args.push(options.since);
3497
+ }
3498
+ sql += ` ORDER BY rank LIMIT ?`;
3499
+ args.push(limit);
3500
+ const result = await getClient().execute({ sql, args });
3501
+ return result.rows.map((row) => ({
3502
+ id: `card:${String(row.id)}`,
3503
+ agent_id: String(row.agent_id),
3504
+ agent_role: "memory_card",
3505
+ session_id: String(row.session_id),
3506
+ timestamp: String(row.timestamp),
3507
+ tool_name: `memory_card:${String(row.card_type)}`,
3508
+ project_name: row.project_name == null ? "" : String(row.project_name),
3509
+ has_error: false,
3510
+ raw_text: `[${String(row.card_type)}] ${String(row.content)}
3511
+ Source memory: ${String(row.source_ref ?? row.memory_id)}`,
3512
+ vector: [],
3513
+ importance: 6,
3514
+ status: "active",
3515
+ confidence: Number(row.confidence ?? 0.6),
3516
+ last_accessed: String(row.timestamp)
3517
+ }));
3518
+ }
3519
+ var MAX_CARDS_PER_MEMORY, MAX_SENTENCE_CHARS;
3520
+ var init_memory_cards = __esm({
3521
+ "src/lib/memory-cards.ts"() {
3522
+ "use strict";
3523
+ init_database();
3524
+ MAX_CARDS_PER_MEMORY = 6;
3525
+ MAX_SENTENCE_CHARS = 360;
3526
+ }
3527
+ });
3528
+
3326
3529
  // src/lib/store.ts
3327
3530
  init_memory();
3328
3531
  init_database();
@@ -3877,6 +4080,11 @@ async function flushBatch() {
3877
4080
  const globalClient = getClient();
3878
4081
  const globalStmts = batch.map(buildStmt);
3879
4082
  await globalClient.batch(globalStmts, "write");
4083
+ try {
4084
+ const { insertMemoryCardsForBatch: insertMemoryCardsForBatch2 } = await Promise.resolve().then(() => (init_memory_cards(), memory_cards_exports));
4085
+ await insertMemoryCardsForBatch2(batch);
4086
+ } catch {
4087
+ }
3880
4088
  schedulePostWriteMemoryHygiene(batch.map((row) => row.id));
3881
4089
  _pendingRecords.splice(0, batch.length);
3882
4090
  try {
@@ -20,12 +20,64 @@ function isMainModule(importMetaUrl) {
20
20
 
21
21
  // src/lib/stack-update.ts
22
22
  import { execFileSync } from "child_process";
23
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
23
+ import { createVerify, verify as verifySignature } from "crypto";
24
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "fs";
24
25
  import http from "http";
25
26
  import https from "https";
26
27
  import path from "path";
27
- function parseStackManifest(raw) {
28
- const parsed = JSON.parse(raw);
28
+ function isSignedEnvelope(value) {
29
+ return !!value && typeof value === "object" && "manifest" in value && "signature" in value;
30
+ }
31
+ function canonicalizeStackManifest(manifest) {
32
+ const clone = JSON.parse(JSON.stringify(manifest));
33
+ delete clone.signature;
34
+ return stableJson(clone);
35
+ }
36
+ function verifyStackManifestSignature(manifest, publicKeyPem) {
37
+ const signature = manifest.signature;
38
+ if (!signature) throw new Error("Stack manifest signature required but missing");
39
+ const payload = Buffer.from(canonicalizeStackManifest(manifest));
40
+ const sig = Buffer.from(signature.signature, "base64");
41
+ let ok = false;
42
+ if (signature.alg === "ed25519") {
43
+ ok = verifySignature(null, payload, publicKeyPem, sig);
44
+ } else if (signature.alg === "rsa-sha256") {
45
+ const verifier = createVerify("RSA-SHA256");
46
+ verifier.update(payload);
47
+ verifier.end();
48
+ ok = verifier.verify(publicKeyPem, sig);
49
+ } else {
50
+ throw new Error(`Unsupported stack manifest signature alg: ${signature.alg}`);
51
+ }
52
+ if (!ok) throw new Error("Stack manifest signature verification failed");
53
+ }
54
+ function stableJson(value) {
55
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
56
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
57
+ const obj = value;
58
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableJson(obj[key])}`).join(",")}}`;
59
+ }
60
+ function findLatestBackupEnvFile(envFile) {
61
+ const backupDir = path.join(path.dirname(envFile), ".exe-stack-backups");
62
+ if (!existsSync(backupDir)) return null;
63
+ const backups = readdirSync(backupDir).filter((name) => name.startsWith("env-") && name.endsWith(".bak")).sort();
64
+ const latest = backups.at(-1);
65
+ return latest ? path.join(backupDir, latest) : null;
66
+ }
67
+ async function rollbackStackUpdate(options) {
68
+ const exec = options.exec ?? defaultExec;
69
+ const backupEnvFile = options.lockFile && existsSync(options.lockFile) ? JSON.parse(readFileSync(options.lockFile, "utf8")).backupEnvFile : void 0;
70
+ const rollbackEnv = backupEnvFile && existsSync(backupEnvFile) ? backupEnvFile : findLatestBackupEnvFile(options.envFile);
71
+ if (!rollbackEnv) throw new Error(`No stack backup env found beside ${options.envFile}`);
72
+ writeFileSync(options.envFile, readFileSync(rollbackEnv), { mode: 384 });
73
+ const composeArgs = ["compose", "--file", options.composeFile, "--env-file", options.envFile];
74
+ exec("docker", [...composeArgs, "up", "-d"]);
75
+ return { status: "rolled_back", targetVersion: "previous", changes: [], backupEnvFile: rollbackEnv, lockFile: options.lockFile ?? path.join(path.dirname(options.envFile), ".exe-stack-lock.json") };
76
+ }
77
+ function parseStackManifest(raw, publicKey) {
78
+ const parsedRaw = JSON.parse(raw);
79
+ const parsed = isSignedEnvelope(parsedRaw) ? { ...parsedRaw.manifest, signature: parsedRaw.signature } : parsedRaw;
80
+ if (publicKey) verifyStackManifestSignature(parsed, publicKey);
29
81
  if (parsed.schemaVersion !== 1) throw new Error("Unsupported stack manifest schemaVersion");
30
82
  if (!parsed.latest || !parsed.stacks || typeof parsed.stacks !== "object") {
31
83
  throw new Error("Invalid stack manifest: latest and stacks are required");
@@ -43,9 +95,13 @@ function parseStackManifest(raw) {
43
95
  }
44
96
  return parsed;
45
97
  }
46
- async function loadStackManifest(ref, fetchText = defaultFetchText) {
47
- if (/^https?:\/\//.test(ref)) return parseStackManifest(await fetchText(ref));
48
- return parseStackManifest(readFileSync(ref, "utf8"));
98
+ async function loadStackManifest(ref, fetchText = defaultFetchText, publicKey, authToken) {
99
+ if (/^https?:\/\//.test(ref)) return parseStackManifest(await fetchTextWithAuth(ref, fetchText, authToken), publicKey);
100
+ return parseStackManifest(readFileSync(ref, "utf8"), publicKey);
101
+ }
102
+ async function fetchTextWithAuth(ref, fetchText, authToken) {
103
+ if (!authToken || fetchText !== defaultFetchText) return fetchText(ref);
104
+ return defaultFetchText(ref, authToken);
49
105
  }
50
106
  function parseEnv(raw) {
51
107
  const env = /* @__PURE__ */ new Map();
@@ -111,14 +167,17 @@ Re-run with --allow-breaking ${missing.map((c) => c.id).join(",")}`
111
167
  async function runStackUpdate(options) {
112
168
  const exec = options.exec ?? defaultExec;
113
169
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
114
- const manifest = await loadStackManifest(options.manifestRef, options.fetchText);
170
+ if (options.rollback) return rollbackStackUpdate(options);
171
+ const manifest = await loadStackManifest(options.manifestRef, options.fetchText, options.manifestPublicKey, options.manifestAuthToken);
115
172
  const envRaw = readFileSync(options.envFile, "utf8");
116
173
  const plan = createStackUpdatePlan(manifest, envRaw, options.targetVersion);
117
174
  assertBreakingChangesAllowed(plan, options.allowedBreakingChangeIds ?? []);
118
175
  const lockFile = options.lockFile ?? path.join(path.dirname(options.envFile), ".exe-stack-lock.json");
176
+ const previousVersion = readCurrentStackVersion(lockFile);
119
177
  if (options.dryRun || plan.changes.length === 0) {
120
178
  return { status: "planned", targetVersion: plan.targetVersion, changes: plan.changes, lockFile };
121
179
  }
180
+ await postDeployAudit(options, "started", plan.targetVersion, previousVersion);
122
181
  const backupDir = path.join(path.dirname(options.envFile), ".exe-stack-backups");
123
182
  mkdirSync(backupDir, { recursive: true });
124
183
  const stamp = now().toISOString().replace(/[:.]/g, "-");
@@ -134,7 +193,8 @@ async function runStackUpdate(options) {
134
193
  exec("docker", [...composeArgs, "pull"]);
135
194
  exec("docker", [...composeArgs, "up", "-d"]);
136
195
  await verifyReleaseHealth(plan.release, options.healthRetries ?? 12, options.healthDelayMs ?? 5e3);
137
- writeFileSync(lockFile, JSON.stringify({ stackVersion: plan.targetVersion, updatedAt: now().toISOString(), services: plan.release.services }, null, 2) + "\n");
196
+ writeFileSync(lockFile, JSON.stringify({ stackVersion: plan.targetVersion, updatedAt: now().toISOString(), backupEnvFile, services: plan.release.services }, null, 2) + "\n");
197
+ await postDeployAudit(options, "success", plan.targetVersion, previousVersion, void 0, { changes: plan.changes.length });
138
198
  return { status: "updated", targetVersion: plan.targetVersion, changes: plan.changes, backupEnvFile, lockFile };
139
199
  } catch (err) {
140
200
  writeFileSync(options.envFile, envRaw, { mode: 384 });
@@ -143,9 +203,37 @@ async function runStackUpdate(options) {
143
203
  } catch {
144
204
  }
145
205
  const reason = err instanceof Error ? err.message : String(err);
206
+ await postDeployAudit(options, "failed", plan.targetVersion, previousVersion, reason, { rollbackAttempted: true });
146
207
  throw new Error(`Stack update failed and rollback was attempted: ${reason}`);
147
208
  }
148
209
  }
210
+ function readCurrentStackVersion(lockFile) {
211
+ if (!existsSync(lockFile)) return void 0;
212
+ try {
213
+ const parsed = JSON.parse(readFileSync(lockFile, "utf8"));
214
+ return parsed.stackVersion;
215
+ } catch {
216
+ return void 0;
217
+ }
218
+ }
219
+ async function postDeployAudit(options, status, stackVersion, previousVersion, error, metadata) {
220
+ if (!options.auditUrl) return;
221
+ const postJson = options.postJson ?? defaultPostJson;
222
+ try {
223
+ await postJson(options.auditUrl, {
224
+ stackVersion,
225
+ previousVersion,
226
+ status,
227
+ error,
228
+ metadata,
229
+ deviceId: options.deviceId,
230
+ licenseKey: options.licenseKey
231
+ }, options.manifestAuthToken);
232
+ } catch (err) {
233
+ const reason = err instanceof Error ? err.message : String(err);
234
+ console.warn(`[stack-update] deploy audit failed: ${reason}`);
235
+ }
236
+ }
149
237
  async function verifyReleaseHealth(release, retries, delayMs) {
150
238
  for (const [serviceName, service] of Object.entries(release.services)) {
151
239
  if (!service.healthUrl) continue;
@@ -182,20 +270,43 @@ function httpStatus(urlString) {
182
270
  function defaultExec(cmd, args, opts) {
183
271
  execFileSync(cmd, args, { stdio: "inherit", cwd: opts?.cwd });
184
272
  }
185
- async function defaultFetchText(ref) {
186
- const res = await fetch(ref);
273
+ async function defaultFetchText(ref, authToken) {
274
+ const res = await fetch(ref, {
275
+ headers: authToken ? { authorization: `Bearer ${authToken}` } : void 0
276
+ });
187
277
  if (!res.ok) throw new Error(`Failed to fetch ${ref}: HTTP ${res.status}`);
188
278
  return res.text();
189
279
  }
280
+ async function defaultPostJson(url, body, authToken) {
281
+ const res = await fetch(url, {
282
+ method: "POST",
283
+ headers: {
284
+ "content-type": "application/json",
285
+ ...authToken ? { authorization: `Bearer ${authToken}` } : {}
286
+ },
287
+ body: JSON.stringify(body)
288
+ });
289
+ if (!res.ok) throw new Error(`Failed to POST ${url}: HTTP ${res.status}`);
290
+ }
190
291
  function defaultStackPaths() {
191
292
  const cwdCompose = path.resolve("docker-compose.yml");
192
293
  const cwdEnv = path.resolve(".env");
193
294
  return {
194
295
  composeFile: process.env.EXE_STACK_COMPOSE_FILE || (existsSync(cwdCompose) ? cwdCompose : "/opt/exe-stack/docker-compose.yml"),
195
296
  envFile: process.env.EXE_STACK_ENV_FILE || (existsSync(cwdEnv) ? cwdEnv : "/opt/exe-stack/.env"),
196
- manifestRef: process.env.EXE_STACK_MANIFEST || "https://updates.askexe.com/stack-manifest.json"
297
+ manifestRef: process.env.EXE_STACK_MANIFEST || "https://update.askexe.com/stack-manifest.json",
298
+ auditUrl: process.env.EXE_STACK_AUDIT_URL || "https://update.askexe.com/v1/deploy-audits",
299
+ manifestAuthToken: process.env.EXE_STACK_UPDATE_TOKEN,
300
+ manifestPublicKey: loadDefaultPublicKey()
197
301
  };
198
302
  }
303
+ function loadDefaultPublicKey() {
304
+ if (process.env.EXE_STACK_PUBLIC_KEY) return process.env.EXE_STACK_PUBLIC_KEY;
305
+ if (process.env.EXE_STACK_PUBLIC_KEY_FILE && existsSync(process.env.EXE_STACK_PUBLIC_KEY_FILE)) {
306
+ return readFileSync(process.env.EXE_STACK_PUBLIC_KEY_FILE, "utf8");
307
+ }
308
+ return void 0;
309
+ }
199
310
 
200
311
  // src/bin/stack-update.ts
201
312
  function parseArgs(args) {
@@ -204,8 +315,14 @@ function parseArgs(args) {
204
315
  manifestRef: defaults.manifestRef,
205
316
  composeFile: defaults.composeFile,
206
317
  envFile: defaults.envFile,
318
+ auditUrl: defaults.auditUrl,
319
+ manifestAuthToken: defaults.manifestAuthToken,
320
+ manifestPublicKey: defaults.manifestPublicKey,
321
+ deviceId: process.env.EXE_DEVICE_ID,
322
+ licenseKey: process.env.EXE_LICENSE_KEY,
207
323
  dryRun: false,
208
324
  check: false,
325
+ rollback: false,
209
326
  yes: false,
210
327
  allowedBreakingChangeIds: []
211
328
  };
@@ -221,6 +338,18 @@ function parseArgs(args) {
221
338
  else if (arg === "--env-file") opts.envFile = next();
222
339
  else if (arg.startsWith("--env-file=")) opts.envFile = arg.split("=").slice(1).join("=");
223
340
  else if (arg === "--lock-file") opts.lockFile = next();
341
+ else if (arg === "--public-key") opts.manifestPublicKey = readFileSync2(next(), "utf8");
342
+ else if (arg.startsWith("--public-key=")) opts.manifestPublicKey = readFileSync2(arg.split("=").slice(1).join("="), "utf8");
343
+ else if (arg === "--auth-token") opts.manifestAuthToken = next();
344
+ else if (arg.startsWith("--auth-token=")) opts.manifestAuthToken = arg.split("=").slice(1).join("=");
345
+ else if (arg === "--auth-token-env") opts.manifestAuthToken = process.env[next()] ?? "";
346
+ else if (arg === "--audit-url") opts.auditUrl = next();
347
+ else if (arg.startsWith("--audit-url=")) opts.auditUrl = arg.split("=").slice(1).join("=");
348
+ else if (arg === "--device-id") opts.deviceId = next();
349
+ else if (arg.startsWith("--device-id=")) opts.deviceId = arg.split("=").slice(1).join("=");
350
+ else if (arg === "--license-key") opts.licenseKey = next();
351
+ else if (arg.startsWith("--license-key=")) opts.licenseKey = arg.split("=").slice(1).join("=");
352
+ else if (arg === "--rollback") opts.rollback = true;
224
353
  else if (arg === "--dry-run") opts.dryRun = true;
225
354
  else if (arg === "--check") opts.check = true;
226
355
  else if (arg === "--yes" || arg === "-y") opts.yes = true;
@@ -242,13 +371,20 @@ Usage:
242
371
  exe-os stack-update [--manifest <path-or-url>] [--target <version>] [--yes]
243
372
 
244
373
  Options:
245
- --manifest <ref> Stack manifest JSON path or URL (default: updates.askexe.com)
374
+ --manifest <ref> Stack manifest JSON path or URL (default: update.askexe.com)
246
375
  --target <version> Stack version to install (default: manifest.latest)
247
376
  --compose-file <path> docker-compose.yml path (default: ./docker-compose.yml or /opt/exe-stack/docker-compose.yml)
248
377
  --env-file <path> .env path (default: ./.env or /opt/exe-stack/.env)
249
378
  --lock-file <path> Lock file path (default: beside .env)
250
379
  --check Print available changes only
251
380
  --dry-run Plan only; do not run Docker
381
+ --public-key <path> PEM public key for signed manifest verification
382
+ --auth-token <token> Bearer token for private update manifest/audit API
383
+ --auth-token-env <name> Read bearer token from an environment variable
384
+ --audit-url <url> Deploy-audit endpoint (default: EXE_STACK_AUDIT_URL/update.askexe.com)
385
+ --device-id <id> Device ID to include in deploy audit
386
+ --license-key <key> License key to include in deploy audit
387
+ --rollback Restore latest backed-up .env and restart stack
252
388
  --allow-breaking <ids> Confirm breaking changes, comma-separated
253
389
  -y, --yes Non-interactive confirmation
254
390
  `);
@@ -276,7 +412,16 @@ function printBreaking(changes) {
276
412
  }
277
413
  async function main() {
278
414
  const opts = parseArgs(process.argv.slice(2));
279
- const manifest = await loadStackManifest(opts.manifestRef);
415
+ if (opts.rollback) {
416
+ if (!opts.yes) {
417
+ console.error("Refusing to rollback without --yes.");
418
+ process.exit(2);
419
+ }
420
+ const result2 = await runStackUpdate(opts);
421
+ console.log(`\u2705 Stack rollback attempted using backup: ${result2.backupEnvFile ?? "latest backup"}`);
422
+ return;
423
+ }
424
+ const manifest = await loadStackManifest(opts.manifestRef, void 0, opts.manifestPublicKey, opts.manifestAuthToken);
280
425
  const envRaw = readFileSync2(opts.envFile, "utf8");
281
426
  const plan = createStackUpdatePlan(manifest, envRaw, opts.targetVersion);
282
427
  console.log(`Exe OS stack target: ${plan.targetVersion}`);
@@ -80,8 +80,8 @@ var init_config = __esm({
80
80
  rerankerAutoTrigger: {
81
81
  enabled: true,
82
82
  broadQueryMinCardinality: 5e4,
83
- fetchTopK: 150,
84
- returnTopK: 5
83
+ fetchTopK: 200,
84
+ returnTopK: 20
85
85
  }
86
86
  },
87
87
  graphRagEnabled: true,