@askexenow/exe-os 0.8.33 → 0.8.36

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 (86) hide show
  1. package/dist/bin/backfill-conversations.js +332 -348
  2. package/dist/bin/backfill-responses.js +72 -12
  3. package/dist/bin/backfill-vectors.js +72 -12
  4. package/dist/bin/cleanup-stale-review-tasks.js +63 -3
  5. package/dist/bin/cli.js +1491 -1095
  6. package/dist/bin/exe-assign.js +80 -18
  7. package/dist/bin/exe-boot.js +407 -88
  8. package/dist/bin/exe-call.js +61 -2
  9. package/dist/bin/exe-dispatch.js +18 -10
  10. package/dist/bin/exe-doctor.js +63 -3
  11. package/dist/bin/exe-export-behaviors.js +64 -3
  12. package/dist/bin/exe-forget.js +69 -4
  13. package/dist/bin/exe-gateway.js +121 -36
  14. package/dist/bin/exe-heartbeat.js +77 -13
  15. package/dist/bin/exe-kill.js +64 -3
  16. package/dist/bin/exe-launch-agent.js +140 -13
  17. package/dist/bin/exe-link.js +946 -0
  18. package/dist/bin/exe-new-employee.js +98 -13
  19. package/dist/bin/exe-pending-messages.js +72 -7
  20. package/dist/bin/exe-pending-notifications.js +63 -3
  21. package/dist/bin/exe-pending-reviews.js +75 -10
  22. package/dist/bin/exe-rename.js +1287 -0
  23. package/dist/bin/exe-review.js +64 -4
  24. package/dist/bin/exe-search.js +79 -13
  25. package/dist/bin/exe-session-cleanup.js +91 -26
  26. package/dist/bin/exe-status.js +64 -4
  27. package/dist/bin/exe-team.js +64 -4
  28. package/dist/bin/git-sweep.js +71 -4
  29. package/dist/bin/graph-backfill.js +64 -3
  30. package/dist/bin/graph-export.js +64 -3
  31. package/dist/bin/install.js +3 -3
  32. package/dist/bin/scan-tasks.js +71 -4
  33. package/dist/bin/setup.js +128 -10
  34. package/dist/bin/shard-migrate.js +64 -3
  35. package/dist/bin/wiki-sync.js +64 -3
  36. package/dist/gateway/index.js +122 -37
  37. package/dist/hooks/bug-report-worker.js +209 -23
  38. package/dist/hooks/commit-complete.js +71 -4
  39. package/dist/hooks/error-recall.js +79 -13
  40. package/dist/hooks/ingest-worker.js +129 -43
  41. package/dist/hooks/instructions-loaded.js +71 -4
  42. package/dist/hooks/notification.js +71 -4
  43. package/dist/hooks/post-compact.js +71 -4
  44. package/dist/hooks/pre-compact.js +71 -4
  45. package/dist/hooks/pre-tool-use.js +413 -194
  46. package/dist/hooks/prompt-ingest-worker.js +82 -22
  47. package/dist/hooks/prompt-submit.js +103 -37
  48. package/dist/hooks/response-ingest-worker.js +87 -22
  49. package/dist/hooks/session-end.js +71 -4
  50. package/dist/hooks/session-start.js +79 -13
  51. package/dist/hooks/stop.js +71 -4
  52. package/dist/hooks/subagent-stop.js +71 -4
  53. package/dist/hooks/summary-worker.js +303 -50
  54. package/dist/index.js +134 -46
  55. package/dist/lib/cloud-sync.js +209 -15
  56. package/dist/lib/consolidation.js +4 -4
  57. package/dist/lib/database.js +64 -2
  58. package/dist/lib/device-registry.js +70 -3
  59. package/dist/lib/employee-templates.js +26 -0
  60. package/dist/lib/employees.js +34 -1
  61. package/dist/lib/exe-daemon.js +136 -53
  62. package/dist/lib/hybrid-search.js +79 -13
  63. package/dist/lib/identity-templates.js +51 -0
  64. package/dist/lib/identity.js +3 -3
  65. package/dist/lib/messaging.js +22 -14
  66. package/dist/lib/reminders.js +3 -3
  67. package/dist/lib/schedules.js +63 -3
  68. package/dist/lib/skill-learning.js +3 -3
  69. package/dist/lib/status-brief.js +63 -5
  70. package/dist/lib/store.js +64 -3
  71. package/dist/lib/task-router.js +4 -2
  72. package/dist/lib/tasks.js +48 -21
  73. package/dist/lib/tmux-routing.js +47 -20
  74. package/dist/mcp/server.js +727 -58
  75. package/dist/mcp/tools/complete-reminder.js +3 -3
  76. package/dist/mcp/tools/create-reminder.js +3 -3
  77. package/dist/mcp/tools/create-task.js +151 -24
  78. package/dist/mcp/tools/deactivate-behavior.js +3 -3
  79. package/dist/mcp/tools/list-reminders.js +3 -3
  80. package/dist/mcp/tools/list-tasks.js +17 -8
  81. package/dist/mcp/tools/send-message.js +24 -16
  82. package/dist/mcp/tools/update-task.js +25 -16
  83. package/dist/runtime/index.js +112 -24
  84. package/dist/tui/App.js +139 -36
  85. package/package.json +6 -2
  86. package/src/commands/exe/rename.md +12 -0
@@ -262,7 +262,7 @@ function listShards() {
262
262
  }
263
263
  async function ensureShardSchema(client) {
264
264
  await client.execute("PRAGMA journal_mode = WAL");
265
- await client.execute("PRAGMA busy_timeout = 5000");
265
+ await client.execute("PRAGMA busy_timeout = 30000");
266
266
  try {
267
267
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
268
268
  } catch {
@@ -446,124 +446,79 @@ var init_shard_manager = __esm({
446
446
  }
447
447
  });
448
448
 
449
- // src/lib/employees.ts
450
- var employees_exports = {};
451
- __export(employees_exports, {
452
- EMPLOYEES_PATH: () => EMPLOYEES_PATH,
453
- addEmployee: () => addEmployee,
454
- getEmployee: () => getEmployee,
455
- loadEmployees: () => loadEmployees,
456
- registerBinSymlinks: () => registerBinSymlinks,
457
- saveEmployees: () => saveEmployees,
458
- validateEmployeeName: () => validateEmployeeName
459
- });
460
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
461
- import { existsSync as existsSync5, symlinkSync, readlinkSync } from "fs";
462
- import { execSync } from "child_process";
463
- import path5 from "path";
464
- function validateEmployeeName(name) {
465
- if (!name) {
466
- return { valid: false, error: "Name is required" };
467
- }
468
- if (name.length > 32) {
469
- return { valid: false, error: "Name must be 32 characters or fewer" };
470
- }
471
- if (!/^[a-z][a-z0-9]*$/.test(name)) {
472
- return {
473
- valid: false,
474
- error: "Name must start with a letter and contain only lowercase alphanumeric characters"
475
- };
476
- }
477
- return { valid: true };
478
- }
479
- async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
480
- if (!existsSync5(employeesPath)) {
481
- return [];
482
- }
483
- const raw = await readFile3(employeesPath, "utf-8");
484
- try {
485
- return JSON.parse(raw);
486
- } catch {
487
- return [];
488
- }
489
- }
490
- async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
491
- await mkdir3(path5.dirname(employeesPath), { recursive: true });
492
- await writeFile3(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
493
- }
494
- function getEmployee(employees, name) {
495
- return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
496
- }
497
- function addEmployee(employees, employee) {
498
- const normalized = { ...employee, name: employee.name.toLowerCase() };
499
- if (employees.some((e) => e.name.toLowerCase() === normalized.name)) {
500
- throw new Error(`Employee '${normalized.name}' already exists`);
501
- }
502
- return [...employees, normalized];
503
- }
504
- function registerBinSymlinks(name) {
505
- const created = [];
506
- const skipped = [];
507
- const errors = [];
508
- let exeBinPath;
509
- try {
510
- exeBinPath = execSync("which exe", { encoding: "utf-8" }).trim();
511
- } catch {
512
- errors.push("Could not find 'exe' in PATH");
513
- return { created, skipped, errors };
514
- }
515
- const binDir = path5.dirname(exeBinPath);
516
- let target;
517
- try {
518
- target = readlinkSync(exeBinPath);
519
- } catch {
520
- errors.push("Could not read 'exe' symlink");
521
- return { created, skipped, errors };
522
- }
523
- for (const suffix of ["", "-opencode"]) {
524
- const linkName = `${name}${suffix}`;
525
- const linkPath = path5.join(binDir, linkName);
526
- if (existsSync5(linkPath)) {
527
- skipped.push(linkName);
528
- continue;
529
- }
530
- try {
531
- symlinkSync(target, linkPath);
532
- created.push(linkName);
533
- } catch (err) {
534
- errors.push(`${linkName}: ${err instanceof Error ? err.message : String(err)}`);
535
- }
536
- }
537
- return { created, skipped, errors };
538
- }
539
- var EMPLOYEES_PATH;
540
- var init_employees = __esm({
541
- "src/lib/employees.ts"() {
542
- "use strict";
543
- init_config();
544
- EMPLOYEES_PATH = path5.join(EXE_AI_DIR, "exe-employees.json");
545
- }
546
- });
547
-
548
449
  // src/bin/backfill-conversations.ts
549
450
  import crypto2 from "crypto";
550
451
  import { createReadStream } from "fs";
551
452
  import { readdir, stat } from "fs/promises";
552
- import path6 from "path";
453
+ import path5 from "path";
553
454
  import { createInterface } from "readline";
554
455
  import { homedir } from "os";
456
+ import { parseArgs } from "util";
555
457
 
556
458
  // src/types/memory.ts
557
459
  var EMBEDDING_DIM = 1024;
558
460
 
559
461
  // src/lib/database.ts
560
462
  import { createClient } from "@libsql/client";
463
+
464
+ // src/lib/db-retry.ts
465
+ var MAX_RETRIES = 3;
466
+ var BASE_DELAY_MS = 200;
467
+ var MAX_JITTER_MS = 300;
468
+ function isBusyError(err) {
469
+ if (err instanceof Error) {
470
+ const msg = err.message.toLowerCase();
471
+ return msg.includes("sqlite_busy") || msg.includes("database is locked");
472
+ }
473
+ return false;
474
+ }
475
+ function delay(ms) {
476
+ return new Promise((resolve) => setTimeout(resolve, ms));
477
+ }
478
+ async function retryOnBusy(fn, label) {
479
+ let lastError;
480
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
481
+ try {
482
+ return await fn();
483
+ } catch (err) {
484
+ lastError = err;
485
+ if (!isBusyError(err) || attempt === MAX_RETRIES) {
486
+ throw err;
487
+ }
488
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
489
+ const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
490
+ process.stderr.write(
491
+ `[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
492
+ `
493
+ );
494
+ await delay(backoff + jitter);
495
+ }
496
+ }
497
+ throw lastError;
498
+ }
499
+ function wrapWithRetry(client) {
500
+ return new Proxy(client, {
501
+ get(target, prop, receiver) {
502
+ if (prop === "execute") {
503
+ return (sql) => retryOnBusy(() => target.execute(sql), "execute");
504
+ }
505
+ if (prop === "batch") {
506
+ return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
507
+ }
508
+ return Reflect.get(target, prop, receiver);
509
+ }
510
+ });
511
+ }
512
+
513
+ // src/lib/database.ts
561
514
  var _client = null;
515
+ var _resilientClient = null;
562
516
  var initTurso = initDatabase;
563
517
  async function initDatabase(config) {
564
518
  if (_client) {
565
519
  _client.close();
566
520
  _client = null;
521
+ _resilientClient = null;
567
522
  }
568
523
  const opts = {
569
524
  url: `file:${config.dbPath}`
@@ -572,17 +527,24 @@ async function initDatabase(config) {
572
527
  opts.encryptionKey = config.encryptionKey;
573
528
  }
574
529
  _client = createClient(opts);
530
+ _resilientClient = wrapWithRetry(_client);
575
531
  }
576
532
  function getClient() {
533
+ if (!_resilientClient) {
534
+ throw new Error("Database client not initialized. Call initDatabase() first.");
535
+ }
536
+ return _resilientClient;
537
+ }
538
+ function getRawClient() {
577
539
  if (!_client) {
578
540
  throw new Error("Database client not initialized. Call initDatabase() first.");
579
541
  }
580
542
  return _client;
581
543
  }
582
544
  async function ensureSchema() {
583
- const client = getClient();
545
+ const client = getRawClient();
584
546
  await client.execute("PRAGMA journal_mode = WAL");
585
- await client.execute("PRAGMA busy_timeout = 5000");
547
+ await client.execute("PRAGMA busy_timeout = 30000");
586
548
  try {
587
549
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
588
550
  } catch {
@@ -1831,11 +1793,11 @@ async function connectEmbedDaemon() {
1831
1793
  }
1832
1794
  }
1833
1795
  const start = Date.now();
1834
- let delay = 100;
1796
+ let delay2 = 100;
1835
1797
  while (Date.now() - start < CONNECT_TIMEOUT_MS) {
1836
- await new Promise((r) => setTimeout(r, delay));
1798
+ await new Promise((r) => setTimeout(r, delay2));
1837
1799
  if (await connectToSocket()) return true;
1838
- delay = Math.min(delay * 2, 3e3);
1800
+ delay2 = Math.min(delay2 * 2, 3e3);
1839
1801
  }
1840
1802
  return false;
1841
1803
  }
@@ -1927,11 +1889,11 @@ async function embedViaClient(text, priority = "high") {
1927
1889
  `);
1928
1890
  killAndRespawnDaemon();
1929
1891
  const start = Date.now();
1930
- let delay = 200;
1892
+ let delay2 = 200;
1931
1893
  while (Date.now() - start < CONNECT_TIMEOUT_MS) {
1932
- await new Promise((r) => setTimeout(r, delay));
1894
+ await new Promise((r) => setTimeout(r, delay2));
1933
1895
  if (await connectToSocket()) break;
1934
- delay = Math.min(delay * 2, 3e3);
1896
+ delay2 = Math.min(delay2 * 2, 3e3);
1935
1897
  }
1936
1898
  if (!_connected) return null;
1937
1899
  }
@@ -1943,11 +1905,11 @@ async function embedViaClient(text, priority = "high") {
1943
1905
  `);
1944
1906
  killAndRespawnDaemon();
1945
1907
  const start = Date.now();
1946
- let delay = 200;
1908
+ let delay2 = 200;
1947
1909
  while (Date.now() - start < CONNECT_TIMEOUT_MS) {
1948
- await new Promise((r) => setTimeout(r, delay));
1910
+ await new Promise((r) => setTimeout(r, delay2));
1949
1911
  if (await connectToSocket()) break;
1950
- delay = Math.min(delay * 2, 3e3);
1912
+ delay2 = Math.min(delay2 * 2, 3e3);
1951
1913
  }
1952
1914
  if (!_connected) return null;
1953
1915
  const retry = await sendRequest([text], priority);
@@ -1973,9 +1935,11 @@ function isMainModule(importMetaUrl) {
1973
1935
  }
1974
1936
 
1975
1937
  // src/bin/backfill-conversations.ts
1976
- process.env.EXE_EMBED_PRIORITY = "low";
1977
- async function findJsonlFiles(cutoffMs) {
1978
- const projectsDir = path6.join(homedir(), ".claude", "projects");
1938
+ var TOOL_NAME = "backfill-conversation";
1939
+ var MIN_MESSAGES = 3;
1940
+ var MAX_SUMMARY_LENGTH = 4e3;
1941
+ async function findJsonlFiles(sinceDate, projectFilter) {
1942
+ const projectsDir = path5.join(homedir(), ".claude", "projects");
1979
1943
  const files = [];
1980
1944
  async function walk(dir) {
1981
1945
  let entries;
@@ -1985,61 +1949,67 @@ async function findJsonlFiles(cutoffMs) {
1985
1949
  return;
1986
1950
  }
1987
1951
  for (const entry of entries) {
1988
- const full = path6.join(dir, entry.name);
1952
+ const full = path5.join(dir, entry.name);
1989
1953
  if (entry.isDirectory()) {
1954
+ if (entry.name === "subagents" || entry.name === "tool-results") continue;
1990
1955
  await walk(full);
1991
1956
  } else if (entry.name.endsWith(".jsonl")) {
1992
1957
  try {
1993
1958
  const s = await stat(full);
1994
- if (s.mtimeMs >= cutoffMs) files.push(full);
1959
+ if (sinceDate && s.mtimeMs < sinceDate.getTime()) continue;
1960
+ files.push(full);
1995
1961
  } catch {
1996
1962
  }
1997
1963
  }
1998
1964
  }
1999
1965
  }
2000
- await walk(projectsDir);
1966
+ if (projectFilter) {
1967
+ let projectDirs;
1968
+ try {
1969
+ projectDirs = await readdir(projectsDir, { withFileTypes: true });
1970
+ } catch {
1971
+ return files;
1972
+ }
1973
+ for (const entry of projectDirs) {
1974
+ if (!entry.isDirectory()) continue;
1975
+ const decoded = decodeProjectDir(entry.name);
1976
+ if (decoded.toLowerCase().includes(projectFilter.toLowerCase())) {
1977
+ await walk(path5.join(projectsDir, entry.name));
1978
+ }
1979
+ }
1980
+ } else {
1981
+ await walk(projectsDir);
1982
+ }
2001
1983
  return files;
2002
1984
  }
2003
- function projectNameFromPath(filePath) {
2004
- const projectsDir = path6.join(homedir(), ".claude", "projects");
2005
- const relative = path6.relative(projectsDir, filePath);
2006
- const projectDir = relative.split(path6.sep)[0] ?? relative;
1985
+ function decodeProjectDir(dirName) {
2007
1986
  const homeEncoded = homedir().replaceAll("/", "-");
2008
- if (projectDir.startsWith(homeEncoded + "-")) {
2009
- return projectDir.slice(homeEncoded.length + 1);
1987
+ if (dirName.startsWith(homeEncoded + "-")) {
1988
+ return dirName.slice(homeEncoded.length + 1);
2010
1989
  }
2011
- if (projectDir === homeEncoded) return "home";
2012
- return projectDir;
1990
+ if (dirName === homeEncoded) return "home";
1991
+ return dirName;
2013
1992
  }
2014
- function extractAssistantText(content) {
2015
- if (typeof content === "string") return content;
2016
- const texts = [];
2017
- for (const block of content) {
2018
- if (block.type === "text" && block.text) {
2019
- texts.push(block.text);
2020
- }
2021
- }
2022
- return texts.join("\n");
2023
- }
2024
- function extractUserText(content) {
2025
- if (typeof content === "string") return content;
2026
- const texts = [];
2027
- for (const block of content) {
2028
- if (block.type === "text" && block.text) {
2029
- texts.push(block.text);
2030
- }
2031
- }
2032
- return texts.join("\n");
1993
+ function projectNameFromPath(filePath) {
1994
+ const projectsDir = path5.join(homedir(), ".claude", "projects");
1995
+ const relative = path5.relative(projectsDir, filePath);
1996
+ const projectDir = relative.split(path5.sep)[0] ?? "unknown";
1997
+ return decodeProjectDir(projectDir);
2033
1998
  }
2034
- async function extractConversationPairs(filePath, projectFilter) {
2035
- const fallbackProject = projectNameFromPath(filePath);
2036
- if (projectFilter && !fallbackProject.toLowerCase().includes(projectFilter.toLowerCase())) {
2037
- return [];
2038
- }
2039
- const pairs = [];
2040
- let fileCwd = "";
2041
- let fileSessionId = "";
2042
- let pendingUser = null;
1999
+ async function parseConversation(filePath) {
2000
+ const conv = {
2001
+ sessionId: path5.basename(filePath, ".jsonl"),
2002
+ projectName: projectNameFromPath(filePath),
2003
+ cwd: void 0,
2004
+ startTime: void 0,
2005
+ endTime: void 0,
2006
+ userMessages: [],
2007
+ toolCounts: {},
2008
+ filesTouched: /* @__PURE__ */ new Set(),
2009
+ errorCount: 0,
2010
+ totalMessages: 0,
2011
+ agentId: "default"
2012
+ };
2043
2013
  const rl = createInterface({
2044
2014
  input: createReadStream(filePath, { encoding: "utf8" }),
2045
2015
  crlfDelay: Infinity
@@ -2052,251 +2022,265 @@ async function extractConversationPairs(filePath, projectFilter) {
2052
2022
  } catch {
2053
2023
  continue;
2054
2024
  }
2055
- if (entry.cwd) fileCwd = entry.cwd;
2056
- if (entry.sessionId) fileSessionId = entry.sessionId;
2057
- if (entry.type === "user" && entry.message?.content) {
2058
- const text = extractUserText(entry.message.content);
2059
- if (text.trim()) {
2060
- pendingUser = {
2061
- text,
2062
- timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
2063
- uuid: entry.uuid ?? ""
2064
- };
2025
+ if (entry.cwd && typeof entry.cwd === "string") {
2026
+ conv.cwd = entry.cwd;
2027
+ }
2028
+ const ts = entry.timestamp;
2029
+ if (ts) {
2030
+ if (!conv.startTime || ts < conv.startTime) conv.startTime = ts;
2031
+ if (!conv.endTime || ts > conv.endTime) conv.endTime = ts;
2032
+ }
2033
+ const entryType = entry.type;
2034
+ if (entryType === "user") {
2035
+ conv.totalMessages++;
2036
+ const message = entry.message;
2037
+ if (message?.content) {
2038
+ const text = extractUserText(message.content);
2039
+ if (text && text.length > 10) {
2040
+ conv.userMessages.push(text);
2041
+ }
2042
+ }
2043
+ } else if (entryType === "assistant") {
2044
+ conv.totalMessages++;
2045
+ const message = entry.message;
2046
+ if (message?.content && Array.isArray(message.content)) {
2047
+ for (const block of message.content) {
2048
+ if (typeof block !== "object" || block === null) continue;
2049
+ const b = block;
2050
+ if (b.type === "tool_use") {
2051
+ const toolName = b.name;
2052
+ conv.toolCounts[toolName] = (conv.toolCounts[toolName] || 0) + 1;
2053
+ const input = b.input;
2054
+ if (input?.file_path && typeof input.file_path === "string") {
2055
+ if (toolName === "Write" || toolName === "Edit") {
2056
+ conv.filesTouched.add(input.file_path);
2057
+ }
2058
+ }
2059
+ }
2060
+ }
2065
2061
  }
2066
- } else if (entry.type === "assistant" && entry.message?.content) {
2067
- const assistantText = extractAssistantText(entry.message.content);
2068
- if (!assistantText.trim()) continue;
2069
- const resolvedProject = fileCwd ? path6.basename(fileCwd) : fallbackProject;
2070
- const userText = pendingUser?.text ?? "";
2071
- const timestamp = entry.timestamp ?? pendingUser?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
2072
- pairs.push({
2073
- userText,
2074
- assistantText,
2075
- timestamp,
2076
- sessionId: fileSessionId || path6.basename(filePath, ".jsonl"),
2077
- project: resolvedProject,
2078
- cwd: fileCwd || fallbackProject
2079
- });
2080
- pendingUser = null;
2081
2062
  }
2082
2063
  }
2083
- return pairs;
2064
+ if (conv.cwd) {
2065
+ conv.projectName = path5.basename(conv.cwd);
2066
+ const worktreeMatch = conv.cwd.match(/\.worktrees\/([^/]+)/);
2067
+ if (worktreeMatch?.[1]) {
2068
+ conv.agentId = worktreeMatch[1];
2069
+ }
2070
+ }
2071
+ return conv;
2084
2072
  }
2085
- function hashPair(userText, assistantText) {
2086
- return crypto2.createHash("sha256").update(userText.slice(0, 500) + "||" + assistantText.slice(0, 500)).digest("hex");
2073
+ function extractUserText(content) {
2074
+ if (typeof content === "string") return content;
2075
+ if (Array.isArray(content)) {
2076
+ const parts = [];
2077
+ for (const block of content) {
2078
+ if (typeof block === "string") {
2079
+ parts.push(block);
2080
+ } else if (typeof block === "object" && block !== null) {
2081
+ const b = block;
2082
+ if (b.type === "text" && typeof b.text === "string") {
2083
+ parts.push(b.text);
2084
+ }
2085
+ }
2086
+ }
2087
+ return parts.join("\n");
2088
+ }
2089
+ return "";
2087
2090
  }
2088
- async function loadExistingHashes(cutoffIso) {
2091
+ function buildSummary(conv) {
2092
+ const parts = [];
2093
+ parts.push(`Session: ${conv.sessionId}`);
2094
+ parts.push(`Project: ${conv.projectName}`);
2095
+ if (conv.startTime) {
2096
+ parts.push(`Time: ${conv.startTime}${conv.endTime ? ` \u2192 ${conv.endTime}` : ""}`);
2097
+ }
2098
+ parts.push(`Messages: ${conv.totalMessages}`);
2099
+ if (conv.agentId !== "default") {
2100
+ parts.push(`Agent: ${conv.agentId}`);
2101
+ }
2102
+ parts.push("");
2103
+ if (conv.userMessages.length > 0) {
2104
+ parts.push("## What was asked");
2105
+ const prompts = conv.userMessages.slice(0, 5);
2106
+ for (const prompt of prompts) {
2107
+ const truncated = prompt.length > 300 ? prompt.slice(0, 300) + "..." : prompt;
2108
+ const cleaned = truncated.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
2109
+ if (cleaned) parts.push(`- ${cleaned}`);
2110
+ }
2111
+ if (conv.userMessages.length > 5) {
2112
+ parts.push(`- ... and ${conv.userMessages.length - 5} more prompts`);
2113
+ }
2114
+ parts.push("");
2115
+ }
2116
+ const toolEntries = Object.entries(conv.toolCounts).sort((a, b) => b[1] - a[1]);
2117
+ if (toolEntries.length > 0) {
2118
+ parts.push("## Tools used");
2119
+ for (const [tool, count] of toolEntries) {
2120
+ parts.push(`- ${tool}: ${count}`);
2121
+ }
2122
+ parts.push("");
2123
+ }
2124
+ if (conv.filesTouched.size > 0) {
2125
+ parts.push("## Files modified");
2126
+ const fileList = [...conv.filesTouched].sort();
2127
+ const shown = fileList.slice(0, 20);
2128
+ for (const f of shown) {
2129
+ parts.push(`- ${f}`);
2130
+ }
2131
+ if (fileList.length > 20) {
2132
+ parts.push(`- ... and ${fileList.length - 20} more files`);
2133
+ }
2134
+ parts.push("");
2135
+ }
2136
+ if (conv.errorCount > 0) {
2137
+ parts.push(`Errors: ${conv.errorCount}`);
2138
+ }
2139
+ let summary = parts.join("\n");
2140
+ if (summary.length > MAX_SUMMARY_LENGTH) {
2141
+ summary = summary.slice(0, MAX_SUMMARY_LENGTH);
2142
+ }
2143
+ return summary;
2144
+ }
2145
+ async function loadExistingSourcePaths() {
2089
2146
  const client = getClient();
2090
- const hashes = /* @__PURE__ */ new Set();
2147
+ const paths = /* @__PURE__ */ new Set();
2091
2148
  let offset = 0;
2092
2149
  const batchSize = 500;
2093
2150
  while (true) {
2094
2151
  const result = await client.execute({
2095
- sql: `SELECT content_text, agent_response FROM conversations
2096
- WHERE platform = 'claude-code' AND timestamp > ?
2152
+ sql: `SELECT source_path FROM memories
2153
+ WHERE tool_name = ? AND source_path IS NOT NULL
2097
2154
  ORDER BY id LIMIT ? OFFSET ?`,
2098
- args: [cutoffIso, batchSize, offset]
2155
+ args: [TOOL_NAME, batchSize, offset]
2099
2156
  });
2100
2157
  if (result.rows.length === 0) break;
2101
2158
  for (const row of result.rows) {
2102
- hashes.add(
2103
- hashPair(
2104
- row.content_text ?? "",
2105
- row.agent_response ?? ""
2106
- )
2107
- );
2159
+ paths.add(row.source_path);
2108
2160
  }
2109
2161
  offset += batchSize;
2110
2162
  }
2111
- return hashes;
2112
- }
2113
- var MAX_CONTENT_LENGTH = 8e3;
2114
- var MIN_ASSISTANT_LENGTH = 50;
2115
- async function storePair(pair, daemonConnected, stats, agentId) {
2116
- const client = getClient();
2117
- const id = crypto2.randomUUID();
2118
- const userTrunc = pair.userText.length > MAX_CONTENT_LENGTH ? pair.userText.slice(0, MAX_CONTENT_LENGTH) : pair.userText;
2119
- const assistTrunc = pair.assistantText.length > MAX_CONTENT_LENGTH ? pair.assistantText.slice(0, MAX_CONTENT_LENGTH) : pair.assistantText;
2120
- await client.execute({
2121
- sql: `INSERT INTO conversations
2122
- (id, platform, external_id, sender_id, sender_name, sender_phone, sender_email,
2123
- recipient_id, channel_id, thread_id, reply_to_id,
2124
- content_text, content_media, agent_response, agent_name,
2125
- timestamp, ingested_at)
2126
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2127
- args: [
2128
- id,
2129
- "claude-code",
2130
- null,
2131
- "user",
2132
- "user",
2133
- null,
2134
- null,
2135
- null,
2136
- pair.project,
2137
- pair.sessionId,
2138
- null,
2139
- userTrunc,
2140
- null,
2141
- assistTrunc,
2142
- "claude",
2143
- pair.timestamp,
2144
- (/* @__PURE__ */ new Date()).toISOString()
2145
- ]
2146
- });
2147
- stats.conversationsStored++;
2148
- const rawText = [
2149
- `[claude-code] Conversation in ${pair.project}`,
2150
- userTrunc ? `User: ${userTrunc}` : null,
2151
- `Assistant: ${assistTrunc}`
2152
- ].filter(Boolean).join("\n");
2153
- let vector = null;
2154
- if (daemonConnected) {
2155
- try {
2156
- vector = await embedViaClient(rawText, "low");
2157
- if (!vector) stats.embedFailed++;
2158
- } catch {
2159
- stats.embedFailed++;
2160
- }
2161
- }
2162
- await writeMemory({
2163
- id: crypto2.randomUUID(),
2164
- agent_id: agentId,
2165
- agent_role: "coo",
2166
- session_id: pair.sessionId,
2167
- timestamp: pair.timestamp,
2168
- tool_name: "ConversationBackfill",
2169
- project_name: pair.project,
2170
- has_error: false,
2171
- raw_text: rawText,
2172
- vector,
2173
- importance: 3
2174
- });
2175
- stats.memoriesStored++;
2163
+ return paths;
2176
2164
  }
2177
2165
  async function backfillConversations(options) {
2178
2166
  const stats = {
2179
2167
  filesScanned: 0,
2180
2168
  conversationsStored: 0,
2181
- memoriesStored: 0,
2182
2169
  skippedDedup: 0,
2183
- skippedShort: 0,
2170
+ skippedTooShort: 0,
2184
2171
  embedFailed: 0
2185
2172
  };
2186
- const cutoffMs = Date.now() - options.days * 24 * 60 * 60 * 1e3;
2187
- const cutoffIso = new Date(cutoffMs).toISOString();
2188
- let cooAgentId = "exe";
2189
- try {
2190
- const { loadEmployees: loadEmployees2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
2191
- const employees = await loadEmployees2();
2192
- const coo = employees.find((e) => e.role === "COO");
2193
- if (coo) cooAgentId = coo.name.toLowerCase();
2194
- } catch {
2173
+ const sinceDate = options.since ? new Date(options.since) : void 0;
2174
+ if (sinceDate && isNaN(sinceDate.getTime())) {
2175
+ throw new Error(`Invalid --since date: ${options.since}`);
2195
2176
  }
2196
- process.stderr.write(`[backfill-conversations] Scanning last ${options.days} days (agent: ${cooAgentId})...
2197
- `);
2177
+ process.stderr.write("[backfill-conversations] Initializing store...\n");
2178
+ await initStore();
2179
+ let daemonConnected = false;
2198
2180
  if (!options.dryRun) {
2199
- process.stderr.write("[backfill-conversations] Initializing store...\n");
2200
- await initStore();
2201
- try {
2202
- const client = getClient();
2203
- const old = await client.execute({
2204
- sql: "SELECT COUNT(*) as cnt FROM memories WHERE agent_id = 'backfill' OR (tool_name = 'ConversationBackfill' AND agent_id != ?)",
2205
- args: [cooAgentId]
2206
- });
2207
- const count = Number(old.rows[0]?.cnt ?? 0);
2208
- if (count > 0) {
2209
- await client.execute({
2210
- sql: "UPDATE memories SET agent_id = ?, agent_role = 'coo' WHERE agent_id = 'backfill' OR (tool_name = 'ConversationBackfill' AND agent_id != ?)",
2211
- args: [cooAgentId, cooAgentId]
2212
- });
2213
- process.stderr.write(`[backfill-conversations] Migrated ${count} records \u2192 agent_id='${cooAgentId}'
2214
- `);
2215
- }
2216
- } catch {
2181
+ daemonConnected = await connectEmbedDaemon();
2182
+ if (!daemonConnected) {
2183
+ process.stderr.write(
2184
+ "[backfill-conversations] WARNING: Daemon unavailable \u2014 vectors will be NULL (backfill-vectors can fix later)\n"
2185
+ );
2217
2186
  }
2218
2187
  }
2219
- const daemonConnected = options.dryRun ? false : await connectEmbedDaemon();
2220
- if (!daemonConnected && !options.dryRun) {
2221
- process.stderr.write(
2222
- "[backfill-conversations] WARNING: Daemon unavailable \u2014 vectors will be NULL (backfill-vectors can fix later)\n"
2223
- );
2224
- }
2225
- let seenHashes = /* @__PURE__ */ new Set();
2226
- if (!options.dryRun) {
2227
- process.stderr.write("[backfill-conversations] Loading existing hashes for dedup...\n");
2228
- seenHashes = await loadExistingHashes(cutoffIso);
2229
- process.stderr.write(`[backfill-conversations] ${seenHashes.size} existing conversations loaded
2188
+ process.stderr.write("[backfill-conversations] Loading already-ingested conversations...\n");
2189
+ const existingPaths = options.dryRun ? /* @__PURE__ */ new Set() : await loadExistingSourcePaths();
2190
+ process.stderr.write(`[backfill-conversations] ${existingPaths.size} conversations already ingested
2230
2191
  `);
2231
- }
2232
- const files = await findJsonlFiles(cutoffMs);
2233
- process.stderr.write(`[backfill-conversations] Found ${files.length} JSONL files
2192
+ const files = await findJsonlFiles(sinceDate, options.project);
2193
+ process.stderr.write(`[backfill-conversations] Found ${files.length} JSONL files to process
2234
2194
  `);
2195
+ process.env.EXE_EMBED_PRIORITY = "low";
2235
2196
  for (const file of files) {
2236
- const pairs = await extractConversationPairs(file, options.projectFilter);
2237
2197
  stats.filesScanned++;
2238
- for (const pair of pairs) {
2239
- if (pair.assistantText.length < MIN_ASSISTANT_LENGTH) {
2240
- stats.skippedShort++;
2241
- continue;
2242
- }
2243
- const hash = hashPair(pair.userText, pair.assistantText);
2244
- if (seenHashes.has(hash)) {
2245
- stats.skippedDedup++;
2246
- continue;
2198
+ if (existingPaths.has(file)) {
2199
+ stats.skippedDedup++;
2200
+ continue;
2201
+ }
2202
+ const conv = await parseConversation(file);
2203
+ if (conv.totalMessages < MIN_MESSAGES) {
2204
+ stats.skippedTooShort++;
2205
+ continue;
2206
+ }
2207
+ const summary = buildSummary(conv);
2208
+ if (options.dryRun) {
2209
+ process.stdout.write(`
2210
+ \u2500\u2500\u2500 ${file} \u2500\u2500\u2500
2211
+ `);
2212
+ process.stdout.write(`Project: ${conv.projectName} | Messages: ${conv.totalMessages}`);
2213
+ process.stdout.write(` | Tools: ${Object.keys(conv.toolCounts).length}`);
2214
+ process.stdout.write(` | Files: ${conv.filesTouched.size}
2215
+ `);
2216
+ const firstPrompt = conv.userMessages[0];
2217
+ if (firstPrompt) {
2218
+ process.stdout.write(`First prompt: ${firstPrompt.slice(0, 120)}
2219
+ `);
2247
2220
  }
2248
- seenHashes.add(hash);
2249
- if (!options.dryRun) {
2250
- await storePair(pair, daemonConnected, stats, cooAgentId);
2251
- } else {
2252
- stats.conversationsStored++;
2253
- stats.memoriesStored++;
2221
+ stats.conversationsStored++;
2222
+ continue;
2223
+ }
2224
+ let vector = null;
2225
+ if (daemonConnected) {
2226
+ try {
2227
+ vector = await embedViaClient(summary, "low");
2228
+ if (!vector) stats.embedFailed++;
2229
+ } catch {
2230
+ stats.embedFailed++;
2254
2231
  }
2255
2232
  }
2256
- if (stats.filesScanned % 20 === 0) {
2233
+ await writeMemory({
2234
+ id: crypto2.randomUUID(),
2235
+ agent_id: conv.agentId,
2236
+ agent_role: conv.agentId === "exe" ? "COO" : "specialist",
2237
+ session_id: conv.sessionId,
2238
+ timestamp: conv.startTime ?? (/* @__PURE__ */ new Date()).toISOString(),
2239
+ tool_name: TOOL_NAME,
2240
+ project_name: conv.projectName,
2241
+ has_error: conv.errorCount > 0,
2242
+ raw_text: summary,
2243
+ vector,
2244
+ source_path: file,
2245
+ source_type: "conversation"
2246
+ });
2247
+ existingPaths.add(file);
2248
+ stats.conversationsStored++;
2249
+ if (stats.filesScanned % 50 === 0) {
2257
2250
  process.stderr.write(
2258
2251
  `[backfill-conversations] Progress: ${stats.filesScanned}/${files.length} files, ${stats.conversationsStored} stored
2259
2252
  `
2260
2253
  );
2261
- if (!options.dryRun) await flushBatch();
2254
+ await flushBatch();
2262
2255
  }
2263
2256
  }
2264
- if (!options.dryRun) await flushBatch();
2257
+ if (!options.dryRun) {
2258
+ await flushBatch();
2259
+ }
2265
2260
  process.stderr.write(
2266
- `[backfill-conversations] Done. Files: ${stats.filesScanned}, Conversations: ${stats.conversationsStored}, Memories: ${stats.memoriesStored}, Dedup: ${stats.skippedDedup}, Short: ${stats.skippedShort}, EmbedFail: ${stats.embedFailed}
2261
+ `[backfill-conversations] Done. Scanned: ${stats.filesScanned}, Stored: ${stats.conversationsStored}, Dedup: ${stats.skippedDedup}, TooShort: ${stats.skippedTooShort}, EmbedFail: ${stats.embedFailed}
2267
2262
  `
2268
2263
  );
2269
2264
  return stats;
2270
2265
  }
2271
- function parseArgs(argv) {
2272
- let days = 30;
2273
- let projectFilter;
2274
- let dryRun = false;
2275
- for (let i = 0; i < argv.length; i++) {
2276
- switch (argv[i]) {
2277
- case "--days":
2278
- days = parseInt(argv[++i] ?? "30", 10) || 30;
2279
- break;
2280
- case "--project":
2281
- projectFilter = argv[++i] ?? void 0;
2282
- break;
2283
- case "--dry-run":
2284
- dryRun = true;
2285
- break;
2286
- }
2287
- }
2288
- return { days, projectFilter, dryRun };
2289
- }
2290
2266
  if (isMainModule(import.meta.url)) {
2291
- const options = parseArgs(process.argv.slice(2));
2292
- backfillConversations(options).then((result) => {
2267
+ const { values } = parseArgs({
2268
+ options: {
2269
+ since: { type: "string" },
2270
+ project: { type: "string" },
2271
+ "dry-run": { type: "boolean", default: false }
2272
+ },
2273
+ strict: true
2274
+ });
2275
+ backfillConversations({
2276
+ since: values.since,
2277
+ project: values.project,
2278
+ dryRun: values["dry-run"]
2279
+ }).then((result) => {
2293
2280
  console.log(JSON.stringify(result, null, 2));
2294
2281
  process.exit(0);
2295
2282
  }).catch((err) => {
2296
- console.error(
2297
- "Backfill failed:",
2298
- err instanceof Error ? err.message : String(err)
2299
- );
2283
+ console.error("Backfill failed:", err instanceof Error ? err.message : String(err));
2300
2284
  process.exit(1);
2301
2285
  });
2302
2286
  }