@askexenow/exe-os 0.9.10 → 0.9.12

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.
@@ -448,13 +448,13 @@ __export(config_exports, {
448
448
  });
449
449
  import { readFile, writeFile } from "fs/promises";
450
450
  import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
451
- import path from "path";
452
- import os from "os";
451
+ import path2 from "path";
452
+ import os2 from "os";
453
453
  function resolveDataDir() {
454
454
  if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
455
455
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
456
- const newDir = path.join(os.homedir(), ".exe-os");
457
- const legacyDir = path.join(os.homedir(), ".exe-mem");
456
+ const newDir = path2.join(os2.homedir(), ".exe-os");
457
+ const legacyDir = path2.join(os2.homedir(), ".exe-mem");
458
458
  if (!existsSync2(newDir) && existsSync2(legacyDir)) {
459
459
  try {
460
460
  renameSync(legacyDir, newDir);
@@ -519,9 +519,9 @@ function normalizeAutoUpdate(raw) {
519
519
  async function loadConfig() {
520
520
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
521
521
  await ensurePrivateDir(dir);
522
- const configPath = path.join(dir, "config.json");
522
+ const configPath = path2.join(dir, "config.json");
523
523
  if (!existsSync2(configPath)) {
524
- return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
524
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
525
525
  }
526
526
  const raw = await readFile(configPath, "utf-8");
527
527
  try {
@@ -540,20 +540,20 @@ async function loadConfig() {
540
540
  normalizeScalingRoadmap(migratedCfg);
541
541
  normalizeSessionLifecycle(migratedCfg);
542
542
  normalizeAutoUpdate(migratedCfg);
543
- const config2 = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
543
+ const config2 = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
544
544
  if (config2.dbPath.startsWith("~")) {
545
- config2.dbPath = config2.dbPath.replace(/^~/, os.homedir());
545
+ config2.dbPath = config2.dbPath.replace(/^~/, os2.homedir());
546
546
  }
547
547
  return config2;
548
548
  } catch {
549
- return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
549
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
550
550
  }
551
551
  }
552
552
  function loadConfigSync() {
553
553
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
554
- const configPath = path.join(dir, "config.json");
554
+ const configPath = path2.join(dir, "config.json");
555
555
  if (!existsSync2(configPath)) {
556
- return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
556
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
557
557
  }
558
558
  try {
559
559
  const raw = readFileSync(configPath, "utf-8");
@@ -563,15 +563,15 @@ function loadConfigSync() {
563
563
  normalizeScalingRoadmap(migratedCfg);
564
564
  normalizeSessionLifecycle(migratedCfg);
565
565
  normalizeAutoUpdate(migratedCfg);
566
- return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
566
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
567
567
  } catch {
568
- return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
568
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
569
569
  }
570
570
  }
571
571
  async function saveConfig(config2) {
572
572
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
573
573
  await ensurePrivateDir(dir);
574
- const configPath = path.join(dir, "config.json");
574
+ const configPath = path2.join(dir, "config.json");
575
575
  await writeFile(configPath, JSON.stringify(config2, null, 2) + "\n");
576
576
  await enforcePrivateFile(configPath);
577
577
  }
@@ -595,10 +595,10 @@ var init_config = __esm({
595
595
  "use strict";
596
596
  init_secure_files();
597
597
  EXE_AI_DIR = resolveDataDir();
598
- DB_PATH = path.join(EXE_AI_DIR, "memories.db");
599
- MODELS_DIR = path.join(EXE_AI_DIR, "models");
600
- CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
601
- LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
598
+ DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
599
+ MODELS_DIR = path2.join(EXE_AI_DIR, "models");
600
+ CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
601
+ LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
602
602
  CURRENT_CONFIG_VERSION = 1;
603
603
  DEFAULT_CONFIG = {
604
604
  config_version: CURRENT_CONFIG_VERSION,
@@ -711,7 +711,7 @@ __export(agent_config_exports, {
711
711
  setAgentRuntime: () => setAgentRuntime
712
712
  });
713
713
  import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
714
- import path2 from "path";
714
+ import path3 from "path";
715
715
  function loadAgentConfig() {
716
716
  if (!existsSync3(AGENT_CONFIG_PATH)) return {};
717
717
  try {
@@ -721,7 +721,7 @@ function loadAgentConfig() {
721
721
  }
722
722
  }
723
723
  function saveAgentConfig(config2) {
724
- const dir = path2.dirname(AGENT_CONFIG_PATH);
724
+ const dir = path3.dirname(AGENT_CONFIG_PATH);
725
725
  ensurePrivateDirSync(dir);
726
726
  writeFileSync(AGENT_CONFIG_PATH, JSON.stringify(config2, null, 2) + "\n", "utf-8");
727
727
  enforcePrivateFileSync(AGENT_CONFIG_PATH);
@@ -765,7 +765,7 @@ var init_agent_config = __esm({
765
765
  init_config();
766
766
  init_runtime_table();
767
767
  init_secure_files();
768
- AGENT_CONFIG_PATH = path2.join(EXE_AI_DIR, "agent-config.json");
768
+ AGENT_CONFIG_PATH = path3.join(EXE_AI_DIR, "agent-config.json");
769
769
  KNOWN_RUNTIMES = {
770
770
  claude: ["claude-opus-4", "claude-sonnet-4", "claude-haiku-4.5"],
771
771
  codex: ["gpt-5.4", "gpt-5.5", "gpt-5.3-codex-spark", "o3", "o4-mini"],
@@ -814,8 +814,8 @@ __export(employees_exports, {
814
814
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
815
815
  import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
816
816
  import { execSync } from "child_process";
817
- import path3 from "path";
818
- import os2 from "os";
817
+ import path4 from "path";
818
+ import os3 from "os";
819
819
  function normalizeRole(role) {
820
820
  return (role ?? "").trim().toLowerCase();
821
821
  }
@@ -862,7 +862,7 @@ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
862
862
  }
863
863
  }
864
864
  async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
865
- await mkdir2(path3.dirname(employeesPath), { recursive: true });
865
+ await mkdir2(path4.dirname(employeesPath), { recursive: true });
866
866
  await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
867
867
  }
868
868
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
@@ -913,7 +913,7 @@ function addEmployee(employees, employee) {
913
913
  function appendToCoordinatorTeam(employee) {
914
914
  const coordinator = getCoordinatorEmployee(loadEmployeesSync());
915
915
  if (!coordinator) return;
916
- const idPath = path3.join(IDENTITY_DIR, `${coordinator.name}.md`);
916
+ const idPath = path4.join(IDENTITY_DIR, `${coordinator.name}.md`);
917
917
  if (!existsSync4(idPath)) return;
918
918
  const content = readFileSync3(idPath, "utf-8");
919
919
  if (content.includes(`**${capitalize(employee.name)}`)) return;
@@ -965,9 +965,9 @@ async function normalizeRosterCase(rosterPath) {
965
965
  emp.name = emp.name.toLowerCase();
966
966
  changed = true;
967
967
  try {
968
- const identityDir = path3.join(os2.homedir(), ".exe-os", "identity");
969
- const oldPath = path3.join(identityDir, `${oldName}.md`);
970
- const newPath = path3.join(identityDir, `${emp.name}.md`);
968
+ const identityDir = path4.join(os3.homedir(), ".exe-os", "identity");
969
+ const oldPath = path4.join(identityDir, `${oldName}.md`);
970
+ const newPath = path4.join(identityDir, `${emp.name}.md`);
971
971
  if (existsSync4(oldPath) && !existsSync4(newPath)) {
972
972
  renameSync2(oldPath, newPath);
973
973
  } else if (existsSync4(oldPath) && oldPath !== newPath) {
@@ -1002,7 +1002,7 @@ function registerBinSymlinks(name) {
1002
1002
  errors.push("Could not find 'exe-os' in PATH");
1003
1003
  return { created, skipped, errors };
1004
1004
  }
1005
- const binDir = path3.dirname(exeBinPath);
1005
+ const binDir = path4.dirname(exeBinPath);
1006
1006
  let target;
1007
1007
  try {
1008
1008
  target = readlinkSync(exeBinPath);
@@ -1012,7 +1012,7 @@ function registerBinSymlinks(name) {
1012
1012
  }
1013
1013
  for (const suffix of ["", "-opencode"]) {
1014
1014
  const linkName = `${name}${suffix}`;
1015
- const linkPath = path3.join(binDir, linkName);
1015
+ const linkPath = path4.join(binDir, linkName);
1016
1016
  if (existsSync4(linkPath)) {
1017
1017
  skipped.push(linkName);
1018
1018
  continue;
@@ -1031,20 +1031,20 @@ var init_employees = __esm({
1031
1031
  "src/lib/employees.ts"() {
1032
1032
  "use strict";
1033
1033
  init_config();
1034
- EMPLOYEES_PATH = path3.join(EXE_AI_DIR, "exe-employees.json");
1034
+ EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
1035
1035
  DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
1036
1036
  COORDINATOR_ROLE = "COO";
1037
1037
  MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
1038
- IDENTITY_DIR = path3.join(EXE_AI_DIR, "identity");
1038
+ IDENTITY_DIR = path4.join(EXE_AI_DIR, "identity");
1039
1039
  TEAM_SECTION_RE = /^## Team\b.*$/m;
1040
1040
  }
1041
1041
  });
1042
1042
 
1043
1043
  // src/lib/database-adapter.ts
1044
- import os3 from "os";
1045
- import path4 from "path";
1046
- import { createRequire } from "module";
1047
- import { pathToFileURL } from "url";
1044
+ import os4 from "os";
1045
+ import path5 from "path";
1046
+ import { createRequire as createRequire2 } from "module";
1047
+ import { pathToFileURL as pathToFileURL2 } from "url";
1048
1048
  function quotedIdentifier(identifier) {
1049
1049
  return `"${identifier.replace(/"/g, '""')}"`;
1050
1050
  }
@@ -1346,17 +1346,17 @@ async function loadPrismaClient() {
1346
1346
  prismaClientPromise = (async () => {
1347
1347
  const explicitPath = process.env.EXE_OS_PRISMA_CLIENT_PATH;
1348
1348
  if (explicitPath) {
1349
- const module2 = await import(pathToFileURL(explicitPath).href);
1349
+ const module2 = await import(pathToFileURL2(explicitPath).href);
1350
1350
  const PrismaClient2 = module2.PrismaClient ?? module2.default?.PrismaClient;
1351
1351
  if (!PrismaClient2) {
1352
1352
  throw new Error(`No PrismaClient export found at ${explicitPath}`);
1353
1353
  }
1354
1354
  return new PrismaClient2();
1355
1355
  }
1356
- const exeDbRoot = process.env.EXE_DB_ROOT ?? path4.join(os3.homedir(), "exe-db");
1357
- const requireFromExeDb = createRequire(path4.join(exeDbRoot, "package.json"));
1356
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path5.join(os4.homedir(), "exe-db");
1357
+ const requireFromExeDb = createRequire2(path5.join(exeDbRoot, "package.json"));
1358
1358
  const prismaEntry = requireFromExeDb.resolve("@prisma/client");
1359
- const module = await import(pathToFileURL(prismaEntry).href);
1359
+ const module = await import(pathToFileURL2(prismaEntry).href);
1360
1360
  const PrismaClient = module.PrismaClient ?? module.default?.PrismaClient;
1361
1361
  if (!PrismaClient) {
1362
1362
  throw new Error(`No PrismaClient export found in ${prismaEntry}`);
@@ -2690,7 +2690,7 @@ var init_memory = __esm({
2690
2690
 
2691
2691
  // src/lib/daemon-auth.ts
2692
2692
  import crypto from "crypto";
2693
- import path5 from "path";
2693
+ import path6 from "path";
2694
2694
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2695
2695
  function normalizeToken(token) {
2696
2696
  if (!token) return null;
@@ -2721,17 +2721,17 @@ var init_daemon_auth = __esm({
2721
2721
  "use strict";
2722
2722
  init_config();
2723
2723
  init_secure_files();
2724
- DAEMON_TOKEN_PATH = path5.join(EXE_AI_DIR, "exed.token");
2724
+ DAEMON_TOKEN_PATH = path6.join(EXE_AI_DIR, "exed.token");
2725
2725
  }
2726
2726
  });
2727
2727
 
2728
2728
  // src/lib/exe-daemon-client.ts
2729
2729
  import net from "net";
2730
- import os4 from "os";
2730
+ import os5 from "os";
2731
2731
  import { spawn } from "child_process";
2732
2732
  import { randomUUID } from "crypto";
2733
2733
  import { existsSync as existsSync6, unlinkSync as unlinkSync2, readFileSync as readFileSync5, openSync, closeSync, statSync } from "fs";
2734
- import path6 from "path";
2734
+ import path7 from "path";
2735
2735
  import { fileURLToPath } from "url";
2736
2736
  function handleData(chunk) {
2737
2737
  _buffer += chunk.toString();
@@ -2782,17 +2782,17 @@ function cleanupStaleFiles() {
2782
2782
  }
2783
2783
  }
2784
2784
  function findPackageRoot() {
2785
- let dir = path6.dirname(fileURLToPath(import.meta.url));
2786
- const { root } = path6.parse(dir);
2785
+ let dir = path7.dirname(fileURLToPath(import.meta.url));
2786
+ const { root } = path7.parse(dir);
2787
2787
  while (dir !== root) {
2788
- if (existsSync6(path6.join(dir, "package.json"))) return dir;
2789
- dir = path6.dirname(dir);
2788
+ if (existsSync6(path7.join(dir, "package.json"))) return dir;
2789
+ dir = path7.dirname(dir);
2790
2790
  }
2791
2791
  return null;
2792
2792
  }
2793
2793
  function spawnDaemon() {
2794
- const freeGB = os4.freemem() / (1024 * 1024 * 1024);
2795
- const totalGB = os4.totalmem() / (1024 * 1024 * 1024);
2794
+ const freeGB = os5.freemem() / (1024 * 1024 * 1024);
2795
+ const totalGB = os5.totalmem() / (1024 * 1024 * 1024);
2796
2796
  if (totalGB <= 8) {
2797
2797
  process.stderr.write(
2798
2798
  `[exed-client] SKIP: ${totalGB.toFixed(0)}GB system \u2014 embedding daemon disabled. Using keyword search only. Minimum 16GB recommended for vector search.
@@ -2812,7 +2812,7 @@ function spawnDaemon() {
2812
2812
  process.stderr.write("[exed-client] WARN: cannot find package root\n");
2813
2813
  return;
2814
2814
  }
2815
- const daemonPath = path6.join(pkgRoot, "dist", "lib", "exe-daemon.js");
2815
+ const daemonPath = path7.join(pkgRoot, "dist", "lib", "exe-daemon.js");
2816
2816
  if (!existsSync6(daemonPath)) {
2817
2817
  process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
2818
2818
  `);
@@ -2822,7 +2822,7 @@ function spawnDaemon() {
2822
2822
  const daemonToken = ensureDaemonToken(process.env[DAEMON_TOKEN_ENV] ?? null);
2823
2823
  process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
2824
2824
  `);
2825
- const logPath = path6.join(path6.dirname(SOCKET_PATH), "exed.log");
2825
+ const logPath = path7.join(path7.dirname(SOCKET_PATH), "exed.log");
2826
2826
  let stderrFd = "ignore";
2827
2827
  try {
2828
2828
  stderrFd = openSync(logPath, "a");
@@ -3112,9 +3112,9 @@ var init_exe_daemon_client = __esm({
3112
3112
  "use strict";
3113
3113
  init_config();
3114
3114
  init_daemon_auth();
3115
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path6.join(EXE_AI_DIR, "exed.sock");
3116
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path6.join(EXE_AI_DIR, "exed.pid");
3117
- SPAWN_LOCK_PATH = path6.join(EXE_AI_DIR, "exed-spawn.lock");
3115
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path7.join(EXE_AI_DIR, "exed.sock");
3116
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path7.join(EXE_AI_DIR, "exed.pid");
3117
+ SPAWN_LOCK_PATH = path7.join(EXE_AI_DIR, "exed-spawn.lock");
3118
3118
  SPAWN_LOCK_STALE_MS = 3e4;
3119
3119
  CONNECT_TIMEOUT_MS = 15e3;
3120
3120
  REQUEST_TIMEOUT_MS = 3e4;
@@ -3171,8 +3171,8 @@ async function embedDirect(text) {
3171
3171
  const llamaCpp = await import("node-llama-cpp");
3172
3172
  const { MODELS_DIR: MODELS_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
3173
3173
  const { existsSync: existsSync19 } = await import("fs");
3174
- const path23 = await import("path");
3175
- const modelPath = path23.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
3174
+ const path24 = await import("path");
3175
+ const modelPath = path24.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
3176
3176
  if (!existsSync19(modelPath)) {
3177
3177
  throw new Error(`Embedding model not found at ${modelPath}. Run '/exe-setup' to download it.`);
3178
3178
  }
@@ -3204,13 +3204,13 @@ var init_embedder = __esm({
3204
3204
  // src/lib/keychain.ts
3205
3205
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
3206
3206
  import { existsSync as existsSync7 } from "fs";
3207
- import path7 from "path";
3208
- import os5 from "os";
3207
+ import path8 from "path";
3208
+ import os6 from "os";
3209
3209
  function getKeyDir() {
3210
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path7.join(os5.homedir(), ".exe-os");
3210
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path8.join(os6.homedir(), ".exe-os");
3211
3211
  }
3212
3212
  function getKeyPath() {
3213
- return path7.join(getKeyDir(), "master.key");
3213
+ return path8.join(getKeyDir(), "master.key");
3214
3214
  }
3215
3215
  async function tryKeytar() {
3216
3216
  try {
@@ -3233,7 +3233,7 @@ async function getMasterKey() {
3233
3233
  const keyPath = getKeyPath();
3234
3234
  if (!existsSync7(keyPath)) {
3235
3235
  process.stderr.write(
3236
- `[keychain] Key not found at ${keyPath} (HOME=${os5.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
3236
+ `[keychain] Key not found at ${keyPath} (HOME=${os6.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
3237
3237
  `
3238
3238
  );
3239
3239
  return null;
@@ -3272,7 +3272,7 @@ __export(shard_manager_exports, {
3272
3272
  listShards: () => listShards,
3273
3273
  shardExists: () => shardExists
3274
3274
  });
3275
- import path8 from "path";
3275
+ import path9 from "path";
3276
3276
  import { existsSync as existsSync8, mkdirSync as mkdirSync2, readdirSync } from "fs";
3277
3277
  import { createClient as createClient2 } from "@libsql/client";
3278
3278
  function initShardManager(encryptionKey) {
@@ -3307,7 +3307,7 @@ function getShardClient(projectName) {
3307
3307
  while (_shards.size >= MAX_OPEN_SHARDS) {
3308
3308
  evictLRU();
3309
3309
  }
3310
- const dbPath = path8.join(SHARDS_DIR, `${safeName}.db`);
3310
+ const dbPath = path9.join(SHARDS_DIR, `${safeName}.db`);
3311
3311
  const client = createClient2({
3312
3312
  url: `file:${dbPath}`,
3313
3313
  encryptionKey: _encryptionKey
@@ -3318,7 +3318,7 @@ function getShardClient(projectName) {
3318
3318
  }
3319
3319
  function shardExists(projectName) {
3320
3320
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
3321
- return existsSync8(path8.join(SHARDS_DIR, `${safeName}.db`));
3321
+ return existsSync8(path9.join(SHARDS_DIR, `${safeName}.db`));
3322
3322
  }
3323
3323
  function listShards() {
3324
3324
  if (!existsSync8(SHARDS_DIR)) return [];
@@ -3568,7 +3568,7 @@ var init_shard_manager = __esm({
3568
3568
  "src/lib/shard-manager.ts"() {
3569
3569
  "use strict";
3570
3570
  init_config();
3571
- SHARDS_DIR = path8.join(EXE_AI_DIR, "shards");
3571
+ SHARDS_DIR = path9.join(EXE_AI_DIR, "shards");
3572
3572
  SHARD_IDLE_MS = 5 * 60 * 1e3;
3573
3573
  MAX_OPEN_SHARDS = 10;
3574
3574
  EVICTION_INTERVAL_MS = 60 * 1e3;
@@ -4346,8 +4346,8 @@ __export(wiki_client_exports, {
4346
4346
  listDocuments: () => listDocuments,
4347
4347
  listWorkspaces: () => listWorkspaces
4348
4348
  });
4349
- async function wikiFetch(config2, path23, method = "GET", body) {
4350
- const url = `${config2.baseUrl}/api/v1${path23}`;
4349
+ async function wikiFetch(config2, path24, method = "GET", body) {
4350
+ const url = `${config2.baseUrl}/api/v1${path24}`;
4351
4351
  const headers = {
4352
4352
  Authorization: `Bearer ${config2.apiKey}`,
4353
4353
  "Content-Type": "application/json"
@@ -4380,7 +4380,7 @@ async function wikiFetch(config2, path23, method = "GET", body) {
4380
4380
  }
4381
4381
  }
4382
4382
  if (!response.ok) {
4383
- throw new Error(`Wiki API ${method} ${path23}: ${response.status} ${response.statusText}`);
4383
+ throw new Error(`Wiki API ${method} ${path24}: ${response.status} ${response.statusText}`);
4384
4384
  }
4385
4385
  return response.json();
4386
4386
  } finally {
@@ -5411,10 +5411,10 @@ __export(license_exports, {
5411
5411
  });
5412
5412
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync3 } from "fs";
5413
5413
  import { randomUUID as randomUUID3 } from "crypto";
5414
- import { createRequire as createRequire2 } from "module";
5415
- import { pathToFileURL as pathToFileURL2 } from "url";
5416
- import os6 from "os";
5417
- import path9 from "path";
5414
+ import { createRequire as createRequire3 } from "module";
5415
+ import { pathToFileURL as pathToFileURL3 } from "url";
5416
+ import os7 from "os";
5417
+ import path10 from "path";
5418
5418
  import { jwtVerify, importSPKI } from "jose";
5419
5419
  async function fetchRetry(url, init) {
5420
5420
  try {
@@ -5425,7 +5425,7 @@ async function fetchRetry(url, init) {
5425
5425
  }
5426
5426
  }
5427
5427
  function loadDeviceId() {
5428
- const deviceJsonPath = path9.join(EXE_AI_DIR, "device.json");
5428
+ const deviceJsonPath = path10.join(EXE_AI_DIR, "device.json");
5429
5429
  try {
5430
5430
  if (existsSync9(deviceJsonPath)) {
5431
5431
  const data = JSON.parse(readFileSync6(deviceJsonPath, "utf8"));
@@ -5534,8 +5534,8 @@ function loadPrismaForLicense() {
5534
5534
  if (_prismaFailed) return null;
5535
5535
  const dbUrl = process.env.DATABASE_URL;
5536
5536
  if (!dbUrl) {
5537
- const exeDbRoot = process.env.EXE_DB_ROOT ?? path9.join(os6.homedir(), "exe-db");
5538
- if (!existsSync9(path9.join(exeDbRoot, "package.json"))) {
5537
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path10.join(os7.homedir(), "exe-db");
5538
+ if (!existsSync9(path10.join(exeDbRoot, "package.json"))) {
5539
5539
  _prismaFailed = true;
5540
5540
  return null;
5541
5541
  }
@@ -5544,15 +5544,15 @@ function loadPrismaForLicense() {
5544
5544
  _prismaPromise = (async () => {
5545
5545
  const explicitPath = process.env.EXE_OS_PRISMA_CLIENT_PATH;
5546
5546
  if (explicitPath) {
5547
- const mod2 = await import(pathToFileURL2(explicitPath).href);
5547
+ const mod2 = await import(pathToFileURL3(explicitPath).href);
5548
5548
  const Ctor2 = mod2.PrismaClient ?? mod2.default?.PrismaClient;
5549
5549
  if (!Ctor2) throw new Error(`No PrismaClient at ${explicitPath}`);
5550
5550
  return new Ctor2();
5551
5551
  }
5552
- const exeDbRoot = process.env.EXE_DB_ROOT ?? path9.join(os6.homedir(), "exe-db");
5553
- const req = createRequire2(path9.join(exeDbRoot, "package.json"));
5552
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path10.join(os7.homedir(), "exe-db");
5553
+ const req = createRequire3(path10.join(exeDbRoot, "package.json"));
5554
5554
  const entry = req.resolve("@prisma/client");
5555
- const mod = await import(pathToFileURL2(entry).href);
5555
+ const mod = await import(pathToFileURL3(entry).href);
5556
5556
  const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
5557
5557
  if (!Ctor) throw new Error(`No PrismaClient in ${entry}`);
5558
5558
  return new Ctor();
@@ -5664,7 +5664,7 @@ async function checkLicense() {
5664
5664
  let key = loadLicense();
5665
5665
  if (!key) {
5666
5666
  try {
5667
- const configPath = path9.join(EXE_AI_DIR, "config.json");
5667
+ const configPath = path10.join(EXE_AI_DIR, "config.json");
5668
5668
  if (existsSync9(configPath)) {
5669
5669
  const raw = JSON.parse(readFileSync6(configPath, "utf8"));
5670
5670
  const cloud = raw.cloud;
@@ -5825,9 +5825,9 @@ var init_license = __esm({
5825
5825
  "src/lib/license.ts"() {
5826
5826
  "use strict";
5827
5827
  init_config();
5828
- LICENSE_PATH = path9.join(EXE_AI_DIR, "license.key");
5829
- CACHE_PATH = path9.join(EXE_AI_DIR, "license-cache.json");
5830
- DEVICE_ID_PATH = path9.join(EXE_AI_DIR, "device-id");
5828
+ LICENSE_PATH = path10.join(EXE_AI_DIR, "license.key");
5829
+ CACHE_PATH = path10.join(EXE_AI_DIR, "license-cache.json");
5830
+ DEVICE_ID_PATH = path10.join(EXE_AI_DIR, "device-id");
5831
5831
  API_BASE = "https://askexe.com/cloud";
5832
5832
  RETRY_DELAY_MS = 500;
5833
5833
  LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
@@ -6727,16 +6727,16 @@ __export(imessage_exports, {
6727
6727
  });
6728
6728
  import { execFile } from "child_process";
6729
6729
  import { promisify } from "util";
6730
- import os7 from "os";
6731
- import path10 from "path";
6730
+ import os8 from "os";
6731
+ import path11 from "path";
6732
6732
  var execFileAsync, POLL_INTERVAL_MS, MESSAGES_DB_PATH, IMessageAdapter;
6733
6733
  var init_imessage = __esm({
6734
6734
  "src/gateway/adapters/imessage.ts"() {
6735
6735
  "use strict";
6736
6736
  execFileAsync = promisify(execFile);
6737
6737
  POLL_INTERVAL_MS = 5e3;
6738
- MESSAGES_DB_PATH = path10.join(
6739
- process.env.HOME ?? os7.homedir(),
6738
+ MESSAGES_DB_PATH = path11.join(
6739
+ process.env.HOME ?? os8.homedir(),
6740
6740
  "Library/Messages/chat.db"
6741
6741
  );
6742
6742
  IMessageAdapter = class {
@@ -7049,9 +7049,9 @@ __export(webhook_exports, {
7049
7049
  WebhookAdapter: () => WebhookAdapter
7050
7050
  });
7051
7051
  import { randomUUID as randomUUID7 } from "crypto";
7052
- function resolvePath(obj, path23) {
7052
+ function resolvePath(obj, path24) {
7053
7053
  let current = obj;
7054
- for (const segment of path23.split(".")) {
7054
+ for (const segment of path24.split(".")) {
7055
7055
  if (current == null || typeof current !== "object") return void 0;
7056
7056
  current = current[segment];
7057
7057
  }
@@ -7201,10 +7201,10 @@ var init_whatsapp_accounts = __esm({
7201
7201
 
7202
7202
  // src/lib/session-registry.ts
7203
7203
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
7204
- import path11 from "path";
7205
- import os8 from "os";
7204
+ import path12 from "path";
7205
+ import os9 from "os";
7206
7206
  function registerSession(entry) {
7207
- const dir = path11.dirname(REGISTRY_PATH);
7207
+ const dir = path12.dirname(REGISTRY_PATH);
7208
7208
  if (!existsSync10(dir)) {
7209
7209
  mkdirSync5(dir, { recursive: true });
7210
7210
  }
@@ -7229,7 +7229,7 @@ var REGISTRY_PATH;
7229
7229
  var init_session_registry = __esm({
7230
7230
  "src/lib/session-registry.ts"() {
7231
7231
  "use strict";
7232
- REGISTRY_PATH = path11.join(os8.homedir(), ".exe-os", "session-registry.json");
7232
+ REGISTRY_PATH = path12.join(os9.homedir(), ".exe-os", "session-registry.json");
7233
7233
  }
7234
7234
  });
7235
7235
 
@@ -7491,10 +7491,10 @@ __export(intercom_queue_exports, {
7491
7491
  readQueue: () => readQueue
7492
7492
  });
7493
7493
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, renameSync as renameSync3, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
7494
- import path12 from "path";
7495
- import os9 from "os";
7494
+ import path13 from "path";
7495
+ import os10 from "os";
7496
7496
  function ensureDir() {
7497
- const dir = path12.dirname(QUEUE_PATH);
7497
+ const dir = path13.dirname(QUEUE_PATH);
7498
7498
  if (!existsSync11(dir)) mkdirSync6(dir, { recursive: true });
7499
7499
  }
7500
7500
  function readQueue() {
@@ -7600,16 +7600,16 @@ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
7600
7600
  var init_intercom_queue = __esm({
7601
7601
  "src/lib/intercom-queue.ts"() {
7602
7602
  "use strict";
7603
- QUEUE_PATH = path12.join(os9.homedir(), ".exe-os", "intercom-queue.json");
7603
+ QUEUE_PATH = path13.join(os10.homedir(), ".exe-os", "intercom-queue.json");
7604
7604
  MAX_RETRIES2 = 5;
7605
7605
  TTL_MS = 60 * 60 * 1e3;
7606
- INTERCOM_LOG = path12.join(os9.homedir(), ".exe-os", "intercom.log");
7606
+ INTERCOM_LOG = path13.join(os10.homedir(), ".exe-os", "intercom.log");
7607
7607
  }
7608
7608
  });
7609
7609
 
7610
7610
  // src/lib/plan-limits.ts
7611
7611
  import { readFileSync as readFileSync10, existsSync as existsSync12 } from "fs";
7612
- import path13 from "path";
7612
+ import path14 from "path";
7613
7613
  function getLicenseSync() {
7614
7614
  try {
7615
7615
  if (!existsSync12(CACHE_PATH2)) return freeLicense();
@@ -7681,7 +7681,7 @@ var init_plan_limits = __esm({
7681
7681
  this.name = "PlanLimitError";
7682
7682
  }
7683
7683
  };
7684
- CACHE_PATH2 = path13.join(EXE_AI_DIR, "license-cache.json");
7684
+ CACHE_PATH2 = path14.join(EXE_AI_DIR, "license-cache.json");
7685
7685
  }
7686
7686
  });
7687
7687
 
@@ -7720,8 +7720,8 @@ var init_task_scope = __esm({
7720
7720
 
7721
7721
  // src/lib/notifications.ts
7722
7722
  import crypto4 from "crypto";
7723
- import path14 from "path";
7724
- import os10 from "os";
7723
+ import path15 from "path";
7724
+ import os11 from "os";
7725
7725
  import {
7726
7726
  readFileSync as readFileSync11,
7727
7727
  readdirSync as readdirSync2,
@@ -7811,7 +7811,7 @@ var init_session_kill_telemetry = __esm({
7811
7811
 
7812
7812
  // src/lib/project-name.ts
7813
7813
  import { execSync as execSync4 } from "child_process";
7814
- import path15 from "path";
7814
+ import path16 from "path";
7815
7815
  function getProjectName(cwd) {
7816
7816
  const dir = cwd ?? process.cwd();
7817
7817
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -7824,7 +7824,7 @@ function getProjectName(cwd) {
7824
7824
  timeout: 2e3,
7825
7825
  stdio: ["pipe", "pipe", "pipe"]
7826
7826
  }).trim();
7827
- repoRoot = path15.dirname(gitCommonDir);
7827
+ repoRoot = path16.dirname(gitCommonDir);
7828
7828
  } catch {
7829
7829
  repoRoot = execSync4("git rev-parse --show-toplevel", {
7830
7830
  cwd: dir,
@@ -7833,11 +7833,11 @@ function getProjectName(cwd) {
7833
7833
  stdio: ["pipe", "pipe", "pipe"]
7834
7834
  }).trim();
7835
7835
  }
7836
- _cached2 = path15.basename(repoRoot);
7836
+ _cached2 = path16.basename(repoRoot);
7837
7837
  _cachedCwd = dir;
7838
7838
  return _cached2;
7839
7839
  } catch {
7840
- _cached2 = path15.basename(dir);
7840
+ _cached2 = path16.basename(dir);
7841
7841
  _cachedCwd = dir;
7842
7842
  return _cached2;
7843
7843
  }
@@ -7915,8 +7915,8 @@ var init_session_scope = __esm({
7915
7915
 
7916
7916
  // src/lib/tasks-crud.ts
7917
7917
  import crypto6 from "crypto";
7918
- import path16 from "path";
7919
- import os11 from "os";
7918
+ import path17 from "path";
7919
+ import os12 from "os";
7920
7920
  import { execSync as execSync5 } from "child_process";
7921
7921
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
7922
7922
  import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
@@ -8113,8 +8113,8 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
8113
8113
  }
8114
8114
  if (input.baseDir) {
8115
8115
  try {
8116
- await mkdir4(path16.join(input.baseDir, "exe", "output"), { recursive: true });
8117
- await mkdir4(path16.join(input.baseDir, "exe", "research"), { recursive: true });
8116
+ await mkdir4(path17.join(input.baseDir, "exe", "output"), { recursive: true });
8117
+ await mkdir4(path17.join(input.baseDir, "exe", "research"), { recursive: true });
8118
8118
  await ensureArchitectureDoc(input.baseDir, input.projectName);
8119
8119
  await ensureGitignoreExe(input.baseDir);
8120
8120
  } catch {
@@ -8150,9 +8150,9 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
8150
8150
  });
8151
8151
  if (input.baseDir) {
8152
8152
  try {
8153
- const EXE_OS_DIR = path16.join(os11.homedir(), ".exe-os");
8154
- const mdPath = path16.join(EXE_OS_DIR, taskFile);
8155
- const mdDir = path16.dirname(mdPath);
8153
+ const EXE_OS_DIR = path17.join(os12.homedir(), ".exe-os");
8154
+ const mdPath = path17.join(EXE_OS_DIR, taskFile);
8155
+ const mdDir = path17.dirname(mdPath);
8156
8156
  if (!existsSync14(mdDir)) await mkdir4(mdDir, { recursive: true });
8157
8157
  const reviewer = input.reviewer ?? input.assignedBy;
8158
8158
  const mdContent = `# ${input.title}
@@ -8453,7 +8453,7 @@ async function deleteTaskCore(taskId, _baseDir) {
8453
8453
  return { taskFile, assignedTo, assignedBy, taskSlug };
8454
8454
  }
8455
8455
  async function ensureArchitectureDoc(baseDir, projectName) {
8456
- const archPath = path16.join(baseDir, "exe", "ARCHITECTURE.md");
8456
+ const archPath = path17.join(baseDir, "exe", "ARCHITECTURE.md");
8457
8457
  try {
8458
8458
  if (existsSync14(archPath)) return;
8459
8459
  const template = [
@@ -8488,7 +8488,7 @@ async function ensureArchitectureDoc(baseDir, projectName) {
8488
8488
  }
8489
8489
  }
8490
8490
  async function ensureGitignoreExe(baseDir) {
8491
- const gitignorePath = path16.join(baseDir, ".gitignore");
8491
+ const gitignorePath = path17.join(baseDir, ".gitignore");
8492
8492
  try {
8493
8493
  if (existsSync14(gitignorePath)) {
8494
8494
  const content = readFileSync12(gitignorePath, "utf-8");
@@ -8522,8 +8522,35 @@ var init_tasks_crud = __esm({
8522
8522
  });
8523
8523
 
8524
8524
  // src/lib/tasks-review.ts
8525
- import path17 from "path";
8525
+ var tasks_review_exports = {};
8526
+ __export(tasks_review_exports, {
8527
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
8528
+ cleanupReviewFile: () => cleanupReviewFile,
8529
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
8530
+ countPendingReviews: () => countPendingReviews,
8531
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
8532
+ formatAge: () => formatAge,
8533
+ getReviewChecklist: () => getReviewChecklist,
8534
+ isStale: () => isStale,
8535
+ listPendingReviews: () => listPendingReviews
8536
+ });
8537
+ import path18 from "path";
8526
8538
  import { existsSync as existsSync15, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
8539
+ function formatAge(isoTimestamp) {
8540
+ if (!isoTimestamp) return "";
8541
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
8542
+ if (ms < 0) return "just now";
8543
+ const minutes = Math.floor(ms / 6e4);
8544
+ if (minutes < 60) return `${minutes}m ago`;
8545
+ const hours = Math.floor(minutes / 60);
8546
+ if (hours < 24) return `${hours}h ago`;
8547
+ const days = Math.floor(hours / 24);
8548
+ return `${days}d ago`;
8549
+ }
8550
+ function isStale(isoTimestamp) {
8551
+ if (!isoTimestamp) return false;
8552
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
8553
+ }
8527
8554
  async function countPendingReviews(sessionScope) {
8528
8555
  const client = getClient();
8529
8556
  const scope = strictSessionScopeFilter(
@@ -8644,6 +8671,95 @@ function getReviewChecklist(role, agent, taskSlug) {
8644
8671
  ]
8645
8672
  };
8646
8673
  }
8674
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
8675
+ const taskFile = String(row.task_file);
8676
+ const employees = await loadEmployees();
8677
+ const coordinatorName = getCoordinatorName(employees);
8678
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
8679
+ if (String(row.title).startsWith("Review:")) return;
8680
+ const fileName = taskFile.split("/").pop() ?? "";
8681
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
8682
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
8683
+ const client = getClient();
8684
+ const agent = String(row.assigned_to);
8685
+ const rawReviewer = row.reviewer || row.assigned_by;
8686
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
8687
+ const currentStatus = String(row.status ?? "");
8688
+ if (currentStatus === "done") return;
8689
+ const existingResult = String(row.result ?? "");
8690
+ if (existingResult.includes("## Review notes")) return;
8691
+ let reviewerRole = "unknown";
8692
+ try {
8693
+ const emp = getEmployee(employees, reviewer);
8694
+ if (emp) reviewerRole = emp.role;
8695
+ } catch {
8696
+ }
8697
+ const taskTitle = String(row.title);
8698
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
8699
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
8700
+ process.stderr.write(
8701
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
8702
+ `
8703
+ );
8704
+ const reviewNotes = [
8705
+ `
8706
+ ---
8707
+ ## Review notes`,
8708
+ `Review lens: ${lens}`,
8709
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
8710
+ "",
8711
+ "### Checklist",
8712
+ ...checklist,
8713
+ "",
8714
+ "### Verdict",
8715
+ "- **Approved:** mark this task as done",
8716
+ "- **Needs work:** re-open with notes"
8717
+ ].join("\n");
8718
+ const originalTaskId = String(row.id);
8719
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
8720
+ await client.execute({
8721
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
8722
+ WHERE id = ?`,
8723
+ args: [updatedResult, now, originalTaskId]
8724
+ });
8725
+ orgBus.emit({
8726
+ type: "review_created",
8727
+ reviewId: originalTaskId,
8728
+ employee: agent,
8729
+ reviewer,
8730
+ timestamp: now
8731
+ });
8732
+ await writeNotification({
8733
+ agentId: agent,
8734
+ agentRole: String(row.assigned_to),
8735
+ event: "task_complete",
8736
+ project: String(row.project_name),
8737
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
8738
+ taskFile
8739
+ });
8740
+ const originalPriority = String(row.priority).toLowerCase();
8741
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
8742
+ if (!autoApprove) {
8743
+ try {
8744
+ const key = getSessionKey();
8745
+ const exeSession = getParentExe(key);
8746
+ if (exeSession) {
8747
+ sendIntercom(exeSession);
8748
+ }
8749
+ } catch {
8750
+ }
8751
+ }
8752
+ if (autoApprove) {
8753
+ process.stderr.write(
8754
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
8755
+ `
8756
+ );
8757
+ await client.execute({
8758
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
8759
+ args: [now, originalTaskId]
8760
+ });
8761
+ }
8762
+ }
8647
8763
  async function cleanupReviewFile(row, taskFile, _baseDir) {
8648
8764
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
8649
8765
  try {
@@ -8688,11 +8804,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
8688
8804
  );
8689
8805
  }
8690
8806
  try {
8691
- const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
8807
+ const cacheDir = path18.join(EXE_AI_DIR, "session-cache");
8692
8808
  if (existsSync15(cacheDir)) {
8693
8809
  for (const f of readdirSync3(cacheDir)) {
8694
8810
  if (f.startsWith("review-notified-")) {
8695
- unlinkSync4(path17.join(cacheDir, f));
8811
+ unlinkSync4(path18.join(cacheDir, f));
8696
8812
  }
8697
8813
  }
8698
8814
  }
@@ -8714,7 +8830,7 @@ var init_tasks_review = __esm({
8714
8830
  });
8715
8831
 
8716
8832
  // src/lib/tasks-chain.ts
8717
- import path18 from "path";
8833
+ import path19 from "path";
8718
8834
  import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
8719
8835
  async function cascadeUnblock(taskId, baseDir, now) {
8720
8836
  const client = getClient();
@@ -8731,7 +8847,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
8731
8847
  });
8732
8848
  for (const ur of unblockedRows.rows) {
8733
8849
  try {
8734
- const ubFile = path18.join(baseDir, String(ur.task_file));
8850
+ const ubFile = path19.join(baseDir, String(ur.task_file));
8735
8851
  let ubContent = await readFile4(ubFile, "utf-8");
8736
8852
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
8737
8853
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -9195,7 +9311,7 @@ __export(tasks_exports, {
9195
9311
  updateTaskStatus: () => updateTaskStatus,
9196
9312
  writeCheckpoint: () => writeCheckpoint
9197
9313
  });
9198
- import path19 from "path";
9314
+ import path20 from "path";
9199
9315
  import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
9200
9316
  async function createTask(input) {
9201
9317
  const result = await createTaskCore(input);
@@ -9215,8 +9331,8 @@ async function updateTask(input) {
9215
9331
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
9216
9332
  try {
9217
9333
  const agent = String(row.assigned_to);
9218
- const cacheDir = path19.join(EXE_AI_DIR, "session-cache");
9219
- const cachePath = path19.join(cacheDir, `current-task-${agent}.json`);
9334
+ const cacheDir = path20.join(EXE_AI_DIR, "session-cache");
9335
+ const cachePath = path20.join(cacheDir, `current-task-${agent}.json`);
9220
9336
  if (input.status === "in_progress") {
9221
9337
  mkdirSync7(cacheDir, { recursive: true });
9222
9338
  writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
@@ -9688,12 +9804,12 @@ __export(tmux_routing_exports, {
9688
9804
  });
9689
9805
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
9690
9806
  import { readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync16, appendFileSync, readdirSync as readdirSync4 } from "fs";
9691
- import path20 from "path";
9692
- import os12 from "os";
9807
+ import path21 from "path";
9808
+ import os13 from "os";
9693
9809
  import { fileURLToPath as fileURLToPath2 } from "url";
9694
9810
  import { unlinkSync as unlinkSync6 } from "fs";
9695
9811
  function spawnLockPath(sessionName) {
9696
- return path20.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
9812
+ return path21.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
9697
9813
  }
9698
9814
  function isProcessAlive(pid) {
9699
9815
  try {
@@ -9730,8 +9846,8 @@ function releaseSpawnLock2(sessionName) {
9730
9846
  function resolveBehaviorsExporterScript() {
9731
9847
  try {
9732
9848
  const thisFile = fileURLToPath2(import.meta.url);
9733
- const scriptPath = path20.join(
9734
- path20.dirname(thisFile),
9849
+ const scriptPath = path21.join(
9850
+ path21.dirname(thisFile),
9735
9851
  "..",
9736
9852
  "bin",
9737
9853
  "exe-export-behaviors.js"
@@ -9806,7 +9922,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
9806
9922
  mkdirSync8(SESSION_CACHE, { recursive: true });
9807
9923
  }
9808
9924
  const rootExe = extractRootExe(parentExe) ?? parentExe;
9809
- const filePath = path20.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
9925
+ const filePath = path21.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
9810
9926
  writeFileSync8(filePath, JSON.stringify({
9811
9927
  parentExe: rootExe,
9812
9928
  dispatchedBy: dispatchedBy || rootExe,
@@ -9815,7 +9931,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
9815
9931
  }
9816
9932
  function getParentExe(sessionKey) {
9817
9933
  try {
9818
- const data = JSON.parse(readFileSync13(path20.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
9934
+ const data = JSON.parse(readFileSync13(path21.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
9819
9935
  return data.parentExe || null;
9820
9936
  } catch {
9821
9937
  return null;
@@ -9824,7 +9940,7 @@ function getParentExe(sessionKey) {
9824
9940
  function getDispatchedBy(sessionKey) {
9825
9941
  try {
9826
9942
  const data = JSON.parse(readFileSync13(
9827
- path20.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
9943
+ path21.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
9828
9944
  "utf8"
9829
9945
  ));
9830
9946
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -10007,20 +10123,22 @@ function sendIntercom(targetSession) {
10007
10123
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
10008
10124
  return "queued";
10009
10125
  }
10010
- try {
10011
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
10012
- const agent = baseAgentName(rawAgent);
10013
- const markerPath = path20.join(SESSION_CACHE, `current-task-${agent}.json`);
10014
- if (existsSync16(markerPath)) {
10015
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
10016
- return "debounced";
10126
+ if (sessionState !== "idle") {
10127
+ try {
10128
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
10129
+ const agent = baseAgentName(rawAgent);
10130
+ const markerPath = path21.join(SESSION_CACHE, `current-task-${agent}.json`);
10131
+ if (existsSync16(markerPath)) {
10132
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
10133
+ return "debounced";
10134
+ }
10135
+ } catch {
10017
10136
  }
10018
- } catch {
10019
10137
  }
10020
10138
  try {
10021
10139
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
10022
10140
  const agent = baseAgentName(rawAgent);
10023
- const taskDir = path20.join(process.cwd(), "exe", agent);
10141
+ const taskDir = path21.join(process.cwd(), "exe", agent);
10024
10142
  if (existsSync16(taskDir)) {
10025
10143
  const files = readdirSync4(taskDir).filter(
10026
10144
  (f) => f.endsWith(".md") && f !== "DONE.txt"
@@ -10086,6 +10204,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
10086
10204
  try {
10087
10205
  const sessions = transport.listSessions();
10088
10206
  if (!sessions.includes(coordinatorSession)) return false;
10207
+ try {
10208
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
10209
+ const pending = countPendingReviews2(coordinatorSession);
10210
+ if (pending instanceof Promise) {
10211
+ pending.then((count) => {
10212
+ if (count > 0) {
10213
+ execSync6(
10214
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
10215
+ { timeout: 3e3 }
10216
+ );
10217
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
10218
+ }
10219
+ }).catch(() => {
10220
+ });
10221
+ return true;
10222
+ }
10223
+ } catch {
10224
+ }
10089
10225
  execSync6(
10090
10226
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
10091
10227
  { timeout: 3e3 }
@@ -10169,8 +10305,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
10169
10305
  const transport = getTransport();
10170
10306
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
10171
10307
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
10172
- const logDir = path20.join(os12.homedir(), ".exe-os", "session-logs");
10173
- const logFile = path20.join(logDir, `${instanceLabel}-${Date.now()}.log`);
10308
+ const logDir = path21.join(os13.homedir(), ".exe-os", "session-logs");
10309
+ const logFile = path21.join(logDir, `${instanceLabel}-${Date.now()}.log`);
10174
10310
  if (!existsSync16(logDir)) {
10175
10311
  mkdirSync8(logDir, { recursive: true });
10176
10312
  }
@@ -10178,14 +10314,14 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
10178
10314
  let cleanupSuffix = "";
10179
10315
  try {
10180
10316
  const thisFile = fileURLToPath2(import.meta.url);
10181
- const cleanupScript = path20.join(path20.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
10317
+ const cleanupScript = path21.join(path21.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
10182
10318
  if (existsSync16(cleanupScript)) {
10183
10319
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
10184
10320
  }
10185
10321
  } catch {
10186
10322
  }
10187
10323
  try {
10188
- const claudeJsonPath = path20.join(os12.homedir(), ".claude.json");
10324
+ const claudeJsonPath = path21.join(os13.homedir(), ".claude.json");
10189
10325
  let claudeJson = {};
10190
10326
  try {
10191
10327
  claudeJson = JSON.parse(readFileSync13(claudeJsonPath, "utf8"));
@@ -10200,10 +10336,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
10200
10336
  } catch {
10201
10337
  }
10202
10338
  try {
10203
- const settingsDir = path20.join(os12.homedir(), ".claude", "projects");
10339
+ const settingsDir = path21.join(os13.homedir(), ".claude", "projects");
10204
10340
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
10205
- const projSettingsDir = path20.join(settingsDir, normalizedKey);
10206
- const settingsPath = path20.join(projSettingsDir, "settings.json");
10341
+ const projSettingsDir = path21.join(settingsDir, normalizedKey);
10342
+ const settingsPath = path21.join(projSettingsDir, "settings.json");
10207
10343
  let settings = {};
10208
10344
  try {
10209
10345
  settings = JSON.parse(readFileSync13(settingsPath, "utf8"));
@@ -10250,8 +10386,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
10250
10386
  let behaviorsFlag = "";
10251
10387
  let legacyFallbackWarned = false;
10252
10388
  if (!useExeAgent && !useBinSymlink) {
10253
- const identityPath = path20.join(
10254
- os12.homedir(),
10389
+ const identityPath = path21.join(
10390
+ os13.homedir(),
10255
10391
  ".exe-os",
10256
10392
  "identity",
10257
10393
  `${employeeName}.md`
@@ -10266,7 +10402,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
10266
10402
  }
10267
10403
  const behaviorsFile = exportBehaviorsSync(
10268
10404
  employeeName,
10269
- path20.basename(spawnCwd),
10405
+ path21.basename(spawnCwd),
10270
10406
  sessionName
10271
10407
  );
10272
10408
  if (behaviorsFile) {
@@ -10281,9 +10417,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
10281
10417
  }
10282
10418
  let sessionContextFlag = "";
10283
10419
  try {
10284
- const ctxDir = path20.join(os12.homedir(), ".exe-os", "session-cache");
10420
+ const ctxDir = path21.join(os13.homedir(), ".exe-os", "session-cache");
10285
10421
  mkdirSync8(ctxDir, { recursive: true });
10286
- const ctxFile = path20.join(ctxDir, `session-context-${sessionName}.md`);
10422
+ const ctxFile = path21.join(ctxDir, `session-context-${sessionName}.md`);
10287
10423
  const ctxContent = [
10288
10424
  `## Session Context`,
10289
10425
  `You are running in tmux session: ${sessionName}.`,
@@ -10367,7 +10503,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
10367
10503
  transport.pipeLog(sessionName, logFile);
10368
10504
  try {
10369
10505
  const mySession = getMySession();
10370
- const dispatchInfo = path20.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
10506
+ const dispatchInfo = path21.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
10371
10507
  writeFileSync8(dispatchInfo, JSON.stringify({
10372
10508
  dispatchedBy: mySession,
10373
10509
  rootExe: exeSession,
@@ -10442,15 +10578,15 @@ var init_tmux_routing = __esm({
10442
10578
  init_intercom_queue();
10443
10579
  init_plan_limits();
10444
10580
  init_employees();
10445
- SPAWN_LOCK_DIR = path20.join(os12.homedir(), ".exe-os", "spawn-locks");
10446
- SESSION_CACHE = path20.join(os12.homedir(), ".exe-os", "session-cache");
10581
+ SPAWN_LOCK_DIR = path21.join(os13.homedir(), ".exe-os", "spawn-locks");
10582
+ SESSION_CACHE = path21.join(os13.homedir(), ".exe-os", "session-cache");
10447
10583
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
10448
10584
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
10449
10585
  VERIFY_PANE_LINES = 200;
10450
10586
  INTERCOM_DEBOUNCE_MS = 3e4;
10451
10587
  CODEX_DEBOUNCE_MS = 12e4;
10452
- INTERCOM_LOG2 = path20.join(os12.homedir(), ".exe-os", "intercom.log");
10453
- DEBOUNCE_FILE = path20.join(SESSION_CACHE, "intercom-debounce.json");
10588
+ INTERCOM_LOG2 = path21.join(os13.homedir(), ".exe-os", "intercom.log");
10589
+ DEBOUNCE_FILE = path21.join(SESSION_CACHE, "intercom-debounce.json");
10454
10590
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
10455
10591
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
10456
10592
  }
@@ -10732,8 +10868,8 @@ var init_messaging = __esm({
10732
10868
  // src/automation/trigger-engine.ts
10733
10869
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync9, existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
10734
10870
  import { randomUUID as randomUUID8 } from "crypto";
10735
- import path21 from "path";
10736
- import os13 from "os";
10871
+ import path22 from "path";
10872
+ import os14 from "os";
10737
10873
  function substituteTemplate(template, record) {
10738
10874
  return template.replace(
10739
10875
  /\{\{(\w+(?:\.\w+)*)\}\}/g,
@@ -11028,7 +11164,7 @@ var TRIGGERS_PATH, GRAPH_API_VERSION;
11028
11164
  var init_trigger_engine = __esm({
11029
11165
  "src/automation/trigger-engine.ts"() {
11030
11166
  "use strict";
11031
- TRIGGERS_PATH = path21.join(os13.homedir(), ".exe-os", "triggers.json");
11167
+ TRIGGERS_PATH = path22.join(os14.homedir(), ".exe-os", "triggers.json");
11032
11168
  GRAPH_API_VERSION = "v21.0";
11033
11169
  }
11034
11170
  });
@@ -11092,18 +11228,215 @@ var init_crm_webhook = __esm({
11092
11228
 
11093
11229
  // src/bin/exe-gateway.ts
11094
11230
  import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
11095
- import path22 from "path";
11096
- import os14 from "os";
11231
+ import path23 from "path";
11232
+ import os15 from "os";
11097
11233
 
11098
11234
  // src/gateway/webhook-server.ts
11099
11235
  import {
11100
11236
  createServer
11101
11237
  } from "http";
11238
+
11239
+ // src/gateway/query-handler.ts
11240
+ import os from "os";
11241
+ import path from "path";
11242
+ import { createRequire } from "module";
11243
+ import { pathToFileURL } from "url";
11244
+ var DEFAULT_LIMIT = 20;
11245
+ var MAX_LIMIT = 100;
11246
+ var DEFAULT_SCOPE = "all";
11247
+ var ALL_SCOPES = ["raw", "wiki", "memory", "gateway", "all"];
11248
+ var SCOPE_ORDER = ["raw", "memory", "wiki", "gateway"];
11249
+ var UNKNOWN_TIMESTAMP = (/* @__PURE__ */ new Date(0)).toISOString();
11250
+ var prismaPromise = null;
11251
+ function loadPrisma() {
11252
+ if (!prismaPromise) {
11253
+ prismaPromise = (async () => {
11254
+ const explicitPath = process.env.EXE_OS_PRISMA_CLIENT_PATH;
11255
+ if (explicitPath) {
11256
+ const mod2 = await import(pathToFileURL(explicitPath).href);
11257
+ const Ctor2 = mod2.PrismaClient ?? mod2.default?.PrismaClient;
11258
+ if (!Ctor2) throw new Error(`No PrismaClient at ${explicitPath}`);
11259
+ return new Ctor2();
11260
+ }
11261
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path.join(os.homedir(), "exe-db");
11262
+ const req = createRequire(path.join(exeDbRoot, "package.json"));
11263
+ const entry = req.resolve("@prisma/client");
11264
+ const mod = await import(pathToFileURL(entry).href);
11265
+ const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
11266
+ if (!Ctor) throw new Error(`No PrismaClient in ${entry}`);
11267
+ return new Ctor();
11268
+ })();
11269
+ }
11270
+ return prismaPromise;
11271
+ }
11272
+ var QueryValidationError = class extends Error {
11273
+ constructor(message) {
11274
+ super(message);
11275
+ this.name = "QueryValidationError";
11276
+ }
11277
+ };
11278
+ var QueryHandler = class {
11279
+ constructor(getPrisma = loadPrisma) {
11280
+ this.getPrisma = getPrisma;
11281
+ }
11282
+ parseParams(input) {
11283
+ const url = typeof input === "string" ? new URL(input, "http://localhost") : input;
11284
+ const q = url.searchParams.get("q")?.trim() ?? "";
11285
+ if (!q) {
11286
+ throw new QueryValidationError("Query parameter 'q' is required");
11287
+ }
11288
+ const scope = url.searchParams.get("scope")?.trim().toLowerCase() ?? DEFAULT_SCOPE;
11289
+ if (!ALL_SCOPES.includes(scope)) {
11290
+ throw new QueryValidationError("Invalid scope");
11291
+ }
11292
+ const rawLimit = url.searchParams.get("limit");
11293
+ const parsedLimit = rawLimit ? Number.parseInt(rawLimit, 10) : DEFAULT_LIMIT;
11294
+ const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, MAX_LIMIT) : DEFAULT_LIMIT;
11295
+ const source = url.searchParams.get("source")?.trim() || void 0;
11296
+ return { q, scope, source, limit };
11297
+ }
11298
+ async query(params) {
11299
+ const prisma = await this.getPrisma();
11300
+ const scopes = params.scope === "all" ? SCOPE_ORDER : [params.scope];
11301
+ const searchTerm = `%${params.q}%`;
11302
+ const results = (await Promise.all(
11303
+ scopes.map((scope) => this.queryScope(prisma, scope, searchTerm, params))
11304
+ )).flat();
11305
+ return {
11306
+ results,
11307
+ count: results.length,
11308
+ scopes_searched: scopes
11309
+ };
11310
+ }
11311
+ async queryScope(prisma, scope, searchTerm, params) {
11312
+ switch (scope) {
11313
+ case "raw":
11314
+ return this.queryRaw(prisma, searchTerm, params.source, params.limit);
11315
+ case "memory":
11316
+ return this.queryMemory(prisma, searchTerm, params.limit);
11317
+ case "wiki":
11318
+ return this.queryWiki(prisma, searchTerm, params.limit);
11319
+ case "gateway":
11320
+ return this.queryGateway(prisma, searchTerm, params.limit);
11321
+ }
11322
+ }
11323
+ async queryRaw(prisma, searchTerm, source, limit) {
11324
+ const rows = await prisma.$queryRawUnsafe(
11325
+ `SELECT "id", "source", "event_type", substring("payload"::text, 1, 200) as snippet, "timestamp"
11326
+ FROM "raw"."raw_events"
11327
+ WHERE ("payload"::text ILIKE $1 OR "metadata"::text ILIKE $1)
11328
+ AND ($2::text IS NULL OR "source" = $2)
11329
+ ORDER BY "timestamp" DESC
11330
+ LIMIT $3`,
11331
+ searchTerm,
11332
+ source ?? null,
11333
+ limit
11334
+ );
11335
+ return rows.map((row) => ({
11336
+ scope: "raw",
11337
+ type: "event",
11338
+ snippet: sanitizeSnippet(row.snippet),
11339
+ timestamp: toIsoTimestamp(row.timestamp),
11340
+ metadata: {
11341
+ id: row.id,
11342
+ source: row.source,
11343
+ event_type: row.event_type
11344
+ }
11345
+ }));
11346
+ }
11347
+ async queryMemory(prisma, searchTerm, limit) {
11348
+ const rows = await prisma.$queryRawUnsafe(
11349
+ `SELECT "id", "agent_id", "tool_name", substring("raw_text", 1, 200) as snippet, "timestamp", "project_name"
11350
+ FROM "memory"."memory_records"
11351
+ WHERE "raw_text" ILIKE $1 AND "status" = 'active'
11352
+ ORDER BY "timestamp" DESC
11353
+ LIMIT $2`,
11354
+ searchTerm,
11355
+ limit
11356
+ );
11357
+ return rows.map((row) => ({
11358
+ scope: "memory",
11359
+ type: "memory",
11360
+ snippet: sanitizeSnippet(row.snippet),
11361
+ timestamp: toIsoTimestamp(row.timestamp),
11362
+ metadata: {
11363
+ id: row.id,
11364
+ agent_id: row.agent_id,
11365
+ tool_name: row.tool_name,
11366
+ project_name: row.project_name
11367
+ }
11368
+ }));
11369
+ }
11370
+ async queryWiki(prisma, searchTerm, limit) {
11371
+ const rows = await prisma.$queryRawUnsafe(
11372
+ `SELECT d."doc_id", d."filename", d."docpath", w."name" as workspace_name
11373
+ FROM "wiki"."workspace_documents" d
11374
+ JOIN "wiki"."workspaces" w ON w."id" = d."workspace_id"
11375
+ WHERE d."filename" ILIKE $1 OR d."docpath" ILIKE $1
11376
+ LIMIT $2`,
11377
+ searchTerm,
11378
+ limit
11379
+ );
11380
+ return rows.map((row) => ({
11381
+ scope: "wiki",
11382
+ type: "document",
11383
+ snippet: sanitizeSnippet([row.filename, row.docpath].filter(Boolean).join(" \u2014 ")),
11384
+ timestamp: UNKNOWN_TIMESTAMP,
11385
+ metadata: {
11386
+ doc_id: row.doc_id,
11387
+ filename: row.filename,
11388
+ docpath: row.docpath,
11389
+ workspace_name: row.workspace_name
11390
+ }
11391
+ }));
11392
+ }
11393
+ async queryGateway(prisma, searchTerm, limit) {
11394
+ const rows = await prisma.$queryRawUnsafe(
11395
+ `SELECT m."id", m."text", m."from_jid", m."timestamp", c."display_name", c."platform"
11396
+ FROM "gateway"."messages" m
11397
+ JOIN "gateway"."threads" t ON t."id" = m."thread_id"
11398
+ JOIN "gateway"."contacts" c ON c."id" = t."contact_id"
11399
+ WHERE m."text" ILIKE $1
11400
+ ORDER BY m."timestamp" DESC
11401
+ LIMIT $2`,
11402
+ searchTerm,
11403
+ limit
11404
+ );
11405
+ return rows.map((row) => ({
11406
+ scope: "gateway",
11407
+ type: "message",
11408
+ snippet: sanitizeSnippet(row.text),
11409
+ timestamp: toIsoTimestamp(row.timestamp),
11410
+ metadata: {
11411
+ id: row.id,
11412
+ from_jid: row.from_jid,
11413
+ display_name: row.display_name,
11414
+ platform: row.platform
11415
+ }
11416
+ }));
11417
+ }
11418
+ };
11419
+ function sanitizeSnippet(value) {
11420
+ return String(value ?? "").slice(0, 200);
11421
+ }
11422
+ function toIsoTimestamp(value) {
11423
+ if (value instanceof Date) return value.toISOString();
11424
+ if (typeof value === "string" || typeof value === "number") {
11425
+ const parsed = new Date(value);
11426
+ if (!Number.isNaN(parsed.getTime())) {
11427
+ return parsed.toISOString();
11428
+ }
11429
+ }
11430
+ return UNKNOWN_TIMESTAMP;
11431
+ }
11432
+
11433
+ // src/gateway/webhook-server.ts
11102
11434
  var DEFAULT_HOST = process.env.EXE_WEBHOOK_HOST || "127.0.0.1";
11103
11435
  var BODY_SIZE_LIMIT = 1048576;
11104
11436
  var WebhookServer = class {
11105
- constructor(config2) {
11437
+ constructor(config2, queryHandler = new QueryHandler()) {
11106
11438
  this.config = config2;
11439
+ this.queryHandler = queryHandler;
11107
11440
  if (process.env.NODE_ENV === "production" && !config2.authToken) {
11108
11441
  throw new Error(
11109
11442
  "[webhook-server] authToken is required in production. Set it in ~/.exe-os/gateway.json or pass it in WebhookServerConfig."
@@ -11155,6 +11488,10 @@ var WebhookServer = class {
11155
11488
  this.handleHealth(res);
11156
11489
  return;
11157
11490
  }
11491
+ if (method === "GET" && isRoute(url, "/query")) {
11492
+ await this.handleQuery(req, res);
11493
+ return;
11494
+ }
11158
11495
  if (method === "GET" && url.startsWith("/webhook/whatsapp")) {
11159
11496
  this.handleWhatsAppVerification(req, res);
11160
11497
  return;
@@ -11173,6 +11510,31 @@ var WebhookServer = class {
11173
11510
  handlers: [...this.handlers.keys()]
11174
11511
  });
11175
11512
  }
11513
+ async handleQuery(req, res) {
11514
+ if (this.config.authToken && !this.verifyAuth(req)) {
11515
+ sendJson(res, 401, { error: "Unauthorized" });
11516
+ return;
11517
+ }
11518
+ try {
11519
+ const url = new URL(
11520
+ req.url ?? "/query",
11521
+ `http://${req.headers.host ?? "localhost"}`
11522
+ );
11523
+ const params = this.queryHandler.parseParams(url);
11524
+ const result = await this.queryHandler.query(params);
11525
+ sendJson(res, 200, result);
11526
+ } catch (err) {
11527
+ if (err instanceof QueryValidationError) {
11528
+ sendJson(res, 400, { error: err.message });
11529
+ return;
11530
+ }
11531
+ console.error(
11532
+ "[webhook-server] Query error:",
11533
+ err instanceof Error ? err.message : err
11534
+ );
11535
+ sendJson(res, 503, { error: "Database unavailable" });
11536
+ }
11537
+ }
11176
11538
  handleWhatsAppVerification(req, res) {
11177
11539
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
11178
11540
  const mode = url.searchParams.get("hub.mode");
@@ -11234,6 +11596,9 @@ function extractPlatform(url) {
11234
11596
  const match = url.match(/^\/webhook\/([a-zA-Z0-9_-]+)/);
11235
11597
  return match?.[1] ?? null;
11236
11598
  }
11599
+ function isRoute(url, path24) {
11600
+ return url === path24 || url.startsWith(`${path24}?`);
11601
+ }
11237
11602
  function readBody(req) {
11238
11603
  return new Promise((resolve, reject) => {
11239
11604
  const chunks = [];
@@ -12059,8 +12424,8 @@ var BotRegistry = class {
12059
12424
 
12060
12425
  // src/bin/exe-gateway.ts
12061
12426
  init_employees();
12062
- var CONFIG_DIR = process.env.EXE_GATEWAY_HOME || path22.join(os14.homedir(), ".exe-os");
12063
- var CONFIG_PATH3 = process.env.EXE_GATEWAY_CONFIG || path22.join(CONFIG_DIR, "gateway.json");
12427
+ var CONFIG_DIR = process.env.EXE_GATEWAY_HOME || path23.join(os15.homedir(), ".exe-os");
12428
+ var CONFIG_PATH3 = process.env.EXE_GATEWAY_CONFIG || path23.join(CONFIG_DIR, "gateway.json");
12064
12429
  var DEFAULT_PORT = 3100;
12065
12430
  function loadConfig2() {
12066
12431
  if (!existsSync18(CONFIG_PATH3)) {