context-mode 1.0.98 → 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 (56) 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/README.md +9 -7
  6. package/build/adapters/claude-code-base.js +4 -4
  7. package/build/adapters/codex/index.js +23 -1
  8. package/build/adapters/qwen-code/index.d.ts +1 -1
  9. package/build/adapters/qwen-code/index.js +110 -4
  10. package/build/cli.js +2 -0
  11. package/build/opencode-plugin.js +1 -1
  12. package/build/pi-extension.js +1 -1
  13. package/build/search/auto-memory.d.ts +29 -0
  14. package/build/search/auto-memory.js +121 -0
  15. package/build/search/unified.d.ts +41 -0
  16. package/build/search/unified.js +89 -0
  17. package/build/server.js +88 -40
  18. package/build/session/analytics.js +1 -1
  19. package/build/session/db.d.ts +17 -0
  20. package/build/session/db.js +28 -0
  21. package/build/session/extract.d.ts +4 -0
  22. package/build/session/extract.js +232 -1
  23. package/build/session/snapshot.js +31 -0
  24. package/build/store.js +118 -8
  25. package/build/types.d.ts +1 -0
  26. package/cli.bundle.mjs +260 -125
  27. package/configs/claude-code/CLAUDE.md +21 -1
  28. package/configs/codex/AGENTS.md +23 -2
  29. package/configs/codex/hooks.json +14 -0
  30. package/configs/cursor/context-mode.mdc +18 -1
  31. package/configs/gemini-cli/GEMINI.md +22 -1
  32. package/configs/jetbrains-copilot/copilot-instructions.md +22 -1
  33. package/configs/kilo/AGENTS.md +19 -2
  34. package/configs/kiro/KIRO.md +18 -1
  35. package/configs/openclaw/AGENTS.md +22 -2
  36. package/configs/opencode/AGENTS.md +18 -1
  37. package/configs/pi/AGENTS.md +18 -1
  38. package/configs/qwen-code/QWEN.md +38 -18
  39. package/configs/vscode-copilot/copilot-instructions.md +22 -1
  40. package/hooks/auto-injection.mjs +76 -0
  41. package/hooks/codex/stop.mjs +43 -0
  42. package/hooks/codex/userpromptsubmit.mjs +75 -0
  43. package/hooks/core/mcp-ready.mjs +7 -1
  44. package/hooks/posttooluse.mjs +50 -1
  45. package/hooks/precompact.mjs +9 -0
  46. package/hooks/pretooluse.mjs +27 -0
  47. package/hooks/routing-block.mjs +7 -1
  48. package/hooks/session-db.bundle.mjs +19 -13
  49. package/hooks/session-extract.bundle.mjs +2 -2
  50. package/hooks/session-snapshot.bundle.mjs +18 -17
  51. package/hooks/sessionstart.mjs +17 -0
  52. package/hooks/userpromptsubmit.mjs +1 -1
  53. package/openclaw.plugin.json +1 -1
  54. package/package.json +1 -1
  55. package/server.bundle.mjs +228 -93
  56. package/skills/context-mode-ops/agent-teams.md +1 -1
@@ -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
@@ -218,6 +218,40 @@ function findAllPositions(text, term) {
218
218
  }
219
219
  return positions;
220
220
  }
221
+ /**
222
+ * Count matched adjacent pairs across consecutive query terms.
223
+ * For each pair (term[i], term[i+1]), pairs each left position with at most one
224
+ * right position whose offset falls within `gap` chars of `p + len(term[i])`.
225
+ * `positionLists` must be sorted ascending (output of `findAllPositions` is).
226
+ * Each right position is consumed by at most one left, so `"foo foo bar"`
227
+ * counts 1 pair, not 2 — matches IR phrase-occurrence intent and avoids
228
+ * inflating boosts for repeated-token queries.
229
+ * Used by reranker to layer a frequency signal on top of minSpan proximity:
230
+ * 30-char gap covers natural prose without rewarding distant matches.
231
+ */
232
+ function countAdjacentPairs(positionLists, terms, gap = 30) {
233
+ if (positionLists.length < 2 || terms.length < 2)
234
+ return 0;
235
+ let total = 0;
236
+ const pairs = Math.min(positionLists.length, terms.length) - 1;
237
+ for (let i = 0; i < pairs; i++) {
238
+ const left = positionLists[i];
239
+ const right = positionLists[i + 1];
240
+ const leftLen = terms[i].length;
241
+ let j = 0;
242
+ for (const p of left) {
243
+ const minStart = p + leftLen;
244
+ const maxStart = minStart + gap;
245
+ while (j < right.length && right[j] < minStart)
246
+ j++;
247
+ if (j < right.length && right[j] <= maxStart) {
248
+ total++;
249
+ j++;
250
+ }
251
+ }
252
+ }
253
+ return total;
254
+ }
221
255
  /**
222
256
  * Find minimum span (window) covering at least one position from each list.
223
257
  * Uses a sweep-line approach: advance the pointer at the current minimum.
@@ -368,6 +402,10 @@ export class ContentStore {
368
402
  content,
369
403
  source_id UNINDEXED,
370
404
  content_type UNINDEXED,
405
+ source_category UNINDEXED,
406
+ session_id UNINDEXED,
407
+ event_id UNINDEXED,
408
+ timestamp UNINDEXED,
371
409
  tokenize='porter unicode61'
372
410
  );
373
411
 
@@ -376,6 +414,10 @@ export class ContentStore {
376
414
  content,
377
415
  source_id UNINDEXED,
378
416
  content_type UNINDEXED,
417
+ source_category UNINDEXED,
418
+ session_id UNINDEXED,
419
+ event_id UNINDEXED,
420
+ timestamp UNINDEXED,
379
421
  tokenize='trigram'
380
422
  );
381
423
 
@@ -385,6 +427,47 @@ export class ContentStore {
385
427
 
386
428
  CREATE INDEX IF NOT EXISTS idx_sources_label ON sources(label);
387
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 */ }
388
471
  // Stale detection columns — safe for existing DBs (ALTER is O(1) in SQLite)
389
472
  try {
390
473
  this.#db.exec("ALTER TABLE sources ADD COLUMN file_path TEXT");
@@ -399,8 +482,8 @@ export class ContentStore {
399
482
  // Write path
400
483
  this.#stmtInsertSourceEmpty = this.#db.prepare("INSERT INTO sources (label, chunk_count, code_chunk_count, file_path, content_hash) VALUES (?, 0, 0, ?, ?)");
401
484
  this.#stmtInsertSource = this.#db.prepare("INSERT INTO sources (label, chunk_count, code_chunk_count, file_path, content_hash) VALUES (?, ?, ?, ?, ?)");
402
- this.#stmtInsertChunk = this.#db.prepare("INSERT INTO chunks (title, content, source_id, content_type) VALUES (?, ?, ?, ?)");
403
- 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 (?, ?, ?, ?, ?, ?, ?, ?)");
404
487
  this.#stmtInsertVocab = this.#db.prepare("INSERT OR IGNORE INTO vocabulary (word) VALUES (?)");
405
488
  // Dedup path: delete previous source with same label before re-indexing
406
489
  // Prevents stale outputs from accumulating in iterative workflows (build-fix-build)
@@ -413,6 +496,7 @@ export class ContentStore {
413
496
  chunks.title,
414
497
  chunks.content,
415
498
  chunks.content_type,
499
+ chunks.timestamp,
416
500
  sources.label,
417
501
  bm25(chunks, 5.0, 1.0) AS rank,
418
502
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -427,6 +511,7 @@ export class ContentStore {
427
511
  chunks.title,
428
512
  chunks.content,
429
513
  chunks.content_type,
514
+ chunks.timestamp,
430
515
  sources.label,
431
516
  bm25(chunks, 5.0, 1.0) AS rank,
432
517
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -441,6 +526,7 @@ export class ContentStore {
441
526
  chunks.title,
442
527
  chunks.content,
443
528
  chunks.content_type,
529
+ chunks.timestamp,
444
530
  sources.label,
445
531
  bm25(chunks, 5.0, 1.0) AS rank,
446
532
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -455,6 +541,7 @@ export class ContentStore {
455
541
  chunks_trigram.title,
456
542
  chunks_trigram.content,
457
543
  chunks_trigram.content_type,
544
+ chunks_trigram.timestamp,
458
545
  sources.label,
459
546
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
460
547
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -469,6 +556,7 @@ export class ContentStore {
469
556
  chunks_trigram.title,
470
557
  chunks_trigram.content,
471
558
  chunks_trigram.content_type,
559
+ chunks_trigram.timestamp,
472
560
  sources.label,
473
561
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
474
562
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -483,6 +571,7 @@ export class ContentStore {
483
571
  chunks_trigram.title,
484
572
  chunks_trigram.content,
485
573
  chunks_trigram.content_type,
574
+ chunks_trigram.timestamp,
486
575
  sources.label,
487
576
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
488
577
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -498,6 +587,7 @@ export class ContentStore {
498
587
  chunks.title,
499
588
  chunks.content,
500
589
  chunks.content_type,
590
+ chunks.timestamp,
501
591
  sources.label,
502
592
  bm25(chunks, 5.0, 1.0) AS rank,
503
593
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -512,6 +602,7 @@ export class ContentStore {
512
602
  chunks.title,
513
603
  chunks.content,
514
604
  chunks.content_type,
605
+ chunks.timestamp,
515
606
  sources.label,
516
607
  bm25(chunks, 5.0, 1.0) AS rank,
517
608
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -526,6 +617,7 @@ export class ContentStore {
526
617
  chunks.title,
527
618
  chunks.content,
528
619
  chunks.content_type,
620
+ chunks.timestamp,
529
621
  sources.label,
530
622
  bm25(chunks, 5.0, 1.0) AS rank,
531
623
  highlight(chunks, 1, char(2), char(3)) AS highlighted
@@ -540,6 +632,7 @@ export class ContentStore {
540
632
  chunks_trigram.title,
541
633
  chunks_trigram.content,
542
634
  chunks_trigram.content_type,
635
+ chunks_trigram.timestamp,
543
636
  sources.label,
544
637
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
545
638
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -554,6 +647,7 @@ export class ContentStore {
554
647
  chunks_trigram.title,
555
648
  chunks_trigram.content,
556
649
  chunks_trigram.content_type,
650
+ chunks_trigram.timestamp,
557
651
  sources.label,
558
652
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
559
653
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -568,6 +662,7 @@ export class ContentStore {
568
662
  chunks_trigram.title,
569
663
  chunks_trigram.content,
570
664
  chunks_trigram.content_type,
665
+ chunks_trigram.timestamp,
571
666
  sources.label,
572
667
  bm25(chunks_trigram, 5.0, 1.0) AS rank,
573
668
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
@@ -603,10 +698,16 @@ export class ContentStore {
603
698
  // ── Index ──
604
699
  index(options) {
605
700
  const { content, path, source } = options;
606
- if (!content && !path) {
701
+ // Treat empty string as "no content" so an empty `content` paired with a
702
+ // valid `path` falls back to reading the file. Some MCP clients
703
+ // materialize optional string fields as `""` and the previous
704
+ // `content ?? readFileSync(path)` kept the empty string, indexing 0
705
+ // chunks. See issue #350.
706
+ const hasContent = typeof content === "string" && content.length > 0;
707
+ if (!hasContent && !path) {
607
708
  throw new Error("Either content or path must be provided");
608
709
  }
609
- const text = content ?? readFileSync(path, "utf-8");
710
+ const text = hasContent ? content : readFileSync(path, "utf-8");
610
711
  const label = source ?? path ?? "untitled";
611
712
  const chunks = this.#chunkMarkdown(text);
612
713
  // Stale detection: store file_path + SHA-256 for file-backed sources
@@ -674,10 +775,11 @@ export class ContentStore {
674
775
  }
675
776
  const info = this.#stmtInsertSource.run(label, chunks.length, codeChunks, filePath ?? null, contentHash ?? null);
676
777
  const sourceId = Number(info.lastInsertRowid);
778
+ const now = new Date().toISOString();
677
779
  for (const chunk of chunks) {
678
780
  const ct = chunk.hasCode ? "code" : "prose";
679
- this.#stmtInsertChunk.run(chunk.title, chunk.content, sourceId, ct);
680
- 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);
681
783
  }
682
784
  return sourceId;
683
785
  });
@@ -708,6 +810,7 @@ export class ContentStore {
708
810
  rank: r.rank,
709
811
  contentType: r.content_type,
710
812
  highlighted: r.highlighted,
813
+ timestamp: r.timestamp ?? undefined,
711
814
  }));
712
815
  }
713
816
  #sourceFilterParam(source, sourceMatchMode) {
@@ -859,17 +962,24 @@ export class ContentStore {
859
962
  const titleHits = terms.filter((t) => titleLower.includes(t)).length;
860
963
  const titleWeight = r.contentType === "code" ? 0.6 : 0.3;
861
964
  const titleBoost = titleHits > 0 ? titleWeight * (titleHits / terms.length) : 0;
862
- // Proximity boost for multi-term queries
965
+ // Proximity boost for multi-term queries. minSpan picks the single
966
+ // tightest window — frequency doesn't move it, so a long doc with one
967
+ // tight occurrence outranks a short doc with several. Phrase-frequency
968
+ // reward layers a saturating frequency signal on top: cap 0.5 (below
969
+ // proximity max ≈1.0, in title-boost range), saturates at 4 hits.
863
970
  let proximityBoost = 0;
971
+ let phraseBoost = 0;
864
972
  if (terms.length >= 2) {
865
973
  const content = r.content.toLowerCase();
866
974
  const positions = terms.map((t) => findAllPositions(content, t));
867
975
  if (!positions.some((p) => p.length === 0)) {
868
976
  const minSpan = findMinSpan(positions);
869
977
  proximityBoost = 1 / (1 + minSpan / Math.max(content.length, 1));
978
+ const adjacentPairs = countAdjacentPairs(positions, terms);
979
+ phraseBoost = 0.5 * Math.min(1, adjacentPairs / 4);
870
980
  }
871
981
  }
872
- return { result: r, boost: titleBoost + proximityBoost };
982
+ return { result: r, boost: titleBoost + proximityBoost + phraseBoost };
873
983
  })
874
984
  .sort((a, b) => b.boost - a.boost || a.result.rank - b.result.rank)
875
985
  .map(({ result }) => result);
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.