@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
package/dist/bin/cli.js CHANGED
@@ -258,8 +258,8 @@ var init_config = __esm({
258
258
  rerankerAutoTrigger: {
259
259
  enabled: true,
260
260
  broadQueryMinCardinality: 5e4,
261
- fetchTopK: 150,
262
- returnTopK: 5
261
+ fetchTopK: 200,
262
+ returnTopK: 20
263
263
  }
264
264
  },
265
265
  graphRagEnabled: true,
@@ -820,7 +820,7 @@ function isLegacySplitPostToolCommand(command) {
820
820
  function textHasLegacySplitPostToolHook(text) {
821
821
  return [EXE_HOOK_FILES.ingest, EXE_HOOK_FILES.errorRecall, EXE_HOOK_FILES.ingestWorker].some((file) => text.includes(file));
822
822
  }
823
- var EXE_HOOK_FILES, EXE_HOOKS, LEGACY_SPLIT_POST_TOOL_HOOK_MARKERS;
823
+ var EXE_HOOK_FILES, EXE_HOOKS, EXE_HOOK_MANIFEST, LEGACY_SPLIT_POST_TOOL_HOOK_MARKERS;
824
824
  var init_runtime_hook_manifest = __esm({
825
825
  "src/adapters/runtime-hook-manifest.ts"() {
826
826
  "use strict";
@@ -844,6 +844,116 @@ var init_runtime_hook_manifest = __esm({
844
844
  notification: "dist/hooks/notification.js",
845
845
  instructionsLoaded: "dist/hooks/instructions-loaded.js"
846
846
  };
847
+ EXE_HOOK_MANIFEST = [
848
+ {
849
+ key: "postToolCombined",
850
+ event: "PostToolUse",
851
+ commandMarker: EXE_HOOKS.postToolCombined,
852
+ owner: "exe-os",
853
+ purpose: "Single PostToolUse entrypoint for ingestion, error recall, summaries, and bug detection.",
854
+ runtimes: ["claude", "codex", "opencode"],
855
+ checkpointRole: "none"
856
+ },
857
+ {
858
+ key: "sessionStart",
859
+ event: "SessionStart",
860
+ commandMarker: EXE_HOOKS.sessionStart,
861
+ owner: "exe-os",
862
+ purpose: "Loads agent identity, procedures, and boot context.",
863
+ runtimes: ["claude", "codex", "opencode"],
864
+ checkpointRole: "none"
865
+ },
866
+ {
867
+ key: "promptSubmit",
868
+ event: "UserPromptSubmit",
869
+ commandMarker: EXE_HOOKS.promptSubmit,
870
+ owner: "exe-os",
871
+ purpose: "Injects current tasks, pending reviews, and local context before each prompt.",
872
+ runtimes: ["claude", "codex", "opencode"],
873
+ checkpointRole: "none"
874
+ },
875
+ {
876
+ key: "heartbeat",
877
+ event: "UserPromptSubmit",
878
+ commandMarker: EXE_HOOKS.heartbeat,
879
+ owner: "exe-os",
880
+ purpose: "Lightweight heartbeat/status sidecar for Claude Code sessions.",
881
+ runtimes: ["claude"],
882
+ checkpointRole: "none"
883
+ },
884
+ {
885
+ key: "stop",
886
+ event: "Stop",
887
+ commandMarker: EXE_HOOKS.stop,
888
+ owner: "exe-os",
889
+ purpose: "Finalizes task state, capacity signals, and emergency checkpointing.",
890
+ runtimes: ["claude", "codex", "opencode"],
891
+ checkpointRole: "capacity_checkpoint"
892
+ },
893
+ {
894
+ key: "preToolUse",
895
+ event: "PreToolUse",
896
+ commandMarker: EXE_HOOKS.preToolUse,
897
+ owner: "exe-os",
898
+ purpose: "Preflight guardrails before shell/tool execution.",
899
+ runtimes: ["claude", "codex", "opencode"],
900
+ checkpointRole: "none"
901
+ },
902
+ {
903
+ key: "subagentStop",
904
+ event: "SubagentStop",
905
+ commandMarker: EXE_HOOKS.subagentStop,
906
+ owner: "exe-os",
907
+ purpose: "Captures subagent completion context.",
908
+ runtimes: ["claude"],
909
+ checkpointRole: "none"
910
+ },
911
+ {
912
+ key: "preCompact",
913
+ event: "PreCompact",
914
+ commandMarker: EXE_HOOKS.preCompact,
915
+ owner: "exe-os",
916
+ purpose: "Writes active-task snapshot and compaction recovery context.",
917
+ runtimes: ["claude"],
918
+ checkpointRole: "recovery_context"
919
+ },
920
+ {
921
+ key: "postCompact",
922
+ event: "PostCompact",
923
+ commandMarker: EXE_HOOKS.postCompact,
924
+ owner: "exe-os",
925
+ purpose: "Rehydrates recovery context after compaction.",
926
+ runtimes: ["claude"],
927
+ checkpointRole: "recovery_context"
928
+ },
929
+ {
930
+ key: "sessionEnd",
931
+ event: "SessionEnd",
932
+ commandMarker: EXE_HOOKS.sessionEnd,
933
+ owner: "exe-os",
934
+ purpose: "Stores session-end checkpoint and triages orphaned in-progress tasks.",
935
+ runtimes: ["claude"],
936
+ checkpointRole: "session_summary"
937
+ },
938
+ {
939
+ key: "notification",
940
+ event: "Notification",
941
+ commandMarker: EXE_HOOKS.notification,
942
+ owner: "exe-os",
943
+ purpose: "Captures runtime notifications and nudges.",
944
+ runtimes: ["claude"],
945
+ checkpointRole: "none"
946
+ },
947
+ {
948
+ key: "instructionsLoaded",
949
+ event: "InstructionsLoaded",
950
+ commandMarker: EXE_HOOKS.instructionsLoaded,
951
+ owner: "exe-os",
952
+ purpose: "Applies runtime instruction post-processing.",
953
+ runtimes: ["claude"],
954
+ checkpointRole: "none"
955
+ }
956
+ ];
847
957
  LEGACY_SPLIT_POST_TOOL_HOOK_MARKERS = [
848
958
  "dist/hooks/ingest.js",
849
959
  "dist/hooks/error-recall.js",
@@ -4561,6 +4671,51 @@ async function ensureSchema() {
4561
4671
  VALUES (new.rowid, new.content_text, new.sender_name, new.agent_response);
4562
4672
  END;
4563
4673
  `);
4674
+ await client.executeMultiple(`
4675
+ CREATE TABLE IF NOT EXISTS memory_cards (
4676
+ id TEXT PRIMARY KEY,
4677
+ memory_id TEXT NOT NULL,
4678
+ agent_id TEXT NOT NULL,
4679
+ session_id TEXT NOT NULL,
4680
+ project_name TEXT,
4681
+ timestamp TEXT NOT NULL,
4682
+ card_type TEXT NOT NULL,
4683
+ subject TEXT,
4684
+ predicate TEXT,
4685
+ object TEXT,
4686
+ content TEXT NOT NULL,
4687
+ source_ref TEXT,
4688
+ confidence REAL DEFAULT 0.6,
4689
+ active INTEGER DEFAULT 1,
4690
+ created_at TEXT NOT NULL
4691
+ );
4692
+
4693
+ CREATE INDEX IF NOT EXISTS idx_memory_cards_agent
4694
+ ON memory_cards(agent_id, active, timestamp);
4695
+
4696
+ CREATE INDEX IF NOT EXISTS idx_memory_cards_memory
4697
+ ON memory_cards(memory_id);
4698
+
4699
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_cards_fts
4700
+ USING fts5(content, subject, predicate, object, content='memory_cards', content_rowid='rowid');
4701
+
4702
+ CREATE TRIGGER IF NOT EXISTS memory_cards_fts_ai AFTER INSERT ON memory_cards BEGIN
4703
+ INSERT INTO memory_cards_fts(rowid, content, subject, predicate, object)
4704
+ VALUES (new.rowid, new.content, new.subject, new.predicate, new.object);
4705
+ END;
4706
+
4707
+ CREATE TRIGGER IF NOT EXISTS memory_cards_fts_ad AFTER DELETE ON memory_cards BEGIN
4708
+ INSERT INTO memory_cards_fts(memory_cards_fts, rowid, content, subject, predicate, object)
4709
+ VALUES('delete', old.rowid, old.content, old.subject, old.predicate, old.object);
4710
+ END;
4711
+
4712
+ CREATE TRIGGER IF NOT EXISTS memory_cards_fts_au AFTER UPDATE ON memory_cards BEGIN
4713
+ INSERT INTO memory_cards_fts(memory_cards_fts, rowid, content, subject, predicate, object)
4714
+ VALUES('delete', old.rowid, old.content, old.subject, old.predicate, old.object);
4715
+ INSERT INTO memory_cards_fts(rowid, content, subject, predicate, object)
4716
+ VALUES (new.rowid, new.content, new.subject, new.predicate, new.object);
4717
+ END;
4718
+ `);
4564
4719
  try {
4565
4720
  await client.execute({
4566
4721
  sql: `ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3`,
@@ -7941,6 +8096,164 @@ ${p.content}`).join("\n\n");
7941
8096
  }
7942
8097
  });
7943
8098
 
8099
+ // src/lib/memory-cards.ts
8100
+ var memory_cards_exports = {};
8101
+ __export(memory_cards_exports, {
8102
+ extractMemoryCards: () => extractMemoryCards,
8103
+ insertMemoryCardsForBatch: () => insertMemoryCardsForBatch,
8104
+ searchMemoryCards: () => searchMemoryCards
8105
+ });
8106
+ import { createHash as createHash3 } from "crypto";
8107
+ function stableId(memoryId, type, content) {
8108
+ return createHash3("sha256").update(`${memoryId}:${type}:${content}`).digest("hex").slice(0, 32);
8109
+ }
8110
+ function cleanText(text) {
8111
+ return text.replace(/```[\s\S]*?```/g, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
8112
+ }
8113
+ function splitSentences(text) {
8114
+ return cleanText(text).split(/(?<=[.!?])\s+|\n+/).map((s) => s.trim()).filter((s) => s.length >= 24 && s.length <= MAX_SENTENCE_CHARS);
8115
+ }
8116
+ function inferCardType(sentence, toolName) {
8117
+ const lower = sentence.toLowerCase();
8118
+ if (toolName === "store_decision" || /\b(decided|decision|adr|approved|rejected)\b/.test(lower)) return "decision";
8119
+ if (/\b(prefers|preference|likes|dislikes|wants|doesn't want|does not want)\b/.test(lower)) return "preference";
8120
+ if (/\b(changed|updated|replaced|now|no longer|instead|supersedes)\b/.test(lower)) return "belief_update";
8121
+ if (toolName && ["Read", "Write", "Edit", "Bash"].includes(toolName)) return "code";
8122
+ if (/\b(meeting|deadline|shipped|launched|completed|failed|blocked|assigned|created)\b/.test(lower)) return "event";
8123
+ return "fact";
8124
+ }
8125
+ function extractSubject(sentence, agentId) {
8126
+ const explicit = sentence.match(/\b([A-Z][a-zA-Z0-9_-]{2,}(?:\s+[A-Z][a-zA-Z0-9_-]{2,})?)\b/);
8127
+ return explicit?.[1] ?? agentId;
8128
+ }
8129
+ function predicateFor(type) {
8130
+ switch (type) {
8131
+ case "preference":
8132
+ return "prefers";
8133
+ case "belief_update":
8134
+ return "updated";
8135
+ case "decision":
8136
+ return "decided";
8137
+ case "event":
8138
+ return "happened";
8139
+ case "code":
8140
+ return "implemented";
8141
+ default:
8142
+ return "states";
8143
+ }
8144
+ }
8145
+ function extractMemoryCards(row) {
8146
+ const sentences = splitSentences(row.raw_text);
8147
+ const cards = [];
8148
+ for (const sentence of sentences) {
8149
+ const type = inferCardType(sentence, row.tool_name);
8150
+ const subject = extractSubject(sentence, row.agent_id);
8151
+ const content = sentence.length > MAX_SENTENCE_CHARS ? `${sentence.slice(0, MAX_SENTENCE_CHARS - 1)}\u2026` : sentence;
8152
+ cards.push({
8153
+ id: stableId(row.id, type, content),
8154
+ memory_id: row.id,
8155
+ agent_id: row.agent_id,
8156
+ session_id: row.session_id,
8157
+ project_name: row.project_name ?? null,
8158
+ timestamp: row.timestamp,
8159
+ card_type: type,
8160
+ subject,
8161
+ predicate: predicateFor(type),
8162
+ object: content,
8163
+ content,
8164
+ source_ref: row.id,
8165
+ confidence: type === "fact" ? 0.55 : 0.65
8166
+ });
8167
+ if (cards.length >= MAX_CARDS_PER_MEMORY) break;
8168
+ }
8169
+ return cards;
8170
+ }
8171
+ async function insertMemoryCardsForBatch(rows) {
8172
+ const cards = rows.flatMap(extractMemoryCards);
8173
+ if (cards.length === 0) return 0;
8174
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8175
+ const client = getClient();
8176
+ const stmts = cards.map((card) => ({
8177
+ sql: `INSERT OR IGNORE INTO memory_cards
8178
+ (id, memory_id, agent_id, session_id, project_name, timestamp, card_type,
8179
+ subject, predicate, object, content, source_ref, confidence, active, created_at)
8180
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
8181
+ args: [
8182
+ card.id,
8183
+ card.memory_id,
8184
+ card.agent_id,
8185
+ card.session_id,
8186
+ card.project_name,
8187
+ card.timestamp,
8188
+ card.card_type,
8189
+ card.subject,
8190
+ card.predicate,
8191
+ card.object,
8192
+ card.content,
8193
+ card.source_ref,
8194
+ card.confidence,
8195
+ now
8196
+ ]
8197
+ }));
8198
+ await client.batch(stmts, "write");
8199
+ return cards.length;
8200
+ }
8201
+ function buildMatchExpr(queryText) {
8202
+ 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);
8203
+ if (terms.length === 0) return null;
8204
+ return terms.map((t) => `${t}*`).join(terms.length >= 3 ? " AND " : " OR ");
8205
+ }
8206
+ async function searchMemoryCards(queryText, agentId, options) {
8207
+ const limit = options?.limit ?? 10;
8208
+ const matchExpr = buildMatchExpr(queryText);
8209
+ if (!matchExpr) return [];
8210
+ let sql = `SELECT c.id, c.memory_id, c.agent_id, c.session_id, c.project_name,
8211
+ c.timestamp, c.card_type, c.content, c.source_ref, c.confidence
8212
+ FROM memory_cards c
8213
+ JOIN memory_cards_fts fts ON c.rowid = fts.rowid
8214
+ WHERE memory_cards_fts MATCH ?
8215
+ AND c.agent_id = ?
8216
+ AND COALESCE(c.active, 1) = 1`;
8217
+ const args2 = [matchExpr, agentId];
8218
+ if (options?.projectName) {
8219
+ sql += ` AND c.project_name = ?`;
8220
+ args2.push(options.projectName);
8221
+ }
8222
+ if (options?.since) {
8223
+ sql += ` AND c.timestamp >= ?`;
8224
+ args2.push(options.since);
8225
+ }
8226
+ sql += ` ORDER BY rank LIMIT ?`;
8227
+ args2.push(limit);
8228
+ const result = await getClient().execute({ sql, args: args2 });
8229
+ return result.rows.map((row) => ({
8230
+ id: `card:${String(row.id)}`,
8231
+ agent_id: String(row.agent_id),
8232
+ agent_role: "memory_card",
8233
+ session_id: String(row.session_id),
8234
+ timestamp: String(row.timestamp),
8235
+ tool_name: `memory_card:${String(row.card_type)}`,
8236
+ project_name: row.project_name == null ? "" : String(row.project_name),
8237
+ has_error: false,
8238
+ raw_text: `[${String(row.card_type)}] ${String(row.content)}
8239
+ Source memory: ${String(row.source_ref ?? row.memory_id)}`,
8240
+ vector: [],
8241
+ importance: 6,
8242
+ status: "active",
8243
+ confidence: Number(row.confidence ?? 0.6),
8244
+ last_accessed: String(row.timestamp)
8245
+ }));
8246
+ }
8247
+ var MAX_CARDS_PER_MEMORY, MAX_SENTENCE_CHARS;
8248
+ var init_memory_cards = __esm({
8249
+ "src/lib/memory-cards.ts"() {
8250
+ "use strict";
8251
+ init_database();
8252
+ MAX_CARDS_PER_MEMORY = 6;
8253
+ MAX_SENTENCE_CHARS = 360;
8254
+ }
8255
+ });
8256
+
7944
8257
  // src/lib/store.ts
7945
8258
  var store_exports = {};
7946
8259
  __export(store_exports, {
@@ -8279,6 +8592,11 @@ async function flushBatch() {
8279
8592
  const globalClient = getClient();
8280
8593
  const globalStmts = batch.map(buildStmt);
8281
8594
  await globalClient.batch(globalStmts, "write");
8595
+ try {
8596
+ const { insertMemoryCardsForBatch: insertMemoryCardsForBatch2 } = await Promise.resolve().then(() => (init_memory_cards(), memory_cards_exports));
8597
+ await insertMemoryCardsForBatch2(batch);
8598
+ } catch {
8599
+ }
8282
8600
  schedulePostWriteMemoryHygiene(batch.map((row) => row.id));
8283
8601
  _pendingRecords.splice(0, batch.length);
8284
8602
  try {
@@ -9537,7 +9855,7 @@ __export(identity_exports, {
9537
9855
  import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
9538
9856
  import { readdirSync as readdirSync4 } from "fs";
9539
9857
  import path17 from "path";
9540
- import { createHash as createHash3 } from "crypto";
9858
+ import { createHash as createHash4 } from "crypto";
9541
9859
  function ensureDir() {
9542
9860
  if (!existsSync16(IDENTITY_DIR2)) {
9543
9861
  mkdirSync9(IDENTITY_DIR2, { recursive: true });
@@ -9584,7 +9902,7 @@ function parseFrontmatter(raw) {
9584
9902
  };
9585
9903
  }
9586
9904
  function contentHash(content) {
9587
- return createHash3("sha256").update(content).digest("hex").slice(0, 16);
9905
+ return createHash4("sha256").update(content).digest("hex").slice(0, 16);
9588
9906
  }
9589
9907
  function getIdentity(agentId) {
9590
9908
  const filePath = identityPath(agentId);
@@ -15004,7 +15322,7 @@ var init_exe_rename = __esm({
15004
15322
  // src/lib/model-downloader.ts
15005
15323
  import { createWriteStream, createReadStream as createReadStream2, existsSync as existsSync26, unlinkSync as unlinkSync12, renameSync as renameSync6 } from "fs";
15006
15324
  import { mkdir as mkdir6 } from "fs/promises";
15007
- import { createHash as createHash4 } from "crypto";
15325
+ import { createHash as createHash5 } from "crypto";
15008
15326
  import path32 from "path";
15009
15327
  async function downloadModel(opts) {
15010
15328
  const { destDir, onProgress, fetchFn = globalThis.fetch } = opts;
@@ -15032,7 +15350,7 @@ async function downloadModel(opts) {
15032
15350
  throw new Error(`Download failed: HTTP ${response.status}`);
15033
15351
  }
15034
15352
  const contentLength = Number(response.headers.get("content-length") ?? EXPECTED_SIZE);
15035
- const hash = createHash4("sha256");
15353
+ const hash = createHash5("sha256");
15036
15354
  const fileStream = createWriteStream(tmpPath);
15037
15355
  const reader = response.body.getReader();
15038
15356
  try {
@@ -15076,7 +15394,7 @@ Download attempt ${attempt} failed, retrying...
15076
15394
  }
15077
15395
  async function fileHash(filePath) {
15078
15396
  return new Promise((resolve, reject) => {
15079
- const hash = createHash4("sha256");
15397
+ const hash = createHash5("sha256");
15080
15398
  const stream = createReadStream2(filePath);
15081
15399
  stream.on("data", (chunk) => hash.update(chunk));
15082
15400
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -16898,12 +17216,64 @@ var init_update = __esm({
16898
17216
 
16899
17217
  // src/lib/stack-update.ts
16900
17218
  import { execFileSync as execFileSync3 } from "child_process";
16901
- import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync25, renameSync as renameSync7, writeFileSync as writeFileSync20 } from "fs";
17219
+ import { createVerify, verify as verifySignature } from "crypto";
17220
+ import { existsSync as existsSync30, mkdirSync as mkdirSync19, readdirSync as readdirSync10, readFileSync as readFileSync25, renameSync as renameSync7, writeFileSync as writeFileSync20 } from "fs";
16902
17221
  import http from "http";
16903
17222
  import https from "https";
16904
17223
  import path37 from "path";
16905
- function parseStackManifest(raw) {
16906
- const parsed = JSON.parse(raw);
17224
+ function isSignedEnvelope(value) {
17225
+ return !!value && typeof value === "object" && "manifest" in value && "signature" in value;
17226
+ }
17227
+ function canonicalizeStackManifest(manifest) {
17228
+ const clone = JSON.parse(JSON.stringify(manifest));
17229
+ delete clone.signature;
17230
+ return stableJson(clone);
17231
+ }
17232
+ function verifyStackManifestSignature(manifest, publicKeyPem) {
17233
+ const signature = manifest.signature;
17234
+ if (!signature) throw new Error("Stack manifest signature required but missing");
17235
+ const payload = Buffer.from(canonicalizeStackManifest(manifest));
17236
+ const sig = Buffer.from(signature.signature, "base64");
17237
+ let ok = false;
17238
+ if (signature.alg === "ed25519") {
17239
+ ok = verifySignature(null, payload, publicKeyPem, sig);
17240
+ } else if (signature.alg === "rsa-sha256") {
17241
+ const verifier = createVerify("RSA-SHA256");
17242
+ verifier.update(payload);
17243
+ verifier.end();
17244
+ ok = verifier.verify(publicKeyPem, sig);
17245
+ } else {
17246
+ throw new Error(`Unsupported stack manifest signature alg: ${signature.alg}`);
17247
+ }
17248
+ if (!ok) throw new Error("Stack manifest signature verification failed");
17249
+ }
17250
+ function stableJson(value) {
17251
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
17252
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
17253
+ const obj = value;
17254
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableJson(obj[key])}`).join(",")}}`;
17255
+ }
17256
+ function findLatestBackupEnvFile(envFile) {
17257
+ const backupDir = path37.join(path37.dirname(envFile), ".exe-stack-backups");
17258
+ if (!existsSync30(backupDir)) return null;
17259
+ const backups = readdirSync10(backupDir).filter((name) => name.startsWith("env-") && name.endsWith(".bak")).sort();
17260
+ const latest = backups.at(-1);
17261
+ return latest ? path37.join(backupDir, latest) : null;
17262
+ }
17263
+ async function rollbackStackUpdate(options) {
17264
+ const exec2 = options.exec ?? defaultExec;
17265
+ const backupEnvFile = options.lockFile && existsSync30(options.lockFile) ? JSON.parse(readFileSync25(options.lockFile, "utf8")).backupEnvFile : void 0;
17266
+ const rollbackEnv = backupEnvFile && existsSync30(backupEnvFile) ? backupEnvFile : findLatestBackupEnvFile(options.envFile);
17267
+ if (!rollbackEnv) throw new Error(`No stack backup env found beside ${options.envFile}`);
17268
+ writeFileSync20(options.envFile, readFileSync25(rollbackEnv), { mode: 384 });
17269
+ const composeArgs = ["compose", "--file", options.composeFile, "--env-file", options.envFile];
17270
+ exec2("docker", [...composeArgs, "up", "-d"]);
17271
+ return { status: "rolled_back", targetVersion: "previous", changes: [], backupEnvFile: rollbackEnv, lockFile: options.lockFile ?? path37.join(path37.dirname(options.envFile), ".exe-stack-lock.json") };
17272
+ }
17273
+ function parseStackManifest(raw, publicKey) {
17274
+ const parsedRaw = JSON.parse(raw);
17275
+ const parsed = isSignedEnvelope(parsedRaw) ? { ...parsedRaw.manifest, signature: parsedRaw.signature } : parsedRaw;
17276
+ if (publicKey) verifyStackManifestSignature(parsed, publicKey);
16907
17277
  if (parsed.schemaVersion !== 1) throw new Error("Unsupported stack manifest schemaVersion");
16908
17278
  if (!parsed.latest || !parsed.stacks || typeof parsed.stacks !== "object") {
16909
17279
  throw new Error("Invalid stack manifest: latest and stacks are required");
@@ -16921,9 +17291,13 @@ function parseStackManifest(raw) {
16921
17291
  }
16922
17292
  return parsed;
16923
17293
  }
16924
- async function loadStackManifest(ref, fetchText = defaultFetchText) {
16925
- if (/^https?:\/\//.test(ref)) return parseStackManifest(await fetchText(ref));
16926
- return parseStackManifest(readFileSync25(ref, "utf8"));
17294
+ async function loadStackManifest(ref, fetchText = defaultFetchText, publicKey, authToken) {
17295
+ if (/^https?:\/\//.test(ref)) return parseStackManifest(await fetchTextWithAuth(ref, fetchText, authToken), publicKey);
17296
+ return parseStackManifest(readFileSync25(ref, "utf8"), publicKey);
17297
+ }
17298
+ async function fetchTextWithAuth(ref, fetchText, authToken) {
17299
+ if (!authToken || fetchText !== defaultFetchText) return fetchText(ref);
17300
+ return defaultFetchText(ref, authToken);
16927
17301
  }
16928
17302
  function parseEnv(raw) {
16929
17303
  const env = /* @__PURE__ */ new Map();
@@ -16989,14 +17363,17 @@ Re-run with --allow-breaking ${missing.map((c) => c.id).join(",")}`
16989
17363
  async function runStackUpdate(options) {
16990
17364
  const exec2 = options.exec ?? defaultExec;
16991
17365
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
16992
- const manifest = await loadStackManifest(options.manifestRef, options.fetchText);
17366
+ if (options.rollback) return rollbackStackUpdate(options);
17367
+ const manifest = await loadStackManifest(options.manifestRef, options.fetchText, options.manifestPublicKey, options.manifestAuthToken);
16993
17368
  const envRaw = readFileSync25(options.envFile, "utf8");
16994
17369
  const plan = createStackUpdatePlan(manifest, envRaw, options.targetVersion);
16995
17370
  assertBreakingChangesAllowed(plan, options.allowedBreakingChangeIds ?? []);
16996
17371
  const lockFile = options.lockFile ?? path37.join(path37.dirname(options.envFile), ".exe-stack-lock.json");
17372
+ const previousVersion = readCurrentStackVersion(lockFile);
16997
17373
  if (options.dryRun || plan.changes.length === 0) {
16998
17374
  return { status: "planned", targetVersion: plan.targetVersion, changes: plan.changes, lockFile };
16999
17375
  }
17376
+ await postDeployAudit(options, "started", plan.targetVersion, previousVersion);
17000
17377
  const backupDir = path37.join(path37.dirname(options.envFile), ".exe-stack-backups");
17001
17378
  mkdirSync19(backupDir, { recursive: true });
17002
17379
  const stamp = now().toISOString().replace(/[:.]/g, "-");
@@ -17012,7 +17389,8 @@ async function runStackUpdate(options) {
17012
17389
  exec2("docker", [...composeArgs, "pull"]);
17013
17390
  exec2("docker", [...composeArgs, "up", "-d"]);
17014
17391
  await verifyReleaseHealth(plan.release, options.healthRetries ?? 12, options.healthDelayMs ?? 5e3);
17015
- writeFileSync20(lockFile, JSON.stringify({ stackVersion: plan.targetVersion, updatedAt: now().toISOString(), services: plan.release.services }, null, 2) + "\n");
17392
+ writeFileSync20(lockFile, JSON.stringify({ stackVersion: plan.targetVersion, updatedAt: now().toISOString(), backupEnvFile, services: plan.release.services }, null, 2) + "\n");
17393
+ await postDeployAudit(options, "success", plan.targetVersion, previousVersion, void 0, { changes: plan.changes.length });
17016
17394
  return { status: "updated", targetVersion: plan.targetVersion, changes: plan.changes, backupEnvFile, lockFile };
17017
17395
  } catch (err) {
17018
17396
  writeFileSync20(options.envFile, envRaw, { mode: 384 });
@@ -17021,9 +17399,37 @@ async function runStackUpdate(options) {
17021
17399
  } catch {
17022
17400
  }
17023
17401
  const reason = err instanceof Error ? err.message : String(err);
17402
+ await postDeployAudit(options, "failed", plan.targetVersion, previousVersion, reason, { rollbackAttempted: true });
17024
17403
  throw new Error(`Stack update failed and rollback was attempted: ${reason}`);
17025
17404
  }
17026
17405
  }
17406
+ function readCurrentStackVersion(lockFile) {
17407
+ if (!existsSync30(lockFile)) return void 0;
17408
+ try {
17409
+ const parsed = JSON.parse(readFileSync25(lockFile, "utf8"));
17410
+ return parsed.stackVersion;
17411
+ } catch {
17412
+ return void 0;
17413
+ }
17414
+ }
17415
+ async function postDeployAudit(options, status, stackVersion, previousVersion, error, metadata) {
17416
+ if (!options.auditUrl) return;
17417
+ const postJson = options.postJson ?? defaultPostJson;
17418
+ try {
17419
+ await postJson(options.auditUrl, {
17420
+ stackVersion,
17421
+ previousVersion,
17422
+ status,
17423
+ error,
17424
+ metadata,
17425
+ deviceId: options.deviceId,
17426
+ licenseKey: options.licenseKey
17427
+ }, options.manifestAuthToken);
17428
+ } catch (err) {
17429
+ const reason = err instanceof Error ? err.message : String(err);
17430
+ console.warn(`[stack-update] deploy audit failed: ${reason}`);
17431
+ }
17432
+ }
17027
17433
  async function verifyReleaseHealth(release, retries, delayMs) {
17028
17434
  for (const [serviceName, service] of Object.entries(release.services)) {
17029
17435
  if (!service.healthUrl) continue;
@@ -17060,20 +17466,43 @@ function httpStatus(urlString) {
17060
17466
  function defaultExec(cmd, args2, opts) {
17061
17467
  execFileSync3(cmd, args2, { stdio: "inherit", cwd: opts?.cwd });
17062
17468
  }
17063
- async function defaultFetchText(ref) {
17064
- const res = await fetch(ref);
17469
+ async function defaultFetchText(ref, authToken) {
17470
+ const res = await fetch(ref, {
17471
+ headers: authToken ? { authorization: `Bearer ${authToken}` } : void 0
17472
+ });
17065
17473
  if (!res.ok) throw new Error(`Failed to fetch ${ref}: HTTP ${res.status}`);
17066
17474
  return res.text();
17067
17475
  }
17476
+ async function defaultPostJson(url, body, authToken) {
17477
+ const res = await fetch(url, {
17478
+ method: "POST",
17479
+ headers: {
17480
+ "content-type": "application/json",
17481
+ ...authToken ? { authorization: `Bearer ${authToken}` } : {}
17482
+ },
17483
+ body: JSON.stringify(body)
17484
+ });
17485
+ if (!res.ok) throw new Error(`Failed to POST ${url}: HTTP ${res.status}`);
17486
+ }
17068
17487
  function defaultStackPaths() {
17069
17488
  const cwdCompose = path37.resolve("docker-compose.yml");
17070
17489
  const cwdEnv = path37.resolve(".env");
17071
17490
  return {
17072
17491
  composeFile: process.env.EXE_STACK_COMPOSE_FILE || (existsSync30(cwdCompose) ? cwdCompose : "/opt/exe-stack/docker-compose.yml"),
17073
17492
  envFile: process.env.EXE_STACK_ENV_FILE || (existsSync30(cwdEnv) ? cwdEnv : "/opt/exe-stack/.env"),
17074
- manifestRef: process.env.EXE_STACK_MANIFEST || "https://updates.askexe.com/stack-manifest.json"
17493
+ manifestRef: process.env.EXE_STACK_MANIFEST || "https://update.askexe.com/stack-manifest.json",
17494
+ auditUrl: process.env.EXE_STACK_AUDIT_URL || "https://update.askexe.com/v1/deploy-audits",
17495
+ manifestAuthToken: process.env.EXE_STACK_UPDATE_TOKEN,
17496
+ manifestPublicKey: loadDefaultPublicKey()
17075
17497
  };
17076
17498
  }
17499
+ function loadDefaultPublicKey() {
17500
+ if (process.env.EXE_STACK_PUBLIC_KEY) return process.env.EXE_STACK_PUBLIC_KEY;
17501
+ if (process.env.EXE_STACK_PUBLIC_KEY_FILE && existsSync30(process.env.EXE_STACK_PUBLIC_KEY_FILE)) {
17502
+ return readFileSync25(process.env.EXE_STACK_PUBLIC_KEY_FILE, "utf8");
17503
+ }
17504
+ return void 0;
17505
+ }
17077
17506
  var init_stack_update = __esm({
17078
17507
  "src/lib/stack-update.ts"() {
17079
17508
  "use strict";
@@ -17092,8 +17521,14 @@ function parseArgs4(args2) {
17092
17521
  manifestRef: defaults.manifestRef,
17093
17522
  composeFile: defaults.composeFile,
17094
17523
  envFile: defaults.envFile,
17524
+ auditUrl: defaults.auditUrl,
17525
+ manifestAuthToken: defaults.manifestAuthToken,
17526
+ manifestPublicKey: defaults.manifestPublicKey,
17527
+ deviceId: process.env.EXE_DEVICE_ID,
17528
+ licenseKey: process.env.EXE_LICENSE_KEY,
17095
17529
  dryRun: false,
17096
17530
  check: false,
17531
+ rollback: false,
17097
17532
  yes: false,
17098
17533
  allowedBreakingChangeIds: []
17099
17534
  };
@@ -17109,6 +17544,18 @@ function parseArgs4(args2) {
17109
17544
  else if (arg === "--env-file") opts.envFile = next();
17110
17545
  else if (arg.startsWith("--env-file=")) opts.envFile = arg.split("=").slice(1).join("=");
17111
17546
  else if (arg === "--lock-file") opts.lockFile = next();
17547
+ else if (arg === "--public-key") opts.manifestPublicKey = readFileSync26(next(), "utf8");
17548
+ else if (arg.startsWith("--public-key=")) opts.manifestPublicKey = readFileSync26(arg.split("=").slice(1).join("="), "utf8");
17549
+ else if (arg === "--auth-token") opts.manifestAuthToken = next();
17550
+ else if (arg.startsWith("--auth-token=")) opts.manifestAuthToken = arg.split("=").slice(1).join("=");
17551
+ else if (arg === "--auth-token-env") opts.manifestAuthToken = process.env[next()] ?? "";
17552
+ else if (arg === "--audit-url") opts.auditUrl = next();
17553
+ else if (arg.startsWith("--audit-url=")) opts.auditUrl = arg.split("=").slice(1).join("=");
17554
+ else if (arg === "--device-id") opts.deviceId = next();
17555
+ else if (arg.startsWith("--device-id=")) opts.deviceId = arg.split("=").slice(1).join("=");
17556
+ else if (arg === "--license-key") opts.licenseKey = next();
17557
+ else if (arg.startsWith("--license-key=")) opts.licenseKey = arg.split("=").slice(1).join("=");
17558
+ else if (arg === "--rollback") opts.rollback = true;
17112
17559
  else if (arg === "--dry-run") opts.dryRun = true;
17113
17560
  else if (arg === "--check") opts.check = true;
17114
17561
  else if (arg === "--yes" || arg === "-y") opts.yes = true;
@@ -17130,13 +17577,20 @@ Usage:
17130
17577
  exe-os stack-update [--manifest <path-or-url>] [--target <version>] [--yes]
17131
17578
 
17132
17579
  Options:
17133
- --manifest <ref> Stack manifest JSON path or URL (default: updates.askexe.com)
17580
+ --manifest <ref> Stack manifest JSON path or URL (default: update.askexe.com)
17134
17581
  --target <version> Stack version to install (default: manifest.latest)
17135
17582
  --compose-file <path> docker-compose.yml path (default: ./docker-compose.yml or /opt/exe-stack/docker-compose.yml)
17136
17583
  --env-file <path> .env path (default: ./.env or /opt/exe-stack/.env)
17137
17584
  --lock-file <path> Lock file path (default: beside .env)
17138
17585
  --check Print available changes only
17139
17586
  --dry-run Plan only; do not run Docker
17587
+ --public-key <path> PEM public key for signed manifest verification
17588
+ --auth-token <token> Bearer token for private update manifest/audit API
17589
+ --auth-token-env <name> Read bearer token from an environment variable
17590
+ --audit-url <url> Deploy-audit endpoint (default: EXE_STACK_AUDIT_URL/update.askexe.com)
17591
+ --device-id <id> Device ID to include in deploy audit
17592
+ --license-key <key> License key to include in deploy audit
17593
+ --rollback Restore latest backed-up .env and restart stack
17140
17594
  --allow-breaking <ids> Confirm breaking changes, comma-separated
17141
17595
  -y, --yes Non-interactive confirmation
17142
17596
  `);
@@ -17164,7 +17618,16 @@ function printBreaking(changes) {
17164
17618
  }
17165
17619
  async function main3() {
17166
17620
  const opts = parseArgs4(process.argv.slice(2));
17167
- const manifest = await loadStackManifest(opts.manifestRef);
17621
+ if (opts.rollback) {
17622
+ if (!opts.yes) {
17623
+ console.error("Refusing to rollback without --yes.");
17624
+ process.exit(2);
17625
+ }
17626
+ const result2 = await runStackUpdate(opts);
17627
+ console.log(`\u2705 Stack rollback attempted using backup: ${result2.backupEnvFile ?? "latest backup"}`);
17628
+ return;
17629
+ }
17630
+ const manifest = await loadStackManifest(opts.manifestRef, void 0, opts.manifestPublicKey, opts.manifestAuthToken);
17168
17631
  const envRaw = readFileSync26(opts.envFile, "utf8");
17169
17632
  const plan = createStackUpdatePlan(manifest, envRaw, opts.targetVersion);
17170
17633
  console.log(`Exe OS stack target: ${plan.targetVersion}`);
@@ -32104,7 +32567,7 @@ var init_mcp_diagnostics = __esm({
32104
32567
  });
32105
32568
 
32106
32569
  // src/bin/cli.ts
32107
- import { existsSync as existsSync35, readFileSync as readFileSync30, writeFileSync as writeFileSync21, readdirSync as readdirSync10, rmSync } from "fs";
32570
+ import { existsSync as existsSync35, readFileSync as readFileSync30, writeFileSync as writeFileSync21, readdirSync as readdirSync11, rmSync } from "fs";
32108
32571
  import path49 from "path";
32109
32572
  import os22 from "os";
32110
32573
  var args = process.argv.slice(2);
@@ -32553,7 +33016,7 @@ async function runClaudeUninstall(flags = []) {
32553
33016
  if (existsSync35(skillsDir)) {
32554
33017
  let skillCount = 0;
32555
33018
  try {
32556
- const entries = readdirSync10(skillsDir);
33019
+ const entries = readdirSync11(skillsDir);
32557
33020
  for (const entry of entries) {
32558
33021
  if (entry.startsWith("exe")) {
32559
33022
  const fullPath = path49.join(skillsDir, entry);
@@ -32586,7 +33049,7 @@ async function runClaudeUninstall(flags = []) {
32586
33049
  if (existsSync35(agentsDir)) {
32587
33050
  let agentCount = 0;
32588
33051
  try {
32589
- const entries = readdirSync10(agentsDir).filter((f) => f.endsWith(".md"));
33052
+ const entries = readdirSync11(agentsDir).filter((f) => f.endsWith(".md"));
32590
33053
  let knownNames = /* @__PURE__ */ new Set();
32591
33054
  const rosterPath = path49.join(exeOsDir, "exe-employees.json");
32592
33055
  if (existsSync35(rosterPath)) {
@@ -32614,7 +33077,7 @@ async function runClaudeUninstall(flags = []) {
32614
33077
  if (existsSync35(projectsDir)) {
32615
33078
  let projectCount = 0;
32616
33079
  try {
32617
- const projects = readdirSync10(projectsDir);
33080
+ const projects = readdirSync11(projectsDir);
32618
33081
  for (const proj of projects) {
32619
33082
  const projSettings = path49.join(projectsDir, proj, "settings.json");
32620
33083
  if (!existsSync35(projSettings)) continue;