context-mode 1.0.99 → 1.0.100

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 (46) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/build/pi-extension.js +1 -1
  6. package/build/search/auto-memory.d.ts +29 -0
  7. package/build/search/auto-memory.js +121 -0
  8. package/build/search/unified.d.ts +41 -0
  9. package/build/search/unified.js +89 -0
  10. package/build/server.js +69 -21
  11. package/build/session/analytics.js +1 -1
  12. package/build/session/db.d.ts +17 -0
  13. package/build/session/db.js +28 -0
  14. package/build/session/extract.d.ts +4 -0
  15. package/build/session/extract.js +232 -1
  16. package/build/session/snapshot.js +31 -0
  17. package/build/store.js +67 -4
  18. package/build/types.d.ts +1 -0
  19. package/cli.bundle.mjs +254 -119
  20. package/configs/claude-code/CLAUDE.md +21 -1
  21. package/configs/codex/AGENTS.md +22 -1
  22. package/configs/cursor/context-mode.mdc +18 -1
  23. package/configs/gemini-cli/GEMINI.md +22 -1
  24. package/configs/jetbrains-copilot/copilot-instructions.md +22 -1
  25. package/configs/kilo/AGENTS.md +19 -2
  26. package/configs/kiro/KIRO.md +18 -1
  27. package/configs/openclaw/AGENTS.md +22 -2
  28. package/configs/opencode/AGENTS.md +18 -1
  29. package/configs/pi/AGENTS.md +18 -1
  30. package/configs/qwen-code/QWEN.md +38 -18
  31. package/configs/vscode-copilot/copilot-instructions.md +22 -1
  32. package/hooks/auto-injection.mjs +76 -0
  33. package/hooks/codex/userpromptsubmit.mjs +1 -1
  34. package/hooks/core/mcp-ready.mjs +7 -1
  35. package/hooks/posttooluse.mjs +50 -1
  36. package/hooks/precompact.mjs +9 -0
  37. package/hooks/pretooluse.mjs +27 -0
  38. package/hooks/routing-block.mjs +7 -1
  39. package/hooks/session-db.bundle.mjs +19 -13
  40. package/hooks/session-extract.bundle.mjs +2 -2
  41. package/hooks/session-snapshot.bundle.mjs +18 -17
  42. package/hooks/sessionstart.mjs +17 -0
  43. package/hooks/userpromptsubmit.mjs +1 -1
  44. package/openclaw.plugin.json +1 -1
  45. package/package.json +1 -1
  46. package/server.bundle.mjs +227 -92
@@ -335,9 +335,36 @@ function extractSkill(input) {
335
335
  type: "skill",
336
336
  category: "skill",
337
337
  data: safeString(skillName),
338
- priority: 3,
338
+ priority: 2,
339
339
  }];
340
340
  }
341
+ /**
342
+ * Category 16: constraint
343
+ * Constraints discovered through error events — tool failures reveal
344
+ * platform/environment limitations worth remembering.
345
+ */
346
+ function extractConstraint(input) {
347
+ // Only fire on error events — constraints are discovered through failures
348
+ if (!input.tool_response?.includes("Error") && !input.tool_output?.isError)
349
+ return [];
350
+ const response = String(input.tool_response || "");
351
+ const patterns = [/not supported/i, /cannot/i, /does not support/i, /FAIL/i, /refused/i, /permission denied/i, /incompatible/i];
352
+ for (const pattern of patterns) {
353
+ const match = response.match(pattern);
354
+ if (match) {
355
+ // Extract context around the match
356
+ const idx = response.toLowerCase().indexOf(match[0].toLowerCase());
357
+ const context = response.slice(Math.max(0, idx - 50), Math.min(response.length, idx + 200)).trim();
358
+ return [{
359
+ type: "constraint_discovered",
360
+ category: "constraint",
361
+ data: safeString(context),
362
+ priority: 2,
363
+ }];
364
+ }
365
+ }
366
+ return [];
367
+ }
341
368
  /**
342
369
  * Category 9: subagent
343
370
  * Agent tool calls — tracks both launch and completion.
@@ -410,6 +437,67 @@ function extractDecision(input) {
410
437
  priority: 2,
411
438
  }];
412
439
  }
440
+ /**
441
+ * Category 22: agent-finding
442
+ * When the Agent tool completes (subagent returns), capture a structured
443
+ * summary of its findings (first 500 chars of tool_response).
444
+ */
445
+ function extractAgentFinding(input) {
446
+ if (input.tool_name !== "Agent")
447
+ return [];
448
+ if (!input.tool_response || input.tool_response.length === 0)
449
+ return [];
450
+ const summary = input.tool_response.length > 500
451
+ ? input.tool_response.slice(0, 500)
452
+ : input.tool_response;
453
+ return [{
454
+ type: "agent_finding",
455
+ category: "agent-finding",
456
+ data: safeString(summary),
457
+ priority: 2,
458
+ }];
459
+ }
460
+ /**
461
+ * Category 24: external-ref
462
+ * Scan tool_input and tool_response for external URLs, GitHub issues, and PRs.
463
+ * Deduplicates found refs and skips internal URLs (localhost, 127.0.0.1).
464
+ */
465
+ function extractExternalRef(input) {
466
+ const haystack = [
467
+ safeStringAny(input.tool_input),
468
+ safeString(input.tool_response),
469
+ ].join(" ");
470
+ if (haystack.length === 0)
471
+ return [];
472
+ const refs = new Set();
473
+ // URLs — skip localhost / 127.0.0.1
474
+ const urlMatches = haystack.match(/https?:\/\/[^\s)]+/g);
475
+ if (urlMatches) {
476
+ for (let url of urlMatches) {
477
+ // Strip trailing punctuation that gets captured from JSON/prose
478
+ url = url.replace(/["'})\],;.]+$/, "");
479
+ if (!/localhost|127\.0\.0\.1/i.test(url)) {
480
+ refs.add(url);
481
+ }
482
+ }
483
+ }
484
+ // Full GitHub issue/PR URLs are already captured above.
485
+ // Shorthand GitHub issue refs: #123 (only bare, not inside a URL)
486
+ const issueMatches = haystack.match(/(?<!\w)#(\d+)/g);
487
+ if (issueMatches) {
488
+ for (const m of issueMatches) {
489
+ refs.add(m);
490
+ }
491
+ }
492
+ if (refs.size === 0)
493
+ return [];
494
+ return [{
495
+ type: "external_ref",
496
+ category: "external-ref",
497
+ data: safeString(Array.from(refs).join(", ")),
498
+ priority: 3,
499
+ }];
500
+ }
413
501
  /**
414
502
  * Category 8: env (worktree)
415
503
  * EnterWorktree tool — tracks worktree creation.
@@ -490,6 +578,52 @@ function extractIntent(message) {
490
578
  priority: 4,
491
579
  }];
492
580
  }
581
+ /**
582
+ * Category 25: blocked-on
583
+ * Detect when work is blocked on something, or when a blocker is resolved.
584
+ */
585
+ const BLOCKER_PATTERNS = [
586
+ /\bblocked on\b/i,
587
+ /\bwaiting for\b/i,
588
+ /\bneed\s+\S+\s+before\b/i,
589
+ /\bcan'?t proceed until\b/i,
590
+ /\bdepends on\b/i,
591
+ /\bblocked\b/i,
592
+ // Turkish patterns
593
+ /\bbekliyor\b/i,
594
+ /\bbekliyorum\b/i,
595
+ ];
596
+ const BLOCKER_RESOLVED_PATTERNS = [
597
+ /\bunblocked\b/i,
598
+ /\bresolved\b/i,
599
+ /\bgot the\s+\S+/i,
600
+ /\bis ready now\b/i,
601
+ /\bcan proceed\b/i,
602
+ ];
603
+ function extractBlocker(message) {
604
+ const events = [];
605
+ // Check resolution first — if both match, resolution takes priority
606
+ const isResolved = BLOCKER_RESOLVED_PATTERNS.some(p => p.test(message));
607
+ if (isResolved) {
608
+ events.push({
609
+ type: "blocker_resolved",
610
+ category: "blocked-on",
611
+ data: safeString(message),
612
+ priority: 2,
613
+ });
614
+ return events;
615
+ }
616
+ const isBlocked = BLOCKER_PATTERNS.some(p => p.test(message));
617
+ if (isBlocked) {
618
+ events.push({
619
+ type: "blocker",
620
+ category: "blocked-on",
621
+ data: safeString(message),
622
+ priority: 2,
623
+ });
624
+ }
625
+ return events;
626
+ }
493
627
  /**
494
628
  * Category 12: data
495
629
  * Large user-pasted data references (message > 1KB).
@@ -504,6 +638,96 @@ function extractData(message) {
504
638
  priority: 4,
505
639
  }];
506
640
  }
641
+ // ── Cross-event stateful extractors ───────────────────────────────────────
642
+ /**
643
+ * Category 23: error-resolution
644
+ * Detects when an error is followed by a successful fix (cross-event state).
645
+ */
646
+ let lastError = null;
647
+ function extractErrorResolution(input) {
648
+ const { tool_name, tool_response, tool_output } = input;
649
+ const response = String(tool_response ?? "");
650
+ const isErrorFlag = tool_output?.isError === true;
651
+ const isBashError = tool_name === "Bash" &&
652
+ /exit code [1-9]|error:|Error:|FAIL|failed/i.test(response);
653
+ // If this call is an error, store it and return
654
+ if (isBashError || isErrorFlag) {
655
+ lastError = { tool: tool_name, error: response.slice(0, 200), callsSince: 0 };
656
+ return [];
657
+ }
658
+ // No pending error → nothing to resolve
659
+ if (!lastError)
660
+ return [];
661
+ // Increment staleness counter
662
+ lastError.callsSince++;
663
+ // Timeout: clear after 10 calls without resolution
664
+ if (lastError.callsSince > 10) {
665
+ lastError = null;
666
+ return [];
667
+ }
668
+ // Check if this is a resolution: same tool, or Edit/Write after a Read error
669
+ const sameTool = tool_name === lastError.tool;
670
+ const editAfterReadError = lastError.tool === "Read" && (tool_name === "Edit" || tool_name === "Write");
671
+ if (sameTool || editAfterReadError) {
672
+ const event = {
673
+ type: "error_resolved",
674
+ category: "error-resolution",
675
+ data: safeString(`Error in ${lastError.tool}: ${lastError.error} → Fixed`),
676
+ priority: 2,
677
+ };
678
+ lastError = null;
679
+ return [event];
680
+ }
681
+ return [];
682
+ }
683
+ /** Reset error-resolution state (for testing). */
684
+ export function resetErrorResolutionState() {
685
+ lastError = null;
686
+ }
687
+ /**
688
+ * Category 26: iteration-loop
689
+ * Detects when the same tool is called repeatedly with similar input (stuck loop).
690
+ */
691
+ const callHistory = [];
692
+ function simpleHash(str) {
693
+ return `${str.length}:${str.slice(0, 20)}`;
694
+ }
695
+ function extractIterationLoop(input) {
696
+ const { tool_name, tool_input } = input;
697
+ const inputHash = simpleHash(JSON.stringify(tool_input).slice(0, 200));
698
+ callHistory.push({ tool: tool_name, inputHash });
699
+ // Keep history bounded
700
+ if (callHistory.length > 50) {
701
+ callHistory.splice(0, callHistory.length - 50);
702
+ }
703
+ // Check last N entries for repeated pattern (minimum 3)
704
+ if (callHistory.length < 3)
705
+ return [];
706
+ let count = 0;
707
+ for (let i = callHistory.length - 1; i >= 0; i--) {
708
+ if (callHistory[i].tool === tool_name && callHistory[i].inputHash === inputHash) {
709
+ count++;
710
+ }
711
+ else {
712
+ break;
713
+ }
714
+ }
715
+ if (count >= 3) {
716
+ // Reset the matching tail to avoid duplicate emissions
717
+ callHistory.splice(callHistory.length - count);
718
+ return [{
719
+ type: "retry_detected",
720
+ category: "iteration-loop",
721
+ data: safeString(`${tool_name} called ${count} times with similar input`),
722
+ priority: 2,
723
+ }];
724
+ }
725
+ return [];
726
+ }
727
+ /** Reset iteration-loop state (for testing). */
728
+ export function resetIterationLoopState() {
729
+ callHistory.length = 0;
730
+ }
507
731
  // ── Public API ─────────────────────────────────────────────────────────────
508
732
  /**
509
733
  * Extract session events from a PostToolUse hook input.
@@ -528,7 +752,13 @@ export function extractEvents(input) {
528
752
  events.push(...extractSubagent(input));
529
753
  events.push(...extractMcp(input));
530
754
  events.push(...extractDecision(input));
755
+ events.push(...extractConstraint(input));
531
756
  events.push(...extractWorktree(input));
757
+ events.push(...extractAgentFinding(input));
758
+ events.push(...extractExternalRef(input));
759
+ // Cross-event stateful extractors
760
+ events.push(...extractErrorResolution(input));
761
+ events.push(...extractIterationLoop(input));
532
762
  return events;
533
763
  }
534
764
  catch {
@@ -548,6 +778,7 @@ export function extractUserEvents(message) {
548
778
  events.push(...extractUserDecision(message));
549
779
  events.push(...extractRole(message));
550
780
  events.push(...extractIntent(message));
781
+ events.push(...extractBlocker(message));
551
782
  events.push(...extractData(message));
552
783
  return events;
553
784
  }
@@ -312,6 +312,30 @@ function buildSkillsSection(skillEvents, searchTool) {
312
312
  ];
313
313
  return lines.join("\n");
314
314
  }
315
+ function buildRolesSection(roleEvents, searchTool) {
316
+ if (roleEvents.length === 0)
317
+ return "";
318
+ const seen = new Set();
319
+ const summaryLines = [];
320
+ const queryTerms = [];
321
+ for (const ev of roleEvents) {
322
+ if (seen.has(ev.data))
323
+ continue;
324
+ seen.add(ev.data);
325
+ summaryLines.push(` ${escapeXML(ev.data)}`);
326
+ queryTerms.push(ev.data);
327
+ }
328
+ if (summaryLines.length === 0)
329
+ return "";
330
+ const queries = buildQueries(queryTerms);
331
+ const lines = [
332
+ ` <roles count="${summaryLines.length}">`,
333
+ ...summaryLines,
334
+ toolCall(searchTool, queries),
335
+ ` </roles>`,
336
+ ];
337
+ return lines.join("\n");
338
+ }
315
339
  function buildIntentSection(intentEvents) {
316
340
  if (intentEvents.length === 0)
317
341
  return "";
@@ -344,6 +368,7 @@ export function buildResumeSnapshot(events, opts) {
344
368
  const subagentEvents = [];
345
369
  const intentEvents = [];
346
370
  const skillEvents = [];
371
+ const roleEvents = [];
347
372
  for (const ev of events) {
348
373
  switch (ev.category) {
349
374
  case "file":
@@ -379,6 +404,9 @@ export function buildResumeSnapshot(events, opts) {
379
404
  case "skill":
380
405
  skillEvents.push(ev);
381
406
  break;
407
+ case "role":
408
+ roleEvents.push(ev);
409
+ break;
382
410
  }
383
411
  }
384
412
  // ── Build all sections ──
@@ -417,6 +445,9 @@ export function buildResumeSnapshot(events, opts) {
417
445
  const skills = buildSkillsSection(skillEvents, searchTool);
418
446
  if (skills)
419
447
  sections.push(skills);
448
+ const roles = buildRolesSection(roleEvents, searchTool);
449
+ if (roles)
450
+ sections.push(roles);
420
451
  const intent = buildIntentSection(intentEvents);
421
452
  if (intent)
422
453
  sections.push(intent);
package/build/store.js CHANGED
@@ -402,6 +402,10 @@ export class ContentStore {
402
402
  content,
403
403
  source_id UNINDEXED,
404
404
  content_type UNINDEXED,
405
+ source_category UNINDEXED,
406
+ session_id UNINDEXED,
407
+ event_id UNINDEXED,
408
+ timestamp UNINDEXED,
405
409
  tokenize='porter unicode61'
406
410
  );
407
411
 
@@ -410,6 +414,10 @@ export class ContentStore {
410
414
  content,
411
415
  source_id UNINDEXED,
412
416
  content_type UNINDEXED,
417
+ source_category UNINDEXED,
418
+ session_id UNINDEXED,
419
+ event_id UNINDEXED,
420
+ timestamp UNINDEXED,
413
421
  tokenize='trigram'
414
422
  );
415
423
 
@@ -419,6 +427,47 @@ export class ContentStore {
419
427
 
420
428
  CREATE INDEX IF NOT EXISTS idx_sources_label ON sources(label);
421
429
  `);
430
+ // FTS5 schema migration: old schema (4 cols) → new schema (8 cols).
431
+ // FTS5 virtual tables do not support ALTER TABLE ADD COLUMN, so we must
432
+ // DROP + re-CREATE. Detection: check for sentinel column `source_category`
433
+ // via pragma_table_xinfo. Three states:
434
+ // 1. No table → CREATE above handled it (fresh DB)
435
+ // 2. Old schema (4 cols) → DROP + CREATE new
436
+ // 3. New schema (8 cols) → do nothing
437
+ try {
438
+ const cols = this.#db.prepare("SELECT name FROM pragma_table_xinfo('chunks')").all();
439
+ const colNames = new Set(cols.map(c => c.name));
440
+ if (cols.length > 0 && !colNames.has("source_category")) {
441
+ // Old schema detected — drop both FTS5 tables and re-create with new columns
442
+ this.#db.exec("DROP TABLE IF EXISTS chunks");
443
+ this.#db.exec("DROP TABLE IF EXISTS chunks_trigram");
444
+ this.#db.exec(`
445
+ CREATE VIRTUAL TABLE chunks USING fts5(
446
+ title,
447
+ content,
448
+ source_id UNINDEXED,
449
+ content_type UNINDEXED,
450
+ source_category UNINDEXED,
451
+ session_id UNINDEXED,
452
+ event_id UNINDEXED,
453
+ timestamp UNINDEXED,
454
+ tokenize='porter unicode61'
455
+ );
456
+ CREATE VIRTUAL TABLE chunks_trigram USING fts5(
457
+ title,
458
+ content,
459
+ source_id UNINDEXED,
460
+ content_type UNINDEXED,
461
+ source_category UNINDEXED,
462
+ session_id UNINDEXED,
463
+ event_id UNINDEXED,
464
+ timestamp UNINDEXED,
465
+ tokenize='trigram'
466
+ );
467
+ `);
468
+ }
469
+ }
470
+ catch { /* pragma_table_xinfo may fail if table doesn't exist yet — safe to ignore */ }
422
471
  // Stale detection columns — safe for existing DBs (ALTER is O(1) in SQLite)
423
472
  try {
424
473
  this.#db.exec("ALTER TABLE sources ADD COLUMN file_path TEXT");
@@ -433,8 +482,8 @@ export class ContentStore {
433
482
  // Write path
434
483
  this.#stmtInsertSourceEmpty = this.#db.prepare("INSERT INTO sources (label, chunk_count, code_chunk_count, file_path, content_hash) VALUES (?, 0, 0, ?, ?)");
435
484
  this.#stmtInsertSource = this.#db.prepare("INSERT INTO sources (label, chunk_count, code_chunk_count, file_path, content_hash) VALUES (?, ?, ?, ?, ?)");
436
- this.#stmtInsertChunk = this.#db.prepare("INSERT INTO chunks (title, content, source_id, content_type) VALUES (?, ?, ?, ?)");
437
- this.#stmtInsertChunkTrigram = this.#db.prepare("INSERT INTO chunks_trigram (title, content, source_id, content_type) VALUES (?, ?, ?, ?)");
485
+ this.#stmtInsertChunk = this.#db.prepare("INSERT INTO chunks (title, content, source_id, content_type, source_category, session_id, event_id, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
486
+ this.#stmtInsertChunkTrigram = this.#db.prepare("INSERT INTO chunks_trigram (title, content, source_id, content_type, source_category, session_id, event_id, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
438
487
  this.#stmtInsertVocab = this.#db.prepare("INSERT OR IGNORE INTO vocabulary (word) VALUES (?)");
439
488
  // Dedup path: delete previous source with same label before re-indexing
440
489
  // Prevents stale outputs from accumulating in iterative workflows (build-fix-build)
@@ -447,6 +496,7 @@ export class ContentStore {
447
496
  chunks.title,
448
497
  chunks.content,
449
498
  chunks.content_type,
499
+ chunks.timestamp,
450
500
  sources.label,
451
501
  bm25(chunks, 5.0, 1.0) AS rank,
452
502
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -461,6 +511,7 @@ export class ContentStore {
461
511
  chunks.title,
462
512
  chunks.content,
463
513
  chunks.content_type,
514
+ chunks.timestamp,
464
515
  sources.label,
465
516
  bm25(chunks, 5.0, 1.0) AS rank,
466
517
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -475,6 +526,7 @@ export class ContentStore {
475
526
  chunks.title,
476
527
  chunks.content,
477
528
  chunks.content_type,
529
+ chunks.timestamp,
478
530
  sources.label,
479
531
  bm25(chunks, 5.0, 1.0) AS rank,
480
532
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -489,6 +541,7 @@ export class ContentStore {
489
541
  chunks_trigram.title,
490
542
  chunks_trigram.content,
491
543
  chunks_trigram.content_type,
544
+ chunks_trigram.timestamp,
492
545
  sources.label,
493
546
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
494
547
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -503,6 +556,7 @@ export class ContentStore {
503
556
  chunks_trigram.title,
504
557
  chunks_trigram.content,
505
558
  chunks_trigram.content_type,
559
+ chunks_trigram.timestamp,
506
560
  sources.label,
507
561
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
508
562
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -517,6 +571,7 @@ export class ContentStore {
517
571
  chunks_trigram.title,
518
572
  chunks_trigram.content,
519
573
  chunks_trigram.content_type,
574
+ chunks_trigram.timestamp,
520
575
  sources.label,
521
576
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
522
577
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -532,6 +587,7 @@ export class ContentStore {
532
587
  chunks.title,
533
588
  chunks.content,
534
589
  chunks.content_type,
590
+ chunks.timestamp,
535
591
  sources.label,
536
592
  bm25(chunks, 5.0, 1.0) AS rank,
537
593
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -546,6 +602,7 @@ export class ContentStore {
546
602
  chunks.title,
547
603
  chunks.content,
548
604
  chunks.content_type,
605
+ chunks.timestamp,
549
606
  sources.label,
550
607
  bm25(chunks, 5.0, 1.0) AS rank,
551
608
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -560,6 +617,7 @@ export class ContentStore {
560
617
  chunks.title,
561
618
  chunks.content,
562
619
  chunks.content_type,
620
+ chunks.timestamp,
563
621
  sources.label,
564
622
  bm25(chunks, 5.0, 1.0) AS rank,
565
623
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -574,6 +632,7 @@ export class ContentStore {
574
632
  chunks_trigram.title,
575
633
  chunks_trigram.content,
576
634
  chunks_trigram.content_type,
635
+ chunks_trigram.timestamp,
577
636
  sources.label,
578
637
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
579
638
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -588,6 +647,7 @@ export class ContentStore {
588
647
  chunks_trigram.title,
589
648
  chunks_trigram.content,
590
649
  chunks_trigram.content_type,
650
+ chunks_trigram.timestamp,
591
651
  sources.label,
592
652
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
593
653
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -602,6 +662,7 @@ export class ContentStore {
602
662
  chunks_trigram.title,
603
663
  chunks_trigram.content,
604
664
  chunks_trigram.content_type,
665
+ chunks_trigram.timestamp,
605
666
  sources.label,
606
667
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
607
668
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -714,10 +775,11 @@ export class ContentStore {
714
775
  }
715
776
  const info = this.#stmtInsertSource.run(label, chunks.length, codeChunks, filePath ?? null, contentHash ?? null);
716
777
  const sourceId = Number(info.lastInsertRowid);
778
+ const now = new Date().toISOString();
717
779
  for (const chunk of chunks) {
718
780
  const ct = chunk.hasCode ? "code" : "prose";
719
- this.#stmtInsertChunk.run(chunk.title, chunk.content, sourceId, ct);
720
- this.#stmtInsertChunkTrigram.run(chunk.title, chunk.content, sourceId, ct);
781
+ this.#stmtInsertChunk.run(chunk.title, chunk.content, sourceId, ct, null, null, null, now);
782
+ this.#stmtInsertChunkTrigram.run(chunk.title, chunk.content, sourceId, ct, null, null, null, now);
721
783
  }
722
784
  return sourceId;
723
785
  });
@@ -748,6 +810,7 @@ export class ContentStore {
748
810
  rank: r.rank,
749
811
  contentType: r.content_type,
750
812
  highlighted: r.highlighted,
813
+ timestamp: r.timestamp ?? undefined,
751
814
  }));
752
815
  }
753
816
  #sourceFilterParam(source, sourceMatchMode) {
package/build/types.d.ts CHANGED
@@ -74,6 +74,7 @@ export interface SearchResult {
74
74
  contentType: "code" | "prose";
75
75
  matchLayer?: "porter" | "trigram" | "fuzzy" | "rrf" | "rrf-fuzzy";
76
76
  highlighted?: string;
77
+ timestamp?: string;
77
78
  }
78
79
  /**
79
80
  * Aggregate statistics for a ContentStore instance.