@gi4nks/ph 1.5.0 → 1.6.0

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 (34) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +118 -152
  3. package/dist/cli.js +2049 -1055
  4. package/dist/cli.js.map +1 -1
  5. package/docs/evals/context-retrieval-golden.json +26 -0
  6. package/docs/evals/context-retrieval.md +27 -0
  7. package/docs/hooks.md +13 -7
  8. package/docs/ph-analysis.html +145 -0
  9. package/docs/ph-manual.md +337 -376
  10. package/docs/specs/SPEC-001-architecture-cli.md +82 -0
  11. package/docs/specs/SPEC-002-database-layer.md +105 -0
  12. package/docs/specs/SPEC-003-capture-modes.md +110 -0
  13. package/docs/specs/SPEC-004-filter-metadata.md +82 -0
  14. package/docs/specs/SPEC-005-search.md +102 -0
  15. package/docs/specs/SPEC-006-analysis-pipeline.md +98 -0
  16. package/docs/specs/SPEC-007-memory-summaries.md +96 -0
  17. package/docs/specs/SPEC-008-embeddings.md +90 -0
  18. package/docs/specs/SPEC-009-retention-archive.md +91 -0
  19. package/docs/specs/SPEC-010-remote-sync.md +106 -0
  20. package/docs/specs/SPEC-011-mcp-server.md +94 -0
  21. package/docs/specs/SPEC-012-tui-browser.md +98 -0
  22. package/docs/specs/SPEC-013-importers.md +109 -0
  23. package/docs/specs/SPEC-014-analytics-commands.md +109 -0
  24. package/docs/specs/SPEC-015-config-env.md +85 -0
  25. package/docs/specs/SPEC-INDEX.md +35 -0
  26. package/docs/specs/SPEC-ISSUES.md +122 -0
  27. package/docs/specs/_TEMPLATE.md +65 -0
  28. package/docs/superpowers/plans/2026-06-12-ph-evolution-phase1.md +466 -0
  29. package/docs/superpowers/plans/2026-06-12-ph-evolution-phase2.md +351 -0
  30. package/docs/superpowers/specs/2026-06-12-ph-evolution-design.md +173 -0
  31. package/hooks/codex/README.md +54 -0
  32. package/hooks/codex/ph-hook.sh +14 -0
  33. package/hooks/opencode/ph-plugin.ts +60 -57
  34. package/package.json +13 -12
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { spawnSync as spawnSync2 } from "child_process";
5
- import React7 from "react";
5
+ import React8 from "react";
6
6
  import { render } from "ink";
7
7
 
8
8
  // src/config/index.ts
@@ -29,15 +29,103 @@ import path2 from "path";
29
29
  import os2 from "os";
30
30
  import { createHash } from "crypto";
31
31
  import * as sqliteVec from "sqlite-vec";
32
+
33
+ // src/db/semantic-index.ts
34
+ var SemanticIndex = class {
35
+ constructor(db) {
36
+ this.db = db;
37
+ }
38
+ save(promptId, vector) {
39
+ const encoded = this.encode(vector);
40
+ this.db.prepare("INSERT OR REPLACE INTO embeddings (prompt_id, vector) VALUES (?, ?)").run(promptId, encoded);
41
+ this.db.prepare("INSERT OR REPLACE INTO vec_embeddings(rowid, embedding) VALUES (?, vec_f32(?))").run(BigInt(promptId), encoded);
42
+ }
43
+ search(query, limit, project) {
44
+ const encodedQuery = this.encode(query);
45
+ const projectFilter = project ? "AND json_extract(p.metadata, '$.project') = ?" : "";
46
+ const candidateLimit = project ? Math.max(1, this.db.prepare("SELECT count(*) AS count FROM vec_embeddings").get().count) : limit;
47
+ const sql = `
48
+ SELECT p.*, v.distance
49
+ FROM vec_embeddings v
50
+ JOIN prompts p ON p.id = v.rowid
51
+ WHERE v.embedding MATCH vec_f32(?) AND k = ?
52
+ ${projectFilter}
53
+ ORDER BY v.distance ASC
54
+ `;
55
+ return this.db.prepare(project ? `${sql} LIMIT ?` : sql).all(
56
+ encodedQuery,
57
+ candidateLimit,
58
+ ...project ? [project, limit] : []
59
+ );
60
+ }
61
+ loadAll() {
62
+ const rows = this.db.prepare("SELECT prompt_id, vector FROM embeddings").all();
63
+ return new Map(rows.map((row) => [row.prompt_id, this.decode(row.vector)]));
64
+ }
65
+ findMissingPrompts() {
66
+ return this.db.prepare(
67
+ `SELECT p.* FROM prompts p
68
+ LEFT JOIN vec_embeddings e ON e.rowid = p.id
69
+ WHERE e.rowid IS NULL`
70
+ ).all();
71
+ }
72
+ encode(vector) {
73
+ const buffer = Buffer.alloc(vector.length * 4);
74
+ for (let i = 0; i < vector.length; i++) buffer.writeFloatLE(vector[i], i * 4);
75
+ return buffer;
76
+ }
77
+ decode(buffer) {
78
+ const vector = new Float32Array(buffer.length / 4);
79
+ for (let i = 0; i < vector.length; i++) vector[i] = buffer.readFloatLE(i * 4);
80
+ return vector;
81
+ }
82
+ };
83
+
84
+ // src/db/index.ts
32
85
  function defaultPath() {
33
86
  return path2.join(os2.homedir(), ".prompt_history.db");
34
87
  }
35
- var PhDB = class {
88
+ var PhDB = class _PhDB {
89
+ static PROJECT_SUMMARIES_SCHEMA = `
90
+ CREATE TABLE IF NOT EXISTS project_summaries (
91
+ project TEXT PRIMARY KEY,
92
+ summary TEXT NOT NULL DEFAULT '',
93
+ key_insights TEXT NOT NULL DEFAULT '[]',
94
+ technical_decisions TEXT NOT NULL DEFAULT '[]',
95
+ prompt_count INTEGER NOT NULL DEFAULT 0,
96
+ first_analyzed TEXT NOT NULL,
97
+ last_analyzed TEXT NOT NULL,
98
+ git_context_snapshot TEXT
99
+ );
100
+ `;
101
+ static ARCHIVE_SCHEMA = `
102
+ CREATE TABLE IF NOT EXISTS prompts_archive (
103
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ timestamp TEXT NOT NULL,
105
+ tool TEXT NOT NULL,
106
+ prompt TEXT NOT NULL,
107
+ response TEXT NOT NULL DEFAULT '',
108
+ args TEXT NOT NULL DEFAULT '',
109
+ workdir TEXT NOT NULL DEFAULT '',
110
+ hostname TEXT NOT NULL DEFAULT '',
111
+ exit_code INTEGER NOT NULL DEFAULT 0,
112
+ metadata TEXT NOT NULL DEFAULT '{}',
113
+ archived_at TEXT NOT NULL,
114
+ original_id INTEGER
115
+ );
116
+ CREATE INDEX IF NOT EXISTS idx_archive_timestamp ON prompts_archive(timestamp);
117
+ CREATE INDEX IF NOT EXISTS idx_archive_original_id ON prompts_archive(original_id);
118
+ `;
36
119
  db;
120
+ semanticIndex;
121
+ /** Absolute path of the SQLite file (used by the HTTP server health endpoint). */
122
+ dbPath;
37
123
  constructor(dbPath) {
124
+ this.dbPath = dbPath;
38
125
  this.db = new Database(dbPath);
39
126
  this.db.pragma("journal_mode = WAL");
40
127
  sqliteVec.load(this.db);
128
+ this.semanticIndex = new SemanticIndex(this.db);
41
129
  this.migrate();
42
130
  }
43
131
  migrate() {
@@ -104,6 +192,8 @@ var PhDB = class {
104
192
  );
105
193
  CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
106
194
  `);
195
+ this.db.exec(_PhDB.PROJECT_SUMMARIES_SCHEMA);
196
+ this.db.exec(_PhDB.ARCHIVE_SCHEMA);
107
197
  const rowsWithoutSchema = this.db.prepare("SELECT id, metadata FROM prompts WHERE json_extract(metadata, '$.\\$schema_version') IS NULL").all();
108
198
  for (const row of rowsWithoutSchema) {
109
199
  try {
@@ -163,6 +253,7 @@ var PhDB = class {
163
253
  `);
164
254
  }
165
255
  }
256
+ /** `response` is optional at the call site (runtime defaults to ''). */
166
257
  insert(entry) {
167
258
  const meta = entry.metadata || "{}";
168
259
  const response = entry.response ?? "";
@@ -281,45 +372,16 @@ var PhDB = class {
281
372
  }
282
373
  }
283
374
  saveEmbedding(id, vector) {
284
- const buf = Buffer.alloc(vector.length * 4);
285
- for (let i = 0; i < vector.length; i++) {
286
- buf.writeFloatLE(vector[i], i * 4);
287
- }
288
- this.db.prepare("INSERT OR REPLACE INTO embeddings (prompt_id, vector) VALUES (?, ?)").run(id, buf);
289
- this.db.prepare("INSERT OR REPLACE INTO vec_embeddings(rowid, embedding) VALUES (?, vec_f32(?))").run(BigInt(id), buf);
375
+ this.semanticIndex.save(id, vector);
290
376
  }
291
- searchSemantic(queryVector, limit) {
292
- const buf = Buffer.alloc(queryVector.length * 4);
293
- for (let i = 0; i < queryVector.length; i++) {
294
- buf.writeFloatLE(queryVector[i], i * 4);
295
- }
296
- const sql = `
297
- SELECT p.*, v.distance
298
- FROM vec_embeddings v
299
- JOIN prompts p ON p.id = v.rowid
300
- WHERE v.embedding MATCH vec_f32(?) AND k = ?
301
- ORDER BY v.distance ASC
302
- `;
303
- return this.db.prepare(sql).all(buf, limit);
377
+ searchSemantic(queryVector, limit, project) {
378
+ return this.semanticIndex.search(queryVector, limit, project);
304
379
  }
305
380
  getAllEmbeddings() {
306
- const rows = this.db.prepare("SELECT prompt_id, vector FROM embeddings").all();
307
- const map = /* @__PURE__ */ new Map();
308
- for (const row of rows) {
309
- const vec = new Float32Array(row.vector.length / 4);
310
- for (let i = 0; i < vec.length; i++) {
311
- vec[i] = row.vector.readFloatLE(i * 4);
312
- }
313
- map.set(row.prompt_id, vec);
314
- }
315
- return map;
381
+ return this.semanticIndex.loadAll();
316
382
  }
317
383
  getPromptsWithoutEmbeddings() {
318
- return this.db.prepare(
319
- `SELECT p.* FROM prompts p
320
- LEFT JOIN vec_embeddings e ON e.rowid = p.id
321
- WHERE e.rowid IS NULL`
322
- ).all();
384
+ return this.semanticIndex.findMissingPrompts();
323
385
  }
324
386
  getAllPrompts(limit = 1e6) {
325
387
  return this.db.prepare("SELECT * FROM prompts ORDER BY timestamp DESC LIMIT ?").all(limit);
@@ -330,6 +392,15 @@ var PhDB = class {
330
392
  getPromptCount() {
331
393
  return this.db.prepare("SELECT count(*) as c FROM prompts").get().c;
332
394
  }
395
+ getPromptCountSince(timestamp) {
396
+ return this.db.prepare("SELECT count(*) as c FROM prompts WHERE timestamp > ?").get(timestamp).c;
397
+ }
398
+ getStats() {
399
+ const total = this.db.prepare("SELECT count(*) as c FROM prompts").get().c;
400
+ const totalMemories = this.db.prepare("SELECT count(*) as c FROM memories").get().c;
401
+ const byTool = this.db.prepare("SELECT tool, count(*) as count FROM prompts GROUP BY tool ORDER BY count DESC").all();
402
+ return { total, totalMemories, byTool };
403
+ }
333
404
  getPromptBySyncHash(hash) {
334
405
  return this.db.prepare("SELECT * FROM prompts WHERE json_extract(metadata, '$.sync_hash') = ?").get(hash);
335
406
  }
@@ -369,6 +440,72 @@ var PhDB = class {
369
440
  }
370
441
  return 0;
371
442
  }
443
+ archivePrompts(ids) {
444
+ if (ids.length === 0) return 0;
445
+ const now = (/* @__PURE__ */ new Date()).toISOString();
446
+ const select = this.db.prepare(`
447
+ SELECT id, timestamp, tool, prompt, response, args, workdir, hostname, exit_code, metadata
448
+ FROM prompts WHERE id = ?
449
+ `);
450
+ const insert = this.db.prepare(`
451
+ INSERT INTO prompts_archive (timestamp, tool, prompt, response, args, workdir, hostname, exit_code, metadata, archived_at, original_id)
452
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
453
+ `);
454
+ const del = this.db.prepare("DELETE FROM prompts WHERE id = ?");
455
+ const transaction = this.db.transaction((toArchive) => {
456
+ let count = 0;
457
+ for (const id of toArchive) {
458
+ const row = select.get(id);
459
+ if (!row) continue;
460
+ insert.run(
461
+ row.timestamp,
462
+ row.tool,
463
+ row.prompt,
464
+ row.response,
465
+ row.args,
466
+ row.workdir,
467
+ row.hostname,
468
+ row.exit_code,
469
+ row.metadata,
470
+ now,
471
+ id
472
+ );
473
+ del.run(id);
474
+ count++;
475
+ }
476
+ return count;
477
+ });
478
+ return transaction(ids);
479
+ }
480
+ searchArchive(opts) {
481
+ let sql = "SELECT * FROM prompts_archive WHERE 1=1";
482
+ const params = [];
483
+ if (opts.since) {
484
+ sql += " AND timestamp >= ?";
485
+ params.push(opts.since);
486
+ }
487
+ if (opts.until) {
488
+ sql += " AND timestamp <= ?";
489
+ params.push(opts.until);
490
+ }
491
+ sql += " ORDER BY timestamp DESC";
492
+ if (opts.limit) {
493
+ sql += " LIMIT ?";
494
+ params.push(opts.limit);
495
+ }
496
+ return this.db.prepare(sql).all(...params);
497
+ }
498
+ purgeArchive(beforeDate) {
499
+ const result = this.db.prepare("DELETE FROM prompts_archive WHERE archived_at < ?").run(beforeDate);
500
+ return result.changes;
501
+ }
502
+ getArchiveStats() {
503
+ const row = this.db.prepare(`
504
+ SELECT COUNT(*) as total, MIN(timestamp) as oldest, MAX(timestamp) as newest
505
+ FROM prompts_archive
506
+ `).get();
507
+ return row;
508
+ }
372
509
  vacuum() {
373
510
  this.db.exec("VACUUM");
374
511
  }
@@ -460,10 +597,62 @@ var PhDB = class {
460
597
  last_accessed: void 0
461
598
  });
462
599
  }
600
+ getLastMemoryForProject(project) {
601
+ const row = this.db.prepare("SELECT * FROM memories WHERE project = ? ORDER BY updated_at DESC LIMIT 1").get(project);
602
+ if (!row) return void 0;
603
+ return this.hydrateMemory(row);
604
+ }
463
605
  getAllProjectsWithMemories() {
464
606
  const rows = this.db.prepare("SELECT DISTINCT project FROM memories ORDER BY project").all();
465
607
  return rows.map((r) => r.project);
466
608
  }
609
+ upsertProjectSummary(params) {
610
+ const existing = this.getProjectSummary(params.project);
611
+ const now = (/* @__PURE__ */ new Date()).toISOString();
612
+ if (existing) {
613
+ const allInsights = /* @__PURE__ */ new Set([...existing.key_insights, ...params.key_insights]);
614
+ const allDecisions = /* @__PURE__ */ new Set([...existing.technical_decisions, ...params.technical_decisions]);
615
+ this.db.prepare(`
616
+ UPDATE project_summaries
617
+ SET summary = ?,
618
+ key_insights = ?,
619
+ technical_decisions = ?,
620
+ prompt_count = prompt_count + 1,
621
+ last_analyzed = ?,
622
+ git_context_snapshot = COALESCE(?, git_context_snapshot)
623
+ WHERE project = ?
624
+ `).run(
625
+ params.summary,
626
+ JSON.stringify([...allInsights]),
627
+ JSON.stringify([...allDecisions]),
628
+ now,
629
+ params.git_context_snapshot || null,
630
+ params.project
631
+ );
632
+ } else {
633
+ this.db.prepare(`
634
+ INSERT INTO project_summaries (project, summary, key_insights, technical_decisions, prompt_count, first_analyzed, last_analyzed, git_context_snapshot)
635
+ VALUES (?, ?, ?, ?, 1, ?, ?, ?)
636
+ `).run(
637
+ params.project,
638
+ params.summary,
639
+ JSON.stringify(params.key_insights),
640
+ JSON.stringify(params.technical_decisions),
641
+ now,
642
+ now,
643
+ params.git_context_snapshot || null
644
+ );
645
+ }
646
+ }
647
+ getProjectSummary(project) {
648
+ const row = this.db.prepare("SELECT * FROM project_summaries WHERE project = ?").get(project);
649
+ if (!row) return null;
650
+ return this.hydrateProjectSummary(row);
651
+ }
652
+ getAllProjectsWithSummaries() {
653
+ const rows = this.db.prepare("SELECT project FROM project_summaries ORDER BY project").all();
654
+ return rows.map((r) => r.project);
655
+ }
467
656
  recordMemoryAccess(id) {
468
657
  this.db.prepare("UPDATE memories SET access_count = access_count + 1, last_accessed = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), id);
469
658
  }
@@ -485,6 +674,18 @@ var PhDB = class {
485
674
  last_accessed: row.last_accessed
486
675
  };
487
676
  }
677
+ hydrateProjectSummary(row) {
678
+ return {
679
+ project: row.project,
680
+ summary: row.summary,
681
+ key_insights: JSON.parse(row.key_insights),
682
+ technical_decisions: JSON.parse(row.technical_decisions),
683
+ prompt_count: row.prompt_count,
684
+ first_analyzed: row.first_analyzed,
685
+ last_analyzed: row.last_analyzed,
686
+ git_context_snapshot: row.git_context_snapshot
687
+ };
688
+ }
488
689
  getAllPromptsByProject(project) {
489
690
  return this.db.prepare(`
490
691
  SELECT * FROM prompts
@@ -492,6 +693,20 @@ var PhDB = class {
492
693
  ORDER BY timestamp ASC
493
694
  `).all(project);
494
695
  }
696
+ getPromptsByProjectPaginated(project, page, pageSize) {
697
+ const total = this.db.prepare("SELECT COUNT(*) as count FROM prompts WHERE json_extract(metadata, '$.project') = ?").get(project);
698
+ const entries = this.db.prepare(`
699
+ SELECT * FROM prompts
700
+ WHERE json_extract(metadata, '$.project') = ?
701
+ ORDER BY timestamp DESC
702
+ LIMIT ? OFFSET ?
703
+ `).all(project, pageSize, (page - 1) * pageSize);
704
+ return { entries, total: total.count };
705
+ }
706
+ getPromptCountByProject(project) {
707
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM prompts WHERE json_extract(metadata, '$.project') = ?").get(project);
708
+ return row.count;
709
+ }
495
710
  getAllMemoriesByProject(project) {
496
711
  const rows = this.db.prepare("SELECT * FROM memories WHERE project = ? ORDER BY created_at ASC").all(project);
497
712
  return rows.map((r) => this.hydrateMemory(r));
@@ -515,6 +730,34 @@ function cosineSimilarity(a, b) {
515
730
  // src/runner/inline.ts
516
731
  import { spawn, execFileSync as execFileSync2 } from "child_process";
517
732
  import fs3 from "fs";
733
+
734
+ // src/runner/git-context.ts
735
+ import { execFileSync } from "child_process";
736
+ function captureGitContext(workdir) {
737
+ try {
738
+ const branch = execFileSync("git", ["-C", workdir, "rev-parse", "--abbrev-ref", "HEAD"], {
739
+ encoding: "utf-8",
740
+ stdio: ["ignore", "pipe", "pipe"]
741
+ }).trim();
742
+ const filesRaw = execFileSync("git", ["-C", workdir, "diff", "--name-only", "HEAD"], {
743
+ encoding: "utf-8",
744
+ stdio: ["ignore", "pipe", "pipe"]
745
+ }).trim();
746
+ const files = filesRaw ? filesRaw.split("\n").filter(Boolean) : [];
747
+ let diff = execFileSync("git", ["-C", workdir, "diff", "HEAD"], {
748
+ encoding: "utf-8",
749
+ stdio: ["ignore", "pipe", "pipe"]
750
+ });
751
+ if (diff.length > 4e3) {
752
+ diff = diff.slice(0, 4e3) + "\n... (truncated)";
753
+ }
754
+ return { branch, files, diff };
755
+ } catch {
756
+ return null;
757
+ }
758
+ }
759
+
760
+ // src/capture/index.ts
518
761
  import os3 from "os";
519
762
 
520
763
  // src/runner/project.ts
@@ -561,30 +804,119 @@ function detectLanguage(rootDir) {
561
804
  return "";
562
805
  }
563
806
 
564
- // src/runner/git-context.ts
565
- import { execFileSync } from "child_process";
566
- function captureGitContext(workdir) {
567
- try {
568
- const branch = execFileSync("git", ["-C", workdir, "rev-parse", "--abbrev-ref", "HEAD"], {
569
- encoding: "utf-8",
570
- stdio: ["ignore", "pipe", "pipe"]
571
- }).trim();
572
- const filesRaw = execFileSync("git", ["-C", workdir, "diff", "--name-only", "HEAD"], {
573
- encoding: "utf-8",
574
- stdio: ["ignore", "pipe", "pipe"]
575
- }).trim();
576
- const files = filesRaw ? filesRaw.split("\n").filter(Boolean) : [];
577
- let diff = execFileSync("git", ["-C", workdir, "diff", "HEAD"], {
578
- encoding: "utf-8",
579
- stdio: ["ignore", "pipe", "pipe"]
580
- });
581
- if (diff.length > 4e3) {
582
- diff = diff.slice(0, 4e3) + "\n... (truncated)";
807
+ // src/utils/extractTopic.ts
808
+ function extractTopic(prompt) {
809
+ let text = prompt.trim();
810
+ if (!text) {
811
+ return "(empty prompt)";
812
+ }
813
+ if (text.startsWith("```") || text.startsWith("`")) {
814
+ return truncateAtWordBoundary(text, 72);
815
+ }
816
+ const originalTrimmed = text;
817
+ if (text.startsWith("#")) {
818
+ const headingMatch = text.match(/^#+\s+(.*?)(?:\s+#+)?(?:\n|$)/);
819
+ if (headingMatch && headingMatch[1]) {
820
+ text = headingMatch[1].trim();
583
821
  }
584
- return { branch, files, diff };
585
- } catch {
586
- return null;
587
822
  }
823
+ const prefixes = [
824
+ // English
825
+ "please, ",
826
+ "please ",
827
+ "can you please ",
828
+ "could you please ",
829
+ "can you ",
830
+ "could you ",
831
+ "would you ",
832
+ "i need you to ",
833
+ "i need to ",
834
+ "i want you to ",
835
+ "i want to ",
836
+ "i'd like you to ",
837
+ "i'd like to ",
838
+ "help me to ",
839
+ "help me ",
840
+ "i need ",
841
+ "claude, ",
842
+ "gemini, ",
843
+ "chatgpt, ",
844
+ // Italiano
845
+ "puoi ",
846
+ "potresti ",
847
+ "potreste ",
848
+ "per favore ",
849
+ "per piacere ",
850
+ "ho bisogno che ",
851
+ "ho bisogno di ",
852
+ "voglio che ",
853
+ "vorrei che ",
854
+ "voglio ",
855
+ "vorrei ",
856
+ "aiutami a ",
857
+ "aiutami ",
858
+ "fammi vedere ",
859
+ "fammi ",
860
+ "si prega di ",
861
+ "si "
862
+ ];
863
+ let changed = true;
864
+ while (changed) {
865
+ changed = false;
866
+ const lowerText = text.toLowerCase();
867
+ for (const prefix of prefixes) {
868
+ if (lowerText.startsWith(prefix.toLowerCase())) {
869
+ text = text.slice(prefix.length).trim();
870
+ changed = true;
871
+ break;
872
+ }
873
+ }
874
+ }
875
+ const sentenceEndMatch = text.match(/[\n.?!`]/);
876
+ if (sentenceEndMatch && sentenceEndMatch.index !== void 0) {
877
+ text = text.slice(0, sentenceEndMatch.index).trim();
878
+ }
879
+ if (!text) {
880
+ return truncateAtWordBoundary(originalTrimmed, 72);
881
+ }
882
+ text = truncateAtWordBoundary(text, 72);
883
+ return text.charAt(0).toUpperCase() + text.slice(1);
884
+ }
885
+ function truncateAtWordBoundary(str, limit) {
886
+ const clean = str.replace(/\s+/g, " ").trim();
887
+ if (clean.length <= limit) return clean;
888
+ const sub = clean.slice(0, limit);
889
+ const lastSpace = sub.lastIndexOf(" ");
890
+ if (lastSpace > 0) {
891
+ return sub.slice(0, lastSpace);
892
+ }
893
+ return sub;
894
+ }
895
+
896
+ // src/capture/index.ts
897
+ function createCaptureRecord(input) {
898
+ const { rootDir, projectName } = detectProject(input.workdir);
899
+ const language = detectLanguage(rootDir);
900
+ const title = extractTopic(input.prompt);
901
+ const metadata = {
902
+ $schema_version: 1,
903
+ ...projectName ? { project: projectName } : {},
904
+ ...language ? { language } : {},
905
+ ...title ? { title } : {},
906
+ ...input.gitContext ? { git_context: input.gitContext } : {},
907
+ ...input.metadata
908
+ };
909
+ return {
910
+ timestamp: input.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
911
+ tool: input.tool,
912
+ prompt: input.prompt,
913
+ response: input.response ?? "",
914
+ args: input.args ?? "",
915
+ workdir: input.workdir,
916
+ hostname: input.hostname ?? os3.hostname(),
917
+ exit_code: input.exit_code ?? 0,
918
+ metadata: JSON.stringify(metadata)
919
+ };
588
920
  }
589
921
 
590
922
  // src/runner/inline.ts
@@ -613,26 +945,15 @@ function resolveRealBinary(tool) {
613
945
  }
614
946
  async function runInline(realBin, args, db, tool, tags, role, onInserted) {
615
947
  const workdir = process.cwd();
616
- const { rootDir, projectName } = detectProject(workdir);
617
- const language = detectLanguage(rootDir);
618
948
  const gitContext = captureGitContext(workdir);
619
- const metaObj = {};
620
- if (projectName) metaObj.project = projectName;
621
- if (language) metaObj.language = language;
622
- if (role) metaObj.role = role;
623
- if (tags.length > 0) metaObj.tags = tags;
624
- if (gitContext) metaObj.git_context = gitContext;
625
- const metadata = Object.keys(metaObj).length > 0 ? JSON.stringify(metaObj) : "{}";
626
- const id = db.insert({
627
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
949
+ const id = db.insert(createCaptureRecord({
628
950
  tool,
629
951
  prompt: extractPrompt(args),
630
952
  args: args.join(" "),
631
953
  workdir,
632
- hostname: os3.hostname(),
633
- exit_code: 0,
634
- metadata
635
- });
954
+ gitContext,
955
+ metadata: { ...role ? { role } : {}, ...tags.length > 0 ? { tags } : {} }
956
+ }));
636
957
  if (onInserted) onInserted(id);
637
958
  const child = spawn(realBin, args, { stdio: "inherit" });
638
959
  child.on("error", (err) => {
@@ -654,7 +975,7 @@ function cleanLine(bytes) {
654
975
  const s = Buffer.from(bytes).toString("utf-8");
655
976
  return s.trim();
656
977
  }
657
- function stripAnsi(text) {
978
+ function stripPtyControlSequences(text) {
658
979
  return text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
659
980
  }
660
981
  async function runPTY(binary, args, onPrompt, onResponse, debugLog) {
@@ -707,7 +1028,7 @@ async function runPTY(binary, args, onPrompt, onResponse, debugLog) {
707
1028
  log(`ENTER \u2192 prompt: ${JSON.stringify(prompt)}`);
708
1029
  if (prompt) {
709
1030
  if (currentPromptId !== null) {
710
- onResponse(currentPromptId, stripAnsi(responseBuffer));
1031
+ onResponse(currentPromptId, stripPtyControlSequences(responseBuffer));
711
1032
  }
712
1033
  currentPromptId = onPrompt(prompt, /* @__PURE__ */ new Date());
713
1034
  responseBuffer = "";
@@ -751,7 +1072,7 @@ async function runPTY(binary, args, onPrompt, onResponse, debugLog) {
751
1072
  return new Promise((resolve) => {
752
1073
  ptyProcess.onExit(({ exitCode }) => {
753
1074
  if (currentPromptId !== null) {
754
- onResponse(currentPromptId, stripAnsi(responseBuffer));
1075
+ onResponse(currentPromptId, stripPtyControlSequences(responseBuffer));
755
1076
  }
756
1077
  process.stdin.removeListener("data", onData);
757
1078
  process.stdout.removeListener("resize", onResize);
@@ -801,26 +1122,169 @@ async function getEmbeddings(texts, ollamaUrl, model, batchSize = 20) {
801
1122
  return results;
802
1123
  }
803
1124
 
804
- // src/mcp/server.ts
805
- import { z } from "zod";
806
- async function runMCPServer() {
807
- const cfg = load();
808
- const dbPath = process.env.PH_DB ?? cfg.dbPath ?? defaultPath();
809
- const db = new PhDB(dbPath);
810
- const server = new Server(
811
- {
812
- name: "ph-memory",
813
- version: "1.0.0"
814
- },
815
- {
816
- capabilities: {
817
- tools: {}
1125
+ // src/context/index.ts
1126
+ async function getProjectContext(db, options, embedQuery) {
1127
+ const limit = Math.max(1, Math.floor(options.limit ?? 5));
1128
+ const memoryLimit = Math.max(1, Math.floor(options.memoryLimit ?? limit));
1129
+ const includeSummary = options.includeSummary ?? true;
1130
+ const includeMemories = options.includeMemories ?? true;
1131
+ const includePrompts = options.includePrompts ?? true;
1132
+ const summary = includeSummary ? db.getProjectSummary(options.project) : null;
1133
+ const memories = includeMemories ? db.searchMemories(options.project, memoryLimit) : [];
1134
+ let prompts = [];
1135
+ if (includePrompts) {
1136
+ const query = options.query?.trim();
1137
+ if (query) {
1138
+ if (!embedQuery) throw new Error("Semantic context requires an embedding provider.");
1139
+ const vector = await embedQuery(query);
1140
+ prompts = vector ? db.searchSemantic(vector, limit, options.project) : [];
1141
+ } else {
1142
+ prompts = db.getProjectMemory(options.project, limit);
1143
+ }
1144
+ }
1145
+ return { project: options.project, summary, memories, prompts };
1146
+ }
1147
+ function formatProjectContext(context, options = {}) {
1148
+ const parts = [];
1149
+ const { summary, memories, prompts, project } = context;
1150
+ if (summary || memories.length > 0) {
1151
+ parts.push(`## Project Knowledge: ${project}
1152
+ `);
1153
+ if (summary?.summary) parts.push(`${summary.summary}
1154
+ `);
1155
+ if (summary?.key_insights.length) {
1156
+ parts.push("Key Insights:");
1157
+ for (const insight of summary.key_insights) parts.push(` - ${insight}`);
1158
+ parts.push("");
1159
+ }
1160
+ if (summary?.technical_decisions.length) {
1161
+ parts.push("Technical Decisions:");
1162
+ for (const decision of summary.technical_decisions) parts.push(` - ${decision}`);
1163
+ parts.push("");
1164
+ }
1165
+ if (!summary && memories.length > 0) {
1166
+ for (const memory of memories) {
1167
+ parts.push(`### Memory #${memory.id}`);
1168
+ if (memory.summary) parts.push(`${memory.summary}
1169
+ `);
1170
+ if (memory.key_insights.length) {
1171
+ parts.push("Key Insights:");
1172
+ for (const insight of memory.key_insights) parts.push(` - ${insight}`);
1173
+ parts.push("");
1174
+ }
1175
+ if (memory.technical_decisions.length) {
1176
+ parts.push("Technical Decisions:");
1177
+ for (const decision of memory.technical_decisions) parts.push(` - ${decision}`);
1178
+ parts.push("");
1179
+ }
818
1180
  }
819
1181
  }
820
- );
821
- server.setRequestHandler(ListToolsRequestSchema, async () => {
822
- return {
823
- tools: [
1182
+ if (summary && memories.length > 0) {
1183
+ parts.push("Recent Memory Entries:");
1184
+ for (const memory of memories) {
1185
+ if (memory.summary) parts.push(` - [memory #${memory.id}] ${memory.summary}`);
1186
+ }
1187
+ parts.push("");
1188
+ }
1189
+ }
1190
+ if (prompts.length > 0) {
1191
+ if (parts.length > 0) parts.push("---\n");
1192
+ parts.push("## Recent Context\n");
1193
+ for (const entry of prompts) {
1194
+ let metadata = {};
1195
+ try {
1196
+ metadata = JSON.parse(entry.metadata);
1197
+ } catch {
1198
+ }
1199
+ parts.push(`### Interaction #${entry.id} \u2014 ${entry.tool} (${new Date(entry.timestamp).toLocaleDateString()})`);
1200
+ if (metadata.role) parts.push(`Role: ${metadata.role}`);
1201
+ const promptSummary = metadata.summary || (options.includePromptText || options.promptExcerptLength ? "" : entry.prompt.slice(0, 100).replace(/\n/g, " "));
1202
+ if (promptSummary) parts.push(promptSummary);
1203
+ if (metadata.key_insights?.length) {
1204
+ for (const insight of metadata.key_insights) parts.push(` - ${insight}`);
1205
+ }
1206
+ if (options.includePromptText) {
1207
+ parts.push("```\n" + entry.prompt.slice(0, 1e3) + (entry.prompt.length > 1e3 ? "\n..." : "") + "\n```");
1208
+ } else if (options.promptExcerptLength) {
1209
+ const excerpt = entry.prompt.slice(0, options.promptExcerptLength);
1210
+ parts.push(`Prompt: ${excerpt}${entry.prompt.length > options.promptExcerptLength ? "..." : ""}`);
1211
+ }
1212
+ if (options.responseExcerptLength && entry.response) {
1213
+ const response = entry.response.slice(0, options.responseExcerptLength);
1214
+ parts.push(`Response: ${response}${entry.response.length > options.responseExcerptLength ? "..." : ""}`);
1215
+ }
1216
+ parts.push("");
1217
+ }
1218
+ }
1219
+ if (parts.length === 0) return options.emptyMessage ?? `No relevant context found for project "${project}".`;
1220
+ return parts.join("\n").trimEnd();
1221
+ }
1222
+
1223
+ // src/mcp/server.ts
1224
+ import { z } from "zod";
1225
+ function createMCPServer(db, cfg = load()) {
1226
+ const server = new Server(
1227
+ {
1228
+ name: "ph-memory",
1229
+ version: "1.0.0"
1230
+ },
1231
+ {
1232
+ capabilities: {
1233
+ tools: {}
1234
+ }
1235
+ }
1236
+ );
1237
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
1238
+ return {
1239
+ tools: [
1240
+ {
1241
+ name: "list_prompts",
1242
+ description: "List prompts for a project with pagination. Use page=1, pageSize=10 to browse.",
1243
+ inputSchema: {
1244
+ type: "object",
1245
+ properties: {
1246
+ project: { type: "string", description: "Project name to list prompts for" },
1247
+ page: { type: "number", description: "Page number (1-based)", default: 1 },
1248
+ pageSize: { type: "number", description: "Results per page (max 100)", default: 20 }
1249
+ },
1250
+ required: ["project"]
1251
+ }
1252
+ },
1253
+ {
1254
+ name: "save_decision",
1255
+ description: "Save an architectural decision or key insight to project memory.",
1256
+ inputSchema: {
1257
+ type: "object",
1258
+ properties: {
1259
+ project: { type: "string", description: "Project name" },
1260
+ summary: { type: "string", description: "Brief summary of the decision" },
1261
+ keyInsights: {
1262
+ type: "array",
1263
+ items: { type: "string" },
1264
+ description: "Key insights or takeaways"
1265
+ },
1266
+ technicalDecisions: {
1267
+ type: "array",
1268
+ items: { type: "string" },
1269
+ description: "Technical decisions made"
1270
+ }
1271
+ },
1272
+ required: ["project", "summary"]
1273
+ }
1274
+ },
1275
+ {
1276
+ name: "get_project_diff",
1277
+ description: "Compare project state between two points in time. Shows prompts and memory entries created in the date range.",
1278
+ inputSchema: {
1279
+ type: "object",
1280
+ properties: {
1281
+ project: { type: "string", description: "Project name" },
1282
+ since: { type: "string", description: "Start date (ISO 8601, e.g. 2026-01-01)" },
1283
+ until: { type: "string", description: "End date (ISO 8601, defaults to now)" }
1284
+ },
1285
+ required: ["project", "since"]
1286
+ }
1287
+ },
824
1288
  {
825
1289
  name: "search_project_memory",
826
1290
  description: "Search for past interactions and technical decisions in a specific project using semantic search.",
@@ -904,7 +1368,10 @@ async function runMCPServer() {
904
1368
  inputSchema: {
905
1369
  type: "object",
906
1370
  properties: {
907
- project: { type: "string", description: "The project name" }
1371
+ project: { type: "string", description: "The project name" },
1372
+ since: { type: "string", description: "Start date (ISO 8601, optional)" },
1373
+ until: { type: "string", description: "End date (ISO 8601, optional)" },
1374
+ limit: { type: "number", description: "Max events to return (default 100)", default: 100 }
908
1375
  },
909
1376
  required: ["project"]
910
1377
  }
@@ -928,6 +1395,115 @@ async function runMCPServer() {
928
1395
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
929
1396
  const { name, arguments: args } = request.params;
930
1397
  try {
1398
+ if (name === "list_prompts") {
1399
+ const { project, page = 1, pageSize = 20 } = z.object({
1400
+ project: z.string(),
1401
+ page: z.number().optional(),
1402
+ pageSize: z.number().max(100).optional()
1403
+ }).parse(args);
1404
+ const result = db.getPromptsByProjectPaginated(project, page, pageSize);
1405
+ const totalPages = Math.ceil(result.total / pageSize);
1406
+ const text = [
1407
+ `## Prompts for "${project}"`,
1408
+ `Page ${page}/${totalPages} (${result.total} total)
1409
+ `,
1410
+ ...result.entries.map((e) => {
1411
+ let meta = {};
1412
+ try {
1413
+ meta = JSON.parse(e.metadata);
1414
+ } catch {
1415
+ }
1416
+ const date = new Date(e.timestamp).toLocaleString();
1417
+ const title = meta.title ? ` \u2014 ${meta.title}` : "";
1418
+ const roleStr = meta.role ? ` (${meta.role})` : "";
1419
+ return `### #${e.id} \u2014 ${e.tool}${roleStr} (${date})${title}
1420
+ ${meta.summary ? `Summary: ${meta.summary}
1421
+ ` : ""}Prompt: ${e.prompt.slice(0, 200)}${e.prompt.length > 200 ? "..." : ""}`;
1422
+ }).join("\n\n"),
1423
+ "",
1424
+ `Page ${page}/${totalPages} | ${result.total} total prompts`
1425
+ ].join("\n");
1426
+ return { content: [{ type: "text", text }] };
1427
+ }
1428
+ if (name === "save_decision") {
1429
+ const { project, summary, keyInsights, technicalDecisions } = z.object({
1430
+ project: z.string(),
1431
+ summary: z.string(),
1432
+ keyInsights: z.array(z.string()).optional(),
1433
+ technicalDecisions: z.array(z.string()).optional()
1434
+ }).parse(args);
1435
+ const id = db.insertMemory({
1436
+ project,
1437
+ prompt_ids: [],
1438
+ summary,
1439
+ key_insights: keyInsights ?? [],
1440
+ technical_decisions: technicalDecisions ?? [],
1441
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1442
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
1443
+ access_count: 0
1444
+ });
1445
+ db.upsertProjectSummary({
1446
+ project,
1447
+ summary,
1448
+ key_insights: keyInsights ?? [],
1449
+ technical_decisions: technicalDecisions ?? []
1450
+ });
1451
+ return {
1452
+ content: [{ type: "text", text: `Decision saved as memory #${id} for project "${project}".
1453
+
1454
+ Summary: ${summary}
1455
+ Key Insights: ${(keyInsights ?? []).length}
1456
+ Technical Decisions: ${(technicalDecisions ?? []).length}` }]
1457
+ };
1458
+ }
1459
+ if (name === "get_project_diff") {
1460
+ const { project, since, until } = z.object({
1461
+ project: z.string(),
1462
+ since: z.string(),
1463
+ until: z.string().optional()
1464
+ }).parse(args);
1465
+ const sinceDate = new Date(since);
1466
+ const untilDate = until ? new Date(until) : /* @__PURE__ */ new Date();
1467
+ const prompts = db.search({
1468
+ project,
1469
+ since: sinceDate,
1470
+ until: untilDate,
1471
+ limit: 1e3
1472
+ });
1473
+ const memories = db.getAllMemoriesByProject(project).filter((m) => {
1474
+ const created = new Date(m.created_at);
1475
+ return created >= sinceDate && created <= untilDate;
1476
+ });
1477
+ const lines = [
1478
+ `## Project Diff: ${project}`,
1479
+ `From: ${since} To: ${until || "now"}`,
1480
+ `Prompts: ${prompts.length} Memories: ${memories.length}
1481
+ `
1482
+ ];
1483
+ if (prompts.length > 0) {
1484
+ lines.push("### Prompts\n");
1485
+ for (const p of prompts.reverse()) {
1486
+ let meta = {};
1487
+ try {
1488
+ meta = JSON.parse(p.metadata);
1489
+ } catch {
1490
+ }
1491
+ const title = meta.title ? ` \u2014 ${meta.title}` : "";
1492
+ const roleStr = meta.role ? ` (${meta.role})` : "";
1493
+ lines.push(`- **#${p.id}** ${p.tool}${roleStr}${title} (${new Date(p.timestamp).toLocaleDateString()})`);
1494
+ if (meta.summary) lines.push(` Summary: ${meta.summary}`);
1495
+ }
1496
+ }
1497
+ if (memories.length > 0) {
1498
+ lines.push("\n### Memory Entries\n");
1499
+ for (const m of memories) {
1500
+ lines.push(`- **Memory #${m.id}**: ${m.summary}`);
1501
+ for (const i of m.key_insights) lines.push(` - Insight: ${i}`);
1502
+ for (const d of m.technical_decisions) lines.push(` - Decision: ${d}`);
1503
+ }
1504
+ }
1505
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1506
+ }
931
1507
  if (name === "search_project_memory") {
932
1508
  const { query, project, limit = 5 } = z.object({
933
1509
  query: z.string(),
@@ -940,15 +1516,30 @@ async function runMCPServer() {
940
1516
  if (!queryVec) {
941
1517
  throw new Error("Failed to generate embedding for query");
942
1518
  }
943
- const results = db.searchSemantic(queryVec, limit * 2);
944
- const filtered = results.filter((e) => {
945
- try {
946
- const meta = JSON.parse(e.metadata);
947
- return meta.project === project;
948
- } catch {
949
- return false;
1519
+ const filtered = db.searchSemantic(queryVec, limit, project);
1520
+ if (filtered.length === 0) {
1521
+ const merged = db.getProjectSummary(project);
1522
+ if (merged) {
1523
+ const parts = [
1524
+ `## Project Knowledge: ${project}
1525
+ `,
1526
+ `Based on ${merged.prompt_count} interactions.
1527
+ `
1528
+ ];
1529
+ if (merged.summary) parts.push(`${merged.summary}
1530
+ `);
1531
+ if (merged.key_insights.length > 0) {
1532
+ parts.push("\n**Key Insights:**");
1533
+ for (const i of merged.key_insights) parts.push(`- ${i}`);
1534
+ }
1535
+ if (merged.technical_decisions.length > 0) {
1536
+ parts.push("\n**Technical Decisions:**");
1537
+ for (const d of merged.technical_decisions) parts.push(`- ${d}`);
1538
+ }
1539
+ return { content: [{ type: "text", text: parts.join("\n") }] };
950
1540
  }
951
- }).slice(0, limit);
1541
+ return { content: [{ type: "text", text: `No relevant memory found for project "${project}".` }] };
1542
+ }
952
1543
  return {
953
1544
  content: [{ type: "text", text: formatResultsAsMarkdown(filtered) }]
954
1545
  };
@@ -958,59 +1549,39 @@ async function runMCPServer() {
958
1549
  project: z.string(),
959
1550
  limit: z.number().optional()
960
1551
  }).parse(args);
961
- const memories = db.searchMemories(project, 3);
962
- const prompts = db.getProjectMemory(project, limit);
963
- const parts = [];
964
- if (memories.length > 0) {
965
- parts.push("## Project Knowledge\n");
966
- for (const mem of memories) {
967
- if (mem.summary) parts.push(`${mem.summary}
968
- `);
969
- if (mem.key_insights.length > 0) {
970
- parts.push("Key Insights:");
971
- for (const i of mem.key_insights) parts.push(` - ${i}`);
972
- parts.push("");
973
- }
974
- if (mem.technical_decisions.length > 0) {
975
- parts.push("Technical Decisions:");
976
- for (const d of mem.technical_decisions) parts.push(` - ${d}`);
977
- parts.push("");
978
- }
979
- }
980
- }
981
- if (prompts.length > 0) {
982
- if (parts.length > 0) parts.push("---\n");
983
- parts.push("## Recent Interactions\n");
984
- parts.push(formatResultsAsMarkdown(prompts));
985
- }
1552
+ const context = await getProjectContext(db, { project, limit, memoryLimit: 3 });
986
1553
  return {
987
- content: [{ type: "text", text: parts.join("\n") || "No context found for this project." }]
1554
+ content: [{ type: "text", text: formatProjectContext(context, {
1555
+ promptExcerptLength: 300,
1556
+ responseExcerptLength: 200,
1557
+ emptyMessage: "No context found for this project."
1558
+ }) }]
988
1559
  };
989
1560
  }
990
1561
  if (name === "get_project_summary") {
991
1562
  const { project } = z.object({
992
1563
  project: z.string()
993
1564
  }).parse(args);
994
- const memories = db.searchMemories(project, 5);
995
- if (memories.length === 0) {
1565
+ const summary = db.getProjectSummary(project);
1566
+ if (!summary) {
996
1567
  return {
997
1568
  content: [{ type: "text", text: `No accumulated knowledge for project "${project}". Run "ph analyze" to generate insights.` }]
998
1569
  };
999
1570
  }
1000
1571
  const parts = [];
1001
- for (const mem of memories) {
1002
- parts.push(`## ${mem.summary || "Project Memory"}
1572
+ parts.push(`## ${summary.summary || "Project Memory"}
1003
1573
  `);
1004
- if (mem.key_insights.length > 0) {
1005
- parts.push("**Key Insights:**");
1006
- for (const i of mem.key_insights) parts.push(`- ${i}`);
1007
- parts.push("");
1008
- }
1009
- if (mem.technical_decisions.length > 0) {
1010
- parts.push("**Technical Decisions:**");
1011
- for (const d of mem.technical_decisions) parts.push(`- ${d}`);
1012
- parts.push("");
1013
- }
1574
+ parts.push(`Based on ${summary.prompt_count} interactions.
1575
+ `);
1576
+ if (summary.key_insights.length > 0) {
1577
+ parts.push("**Key Insights:**");
1578
+ for (const i of summary.key_insights) parts.push(`- ${i}`);
1579
+ parts.push("");
1580
+ }
1581
+ if (summary.technical_decisions.length > 0) {
1582
+ parts.push("**Technical Decisions:**");
1583
+ for (const d of summary.technical_decisions) parts.push(`- ${d}`);
1584
+ parts.push("");
1014
1585
  }
1015
1586
  return {
1016
1587
  content: [{ type: "text", text: parts.join("\n") }]
@@ -1067,32 +1638,37 @@ async function runMCPServer() {
1067
1638
  if (!queryVec) {
1068
1639
  throw new Error("Failed to generate embedding for query");
1069
1640
  }
1070
- const results = db.searchSemantic(queryVec, limit * 2);
1071
- const filtered = project ? results.filter((e) => {
1072
- try {
1073
- const meta = JSON.parse(e.metadata);
1074
- return meta.project === project;
1075
- } catch {
1076
- return false;
1077
- }
1078
- }).slice(0, limit) : results.slice(0, limit);
1641
+ const filtered = db.searchSemantic(queryVec, limit, project);
1079
1642
  return {
1080
1643
  content: [{ type: "text", text: formatPromptList(filtered, query) }]
1081
1644
  };
1082
1645
  }
1083
1646
  if (name === "get_project_timeline") {
1084
- const { project } = z.object({
1085
- project: z.string()
1647
+ const { project, since, until, limit = 100 } = z.object({
1648
+ project: z.string(),
1649
+ since: z.string().optional(),
1650
+ until: z.string().optional(),
1651
+ limit: z.number().optional()
1086
1652
  }).parse(args);
1087
- const prompts = db.getAllPromptsByProject(project);
1088
- const memories = db.getAllMemoriesByProject(project);
1089
- if (prompts.length === 0 && memories.length === 0) {
1653
+ let allPrompts = db.getAllPromptsByProject(project);
1654
+ let memories = db.getAllMemoriesByProject(project);
1655
+ if (since) {
1656
+ const sinceDate = new Date(since);
1657
+ allPrompts = allPrompts.filter((p) => new Date(p.timestamp) >= sinceDate);
1658
+ memories = memories.filter((m) => new Date(m.created_at) >= sinceDate);
1659
+ }
1660
+ if (until) {
1661
+ const untilDate = new Date(until);
1662
+ allPrompts = allPrompts.filter((p) => new Date(p.timestamp) <= untilDate);
1663
+ memories = memories.filter((m) => new Date(m.created_at) <= untilDate);
1664
+ }
1665
+ if (allPrompts.length === 0 && memories.length === 0) {
1090
1666
  return {
1091
- content: [{ type: "text", text: `No history found for project "${project}".` }]
1667
+ content: [{ type: "text", text: `No history found for project "${project}"${since ? ` since ${since}` : ""}${until ? ` until ${until}` : ""}.` }]
1092
1668
  };
1093
1669
  }
1094
1670
  const events = [];
1095
- for (const p of prompts) {
1671
+ for (const p of allPrompts.slice(0, limit)) {
1096
1672
  let meta = {};
1097
1673
  try {
1098
1674
  meta = JSON.parse(p.metadata);
@@ -1128,9 +1704,10 @@ ${m.technical_decisions.map((d) => `- ${d}`).join("\n")}` : ""
1128
1704
  });
1129
1705
  }
1130
1706
  events.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
1131
- const header = `# Timeline: ${project}
1707
+ const range = since || until ? ` (${since || "\u2026"} \u2192 ${until || "now"})` : "";
1708
+ const header = `# Timeline: ${project}${range}
1132
1709
 
1133
- ${prompts.length} prompts \xB7 ${memories.length} memory entries
1710
+ ${allPrompts.length} prompts \xB7 ${memories.length} memory entries
1134
1711
 
1135
1712
  `;
1136
1713
  const body = events.map((e) => {
@@ -1180,15 +1757,7 @@ ${m.technical_decisions.map((d) => `- ${d}`).join("\n")}`);
1180
1757
  try {
1181
1758
  const [queryVec] = await getEmbeddings([`${project}: ${task}`], ollamaUrl, embedModel, 1);
1182
1759
  if (queryVec) {
1183
- const results = db.searchSemantic(queryVec, limit * 3);
1184
- const projectPrompts = results.filter((e) => {
1185
- try {
1186
- const meta = JSON.parse(e.metadata);
1187
- return meta.project === project;
1188
- } catch {
1189
- return false;
1190
- }
1191
- }).slice(0, limit);
1760
+ const projectPrompts = db.searchSemantic(queryVec, limit, project);
1192
1761
  if (projectPrompts.length > 0) {
1193
1762
  parts.push(`## Related Prompts
1194
1763
  `);
@@ -1226,6 +1795,13 @@ ${m.technical_decisions.map((d) => `- ${d}`).join("\n")}`);
1226
1795
  };
1227
1796
  }
1228
1797
  });
1798
+ return server;
1799
+ }
1800
+ async function runMCPServer() {
1801
+ const cfg = load();
1802
+ const dbPath = process.env.PH_DB ?? cfg.dbPath ?? defaultPath();
1803
+ const db = new PhDB(dbPath);
1804
+ const server = createMCPServer(db, cfg);
1229
1805
  const transport = new StdioServerTransport();
1230
1806
  await server.connect(transport);
1231
1807
  console.error("ph MCP server running on stdio");
@@ -1327,12 +1903,31 @@ function formatSinglePrompt(e) {
1327
1903
 
1328
1904
  // src/server/index.ts
1329
1905
  import http from "http";
1906
+ import { URL } from "url";
1907
+
1908
+ // src/utils/syncHash.ts
1330
1909
  import { createHash as createHash2 } from "crypto";
1910
+ function syncHash(entry) {
1911
+ const body = entry.args ? `${entry.tool}|${entry.prompt}|${entry.response}|${entry.args}` : `${entry.tool}|${entry.prompt}|${entry.response}`;
1912
+ return createHash2("sha256").update(body).digest("hex");
1913
+ }
1914
+
1915
+ // src/server/index.ts
1916
+ import { timingSafeEqual } from "crypto";
1331
1917
  async function runServer(port = 3001, host = "0.0.0.0") {
1332
1918
  const cfg = load();
1333
1919
  const dbPath = process.env.PH_DB ?? cfg.dbPath ?? defaultPath();
1334
1920
  const db = new PhDB(dbPath);
1335
- const server = http.createServer(async (req, res) => {
1921
+ const server = http.createServer(createRequestHandler(db, cfg));
1922
+ return new Promise((resolve) => {
1923
+ server.listen(port, host, () => {
1924
+ console.error(`ph server listening on http://${host}:${port}`);
1925
+ resolve();
1926
+ });
1927
+ });
1928
+ }
1929
+ function createRequestHandler(db, cfg) {
1930
+ return async (req, res) => {
1336
1931
  res.setHeader("Access-Control-Allow-Origin", "*");
1337
1932
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1338
1933
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
@@ -1341,28 +1936,34 @@ async function runServer(port = 3001, host = "0.0.0.0") {
1341
1936
  res.end();
1342
1937
  return;
1343
1938
  }
1939
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
1940
+ if (url.pathname !== "/health" && !authorized(req, cfg)) {
1941
+ json(res, 401, { error: "unauthorized" });
1942
+ return;
1943
+ }
1344
1944
  try {
1345
1945
  const body = await readBody(req);
1346
1946
  await route(req, res, body, db, cfg);
1347
1947
  } catch (err) {
1348
1948
  json(res, 500, { error: err.message });
1349
1949
  }
1350
- });
1351
- return new Promise((resolve) => {
1352
- server.listen(port, host, () => {
1353
- console.error(`ph server listening on http://${host}:${port}`);
1354
- resolve();
1355
- });
1356
- });
1950
+ };
1951
+ }
1952
+ function authorized(req, cfg) {
1953
+ if (!cfg.remoteApiKey) return true;
1954
+ const expected = `Bearer ${cfg.remoteApiKey}`;
1955
+ const a = Buffer.from(req.headers.authorization ?? "");
1956
+ const b = Buffer.from(expected);
1957
+ return a.length === b.length && timingSafeEqual(a, b);
1357
1958
  }
1358
1959
  async function route(req, res, body, db, cfg) {
1359
1960
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
1360
- const path9 = url.pathname;
1961
+ const path10 = url.pathname;
1361
1962
  const method = req.method || "GET";
1362
- if (method === "GET" && path9 === "/health") {
1363
- return json(res, 200, { status: "ok", dbPath: db.db?.name || "unknown" });
1963
+ if (method === "GET" && path10 === "/health") {
1964
+ return json(res, 200, { status: "ok", dbPath: db.dbPath });
1364
1965
  }
1365
- if (method === "POST" && path9 === "/api/prompts/search") {
1966
+ if (method === "POST" && path10 === "/api/prompts/search") {
1366
1967
  const { query, tool, project, role, tag, since, until, limit = 10 } = JSON.parse(body);
1367
1968
  const results = db.search({
1368
1969
  query,
@@ -1376,31 +1977,23 @@ async function route(req, res, body, db, cfg) {
1376
1977
  });
1377
1978
  return json(res, 200, { prompts: results });
1378
1979
  }
1379
- if (method === "POST" && path9 === "/api/prompts/by-id") {
1980
+ if (method === "POST" && path10 === "/api/prompts/by-id") {
1380
1981
  const { id } = JSON.parse(body);
1381
1982
  const entry = db.getById(id);
1382
1983
  if (!entry) return json(res, 404, { error: `Prompt #${id} not found` });
1383
1984
  return json(res, 200, { prompt: entry });
1384
1985
  }
1385
- if (method === "POST" && path9 === "/api/prompts/semantic") {
1986
+ if (method === "POST" && path10 === "/api/prompts/semantic") {
1386
1987
  const { query, project, limit = 5 } = JSON.parse(body);
1387
1988
  if (!query) return json(res, 400, { error: "query is required" });
1388
1989
  const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
1389
1990
  const model = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
1390
1991
  const [queryVec] = await getEmbeddings([query], ollamaUrl, model, 1);
1391
1992
  if (!queryVec) return json(res, 500, { error: "Failed to generate embedding" });
1392
- const results = db.searchSemantic(queryVec, limit * 2);
1393
- const filtered = project ? results.filter((e) => {
1394
- try {
1395
- const meta = JSON.parse(e.metadata);
1396
- return meta.project === project;
1397
- } catch {
1398
- return false;
1399
- }
1400
- }).slice(0, limit) : results.slice(0, limit);
1993
+ const filtered = db.searchSemantic(queryVec, limit, project);
1401
1994
  return json(res, 200, { prompts: filtered });
1402
1995
  }
1403
- if (method === "POST" && path9 === "/api/sync/push") {
1996
+ if (method === "POST" && path10 === "/api/sync/push") {
1404
1997
  const { prompts } = JSON.parse(body);
1405
1998
  if (!Array.isArray(prompts)) return json(res, 400, { error: "prompts must be an array" });
1406
1999
  let imported = 0;
@@ -1408,8 +2001,8 @@ async function route(req, res, body, db, cfg) {
1408
2001
  const errors = [];
1409
2002
  for (const p of prompts) {
1410
2003
  try {
1411
- const hash = sha256(`${p.tool}|${p.prompt}|${p.response}`);
1412
- const existing = db.db.prepare("SELECT id FROM prompts WHERE json_extract(metadata, '$.sync_hash') = ?").get(hash);
2004
+ const hash = syncHash(p);
2005
+ const existing = db.getPromptBySyncHash(hash);
1413
2006
  if (existing) {
1414
2007
  skipped++;
1415
2008
  continue;
@@ -1433,35 +2026,28 @@ async function route(req, res, body, db, cfg) {
1433
2026
  }
1434
2027
  return json(res, 200, { imported, skipped, errors });
1435
2028
  }
1436
- if (method === "POST" && path9 === "/api/sync/pull") {
2029
+ if (method === "POST" && path10 === "/api/sync/pull") {
1437
2030
  const { since } = JSON.parse(body);
1438
2031
  const limit = 1e4;
1439
- let results;
1440
- if (since) {
1441
- results = db.db.prepare("SELECT * FROM prompts WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?").all(since, limit);
1442
- } else {
1443
- results = db.db.prepare("SELECT * FROM prompts ORDER BY timestamp ASC LIMIT ?").all(limit);
1444
- }
2032
+ const results = since ? db.getPromptsSince(since, limit) : db.getAllPrompts(limit);
1445
2033
  return json(res, 200, { prompts: results, hasMore: results.length >= limit });
1446
2034
  }
1447
- if (method === "POST" && path9 === "/api/memories/search") {
2035
+ if (method === "POST" && path10 === "/api/memories/search") {
1448
2036
  const { project, limit = 10 } = JSON.parse(body);
1449
2037
  const memories = project ? db.searchMemories(project, limit) : [];
1450
2038
  return json(res, 200, { memories });
1451
2039
  }
1452
- if (method === "POST" && path9 === "/api/memories/summary") {
2040
+ if (method === "POST" && path10 === "/api/memories/summary") {
1453
2041
  const { project } = JSON.parse(body);
1454
2042
  if (!project) return json(res, 400, { error: "project is required" });
1455
2043
  const memories = db.searchMemories(project, 5);
1456
2044
  return json(res, 200, { memories });
1457
2045
  }
1458
- if (method === "GET" && path9 === "/api/stats") {
1459
- const total = db.db.prepare("SELECT count(*) as c FROM prompts").get().c;
1460
- const totalMemories = db.db.prepare("SELECT count(*) as c FROM memories").get().c;
1461
- const byTool = db.db.prepare("SELECT tool, count(*) as count FROM prompts GROUP BY tool ORDER BY count DESC").all();
2046
+ if (method === "GET" && path10 === "/api/stats") {
2047
+ const { total, totalMemories, byTool } = db.getStats();
1462
2048
  return json(res, 200, { total, totalMemories, byTool });
1463
2049
  }
1464
- json(res, 404, { error: `Not found: ${method} ${path9}` });
2050
+ json(res, 404, { error: `Not found: ${method} ${path10}` });
1465
2051
  }
1466
2052
  function json(res, status2, data) {
1467
2053
  res.writeHead(status2, { "Content-Type": "application/json" });
@@ -1476,12 +2062,8 @@ function readBody(req) {
1476
2062
  req.on("error", reject);
1477
2063
  });
1478
2064
  }
1479
- function sha256(s) {
1480
- return createHash2("sha256").update(s).digest("hex");
1481
- }
1482
2065
 
1483
2066
  // src/commands/remote.ts
1484
- import { createHash as createHash3 } from "crypto";
1485
2067
  function remoteUrl(cfg) {
1486
2068
  return process.env.PH_REMOTE_URL || cfg.remoteUrl || "";
1487
2069
  }
@@ -1584,7 +2166,7 @@ async function pull(dbPath, cfg, url) {
1584
2166
  let imported = 0;
1585
2167
  let skipped = 0;
1586
2168
  for (const p of result.prompts) {
1587
- const hash = sha2562(`${p.tool}|${p.prompt}|${p.response}`);
2169
+ const hash = syncHash(p);
1588
2170
  const existing = db.getPromptBySyncHash(hash);
1589
2171
  if (existing) {
1590
2172
  skipped++;
@@ -1622,7 +2204,7 @@ async function status(dbPath, cfg, url) {
1622
2204
  const localTotal = db.getPromptCount();
1623
2205
  const lastPush = cfg.remoteLastPush || "never";
1624
2206
  const lastPull = cfg.remoteLastPull || "never";
1625
- const pending = cfg.remoteLastPush ? db.db.prepare("SELECT count(*) as c FROM prompts WHERE timestamp > ?").get(cfg.remoteLastPush).c : localTotal;
2207
+ const pending = cfg.remoteLastPush ? db.getPromptCountSince(cfg.remoteLastPush) : localTotal;
1626
2208
  process.stdout.write(`Remote: ${url}
1627
2209
  `);
1628
2210
  process.stdout.write(`Local total: ${localTotal} prompts
@@ -1635,13 +2217,10 @@ async function status(dbPath, cfg, url) {
1635
2217
  `);
1636
2218
  db.close();
1637
2219
  }
1638
- function sha2562(s) {
1639
- return createHash3("sha256").update(s).digest("hex");
1640
- }
1641
2220
 
1642
2221
  // src/ui/BrowseApp.tsx
1643
- import React6, { useState as useState2, useCallback, useEffect as useEffect2, useMemo as useMemo3 } from "react";
1644
- import { Box as Box4, Text as Text5, useInput as useInput2, useApp } from "ink";
2222
+ import React7, { useState as useState3, useCallback, useEffect, useMemo as useMemo4 } from "react";
2223
+ import { Box as Box5, Text as Text6, useInput as useInput3, useApp } from "ink";
1645
2224
  import { spawn as spawn2 } from "child_process";
1646
2225
 
1647
2226
  // src/ui/Header.tsx
@@ -1652,7 +2231,7 @@ import { AppHeader, THEMES } from "@gi4nks/ink";
1652
2231
  // package.json
1653
2232
  var package_default = {
1654
2233
  name: "@gi4nks/ph",
1655
- version: "1.4.0",
2234
+ version: "1.5.0",
1656
2235
  description: "Transparent CLI wrapper that captures and indexes AI prompts",
1657
2236
  type: "module",
1658
2237
  license: "MIT",
@@ -1703,37 +2282,38 @@ var package_default = {
1703
2282
  dev: "tsx src/cli.ts",
1704
2283
  start: "node dist/cli.js",
1705
2284
  test: "vitest run",
1706
- lint: "eslint src"
2285
+ lint: "eslint src",
2286
+ "eval:retrieval": "tsx scripts/eval-context-retrieval.ts"
1707
2287
  },
1708
2288
  dependencies: {
1709
2289
  "@gi4nks/ink": "^1.5.0",
1710
2290
  "@lydell/node-pty": "^1.2.0-beta.3",
1711
2291
  "@modelcontextprotocol/sdk": "^1.29.0",
1712
2292
  "better-sqlite3": "^12.8.0",
1713
- ink: "^6.8.0",
1714
- react: "^19.2.4",
2293
+ ink: "^7.0.5",
2294
+ react: "^19.2.7",
1715
2295
  "sqlite-vec": "^0.1.9",
1716
2296
  zod: "^4.3.6"
1717
2297
  },
1718
2298
  devDependencies: {
2299
+ "@eslint/js": "^9.39.5",
1719
2300
  "@semantic-release/changelog": "^6.0.3",
1720
2301
  "@semantic-release/git": "^10.0.1",
1721
2302
  "@semantic-release/github": "^12.0.8",
1722
2303
  "@semantic-release/npm": "^13.1.5",
1723
2304
  "@types/better-sqlite3": "^7.6.13",
1724
- "@types/node": "^25.5.0",
1725
- "@types/react": "^19.2.14",
1726
- "@typescript-eslint/eslint-plugin": "^8.57.2",
1727
- "@typescript-eslint/parser": "^8.57.2",
1728
- eslint: "^9.39.4",
2305
+ "@types/node": "^25.9.3",
2306
+ "@types/react": "^19.2.7",
2307
+ "@typescript-eslint/eslint-plugin": "^8.58.2",
2308
+ "@typescript-eslint/parser": "^8.58.2",
2309
+ eslint: "^9.39.5",
1729
2310
  "eslint-plugin-react": "^7.37.5",
1730
2311
  "eslint-plugin-react-hooks": "^7.0.1",
1731
2312
  "semantic-release": "^25.0.3",
1732
- "standard-version": "^9.5.0",
1733
2313
  tsup: "^8.5.1",
1734
2314
  tsx: "^4.21.0",
1735
- typescript: "^5.9.3",
1736
- vitest: "^4.1.0"
2315
+ typescript: "^6.0.3",
2316
+ vitest: "^4.1.8"
1737
2317
  }
1738
2318
  };
1739
2319
 
@@ -1753,6 +2333,7 @@ var FilterBadges = ({ active, textFilter }) => /* @__PURE__ */ React.createEleme
1753
2333
  var Header = ({
1754
2334
  entriesCount,
1755
2335
  allEntriesCount,
2336
+ archiveCount,
1756
2337
  activeFilters,
1757
2338
  textFilter,
1758
2339
  isTextFiltering,
@@ -1762,7 +2343,7 @@ var Header = ({
1762
2343
  const version = package_default.version;
1763
2344
  const rawCwd = process.cwd();
1764
2345
  const cwd = rawCwd.replace(process.env.HOME || "", "~");
1765
- const leftExtra = /* @__PURE__ */ React.createElement(Text, { color: "white", dimColor: true }, " ", entriesCount, entriesCount < allEntriesCount ? `/${allEntriesCount}` : "", " prompts");
2346
+ const leftExtra = /* @__PURE__ */ React.createElement(Text, { color: "white", dimColor: true }, " ", entriesCount, entriesCount < allEntriesCount ? `/${allEntriesCount}` : "", " prompts", archiveCount !== void 0 && archiveCount > 0 ? /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, " (", archiveCount, " archived)") : null);
1766
2347
  const rightContent = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(FilterBadges, { active: activeFilters, textFilter }), isTextFiltering && /* @__PURE__ */ React.createElement(Text, { color: theme.success }, " [typing\u2026]"));
1767
2348
  return /* @__PURE__ */ React.createElement(
1768
2349
  AppHeader,
@@ -1804,6 +2385,7 @@ var Footer = ({
1804
2385
  );
1805
2386
  if (hasProject2) base.push({ key: "C", description: "chat" });
1806
2387
  base.push({ key: "r", description: "rerun" });
2388
+ base.push({ key: "p", description: "provider" });
1807
2389
  base.push("|");
1808
2390
  base.push(
1809
2391
  { key: "x", description: "del" },
@@ -1825,113 +2407,217 @@ var Footer = ({
1825
2407
  );
1826
2408
  };
1827
2409
 
1828
- // src/ui/themes.ts
1829
- import { THEMES as THEMES3, THEME_NAMES } from "@gi4nks/ink";
2410
+ // src/ui/FilterPanel.tsx
2411
+ import React3, { useMemo as useMemo2, useState } from "react";
2412
+ import { Box, Text as Text2, useInput } from "ink";
1830
2413
 
1831
- // src/ui/SearchBar.tsx
1832
- import React3 from "react";
1833
- import { Box, Text as Text2 } from "ink";
1834
- var SearchBar = ({ value, isActive, theme }) => {
1835
- const isEmpty = value === "";
1836
- const bgColor = isActive ? theme.primary : theme.dim;
1837
- return /* @__PURE__ */ React3.createElement(Box, { paddingX: 1, paddingY: 0 }, /* @__PURE__ */ React3.createElement(Text2, { color: bgColor, bold: isActive }, isActive ? "\u2315" : "\u2315"), /* @__PURE__ */ React3.createElement(Text2, { color: isActive ? "white" : theme.dim }, " ", isEmpty ? isActive ? "" : "search (/)" : value, isActive ? "\u2588" : ""));
2414
+ // src/ui/filtering.ts
2415
+ var FILTER_CATEGORIES = ["project", "language", "role", "tool", "tag", "starred", "quality", "relevance"];
2416
+ var ROLE_COLOR2 = {
2417
+ debug: "red",
2418
+ refactor: "yellow",
2419
+ explain: "blue",
2420
+ review: "magenta",
2421
+ architect: "green",
2422
+ test: "cyan",
2423
+ docs: "white",
2424
+ generate: "green",
2425
+ research: "blue"
1838
2426
  };
1839
-
1840
- // src/ui/ListEntry.tsx
1841
- import React4 from "react";
1842
- import { Box as Box2, Text as Text3 } from "ink";
1843
-
1844
- // src/utils/extractTopic.ts
1845
- function extractTopic(prompt) {
1846
- let text = prompt.trim();
1847
- if (!text) {
1848
- return "(empty prompt)";
1849
- }
1850
- if (text.startsWith("```") || text.startsWith("`")) {
1851
- return truncateAtWordBoundary(text, 72);
2427
+ function parseMetadata(raw) {
2428
+ try {
2429
+ return JSON.parse(raw);
2430
+ } catch {
2431
+ return {};
1852
2432
  }
1853
- const originalTrimmed = text;
1854
- if (text.startsWith("#")) {
1855
- const headingMatch = text.match(/^#+\s+(.*?)(?:\s+#+)?(?:\n|$)/);
1856
- if (headingMatch && headingMatch[1]) {
1857
- text = headingMatch[1].trim();
2433
+ }
2434
+ var CODEX_RETRY_WINDOW_MS = 2 * 60 * 1e3;
2435
+ var CODEX_RETRY_MIN_PREFIX_LENGTH = 80;
2436
+ function displayTitle(entry) {
2437
+ return parseMetadata(entry.metadata).title || extractTopic(entry.prompt);
2438
+ }
2439
+ function normalizedPrompt(prompt) {
2440
+ return prompt.trim().replace(/\s+/g, " ");
2441
+ }
2442
+ function collapseCodexRetries(entries) {
2443
+ const buckets = /* @__PURE__ */ new Map();
2444
+ for (const entry of entries) {
2445
+ if (entry.tool.toLowerCase() !== "codex") continue;
2446
+ const key = `${entry.workdir}\0${displayTitle(entry)}`;
2447
+ const bucket = buckets.get(key) ?? [];
2448
+ bucket.push(entry);
2449
+ buckets.set(key, bucket);
2450
+ }
2451
+ const hidden = /* @__PURE__ */ new Set();
2452
+ for (const bucket of buckets.values()) {
2453
+ const incomplete = bucket.filter((entry) => !entry.response.trim());
2454
+ const completed = bucket.filter((entry) => !!entry.response.trim());
2455
+ for (const pending of incomplete) {
2456
+ const pendingPrompt = normalizedPrompt(pending.prompt);
2457
+ if (pendingPrompt.length < CODEX_RETRY_MIN_PREFIX_LENGTH) continue;
2458
+ const pendingTime = Date.parse(pending.timestamp);
2459
+ if (!Number.isFinite(pendingTime)) continue;
2460
+ const retried = completed.some((candidate) => {
2461
+ const candidateTime = Date.parse(candidate.timestamp);
2462
+ const candidatePrompt = normalizedPrompt(candidate.prompt);
2463
+ const delta = Math.abs(candidateTime - pendingTime);
2464
+ return Number.isFinite(candidateTime) && delta <= CODEX_RETRY_WINDOW_MS && candidatePrompt.length > pendingPrompt.length && candidatePrompt.startsWith(pendingPrompt);
2465
+ });
2466
+ if (retried) hidden.add(pending.id);
1858
2467
  }
1859
2468
  }
1860
- const prefixes = [
1861
- // English
1862
- "please, ",
1863
- "please ",
1864
- "can you please ",
1865
- "could you please ",
1866
- "can you ",
1867
- "could you ",
1868
- "would you ",
1869
- "i need you to ",
1870
- "i need to ",
1871
- "i want you to ",
1872
- "i want to ",
1873
- "i'd like you to ",
1874
- "i'd like to ",
1875
- "help me to ",
1876
- "help me ",
1877
- "i need ",
1878
- "claude, ",
1879
- "gemini, ",
1880
- "chatgpt, ",
1881
- // Italiano
1882
- "puoi ",
1883
- "potresti ",
1884
- "potreste ",
1885
- "per favore ",
1886
- "per piacere ",
1887
- "ho bisogno che ",
1888
- "ho bisogno di ",
1889
- "voglio che ",
1890
- "vorrei che ",
1891
- "voglio ",
1892
- "vorrei ",
1893
- "aiutami a ",
1894
- "aiutami ",
1895
- "fammi vedere ",
1896
- "fammi ",
1897
- "si prega di ",
1898
- "si "
1899
- ];
1900
- let changed = true;
1901
- while (changed) {
1902
- changed = false;
1903
- const lowerText = text.toLowerCase();
1904
- for (const prefix of prefixes) {
1905
- if (lowerText.startsWith(prefix.toLowerCase())) {
1906
- text = text.slice(prefix.length).trim();
1907
- changed = true;
1908
- break;
1909
- }
2469
+ return hidden.size ? entries.filter((entry) => !hidden.has(entry.id)) : entries;
2470
+ }
2471
+ function getDistinctValues(entries, category) {
2472
+ const values = /* @__PURE__ */ new Set();
2473
+ for (const entry of entries) {
2474
+ if (category === "tool") {
2475
+ values.add(entry.tool);
2476
+ continue;
1910
2477
  }
2478
+ const metadata = parseMetadata(entry.metadata);
2479
+ if (category === "project" && metadata.project) values.add(metadata.project);
2480
+ if (category === "language" && metadata.language) values.add(metadata.language);
2481
+ if (category === "role" && metadata.role) values.add(metadata.role);
2482
+ if (category === "tag") metadata.tags?.forEach((tag) => values.add(tag));
1911
2483
  }
1912
- const sentenceEndMatch = text.match(/[\n.?!`]/);
1913
- if (sentenceEndMatch && sentenceEndMatch.index !== void 0) {
1914
- text = text.slice(0, sentenceEndMatch.index).trim();
2484
+ return [...values].sort();
2485
+ }
2486
+ function applyFilters(entries, active, textFilter) {
2487
+ let result = collapseCodexRetries(entries);
2488
+ const hasActiveFilter = Object.values(active).some((value) => value !== void 0 && value !== false);
2489
+ if (hasActiveFilter) {
2490
+ result = result.filter((entry) => {
2491
+ const metadata = parseMetadata(entry.metadata);
2492
+ if (active.tool && entry.tool !== active.tool) return false;
2493
+ if (active.project && metadata.project !== active.project) return false;
2494
+ if (active.language && metadata.language !== active.language) return false;
2495
+ if (active.role && metadata.role !== active.role) return false;
2496
+ if (active.tag && !metadata.tags?.includes(active.tag)) return false;
2497
+ if (active.starred && !metadata.starred) return false;
2498
+ if (active.minQuality !== void 0 && (metadata.quality ?? 0) < active.minQuality) return false;
2499
+ if (active.minRelevance !== void 0 && (metadata.relevance ?? 0) < active.minRelevance) return false;
2500
+ return true;
2501
+ });
1915
2502
  }
1916
- if (!text) {
1917
- return truncateAtWordBoundary(originalTrimmed, 72);
2503
+ if (textFilter) {
2504
+ const query = textFilter.toLowerCase();
2505
+ result = result.filter((entry) => {
2506
+ const metadata = parseMetadata(entry.metadata);
2507
+ return entry.prompt.toLowerCase().includes(query) || entry.tool.toLowerCase().includes(query) || (metadata.project?.toLowerCase().includes(query) ?? false) || (metadata.role?.toLowerCase().includes(query) ?? false) || (metadata.tags?.some((tag) => tag.toLowerCase().includes(query)) ?? false);
2508
+ });
1918
2509
  }
1919
- text = truncateAtWordBoundary(text, 72);
1920
- return text.charAt(0).toUpperCase() + text.slice(1);
2510
+ return result;
1921
2511
  }
1922
- function truncateAtWordBoundary(str, limit) {
1923
- const clean = str.replace(/\s+/g, " ").trim();
1924
- if (clean.length <= limit) return clean;
1925
- const sub = clean.slice(0, limit);
1926
- const lastSpace = sub.lastIndexOf(" ");
1927
- if (lastSpace > 0) {
1928
- return sub.slice(0, lastSpace);
2512
+ function toggleFilter(active, option) {
2513
+ if (option.category === "starred") return { ...active, starred: !option.active || void 0 };
2514
+ if (option.category === "quality" || option.category === "relevance") {
2515
+ const value = parseInt(option.label.replace(option.category === "quality" ? "Q \u2265 " : "R \u2265 ", ""), 10);
2516
+ return option.category === "quality" ? { ...active, minQuality: option.active ? void 0 : value } : { ...active, minRelevance: option.active ? void 0 : value };
2517
+ }
2518
+ const filterKey = option.category;
2519
+ if (option.active) {
2520
+ const updated = { ...active };
2521
+ delete updated[filterKey];
2522
+ return updated;
2523
+ }
2524
+ return { ...active, [filterKey]: option.label };
2525
+ }
2526
+ function buildFilterOptions(entries, active) {
2527
+ const options = [];
2528
+ for (const category of FILTER_CATEGORIES) {
2529
+ if (category === "starred") {
2530
+ const count = entries.filter((entry) => parseMetadata(entry.metadata).starred).length;
2531
+ options.push({ category, label: "\u2605 Only starred", count, active: !!active.starred });
2532
+ continue;
2533
+ }
2534
+ if (category === "quality" || category === "relevance") {
2535
+ const isQuality = category === "quality";
2536
+ const activeValue = isQuality ? active.minQuality : active.minRelevance;
2537
+ const prefix = isQuality ? "Q \u2265 " : "R \u2265 ";
2538
+ options.push({ category, label: `${prefix}${activeValue ?? "?"}`, count: 0, active: activeValue !== void 0 });
2539
+ for (const value of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
2540
+ const count = entries.filter((entry) => {
2541
+ const metadata = parseMetadata(entry.metadata);
2542
+ return ((isQuality ? metadata.quality : metadata.relevance) ?? 0) >= value;
2543
+ }).length;
2544
+ options.push({ category, label: `${prefix}${value}`, count, active: activeValue === value });
2545
+ }
2546
+ continue;
2547
+ }
2548
+ for (const value of getDistinctValues(entries, category)) {
2549
+ const count = entries.filter((entry) => {
2550
+ const metadata = parseMetadata(entry.metadata);
2551
+ if (category === "project") return metadata.project === value;
2552
+ if (category === "language") return metadata.language === value;
2553
+ if (category === "role") return metadata.role === value;
2554
+ if (category === "tool") return entry.tool === value;
2555
+ if (category === "tag") return metadata.tags?.includes(value) ?? false;
2556
+ return false;
2557
+ }).length;
2558
+ const activeValue = active[category];
2559
+ options.push({ category, label: value, count, active: activeValue === value });
2560
+ }
1929
2561
  }
1930
- return sub;
2562
+ return options;
1931
2563
  }
1932
2564
 
2565
+ // src/ui/FilterPanel.tsx
2566
+ var FilterPanel = ({ allEntries, active, onUpdate, onClose, theme }) => {
2567
+ const options = useMemo2(() => buildFilterOptions(allEntries, active), [allEntries, active]);
2568
+ const [cursor, setCursor] = useState(0);
2569
+ const visibleCount = Math.min(options.length, 16);
2570
+ const scrollOffset = Math.max(0, Math.min(cursor - Math.floor(visibleCount / 2), options.length - visibleCount));
2571
+ const visible = options.slice(Math.max(0, scrollOffset), scrollOffset + visibleCount);
2572
+ useInput((char, key) => {
2573
+ if (key.escape) {
2574
+ onClose();
2575
+ return;
2576
+ }
2577
+ if (char === "c") {
2578
+ onUpdate({});
2579
+ return;
2580
+ }
2581
+ if (key.return || char === " ") {
2582
+ const option = options[cursor];
2583
+ if (!option) return;
2584
+ onUpdate(toggleFilter(active, option));
2585
+ return;
2586
+ }
2587
+ if (key.upArrow) setCursor((value) => Math.max(0, value - 1));
2588
+ if (key.downArrow) setCursor((value) => Math.min(options.length - 1, value + 1));
2589
+ if (key.pageUp) setCursor((value) => Math.max(0, value - visibleCount));
2590
+ if (key.pageDown) setCursor((value) => Math.min(options.length - 1, value + visibleCount));
2591
+ if (char && /^[a-z]$/.test(char)) {
2592
+ const index = options.findIndex((option, i) => i > cursor && option.category[0] === char);
2593
+ if (index !== -1) setCursor(index);
2594
+ }
2595
+ });
2596
+ const activeCount = Object.values(active).filter((value) => value !== void 0 && value !== false).length;
2597
+ return /* @__PURE__ */ React3.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React3.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text2, { color: theme.primary, bold: true }, "Filters "), activeCount > 0 ? /* @__PURE__ */ React3.createElement(Text2, { color: theme.warning }, activeCount, " active ") : /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, "none active "), activeCount > 0 && /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, "(c clear)")), /* @__PURE__ */ React3.createElement(Box, { borderStyle: "single", borderColor: theme.dim, flexDirection: "column", padding: 1, minHeight: 18 }, /* @__PURE__ */ React3.createElement(Box, { flexDirection: "column" }, visible.map((option, index) => {
2598
+ const absoluteIndex = Math.max(0, scrollOffset) + index;
2599
+ const isCurrent = absoluteIndex === cursor;
2600
+ const categoryColor = option.category === "project" ? "blue" : option.category === "language" ? "green" : option.category === "role" ? ROLE_COLOR2[option.label] || "cyan" : option.category === "tool" ? "yellow" : option.category === "tag" ? "cyan" : "white";
2601
+ return /* @__PURE__ */ React3.createElement(Box, { key: `${option.category}-${option.label}` }, /* @__PURE__ */ React3.createElement(Text2, { bold: isCurrent, color: isCurrent ? theme.primary : theme.dim }, isCurrent ? "\u276F " : " "), /* @__PURE__ */ React3.createElement(Text2, { color: option.active ? theme.warning : categoryColor, bold: option.active || isCurrent }, option.category, ":", option.label), /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, " [", option.count, "]"), option.active && /* @__PURE__ */ React3.createElement(Text2, { color: theme.success }, " \u2713"));
2602
+ })), options.length > visibleCount && /* @__PURE__ */ React3.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, " ", cursor + 1, "/", options.length, " \xB7 \u2191\u2193 navigate \xB7 Enter toggle \xB7 c clear \xB7 ESC close"))), /* @__PURE__ */ React3.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, "\u2191\u2193 navigate \xB7 Enter toggle \xB7 c clear all \xB7 ESC close \xB7 letter jumps to category")));
2603
+ };
2604
+
2605
+ // src/ui/themes.ts
2606
+ import { THEMES as THEMES3, THEME_NAMES } from "@gi4nks/ink";
2607
+
2608
+ // src/ui/SearchBar.tsx
2609
+ import React4 from "react";
2610
+ import { Box as Box2, Text as Text3 } from "ink";
2611
+ var SearchBar = ({ value, isActive, theme }) => {
2612
+ const isEmpty = value === "";
2613
+ const bgColor = isActive ? theme.primary : theme.dim;
2614
+ return /* @__PURE__ */ React4.createElement(Box2, { paddingX: 1, paddingY: 0 }, /* @__PURE__ */ React4.createElement(Text3, { color: bgColor, bold: isActive }, isActive ? "\u2315" : "\u2315"), /* @__PURE__ */ React4.createElement(Text3, { color: isActive ? "white" : theme.dim }, " ", isEmpty ? isActive ? "" : "search (/)" : value, isActive ? "\u2588" : ""));
2615
+ };
2616
+
1933
2617
  // src/ui/ListEntry.tsx
1934
- var ROLE_COLOR2 = {
2618
+ import React5 from "react";
2619
+ import { Box as Box3, Text as Text4 } from "ink";
2620
+ var ROLE_COLOR3 = {
1935
2621
  debug: "red",
1936
2622
  refactor: "yellow",
1937
2623
  explain: "blue",
@@ -1946,6 +2632,7 @@ function toolIndicator(tool) {
1946
2632
  if (tool === "claude") return { char: "\u25CF", color: "#f5a623" };
1947
2633
  if (tool === "gemini") return { char: "\u25CF", color: "#4fc3f7" };
1948
2634
  if (tool === "opencode") return { char: "\u25CF", color: "#e040fb" };
2635
+ if (tool === "codex") return { char: "\u25CF", color: "#74aa9c" };
1949
2636
  return { char: "\u25CB", color: "gray" };
1950
2637
  }
1951
2638
  function truncate(str, width) {
@@ -1966,32 +2653,195 @@ var ListEntry = ({ entry, isSelected, paneWidth, theme }) => {
1966
2653
  hour12: false
1967
2654
  });
1968
2655
  const role = meta.role ? truncate(meta.role, 6) : "";
1969
- const roleColor = meta.role ? ROLE_COLOR2[meta.role] || "white" : "white";
2656
+ const roleColor = meta.role ? ROLE_COLOR3[meta.role] || "white" : "white";
1970
2657
  const hasAnalysis = Boolean(meta.summary || meta.role || meta.tags && meta.tags.length > 0);
1971
- const roleBar = meta.role ? ROLE_COLOR2[meta.role] || theme.dim : theme.dim;
2658
+ const roleBar = meta.role ? ROLE_COLOR3[meta.role] || theme.dim : theme.dim;
1972
2659
  const rawTitle = meta.title || extractTopic(entry.prompt);
1973
2660
  const displayPrompt = truncate(rawTitle, paneWidth - 5);
1974
2661
  let summaryLine = "";
1975
2662
  if (meta.summary) {
1976
2663
  summaryLine = truncate(meta.summary.replace(/\n/g, " "), paneWidth - 5);
1977
2664
  }
1978
- return /* @__PURE__ */ React4.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Box2, { flexDirection: "row", width: paneWidth, overflow: "hidden" }, /* @__PURE__ */ React4.createElement(Text3, { color: roleBar }, "\u2502"), /* @__PURE__ */ React4.createElement(Text3, { color: indicator.color }, " ", indicator.char), /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, " ", entry.tool.padEnd(8)), meta.starred ? /* @__PURE__ */ React4.createElement(Text3, { color: theme.warning }, "\u2605") : /* @__PURE__ */ React4.createElement(Text3, null, " "), hasAnalysis ? /* @__PURE__ */ React4.createElement(Text3, { color: theme.success }, "\u25CF") : /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "\u25CB"), /* @__PURE__ */ React4.createElement(Text3, null, " "), /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, hhmm, " "), role && /* @__PURE__ */ React4.createElement(Text3, { color: roleColor }, role), meta.quality !== void 0 && /* @__PURE__ */ React4.createElement(Text3, { color: theme.success }, " Q", meta.quality), meta.relevance !== void 0 && /* @__PURE__ */ React4.createElement(Text3, { color: theme.warning }, " R", meta.relevance)), /* @__PURE__ */ React4.createElement(Box2, { flexDirection: "row" }, /* @__PURE__ */ React4.createElement(Text3, { color: roleBar }, "\u2502"), isSelected ? /* @__PURE__ */ React4.createElement(Text3, { inverse: true, bold: true }, " ", displayPrompt.padEnd(paneWidth - 3)) : /* @__PURE__ */ React4.createElement(Text3, null, " ", displayPrompt)), summaryLine && /* @__PURE__ */ React4.createElement(Box2, { flexDirection: "row" }, /* @__PURE__ */ React4.createElement(Text3, { color: roleBar }, "\u2502"), /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, " ", summaryLine)));
2665
+ return /* @__PURE__ */ React5.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Box3, { flexDirection: "row", width: paneWidth, overflow: "hidden" }, /* @__PURE__ */ React5.createElement(Text4, { color: roleBar }, "\u2502"), /* @__PURE__ */ React5.createElement(Text4, { color: indicator.color }, " ", indicator.char), /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, " ", entry.tool.padEnd(8)), meta.starred ? /* @__PURE__ */ React5.createElement(Text4, { color: theme.warning }, "\u2605") : /* @__PURE__ */ React5.createElement(Text4, null, " "), hasAnalysis ? /* @__PURE__ */ React5.createElement(Text4, { color: theme.success }, "\u25CF") : /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, "\u25CB"), /* @__PURE__ */ React5.createElement(Text4, null, " "), /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, hhmm, " "), role && /* @__PURE__ */ React5.createElement(Text4, { color: roleColor }, role), meta.quality !== void 0 && /* @__PURE__ */ React5.createElement(Text4, { color: theme.success }, " Q", meta.quality), meta.relevance !== void 0 && /* @__PURE__ */ React5.createElement(Text4, { color: theme.warning }, " R", meta.relevance)), /* @__PURE__ */ React5.createElement(Box3, { flexDirection: "row" }, /* @__PURE__ */ React5.createElement(Text4, { color: roleBar }, "\u2502"), isSelected ? /* @__PURE__ */ React5.createElement(Text4, { inverse: true, bold: true }, " ", displayPrompt.padEnd(paneWidth - 3)) : /* @__PURE__ */ React5.createElement(Text4, null, " ", displayPrompt)), summaryLine && /* @__PURE__ */ React5.createElement(Box3, { flexDirection: "row" }, /* @__PURE__ */ React5.createElement(Text4, { color: roleBar }, "\u2502"), /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, " ", summaryLine)));
1979
2666
  };
1980
2667
 
1981
2668
  // src/ui/PreviewPane.tsx
1982
- import React5, { useState, useEffect, useMemo as useMemo2 } from "react";
1983
- import { Box as Box3, Text as Text4, useInput } from "ink";
1984
- var ROLE_COLOR3 = {
1985
- debug: "red",
1986
- refactor: "yellow",
1987
- explain: "blue",
1988
- review: "magenta",
1989
- architect: "green",
1990
- test: "cyan",
1991
- docs: "white",
1992
- generate: "green",
1993
- research: "blue"
1994
- };
2669
+ import React6, { useState as useState2, useMemo as useMemo3 } from "react";
2670
+ import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
2671
+
2672
+ // src/utils/syntaxHighlight.ts
2673
+ var KEYWORDS = /* @__PURE__ */ new Set([
2674
+ "if",
2675
+ "else",
2676
+ "for",
2677
+ "while",
2678
+ "do",
2679
+ "switch",
2680
+ "case",
2681
+ "break",
2682
+ "continue",
2683
+ "return",
2684
+ "function",
2685
+ "class",
2686
+ "struct",
2687
+ "interface",
2688
+ "enum",
2689
+ "type",
2690
+ "const",
2691
+ "let",
2692
+ "var",
2693
+ "import",
2694
+ "export",
2695
+ "from",
2696
+ "default",
2697
+ "async",
2698
+ "await",
2699
+ "yield",
2700
+ "throw",
2701
+ "try",
2702
+ "catch",
2703
+ "finally",
2704
+ "new",
2705
+ "delete",
2706
+ "typeof",
2707
+ "instanceof",
2708
+ "void",
2709
+ "null",
2710
+ "undefined",
2711
+ "true",
2712
+ "false",
2713
+ "this",
2714
+ "super",
2715
+ "extends",
2716
+ "implements",
2717
+ "package",
2718
+ "namespace",
2719
+ "using",
2720
+ "def",
2721
+ "in",
2722
+ "not",
2723
+ "is",
2724
+ "and",
2725
+ "or",
2726
+ "with",
2727
+ "as",
2728
+ "pass",
2729
+ "raise",
2730
+ "lambda",
2731
+ "pub",
2732
+ "fn",
2733
+ "let",
2734
+ "mut",
2735
+ "impl",
2736
+ "trait",
2737
+ "use",
2738
+ "mod",
2739
+ "where",
2740
+ "ref",
2741
+ "func",
2742
+ "go",
2743
+ "defer",
2744
+ "select",
2745
+ "chan",
2746
+ "range",
2747
+ "map",
2748
+ "SELECT",
2749
+ "FROM",
2750
+ "WHERE",
2751
+ "INSERT",
2752
+ "UPDATE",
2753
+ "DELETE",
2754
+ "CREATE",
2755
+ "TABLE",
2756
+ "INTO",
2757
+ "VALUES",
2758
+ "SET",
2759
+ "JOIN",
2760
+ "LEFT",
2761
+ "RIGHT",
2762
+ "INNER",
2763
+ "OUTER",
2764
+ "ON",
2765
+ "AND",
2766
+ "OR",
2767
+ "NOT",
2768
+ "IN",
2769
+ "AS",
2770
+ "ORDER",
2771
+ "BY",
2772
+ "GROUP",
2773
+ "HAVING",
2774
+ "LIMIT",
2775
+ "OFFSET",
2776
+ "static",
2777
+ "public",
2778
+ "private",
2779
+ "protected",
2780
+ "readonly",
2781
+ "abstract",
2782
+ "virtual",
2783
+ "override",
2784
+ "final",
2785
+ "sealed",
2786
+ "internal",
2787
+ "extern",
2788
+ "module",
2789
+ "requires",
2790
+ "exports",
2791
+ "opens",
2792
+ "provides",
2793
+ "to",
2794
+ "transitive",
2795
+ "match",
2796
+ "enum",
2797
+ "macro",
2798
+ "unsafe"
2799
+ ]);
2800
+ function tokenizeLine(line) {
2801
+ const segments = [];
2802
+ let i = 0;
2803
+ while (i < line.length) {
2804
+ const rest = line.slice(i);
2805
+ const strMatch = rest.match(/^("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/);
2806
+ if (strMatch) {
2807
+ segments.push({ text: strMatch[1], color: "green" });
2808
+ i += strMatch[1].length;
2809
+ continue;
2810
+ }
2811
+ const commentMatch = rest.match(/^(\/\/.*)/);
2812
+ if (commentMatch) {
2813
+ segments.push({ text: commentMatch[1], dim: true });
2814
+ i += commentMatch[1].length;
2815
+ continue;
2816
+ }
2817
+ if (rest[0] === "#") {
2818
+ segments.push({ text: rest.slice(i), dim: true });
2819
+ break;
2820
+ }
2821
+ const numMatch = rest.match(/^(\d+(?:\.\d+)?)/);
2822
+ if (numMatch) {
2823
+ segments.push({ text: numMatch[1], color: "yellow" });
2824
+ i += numMatch[1].length;
2825
+ continue;
2826
+ }
2827
+ const wordMatch = rest.match(/^([a-zA-Z_][a-zA-Z0-9_]*)/);
2828
+ if (wordMatch) {
2829
+ const word = wordMatch[1];
2830
+ if (KEYWORDS.has(word)) {
2831
+ segments.push({ text: word, color: "blue" });
2832
+ } else if (/^[A-Z]/.test(word)) {
2833
+ segments.push({ text: word, color: "cyan" });
2834
+ } else {
2835
+ segments.push({ text: word });
2836
+ }
2837
+ i += word.length;
2838
+ continue;
2839
+ }
2840
+ segments.push({ text: rest[0] });
2841
+ i++;
2842
+ }
2843
+ return segments;
2844
+ }
1995
2845
  function wrapTextLines(text, width) {
1996
2846
  const lines = text.split("\n");
1997
2847
  const result = [];
@@ -2002,13 +2852,77 @@ function wrapTextLines(text, width) {
2002
2852
  }
2003
2853
  let current = line;
2004
2854
  while (current.length > width) {
2005
- result.push(current.substring(0, width));
2006
- current = current.substring(width);
2855
+ result.push(current.slice(0, width));
2856
+ current = current.slice(width);
2007
2857
  }
2008
2858
  if (current.length > 0) result.push(current);
2009
2859
  }
2010
2860
  return result;
2011
2861
  }
2862
+ function wrapRichLines(lines, width) {
2863
+ const result = [];
2864
+ for (const line of lines) {
2865
+ const totalLen = line.segments.reduce((sum, s) => sum + s.text.length, 0);
2866
+ if (totalLen <= width) {
2867
+ result.push(line);
2868
+ continue;
2869
+ }
2870
+ let currentSegs = [];
2871
+ let currentLen = 0;
2872
+ for (const seg of line.segments) {
2873
+ let remaining = seg.text;
2874
+ while (remaining.length > 0) {
2875
+ const space = width - currentLen;
2876
+ if (remaining.length <= space) {
2877
+ currentSegs.push({ text: remaining, color: seg.color, dim: seg.dim });
2878
+ currentLen += remaining.length;
2879
+ remaining = "";
2880
+ } else {
2881
+ currentSegs.push({ text: remaining.slice(0, space), color: seg.color, dim: seg.dim });
2882
+ result.push({ segments: currentSegs });
2883
+ currentSegs = [];
2884
+ currentLen = 0;
2885
+ remaining = remaining.slice(space);
2886
+ }
2887
+ }
2888
+ }
2889
+ if (currentLen > 0) {
2890
+ result.push({ segments: currentSegs });
2891
+ }
2892
+ }
2893
+ return result;
2894
+ }
2895
+ function buildRichLines(text, width) {
2896
+ const rawLines = text.split("\n");
2897
+ let inCode = false;
2898
+ const parsed = [];
2899
+ for (const rawLine of rawLines) {
2900
+ const trimmed = rawLine.trim();
2901
+ if (trimmed.startsWith("```")) {
2902
+ inCode = !inCode;
2903
+ continue;
2904
+ }
2905
+ if (inCode) {
2906
+ parsed.push({ segments: tokenizeLine(rawLine) });
2907
+ } else {
2908
+ parsed.push({ segments: [{ text: rawLine }] });
2909
+ }
2910
+ }
2911
+ return wrapRichLines(parsed, width);
2912
+ }
2913
+
2914
+ // src/ui/PreviewPane.tsx
2915
+ var ROLE_COLOR4 = {
2916
+ debug: "red",
2917
+ refactor: "yellow",
2918
+ explain: "blue",
2919
+ review: "magenta",
2920
+ architect: "green",
2921
+ test: "cyan",
2922
+ docs: "white",
2923
+ generate: "green",
2924
+ research: "blue"
2925
+ };
2012
2926
  function formatTimestamp(ts) {
2013
2927
  try {
2014
2928
  const d = new Date(ts);
@@ -2037,80 +2951,78 @@ var PreviewPane = ({
2037
2951
  theme,
2038
2952
  getProjectMemories
2039
2953
  }) => {
2040
- const [activeTab, setActiveTab] = useState("prompt");
2041
- const [scrollOffset, setScrollOffset] = useState(0);
2042
- useEffect(() => {
2043
- setActiveTab(entry?.response ? "response" : "prompt");
2044
- setScrollOffset(0);
2045
- }, [entry?.id]);
2046
- useEffect(() => {
2954
+ const [activeTab, setActiveTab] = useState2(
2955
+ entry?.response ? "response" : "prompt"
2956
+ );
2957
+ const [scrollOffset, setScrollOffset] = useState2(0);
2958
+ const switchTab = (tab) => {
2959
+ setActiveTab(tab);
2047
2960
  setScrollOffset(0);
2048
- }, [activeTab]);
2049
- const meta = useMemo2(() => entry ? parseMeta(entry.metadata) : {}, [entry]);
2961
+ };
2962
+ const meta = useMemo3(() => entry ? parseMeta(entry.metadata) : {}, [entry]);
2050
2963
  const contentWidth = Math.max(10, paneWidth - 4);
2051
2964
  const contentHeight = Math.max(3, paneHeight - 6);
2052
- const allLines = useMemo2(() => {
2965
+ const allLines = useMemo3(() => {
2053
2966
  if (!entry) return [];
2054
- if (activeTab === "prompt") return wrapTextLines(entry.prompt, contentWidth);
2055
- if (activeTab === "response") return wrapTextLines(entry.response || "(no response captured)", contentWidth);
2967
+ if (activeTab === "prompt") return buildRichLines(entry.prompt, contentWidth);
2968
+ if (activeTab === "response") return buildRichLines(entry.response || "(no response captured)", contentWidth);
2056
2969
  const lines = [];
2970
+ const push2 = (text) => lines.push({ segments: [{ text }] });
2057
2971
  if (meta.project && getProjectMemories) {
2058
2972
  const memories = getProjectMemories(meta.project);
2059
2973
  if (memories.length > 0) {
2060
- lines.push(`\u2500\u2500 Project Knowledge: ${meta.project} \u2500\u2500`);
2061
- lines.push("");
2974
+ push2(`\u2500\u2500 Project Knowledge: ${meta.project} \u2500\u2500`);
2975
+ push2("");
2062
2976
  for (const mem of memories) {
2063
2977
  if (mem.summary) {
2064
- lines.push(...wrapTextLines(mem.summary, contentWidth));
2065
- lines.push("");
2978
+ for (const l of wrapTextLines(mem.summary, contentWidth)) push2(l);
2979
+ push2("");
2066
2980
  }
2067
2981
  if (mem.key_insights.length > 0) {
2068
- lines.push("Key Insights:");
2069
- for (const i of mem.key_insights) lines.push(...wrapTextLines(` \u2022 ${i}`, contentWidth));
2070
- lines.push("");
2982
+ push2("Key Insights:");
2983
+ for (const i of mem.key_insights) for (const l of wrapTextLines(` \u2022 ${i}`, contentWidth)) push2(l);
2984
+ push2("");
2071
2985
  }
2072
2986
  if (mem.technical_decisions.length > 0) {
2073
- lines.push("Technical Decisions:");
2074
- for (const d of mem.technical_decisions) lines.push(...wrapTextLines(` \u2022 ${d}`, contentWidth));
2075
- lines.push("");
2987
+ push2("Technical Decisions:");
2988
+ for (const d of mem.technical_decisions) for (const l of wrapTextLines(` \u2022 ${d}`, contentWidth)) push2(l);
2989
+ push2("");
2076
2990
  }
2077
2991
  }
2078
- lines.push("\u2500\u2500 This Entry \u2500\u2500");
2079
- lines.push("");
2992
+ push2("\u2500\u2500 This Entry \u2500\u2500");
2993
+ push2("");
2080
2994
  }
2081
2995
  }
2082
2996
  if (meta.summary) {
2083
- lines.push(...wrapTextLines(meta.summary, contentWidth));
2084
- lines.push("");
2997
+ for (const l of wrapTextLines(meta.summary, contentWidth)) push2(l);
2998
+ push2("");
2085
2999
  }
2086
3000
  if (meta.key_insights && meta.key_insights.length > 0) {
2087
3001
  for (const insight of meta.key_insights) {
2088
- lines.push(...wrapTextLines(`\u2022 ${insight}`, contentWidth));
3002
+ for (const l of wrapTextLines(`\u2022 ${insight}`, contentWidth)) push2(l);
2089
3003
  }
2090
- lines.push("");
2091
- }
2092
- if (lines.length === 0) {
2093
- lines.push("(no AI analysis - run ph analyze)");
3004
+ push2("");
2094
3005
  }
3006
+ if (lines.length === 0) push2("(no AI analysis - run ph analyze)");
2095
3007
  return lines;
2096
3008
  }, [entry, activeTab, contentWidth, meta, getProjectMemories]);
2097
3009
  const maxScroll = Math.max(0, allLines.length - contentHeight);
2098
- useInput((_char, key) => {
3010
+ useInput2((_char, key) => {
2099
3011
  if (!isFocused) return;
2100
- if (_char === "1") setActiveTab("prompt");
2101
- if (_char === "2") setActiveTab("response");
2102
- if (_char === "3") setActiveTab("memory");
3012
+ if (_char === "1") switchTab("prompt");
3013
+ if (_char === "2") switchTab("response");
3014
+ if (_char === "3") switchTab("memory");
2103
3015
  if (key.upArrow) setScrollOffset((s) => Math.max(0, s - 1));
2104
3016
  if (key.downArrow) setScrollOffset((s) => Math.min(maxScroll, s + 1));
2105
3017
  if (key.pageUp) setScrollOffset((s) => Math.max(0, s - contentHeight));
2106
3018
  if (key.pageDown) setScrollOffset((s) => Math.min(maxScroll, s + contentHeight));
2107
3019
  });
2108
3020
  if (!entry) {
2109
- return /* @__PURE__ */ React5.createElement(Box3, { width: paneWidth, height: paneHeight, paddingX: 2, flexDirection: "column", justifyContent: "center" }, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, "(select an entry to preview)"));
3021
+ return /* @__PURE__ */ React6.createElement(Box4, { width: paneWidth, height: paneHeight, paddingX: 2, flexDirection: "column", justifyContent: "center" }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "(select an entry to preview)"));
2110
3022
  }
2111
3023
  const visibleLines = allLines.slice(scrollOffset, scrollOffset + contentHeight);
2112
3024
  const paddedLines = [...visibleLines];
2113
- while (paddedLines.length < contentHeight) paddedLines.push("");
3025
+ while (paddedLines.length < contentHeight) paddedLines.push({ segments: [{ text: " " }] });
2114
3026
  const topIndicator = scrollOffset > 0 ? ` \u2191 ${scrollOffset} above` : " ";
2115
3027
  const remaining = allLines.length - scrollOffset - contentHeight;
2116
3028
  const botIndicator = scrollOffset < maxScroll ? ` \u2193 ${remaining} more` : " ";
@@ -2120,8 +3032,8 @@ var PreviewPane = ({
2120
3032
  const tabPromptColor = isPromptActive && isFocused ? theme.primary : theme.dim;
2121
3033
  const tabResponseColor = isResponseActive && isFocused ? theme.primary : theme.dim;
2122
3034
  const tabMemoryColor = isMemoryActive && isFocused ? theme.primary : theme.dim;
2123
- return /* @__PURE__ */ React5.createElement(Box3, { flexDirection: "column", paddingX: 2 }, /* @__PURE__ */ React5.createElement(Box3, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.primary, bold: true }, "#", entry.id), /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, " \xB7 ", entry.tool, " \xB7 ", formatTimestamp(entry.timestamp)), meta.starred && /* @__PURE__ */ React5.createElement(Text4, { color: theme.warning }, " \u2605")), /* @__PURE__ */ React5.createElement(Box3, { marginBottom: 1 }, meta.project && /* @__PURE__ */ React5.createElement(Text4, { color: theme.accent }, "proj:", meta.project, " "), meta.language && /* @__PURE__ */ React5.createElement(Text4, { color: theme.dim }, "lang:", meta.language, " "), meta.role && /* @__PURE__ */ React5.createElement(Text4, { color: ROLE_COLOR3[meta.role] || theme.primary }, "role:", meta.role, " "), meta.quality !== void 0 && /* @__PURE__ */ React5.createElement(Text4, { color: theme.success }, "Q:", meta.quality, " "), meta.relevance !== void 0 && /* @__PURE__ */ React5.createElement(Text4, { color: theme.warning }, "R:", meta.relevance, " "), /* @__PURE__ */ React5.createElement(Text4, { color: entry.exit_code === 0 ? theme.dim : theme.error }, "exit:", entry.exit_code)), /* @__PURE__ */ React5.createElement(Box3, null, /* @__PURE__ */ React5.createElement(
2124
- Text4,
3035
+ return /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", paddingX: 2 }, /* @__PURE__ */ React6.createElement(Box4, null, /* @__PURE__ */ React6.createElement(Text5, { color: theme.primary, bold: true }, "#", entry.id), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " \xB7 ", entry.tool, " \xB7 ", formatTimestamp(entry.timestamp)), meta.starred && /* @__PURE__ */ React6.createElement(Text5, { color: theme.warning }, " \u2605")), /* @__PURE__ */ React6.createElement(Box4, { marginBottom: 1 }, meta.project && /* @__PURE__ */ React6.createElement(Text5, { color: theme.accent }, "proj:", meta.project, " "), meta.language && /* @__PURE__ */ React6.createElement(Text5, { color: theme.dim }, "lang:", meta.language, " "), meta.role && /* @__PURE__ */ React6.createElement(Text5, { color: ROLE_COLOR4[meta.role] || theme.primary }, "role:", meta.role, " "), meta.quality !== void 0 && /* @__PURE__ */ React6.createElement(Text5, { color: theme.success }, "Q:", meta.quality, " "), meta.relevance !== void 0 && /* @__PURE__ */ React6.createElement(Text5, { color: theme.warning }, "R:", meta.relevance, " "), /* @__PURE__ */ React6.createElement(Text5, { color: entry.exit_code === 0 ? theme.dim : theme.error }, "exit:", entry.exit_code)), /* @__PURE__ */ React6.createElement(Box4, null, /* @__PURE__ */ React6.createElement(
3036
+ Text5,
2125
3037
  {
2126
3038
  color: tabPromptColor,
2127
3039
  bold: isPromptActive && isFocused,
@@ -2129,8 +3041,8 @@ var PreviewPane = ({
2129
3041
  },
2130
3042
  isPromptActive ? "\u25CF " : "\u25CB ",
2131
3043
  "1:PROMPT"
2132
- ), /* @__PURE__ */ React5.createElement(Text4, null, " "), /* @__PURE__ */ React5.createElement(
2133
- Text4,
3044
+ ), /* @__PURE__ */ React6.createElement(Text5, null, " "), /* @__PURE__ */ React6.createElement(
3045
+ Text5,
2134
3046
  {
2135
3047
  color: tabResponseColor,
2136
3048
  bold: isResponseActive && isFocused,
@@ -2138,8 +3050,8 @@ var PreviewPane = ({
2138
3050
  },
2139
3051
  isResponseActive ? "\u25CF " : "\u25CB ",
2140
3052
  "2:RESPONSE"
2141
- ), /* @__PURE__ */ React5.createElement(Text4, null, " "), /* @__PURE__ */ React5.createElement(
2142
- Text4,
3053
+ ), /* @__PURE__ */ React6.createElement(Text5, null, " "), /* @__PURE__ */ React6.createElement(
3054
+ Text5,
2143
3055
  {
2144
3056
  color: tabMemoryColor,
2145
3057
  bold: isMemoryActive && isFocused,
@@ -2147,22 +3059,10 @@ var PreviewPane = ({
2147
3059
  },
2148
3060
  isMemoryActive ? "\u25CF " : "\u25CB ",
2149
3061
  "3:MEMORY"
2150
- )), /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, topIndicator), paddedLines.map((line, i) => /* @__PURE__ */ React5.createElement(Text4, { key: i }, line || " ")), /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, botIndicator));
3062
+ )), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, topIndicator), paddedLines.map((line, i) => /* @__PURE__ */ React6.createElement(Text5, { key: i }, line.segments.length > 0 ? line.segments.map((seg, j) => /* @__PURE__ */ React6.createElement(Text5, { key: j, color: seg.color, dimColor: seg.dim }, seg.text)) : " ")), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, botIndicator));
2151
3063
  };
2152
3064
 
2153
3065
  // src/ui/BrowseApp.tsx
2154
- var FILTER_CATEGORIES = ["project", "language", "role", "tool", "tag", "starred", "quality", "relevance"];
2155
- var ROLE_COLOR4 = {
2156
- debug: "red",
2157
- refactor: "yellow",
2158
- explain: "blue",
2159
- review: "magenta",
2160
- architect: "green",
2161
- test: "cyan",
2162
- docs: "white",
2163
- generate: "green",
2164
- research: "blue"
2165
- };
2166
3066
  function copyToClipboard(text) {
2167
3067
  try {
2168
3068
  const platform = process.platform;
@@ -2182,28 +3082,12 @@ function copyToClipboard(text) {
2182
3082
  } catch {
2183
3083
  }
2184
3084
  }
2185
- function wrapTextLines2(text, width) {
2186
- const result = [];
2187
- for (const line of text.split("\n")) {
2188
- let current = line;
2189
- if (current.length === 0) {
2190
- result.push("");
2191
- continue;
2192
- }
2193
- while (current.length > width) {
2194
- result.push(current.slice(0, width));
2195
- current = current.slice(width);
2196
- }
2197
- if (current.length > 0) result.push(current);
2198
- }
2199
- return result;
2200
- }
2201
3085
  function useStdoutDimensions() {
2202
- const [dimensions, setDimensions] = useState2({
3086
+ const [dimensions, setDimensions] = useState3({
2203
3087
  columns: process.stdout.columns || 120,
2204
3088
  rows: process.stdout.rows || 24
2205
3089
  });
2206
- useEffect2(() => {
3090
+ useEffect(() => {
2207
3091
  const handler = () => {
2208
3092
  setDimensions({
2209
3093
  columns: process.stdout.columns || 120,
@@ -2226,16 +3110,9 @@ function formatTimestamp2(ts) {
2226
3110
  return ts.slice(0, 16);
2227
3111
  }
2228
3112
  }
2229
- function parseMeta2(raw) {
2230
- try {
2231
- return JSON.parse(raw);
2232
- } catch {
2233
- return {};
2234
- }
2235
- }
2236
3113
  function hasProject(entry) {
2237
3114
  if (!entry) return false;
2238
- const meta = parseMeta2(entry.metadata);
3115
+ const meta = parseMetadata(entry.metadata);
2239
3116
  return Boolean(meta.project);
2240
3117
  }
2241
3118
  var SESSION_GAP_MS = 2 * 60 * 60 * 1e3;
@@ -2254,94 +3131,49 @@ function needsSessionSeparator(entries, idx) {
2254
3131
  const curr = new Date(entries[idx].timestamp).getTime();
2255
3132
  return prev - curr > SESSION_GAP_MS;
2256
3133
  }
2257
- function getDistinctValues(entries, category) {
2258
- const set = /* @__PURE__ */ new Set();
2259
- for (const e of entries) {
2260
- if (category === "tool") {
2261
- set.add(e.tool);
2262
- continue;
2263
- }
2264
- const meta = parseMeta2(e.metadata);
2265
- if (category === "project" && meta.project) set.add(meta.project);
2266
- if (category === "language" && meta.language) set.add(meta.language);
2267
- if (category === "role" && meta.role) set.add(meta.role);
2268
- if (category === "tag") meta.tags?.forEach((t) => set.add(t));
2269
- }
2270
- return [...set].sort();
2271
- }
2272
- function applyFilters(entries, active, textFilter) {
2273
- let result = entries;
2274
- const hasActiveFilter = Object.values(active).some((v) => v !== void 0 && v !== false);
2275
- if (hasActiveFilter) {
2276
- result = result.filter((e) => {
2277
- const meta = parseMeta2(e.metadata);
2278
- if (active.tool && e.tool !== active.tool) return false;
2279
- if (active.project && meta.project !== active.project) return false;
2280
- if (active.language && meta.language !== active.language) return false;
2281
- if (active.role && meta.role !== active.role) return false;
2282
- if (active.tag && !meta.tags?.includes(active.tag)) return false;
2283
- if (active.starred && !meta.starred) return false;
2284
- if (active.minQuality !== void 0 && (meta.quality ?? 0) < active.minQuality) return false;
2285
- if (active.minRelevance !== void 0 && (meta.relevance ?? 0) < active.minRelevance) return false;
2286
- return true;
2287
- });
2288
- }
2289
- if (textFilter) {
2290
- const lq = textFilter.toLowerCase();
2291
- result = result.filter((e) => {
2292
- const meta = parseMeta2(e.metadata);
2293
- return e.prompt.toLowerCase().includes(lq) || e.tool.toLowerCase().includes(lq) || (meta.project?.toLowerCase().includes(lq) ?? false) || (meta.role?.toLowerCase().includes(lq) ?? false) || (meta.tags?.some((t) => t.toLowerCase().includes(lq)) ?? false);
2294
- });
2295
- }
2296
- return result;
2297
- }
2298
3134
  var DetailView = ({ entry, onClose, onEdit, termWidth, termHeight, theme }) => {
2299
- const meta = parseMeta2(entry.metadata);
2300
- const [activeTab, setActiveTab] = useState2(entry.response ? "response" : "prompt");
2301
- const [scrollOffset, setScrollOffset] = useState2(0);
2302
- const [copied, setCopied] = useState2(false);
2303
- useEffect2(() => {
3135
+ const meta = parseMetadata(entry.metadata);
3136
+ const [activeTab, setActiveTab] = useState3(entry.response ? "response" : "prompt");
3137
+ const [scrollOffset, setScrollOffset] = useState3(0);
3138
+ const [copied, setCopied] = useState3(false);
3139
+ const switchTab = (tab) => {
3140
+ setActiveTab(tab);
2304
3141
  setScrollOffset(0);
2305
- }, [activeTab]);
3142
+ };
2306
3143
  const contentWidth = Math.max(10, termWidth - 4);
2307
3144
  const text = activeTab === "prompt" ? entry.prompt : entry.response || "(no response captured)";
2308
- const lines = useMemo3(() => {
3145
+ const lines = useMemo4(() => {
2309
3146
  if (activeTab === "prompt" || activeTab === "response") {
2310
- return wrapTextLines2(text, contentWidth);
3147
+ return buildRichLines(text, contentWidth);
2311
3148
  }
2312
3149
  const memLines = [];
3150
+ const push2 = (t) => memLines.push({ segments: [{ text: t }] });
2313
3151
  if (meta.summary) {
2314
- memLines.push("SUMMARY:");
2315
- memLines.push(...wrapTextLines2(meta.summary, contentWidth));
2316
- memLines.push("");
3152
+ push2("SUMMARY:");
3153
+ for (const l of wrapTextLines(meta.summary, contentWidth)) push2(l);
3154
+ push2("");
2317
3155
  }
2318
3156
  if (meta.key_insights && meta.key_insights.length > 0) {
2319
- memLines.push("KEY INSIGHTS:");
3157
+ push2("KEY INSIGHTS:");
2320
3158
  for (const insight of meta.key_insights) {
2321
- memLines.push(...wrapTextLines2(`\u2022 ${insight}`, contentWidth));
3159
+ for (const l of wrapTextLines(`\u2022 ${insight}`, contentWidth)) push2(l);
2322
3160
  }
2323
3161
  }
2324
- if (memLines.length === 0) {
2325
- memLines.push("(no AI analysis found - run ph analyze)");
2326
- }
3162
+ if (memLines.length === 0) push2("(no AI analysis found - run ph analyze)");
2327
3163
  return memLines;
2328
3164
  }, [activeTab, text, contentWidth, meta]);
2329
3165
  const contentHeight = Math.max(1, termHeight - 10);
2330
3166
  const maxScroll = Math.max(0, lines.length - contentHeight);
2331
- useInput2((char, key) => {
3167
+ useInput3((char, key) => {
2332
3168
  if (key.escape || key.return) onClose();
2333
3169
  else if (char === "e") onEdit();
2334
3170
  else if (key.tab || char === "1" || char === "2" || char === "3") {
2335
- if (char === "1") setActiveTab("prompt");
2336
- else if (char === "2") setActiveTab("response");
2337
- else if (char === "3") setActiveTab("memory");
2338
- else setActiveTab((t) => {
2339
- if (t === "prompt") return "response";
2340
- if (t === "response") return "memory";
2341
- return "prompt";
2342
- });
3171
+ if (char === "1") switchTab("prompt");
3172
+ else if (char === "2") switchTab("response");
3173
+ else if (char === "3") switchTab("memory");
3174
+ else switchTab(activeTab === "prompt" ? "response" : activeTab === "response" ? "memory" : "prompt");
2343
3175
  } else if (char === "y") {
2344
- const copyText = activeTab === "memory" ? lines.join("\n") : text;
3176
+ const copyText = activeTab === "memory" ? lines.map((l) => l.segments.map((s) => s.text).join("")).join("\n") : text;
2345
3177
  copyToClipboard(copyText);
2346
3178
  setCopied(true);
2347
3179
  setTimeout(() => setCopied(false), 2e3);
@@ -2352,15 +3184,15 @@ var DetailView = ({ entry, onClose, onEdit, termWidth, termHeight, theme }) => {
2352
3184
  });
2353
3185
  const visibleLines = lines.slice(scrollOffset, scrollOffset + contentHeight);
2354
3186
  const remaining = lines.length - (scrollOffset + contentHeight);
2355
- return /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", paddingX: 2, paddingTop: 1, flexGrow: 1 }, /* @__PURE__ */ React6.createElement(Box4, null, /* @__PURE__ */ React6.createElement(Text5, { color: theme.primary, bold: true }, "#", entry.id), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " \xB7 ", entry.tool, " \xB7 ", formatTimestamp2(entry.timestamp), " "), meta.starred && /* @__PURE__ */ React6.createElement(Text5, { color: theme.warning }, "\u2605")), /* @__PURE__ */ React6.createElement(Box4, null, meta.project && /* @__PURE__ */ React6.createElement(Text5, { color: theme.accent }, "proj:", meta.project, " "), meta.language && /* @__PURE__ */ React6.createElement(Text5, { color: theme.dim }, "lang:", meta.language, " "), meta.role && /* @__PURE__ */ React6.createElement(Text5, { color: ROLE_COLOR4[meta.role] ?? theme.primary }, "role:", meta.role, " "), meta.quality !== void 0 && /* @__PURE__ */ React6.createElement(Text5, { color: theme.success }, "Q:", meta.quality, " "), meta.relevance !== void 0 && /* @__PURE__ */ React6.createElement(Text5, { color: theme.warning }, "R:", meta.relevance, " "), /* @__PURE__ */ React6.createElement(Text5, { color: entry.exit_code === 0 ? theme.dim : theme.error }, "exit:", entry.exit_code)), /* @__PURE__ */ React6.createElement(Box4, { height: 1 }), /* @__PURE__ */ React6.createElement(Box4, { marginBottom: 0, justifyContent: "space-between" }, /* @__PURE__ */ React6.createElement(Box4, null, /* @__PURE__ */ React6.createElement(Text5, { color: theme.primary, bold: true, underline: activeTab === "prompt" }, activeTab === "prompt" ? "\u25CF" : "\u25CB", " 1:PROMPT"), /* @__PURE__ */ React6.createElement(Text5, null, " "), /* @__PURE__ */ React6.createElement(Text5, { color: theme.primary, bold: true, underline: activeTab === "response" }, activeTab === "response" ? "\u25CF" : "\u25CB", " 2:RESPONSE"), /* @__PURE__ */ React6.createElement(Text5, null, " "), /* @__PURE__ */ React6.createElement(Text5, { color: theme.primary, bold: true, underline: activeTab === "memory" }, activeTab === "memory" ? "\u25CF" : "\u25CB", " 3:MEMORY")), /* @__PURE__ */ React6.createElement(Box4, null, copied && /* @__PURE__ */ React6.createElement(Text5, { color: theme.success }, "Copied! "), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "[y:copy e:edit ESC:back]"))), /* @__PURE__ */ React6.createElement(Box4, { height: 1 }, scrollOffset > 0 && /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " \u2191 ", scrollOffset, " above")), /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", flexGrow: 1 }, visibleLines.map((line, i) => /* @__PURE__ */ React6.createElement(Text5, { key: i }, line || " ")), Array.from({ length: Math.max(0, contentHeight - visibleLines.length) }).map((_, i) => /* @__PURE__ */ React6.createElement(Text5, { key: `pad-${i}` }, " "))), /* @__PURE__ */ React6.createElement(Box4, { height: 1 }, remaining > 0 && /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " \u2193 ", remaining, " more")));
3187
+ return /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", paddingX: 2, paddingTop: 1, flexGrow: 1 }, /* @__PURE__ */ React7.createElement(Box5, null, /* @__PURE__ */ React7.createElement(Text6, { color: theme.primary, bold: true }, "#", entry.id), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, " \xB7 ", entry.tool, " \xB7 ", formatTimestamp2(entry.timestamp), " "), meta.starred && /* @__PURE__ */ React7.createElement(Text6, { color: theme.warning }, "\u2605")), /* @__PURE__ */ React7.createElement(Box5, null, meta.project && /* @__PURE__ */ React7.createElement(Text6, { color: theme.accent }, "proj:", meta.project, " "), meta.language && /* @__PURE__ */ React7.createElement(Text6, { color: theme.dim }, "lang:", meta.language, " "), meta.role && /* @__PURE__ */ React7.createElement(Text6, { color: ROLE_COLOR2[meta.role] ?? theme.primary }, "role:", meta.role, " "), meta.quality !== void 0 && /* @__PURE__ */ React7.createElement(Text6, { color: theme.success }, "Q:", meta.quality, " "), meta.relevance !== void 0 && /* @__PURE__ */ React7.createElement(Text6, { color: theme.warning }, "R:", meta.relevance, " "), /* @__PURE__ */ React7.createElement(Text6, { color: entry.exit_code === 0 ? theme.dim : theme.error }, "exit:", entry.exit_code)), /* @__PURE__ */ React7.createElement(Box5, { height: 1 }), /* @__PURE__ */ React7.createElement(Box5, { marginBottom: 0, justifyContent: "space-between" }, /* @__PURE__ */ React7.createElement(Box5, null, /* @__PURE__ */ React7.createElement(Text6, { color: theme.primary, bold: true, underline: activeTab === "prompt" }, activeTab === "prompt" ? "\u25CF" : "\u25CB", " 1:PROMPT"), /* @__PURE__ */ React7.createElement(Text6, null, " "), /* @__PURE__ */ React7.createElement(Text6, { color: theme.primary, bold: true, underline: activeTab === "response" }, activeTab === "response" ? "\u25CF" : "\u25CB", " 2:RESPONSE"), /* @__PURE__ */ React7.createElement(Text6, null, " "), /* @__PURE__ */ React7.createElement(Text6, { color: theme.primary, bold: true, underline: activeTab === "memory" }, activeTab === "memory" ? "\u25CF" : "\u25CB", " 3:MEMORY")), /* @__PURE__ */ React7.createElement(Box5, null, copied && /* @__PURE__ */ React7.createElement(Text6, { color: theme.success }, "Copied! "), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "[y:copy e:edit ESC:back]"))), /* @__PURE__ */ React7.createElement(Box5, { height: 1 }, scrollOffset > 0 && /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, " \u2191 ", scrollOffset, " above")), /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", flexGrow: 1 }, visibleLines.map((line, i) => /* @__PURE__ */ React7.createElement(Text6, { key: i }, line.segments.length > 0 ? line.segments.map((seg, j) => /* @__PURE__ */ React7.createElement(Text6, { key: j, color: seg.color, dimColor: seg.dim }, seg.text)) : " ")), Array.from({ length: Math.max(0, contentHeight - visibleLines.length) }).map((_, i) => /* @__PURE__ */ React7.createElement(Text6, { key: `pad-${i}` }, " "))), /* @__PURE__ */ React7.createElement(Box5, { height: 1 }, remaining > 0 && /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, " \u2193 ", remaining, " more")));
2356
3188
  };
2357
3189
  var EditView = ({ entry, onSave, onClose, theme }) => {
2358
- const initMeta = parseMeta2(entry.metadata);
2359
- const [field, setField] = useState2("title");
2360
- const [titleValue, setTitleValue] = useState2(initMeta.title ?? extractTopic(entry.prompt));
2361
- const [roleValue, setRoleValue] = useState2(initMeta.role ?? "");
2362
- const [tagsValue, setTagsValue] = useState2(initMeta.tags?.join(", ") ?? "");
2363
- useInput2((char, key) => {
3190
+ const initMeta = parseMetadata(entry.metadata);
3191
+ const [field, setField] = useState3("title");
3192
+ const [titleValue, setTitleValue] = useState3(initMeta.title ?? extractTopic(entry.prompt));
3193
+ const [roleValue, setRoleValue] = useState3(initMeta.role ?? "");
3194
+ const [tagsValue, setTagsValue] = useState3(initMeta.tags?.join(", ") ?? "");
3195
+ useInput3((char, key) => {
2364
3196
  if (key.escape) {
2365
3197
  onClose();
2366
3198
  return;
@@ -2392,150 +3224,56 @@ var EditView = ({ entry, onSave, onClose, theme }) => {
2392
3224
  }
2393
3225
  });
2394
3226
  const ROLES = "debug \xB7 refactor \xB7 explain \xB7 review \xB7 architect \xB7 test \xB7 docs \xB7 generate \xB7 research";
2395
- return /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React6.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: theme.primary, bold: true }, "Edit Metadata \u2014 Prompt #", entry.id)), /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", borderStyle: "single", borderColor: theme.dim, padding: 1 }, /* @__PURE__ */ React6.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Title: "), /* @__PURE__ */ React6.createElement(Text5, { color: field === "title" ? theme.primary : "white", bold: field === "title" }, titleValue || "(none)", field === "title" ? "\u2588" : "")), /* @__PURE__ */ React6.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Role: "), /* @__PURE__ */ React6.createElement(Text5, { color: field === "role" ? theme.primary : "white", bold: field === "role" }, roleValue || "(none)", field === "role" ? "\u2588" : "")), /* @__PURE__ */ React6.createElement(Box4, null, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Tags: "), /* @__PURE__ */ React6.createElement(Text5, { color: field === "tags" ? theme.primary : "white", bold: field === "tags" }, tagsValue || "(none)", field === "tags" ? "\u2588" : ""))), /* @__PURE__ */ React6.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Roles: ", ROLES)), /* @__PURE__ */ React6.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Tab switch field \xB7 Enter save \xB7 ESC cancel")));
3227
+ return /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React7.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: theme.primary, bold: true }, "Edit Metadata \u2014 Prompt #", entry.id)), /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", borderStyle: "single", borderColor: theme.dim, padding: 1 }, /* @__PURE__ */ React7.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Title: "), /* @__PURE__ */ React7.createElement(Text6, { color: field === "title" ? theme.primary : "white", bold: field === "title" }, titleValue || "(none)", field === "title" ? "\u2588" : "")), /* @__PURE__ */ React7.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Role: "), /* @__PURE__ */ React7.createElement(Text6, { color: field === "role" ? theme.primary : "white", bold: field === "role" }, roleValue || "(none)", field === "role" ? "\u2588" : "")), /* @__PURE__ */ React7.createElement(Box5, null, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Tags: "), /* @__PURE__ */ React7.createElement(Text6, { color: field === "tags" ? theme.primary : "white", bold: field === "tags" }, tagsValue || "(none)", field === "tags" ? "\u2588" : ""))), /* @__PURE__ */ React7.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Roles: ", ROLES)), /* @__PURE__ */ React7.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Tab switch field \xB7 Enter save \xB7 ESC cancel")));
2396
3228
  };
2397
3229
  var RerunView = ({ entry, onConfirm, onClose, theme }) => {
2398
- const [field, setField] = useState2("prompt");
2399
- const [toolValue, setToolValue] = useState2(entry.tool);
2400
- const [promptValue, setPromptValue] = useState2(entry.prompt);
2401
- const meta = parseMeta2(entry.metadata);
3230
+ const [field, setField] = useState3("prompt");
3231
+ const [toolValue, setToolValue] = useState3(entry.tool);
3232
+ const [promptValue, setPromptValue] = useState3(entry.prompt);
3233
+ const meta = parseMetadata(entry.metadata);
2402
3234
  const git = meta.git_context;
2403
- useInput2((char, key) => {
2404
- if (key.escape) {
2405
- onClose();
2406
- return;
2407
- }
2408
- if (key.return) {
2409
- onConfirm(toolValue.trim(), promptValue.trim());
2410
- return;
2411
- }
2412
- if (key.tab) {
2413
- setField((f) => f === "tool" ? "prompt" : "tool");
2414
- return;
2415
- }
2416
- const setter = field === "tool" ? setToolValue : setPromptValue;
2417
- if (key.backspace || key.delete) {
2418
- setter((v) => v.slice(0, -1));
2419
- } else if (char && !key.ctrl && !key.meta) {
2420
- setter((v) => v + char);
2421
- }
2422
- });
2423
- return /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React6.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: theme.primary, bold: true }, "Rerun Prompt #", entry.id)), /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Tool: ", /* @__PURE__ */ React6.createElement(Text5, { color: theme.warning }, entry.tool)), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Date: ", /* @__PURE__ */ React6.createElement(Text5, null, formatTimestamp2(entry.timestamp))), meta.project && /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Project: ", /* @__PURE__ */ React6.createElement(Text5, { color: theme.accent }, meta.project)), git && /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Git: ", /* @__PURE__ */ React6.createElement(Text5, { color: theme.warning }, "Captured on branch: ", git.branch, ", ", git.files?.length || 0, " files modified"))), /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", borderStyle: "single", borderColor: theme.dim, padding: 1 }, /* @__PURE__ */ React6.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Tool: "), /* @__PURE__ */ React6.createElement(Text5, { color: field === "tool" ? theme.primary : "white", bold: field === "tool" }, toolValue, field === "tool" ? "\u2588" : "")), /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Prompt: "), /* @__PURE__ */ React6.createElement(Box4, { borderStyle: "round", borderColor: field === "prompt" ? theme.primary : theme.dim, paddingX: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: field === "prompt" ? theme.primary : "white" }, promptValue, field === "prompt" ? "\u2588" : "")))), /* @__PURE__ */ React6.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Tab switch \xB7 Enter run \xB7 ESC cancel")));
2424
- };
2425
- var FilterPanel = ({ allEntries, active, onUpdate, onClose, theme }) => {
2426
- const options = useMemo3(() => {
2427
- const opts = [];
2428
- for (const cat of FILTER_CATEGORIES) {
2429
- if (cat === "starred") {
2430
- const count = allEntries.filter((e) => {
2431
- try {
2432
- return JSON.parse(e.metadata).starred;
2433
- } catch {
2434
- return false;
2435
- }
2436
- }).length;
2437
- opts.push({ category: "starred", label: "\u2605 Only starred", count, active: !!active.starred });
2438
- } else if (cat === "quality") {
2439
- const qActive = active.minQuality !== void 0;
2440
- opts.push({ category: "quality", label: `Q \u2265 ${active.minQuality ?? "?"}`, count: 0, active: qActive });
2441
- for (const v of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
2442
- const c = allEntries.filter((e) => {
2443
- try {
2444
- return (JSON.parse(e.metadata).quality ?? 0) >= v;
2445
- } catch {
2446
- return false;
2447
- }
2448
- }).length;
2449
- opts.push({ category: "quality", label: `Q \u2265 ${v}`, count: c, active: active.minQuality === v });
2450
- }
2451
- } else if (cat === "relevance") {
2452
- const rActive = active.minRelevance !== void 0;
2453
- opts.push({ category: "relevance", label: `R \u2265 ${active.minRelevance ?? "?"}`, count: 0, active: rActive });
2454
- for (const v of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
2455
- const c = allEntries.filter((e) => {
2456
- try {
2457
- return (JSON.parse(e.metadata).relevance ?? 0) >= v;
2458
- } catch {
2459
- return false;
2460
- }
2461
- }).length;
2462
- opts.push({ category: "relevance", label: `R \u2265 ${v}`, count: c, active: active.minRelevance === v });
2463
- }
2464
- } else {
2465
- const vals = getDistinctValues(allEntries, cat);
2466
- for (const v of vals) {
2467
- const c = allEntries.filter((e) => {
2468
- try {
2469
- const m = JSON.parse(e.metadata);
2470
- if (cat === "project") return m.project === v;
2471
- if (cat === "language") return m.language === v;
2472
- if (cat === "role") return m.role === v;
2473
- if (cat === "tool") return e.tool === v;
2474
- if (cat === "tag") return m.tags?.includes(v);
2475
- return false;
2476
- } catch {
2477
- return false;
2478
- }
2479
- }).length;
2480
- const currentVal = active[cat];
2481
- opts.push({ category: cat, label: v, count: c, active: currentVal === v });
2482
- }
2483
- }
2484
- }
2485
- return opts;
2486
- }, [allEntries, active]);
2487
- const [cursor, setCursor] = useState2(0);
2488
- const visibleCount = Math.min(options.length, 16);
2489
- const scrollOffset = Math.max(0, Math.min(cursor - Math.floor(visibleCount / 2), options.length - visibleCount));
2490
- const visible = options.slice(Math.max(0, scrollOffset), scrollOffset + visibleCount);
2491
- useInput2((char, key) => {
3235
+ useInput3((char, key) => {
2492
3236
  if (key.escape) {
2493
3237
  onClose();
2494
3238
  return;
2495
3239
  }
2496
- if (char === "c") {
2497
- onUpdate({});
3240
+ if (key.return) {
3241
+ onConfirm(toolValue.trim(), promptValue.trim());
2498
3242
  return;
2499
3243
  }
2500
- if (key.return || char === " ") {
2501
- const opt = options[cursor];
2502
- if (!opt) return;
2503
- if (opt.category === "starred") {
2504
- onUpdate({ ...active, starred: !opt.active || void 0 });
2505
- } else if (opt.category === "quality") {
2506
- const v = parseInt(opt.label.replace("Q \u2265 ", ""), 10);
2507
- onUpdate({ ...active, minQuality: opt.active ? void 0 : v });
2508
- } else if (opt.category === "relevance") {
2509
- const v = parseInt(opt.label.replace("R \u2265 ", ""), 10);
2510
- onUpdate({ ...active, minRelevance: opt.active ? void 0 : v });
2511
- } else {
2512
- const key2 = opt.category;
2513
- if (opt.active) {
2514
- const updated = { ...active };
2515
- delete updated[key2];
2516
- onUpdate(updated);
2517
- } else {
2518
- onUpdate({ ...active, [key2]: opt.label });
2519
- }
2520
- }
3244
+ if (key.tab) {
3245
+ setField((f) => f === "tool" ? "prompt" : "tool");
2521
3246
  return;
2522
3247
  }
2523
- if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
2524
- if (key.downArrow) setCursor((c) => Math.min(options.length - 1, c + 1));
2525
- if (key.pageUp) setCursor((c) => Math.max(0, c - visibleCount));
2526
- if (key.pageDown) setCursor((c) => Math.min(options.length - 1, c + visibleCount));
2527
- if (char && /^[a-z]$/.test(char)) {
2528
- const idx = options.findIndex((o, i) => i > cursor && o.category[0] === char);
2529
- if (idx !== -1) setCursor(idx);
3248
+ const setter = field === "tool" ? setToolValue : setPromptValue;
3249
+ if (key.backspace || key.delete) {
3250
+ setter((v) => v.slice(0, -1));
3251
+ } else if (char && !key.ctrl && !key.meta) {
3252
+ setter((v) => v + char);
3253
+ }
3254
+ });
3255
+ return /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React7.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: theme.primary, bold: true }, "Rerun Prompt #", entry.id)), /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Tool: ", /* @__PURE__ */ React7.createElement(Text6, { color: theme.warning }, entry.tool)), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Date: ", /* @__PURE__ */ React7.createElement(Text6, null, formatTimestamp2(entry.timestamp))), meta.project && /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Project: ", /* @__PURE__ */ React7.createElement(Text6, { color: theme.accent }, meta.project)), git && /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Git: ", /* @__PURE__ */ React7.createElement(Text6, { color: theme.warning }, "Captured on branch: ", git.branch, ", ", git.files?.length || 0, " files modified"))), /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", borderStyle: "single", borderColor: theme.dim, padding: 1 }, /* @__PURE__ */ React7.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Tool: "), /* @__PURE__ */ React7.createElement(Text6, { color: field === "tool" ? theme.primary : "white", bold: field === "tool" }, toolValue, field === "tool" ? "\u2588" : "")), /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Prompt: "), /* @__PURE__ */ React7.createElement(Box5, { borderStyle: "round", borderColor: field === "prompt" ? theme.primary : theme.dim, paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: field === "prompt" ? theme.primary : "white" }, promptValue, field === "prompt" ? "\u2588" : "")))), /* @__PURE__ */ React7.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Tab switch \xB7 Enter run \xB7 ESC cancel")));
3256
+ };
3257
+ var KNOWN_PROVIDERS = [
3258
+ { key: "1", label: "claude", tool: "claude" },
3259
+ { key: "2", label: "gemini", tool: "gemini" },
3260
+ { key: "3", label: "opencode", tool: "opencode" },
3261
+ { key: "4", label: "codex", tool: "codex" },
3262
+ { key: "5", label: "ollama", tool: "ollama" },
3263
+ { key: "6", label: "chatgpt", tool: "chatgpt" }
3264
+ ];
3265
+ var ProviderPicker = ({ entry, onPick, onClose, theme }) => {
3266
+ useInput3((char) => {
3267
+ if (char === "q" || char === "\x1B") {
3268
+ onClose();
3269
+ return;
3270
+ }
3271
+ const provider = KNOWN_PROVIDERS.find((p) => p.key === char);
3272
+ if (provider) {
3273
+ onPick(provider.tool, entry.prompt);
2530
3274
  }
2531
3275
  });
2532
- const activeCount = Object.values(active).filter((v) => v !== void 0 && v !== false).length;
2533
- return /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React6.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: theme.primary, bold: true }, "Filters "), activeCount > 0 ? /* @__PURE__ */ React6.createElement(Text5, { color: theme.warning }, activeCount, " active ") : /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "none active "), activeCount > 0 && /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "(c clear)")), /* @__PURE__ */ React6.createElement(Box4, { borderStyle: "single", borderColor: theme.dim, flexDirection: "column", padding: 1, minHeight: 18 }, /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column" }, visible.map((opt, i) => {
2534
- const absIdx = Math.max(0, scrollOffset) + i;
2535
- const isCur = absIdx === cursor;
2536
- const catColor = opt.category === "project" ? "blue" : opt.category === "language" ? "green" : opt.category === "role" ? ROLE_COLOR4[opt.label] || "cyan" : opt.category === "tool" ? "yellow" : opt.category === "tag" ? "cyan" : "white";
2537
- return /* @__PURE__ */ React6.createElement(Box4, { key: `${opt.category}-${opt.label}` }, /* @__PURE__ */ React6.createElement(Text5, { bold: isCur, color: isCur ? theme.primary : theme.dim }, isCur ? "\u276F " : " "), /* @__PURE__ */ React6.createElement(Text5, { color: opt.active ? theme.warning : catColor, bold: opt.active || isCur }, opt.category, ":", opt.label), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " [", opt.count, "]"), opt.active && /* @__PURE__ */ React6.createElement(Text5, { color: theme.success }, " \u2713"));
2538
- })), options.length > visibleCount && /* @__PURE__ */ React6.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " ", cursor + 1, "/", options.length, " \xB7 \u2191\u2193 navigate \xB7 Enter toggle \xB7 c clear \xB7 ESC close"))), /* @__PURE__ */ React6.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "\u2191\u2193 navigate \xB7 Enter toggle \xB7 c clear all \xB7 ESC close \xB7 letter jumps to category")));
3276
+ return /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", padding: 2 }, /* @__PURE__ */ React7.createElement(Text6, { bold: true, color: theme.primary }, "Send prompt #", entry.id, " to:"), /* @__PURE__ */ React7.createElement(Box5, { marginTop: 1, flexDirection: "column" }, KNOWN_PROVIDERS.map((p) => /* @__PURE__ */ React7.createElement(Text6, { key: p.key }, /* @__PURE__ */ React7.createElement(Text6, { color: theme.warning }, p.key), /* @__PURE__ */ React7.createElement(Text6, null, ": ", p.label)))), /* @__PURE__ */ React7.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Pick a number \xB7 q to cancel")));
2539
3277
  };
2540
3278
  var SETTING_FIELDS = [
2541
3279
  { key: "backgroundAnalysis", label: "Auto-analysis on capture", type: "boolean" },
@@ -2547,10 +3285,10 @@ var SETTING_FIELDS = [
2547
3285
  { key: "filterMinRelevance", label: "Min relevance filter", type: "number" }
2548
3286
  ];
2549
3287
  var SettingsView = ({ onClose, theme }) => {
2550
- const [cfg, setCfg] = useState2(() => load());
2551
- const [cursor, setCursor] = useState2(0);
2552
- const [dirty, setDirty] = useState2(false);
2553
- const [saved, setSaved] = useState2(false);
3288
+ const [cfg, setCfg] = useState3(() => load());
3289
+ const [cursor, setCursor] = useState3(0);
3290
+ const [dirty, setDirty] = useState3(false);
3291
+ const [saved, setSaved] = useState3(false);
2554
3292
  const toggleField = (key) => {
2555
3293
  const updated = { ...cfg };
2556
3294
  if (key === "backgroundAnalysis") {
@@ -2566,7 +3304,7 @@ var SettingsView = ({ onClose, theme }) => {
2566
3304
  setSaved(true);
2567
3305
  setTimeout(() => setSaved(false), 2e3);
2568
3306
  };
2569
- useInput2((char, key) => {
3307
+ useInput3((char, key) => {
2570
3308
  if (key.escape) {
2571
3309
  onClose();
2572
3310
  return;
@@ -2589,7 +3327,7 @@ var SettingsView = ({ onClose, theme }) => {
2589
3327
  }
2590
3328
  }
2591
3329
  });
2592
- return /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React6.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: theme.primary, bold: true }, "Settings "), dirty && /* @__PURE__ */ React6.createElement(Text5, { color: theme.warning }, "modified "), saved && /* @__PURE__ */ React6.createElement(Text5, { color: theme.success }, "\u2713 saved")), /* @__PURE__ */ React6.createElement(Box4, { borderStyle: "single", borderColor: theme.dim, flexDirection: "column", padding: 1 }, SETTING_FIELDS.map((field, i) => {
3330
+ return /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React7.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: theme.primary, bold: true }, "Settings "), dirty && /* @__PURE__ */ React7.createElement(Text6, { color: theme.warning }, "modified "), saved && /* @__PURE__ */ React7.createElement(Text6, { color: theme.success }, "\u2713 saved")), /* @__PURE__ */ React7.createElement(Box5, { borderStyle: "single", borderColor: theme.dim, flexDirection: "column", padding: 1 }, SETTING_FIELDS.map((field, i) => {
2593
3331
  const isCur = i === cursor;
2594
3332
  const val = cfg[field.key];
2595
3333
  let display;
@@ -2599,25 +3337,27 @@ var SettingsView = ({ onClose, theme }) => {
2599
3337
  display = val !== void 0 ? String(val) : "(default)";
2600
3338
  }
2601
3339
  const valColor = field.type === "boolean" ? val ? theme.success : theme.dim : val !== void 0 ? "white" : theme.dim;
2602
- return /* @__PURE__ */ React6.createElement(Box4, { key: field.key }, /* @__PURE__ */ React6.createElement(Text5, { bold: isCur, color: isCur ? theme.primary : theme.dim }, isCur ? "\u276F " : " "), /* @__PURE__ */ React6.createElement(Text5, { color: isCur ? theme.primary : "white" }, field.label), /* @__PURE__ */ React6.createElement(Text5, null, " ".repeat(Math.max(1, 30 - field.label.length))), /* @__PURE__ */ React6.createElement(Text5, { color: valColor, bold: field.type === "boolean" && isCur }, display), field.type === "boolean" && isCur && /* @__PURE__ */ React6.createElement(Text5, { color: theme.warning }, " [Enter toggle]"));
2603
- })), /* @__PURE__ */ React6.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "\u2191\u2193 navigate \xB7 Enter toggle boolean \xB7 ", dirty ? "s save \xB7 " : "", "q/ESC close")));
3340
+ return /* @__PURE__ */ React7.createElement(Box5, { key: field.key }, /* @__PURE__ */ React7.createElement(Text6, { bold: isCur, color: isCur ? theme.primary : theme.dim }, isCur ? "\u276F " : " "), /* @__PURE__ */ React7.createElement(Text6, { color: isCur ? theme.primary : "white" }, field.label), /* @__PURE__ */ React7.createElement(Text6, null, " ".repeat(Math.max(1, 30 - field.label.length))), /* @__PURE__ */ React7.createElement(Text6, { color: valColor, bold: field.type === "boolean" && isCur }, display), field.type === "boolean" && isCur && /* @__PURE__ */ React7.createElement(Text6, { color: theme.warning }, " [Enter toggle]"));
3341
+ })), /* @__PURE__ */ React7.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "\u2191\u2193 navigate \xB7 Enter toggle boolean \xB7 ", dirty ? "s save \xB7 " : "", "q/ESC close")));
2604
3342
  };
2605
3343
  var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2606
3344
  const { exit } = useApp();
2607
- const [allEntries, setAllEntries] = useState2(() => db.search({ limit: 1e3 }));
2608
- const [refreshKey, setRefreshKey] = useState2(0);
2609
- const [textFilter, setTextFilter] = useState2(initialTextFilter ?? "");
2610
- const [isTextFiltering, setTextFiltering] = useState2(false);
2611
- const [activeFilters, setActiveFilters] = useState2(initialFilters ?? {});
2612
- const [showFilterPanel, setFilterPanel] = useState2(false);
2613
- const [showSettings, setShowSettings] = useState2(false);
2614
- const [cursor, setCursor] = useState2(0);
2615
- const [detail, setDetail] = useState2(null);
2616
- const [editing, setEditing] = useState2(null);
2617
- const [rerunning, setRerunning] = useState2(null);
2618
- const [copiedId, setCopiedId] = useState2(null);
2619
- const [focusPane, setFocusPane] = useState2("list");
2620
- const [scrollOffset, setScrollOffset] = useState2(0);
3345
+ const [allEntries, setAllEntries] = useState3(() => db.search({ limit: 1e3 }));
3346
+ const [refreshKey, setRefreshKey] = useState3(0);
3347
+ const [archiveCount] = useState3(() => db.getArchiveStats().total);
3348
+ const [textFilter, setTextFilter] = useState3(initialTextFilter ?? "");
3349
+ const [isTextFiltering, setTextFiltering] = useState3(false);
3350
+ const [activeFilters, setActiveFilters] = useState3(initialFilters ?? {});
3351
+ const [showFilterPanel, setFilterPanel] = useState3(false);
3352
+ const [showSettings, setShowSettings] = useState3(false);
3353
+ const [cursor, setCursor] = useState3(0);
3354
+ const [detail, setDetail] = useState3(null);
3355
+ const [editing, setEditing] = useState3(null);
3356
+ const [rerunning, setRerunning] = useState3(null);
3357
+ const [providerPicking, setProviderPicking] = useState3(null);
3358
+ const [copiedId, setCopiedId] = useState3(null);
3359
+ const [focusPane, setFocusPane] = useState3("list");
3360
+ const [scrollOffset, setScrollOffset] = useState3(0);
2621
3361
  const currentThemeName = "dark";
2622
3362
  const theme = THEMES3[currentThemeName] || THEMES3.dark;
2623
3363
  const { columns: termWidth, rows: termHeight } = useStdoutDimensions();
@@ -2625,15 +3365,15 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2625
3365
  const isWide = termWidth >= SPLIT_THRESHOLD;
2626
3366
  const leftPaneWidth = isWide ? Math.max(32, Math.min(50, Math.floor(termWidth * 0.33))) : termWidth;
2627
3367
  const PAGE_SIZE = Math.max(1, Math.floor((termHeight - 3) / 4));
2628
- const entries = useMemo3(
3368
+ const entries = useMemo4(
2629
3369
  () => applyFilters(allEntries, activeFilters, textFilter),
2630
3370
  // eslint-disable-next-line react-hooks/exhaustive-deps
2631
3371
  [allEntries, activeFilters, textFilter, refreshKey]
2632
3372
  );
2633
- useEffect2(() => {
3373
+ useEffect(() => {
2634
3374
  setCursor((c) => Math.min(c, Math.max(0, entries.length - 1)));
2635
3375
  }, [entries.length]);
2636
- useEffect2(() => {
3376
+ useEffect(() => {
2637
3377
  setScrollOffset((prev) => {
2638
3378
  const clamped = Math.max(0, Math.min(prev, Math.max(0, entries.length - PAGE_SIZE)));
2639
3379
  if (cursor < clamped) return cursor;
@@ -2645,7 +3385,7 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2645
3385
  const visible = entries.slice(pageStart, pageStart + PAGE_SIZE);
2646
3386
  const triggerRefresh = useCallback(() => setRefreshKey((k) => k + 1), []);
2647
3387
  const toggleStar = useCallback((entry) => {
2648
- const meta = parseMeta2(entry.metadata);
3388
+ const meta = parseMetadata(entry.metadata);
2649
3389
  meta.starred = !meta.starred;
2650
3390
  const newMeta = JSON.stringify(meta);
2651
3391
  db.updateMetadata(entry.id, newMeta);
@@ -2668,8 +3408,8 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2668
3408
  setCursor(0);
2669
3409
  setScrollOffset(0);
2670
3410
  }, []);
2671
- useInput2((char, key) => {
2672
- if (detail || editing || showFilterPanel || showSettings || rerunning) return;
3411
+ useInput3((char, key) => {
3412
+ if (detail || editing || showFilterPanel || showSettings || rerunning || providerPicking) return;
2673
3413
  if (isTextFiltering) {
2674
3414
  if (key.escape) {
2675
3415
  setTextFiltering(false);
@@ -2700,6 +3440,8 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2700
3440
  if (entries[cursor]) setEditing(entries[cursor]);
2701
3441
  } else if (char === "r") {
2702
3442
  if (entries[cursor]) setRerunning(entries[cursor]);
3443
+ } else if (char === "p") {
3444
+ if (entries[cursor]) setProviderPicking(entries[cursor]);
2703
3445
  } else if (char === "x") {
2704
3446
  if (entries[cursor]) handleDelete(entries[cursor]);
2705
3447
  } else if (char === "f") {
@@ -2714,7 +3456,7 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2714
3456
  } else if (char === "C") {
2715
3457
  const entry = entries[cursor];
2716
3458
  if (!entry) return;
2717
- const meta = parseMeta2(entry.metadata);
3459
+ const meta = parseMetadata(entry.metadata);
2718
3460
  const project = meta.project;
2719
3461
  if (!project) return;
2720
3462
  const memories = db.searchMemories(project, 3);
@@ -2738,7 +3480,7 @@ ${mem.summary}`);
2738
3480
  if (recent.length > 0) {
2739
3481
  ctx.push("\n---\n## Recent Context");
2740
3482
  for (const p of recent) {
2741
- const pm = parseMeta2(p.metadata);
3483
+ const pm = parseMetadata(p.metadata);
2742
3484
  ctx.push(`
2743
3485
  - #${p.id}: ${pm.summary || p.prompt.slice(0, 100).replace(/\n/g, " ")}`);
2744
3486
  }
@@ -2784,7 +3526,7 @@ ${entry.prompt}`;
2784
3526
  });
2785
3527
  let mainContent;
2786
3528
  if (showSettings) {
2787
- mainContent = /* @__PURE__ */ React6.createElement(
3529
+ mainContent = /* @__PURE__ */ React7.createElement(
2788
3530
  SettingsView,
2789
3531
  {
2790
3532
  onClose: () => setShowSettings(false),
@@ -2792,7 +3534,7 @@ ${entry.prompt}`;
2792
3534
  }
2793
3535
  );
2794
3536
  } else if (showFilterPanel) {
2795
- mainContent = /* @__PURE__ */ React6.createElement(
3537
+ mainContent = /* @__PURE__ */ React7.createElement(
2796
3538
  FilterPanel,
2797
3539
  {
2798
3540
  allEntries,
@@ -2802,8 +3544,22 @@ ${entry.prompt}`;
2802
3544
  theme
2803
3545
  }
2804
3546
  );
3547
+ } else if (providerPicking) {
3548
+ mainContent = /* @__PURE__ */ React7.createElement(
3549
+ ProviderPicker,
3550
+ {
3551
+ entry: providerPicking,
3552
+ onPick: (tool, prompt) => {
3553
+ onRerun?.(tool, prompt);
3554
+ setProviderPicking(null);
3555
+ exit();
3556
+ },
3557
+ onClose: () => setProviderPicking(null),
3558
+ theme
3559
+ }
3560
+ );
2805
3561
  } else if (rerunning) {
2806
- mainContent = /* @__PURE__ */ React6.createElement(
3562
+ mainContent = /* @__PURE__ */ React7.createElement(
2807
3563
  RerunView,
2808
3564
  {
2809
3565
  entry: rerunning,
@@ -2817,7 +3573,7 @@ ${entry.prompt}`;
2817
3573
  }
2818
3574
  );
2819
3575
  } else if (editing) {
2820
- mainContent = /* @__PURE__ */ React6.createElement(
3576
+ mainContent = /* @__PURE__ */ React7.createElement(
2821
3577
  EditView,
2822
3578
  {
2823
3579
  entry: editing,
@@ -2827,7 +3583,7 @@ ${entry.prompt}`;
2827
3583
  }
2828
3584
  );
2829
3585
  } else if (detail && !isWide) {
2830
- mainContent = /* @__PURE__ */ React6.createElement(
3586
+ mainContent = /* @__PURE__ */ React7.createElement(
2831
3587
  DetailView,
2832
3588
  {
2833
3589
  entry: detail,
@@ -2844,10 +3600,10 @@ ${entry.prompt}`;
2844
3600
  } else if (isWide) {
2845
3601
  const listFocused = focusPane === "list";
2846
3602
  const previewFocused = focusPane === "preview";
2847
- mainContent = /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "row", flexGrow: 1 }, /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", width: leftPaneWidth }, /* @__PURE__ */ React6.createElement(Box4, { paddingX: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: listFocused ? theme.primary : theme.dim, bold: listFocused }, listFocused ? "\u276F " : " ", "HISTORY"), entries.length > 0 && /* @__PURE__ */ React6.createElement(Text5, { color: theme.dim }, " ", cursor + 1, "/", entries.length)), entries.length === 0 ? /* @__PURE__ */ React6.createElement(Box4, { paddingX: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "(no results)")) : visible.map((entry, i) => {
3603
+ mainContent = /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "row", flexGrow: 1 }, /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", width: leftPaneWidth }, /* @__PURE__ */ React7.createElement(Box5, { paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: listFocused ? theme.primary : theme.dim, bold: listFocused }, listFocused ? "\u276F " : " ", "HISTORY"), entries.length > 0 && /* @__PURE__ */ React7.createElement(Text6, { color: theme.dim }, " ", cursor + 1, "/", entries.length)), entries.length === 0 ? /* @__PURE__ */ React7.createElement(Box5, { paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "(no results)")) : visible.map((entry, i) => {
2848
3604
  const absIdx = pageStart + i;
2849
3605
  const isNewSession = needsSessionSeparator(entries, absIdx);
2850
- return /* @__PURE__ */ React6.createElement(React6.Fragment, { key: entry.id }, isNewSession && /* @__PURE__ */ React6.createElement(Box4, null, /* @__PURE__ */ React6.createElement(Text5, { color: theme.dim }, "\u254C".repeat(2), " ", formatDateLabel(entry.timestamp), " ", "\u254C".repeat(leftPaneWidth - 4 - formatDateLabel(entry.timestamp).length))), /* @__PURE__ */ React6.createElement(
3606
+ return /* @__PURE__ */ React7.createElement(React7.Fragment, { key: entry.id }, isNewSession && /* @__PURE__ */ React7.createElement(Box5, null, /* @__PURE__ */ React7.createElement(Text6, { color: theme.dim }, "\u254C".repeat(2), " ", formatDateLabel(entry.timestamp), " ", "\u254C".repeat(leftPaneWidth - 4 - formatDateLabel(entry.timestamp).length))), /* @__PURE__ */ React7.createElement(
2851
3607
  ListEntry,
2852
3608
  {
2853
3609
  entry,
@@ -2855,9 +3611,9 @@ ${entry.prompt}`;
2855
3611
  paneWidth: leftPaneWidth,
2856
3612
  theme
2857
3613
  }
2858
- ), i < visible.length - 1 && !needsSessionSeparator(entries, absIdx + 1) && /* @__PURE__ */ React6.createElement(Box4, null, /* @__PURE__ */ React6.createElement(Text5, { color: theme.dim }, "\xB7".repeat(Math.max(1, leftPaneWidth - 2)))));
2859
- })), /* @__PURE__ */ React6.createElement(
2860
- Box4,
3614
+ ), i < visible.length - 1 && !needsSessionSeparator(entries, absIdx + 1) && /* @__PURE__ */ React7.createElement(Box5, null, /* @__PURE__ */ React7.createElement(Text6, { color: theme.dim }, "\xB7".repeat(Math.max(1, leftPaneWidth - 2)))));
3615
+ })), /* @__PURE__ */ React7.createElement(
3616
+ Box5,
2861
3617
  {
2862
3618
  borderLeft: true,
2863
3619
  borderStyle: "single",
@@ -2865,9 +3621,10 @@ ${entry.prompt}`;
2865
3621
  flexGrow: 1,
2866
3622
  flexDirection: "column"
2867
3623
  },
2868
- /* @__PURE__ */ React6.createElement(
3624
+ /* @__PURE__ */ React7.createElement(
2869
3625
  PreviewPane,
2870
3626
  {
3627
+ key: entries[cursor]?.id ?? "empty",
2871
3628
  entry: entries[cursor] ?? null,
2872
3629
  paneWidth: termWidth - leftPaneWidth - 1,
2873
3630
  paneHeight: termHeight - 3,
@@ -2878,10 +3635,10 @@ ${entry.prompt}`;
2878
3635
  )
2879
3636
  ));
2880
3637
  } else {
2881
- mainContent = /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", paddingX: 1, flexGrow: 1 }, entries.length === 0 ? /* @__PURE__ */ React6.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: theme.dim }, "(no results)")) : visible.map((entry, i) => {
3638
+ mainContent = /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", paddingX: 1, flexGrow: 1 }, entries.length === 0 ? /* @__PURE__ */ React7.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: theme.dim }, "(no results)")) : visible.map((entry, i) => {
2882
3639
  const absIdx = pageStart + i;
2883
3640
  const isNewSession = needsSessionSeparator(entries, absIdx);
2884
- return /* @__PURE__ */ React6.createElement(React6.Fragment, { key: entry.id }, isNewSession && /* @__PURE__ */ React6.createElement(Box4, null, /* @__PURE__ */ React6.createElement(Text5, { color: theme.dim }, "\u254C".repeat(2), " ", formatDateLabel(entry.timestamp), " ", "\u254C".repeat(termWidth - 4 - formatDateLabel(entry.timestamp).length))), /* @__PURE__ */ React6.createElement(
3641
+ return /* @__PURE__ */ React7.createElement(React7.Fragment, { key: entry.id }, isNewSession && /* @__PURE__ */ React7.createElement(Box5, null, /* @__PURE__ */ React7.createElement(Text6, { color: theme.dim }, "\u254C".repeat(2), " ", formatDateLabel(entry.timestamp), " ", "\u254C".repeat(termWidth - 4 - formatDateLabel(entry.timestamp).length))), /* @__PURE__ */ React7.createElement(
2885
3642
  ListEntry,
2886
3643
  {
2887
3644
  entry,
@@ -2889,20 +3646,21 @@ ${entry.prompt}`;
2889
3646
  paneWidth: termWidth,
2890
3647
  theme
2891
3648
  }
2892
- ), i < visible.length - 1 && !needsSessionSeparator(entries, absIdx + 1) && /* @__PURE__ */ React6.createElement(Box4, null, /* @__PURE__ */ React6.createElement(Text5, { color: theme.dim }, "\xB7".repeat(Math.max(1, termWidth - 2)))));
3649
+ ), i < visible.length - 1 && !needsSessionSeparator(entries, absIdx + 1) && /* @__PURE__ */ React7.createElement(Box5, null, /* @__PURE__ */ React7.createElement(Text6, { color: theme.dim }, "\xB7".repeat(Math.max(1, termWidth - 2)))));
2893
3650
  }));
2894
3651
  }
2895
- return /* @__PURE__ */ React6.createElement(Box4, { flexDirection: "column", height: termHeight }, /* @__PURE__ */ React6.createElement(
3652
+ return /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", height: termHeight }, /* @__PURE__ */ React7.createElement(
2896
3653
  Header,
2897
3654
  {
2898
3655
  entriesCount: entries.length,
2899
3656
  allEntriesCount: allEntries.length,
3657
+ archiveCount,
2900
3658
  activeFilters,
2901
3659
  textFilter,
2902
3660
  isTextFiltering: false,
2903
3661
  theme: currentThemeName
2904
3662
  }
2905
- ), /* @__PURE__ */ React6.createElement(SearchBar, { value: textFilter, isActive: isTextFiltering, theme }), /* @__PURE__ */ React6.createElement(Box4, { flexGrow: 1 }, mainContent), /* @__PURE__ */ React6.createElement(
3663
+ ), /* @__PURE__ */ React7.createElement(SearchBar, { value: textFilter, isActive: isTextFiltering, theme }), /* @__PURE__ */ React7.createElement(Box5, { flexGrow: 1 }, mainContent), /* @__PURE__ */ React7.createElement(
2906
3664
  Footer,
2907
3665
  {
2908
3666
  cursor,
@@ -3028,6 +3786,32 @@ function parseDate(s, label) {
3028
3786
  async function cmdSearch(db, cfg, args) {
3029
3787
  const { flags, positional } = parseFlags(args);
3030
3788
  const query = positional.join(" ");
3789
+ if (flags["archive"]) {
3790
+ const archiveResults = db.searchArchive({
3791
+ since: flags["since"] ? new Date(flags["since"]).toISOString() : void 0,
3792
+ until: flags["until"] ? new Date(flags["until"]).toISOString() : void 0,
3793
+ limit: Number(flags["limit"] ?? 50)
3794
+ });
3795
+ if (archiveResults.length === 0) {
3796
+ console.log("No archived prompts found.");
3797
+ return;
3798
+ }
3799
+ console.log(`Found ${archiveResults.length} archived prompts:
3800
+ `);
3801
+ for (const r of archiveResults) {
3802
+ const date = new Date(r.timestamp).toLocaleString();
3803
+ const id = r.original_id ?? r.id;
3804
+ const prompt = r.prompt.replace(/\n/g, " ").slice(0, 120);
3805
+ console.log(` #${id} [${r.tool}] (${date})`);
3806
+ console.log(` ${prompt}${r.prompt.length > 120 ? "..." : ""}`);
3807
+ if (r.response) {
3808
+ const resp = r.response.replace(/\n/g, " ").slice(0, 80);
3809
+ console.log(` Response: ${resp}${r.response.length > 80 ? "..." : ""}`);
3810
+ }
3811
+ console.log();
3812
+ }
3813
+ return;
3814
+ }
3031
3815
  if (flags["semantic"] && query) {
3032
3816
  await cmdSearchSemantic(db, cfg, query, Number(flags["limit"] ?? 50), Boolean(flags["full"]));
3033
3817
  return;
@@ -3207,8 +3991,8 @@ ${meta.git_context.diff}
3207
3991
  }
3208
3992
 
3209
3993
  // src/commands/import.ts
3210
- import path7 from "path";
3211
- import os7 from "os";
3994
+ import path8 from "path";
3995
+ import os8 from "os";
3212
3996
 
3213
3997
  // src/importer/gemini.ts
3214
3998
  import fs5 from "fs";
@@ -3216,6 +4000,7 @@ import path4 from "path";
3216
4000
  import os4 from "os";
3217
4001
 
3218
4002
  // src/analyzer/index.ts
4003
+ import { createHash as createHash3 } from "crypto";
3219
4004
  var ANALYSIS_PROMPT = (text) => `Analyze this AI tool prompt and response and return ONLY a JSON object with these exact fields:
3220
4005
  {
3221
4006
  "project": "<project name if clearly identifiable, else empty string>",
@@ -3325,7 +4110,7 @@ function mergeMetadata(existing, result, force) {
3325
4110
  if (!merged.tags?.length) delete merged.tags;
3326
4111
  return merged;
3327
4112
  }
3328
- function parseMeta3(raw) {
4113
+ function parseMeta2(raw) {
3329
4114
  try {
3330
4115
  return JSON.parse(raw);
3331
4116
  } catch {
@@ -3333,20 +4118,28 @@ function parseMeta3(raw) {
3333
4118
  }
3334
4119
  }
3335
4120
  function hasMetadata(entry) {
3336
- const meta = parseMeta3(entry.metadata);
4121
+ const meta = parseMeta2(entry.metadata);
3337
4122
  return Boolean(meta.role || meta.tags?.length || meta.project || meta.language);
3338
4123
  }
4124
+ function gitFingerprint(ctx) {
4125
+ const hash = createHash3("sha256");
4126
+ hash.update(ctx.branch);
4127
+ hash.update(ctx.diff);
4128
+ return `git:${hash.digest("hex").slice(0, 14)}`;
4129
+ }
3339
4130
  async function analyzeAll(entries, provider, db, opts = {}) {
3340
4131
  const { force = false, pruneBelow = 0, dryRun = false, onProgress, onPrune } = opts;
3341
4132
  const toProcess = force ? entries : entries.filter((e) => !hasMetadata(e));
3342
4133
  const stats = { updated: 0, skipped: entries.length - toProcess.length, failed: 0, pruned: 0 };
4134
+ const gitCtx = captureGitContext(process.cwd());
4135
+ const gitFp = gitCtx ? gitFingerprint(gitCtx) : void 0;
3343
4136
  for (let i = 0; i < toProcess.length; i++) {
3344
4137
  const entry = toProcess[i];
3345
4138
  let result = null;
3346
4139
  let errMsg;
3347
4140
  try {
3348
4141
  result = await analyzePrompt(entry.prompt, provider);
3349
- const existing = parseMeta3(entry.metadata);
4142
+ const existing = parseMeta2(entry.metadata);
3350
4143
  const merged = mergeMetadata(existing, result, force);
3351
4144
  if (pruneBelow > 0 && result.relevance !== void 0 && result.relevance < pruneBelow) {
3352
4145
  onPrune?.(entry, result.relevance);
@@ -3357,11 +4150,25 @@ async function analyzeAll(entries, provider, db, opts = {}) {
3357
4150
  } else {
3358
4151
  db.updateMetadata(entry.id, JSON.stringify(merged));
3359
4152
  if (result.project && result.summary) {
4153
+ if (gitFp) {
4154
+ const lastMem = db.getLastMemoryForProject(result.project);
4155
+ if (lastMem?.git_context_snapshot === gitFp) {
4156
+ stats.updated++;
4157
+ continue;
4158
+ }
4159
+ }
3360
4160
  db.upsertProjectMemory({
3361
4161
  project: result.project,
3362
4162
  prompt_id: entry.id,
3363
4163
  summary: result.summary,
3364
4164
  key_insights: result.key_insights ?? [],
4165
+ technical_decisions: result.technical_decisions ?? [],
4166
+ git_context_snapshot: gitFp
4167
+ });
4168
+ db.upsertProjectSummary({
4169
+ project: result.project,
4170
+ summary: result.summary,
4171
+ key_insights: result.key_insights ?? [],
3365
4172
  technical_decisions: result.technical_decisions ?? []
3366
4173
  });
3367
4174
  }
@@ -3501,7 +4308,7 @@ async function importGeminiHistory(db, geminiDir, dryRun, analyzer, onProgress,
3501
4308
  if (response.length > 8e3) response = response.slice(0, 8e3) + "\n... (truncated)";
3502
4309
  }
3503
4310
  try {
3504
- const id = db.insert({
4311
+ const id = db.insert(createCaptureRecord({
3505
4312
  timestamp: new Date(msg.timestamp).toISOString(),
3506
4313
  tool: "gemini",
3507
4314
  prompt: content,
@@ -3510,8 +4317,8 @@ async function importGeminiHistory(db, geminiDir, dryRun, analyzer, onProgress,
3510
4317
  workdir: inferWorkDir(filePath, geminiDir),
3511
4318
  hostname,
3512
4319
  exit_code: 0,
3513
- metadata
3514
- });
4320
+ metadata: JSON.parse(metadata)
4321
+ }));
3515
4322
  filter?.registerHash(content, id);
3516
4323
  result.promptsImported++;
3517
4324
  } catch (e) {
@@ -3669,7 +4476,7 @@ async function importClaudeHistory(db, claudeDir, dryRun, analyzer, onProgress,
3669
4476
  } catch {
3670
4477
  }
3671
4478
  }
3672
- const id = db.insert({
4479
+ const id = db.insert(createCaptureRecord({
3673
4480
  timestamp: entry.timestamp,
3674
4481
  tool: "claude",
3675
4482
  prompt: entry.prompt,
@@ -3678,8 +4485,8 @@ async function importClaudeHistory(db, claudeDir, dryRun, analyzer, onProgress,
3678
4485
  workdir: entry.workdir,
3679
4486
  hostname,
3680
4487
  exit_code: 0,
3681
- metadata
3682
- });
4488
+ metadata: JSON.parse(metadata)
4489
+ }));
3683
4490
  filter?.registerHash(entry.prompt, id);
3684
4491
  result.promptsImported++;
3685
4492
  } catch (e) {
@@ -3852,7 +4659,7 @@ async function importOpenCodeHistory(db, opencodeDir, dryRun, analyzer, onProgre
3852
4659
  metadata.tokens = entry.tokens;
3853
4660
  }
3854
4661
  }
3855
- const id = db.insert({
4662
+ const id = db.insert(createCaptureRecord({
3856
4663
  timestamp: entry.timestamp,
3857
4664
  tool: "opencode",
3858
4665
  prompt: entry.prompt,
@@ -3861,8 +4668,8 @@ async function importOpenCodeHistory(db, opencodeDir, dryRun, analyzer, onProgre
3861
4668
  workdir: entry.workdir,
3862
4669
  hostname,
3863
4670
  exit_code: 0,
3864
- metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : "{}"
3865
- });
4671
+ metadata
4672
+ }));
3866
4673
  filter?.registerHash(entry.prompt, id);
3867
4674
  result.promptsImported++;
3868
4675
  } catch (e) {
@@ -3873,90 +4680,16 @@ async function importOpenCodeHistory(db, opencodeDir, dryRun, analyzer, onProgre
3873
4680
  return result;
3874
4681
  }
3875
4682
 
3876
- // src/ai/ollama.ts
3877
- var OllamaProvider = class {
3878
- constructor(url, model) {
3879
- this.url = url;
3880
- this.model = model;
3881
- }
3882
- name = "ollama";
3883
- async generate(prompt) {
3884
- const controller = new AbortController();
3885
- const timer = setTimeout(() => controller.abort(), 6e4);
3886
- let res;
3887
- try {
3888
- res = await fetch(`${this.url}/api/generate`, {
3889
- method: "POST",
3890
- headers: { "Content-Type": "application/json" },
3891
- body: JSON.stringify({ model: this.model, prompt, stream: false }),
3892
- signal: controller.signal
3893
- });
3894
- } catch (err) {
3895
- clearTimeout(timer);
3896
- const msg = err.message ?? String(err);
3897
- if (msg.includes("abort") || msg.includes("timeout")) {
3898
- throw new Error(`Ollama request timed out (60s). Is Ollama running at ${this.url}?`);
3899
- }
3900
- throw new Error(`Ollama unreachable at ${this.url}: ${msg}`);
3901
- } finally {
3902
- clearTimeout(timer);
3903
- }
3904
- if (!res.ok) {
3905
- const body = await res.text().catch(() => "");
3906
- throw new Error(`Ollama error ${res.status}: ${body}`);
3907
- }
3908
- const data = await res.json();
3909
- return data.response ?? "";
3910
- }
3911
- };
3912
-
3913
- // src/ai/gemini.ts
3914
- var GENERATE_API = "https://generativelanguage.googleapis.com/v1beta/models";
3915
- async function generateContent(prompt, model, apiKey) {
3916
- const res = await fetch(`${GENERATE_API}/${model}:generateContent?key=${apiKey}`, {
3917
- method: "POST",
3918
- headers: { "Content-Type": "application/json" },
3919
- body: JSON.stringify({
3920
- contents: [{ parts: [{ text: prompt }] }]
3921
- })
3922
- });
3923
- if (!res.ok) {
3924
- const err = await res.text();
3925
- throw new Error(`Gemini API error ${res.status}: ${err}`);
3926
- }
3927
- const data = await res.json();
3928
- return data.candidates[0]?.content?.parts[0]?.text ?? "";
3929
- }
3930
- var GeminiProvider = class {
3931
- constructor(model, apiKey) {
3932
- this.model = model;
3933
- this.apiKey = apiKey;
3934
- }
3935
- name = "gemini";
3936
- generate(prompt) {
3937
- return generateContent(prompt, this.model, this.apiKey);
3938
- }
3939
- };
3940
-
3941
- // src/ai/provider.ts
3942
- function getProvider(cfg) {
3943
- const provider = cfg.analyzeProvider ?? "ollama";
3944
- if (provider === "gemini") {
3945
- const apiKey = process.env.GEMINI_API_KEY ?? cfg.geminiApiKey;
3946
- if (!apiKey) return null;
3947
- return new GeminiProvider("gemini-2.5-flash", apiKey);
3948
- }
3949
- return new OllamaProvider(
3950
- cfg.ollamaUrl ?? "http://localhost:11434",
3951
- cfg.ollamaModel ?? "llama3.1:latest"
3952
- );
3953
- }
4683
+ // src/importer/codex.ts
4684
+ import fs8 from "fs";
4685
+ import path7 from "path";
4686
+ import os7 from "os";
3954
4687
 
3955
4688
  // src/filter/index.ts
3956
4689
  import { createHash as createHash4 } from "crypto";
3957
- var TRIVIAL_PATTERN = /^(yes|no|ok|okay|sure|thanks|thank you|continue|go on|next|exit|quit|stop|help|y|n|👍|👎|che dici\??|dimmi|vai|go|do it|tell me|vadi|procedi|aspetta|wait|[.!?,;:]+|\s*)$/i;
4690
+ var TRIVIAL_PATTERN = /^(yes|no|ok|okay|sure|thanks|thank you|continue|go on|next|exit|quit|stop|help|y|n|👍|👎|che dici\??|dimmi|vai|go|do it|tell me|vadi|procedi|grazie|aspetta|wait|[.!?,;:]+|\s*)$/i;
3958
4691
  var ORPHAN_CODE_PATTERN = /^([{}()[\]<>|&!=\-+*/%^~#@\\:;.,\s]+|\.\.\.|console\.log.*|print.*)$/i;
3959
- var NON_PRINTABLE_PATTERN = /^[\x00-\x1f\x7f\x1b[\]()#;\d;A-Za-z]*$/;
4692
+ var NON_PRINTABLE_PATTERN = /^(?=.*[\x00-\x1f\x7f])[\x00-\x1f\x7f\x1b[\]()#;\d;A-Za-z]*$/;
3960
4693
  var FilterPipeline = class _FilterPipeline {
3961
4694
  minLength;
3962
4695
  minRelevance;
@@ -4012,28 +4745,226 @@ var FilterPipeline = class _FilterPipeline {
4012
4745
  }
4013
4746
  return { keep: true };
4014
4747
  }
4015
- /**
4016
- * Full pipeline check: rules → dedup → (relevance if provided).
4017
- * Short-circuits on first failure.
4018
- */
4019
- check(text, analysisResult) {
4020
- const rulesResult = this.checkRules(text);
4021
- if (!rulesResult.keep) return rulesResult;
4022
- const dupResult = this.checkDuplicate(text);
4023
- if (!dupResult.keep) return dupResult;
4024
- if (analysisResult !== void 0) {
4025
- const relResult = this.checkRelevance(analysisResult);
4026
- if (!relResult.keep) return relResult;
4027
- }
4028
- return { keep: true };
4748
+ /**
4749
+ * Full pipeline check: rules → dedup → (relevance if provided).
4750
+ * Short-circuits on first failure.
4751
+ */
4752
+ check(text, analysisResult) {
4753
+ const rulesResult = this.checkRules(text);
4754
+ if (!rulesResult.keep) return rulesResult;
4755
+ const dupResult = this.checkDuplicate(text);
4756
+ if (!dupResult.keep) return dupResult;
4757
+ if (analysisResult !== void 0) {
4758
+ const relResult = this.checkRelevance(analysisResult);
4759
+ if (!relResult.keep) return relResult;
4760
+ }
4761
+ return { keep: true };
4762
+ }
4763
+ };
4764
+
4765
+ // src/importer/codex.ts
4766
+ function textFromContent(content) {
4767
+ if (typeof content === "string") return content;
4768
+ if (!Array.isArray(content)) return "";
4769
+ return content.flatMap((part) => {
4770
+ if (!part || typeof part !== "object") return [];
4771
+ const item = part;
4772
+ return typeof item.text === "string" && ["output_text", "input_text", "text"].includes(String(item.type)) ? [item.text] : [];
4773
+ }).join("");
4774
+ }
4775
+ function rolloutFiles(sessionsDir) {
4776
+ if (!fs8.existsSync(sessionsDir)) return [];
4777
+ const files = [];
4778
+ const visit = (dir) => {
4779
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
4780
+ const full = path7.join(dir, entry.name);
4781
+ if (entry.isDirectory()) visit(full);
4782
+ else if (entry.isFile() && /^rollout-.*\.jsonl$/.test(entry.name)) files.push(full);
4783
+ }
4784
+ };
4785
+ visit(sessionsDir);
4786
+ return files;
4787
+ }
4788
+ async function importCodexHistory(db, codexDir, dryRun, analyzer, onProgress, filter, transcriptFile) {
4789
+ const result = { filesScanned: 0, promptsFound: 0, promptsImported: 0, skipped: 0, filtered: 0, errors: [] };
4790
+ const dedupFilter = filter ?? new FilterPipeline({ minLength: 0, minRelevance: 0, existingHashes: db.getAllPromptHashes() });
4791
+ const files = transcriptFile ? [transcriptFile] : rolloutFiles(path7.join(codexDir, "sessions"));
4792
+ result.filesScanned = files.length;
4793
+ const hostname = os7.hostname();
4794
+ const entries = [];
4795
+ for (const file of files) {
4796
+ let lines;
4797
+ try {
4798
+ lines = fs8.readFileSync(file, "utf8").split("\n");
4799
+ } catch (error) {
4800
+ result.errors.push(`read ${file}: ${error.message}`);
4801
+ continue;
4802
+ }
4803
+ let workdir = "";
4804
+ let sessionId = path7.basename(file, ".jsonl").replace(/^rollout-.*?-/, "");
4805
+ const events = [];
4806
+ for (const line of lines) {
4807
+ if (!line.trim()) continue;
4808
+ try {
4809
+ const event = JSON.parse(line);
4810
+ events.push(event);
4811
+ if (event.type === "session_meta" && event.payload) {
4812
+ if (typeof event.payload.cwd === "string") workdir = event.payload.cwd;
4813
+ if (typeof event.payload.id === "string") sessionId = event.payload.id;
4814
+ }
4815
+ } catch {
4816
+ }
4817
+ }
4818
+ for (let i = 0; i < events.length; i++) {
4819
+ const event = events[i];
4820
+ const payload = event.payload ?? {};
4821
+ const isUserMessage = event.type === "event_msg" && payload.type === "user_message" || event.type === "response_item" && payload.type === "message" && payload.role === "user";
4822
+ const prompt = !isUserMessage ? "" : typeof payload.message === "string" ? payload.message.trim() : textFromContent(payload.content).trim();
4823
+ if (!prompt) continue;
4824
+ const agentMessages = [];
4825
+ const assistantItems = [];
4826
+ const finalAssistantItems = [];
4827
+ for (let j = i + 1; j < events.length; j++) {
4828
+ const candidate = events[j].payload ?? {};
4829
+ const nextIsUserMessage = events[j].type === "event_msg" && candidate.type === "user_message" || events[j].type === "response_item" && candidate.type === "message" && candidate.role === "user";
4830
+ if (nextIsUserMessage) break;
4831
+ if (events[j].type === "event_msg" && candidate.type === "agent_message" && typeof candidate.message === "string") agentMessages.push(candidate.message);
4832
+ if (events[j].type === "response_item" && candidate.type === "message" && candidate.role === "assistant") {
4833
+ const text = textFromContent(candidate.content);
4834
+ assistantItems.push(text);
4835
+ if (candidate.phase === "final_answer") finalAssistantItems.push(text);
4836
+ }
4837
+ }
4838
+ const response = agentMessages.length > 0 ? agentMessages.join("") : finalAssistantItems.length > 0 ? finalAssistantItems.join("") : assistantItems.join("");
4839
+ entries.push({ prompt, response: response.slice(0, 8e3), timestamp: event.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(), workdir, sessionId });
4840
+ }
4841
+ }
4842
+ for (let i = 0; i < entries.length; i++) {
4843
+ const entry = entries[i];
4844
+ result.promptsFound++;
4845
+ onProgress?.(i + 1, result.promptsImported, entries.length, entry.prompt.slice(0, 60));
4846
+ if (dryRun) {
4847
+ result.promptsImported++;
4848
+ continue;
4849
+ }
4850
+ const rules = dedupFilter.checkRules(entry.prompt);
4851
+ if (!rules.keep || !dedupFilter.checkDuplicate(entry.prompt).keep) {
4852
+ result.filtered++;
4853
+ continue;
4854
+ }
4855
+ try {
4856
+ let metadata = {};
4857
+ if (analyzer) {
4858
+ try {
4859
+ metadata = mergeMetadata({}, await analyzePrompt(entry.prompt, analyzer), false);
4860
+ } catch {
4861
+ }
4862
+ }
4863
+ const id = db.insert(createCaptureRecord({
4864
+ timestamp: entry.timestamp,
4865
+ tool: "codex",
4866
+ prompt: entry.prompt,
4867
+ response: entry.response,
4868
+ args: entry.prompt,
4869
+ workdir: entry.workdir,
4870
+ hostname,
4871
+ exit_code: 0,
4872
+ metadata: { ...metadata }
4873
+ }));
4874
+ dedupFilter.registerHash(entry.prompt, id);
4875
+ result.promptsImported++;
4876
+ } catch (error) {
4877
+ result.errors.push(`insert session ${entry.sessionId}: ${error.message}`);
4878
+ result.skipped++;
4879
+ }
4880
+ }
4881
+ return result;
4882
+ }
4883
+
4884
+ // src/ai/ollama.ts
4885
+ var OllamaProvider = class {
4886
+ constructor(url, model) {
4887
+ this.url = url;
4888
+ this.model = model;
4889
+ }
4890
+ name = "ollama";
4891
+ async generate(prompt) {
4892
+ const controller = new AbortController();
4893
+ const timer = setTimeout(() => controller.abort(), 6e4);
4894
+ let res;
4895
+ try {
4896
+ res = await fetch(`${this.url}/api/generate`, {
4897
+ method: "POST",
4898
+ headers: { "Content-Type": "application/json" },
4899
+ body: JSON.stringify({ model: this.model, prompt, stream: false }),
4900
+ signal: controller.signal
4901
+ });
4902
+ } catch (err) {
4903
+ clearTimeout(timer);
4904
+ const msg = err.message ?? String(err);
4905
+ if (msg.includes("abort") || msg.includes("timeout")) {
4906
+ throw new Error(`Ollama request timed out (60s). Is Ollama running at ${this.url}?`, { cause: err });
4907
+ }
4908
+ throw new Error(`Ollama unreachable at ${this.url}: ${msg}`, { cause: err });
4909
+ } finally {
4910
+ clearTimeout(timer);
4911
+ }
4912
+ if (!res.ok) {
4913
+ const body = await res.text().catch(() => "");
4914
+ throw new Error(`Ollama error ${res.status}: ${body}`);
4915
+ }
4916
+ const data = await res.json();
4917
+ return data.response ?? "";
4918
+ }
4919
+ };
4920
+
4921
+ // src/ai/gemini.ts
4922
+ var GENERATE_API = "https://generativelanguage.googleapis.com/v1beta/models";
4923
+ async function generateContent(prompt, model, apiKey) {
4924
+ const res = await fetch(`${GENERATE_API}/${model}:generateContent?key=${apiKey}`, {
4925
+ method: "POST",
4926
+ headers: { "Content-Type": "application/json" },
4927
+ body: JSON.stringify({
4928
+ contents: [{ parts: [{ text: prompt }] }]
4929
+ })
4930
+ });
4931
+ if (!res.ok) {
4932
+ const err = await res.text();
4933
+ throw new Error(`Gemini API error ${res.status}: ${err}`);
4934
+ }
4935
+ const data = await res.json();
4936
+ return data.candidates[0]?.content?.parts[0]?.text ?? "";
4937
+ }
4938
+ var GeminiProvider = class {
4939
+ constructor(model, apiKey) {
4940
+ this.model = model;
4941
+ this.apiKey = apiKey;
4942
+ }
4943
+ name = "gemini";
4944
+ generate(prompt) {
4945
+ return generateContent(prompt, this.model, this.apiKey);
4029
4946
  }
4030
4947
  };
4031
4948
 
4949
+ // src/ai/provider.ts
4950
+ function getProvider(cfg) {
4951
+ const provider = cfg.analyzeProvider ?? "ollama";
4952
+ if (provider === "gemini") {
4953
+ const apiKey = process.env.GEMINI_API_KEY ?? cfg.geminiApiKey;
4954
+ if (!apiKey) return null;
4955
+ return new GeminiProvider("gemini-2.5-flash", apiKey);
4956
+ }
4957
+ return new OllamaProvider(
4958
+ cfg.ollamaUrl ?? "http://localhost:11434",
4959
+ cfg.ollamaModel ?? "llama3.1:latest"
4960
+ );
4961
+ }
4962
+
4032
4963
  // src/commands/import.ts
4033
4964
  async function cmdImport(db, cfg, args) {
4034
4965
  if (args.length === 0) {
4035
4966
  process.stderr.write("Usage: ph import <source> [--dry-run] [--analyze]\n");
4036
- process.stderr.write("Supported sources: gemini, claude, opencode\n");
4967
+ process.stderr.write("Supported sources: gemini, claude, opencode, codex\n");
4037
4968
  process.exit(1);
4038
4969
  }
4039
4970
  const source = args[0];
@@ -4041,7 +4972,8 @@ async function cmdImport(db, cfg, args) {
4041
4972
  const dryRun = Boolean(flags["dry-run"]);
4042
4973
  const analyze = Boolean(flags["analyze"]);
4043
4974
  const useFilter = Boolean(flags["filter"]);
4044
- const homeDir = os7.homedir();
4975
+ const transcriptFile = typeof flags.file === "string" ? flags.file : void 0;
4976
+ const homeDir = os8.homedir();
4045
4977
  if (dryRun) console.log("Dry run \u2014 nothing will be written to the database.");
4046
4978
  let analyzer;
4047
4979
  if (analyze) {
@@ -4076,16 +5008,19 @@ async function cmdImport(db, cfg, args) {
4076
5008
  let result;
4077
5009
  switch (source) {
4078
5010
  case "gemini":
4079
- result = await importGeminiHistory(db, path7.join(homeDir, ".gemini"), dryRun, analyzer, onProgress, filter);
5011
+ result = await importGeminiHistory(db, path8.join(homeDir, ".gemini"), dryRun, analyzer, onProgress, filter);
4080
5012
  break;
4081
5013
  case "claude":
4082
- result = await importClaudeHistory(db, path7.join(homeDir, ".claude"), dryRun, analyzer, onProgress, filter);
5014
+ result = await importClaudeHistory(db, path8.join(homeDir, ".claude"), dryRun, analyzer, onProgress, filter);
4083
5015
  break;
4084
5016
  case "opencode":
4085
- result = await importOpenCodeHistory(db, path7.join(homeDir, ".local", "share", "opencode", "storage"), dryRun, analyzer, onProgress, filter);
5017
+ result = await importOpenCodeHistory(db, path8.join(homeDir, ".local", "share", "opencode", "storage"), dryRun, analyzer, onProgress, filter);
5018
+ break;
5019
+ case "codex":
5020
+ result = await importCodexHistory(db, path8.join(homeDir, ".codex"), dryRun, analyzer, onProgress, filter, transcriptFile);
4086
5021
  break;
4087
5022
  default:
4088
- process.stderr.write(`ph: unknown import source "${source}". Supported: gemini, claude, opencode
5023
+ process.stderr.write(`ph: unknown import source "${source}". Supported: gemini, claude, opencode, codex
4089
5024
  `);
4090
5025
  process.exit(1);
4091
5026
  }
@@ -4190,9 +5125,6 @@ ${e.response}` : e.prompt);
4190
5125
  console.log("\nDone!");
4191
5126
  }
4192
5127
 
4193
- // src/commands/log.ts
4194
- import os8 from "os";
4195
-
4196
5128
  // src/background/analyzer.ts
4197
5129
  import { spawn as spawn3 } from "child_process";
4198
5130
  function spawnBackgroundAnalysis(id, dbPath) {
@@ -4245,32 +5177,16 @@ async function cmdLog(dbPath, cfg, args) {
4245
5177
  process.exit(1);
4246
5178
  }
4247
5179
  }
4248
- const { rootDir, projectName } = detectProject(workdir);
4249
- const language = detectLanguage(rootDir);
4250
- const metaObj = { $schema_version: 1 };
4251
- if (projectName) metaObj.project = projectName;
4252
- if (language) metaObj.language = language;
4253
- const title = extractTopic(prompt);
4254
- if (title) metaObj.title = title;
5180
+ const record = createCaptureRecord({ tool, prompt, response, args: "", workdir });
4255
5181
  const db = new PhDB(dbPath);
4256
- const id = db.insert({
4257
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4258
- tool,
4259
- prompt,
4260
- response,
4261
- args: "",
4262
- workdir,
4263
- hostname: os8.hostname(),
4264
- exit_code: 0,
4265
- metadata: JSON.stringify(metaObj)
4266
- });
5182
+ const id = db.insert(record);
4267
5183
  db.close();
4268
5184
  if (cfg.backgroundAnalysis) {
4269
5185
  spawnBackgroundAnalysis(id, dbPath);
4270
5186
  }
4271
5187
  const pushUrl = process.env.PH_REMOTE_URL || cfg.remoteUrl;
4272
5188
  if (pushUrl) {
4273
- pushToRemote(pushUrl, cfg.remoteApiKey, { timestamp: (/* @__PURE__ */ new Date()).toISOString(), tool, prompt, response, args: "", workdir, hostname: os8.hostname(), exit_code: 0, metadata: JSON.stringify(metaObj) }).catch(() => {
5189
+ pushToRemote(pushUrl, cfg.remoteApiKey, record).catch(() => {
4274
5190
  });
4275
5191
  }
4276
5192
  }
@@ -4361,6 +5277,13 @@ function cmdConfig(args) {
4361
5277
  }
4362
5278
 
4363
5279
  // src/commands/background-analyze.ts
5280
+ import { createHash as createHash5 } from "crypto";
5281
+ function gitFingerprint2(ctx) {
5282
+ const hash = createHash5("sha256");
5283
+ hash.update(ctx.branch);
5284
+ hash.update(ctx.diff);
5285
+ return `git:${hash.digest("hex").slice(0, 14)}`;
5286
+ }
4364
5287
  async function cmdBackgroundAnalyze(dbPath, args) {
4365
5288
  const id = parseInt(args[0], 10);
4366
5289
  if (isNaN(id)) return;
@@ -4386,11 +5309,24 @@ async function cmdBackgroundAnalyze(dbPath, args) {
4386
5309
  const merged = mergeMetadata(existing, result, false);
4387
5310
  db.updateMetadata(id, JSON.stringify(merged));
4388
5311
  if (result.project && result.summary) {
5312
+ const gitCtx = captureGitContext(process.cwd());
5313
+ const gitFp = gitCtx ? gitFingerprint2(gitCtx) : void 0;
5314
+ if (gitFp) {
5315
+ const lastMem = db.getLastMemoryForProject(result.project);
5316
+ if (lastMem?.git_context_snapshot === gitFp) return;
5317
+ }
4389
5318
  db.upsertProjectMemory({
4390
5319
  project: result.project,
4391
5320
  prompt_id: id,
4392
5321
  summary: result.summary,
4393
5322
  key_insights: result.key_insights ?? [],
5323
+ technical_decisions: result.technical_decisions ?? [],
5324
+ git_context_snapshot: gitFp
5325
+ });
5326
+ db.upsertProjectSummary({
5327
+ project: result.project,
5328
+ summary: result.summary,
5329
+ key_insights: result.key_insights ?? [],
4394
5330
  technical_decisions: result.technical_decisions ?? []
4395
5331
  });
4396
5332
  }
@@ -4404,6 +5340,11 @@ async function cmdBackgroundAnalyze(dbPath, args) {
4404
5340
  async function cmdCleanup(db, cfg, args) {
4405
5341
  const { flags } = parseFlags(args);
4406
5342
  const dryRun = Boolean(flags["dry-run"]);
5343
+ const retention = Boolean(flags["retention"]);
5344
+ if (retention) {
5345
+ await runRetention(db, cfg, dryRun);
5346
+ return;
5347
+ }
4407
5348
  const minLength = flags["min-length"] ? Number(flags["min-length"]) : cfg.filterMinLength ?? 15;
4408
5349
  const minScore = flags["min-score"] ? Number(flags["min-score"]) : cfg.filterMinRelevance ?? 3;
4409
5350
  const days = flags["days"] ? Number(flags["days"]) : void 0;
@@ -4467,6 +5408,71 @@ Candidates for rule-based deletion: ${toDelete.length}`);
4467
5408
  console.log(`
4468
5409
  Deleted ${deleted} prompts.`);
4469
5410
  }
5411
+ async function runRetention(db, cfg, dryRun) {
5412
+ const retentionDays = cfg.retentionDays ?? 90;
5413
+ const minStarred = cfg.retentionMinStarred ?? true;
5414
+ const minAnalyzed = cfg.retentionMinAnalyzed ?? true;
5415
+ const minRelevance = cfg.retentionMinRelevance ?? 3;
5416
+ const cutoff = /* @__PURE__ */ new Date();
5417
+ cutoff.setDate(cutoff.getDate() - retentionDays);
5418
+ const cutoffStr = cutoff.toISOString();
5419
+ const allEntries = db.search({ limit: 1e5 });
5420
+ const stats = db.getArchiveStats();
5421
+ console.log(`Retention policy: ${retentionDays} days, minRelevance=${minRelevance}, keepStarred=${minStarred}, keepAnalyzed=${minAnalyzed}`);
5422
+ console.log(`Archive: ${stats.total} entries (${stats.oldest ? `oldest ${stats.oldest}` : "empty"})`);
5423
+ console.log(`Scanning ${allEntries.length} prompts...
5424
+ `);
5425
+ const toArchive = [];
5426
+ for (const entry of allEntries) {
5427
+ let meta = {};
5428
+ try {
5429
+ meta = JSON.parse(entry.metadata);
5430
+ } catch {
5431
+ }
5432
+ const age = entry.timestamp;
5433
+ const isOld = age < cutoffStr;
5434
+ const isStarred = meta.starred === true;
5435
+ const isAnalyzed = Boolean(meta.summary || meta.role);
5436
+ const relevance = meta.relevance ?? 5;
5437
+ if (!isOld) continue;
5438
+ if (relevance < minRelevance) {
5439
+ toArchive.push({ id: entry.id, prompt: entry.prompt, reason: `low_relevance(${relevance})`, meta });
5440
+ continue;
5441
+ }
5442
+ if (!(minStarred && isStarred) && !(minAnalyzed && isAnalyzed)) {
5443
+ toArchive.push({ id: entry.id, prompt: entry.prompt, reason: `old(>${retentionDays}d)`, meta });
5444
+ }
5445
+ }
5446
+ if (toArchive.length === 0) {
5447
+ console.log("No prompts match retention criteria.");
5448
+ return;
5449
+ }
5450
+ console.log(`Candidates for archiving: ${toArchive.length}`);
5451
+ for (const item of toArchive.slice(0, 20)) {
5452
+ const short = item.prompt.replace(/\n/g, " ").slice(0, 60);
5453
+ const starred = item.meta.starred ? " *" : "";
5454
+ const analyzed = item.meta.summary ? " [A]" : "";
5455
+ console.log(` #${String(item.id).padEnd(5)} [${item.reason}]${starred}${analyzed} "${short}"`);
5456
+ }
5457
+ if (toArchive.length > 20) {
5458
+ console.log(` ... and ${toArchive.length - 20} more`);
5459
+ }
5460
+ if (dryRun) {
5461
+ console.log(`
5462
+ (dry-run) Would archive ${toArchive.length} prompts. Run without --dry-run to apply.`);
5463
+ return;
5464
+ }
5465
+ const ids = toArchive.map((e) => e.id);
5466
+ const archived = db.archivePrompts(ids);
5467
+ console.log(`
5468
+ Archived ${archived} prompts.`);
5469
+ const purgeCutoff = /* @__PURE__ */ new Date();
5470
+ purgeCutoff.setDate(purgeCutoff.getDate() - retentionDays * 2);
5471
+ const purged = db.purgeArchive(purgeCutoff.toISOString());
5472
+ if (purged > 0) {
5473
+ console.log(`Purged ${purged} archived entries older than ${retentionDays * 2} days.`);
5474
+ }
5475
+ }
4470
5476
 
4471
5477
  // src/commands/vacuum.ts
4472
5478
  async function cmdVacuum(db) {
@@ -4490,75 +5496,20 @@ async function cmdContext(db, cfg, args) {
4490
5496
  process.stderr.write("ph: could not detect project. Use --project <name>\n");
4491
5497
  process.exit(1);
4492
5498
  }
4493
- const memories = !promptsOnly ? db.searchMemories(project, limit) : [];
4494
- let prompts = [];
4495
- if (!memoriesOnly) {
4496
- if (query) {
4497
- const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
4498
- const model = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
4499
- const [queryVec] = await getEmbeddings([query], ollamaUrl, model, 1);
4500
- if (queryVec) {
4501
- const semResults = db.searchSemantic(queryVec, limit * 3);
4502
- prompts = semResults.filter((e) => {
4503
- try {
4504
- const meta = JSON.parse(e.metadata);
4505
- return meta.project === project;
4506
- } catch {
4507
- return false;
4508
- }
4509
- }).slice(0, limit);
4510
- }
4511
- } else {
4512
- prompts = db.getProjectMemory(project, limit);
4513
- }
4514
- }
4515
- if (memories.length === 0 && prompts.length === 0) {
4516
- process.stdout.write(`No relevant context found for project "${project}".
4517
- `);
4518
- return;
4519
- }
4520
- const output = [];
4521
- if (memories.length > 0) {
4522
- output.push(`## Project Knowledge: ${project}
4523
- `);
4524
- for (const mem of memories) {
4525
- if (mem.summary) output.push(`${mem.summary}
4526
- `);
4527
- if (mem.key_insights.length > 0) {
4528
- output.push("Key Insights:");
4529
- for (const insight of mem.key_insights) output.push(` - ${insight}`);
4530
- output.push("");
4531
- }
4532
- if (mem.technical_decisions.length > 0) {
4533
- output.push("Technical Decisions:");
4534
- for (const dec of mem.technical_decisions) output.push(` - ${dec}`);
4535
- output.push("");
4536
- }
4537
- }
4538
- }
4539
- if (prompts.length > 0) {
4540
- output.push("---\n");
4541
- output.push(`## Recent Context
4542
- `);
4543
- for (const e of prompts) {
4544
- let meta = {};
4545
- try {
4546
- meta = JSON.parse(e.metadata);
4547
- } catch {
4548
- }
4549
- output.push(`### #${e.id}`);
4550
- if (meta.role) output.push(`Role: ${meta.role}`);
4551
- if (meta.summary) output.push(`${meta.summary}`);
4552
- if (meta.key_insights && meta.key_insights.length > 0) {
4553
- for (const i of meta.key_insights) output.push(` - ${i}`);
4554
- }
4555
- if (verbose) {
4556
- output.push("```\n" + e.prompt.slice(0, 1e3) + (e.prompt.length > 1e3 ? "\n..." : "") + "\n```");
4557
- }
4558
- output.push("");
4559
- }
4560
- }
4561
- process.stdout.write(output.join("\n"));
5499
+ const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
5500
+ const model = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
5501
+ const context = await getProjectContext(db, {
5502
+ project,
5503
+ query,
5504
+ limit,
5505
+ includeSummary: !promptsOnly,
5506
+ includeMemories: !promptsOnly,
5507
+ includePrompts: !memoriesOnly
5508
+ }, async (text) => {
5509
+ const [vector] = await getEmbeddings([text], ollamaUrl, model, 1);
5510
+ return vector;
5511
+ });
5512
+ process.stdout.write(formatProjectContext(context, { includePromptText: verbose }) + "\n");
4562
5513
  }
4563
5514
 
4564
5515
  // src/sessions/index.ts
@@ -4613,42 +5564,100 @@ function computeSessionCohesion(session, embeddings) {
4613
5564
  }
4614
5565
 
4615
5566
  // src/commands/sessions.ts
5567
+ function formatTimestamp4(ts) {
5568
+ const d = new Date(ts);
5569
+ const Y = d.getFullYear();
5570
+ const M = String(d.getMonth() + 1).padStart(2, "0");
5571
+ const D = String(d.getDate()).padStart(2, "0");
5572
+ const h = String(d.getHours()).padStart(2, "0");
5573
+ const m = String(d.getMinutes()).padStart(2, "0");
5574
+ return `${Y}-${M}-${D} ${h}:${m}`;
5575
+ }
5576
+ function fmtTime(ts) {
5577
+ const d = new Date(ts);
5578
+ const hh = String(d.getHours()).padStart(2, "0");
5579
+ const mm = String(d.getMinutes()).padStart(2, "0");
5580
+ return `${hh}:${mm}`;
5581
+ }
5582
+ function parseMeta3(raw) {
5583
+ try {
5584
+ return JSON.parse(raw);
5585
+ } catch {
5586
+ return {};
5587
+ }
5588
+ }
5589
+ function exportSessionMarkdown(session) {
5590
+ const lines = [];
5591
+ const dateStr = formatTimestamp4(session.startTime.toISOString());
5592
+ const endStr = fmtTime(session.endTime.toISOString());
5593
+ lines.push(`# Session ${session.index} \u2014 ${dateStr} \u2192 ${endStr}`);
5594
+ lines.push("");
5595
+ lines.push(`**${session.entries.length} prompts**`);
5596
+ lines.push("");
5597
+ lines.push("---");
5598
+ lines.push("");
5599
+ for (const entry of session.entries) {
5600
+ const meta = parseMeta3(entry.metadata);
5601
+ const ts = formatTimestamp4(entry.timestamp);
5602
+ const title = meta.title || entry.prompt.replace(/\n/g, " ").slice(0, 60);
5603
+ lines.push(`## #${entry.id} \u2014 ${entry.tool} \u2014 ${ts}`);
5604
+ if (title) lines.push(`**${title}**`);
5605
+ lines.push("");
5606
+ const metaLine = [];
5607
+ if (meta.role) metaLine.push(`Role: ${meta.role}`);
5608
+ if (meta.project) metaLine.push(`Project: ${meta.project}`);
5609
+ if (meta.language) metaLine.push(`Language: ${meta.language}`);
5610
+ if (meta.tags?.length) metaLine.push(`Tags: ${meta.tags.join(", ")}`);
5611
+ if (meta.relevance !== void 0) metaLine.push(`Relevance: ${meta.relevance}/10`);
5612
+ if (meta.quality !== void 0) metaLine.push(`Quality: ${meta.quality}/10`);
5613
+ if (metaLine.length > 0) {
5614
+ lines.push(metaLine.join(" \xB7 "));
5615
+ lines.push("");
5616
+ }
5617
+ if (meta.summary) {
5618
+ lines.push(`> ${meta.summary}`);
5619
+ lines.push("");
5620
+ }
5621
+ lines.push("### Prompt");
5622
+ lines.push("");
5623
+ lines.push("```");
5624
+ lines.push(entry.prompt);
5625
+ lines.push("```");
5626
+ lines.push("");
5627
+ if (entry.response) {
5628
+ lines.push("### Response");
5629
+ lines.push("");
5630
+ lines.push("```");
5631
+ lines.push(entry.response.slice(0, 2e3));
5632
+ lines.push("```");
5633
+ lines.push("");
5634
+ }
5635
+ if (meta.key_insights?.length) {
5636
+ lines.push("**Insights:**");
5637
+ for (const ins of meta.key_insights) lines.push(`- ${ins}`);
5638
+ lines.push("");
5639
+ }
5640
+ if (meta.git_context) {
5641
+ lines.push(`*Git: ${meta.git_context.branch}, ${meta.git_context.files?.length || 0} files modified*`);
5642
+ lines.push("");
5643
+ }
5644
+ lines.push("---");
5645
+ lines.push("");
5646
+ }
5647
+ return lines.join("\n");
5648
+ }
4616
5649
  async function cmdSessions(db, args) {
4617
5650
  const { flags } = parseFlags(args);
4618
5651
  const gapHours = Number(flags["gap-hours"] ?? 2);
4619
5652
  const limit = Number(flags["limit"] ?? 20);
4620
5653
  const minSize = Number(flags["min-size"] ?? 1);
4621
5654
  const noCohesion = Boolean(flags["no-cohesion"]);
4622
- const C2 = {
4623
- reset: "\x1B[0m",
4624
- cyan: "\x1B[36m",
4625
- yellow: "\x1B[33m",
4626
- green: "\x1B[32m",
4627
- gray: "\x1B[90m",
4628
- red: "\x1B[31m",
4629
- blue: "\x1B[34m",
4630
- magenta: "\x1B[35m"
4631
- };
4632
- function fmtDate(ts) {
4633
- const d = new Date(ts);
4634
- const yr = d.getFullYear();
4635
- const mo = String(d.getMonth() + 1).padStart(2, "0");
4636
- const dy = String(d.getDate()).padStart(2, "0");
4637
- const hh = String(d.getHours()).padStart(2, "0");
4638
- const mm = String(d.getMinutes()).padStart(2, "0");
4639
- return `${yr}-${mo}-${dy} ${hh}:${mm}`;
4640
- }
4641
- function fmtTime(ts) {
4642
- const d = new Date(ts);
4643
- const hh = String(d.getHours()).padStart(2, "0");
4644
- const mm = String(d.getMinutes()).padStart(2, "0");
4645
- return `${hh}:${mm}`;
4646
- }
5655
+ const exportIndex = flags["export"] !== void 0 ? Number(flags["export"]) : void 0;
4647
5656
  const allEntries = db.search({ limit: 1e5 });
4648
5657
  const entriesAsc = [...allEntries].reverse();
4649
5658
  const sessions = groupIntoSessions(entriesAsc, gapHours);
4650
5659
  let embeddings = null;
4651
- if (!noCohesion) {
5660
+ if (!noCohesion && exportIndex === void 0) {
4652
5661
  embeddings = db.getAllEmbeddings();
4653
5662
  }
4654
5663
  for (const session of sessions) {
@@ -4656,21 +5665,37 @@ async function cmdSessions(db, args) {
4656
5665
  session.cohesion = computeSessionCohesion(session, embeddings);
4657
5666
  }
4658
5667
  }
5668
+ if (exportIndex !== void 0) {
5669
+ const session = sessions.find((s) => s.index === exportIndex);
5670
+ if (!session) {
5671
+ process.stderr.write(`Session ${exportIndex} not found.
5672
+ `);
5673
+ return;
5674
+ }
5675
+ process.stdout.write(exportSessionMarkdown(session));
5676
+ return;
5677
+ }
4659
5678
  const filtered = sessions.filter((s) => s.entries.length >= minSize);
4660
5679
  const visible = filtered.slice(-limit);
4661
5680
  if (visible.length === 0) {
4662
5681
  process.stdout.write("No sessions found.\n");
4663
5682
  return;
4664
5683
  }
5684
+ const C2 = {
5685
+ reset: "\x1B[0m",
5686
+ cyan: "\x1B[36m",
5687
+ yellow: "\x1B[33m",
5688
+ green: "\x1B[32m",
5689
+ gray: "\x1B[90m",
5690
+ red: "\x1B[31m",
5691
+ blue: "\x1B[34m",
5692
+ magenta: "\x1B[35m"
5693
+ };
4665
5694
  for (const session of visible) {
4666
5695
  const projectCounts = /* @__PURE__ */ new Map();
4667
5696
  const languageCounts = /* @__PURE__ */ new Map();
4668
5697
  for (const entry of session.entries) {
4669
- let meta = {};
4670
- try {
4671
- meta = JSON.parse(entry.metadata);
4672
- } catch {
4673
- }
5698
+ const meta = parseMeta3(entry.metadata);
4674
5699
  if (meta.project) projectCounts.set(meta.project, (projectCounts.get(meta.project) ?? 0) + 1);
4675
5700
  if (meta.language) languageCounts.set(meta.language, (languageCounts.get(meta.language) ?? 0) + 1);
4676
5701
  }
@@ -4689,7 +5714,7 @@ async function cmdSessions(db, args) {
4689
5714
  break;
4690
5715
  }
4691
5716
  }
4692
- const startStr = fmtDate(session.startTime.toISOString());
5717
+ const startStr = formatTimestamp4(session.startTime.toISOString());
4693
5718
  const endStr = fmtTime(session.endTime.toISOString());
4694
5719
  const promptWord = session.entries.length === 1 ? "prompt" : "prompts";
4695
5720
  let header = `${C2.cyan}Session ${session.index}${C2.reset}`;
@@ -4707,11 +5732,7 @@ async function cmdSessions(db, args) {
4707
5732
  }
4708
5733
  process.stdout.write(header + "\n");
4709
5734
  for (const entry of session.entries) {
4710
- let meta = {};
4711
- try {
4712
- meta = JSON.parse(entry.metadata);
4713
- } catch {
4714
- }
5735
+ const meta = parseMeta3(entry.metadata);
4715
5736
  const idStr = `${C2.cyan}#${entry.id}${C2.reset}`;
4716
5737
  const toolStr = `${C2.yellow}${entry.tool.padEnd(8)}${C2.reset}`;
4717
5738
  const timeStr = `${C2.gray}${fmtTime(entry.timestamp)}${C2.reset}`;
@@ -4922,7 +5943,7 @@ ${C2.cyan}Cluster #${i + 1}${C2.reset} (${C2.yellow}${cluster.entryIds.length} p
4922
5943
  }
4923
5944
 
4924
5945
  // src/commands/analyze-reusability.ts
4925
- import fs8 from "fs";
5946
+ import fs9 from "fs";
4926
5947
 
4927
5948
  // src/analyzer/reusability.ts
4928
5949
  var ReusabilityAnalyzer = class _ReusabilityAnalyzer {
@@ -5107,7 +6128,7 @@ ${C2.bold}TOP 10 REMOVE (low score + duplicate):${C2.reset}`);
5107
6128
  console.log(` [${C2.red}${scoreStr}${C2.reset}] ${s.prompt.replace(/\n/g, " ").slice(0, 80)}...`);
5108
6129
  });
5109
6130
  if (exportCsv) {
5110
- fs8.writeFileSync(exportCsv, analyzer.toCSV(report));
6131
+ fs9.writeFileSync(exportCsv, analyzer.toCSV(report));
5111
6132
  console.log(`
5112
6133
  ${C2.green}\u2705 Report exported to ${exportCsv}${C2.reset}`);
5113
6134
  }
@@ -5115,7 +6136,7 @@ ${C2.green}\u2705 Report exported to ${exportCsv}${C2.reset}`);
5115
6136
  }
5116
6137
 
5117
6138
  // src/commands/cleanup-reusability.ts
5118
- import fs9 from "fs";
6139
+ import fs10 from "fs";
5119
6140
  async function cmdCleanupReusability(db, _cfg, args) {
5120
6141
  const { flags } = parseFlags(args);
5121
6142
  const threshold = flags["threshold"] ? parseFloat(flags["threshold"]) : 0.7;
@@ -5177,7 +6198,7 @@ Run with ${C2.bold}--force${C2.reset} or without ${C2.bold}--dry-run${C2.reset}
5177
6198
  process.stdout.write(`
5178
6199
  Continue and delete ${candidates.length} prompts? (y/N) `);
5179
6200
  const buffer = Buffer.alloc(16);
5180
- const bytesRead = fs9.readSync(0, buffer, 0, 16);
6201
+ const bytesRead = fs10.readSync(0, buffer, 0, 16, null);
5181
6202
  const answer = buffer.toString("utf8", 0, bytesRead).trim().toLowerCase();
5182
6203
  if (answer !== "y" && answer !== "yes") {
5183
6204
  console.log("Aborted.");
@@ -5292,8 +6313,40 @@ function formatMemoryEntry(m) {
5292
6313
  return lines.join("\n");
5293
6314
  }
5294
6315
 
6316
+ // src/commands/memory-migrate.ts
6317
+ function cmdMemoryMigrate(db) {
6318
+ const projects = db.getAllProjectsWithMemories();
6319
+ if (projects.length === 0) {
6320
+ console.log("No memories found. Nothing to migrate.");
6321
+ return;
6322
+ }
6323
+ let count = 0;
6324
+ for (const project of projects) {
6325
+ const memories = db.searchMemories(project, 1e3);
6326
+ const allInsights = /* @__PURE__ */ new Set();
6327
+ const allDecisions = /* @__PURE__ */ new Set();
6328
+ let lastSummary = "";
6329
+ let gitCtx;
6330
+ for (const mem of memories) {
6331
+ if (mem.summary) lastSummary = mem.summary;
6332
+ for (const i of mem.key_insights) allInsights.add(i);
6333
+ for (const d of mem.technical_decisions) allDecisions.add(d);
6334
+ if (mem.git_context_snapshot) gitCtx = mem.git_context_snapshot;
6335
+ }
6336
+ db.upsertProjectSummary({
6337
+ project,
6338
+ summary: lastSummary,
6339
+ key_insights: [...allInsights],
6340
+ technical_decisions: [...allDecisions],
6341
+ git_context_snapshot: gitCtx
6342
+ });
6343
+ count++;
6344
+ }
6345
+ console.log(`Migrated ${count} project(s) into project_summaries.`);
6346
+ }
6347
+
5295
6348
  // src/commands/wrap.ts
5296
- import path8 from "path";
6349
+ import path9 from "path";
5297
6350
  import os9 from "os";
5298
6351
  async function cmdWrap(dbPath, tool, args, cfg) {
5299
6352
  let debugLog;
@@ -5302,7 +6355,7 @@ async function cmdWrap(dbPath, tool, args, cfg) {
5302
6355
  const cleanArgs = [];
5303
6356
  for (let i = 0; i < args.length; i++) {
5304
6357
  if (args[i] === "--ph-debug") {
5305
- debugLog = path8.join(os9.homedir(), ".ph_debug.log");
6358
+ debugLog = path9.join(os9.homedir(), ".ph_debug.log");
5306
6359
  process.stderr.write(`ph: debug log \u2192 ${debugLog}
5307
6360
  `);
5308
6361
  } else if (args[i] === "--ph-tag" && i + 1 < args.length) {
@@ -5331,28 +6384,17 @@ async function cmdWrap(dbPath, tool, args, cfg) {
5331
6384
  const db = new PhDB(dbPath);
5332
6385
  if (interactive) {
5333
6386
  const workdir = process.cwd();
5334
- const { rootDir, projectName } = detectProject(workdir);
5335
- const language = detectLanguage(rootDir);
5336
6387
  const gitContext = captureGitContext(workdir);
5337
6388
  const onPrompt = (prompt, ts) => {
5338
- const metaObj = { $schema_version: 1 };
5339
- if (projectName) metaObj.project = projectName;
5340
- if (language) metaObj.language = language;
5341
- if (role) metaObj.role = role;
5342
- if (tags.length > 0) metaObj.tags = tags;
5343
- if (gitContext) metaObj.git_context = gitContext;
5344
- const metadata = Object.keys(metaObj).length > 0 ? JSON.stringify(metaObj) : "{}";
5345
- return db.insert({
6389
+ return db.insert(createCaptureRecord({
5346
6390
  timestamp: ts.toISOString(),
5347
6391
  tool,
5348
6392
  prompt,
5349
- response: "",
5350
6393
  args: prompt,
5351
6394
  workdir,
5352
- hostname: os9.hostname(),
5353
- exit_code: 0,
5354
- metadata
5355
- });
6395
+ gitContext,
6396
+ metadata: { ...role ? { role } : {}, ...tags.length > 0 ? { tags } : {} }
6397
+ }));
5356
6398
  };
5357
6399
  const onResponse = (id, response) => {
5358
6400
  if (!response.trim()) return;
@@ -5392,41 +6434,14 @@ async function cmdChat(dbPath, cfg, args) {
5392
6434
  process.exit(1);
5393
6435
  }
5394
6436
  const db = new PhDB(dbPath);
5395
- const memories = db.searchMemories(project, 3);
5396
- const recentPrompts = db.getProjectMemory(project, 5);
5397
- const contextParts = [];
5398
- if (memories.length > 0) {
5399
- contextParts.push(`## Project Knowledge: ${project}`);
5400
- for (const mem of memories) {
5401
- if (mem.summary) contextParts.push(`
5402
- ${mem.summary}`);
5403
- if (mem.key_insights.length > 0) {
5404
- contextParts.push("\nKey Insights:");
5405
- for (const i of mem.key_insights) contextParts.push(` - ${i}`);
5406
- }
5407
- if (mem.technical_decisions.length > 0) {
5408
- contextParts.push("\nTechnical Decisions:");
5409
- for (const d of mem.technical_decisions) contextParts.push(` - ${d}`);
5410
- }
5411
- }
5412
- }
5413
- if (recentPrompts.length > 0) {
5414
- contextParts.push(`
5415
- ---
5416
- ## Recent Context`);
5417
- for (const p of recentPrompts) {
5418
- let meta = {};
5419
- try {
5420
- meta = JSON.parse(p.metadata);
5421
- } catch {
5422
- }
5423
- const summary = meta.summary || p.prompt.slice(0, 100).replace(/\n/g, " ");
5424
- contextParts.push(`
5425
- - #${p.id}: ${summary}`);
5426
- }
5427
- }
6437
+ const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
6438
+ const model = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
6439
+ const context = await getProjectContext(db, { project, limit: 5, memoryLimit: 3 }, async (query) => {
6440
+ const [vector] = await getEmbeddings([query], ollamaUrl, model, 1);
6441
+ return vector;
6442
+ });
5428
6443
  db.close();
5429
- const contextStr = contextParts.join("\n");
6444
+ const contextStr = formatProjectContext(context);
5430
6445
  const fullPrompt = `Context from project "${project}":
5431
6446
 
5432
6447
  ${contextStr}
@@ -5455,14 +6470,16 @@ USAGE:
5455
6470
  ph analyze-reusability [options] Analyze prompt reusability
5456
6471
  ph star <id> Toggle star on a prompt
5457
6472
  ph export <id> [--format txt|json|md] Export a single prompt
5458
- ph import gemini [--dry-run] [--analyze] [--filter] Import from Gemini CLI sessions
5459
- ph import claude [--dry-run] [--analyze] [--filter] Import from Claude CLI sessions
6473
+ ph import <source> [--dry-run] [--analyze] [--filter] [--file <path>]
6474
+ Import from Gemini, Claude, OpenCode, or Codex history
5460
6475
  ph analyze [--limit n] [--force] [--prune] [--dry-run] Analyze prompts with LLM
5461
6476
  ph mcp Start MCP server (Stdio)
5462
6477
  ph server [--port 3001] Start HTTP REST server for remote sync
5463
6478
  ph timeline [project] Show full project history with prompts and memories
6479
+ ph memory-migrate Merge existing memories into project_summaries
5464
6480
  ph remote push|pull|status Sync prompts with remote ph server
5465
6481
  ph cleanup [--dry-run] [--min-length N] [--min-score N] Remove useless prompts
6482
+ ph cleanup --retention [--dry-run] Archive prompts per retention policy
5466
6483
  ph cleanup-reusability [--dry-run] [--threshold 0.7] [--force] Cleanup based on reusability
5467
6484
  ph embed-all Generate embeddings for all prompts
5468
6485
  ph log --tool <name> --prompt <text> [--response <text>] Log a prompt+response directly
@@ -5476,7 +6493,7 @@ WRAP FLAGS (placed before the tool name):
5476
6493
 
5477
6494
  SEARCH OPTIONS:
5478
6495
  -i, --interactive Open results in interactive TUI browser
5479
- --tool <name> Filter by tool name (claude, gemini, \u2026)
6496
+ --tool <name> Filter by tool name (claude, gemini, opencode, codex, \u2026)
5480
6497
  --project <name> Filter by project name
5481
6498
  --language <lang> Filter by language (go, typescript, python, \u2026)
5482
6499
  --role <role> Filter by role (debug, refactor, explain, \u2026)
@@ -5486,6 +6503,7 @@ SEARCH OPTIONS:
5486
6503
  --min-relevance <n> Filter by min relevance (0-10)
5487
6504
  --top Show only top quality prompts (quality >= 8)
5488
6505
  --semantic Use semantic search (requires embeddings)
6506
+ --archive Search archived prompts
5489
6507
  --since YYYY-MM-DD
5490
6508
  --until YYYY-MM-DD
5491
6509
  --limit <n> Max results (default 50)
@@ -5503,6 +6521,7 @@ SESSIONS OPTIONS:
5503
6521
  --limit <n> Max sessions to show (default 20)
5504
6522
  --min-size <n> Minimum prompts per session (default 1)
5505
6523
  --no-cohesion Skip semantic cohesion computation
6524
+ --export <n> Export session N as markdown
5506
6525
 
5507
6526
  CLUSTER OPTIONS:
5508
6527
  -k <number> Number of clusters (default 5)
@@ -5539,6 +6558,31 @@ EXAMPLES:
5539
6558
  ph config set ollama-model llama3.2:latest
5540
6559
  ph ollama-models List models available on the local Ollama instance
5541
6560
  `;
6561
+ async function openBrowser(db, opts = {}) {
6562
+ let pendingRerun = null;
6563
+ process.stdout.write("\x1B[?1049h");
6564
+ try {
6565
+ const { waitUntilExit } = render(
6566
+ React8.createElement(BrowseApp, {
6567
+ db,
6568
+ ...opts,
6569
+ onRerun: (tool, prompt) => {
6570
+ pendingRerun = { tool, prompt };
6571
+ }
6572
+ })
6573
+ );
6574
+ await waitUntilExit();
6575
+ } finally {
6576
+ process.stdout.write("\x1B[?1049l");
6577
+ }
6578
+ return pendingRerun;
6579
+ }
6580
+ function runRerun(rerun) {
6581
+ if (!rerun) return;
6582
+ const realBin = resolveRealBinary(rerun.tool);
6583
+ const child = spawnSync2(realBin, [rerun.prompt], { stdio: "inherit" });
6584
+ process.exit(child.status ?? 0);
6585
+ }
5542
6586
  async function main() {
5543
6587
  const argv = process.argv.slice(2);
5544
6588
  const cfg = load();
@@ -5549,28 +6593,9 @@ async function main() {
5549
6593
  if (argv.length === 0) {
5550
6594
  if (isTerminal()) {
5551
6595
  const db = new PhDB(dbPath);
5552
- process.stdout.write("\x1B[?1049h");
5553
- let pendingRerun = null;
5554
- try {
5555
- const { waitUntilExit } = render(
5556
- React7.createElement(BrowseApp, {
5557
- db,
5558
- onRerun: (tool, prompt) => {
5559
- pendingRerun = { tool, prompt };
5560
- }
5561
- })
5562
- );
5563
- await waitUntilExit();
5564
- } finally {
5565
- process.stdout.write("\x1B[?1049l");
5566
- db.close();
5567
- }
5568
- if (pendingRerun) {
5569
- const { tool, prompt } = pendingRerun;
5570
- const realBin = resolveRealBinary(tool);
5571
- const child = spawnSync2(realBin, [prompt], { stdio: "inherit" });
5572
- process.exit(child.status ?? 0);
5573
- }
6596
+ const rerun = await openBrowser(db);
6597
+ db.close();
6598
+ runRerun(rerun);
5574
6599
  process.exit(0);
5575
6600
  } else {
5576
6601
  process.stdout.write(USAGE);
@@ -5599,39 +6624,21 @@ async function main() {
5599
6624
  const query = positional.join(" ");
5600
6625
  if (flags["i"] || flags["interactive"]) {
5601
6626
  const db2 = new PhDB(dbPath);
5602
- process.stdout.write("\x1B[?1049h");
5603
- let pendingRerun = null;
5604
- try {
5605
- const { waitUntilExit } = render(
5606
- React7.createElement(BrowseApp, {
5607
- db: db2,
5608
- initialTextFilter: query,
5609
- initialFilters: {
5610
- tool: flags["tool"],
5611
- project: flags["project"],
5612
- language: flags["language"],
5613
- role: flags["role"],
5614
- tag: flags["tag"],
5615
- starred: Boolean(flags["starred"]),
5616
- minQuality: flags["top"] ? 8 : flags["min-quality"] ? Number(flags["min-quality"]) : void 0,
5617
- minRelevance: flags["min-relevance"] ? Number(flags["min-relevance"]) : void 0
5618
- },
5619
- onRerun: (tool, prompt) => {
5620
- pendingRerun = { tool, prompt };
5621
- }
5622
- })
5623
- );
5624
- await waitUntilExit();
5625
- } finally {
5626
- process.stdout.write("\x1B[?1049l");
5627
- db2.close();
5628
- }
5629
- if (pendingRerun) {
5630
- const { tool, prompt } = pendingRerun;
5631
- const realBin = resolveRealBinary(tool);
5632
- const child = spawnSync2(realBin, [prompt], { stdio: "inherit" });
5633
- process.exit(child.status ?? 0);
5634
- }
6627
+ const rerun = await openBrowser(db2, {
6628
+ initialTextFilter: query,
6629
+ initialFilters: {
6630
+ tool: flags["tool"],
6631
+ project: flags["project"],
6632
+ language: flags["language"],
6633
+ role: flags["role"],
6634
+ tag: flags["tag"],
6635
+ starred: Boolean(flags["starred"]),
6636
+ minQuality: flags["top"] ? 8 : flags["min-quality"] ? Number(flags["min-quality"]) : void 0,
6637
+ minRelevance: flags["min-relevance"] ? Number(flags["min-relevance"]) : void 0
6638
+ }
6639
+ });
6640
+ db2.close();
6641
+ runRerun(rerun);
5635
6642
  break;
5636
6643
  }
5637
6644
  const db = new PhDB(dbPath);
@@ -5705,6 +6712,12 @@ async function main() {
5705
6712
  db.close();
5706
6713
  break;
5707
6714
  }
6715
+ case "memory-migrate": {
6716
+ const db = new PhDB(dbPath);
6717
+ cmdMemoryMigrate(db);
6718
+ db.close();
6719
+ break;
6720
+ }
5708
6721
  case "mcp": {
5709
6722
  await runMCPServer();
5710
6723
  break;
@@ -5751,28 +6764,9 @@ async function main() {
5751
6764
  }
5752
6765
  case "browse": {
5753
6766
  const db = new PhDB(dbPath);
5754
- process.stdout.write("\x1B[?1049h");
5755
- let pendingRerun = null;
5756
- try {
5757
- const { waitUntilExit } = render(
5758
- React7.createElement(BrowseApp, {
5759
- db,
5760
- onRerun: (tool, prompt) => {
5761
- pendingRerun = { tool, prompt };
5762
- }
5763
- })
5764
- );
5765
- await waitUntilExit();
5766
- } finally {
5767
- process.stdout.write("\x1B[?1049l");
5768
- db.close();
5769
- }
5770
- if (pendingRerun) {
5771
- const { tool, prompt } = pendingRerun;
5772
- const realBin = resolveRealBinary(tool);
5773
- const child = spawnSync2(realBin, [prompt], { stdio: "inherit" });
5774
- process.exit(child.status ?? 0);
5775
- }
6767
+ const rerun = await openBrowser(db);
6768
+ db.close();
6769
+ runRerun(rerun);
5776
6770
  break;
5777
6771
  }
5778
6772
  case "chat": {