@tekmidian/pai 0.7.2 → 0.7.3

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.
@@ -3793,7 +3793,7 @@ function cmdLogs(opts) {
3793
3793
  }
3794
3794
  function registerDaemonCommands(daemonCmd) {
3795
3795
  daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
3796
- const { serve } = await import("../daemon-DuGlDnV7.mjs").then((n) => n.t);
3796
+ const { serve } = await import("../daemon-D8ZxcFhU.mjs").then((n) => n.t);
3797
3797
  const { loadConfig: lc, ensureConfigDir } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
3798
3798
  ensureConfigDir();
3799
3799
  await serve(lc());
@@ -8,7 +8,7 @@ import "../indexer-D53l5d1U.mjs";
8
8
  import { t as PaiClient } from "../ipc-client-CoyUHPod.mjs";
9
9
  import { i as ensureConfigDir, o as loadConfig } from "../config-BuhHWyOK.mjs";
10
10
  import "../factory-Ygqe_bVZ.mjs";
11
- import { n as serve } from "../daemon-DuGlDnV7.mjs";
11
+ import { n as serve } from "../daemon-D8ZxcFhU.mjs";
12
12
  import "../state-C6_vqz7w.mjs";
13
13
  import "../tools-DcaJlYDN.mjs";
14
14
  import "../detector-jGBuYQJM.mjs";
@@ -8,7 +8,7 @@ import { t as createStorageBackend } from "./factory-Ygqe_bVZ.mjs";
8
8
  import { C as setStorageBackend, E as startTime, O as storageBackend, S as setStartTime, T as shutdownRequested, _ as setLastIndexTime, a as indexSchedulerTimer, b as setRegistryDb, c as lastVaultIndexTime, d as setDaemonConfig, f as setEmbedInProgress, g as setLastEmbedTime, h as setIndexSchedulerTimer, i as indexInProgress, k as vaultIndexInProgress, l as notificationConfig, m as setIndexInProgress, n as embedInProgress, o as lastEmbedTime, p as setEmbedSchedulerTimer, r as embedSchedulerTimer, s as lastIndexTime, t as daemonConfig, u as registryDb, v as setLastVaultIndexTime, w as setVaultIndexInProgress, x as setShutdownRequested, y as setNotificationConfig } from "./state-C6_vqz7w.mjs";
9
9
  import { a as toolProjectDetect, c as toolProjectList, d as toolMemorySearch, i as toolSessionRoute, l as toolProjectTodo, n as toolRegistrySearch, o as toolProjectHealth, r as toolSessionList, s as toolProjectInfo, u as toolMemoryGet } from "./tools-DcaJlYDN.mjs";
10
10
  import { t as detectTopicShift } from "./detector-jGBuYQJM.mjs";
11
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
11
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
12
12
  import { homedir, setPriority } from "node:os";
13
13
  import { basename, dirname, join } from "node:path";
14
14
  import { randomUUID } from "node:crypto";
@@ -1117,6 +1117,30 @@ function findTodoPath(cwd) {
1117
1117
  /**
1118
1118
  * Session note creation, editing, checkpointing, renaming, and finalization.
1119
1119
  */
1120
+ /** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */
1121
+ function getMonthDir(notesDir) {
1122
+ const now = /* @__PURE__ */ new Date();
1123
+ const monthDir = join$1(notesDir, String(now.getFullYear()), String(now.getMonth() + 1).padStart(2, "0"));
1124
+ if (!existsSync$1(monthDir)) mkdirSync$1(monthDir, { recursive: true });
1125
+ return monthDir;
1126
+ }
1127
+ /**
1128
+ * Get the next note number (4-digit format: 0001, 0002, etc.).
1129
+ * Numbers are scoped per YYYY/MM directory.
1130
+ */
1131
+ function getNextNoteNumber(notesDir) {
1132
+ const files = readdirSync$1(getMonthDir(notesDir)).filter((f) => f.match(/^\d{3,4}[\s_-]/)).sort();
1133
+ if (files.length === 0) return "0001";
1134
+ let maxNumber = 0;
1135
+ for (const file of files) {
1136
+ const digitMatch = file.match(/^(\d+)/);
1137
+ if (digitMatch) {
1138
+ const num = parseInt(digitMatch[1], 10);
1139
+ if (num > maxNumber) maxNumber = num;
1140
+ }
1141
+ }
1142
+ return String(maxNumber + 1).padStart(4, "0");
1143
+ }
1120
1144
  /**
1121
1145
  * Get the current (latest) note file path, or null if none exists.
1122
1146
  * Searches current month → previous month → flat notesDir (legacy).
@@ -1139,6 +1163,41 @@ function getCurrentNotePath(notesDir) {
1139
1163
  if (prevFound) return prevFound;
1140
1164
  return findLatestIn(notesDir);
1141
1165
  }
1166
+ /**
1167
+ * Create a new session note.
1168
+ * Format: "NNNN - YYYY-MM-DD - New Session.md" filed into YYYY/MM subdirectory.
1169
+ * Claude MUST rename at session end with a meaningful description.
1170
+ */
1171
+ function createSessionNote(notesDir, description) {
1172
+ const noteNumber = getNextNoteNumber(notesDir);
1173
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1174
+ const monthDir = getMonthDir(notesDir);
1175
+ const filename = `${noteNumber} - ${date} - New Session.md`;
1176
+ const filepath = join$1(monthDir, filename);
1177
+ writeFileSync$1(filepath, `# Session ${noteNumber}: ${description}
1178
+
1179
+ **Date:** ${date}
1180
+ **Status:** In Progress
1181
+
1182
+ ---
1183
+
1184
+ ## Work Done
1185
+
1186
+ <!-- PAI will add completed work here during session -->
1187
+
1188
+ ---
1189
+
1190
+ ## Next Steps
1191
+
1192
+ <!-- To be filled at session end -->
1193
+
1194
+ ---
1195
+
1196
+ **Tags:** #Session
1197
+ `);
1198
+ console.error(`Created session note: ${filename}`);
1199
+ return filepath;
1200
+ }
1142
1201
  /** Add work items to the "Work Done" section of a session note. */
1143
1202
  function addWorkToSessionNote(notePath, workItems, sectionTitle) {
1144
1203
  if (!existsSync$1(notePath)) {
@@ -1342,6 +1401,540 @@ ${stateLines}
1342
1401
  console.error("TODO.md ## Continue section updated");
1343
1402
  }
1344
1403
 
1404
+ //#endregion
1405
+ //#region src/daemon/templates/session-summary-prompt.ts
1406
+ /**
1407
+ * Build the prompt string to send to Haiku for session summarization.
1408
+ *
1409
+ * Returns a single string suitable for piping to `claude --model haiku --print`.
1410
+ */
1411
+ function buildSessionSummaryPrompt(params) {
1412
+ const { userMessages, gitLog, cwd, date, filesModified, existingNote } = params;
1413
+ const userSection = userMessages.length > 0 ? userMessages.map((m, i) => `[${i + 1}] ${m}`).join("\n\n") : "(No user messages extracted)";
1414
+ const gitSection = gitLog.trim() || "(No git commits during this session)";
1415
+ const filesSection = filesModified && filesModified.length > 0 ? filesModified.map((f) => `- ${f}`).join("\n") : "";
1416
+ return `You are summarizing a coding session. Given the user messages and git commits below, write a session note.
1417
+
1418
+ Project directory: ${cwd}
1419
+ Date: ${date}
1420
+
1421
+ Focus on:
1422
+ - What problems were encountered and how they were solved
1423
+ - Key architectural decisions and their rationale
1424
+ - What was built (reference actual files and code patterns)
1425
+ - What was left unfinished or needs follow-up
1426
+
1427
+ Do NOT include:
1428
+ - Mechanical metadata (token counts, checkpoint timestamps)
1429
+ - System messages or tool results verbatim
1430
+ - Generic descriptions — be specific about what happened
1431
+ - Markdown frontmatter or YAML headers
1432
+ ${existingNote ? `\nAn existing session note is provided below. Merge the new information into it,
1433
+ preserving what was already written. Add new work items and update the summary.
1434
+ Do NOT duplicate existing content.
1435
+
1436
+ EXISTING NOTE:
1437
+ ${existingNote}
1438
+ ` : ""}
1439
+ Format your response EXACTLY as follows (no extra text before or after):
1440
+
1441
+ # Session: [Descriptive Title - 5-8 words summarizing the main accomplishment]
1442
+
1443
+ **Date:** ${date}
1444
+ **Status:** In Progress
1445
+
1446
+ ---
1447
+
1448
+ ## Work Done
1449
+
1450
+ [Organize by theme, not chronologically. Group related work under descriptive bullet points.
1451
+ Use checkbox format: - [x] for completed items, - [ ] for incomplete items.
1452
+ Include specific file names, function names, and technical details.]
1453
+
1454
+ ## Key Decisions
1455
+
1456
+ [List important choices made during the session with brief rationale.
1457
+ Skip this section entirely if no significant decisions were made.]
1458
+
1459
+ ## Known Issues
1460
+
1461
+ [What was left unfinished, bugs discovered, or follow-up items needed.
1462
+ Skip this section entirely if nothing is pending.]
1463
+
1464
+ ---
1465
+
1466
+ USER MESSAGES:
1467
+ ${userSection}
1468
+
1469
+ GIT COMMITS:
1470
+ ${gitSection}
1471
+ ${filesSection ? `\nFILES MODIFIED:\n${filesSection}` : ""}`;
1472
+ }
1473
+
1474
+ //#endregion
1475
+ //#region src/daemon/session-summary-worker.ts
1476
+ /**
1477
+ * session-summary-worker.ts — AI-powered session note generation
1478
+ *
1479
+ * Processes `session-summary` work items by:
1480
+ * 1. Finding the current session's JSONL transcript
1481
+ * 2. Extracting user messages and assistant context
1482
+ * 3. Gathering git commits from the session period
1483
+ * 4. Spawning Haiku via `claude` CLI to generate a structured summary
1484
+ * 5. Writing the summary to the project's session note
1485
+ *
1486
+ * Designed to run inside the daemon's work queue worker. All errors are
1487
+ * thrown (not swallowed) so the work queue retry logic handles them.
1488
+ */
1489
+ /** Minimum interval between summaries for the same project (ms). */
1490
+ const SUMMARY_COOLDOWN_MS = 1800 * 1e3;
1491
+ /** Maximum JSONL content to feed to Haiku (characters). */
1492
+ const MAX_JSONL_CHARS = 5e4;
1493
+ /** Maximum user messages to include in the prompt. */
1494
+ const MAX_USER_MESSAGES = 30;
1495
+ /** Timeout for the claude CLI process (ms). */
1496
+ const CLAUDE_TIMEOUT_MS = 6e4;
1497
+ /** File tracking last summary timestamps per project. */
1498
+ const COOLDOWN_FILE = join(homedir(), ".config", "pai", "summary-cooldowns.json");
1499
+ /** Claude Code projects directory. */
1500
+ const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
1501
+ function loadCooldowns() {
1502
+ try {
1503
+ if (existsSync(COOLDOWN_FILE)) return JSON.parse(readFileSync(COOLDOWN_FILE, "utf-8"));
1504
+ } catch {}
1505
+ return {};
1506
+ }
1507
+ function saveCooldowns(cooldowns) {
1508
+ try {
1509
+ writeFileSync(COOLDOWN_FILE, JSON.stringify(cooldowns, null, 2), "utf-8");
1510
+ } catch {}
1511
+ }
1512
+ function isOnCooldown(cwd) {
1513
+ const lastRun = loadCooldowns()[cwd];
1514
+ if (!lastRun) return false;
1515
+ return Date.now() - lastRun < SUMMARY_COOLDOWN_MS;
1516
+ }
1517
+ function markCooldown(cwd) {
1518
+ const cooldowns = loadCooldowns();
1519
+ cooldowns[cwd] = Date.now();
1520
+ const cutoff = Date.now() - 1440 * 60 * 1e3;
1521
+ for (const key of Object.keys(cooldowns)) if (cooldowns[key] < cutoff) delete cooldowns[key];
1522
+ saveCooldowns(cooldowns);
1523
+ }
1524
+ /**
1525
+ * Encode a cwd path the same way Claude Code does for its project directories.
1526
+ * Replaces /, space, dot, and hyphen with -.
1527
+ */
1528
+ function encodeProjectPath(cwd) {
1529
+ return cwd.replace(/[\/\s.\-]/g, "-");
1530
+ }
1531
+ /**
1532
+ * Find the most recently modified JSONL file for the given project.
1533
+ *
1534
+ * Claude Code stores transcripts in:
1535
+ * ~/.claude/projects/<encoded-path>/sessions/*.jsonl
1536
+ * ~/.claude/projects/<encoded-path>/<uuid>.jsonl (legacy)
1537
+ */
1538
+ function findLatestJsonl(cwd) {
1539
+ const projectDir = join(CLAUDE_PROJECTS_DIR, encodeProjectPath(cwd));
1540
+ if (!existsSync(projectDir)) {
1541
+ process.stderr.write(`[session-summary] No Claude project dir found: ${projectDir}\n`);
1542
+ return null;
1543
+ }
1544
+ const candidates = [];
1545
+ const sessionsDir = join(projectDir, "sessions");
1546
+ if (existsSync(sessionsDir)) try {
1547
+ for (const f of readdirSync(sessionsDir)) {
1548
+ if (!f.endsWith(".jsonl")) continue;
1549
+ const fullPath = join(sessionsDir, f);
1550
+ try {
1551
+ const st = statSync(fullPath);
1552
+ candidates.push({
1553
+ path: fullPath,
1554
+ mtime: st.mtimeMs
1555
+ });
1556
+ } catch {}
1557
+ }
1558
+ } catch {}
1559
+ try {
1560
+ for (const f of readdirSync(projectDir)) {
1561
+ if (!f.endsWith(".jsonl")) continue;
1562
+ const fullPath = join(projectDir, f);
1563
+ try {
1564
+ const st = statSync(fullPath);
1565
+ candidates.push({
1566
+ path: fullPath,
1567
+ mtime: st.mtimeMs
1568
+ });
1569
+ } catch {}
1570
+ }
1571
+ } catch {}
1572
+ if (candidates.length === 0) {
1573
+ process.stderr.write(`[session-summary] No JSONL files found in ${projectDir}\n`);
1574
+ return null;
1575
+ }
1576
+ candidates.sort((a, b) => b.mtime - a.mtime);
1577
+ return candidates[0].path;
1578
+ }
1579
+ /**
1580
+ * Parse a JSONL transcript and extract relevant content.
1581
+ * Filters noise, truncates to MAX_JSONL_CHARS from the end of the file.
1582
+ */
1583
+ function extractFromJsonl(jsonlPath) {
1584
+ const result = {
1585
+ userMessages: [],
1586
+ filesModified: [],
1587
+ sessionStartTime: ""
1588
+ };
1589
+ let raw;
1590
+ try {
1591
+ raw = readFileSync(jsonlPath, "utf-8");
1592
+ } catch (e) {
1593
+ throw new Error(`Could not read JSONL at ${jsonlPath}: ${e}`);
1594
+ }
1595
+ if (raw.length > MAX_JSONL_CHARS) {
1596
+ const truncPoint = raw.indexOf("\n", raw.length - MAX_JSONL_CHARS);
1597
+ raw = truncPoint >= 0 ? raw.slice(truncPoint + 1) : raw.slice(-MAX_JSONL_CHARS);
1598
+ }
1599
+ const lines = raw.trim().split("\n");
1600
+ const seenMessages = /* @__PURE__ */ new Set();
1601
+ for (const line of lines) {
1602
+ if (!line.trim()) continue;
1603
+ let entry;
1604
+ try {
1605
+ entry = JSON.parse(line);
1606
+ } catch {
1607
+ continue;
1608
+ }
1609
+ if (entry.timestamp && !result.sessionStartTime) result.sessionStartTime = String(entry.timestamp);
1610
+ if (entry.type === "user") {
1611
+ const msg = entry.message;
1612
+ if (msg?.content) {
1613
+ const text = contentToText$1(msg.content);
1614
+ if (text && !isNoise(text) && !seenMessages.has(text)) {
1615
+ seenMessages.add(text);
1616
+ result.userMessages.push(text.slice(0, 500));
1617
+ }
1618
+ }
1619
+ }
1620
+ if (entry.type === "assistant") {
1621
+ const msg = entry.message;
1622
+ if (msg?.content && Array.isArray(msg.content)) {
1623
+ for (const block of msg.content) if (block.type === "tool_use") {
1624
+ const name = block.name;
1625
+ const input = block.input;
1626
+ if ((name === "Edit" || name === "Write") && input?.file_path) {
1627
+ const fp = String(input.file_path);
1628
+ if (!result.filesModified.includes(fp)) result.filesModified.push(fp);
1629
+ }
1630
+ }
1631
+ }
1632
+ }
1633
+ }
1634
+ if (result.userMessages.length > MAX_USER_MESSAGES) result.userMessages = result.userMessages.slice(-MAX_USER_MESSAGES);
1635
+ return result;
1636
+ }
1637
+ /** Convert Claude content (string or content block array) to plain text. */
1638
+ function contentToText$1(content) {
1639
+ if (typeof content === "string") return content;
1640
+ if (Array.isArray(content)) return content.map((c) => {
1641
+ if (typeof c === "string") return c;
1642
+ const block = c;
1643
+ if (block?.text) return String(block.text);
1644
+ if (block?.content) return String(block.content);
1645
+ return "";
1646
+ }).join(" ").trim();
1647
+ return "";
1648
+ }
1649
+ /** Filter out noise entries that shouldn't be included in the summary. */
1650
+ function isNoise(text) {
1651
+ if (!text || text.length < 3) return true;
1652
+ if (text.includes("<task-notification>")) return true;
1653
+ if (text.includes("[object Object]")) return true;
1654
+ if (text.startsWith("<system-reminder>")) return true;
1655
+ if (/^(yes|ok|sure|go|continue|weiter|thanks|thank you)\.?$/i.test(text.trim())) return true;
1656
+ if (text.startsWith("Tool Result:") || text.startsWith("tool_result")) return true;
1657
+ return false;
1658
+ }
1659
+ /**
1660
+ * Get git log for the session period.
1661
+ * Falls back gracefully if git is not available or the dir is not a repo.
1662
+ */
1663
+ async function getGitContext(cwd, sinceTime) {
1664
+ let since = "6 hours ago";
1665
+ if (sinceTime) {
1666
+ const asNum = Number(sinceTime);
1667
+ if (!isNaN(asNum) && asNum > 1e9) since = (/* @__PURE__ */ new Date(asNum * 1e3)).toISOString();
1668
+ else since = sinceTime;
1669
+ }
1670
+ try {
1671
+ const { execFile: execFileCb } = await import("node:child_process");
1672
+ const { promisify } = await import("node:util");
1673
+ const { stdout } = await promisify(execFileCb)("git", [
1674
+ "log",
1675
+ "--format=%h %ai %s",
1676
+ `--since=${since}`,
1677
+ "--stat",
1678
+ "--no-color"
1679
+ ], {
1680
+ cwd,
1681
+ timeout: 1e4,
1682
+ env: {
1683
+ ...process.env,
1684
+ GIT_TERMINAL_PROMPT: "0"
1685
+ }
1686
+ });
1687
+ return stdout.trim();
1688
+ } catch {
1689
+ return "";
1690
+ }
1691
+ }
1692
+ /**
1693
+ * Find the `claude` CLI binary.
1694
+ * Checks PATH first, then common installation locations.
1695
+ */
1696
+ function findClaudeBinary() {
1697
+ const candidates = [
1698
+ "claude",
1699
+ join(homedir(), ".claude", "local", "claude"),
1700
+ "/usr/local/bin/claude",
1701
+ join(homedir(), ".local", "bin", "claude")
1702
+ ];
1703
+ for (const candidate of candidates) try {
1704
+ if (candidate === "claude") return candidate;
1705
+ if (existsSync(candidate)) return candidate;
1706
+ } catch {}
1707
+ return null;
1708
+ }
1709
+ /**
1710
+ * Spawn Haiku via the claude CLI to generate a session summary.
1711
+ * Pipes the prompt via stdin to `claude --model haiku --print --no-input`.
1712
+ * Returns the generated text, or null if spawning fails.
1713
+ */
1714
+ async function spawnHaikuSummarizer(prompt) {
1715
+ const claudeBin = findClaudeBinary();
1716
+ if (!claudeBin) {
1717
+ process.stderr.write("[session-summary] Claude CLI not found in PATH or common locations.\n");
1718
+ return null;
1719
+ }
1720
+ const { spawn } = await import("node:child_process");
1721
+ return new Promise((resolve) => {
1722
+ let timer = null;
1723
+ const child = spawn(claudeBin, [
1724
+ "--model",
1725
+ "haiku",
1726
+ "-p",
1727
+ "--no-session-persistence"
1728
+ ], {
1729
+ env: { ...process.env },
1730
+ stdio: [
1731
+ "pipe",
1732
+ "pipe",
1733
+ "pipe"
1734
+ ]
1735
+ });
1736
+ let stdout = "";
1737
+ let stderr = "";
1738
+ child.stdout.on("data", (chunk) => {
1739
+ stdout += chunk.toString();
1740
+ });
1741
+ child.stderr.on("data", (chunk) => {
1742
+ stderr += chunk.toString();
1743
+ });
1744
+ child.on("error", (err) => {
1745
+ if (timer) {
1746
+ clearTimeout(timer);
1747
+ timer = null;
1748
+ }
1749
+ process.stderr.write(`[session-summary] Haiku spawn error: ${err.message}\n`);
1750
+ resolve(null);
1751
+ });
1752
+ child.on("close", (code) => {
1753
+ if (timer) {
1754
+ clearTimeout(timer);
1755
+ timer = null;
1756
+ }
1757
+ if (code !== 0) {
1758
+ process.stderr.write(`[session-summary] Haiku exited with code ${code}: ${stderr.slice(0, 300)}\n`);
1759
+ resolve(null);
1760
+ } else resolve(stdout.trim() || null);
1761
+ });
1762
+ timer = setTimeout(() => {
1763
+ process.stderr.write("[session-summary] Haiku timed out — killing process.\n");
1764
+ child.kill("SIGTERM");
1765
+ resolve(null);
1766
+ }, CLAUDE_TIMEOUT_MS);
1767
+ child.stdin.write(prompt);
1768
+ child.stdin.end();
1769
+ });
1770
+ }
1771
+ /**
1772
+ * Write (or update) the session note with the AI-generated summary.
1773
+ *
1774
+ * Strategy:
1775
+ * - Find the current month's latest note
1776
+ * - If it's from today, update it with the new summary
1777
+ * - If it's from a different day, create a new note
1778
+ */
1779
+ function writeSessionNote(cwd, summaryText, filesModified) {
1780
+ const notesInfo = findNotesDir(cwd);
1781
+ let notePath = getCurrentNotePath(notesInfo.path);
1782
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1783
+ if (notePath) {
1784
+ const noteFilename = basename(notePath);
1785
+ const dateMatch = noteFilename.match(/(\d{4}-\d{2}-\d{2})/);
1786
+ if ((dateMatch ? dateMatch[1] : "") === today) {
1787
+ updateNoteWithSummary(notePath, summaryText);
1788
+ process.stderr.write(`[session-summary] Updated existing note: ${noteFilename}\n`);
1789
+ } else notePath = createNoteFromSummary(notesInfo.path, summaryText);
1790
+ } else notePath = createNoteFromSummary(notesInfo.path, summaryText);
1791
+ if (notePath) {
1792
+ const titleMatch = summaryText.match(/^# Session:\s*(.+)$/m);
1793
+ if (titleMatch) {
1794
+ const title = titleMatch[1].trim();
1795
+ if (title.length > 5 && title.length < 80) {
1796
+ const newPath = renameSessionNote(notePath, title);
1797
+ if (newPath !== notePath) notePath = newPath;
1798
+ }
1799
+ }
1800
+ }
1801
+ return notePath;
1802
+ }
1803
+ /**
1804
+ * Update an existing session note's Work Done section with AI-generated content.
1805
+ */
1806
+ function updateNoteWithSummary(notePath, summaryText) {
1807
+ if (!existsSync(notePath)) return;
1808
+ let content = readFileSync(notePath, "utf-8");
1809
+ const workDoneMatch = summaryText.match(/## Work Done\n\n([\s\S]*?)(?=\n## Key Decisions|\n## Known Issues|\n\*\*Tags|\n$)/);
1810
+ if (workDoneMatch) {
1811
+ const aiWorkContent = workDoneMatch[1].trim();
1812
+ const sectionHeader = `\n### AI Summary (${(/* @__PURE__ */ new Date()).toISOString().split("T")[1].split(".")[0]})\n\n${aiWorkContent}\n`;
1813
+ const nextStepsIdx = content.indexOf("## Next Steps");
1814
+ const knownIssuesIdx = content.indexOf("## Known Issues");
1815
+ const insertBefore = knownIssuesIdx !== -1 ? knownIssuesIdx : nextStepsIdx !== -1 ? nextStepsIdx : content.length;
1816
+ content = content.slice(0, insertBefore) + sectionHeader + "\n" + content.slice(insertBefore);
1817
+ }
1818
+ const decisionsMatch = summaryText.match(/## Key Decisions\n\n([\s\S]*?)(?=\n## Known Issues|\n\*\*Tags|\n$)/);
1819
+ if (decisionsMatch) {
1820
+ const decisions = decisionsMatch[1].trim();
1821
+ if (decisions && !content.includes("## Key Decisions")) {
1822
+ const nextStepsIdx = content.indexOf("## Next Steps");
1823
+ const insertAt = nextStepsIdx !== -1 ? nextStepsIdx : content.length;
1824
+ content = content.slice(0, insertAt) + `## Key Decisions\n\n${decisions}\n\n` + content.slice(insertAt);
1825
+ }
1826
+ }
1827
+ const issuesMatch = summaryText.match(/## Known Issues\n\n([\s\S]*?)(?=\n\*\*Tags|\n$)/);
1828
+ if (issuesMatch) {
1829
+ const issues = issuesMatch[1].trim();
1830
+ if (issues && !content.includes("## Known Issues")) {
1831
+ const nextStepsIdx = content.indexOf("## Next Steps");
1832
+ const insertAt = nextStepsIdx !== -1 ? nextStepsIdx : content.length;
1833
+ content = content.slice(0, insertAt) + `## Known Issues\n\n${issues}\n\n` + content.slice(insertAt);
1834
+ }
1835
+ }
1836
+ writeFileSync(notePath, content, "utf-8");
1837
+ }
1838
+ /**
1839
+ * Create a brand new session note from the AI summary.
1840
+ */
1841
+ function createNoteFromSummary(notesDir, summaryText) {
1842
+ try {
1843
+ const notePath = createSessionNote(notesDir, "New Session");
1844
+ const noteFilename = basename(notePath);
1845
+ const numberMatch = noteFilename.match(/^(\d+)/);
1846
+ const noteNumber = numberMatch ? numberMatch[1] : "0000";
1847
+ const titleMatch = summaryText.match(/^# Session:\s*(.+)$/m);
1848
+ writeFileSync(notePath, `# Session ${noteNumber}: ${titleMatch ? titleMatch[1].trim() : "New Session"}
1849
+
1850
+ **Date:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
1851
+ **Status:** In Progress
1852
+
1853
+ ---
1854
+
1855
+ ${summaryText.replace(/^# Session:.*$/m, "").replace(/^\*\*Date:\*\*.*$/m, "").replace(/^\*\*Status:\*\*.*$/m, "").replace(/^---$/m, "").trim()}
1856
+
1857
+ ---
1858
+
1859
+ ## Next Steps
1860
+
1861
+ <!-- To be filled at session end -->
1862
+
1863
+ ---
1864
+
1865
+ **Tags:** #Session
1866
+ `, "utf-8");
1867
+ process.stderr.write(`[session-summary] Created AI-powered note: ${noteFilename}\n`);
1868
+ return notePath;
1869
+ } catch (e) {
1870
+ process.stderr.write(`[session-summary] Failed to create note: ${e}\n`);
1871
+ return null;
1872
+ }
1873
+ }
1874
+ /**
1875
+ * Process a `session-summary` work item.
1876
+ *
1877
+ * This is the main function called by work-queue-worker.ts.
1878
+ * Throws on fatal errors (work queue will retry with backoff).
1879
+ */
1880
+ async function handleSessionSummary(payload) {
1881
+ const { cwd, sessionId, projectSlug, transcriptPath, force } = payload;
1882
+ if (!cwd) throw new Error("session-summary payload missing cwd");
1883
+ process.stderr.write(`[session-summary] Starting for ${cwd}${sessionId ? ` (session=${sessionId})` : ""}${force ? " (force=true)" : ""}\n`);
1884
+ if (!force && isOnCooldown(cwd)) {
1885
+ process.stderr.write("[session-summary] Skipping — last summary was less than 30 minutes ago.\n");
1886
+ return;
1887
+ }
1888
+ let jsonlPath = transcriptPath || null;
1889
+ if (jsonlPath && !existsSync(jsonlPath)) {
1890
+ process.stderr.write(`[session-summary] Provided transcript path not found: ${jsonlPath}\n`);
1891
+ jsonlPath = null;
1892
+ }
1893
+ if (!jsonlPath) jsonlPath = findLatestJsonl(cwd);
1894
+ if (!jsonlPath) {
1895
+ process.stderr.write("[session-summary] No JSONL transcript found — skipping.\n");
1896
+ return;
1897
+ }
1898
+ process.stderr.write(`[session-summary] Using transcript: ${jsonlPath}\n`);
1899
+ const extracted = extractFromJsonl(jsonlPath);
1900
+ if (extracted.userMessages.length === 0) {
1901
+ process.stderr.write("[session-summary] No user messages found in transcript — skipping.\n");
1902
+ return;
1903
+ }
1904
+ process.stderr.write(`[session-summary] Extracted ${extracted.userMessages.length} user messages, ${extracted.filesModified.length} modified files.\n`);
1905
+ const gitLog = await getGitContext(cwd, extracted.sessionStartTime);
1906
+ if (gitLog) process.stderr.write(`[session-summary] Got git context (${gitLog.split("\n").length} lines).\n`);
1907
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1908
+ const existingNotePath = getCurrentNotePath(findNotesDir(cwd).path);
1909
+ let existingNote;
1910
+ if (existingNotePath) {
1911
+ const dateMatch = basename(existingNotePath).match(/(\d{4}-\d{2}-\d{2})/);
1912
+ if (dateMatch && dateMatch[1] === today) try {
1913
+ existingNote = readFileSync(existingNotePath, "utf-8");
1914
+ } catch {}
1915
+ }
1916
+ const prompt = buildSessionSummaryPrompt({
1917
+ userMessages: extracted.userMessages,
1918
+ gitLog,
1919
+ cwd,
1920
+ date: today,
1921
+ filesModified: extracted.filesModified,
1922
+ existingNote
1923
+ });
1924
+ process.stderr.write(`[session-summary] Sending ${prompt.length} char prompt to Haiku...\n`);
1925
+ const summaryText = await spawnHaikuSummarizer(prompt);
1926
+ if (!summaryText) {
1927
+ process.stderr.write("[session-summary] Haiku did not produce output — falling back to mechanical checkpoint.\n");
1928
+ markCooldown(cwd);
1929
+ return;
1930
+ }
1931
+ process.stderr.write(`[session-summary] Haiku produced ${summaryText.length} char summary.\n`);
1932
+ const notePath = writeSessionNote(cwd, summaryText, extracted.filesModified);
1933
+ if (notePath) process.stderr.write(`[session-summary] Session note written: ${basename(notePath)}\n`);
1934
+ markCooldown(cwd);
1935
+ process.stderr.write("[session-summary] Done.\n");
1936
+ }
1937
+
1345
1938
  //#endregion
1346
1939
  //#region src/daemon/work-queue-worker.ts
1347
1940
  /**
@@ -1350,8 +1943,9 @@ ${stateLines}
1350
1943
  * Runs every 5 seconds to drain the queue.
1351
1944
  * Handles 'session-end' work items by reading the transcript, extracting
1352
1945
  * work summaries, updating the session note, and updating TODO.md.
1946
+ * Handles 'session-summary' items by spawning Haiku for AI-powered note generation.
1353
1947
  *
1354
- * Other item types (session-summary, note-update, todo-update, topic-detect)
1948
+ * Other item types (note-update, todo-update, topic-detect)
1355
1949
  * are stubs — they log and complete immediately, ready for future expansion.
1356
1950
  */
1357
1951
  const WORKER_INTERVAL_MS = 5e3;
@@ -1405,6 +1999,8 @@ async function processNextItem() {
1405
1999
  await handleSessionEnd(item);
1406
2000
  break;
1407
2001
  case "session-summary":
2002
+ await handleSessionSummary(item.payload);
2003
+ break;
1408
2004
  case "note-update":
1409
2005
  case "todo-update":
1410
2006
  case "topic-detect":
@@ -2159,4 +2755,4 @@ var daemon_exports = /* @__PURE__ */ __exportAll({ serve: () => serve });
2159
2755
 
2160
2756
  //#endregion
2161
2757
  export { serve as n, daemon_exports as t };
2162
- //# sourceMappingURL=daemon-DuGlDnV7.mjs.map
2758
+ //# sourceMappingURL=daemon-D8ZxcFhU.mjs.map