@gi4nks/ph 1.4.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 +7 -0
  2. package/README.md +147 -58
  3. package/dist/cli.js +2292 -1021
  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 +352 -231
  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,43 @@ 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
+ }
689
+ getAllPromptsByProject(project) {
690
+ return this.db.prepare(`
691
+ SELECT * FROM prompts
692
+ WHERE json_extract(metadata, '$.project') = ?
693
+ ORDER BY timestamp ASC
694
+ `).all(project);
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
+ }
710
+ getAllMemoriesByProject(project) {
711
+ const rows = this.db.prepare("SELECT * FROM memories WHERE project = ? ORDER BY created_at ASC").all(project);
712
+ return rows.map((r) => this.hydrateMemory(r));
713
+ }
488
714
  close() {
489
715
  this.db.close();
490
716
  }
@@ -504,6 +730,34 @@ function cosineSimilarity(a, b) {
504
730
  // src/runner/inline.ts
505
731
  import { spawn, execFileSync as execFileSync2 } from "child_process";
506
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
507
761
  import os3 from "os";
508
762
 
509
763
  // src/runner/project.ts
@@ -550,30 +804,119 @@ function detectLanguage(rootDir) {
550
804
  return "";
551
805
  }
552
806
 
553
- // src/runner/git-context.ts
554
- import { execFileSync } from "child_process";
555
- function captureGitContext(workdir) {
556
- try {
557
- const branch = execFileSync("git", ["-C", workdir, "rev-parse", "--abbrev-ref", "HEAD"], {
558
- encoding: "utf-8",
559
- stdio: ["ignore", "pipe", "pipe"]
560
- }).trim();
561
- const filesRaw = execFileSync("git", ["-C", workdir, "diff", "--name-only", "HEAD"], {
562
- encoding: "utf-8",
563
- stdio: ["ignore", "pipe", "pipe"]
564
- }).trim();
565
- const files = filesRaw ? filesRaw.split("\n").filter(Boolean) : [];
566
- let diff = execFileSync("git", ["-C", workdir, "diff", "HEAD"], {
567
- encoding: "utf-8",
568
- stdio: ["ignore", "pipe", "pipe"]
569
- });
570
- if (diff.length > 4e3) {
571
- 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();
572
821
  }
573
- return { branch, files, diff };
574
- } catch {
575
- return null;
576
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
+ };
577
920
  }
578
921
 
579
922
  // src/runner/inline.ts
@@ -602,26 +945,15 @@ function resolveRealBinary(tool) {
602
945
  }
603
946
  async function runInline(realBin, args, db, tool, tags, role, onInserted) {
604
947
  const workdir = process.cwd();
605
- const { rootDir, projectName } = detectProject(workdir);
606
- const language = detectLanguage(rootDir);
607
948
  const gitContext = captureGitContext(workdir);
608
- const metaObj = {};
609
- if (projectName) metaObj.project = projectName;
610
- if (language) metaObj.language = language;
611
- if (role) metaObj.role = role;
612
- if (tags.length > 0) metaObj.tags = tags;
613
- if (gitContext) metaObj.git_context = gitContext;
614
- const metadata = Object.keys(metaObj).length > 0 ? JSON.stringify(metaObj) : "{}";
615
- const id = db.insert({
616
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
949
+ const id = db.insert(createCaptureRecord({
617
950
  tool,
618
951
  prompt: extractPrompt(args),
619
952
  args: args.join(" "),
620
953
  workdir,
621
- hostname: os3.hostname(),
622
- exit_code: 0,
623
- metadata
624
- });
954
+ gitContext,
955
+ metadata: { ...role ? { role } : {}, ...tags.length > 0 ? { tags } : {} }
956
+ }));
625
957
  if (onInserted) onInserted(id);
626
958
  const child = spawn(realBin, args, { stdio: "inherit" });
627
959
  child.on("error", (err) => {
@@ -643,7 +975,7 @@ function cleanLine(bytes) {
643
975
  const s = Buffer.from(bytes).toString("utf-8");
644
976
  return s.trim();
645
977
  }
646
- function stripAnsi(text) {
978
+ function stripPtyControlSequences(text) {
647
979
  return text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
648
980
  }
649
981
  async function runPTY(binary, args, onPrompt, onResponse, debugLog) {
@@ -696,7 +1028,7 @@ async function runPTY(binary, args, onPrompt, onResponse, debugLog) {
696
1028
  log(`ENTER \u2192 prompt: ${JSON.stringify(prompt)}`);
697
1029
  if (prompt) {
698
1030
  if (currentPromptId !== null) {
699
- onResponse(currentPromptId, stripAnsi(responseBuffer));
1031
+ onResponse(currentPromptId, stripPtyControlSequences(responseBuffer));
700
1032
  }
701
1033
  currentPromptId = onPrompt(prompt, /* @__PURE__ */ new Date());
702
1034
  responseBuffer = "";
@@ -740,7 +1072,7 @@ async function runPTY(binary, args, onPrompt, onResponse, debugLog) {
740
1072
  return new Promise((resolve) => {
741
1073
  ptyProcess.onExit(({ exitCode }) => {
742
1074
  if (currentPromptId !== null) {
743
- onResponse(currentPromptId, stripAnsi(responseBuffer));
1075
+ onResponse(currentPromptId, stripPtyControlSequences(responseBuffer));
744
1076
  }
745
1077
  process.stdin.removeListener("data", onData);
746
1078
  process.stdout.removeListener("resize", onResize);
@@ -790,26 +1122,169 @@ async function getEmbeddings(texts, ollamaUrl, model, batchSize = 20) {
790
1122
  return results;
791
1123
  }
792
1124
 
793
- // src/mcp/server.ts
794
- import { z } from "zod";
795
- async function runMCPServer() {
796
- const cfg = load();
797
- const dbPath = process.env.PH_DB ?? cfg.dbPath ?? defaultPath();
798
- const db = new PhDB(dbPath);
799
- const server = new Server(
800
- {
801
- name: "ph-memory",
802
- version: "1.0.0"
803
- },
804
- {
805
- capabilities: {
806
- 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
+ }
1180
+ }
1181
+ }
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: {}
807
1234
  }
808
1235
  }
809
1236
  );
810
1237
  server.setRequestHandler(ListToolsRequestSchema, async () => {
811
1238
  return {
812
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
+ },
813
1288
  {
814
1289
  name: "search_project_memory",
815
1290
  description: "Search for past interactions and technical decisions in a specific project using semantic search.",
@@ -886,6 +1361,33 @@ async function runMCPServer() {
886
1361
  },
887
1362
  required: ["query"]
888
1363
  }
1364
+ },
1365
+ {
1366
+ name: "get_project_timeline",
1367
+ description: "Get full chronological timeline of a project: all prompts, analysis summaries, key insights, and technical decisions in sequence.",
1368
+ inputSchema: {
1369
+ type: "object",
1370
+ properties: {
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 }
1375
+ },
1376
+ required: ["project"]
1377
+ }
1378
+ },
1379
+ {
1380
+ name: "check_project_knowledge",
1381
+ description: "Before implementing a feature, check if it already exists in project history. Searches memories and prompts for previous implementations, discussions, and technical decisions related to a task.",
1382
+ inputSchema: {
1383
+ type: "object",
1384
+ properties: {
1385
+ project: { type: "string", description: "The project name" },
1386
+ task: { type: "string", description: "Description of the feature or task to check" },
1387
+ limit: { type: "number", description: "Max results per search (default 5)", default: 5 }
1388
+ },
1389
+ required: ["project", "task"]
1390
+ }
889
1391
  }
890
1392
  ]
891
1393
  };
@@ -893,6 +1395,115 @@ async function runMCPServer() {
893
1395
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
894
1396
  const { name, arguments: args } = request.params;
895
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
+ }
896
1507
  if (name === "search_project_memory") {
897
1508
  const { query, project, limit = 5 } = z.object({
898
1509
  query: z.string(),
@@ -905,15 +1516,30 @@ async function runMCPServer() {
905
1516
  if (!queryVec) {
906
1517
  throw new Error("Failed to generate embedding for query");
907
1518
  }
908
- const results = db.searchSemantic(queryVec, limit * 2);
909
- const filtered = results.filter((e) => {
910
- try {
911
- const meta = JSON.parse(e.metadata);
912
- return meta.project === project;
913
- } catch {
914
- 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") }] };
915
1540
  }
916
- }).slice(0, limit);
1541
+ return { content: [{ type: "text", text: `No relevant memory found for project "${project}".` }] };
1542
+ }
917
1543
  return {
918
1544
  content: [{ type: "text", text: formatResultsAsMarkdown(filtered) }]
919
1545
  };
@@ -923,59 +1549,39 @@ async function runMCPServer() {
923
1549
  project: z.string(),
924
1550
  limit: z.number().optional()
925
1551
  }).parse(args);
926
- const memories = db.searchMemories(project, 3);
927
- const prompts = db.getProjectMemory(project, limit);
928
- const parts = [];
929
- if (memories.length > 0) {
930
- parts.push("## Project Knowledge\n");
931
- for (const mem of memories) {
932
- if (mem.summary) parts.push(`${mem.summary}
933
- `);
934
- if (mem.key_insights.length > 0) {
935
- parts.push("Key Insights:");
936
- for (const i of mem.key_insights) parts.push(` - ${i}`);
937
- parts.push("");
938
- }
939
- if (mem.technical_decisions.length > 0) {
940
- parts.push("Technical Decisions:");
941
- for (const d of mem.technical_decisions) parts.push(` - ${d}`);
942
- parts.push("");
943
- }
944
- }
945
- }
946
- if (prompts.length > 0) {
947
- if (parts.length > 0) parts.push("---\n");
948
- parts.push("## Recent Interactions\n");
949
- parts.push(formatResultsAsMarkdown(prompts));
950
- }
1552
+ const context = await getProjectContext(db, { project, limit, memoryLimit: 3 });
951
1553
  return {
952
- 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
+ }) }]
953
1559
  };
954
1560
  }
955
1561
  if (name === "get_project_summary") {
956
1562
  const { project } = z.object({
957
1563
  project: z.string()
958
1564
  }).parse(args);
959
- const memories = db.searchMemories(project, 5);
960
- if (memories.length === 0) {
1565
+ const summary = db.getProjectSummary(project);
1566
+ if (!summary) {
961
1567
  return {
962
1568
  content: [{ type: "text", text: `No accumulated knowledge for project "${project}". Run "ph analyze" to generate insights.` }]
963
1569
  };
964
1570
  }
965
1571
  const parts = [];
966
- for (const mem of memories) {
967
- parts.push(`## ${mem.summary || "Project Memory"}
1572
+ parts.push(`## ${summary.summary || "Project Memory"}
968
1573
  `);
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
- }
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("");
979
1585
  }
980
1586
  return {
981
1587
  content: [{ type: "text", text: parts.join("\n") }]
@@ -1032,17 +1638,153 @@ async function runMCPServer() {
1032
1638
  if (!queryVec) {
1033
1639
  throw new Error("Failed to generate embedding for query");
1034
1640
  }
1035
- const results = db.searchSemantic(queryVec, limit * 2);
1036
- const filtered = project ? results.filter((e) => {
1641
+ const filtered = db.searchSemantic(queryVec, limit, project);
1642
+ return {
1643
+ content: [{ type: "text", text: formatPromptList(filtered, query) }]
1644
+ };
1645
+ }
1646
+ if (name === "get_project_timeline") {
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()
1652
+ }).parse(args);
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) {
1666
+ return {
1667
+ content: [{ type: "text", text: `No history found for project "${project}"${since ? ` since ${since}` : ""}${until ? ` until ${until}` : ""}.` }]
1668
+ };
1669
+ }
1670
+ const events = [];
1671
+ for (const p of allPrompts.slice(0, limit)) {
1672
+ let meta = {};
1037
1673
  try {
1038
- const meta = JSON.parse(e.metadata);
1039
- return meta.project === project;
1674
+ meta = JSON.parse(p.metadata);
1040
1675
  } catch {
1041
- return false;
1042
1676
  }
1043
- }).slice(0, limit) : results.slice(0, limit);
1677
+ events.push({
1678
+ timestamp: p.timestamp,
1679
+ text: [
1680
+ `### #${p.id} \u2014 ${p.tool}`,
1681
+ meta.role ? `Role: ${meta.role}` : "",
1682
+ meta.title ? `Title: ${meta.title}` : "",
1683
+ meta.tags?.length ? `Tags: ${meta.tags.join(", ")}` : "",
1684
+ meta.summary ? `Summary: ${meta.summary}` : "",
1685
+ `Prompt: ${p.prompt.slice(0, 500)}${p.prompt.length > 500 ? "..." : ""}`,
1686
+ p.response ? `Response: ${p.response.slice(0, 300)}${p.response.length > 300 ? "..." : ""}` : "",
1687
+ meta.key_insights?.length ? `Insights:
1688
+ ${meta.key_insights.map((i) => `- ${i}`).join("\n")}` : ""
1689
+ ].filter(Boolean).join("\n")
1690
+ });
1691
+ }
1692
+ for (const m of memories) {
1693
+ events.push({
1694
+ timestamp: m.created_at,
1695
+ text: [
1696
+ `\u{1F4CC} Memory #${m.id}`,
1697
+ m.prompt_ids.length > 0 ? `From prompts: #${m.prompt_ids.join(", #")}` : "",
1698
+ m.summary ? `Summary: ${m.summary}` : "",
1699
+ m.key_insights.length > 0 ? `Key Insights:
1700
+ ${m.key_insights.map((i) => `- ${i}`).join("\n")}` : "",
1701
+ m.technical_decisions.length > 0 ? `Technical Decisions:
1702
+ ${m.technical_decisions.map((d) => `- ${d}`).join("\n")}` : ""
1703
+ ].filter(Boolean).join("\n")
1704
+ });
1705
+ }
1706
+ events.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
1707
+ const range = since || until ? ` (${since || "\u2026"} \u2192 ${until || "now"})` : "";
1708
+ const header = `# Timeline: ${project}${range}
1709
+
1710
+ ${allPrompts.length} prompts \xB7 ${memories.length} memory entries
1711
+
1712
+ `;
1713
+ const body = events.map((e) => {
1714
+ const date = new Date(e.timestamp).toLocaleDateString("en-CA");
1715
+ const time = new Date(e.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
1716
+ return `## ${date} ${time}
1717
+
1718
+ ${e.text}
1719
+ `;
1720
+ }).join("---\n");
1044
1721
  return {
1045
- content: [{ type: "text", text: formatPromptList(filtered, query) }]
1722
+ content: [{ type: "text", text: header + body }]
1723
+ };
1724
+ }
1725
+ if (name === "check_project_knowledge") {
1726
+ const { project, task, limit = 5 } = z.object({
1727
+ project: z.string(),
1728
+ task: z.string(),
1729
+ limit: z.number().optional()
1730
+ }).parse(args);
1731
+ const parts = [];
1732
+ parts.push(`# Knowledge Check: "${task}" in ${project}
1733
+ `);
1734
+ const memories = db.searchMemories(project, limit);
1735
+ const relevantMemories = memories.filter(
1736
+ (m) => m.summary.toLowerCase().includes(task.toLowerCase()) || m.key_insights.some((k) => k.toLowerCase().includes(task.toLowerCase())) || m.technical_decisions.some((d) => d.toLowerCase().includes(task.toLowerCase()))
1737
+ );
1738
+ if (relevantMemories.length > 0) {
1739
+ parts.push(`## Found in Project Memories
1740
+ `);
1741
+ for (const m of relevantMemories) {
1742
+ parts.push(`### Memory #${m.id}`);
1743
+ if (m.summary) parts.push(`Summary: ${m.summary}`);
1744
+ if (m.key_insights.length > 0) {
1745
+ parts.push(`Key Insights:
1746
+ ${m.key_insights.map((i) => `- ${i}`).join("\n")}`);
1747
+ }
1748
+ if (m.technical_decisions.length > 0) {
1749
+ parts.push(`Technical Decisions:
1750
+ ${m.technical_decisions.map((d) => `- ${d}`).join("\n")}`);
1751
+ }
1752
+ parts.push("");
1753
+ }
1754
+ }
1755
+ const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
1756
+ const embedModel = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
1757
+ try {
1758
+ const [queryVec] = await getEmbeddings([`${project}: ${task}`], ollamaUrl, embedModel, 1);
1759
+ if (queryVec) {
1760
+ const projectPrompts = db.searchSemantic(queryVec, limit, project);
1761
+ if (projectPrompts.length > 0) {
1762
+ parts.push(`## Related Prompts
1763
+ `);
1764
+ for (const p of projectPrompts) {
1765
+ let meta = {};
1766
+ try {
1767
+ meta = JSON.parse(p.metadata);
1768
+ } catch {
1769
+ }
1770
+ parts.push(`### #${p.id} \u2014 ${p.tool} (${new Date(p.timestamp).toLocaleDateString()})`);
1771
+ if (meta.role) parts.push(`Role: ${meta.role}`);
1772
+ if (meta.summary) parts.push(`Summary: ${meta.summary}`);
1773
+ parts.push(`Prompt: ${p.prompt.slice(0, 400)}${p.prompt.length > 400 ? "..." : ""}`);
1774
+ parts.push("");
1775
+ }
1776
+ }
1777
+ }
1778
+ } catch {
1779
+ parts.push("(Semantic search unavailable \u2014 Ollama may not be running)\n");
1780
+ }
1781
+ if (relevantMemories.length === 0 && !parts.some((p) => p.startsWith("## Related Prompts"))) {
1782
+ parts.push("No existing knowledge found for this task. This appears to be new work.\n");
1783
+ }
1784
+ parts.push("---\n");
1785
+ parts.push("**Recommendation**: Review the above before implementing. If a previous implementation exists, consider reusing or adapting it.\n");
1786
+ return {
1787
+ content: [{ type: "text", text: parts.join("\n") }]
1046
1788
  };
1047
1789
  }
1048
1790
  throw new Error(`Unknown tool: ${name}`);
@@ -1053,6 +1795,13 @@ async function runMCPServer() {
1053
1795
  };
1054
1796
  }
1055
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);
1056
1805
  const transport = new StdioServerTransport();
1057
1806
  await server.connect(transport);
1058
1807
  console.error("ph MCP server running on stdio");
@@ -1154,12 +1903,31 @@ function formatSinglePrompt(e) {
1154
1903
 
1155
1904
  // src/server/index.ts
1156
1905
  import http from "http";
1906
+ import { URL } from "url";
1907
+
1908
+ // src/utils/syncHash.ts
1157
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";
1158
1917
  async function runServer(port = 3001, host = "0.0.0.0") {
1159
1918
  const cfg = load();
1160
1919
  const dbPath = process.env.PH_DB ?? cfg.dbPath ?? defaultPath();
1161
1920
  const db = new PhDB(dbPath);
1162
- 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) => {
1163
1931
  res.setHeader("Access-Control-Allow-Origin", "*");
1164
1932
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1165
1933
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
@@ -1168,28 +1936,34 @@ async function runServer(port = 3001, host = "0.0.0.0") {
1168
1936
  res.end();
1169
1937
  return;
1170
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
+ }
1171
1944
  try {
1172
1945
  const body = await readBody(req);
1173
1946
  await route(req, res, body, db, cfg);
1174
1947
  } catch (err) {
1175
1948
  json(res, 500, { error: err.message });
1176
1949
  }
1177
- });
1178
- return new Promise((resolve) => {
1179
- server.listen(port, host, () => {
1180
- console.error(`ph server listening on http://${host}:${port}`);
1181
- resolve();
1182
- });
1183
- });
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);
1184
1958
  }
1185
1959
  async function route(req, res, body, db, cfg) {
1186
1960
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
1187
- const path9 = url.pathname;
1961
+ const path10 = url.pathname;
1188
1962
  const method = req.method || "GET";
1189
- if (method === "GET" && path9 === "/health") {
1190
- 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 });
1191
1965
  }
1192
- if (method === "POST" && path9 === "/api/prompts/search") {
1966
+ if (method === "POST" && path10 === "/api/prompts/search") {
1193
1967
  const { query, tool, project, role, tag, since, until, limit = 10 } = JSON.parse(body);
1194
1968
  const results = db.search({
1195
1969
  query,
@@ -1203,31 +1977,23 @@ async function route(req, res, body, db, cfg) {
1203
1977
  });
1204
1978
  return json(res, 200, { prompts: results });
1205
1979
  }
1206
- if (method === "POST" && path9 === "/api/prompts/by-id") {
1980
+ if (method === "POST" && path10 === "/api/prompts/by-id") {
1207
1981
  const { id } = JSON.parse(body);
1208
1982
  const entry = db.getById(id);
1209
1983
  if (!entry) return json(res, 404, { error: `Prompt #${id} not found` });
1210
1984
  return json(res, 200, { prompt: entry });
1211
1985
  }
1212
- if (method === "POST" && path9 === "/api/prompts/semantic") {
1986
+ if (method === "POST" && path10 === "/api/prompts/semantic") {
1213
1987
  const { query, project, limit = 5 } = JSON.parse(body);
1214
1988
  if (!query) return json(res, 400, { error: "query is required" });
1215
1989
  const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
1216
1990
  const model = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
1217
1991
  const [queryVec] = await getEmbeddings([query], ollamaUrl, model, 1);
1218
1992
  if (!queryVec) return json(res, 500, { error: "Failed to generate embedding" });
1219
- const results = db.searchSemantic(queryVec, limit * 2);
1220
- const filtered = project ? results.filter((e) => {
1221
- try {
1222
- const meta = JSON.parse(e.metadata);
1223
- return meta.project === project;
1224
- } catch {
1225
- return false;
1226
- }
1227
- }).slice(0, limit) : results.slice(0, limit);
1993
+ const filtered = db.searchSemantic(queryVec, limit, project);
1228
1994
  return json(res, 200, { prompts: filtered });
1229
1995
  }
1230
- if (method === "POST" && path9 === "/api/sync/push") {
1996
+ if (method === "POST" && path10 === "/api/sync/push") {
1231
1997
  const { prompts } = JSON.parse(body);
1232
1998
  if (!Array.isArray(prompts)) return json(res, 400, { error: "prompts must be an array" });
1233
1999
  let imported = 0;
@@ -1235,8 +2001,8 @@ async function route(req, res, body, db, cfg) {
1235
2001
  const errors = [];
1236
2002
  for (const p of prompts) {
1237
2003
  try {
1238
- const hash = sha256(`${p.tool}|${p.prompt}|${p.response}`);
1239
- 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);
1240
2006
  if (existing) {
1241
2007
  skipped++;
1242
2008
  continue;
@@ -1260,35 +2026,28 @@ async function route(req, res, body, db, cfg) {
1260
2026
  }
1261
2027
  return json(res, 200, { imported, skipped, errors });
1262
2028
  }
1263
- if (method === "POST" && path9 === "/api/sync/pull") {
2029
+ if (method === "POST" && path10 === "/api/sync/pull") {
1264
2030
  const { since } = JSON.parse(body);
1265
2031
  const limit = 1e4;
1266
- let results;
1267
- if (since) {
1268
- results = db.db.prepare("SELECT * FROM prompts WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?").all(since, limit);
1269
- } else {
1270
- results = db.db.prepare("SELECT * FROM prompts ORDER BY timestamp ASC LIMIT ?").all(limit);
1271
- }
2032
+ const results = since ? db.getPromptsSince(since, limit) : db.getAllPrompts(limit);
1272
2033
  return json(res, 200, { prompts: results, hasMore: results.length >= limit });
1273
2034
  }
1274
- if (method === "POST" && path9 === "/api/memories/search") {
2035
+ if (method === "POST" && path10 === "/api/memories/search") {
1275
2036
  const { project, limit = 10 } = JSON.parse(body);
1276
2037
  const memories = project ? db.searchMemories(project, limit) : [];
1277
2038
  return json(res, 200, { memories });
1278
2039
  }
1279
- if (method === "POST" && path9 === "/api/memories/summary") {
2040
+ if (method === "POST" && path10 === "/api/memories/summary") {
1280
2041
  const { project } = JSON.parse(body);
1281
2042
  if (!project) return json(res, 400, { error: "project is required" });
1282
2043
  const memories = db.searchMemories(project, 5);
1283
2044
  return json(res, 200, { memories });
1284
2045
  }
1285
- if (method === "GET" && path9 === "/api/stats") {
1286
- const total = db.db.prepare("SELECT count(*) as c FROM prompts").get().c;
1287
- const totalMemories = db.db.prepare("SELECT count(*) as c FROM memories").get().c;
1288
- 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();
1289
2048
  return json(res, 200, { total, totalMemories, byTool });
1290
2049
  }
1291
- json(res, 404, { error: `Not found: ${method} ${path9}` });
2050
+ json(res, 404, { error: `Not found: ${method} ${path10}` });
1292
2051
  }
1293
2052
  function json(res, status2, data) {
1294
2053
  res.writeHead(status2, { "Content-Type": "application/json" });
@@ -1303,12 +2062,8 @@ function readBody(req) {
1303
2062
  req.on("error", reject);
1304
2063
  });
1305
2064
  }
1306
- function sha256(s) {
1307
- return createHash2("sha256").update(s).digest("hex");
1308
- }
1309
2065
 
1310
2066
  // src/commands/remote.ts
1311
- import { createHash as createHash3 } from "crypto";
1312
2067
  function remoteUrl(cfg) {
1313
2068
  return process.env.PH_REMOTE_URL || cfg.remoteUrl || "";
1314
2069
  }
@@ -1411,7 +2166,7 @@ async function pull(dbPath, cfg, url) {
1411
2166
  let imported = 0;
1412
2167
  let skipped = 0;
1413
2168
  for (const p of result.prompts) {
1414
- const hash = sha2562(`${p.tool}|${p.prompt}|${p.response}`);
2169
+ const hash = syncHash(p);
1415
2170
  const existing = db.getPromptBySyncHash(hash);
1416
2171
  if (existing) {
1417
2172
  skipped++;
@@ -1449,7 +2204,7 @@ async function status(dbPath, cfg, url) {
1449
2204
  const localTotal = db.getPromptCount();
1450
2205
  const lastPush = cfg.remoteLastPush || "never";
1451
2206
  const lastPull = cfg.remoteLastPull || "never";
1452
- 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;
1453
2208
  process.stdout.write(`Remote: ${url}
1454
2209
  `);
1455
2210
  process.stdout.write(`Local total: ${localTotal} prompts
@@ -1462,13 +2217,10 @@ async function status(dbPath, cfg, url) {
1462
2217
  `);
1463
2218
  db.close();
1464
2219
  }
1465
- function sha2562(s) {
1466
- return createHash3("sha256").update(s).digest("hex");
1467
- }
1468
2220
 
1469
2221
  // src/ui/BrowseApp.tsx
1470
- import React6, { useState as useState2, useCallback, useEffect as useEffect2, useMemo as useMemo3 } from "react";
1471
- 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";
1472
2224
  import { spawn as spawn2 } from "child_process";
1473
2225
 
1474
2226
  // src/ui/Header.tsx
@@ -1479,7 +2231,7 @@ import { AppHeader, THEMES } from "@gi4nks/ink";
1479
2231
  // package.json
1480
2232
  var package_default = {
1481
2233
  name: "@gi4nks/ph",
1482
- version: "2.0.1",
2234
+ version: "1.5.0",
1483
2235
  description: "Transparent CLI wrapper that captures and indexes AI prompts",
1484
2236
  type: "module",
1485
2237
  license: "MIT",
@@ -1530,37 +2282,38 @@ var package_default = {
1530
2282
  dev: "tsx src/cli.ts",
1531
2283
  start: "node dist/cli.js",
1532
2284
  test: "vitest run",
1533
- lint: "eslint src"
2285
+ lint: "eslint src",
2286
+ "eval:retrieval": "tsx scripts/eval-context-retrieval.ts"
1534
2287
  },
1535
2288
  dependencies: {
1536
2289
  "@gi4nks/ink": "^1.5.0",
1537
2290
  "@lydell/node-pty": "^1.2.0-beta.3",
1538
2291
  "@modelcontextprotocol/sdk": "^1.29.0",
1539
2292
  "better-sqlite3": "^12.8.0",
1540
- ink: "^6.8.0",
1541
- react: "^19.2.4",
2293
+ ink: "^7.0.5",
2294
+ react: "^19.2.7",
1542
2295
  "sqlite-vec": "^0.1.9",
1543
2296
  zod: "^4.3.6"
1544
2297
  },
1545
2298
  devDependencies: {
2299
+ "@eslint/js": "^9.39.5",
1546
2300
  "@semantic-release/changelog": "^6.0.3",
1547
2301
  "@semantic-release/git": "^10.0.1",
1548
2302
  "@semantic-release/github": "^12.0.8",
1549
2303
  "@semantic-release/npm": "^13.1.5",
1550
2304
  "@types/better-sqlite3": "^7.6.13",
1551
- "@types/node": "^25.5.0",
1552
- "@types/react": "^19.2.14",
1553
- "@typescript-eslint/eslint-plugin": "^8.57.2",
1554
- "@typescript-eslint/parser": "^8.57.2",
1555
- 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",
1556
2310
  "eslint-plugin-react": "^7.37.5",
1557
2311
  "eslint-plugin-react-hooks": "^7.0.1",
1558
2312
  "semantic-release": "^25.0.3",
1559
- "standard-version": "^9.5.0",
1560
2313
  tsup: "^8.5.1",
1561
2314
  tsx: "^4.21.0",
1562
- typescript: "^5.9.3",
1563
- vitest: "^4.1.0"
2315
+ typescript: "^6.0.3",
2316
+ vitest: "^4.1.8"
1564
2317
  }
1565
2318
  };
1566
2319
 
@@ -1580,6 +2333,7 @@ var FilterBadges = ({ active, textFilter }) => /* @__PURE__ */ React.createEleme
1580
2333
  var Header = ({
1581
2334
  entriesCount,
1582
2335
  allEntriesCount,
2336
+ archiveCount,
1583
2337
  activeFilters,
1584
2338
  textFilter,
1585
2339
  isTextFiltering,
@@ -1589,7 +2343,7 @@ var Header = ({
1589
2343
  const version = package_default.version;
1590
2344
  const rawCwd = process.cwd();
1591
2345
  const cwd = rawCwd.replace(process.env.HOME || "", "~");
1592
- 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);
1593
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]"));
1594
2348
  return /* @__PURE__ */ React.createElement(
1595
2349
  AppHeader,
@@ -1631,6 +2385,7 @@ var Footer = ({
1631
2385
  );
1632
2386
  if (hasProject2) base.push({ key: "C", description: "chat" });
1633
2387
  base.push({ key: "r", description: "rerun" });
2388
+ base.push({ key: "p", description: "provider" });
1634
2389
  base.push("|");
1635
2390
  base.push(
1636
2391
  { key: "x", description: "del" },
@@ -1652,113 +2407,217 @@ var Footer = ({
1652
2407
  );
1653
2408
  };
1654
2409
 
1655
- // src/ui/themes.ts
1656
- 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";
1657
2413
 
1658
- // src/ui/SearchBar.tsx
1659
- import React3 from "react";
1660
- import { Box, Text as Text2 } from "ink";
1661
- var SearchBar = ({ value, isActive, theme }) => {
1662
- const isEmpty = value === "";
1663
- const bgColor = isActive ? theme.primary : theme.dim;
1664
- 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"
1665
2426
  };
1666
-
1667
- // src/ui/ListEntry.tsx
1668
- import React4 from "react";
1669
- import { Box as Box2, Text as Text3 } from "ink";
1670
-
1671
- // src/utils/extractTopic.ts
1672
- function extractTopic(prompt) {
1673
- let text = prompt.trim();
1674
- if (!text) {
1675
- return "(empty prompt)";
1676
- }
1677
- if (text.startsWith("```") || text.startsWith("`")) {
1678
- return truncateAtWordBoundary(text, 72);
2427
+ function parseMetadata(raw) {
2428
+ try {
2429
+ return JSON.parse(raw);
2430
+ } catch {
2431
+ return {};
1679
2432
  }
1680
- const originalTrimmed = text;
1681
- if (text.startsWith("#")) {
1682
- const headingMatch = text.match(/^#+\s+(.*?)(?:\s+#+)?(?:\n|$)/);
1683
- if (headingMatch && headingMatch[1]) {
1684
- 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);
1685
2467
  }
1686
2468
  }
1687
- const prefixes = [
1688
- // English
1689
- "please, ",
1690
- "please ",
1691
- "can you please ",
1692
- "could you please ",
1693
- "can you ",
1694
- "could you ",
1695
- "would you ",
1696
- "i need you to ",
1697
- "i need to ",
1698
- "i want you to ",
1699
- "i want to ",
1700
- "i'd like you to ",
1701
- "i'd like to ",
1702
- "help me to ",
1703
- "help me ",
1704
- "i need ",
1705
- "claude, ",
1706
- "gemini, ",
1707
- "chatgpt, ",
1708
- // Italiano
1709
- "puoi ",
1710
- "potresti ",
1711
- "potreste ",
1712
- "per favore ",
1713
- "per piacere ",
1714
- "ho bisogno che ",
1715
- "ho bisogno di ",
1716
- "voglio che ",
1717
- "vorrei che ",
1718
- "voglio ",
1719
- "vorrei ",
1720
- "aiutami a ",
1721
- "aiutami ",
1722
- "fammi vedere ",
1723
- "fammi ",
1724
- "si prega di ",
1725
- "si "
1726
- ];
1727
- let changed = true;
1728
- while (changed) {
1729
- changed = false;
1730
- const lowerText = text.toLowerCase();
1731
- for (const prefix of prefixes) {
1732
- if (lowerText.startsWith(prefix.toLowerCase())) {
1733
- text = text.slice(prefix.length).trim();
1734
- changed = true;
1735
- break;
1736
- }
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;
1737
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));
1738
2483
  }
1739
- const sentenceEndMatch = text.match(/[\n.?!`]/);
1740
- if (sentenceEndMatch && sentenceEndMatch.index !== void 0) {
1741
- 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
+ });
1742
2502
  }
1743
- if (!text) {
1744
- 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
+ });
1745
2509
  }
1746
- text = truncateAtWordBoundary(text, 72);
1747
- return text.charAt(0).toUpperCase() + text.slice(1);
2510
+ return result;
1748
2511
  }
1749
- function truncateAtWordBoundary(str, limit) {
1750
- const clean = str.replace(/\s+/g, " ").trim();
1751
- if (clean.length <= limit) return clean;
1752
- const sub = clean.slice(0, limit);
1753
- const lastSpace = sub.lastIndexOf(" ");
1754
- if (lastSpace > 0) {
1755
- 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
+ }
1756
2561
  }
1757
- return sub;
2562
+ return options;
1758
2563
  }
1759
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
+
1760
2617
  // src/ui/ListEntry.tsx
1761
- var ROLE_COLOR2 = {
2618
+ import React5 from "react";
2619
+ import { Box as Box3, Text as Text4 } from "ink";
2620
+ var ROLE_COLOR3 = {
1762
2621
  debug: "red",
1763
2622
  refactor: "yellow",
1764
2623
  explain: "blue",
@@ -1773,6 +2632,7 @@ function toolIndicator(tool) {
1773
2632
  if (tool === "claude") return { char: "\u25CF", color: "#f5a623" };
1774
2633
  if (tool === "gemini") return { char: "\u25CF", color: "#4fc3f7" };
1775
2634
  if (tool === "opencode") return { char: "\u25CF", color: "#e040fb" };
2635
+ if (tool === "codex") return { char: "\u25CF", color: "#74aa9c" };
1776
2636
  return { char: "\u25CB", color: "gray" };
1777
2637
  }
1778
2638
  function truncate(str, width) {
@@ -1793,32 +2653,195 @@ var ListEntry = ({ entry, isSelected, paneWidth, theme }) => {
1793
2653
  hour12: false
1794
2654
  });
1795
2655
  const role = meta.role ? truncate(meta.role, 6) : "";
1796
- const roleColor = meta.role ? ROLE_COLOR2[meta.role] || "white" : "white";
2656
+ const roleColor = meta.role ? ROLE_COLOR3[meta.role] || "white" : "white";
1797
2657
  const hasAnalysis = Boolean(meta.summary || meta.role || meta.tags && meta.tags.length > 0);
1798
- 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;
1799
2659
  const rawTitle = meta.title || extractTopic(entry.prompt);
1800
2660
  const displayPrompt = truncate(rawTitle, paneWidth - 5);
1801
2661
  let summaryLine = "";
1802
2662
  if (meta.summary) {
1803
2663
  summaryLine = truncate(meta.summary.replace(/\n/g, " "), paneWidth - 5);
1804
2664
  }
1805
- 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)));
1806
2666
  };
1807
2667
 
1808
2668
  // src/ui/PreviewPane.tsx
1809
- import React5, { useState, useEffect, useMemo as useMemo2 } from "react";
1810
- import { Box as Box3, Text as Text4, useInput } from "ink";
1811
- var ROLE_COLOR3 = {
1812
- debug: "red",
1813
- refactor: "yellow",
1814
- explain: "blue",
1815
- review: "magenta",
1816
- architect: "green",
1817
- test: "cyan",
1818
- docs: "white",
1819
- generate: "green",
1820
- research: "blue"
1821
- };
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
+ }
1822
2845
  function wrapTextLines(text, width) {
1823
2846
  const lines = text.split("\n");
1824
2847
  const result = [];
@@ -1829,13 +2852,77 @@ function wrapTextLines(text, width) {
1829
2852
  }
1830
2853
  let current = line;
1831
2854
  while (current.length > width) {
1832
- result.push(current.substring(0, width));
1833
- current = current.substring(width);
2855
+ result.push(current.slice(0, width));
2856
+ current = current.slice(width);
1834
2857
  }
1835
2858
  if (current.length > 0) result.push(current);
1836
2859
  }
1837
2860
  return result;
1838
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
+ };
1839
2926
  function formatTimestamp(ts) {
1840
2927
  try {
1841
2928
  const d = new Date(ts);
@@ -1864,80 +2951,78 @@ var PreviewPane = ({
1864
2951
  theme,
1865
2952
  getProjectMemories
1866
2953
  }) => {
1867
- const [activeTab, setActiveTab] = useState("prompt");
1868
- const [scrollOffset, setScrollOffset] = useState(0);
1869
- useEffect(() => {
1870
- setActiveTab(entry?.response ? "response" : "prompt");
1871
- setScrollOffset(0);
1872
- }, [entry?.id]);
1873
- 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);
1874
2960
  setScrollOffset(0);
1875
- }, [activeTab]);
1876
- const meta = useMemo2(() => entry ? parseMeta(entry.metadata) : {}, [entry]);
2961
+ };
2962
+ const meta = useMemo3(() => entry ? parseMeta(entry.metadata) : {}, [entry]);
1877
2963
  const contentWidth = Math.max(10, paneWidth - 4);
1878
2964
  const contentHeight = Math.max(3, paneHeight - 6);
1879
- const allLines = useMemo2(() => {
2965
+ const allLines = useMemo3(() => {
1880
2966
  if (!entry) return [];
1881
- if (activeTab === "prompt") return wrapTextLines(entry.prompt, contentWidth);
1882
- 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);
1883
2969
  const lines = [];
2970
+ const push2 = (text) => lines.push({ segments: [{ text }] });
1884
2971
  if (meta.project && getProjectMemories) {
1885
2972
  const memories = getProjectMemories(meta.project);
1886
2973
  if (memories.length > 0) {
1887
- lines.push(`\u2500\u2500 Project Knowledge: ${meta.project} \u2500\u2500`);
1888
- lines.push("");
2974
+ push2(`\u2500\u2500 Project Knowledge: ${meta.project} \u2500\u2500`);
2975
+ push2("");
1889
2976
  for (const mem of memories) {
1890
2977
  if (mem.summary) {
1891
- lines.push(...wrapTextLines(mem.summary, contentWidth));
1892
- lines.push("");
2978
+ for (const l of wrapTextLines(mem.summary, contentWidth)) push2(l);
2979
+ push2("");
1893
2980
  }
1894
2981
  if (mem.key_insights.length > 0) {
1895
- lines.push("Key Insights:");
1896
- for (const i of mem.key_insights) lines.push(...wrapTextLines(` \u2022 ${i}`, contentWidth));
1897
- 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("");
1898
2985
  }
1899
2986
  if (mem.technical_decisions.length > 0) {
1900
- lines.push("Technical Decisions:");
1901
- for (const d of mem.technical_decisions) lines.push(...wrapTextLines(` \u2022 ${d}`, contentWidth));
1902
- 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("");
1903
2990
  }
1904
2991
  }
1905
- lines.push("\u2500\u2500 This Entry \u2500\u2500");
1906
- lines.push("");
2992
+ push2("\u2500\u2500 This Entry \u2500\u2500");
2993
+ push2("");
1907
2994
  }
1908
2995
  }
1909
2996
  if (meta.summary) {
1910
- lines.push(...wrapTextLines(meta.summary, contentWidth));
1911
- lines.push("");
2997
+ for (const l of wrapTextLines(meta.summary, contentWidth)) push2(l);
2998
+ push2("");
1912
2999
  }
1913
3000
  if (meta.key_insights && meta.key_insights.length > 0) {
1914
3001
  for (const insight of meta.key_insights) {
1915
- lines.push(...wrapTextLines(`\u2022 ${insight}`, contentWidth));
3002
+ for (const l of wrapTextLines(`\u2022 ${insight}`, contentWidth)) push2(l);
1916
3003
  }
1917
- lines.push("");
1918
- }
1919
- if (lines.length === 0) {
1920
- lines.push("(no AI analysis - run ph analyze)");
3004
+ push2("");
1921
3005
  }
3006
+ if (lines.length === 0) push2("(no AI analysis - run ph analyze)");
1922
3007
  return lines;
1923
3008
  }, [entry, activeTab, contentWidth, meta, getProjectMemories]);
1924
3009
  const maxScroll = Math.max(0, allLines.length - contentHeight);
1925
- useInput((_char, key) => {
3010
+ useInput2((_char, key) => {
1926
3011
  if (!isFocused) return;
1927
- if (_char === "1") setActiveTab("prompt");
1928
- if (_char === "2") setActiveTab("response");
1929
- if (_char === "3") setActiveTab("memory");
3012
+ if (_char === "1") switchTab("prompt");
3013
+ if (_char === "2") switchTab("response");
3014
+ if (_char === "3") switchTab("memory");
1930
3015
  if (key.upArrow) setScrollOffset((s) => Math.max(0, s - 1));
1931
3016
  if (key.downArrow) setScrollOffset((s) => Math.min(maxScroll, s + 1));
1932
3017
  if (key.pageUp) setScrollOffset((s) => Math.max(0, s - contentHeight));
1933
3018
  if (key.pageDown) setScrollOffset((s) => Math.min(maxScroll, s + contentHeight));
1934
3019
  });
1935
3020
  if (!entry) {
1936
- 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)"));
1937
3022
  }
1938
3023
  const visibleLines = allLines.slice(scrollOffset, scrollOffset + contentHeight);
1939
3024
  const paddedLines = [...visibleLines];
1940
- while (paddedLines.length < contentHeight) paddedLines.push("");
3025
+ while (paddedLines.length < contentHeight) paddedLines.push({ segments: [{ text: " " }] });
1941
3026
  const topIndicator = scrollOffset > 0 ? ` \u2191 ${scrollOffset} above` : " ";
1942
3027
  const remaining = allLines.length - scrollOffset - contentHeight;
1943
3028
  const botIndicator = scrollOffset < maxScroll ? ` \u2193 ${remaining} more` : " ";
@@ -1947,8 +3032,8 @@ var PreviewPane = ({
1947
3032
  const tabPromptColor = isPromptActive && isFocused ? theme.primary : theme.dim;
1948
3033
  const tabResponseColor = isResponseActive && isFocused ? theme.primary : theme.dim;
1949
3034
  const tabMemoryColor = isMemoryActive && isFocused ? theme.primary : theme.dim;
1950
- 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(
1951
- 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,
1952
3037
  {
1953
3038
  color: tabPromptColor,
1954
3039
  bold: isPromptActive && isFocused,
@@ -1956,8 +3041,8 @@ var PreviewPane = ({
1956
3041
  },
1957
3042
  isPromptActive ? "\u25CF " : "\u25CB ",
1958
3043
  "1:PROMPT"
1959
- ), /* @__PURE__ */ React5.createElement(Text4, null, " "), /* @__PURE__ */ React5.createElement(
1960
- Text4,
3044
+ ), /* @__PURE__ */ React6.createElement(Text5, null, " "), /* @__PURE__ */ React6.createElement(
3045
+ Text5,
1961
3046
  {
1962
3047
  color: tabResponseColor,
1963
3048
  bold: isResponseActive && isFocused,
@@ -1965,8 +3050,8 @@ var PreviewPane = ({
1965
3050
  },
1966
3051
  isResponseActive ? "\u25CF " : "\u25CB ",
1967
3052
  "2:RESPONSE"
1968
- ), /* @__PURE__ */ React5.createElement(Text4, null, " "), /* @__PURE__ */ React5.createElement(
1969
- Text4,
3053
+ ), /* @__PURE__ */ React6.createElement(Text5, null, " "), /* @__PURE__ */ React6.createElement(
3054
+ Text5,
1970
3055
  {
1971
3056
  color: tabMemoryColor,
1972
3057
  bold: isMemoryActive && isFocused,
@@ -1974,22 +3059,10 @@ var PreviewPane = ({
1974
3059
  },
1975
3060
  isMemoryActive ? "\u25CF " : "\u25CB ",
1976
3061
  "3:MEMORY"
1977
- )), /* @__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));
1978
3063
  };
1979
3064
 
1980
3065
  // src/ui/BrowseApp.tsx
1981
- var FILTER_CATEGORIES = ["project", "language", "role", "tool", "tag", "starred", "quality", "relevance"];
1982
- var ROLE_COLOR4 = {
1983
- debug: "red",
1984
- refactor: "yellow",
1985
- explain: "blue",
1986
- review: "magenta",
1987
- architect: "green",
1988
- test: "cyan",
1989
- docs: "white",
1990
- generate: "green",
1991
- research: "blue"
1992
- };
1993
3066
  function copyToClipboard(text) {
1994
3067
  try {
1995
3068
  const platform = process.platform;
@@ -2009,28 +3082,12 @@ function copyToClipboard(text) {
2009
3082
  } catch {
2010
3083
  }
2011
3084
  }
2012
- function wrapTextLines2(text, width) {
2013
- const result = [];
2014
- for (const line of text.split("\n")) {
2015
- let current = line;
2016
- if (current.length === 0) {
2017
- result.push("");
2018
- continue;
2019
- }
2020
- while (current.length > width) {
2021
- result.push(current.slice(0, width));
2022
- current = current.slice(width);
2023
- }
2024
- if (current.length > 0) result.push(current);
2025
- }
2026
- return result;
2027
- }
2028
3085
  function useStdoutDimensions() {
2029
- const [dimensions, setDimensions] = useState2({
3086
+ const [dimensions, setDimensions] = useState3({
2030
3087
  columns: process.stdout.columns || 120,
2031
3088
  rows: process.stdout.rows || 24
2032
3089
  });
2033
- useEffect2(() => {
3090
+ useEffect(() => {
2034
3091
  const handler = () => {
2035
3092
  setDimensions({
2036
3093
  columns: process.stdout.columns || 120,
@@ -2053,16 +3110,9 @@ function formatTimestamp2(ts) {
2053
3110
  return ts.slice(0, 16);
2054
3111
  }
2055
3112
  }
2056
- function parseMeta2(raw) {
2057
- try {
2058
- return JSON.parse(raw);
2059
- } catch {
2060
- return {};
2061
- }
2062
- }
2063
3113
  function hasProject(entry) {
2064
3114
  if (!entry) return false;
2065
- const meta = parseMeta2(entry.metadata);
3115
+ const meta = parseMetadata(entry.metadata);
2066
3116
  return Boolean(meta.project);
2067
3117
  }
2068
3118
  var SESSION_GAP_MS = 2 * 60 * 60 * 1e3;
@@ -2081,94 +3131,49 @@ function needsSessionSeparator(entries, idx) {
2081
3131
  const curr = new Date(entries[idx].timestamp).getTime();
2082
3132
  return prev - curr > SESSION_GAP_MS;
2083
3133
  }
2084
- function getDistinctValues(entries, category) {
2085
- const set = /* @__PURE__ */ new Set();
2086
- for (const e of entries) {
2087
- if (category === "tool") {
2088
- set.add(e.tool);
2089
- continue;
2090
- }
2091
- const meta = parseMeta2(e.metadata);
2092
- if (category === "project" && meta.project) set.add(meta.project);
2093
- if (category === "language" && meta.language) set.add(meta.language);
2094
- if (category === "role" && meta.role) set.add(meta.role);
2095
- if (category === "tag") meta.tags?.forEach((t) => set.add(t));
2096
- }
2097
- return [...set].sort();
2098
- }
2099
- function applyFilters(entries, active, textFilter) {
2100
- let result = entries;
2101
- const hasActiveFilter = Object.values(active).some((v) => v !== void 0 && v !== false);
2102
- if (hasActiveFilter) {
2103
- result = result.filter((e) => {
2104
- const meta = parseMeta2(e.metadata);
2105
- if (active.tool && e.tool !== active.tool) return false;
2106
- if (active.project && meta.project !== active.project) return false;
2107
- if (active.language && meta.language !== active.language) return false;
2108
- if (active.role && meta.role !== active.role) return false;
2109
- if (active.tag && !meta.tags?.includes(active.tag)) return false;
2110
- if (active.starred && !meta.starred) return false;
2111
- if (active.minQuality !== void 0 && (meta.quality ?? 0) < active.minQuality) return false;
2112
- if (active.minRelevance !== void 0 && (meta.relevance ?? 0) < active.minRelevance) return false;
2113
- return true;
2114
- });
2115
- }
2116
- if (textFilter) {
2117
- const lq = textFilter.toLowerCase();
2118
- result = result.filter((e) => {
2119
- const meta = parseMeta2(e.metadata);
2120
- 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);
2121
- });
2122
- }
2123
- return result;
2124
- }
2125
3134
  var DetailView = ({ entry, onClose, onEdit, termWidth, termHeight, theme }) => {
2126
- const meta = parseMeta2(entry.metadata);
2127
- const [activeTab, setActiveTab] = useState2(entry.response ? "response" : "prompt");
2128
- const [scrollOffset, setScrollOffset] = useState2(0);
2129
- const [copied, setCopied] = useState2(false);
2130
- 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);
2131
3141
  setScrollOffset(0);
2132
- }, [activeTab]);
3142
+ };
2133
3143
  const contentWidth = Math.max(10, termWidth - 4);
2134
3144
  const text = activeTab === "prompt" ? entry.prompt : entry.response || "(no response captured)";
2135
- const lines = useMemo3(() => {
3145
+ const lines = useMemo4(() => {
2136
3146
  if (activeTab === "prompt" || activeTab === "response") {
2137
- return wrapTextLines2(text, contentWidth);
3147
+ return buildRichLines(text, contentWidth);
2138
3148
  }
2139
3149
  const memLines = [];
3150
+ const push2 = (t) => memLines.push({ segments: [{ text: t }] });
2140
3151
  if (meta.summary) {
2141
- memLines.push("SUMMARY:");
2142
- memLines.push(...wrapTextLines2(meta.summary, contentWidth));
2143
- memLines.push("");
3152
+ push2("SUMMARY:");
3153
+ for (const l of wrapTextLines(meta.summary, contentWidth)) push2(l);
3154
+ push2("");
2144
3155
  }
2145
3156
  if (meta.key_insights && meta.key_insights.length > 0) {
2146
- memLines.push("KEY INSIGHTS:");
3157
+ push2("KEY INSIGHTS:");
2147
3158
  for (const insight of meta.key_insights) {
2148
- memLines.push(...wrapTextLines2(`\u2022 ${insight}`, contentWidth));
3159
+ for (const l of wrapTextLines(`\u2022 ${insight}`, contentWidth)) push2(l);
2149
3160
  }
2150
3161
  }
2151
- if (memLines.length === 0) {
2152
- memLines.push("(no AI analysis found - run ph analyze)");
2153
- }
3162
+ if (memLines.length === 0) push2("(no AI analysis found - run ph analyze)");
2154
3163
  return memLines;
2155
3164
  }, [activeTab, text, contentWidth, meta]);
2156
3165
  const contentHeight = Math.max(1, termHeight - 10);
2157
3166
  const maxScroll = Math.max(0, lines.length - contentHeight);
2158
- useInput2((char, key) => {
3167
+ useInput3((char, key) => {
2159
3168
  if (key.escape || key.return) onClose();
2160
3169
  else if (char === "e") onEdit();
2161
3170
  else if (key.tab || char === "1" || char === "2" || char === "3") {
2162
- if (char === "1") setActiveTab("prompt");
2163
- else if (char === "2") setActiveTab("response");
2164
- else if (char === "3") setActiveTab("memory");
2165
- else setActiveTab((t) => {
2166
- if (t === "prompt") return "response";
2167
- if (t === "response") return "memory";
2168
- return "prompt";
2169
- });
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");
2170
3175
  } else if (char === "y") {
2171
- 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;
2172
3177
  copyToClipboard(copyText);
2173
3178
  setCopied(true);
2174
3179
  setTimeout(() => setCopied(false), 2e3);
@@ -2179,15 +3184,15 @@ var DetailView = ({ entry, onClose, onEdit, termWidth, termHeight, theme }) => {
2179
3184
  });
2180
3185
  const visibleLines = lines.slice(scrollOffset, scrollOffset + contentHeight);
2181
3186
  const remaining = lines.length - (scrollOffset + contentHeight);
2182
- 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")));
2183
3188
  };
2184
3189
  var EditView = ({ entry, onSave, onClose, theme }) => {
2185
- const initMeta = parseMeta2(entry.metadata);
2186
- const [field, setField] = useState2("title");
2187
- const [titleValue, setTitleValue] = useState2(initMeta.title ?? extractTopic(entry.prompt));
2188
- const [roleValue, setRoleValue] = useState2(initMeta.role ?? "");
2189
- const [tagsValue, setTagsValue] = useState2(initMeta.tags?.join(", ") ?? "");
2190
- 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) => {
2191
3196
  if (key.escape) {
2192
3197
  onClose();
2193
3198
  return;
@@ -2217,152 +3222,58 @@ var EditView = ({ entry, onSave, onClose, theme }) => {
2217
3222
  } else if (char && !key.ctrl && !key.meta) {
2218
3223
  setter((v) => v + char);
2219
3224
  }
2220
- });
2221
- const ROLES = "debug \xB7 refactor \xB7 explain \xB7 review \xB7 architect \xB7 test \xB7 docs \xB7 generate \xB7 research";
2222
- 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")));
2223
- };
2224
- var RerunView = ({ entry, onConfirm, onClose, theme }) => {
2225
- const [field, setField] = useState2("prompt");
2226
- const [toolValue, setToolValue] = useState2(entry.tool);
2227
- const [promptValue, setPromptValue] = useState2(entry.prompt);
2228
- const meta = parseMeta2(entry.metadata);
2229
- const git = meta.git_context;
2230
- useInput2((char, key) => {
2231
- if (key.escape) {
2232
- onClose();
2233
- return;
2234
- }
2235
- if (key.return) {
2236
- onConfirm(toolValue.trim(), promptValue.trim());
2237
- return;
2238
- }
2239
- if (key.tab) {
2240
- setField((f) => f === "tool" ? "prompt" : "tool");
2241
- return;
2242
- }
2243
- const setter = field === "tool" ? setToolValue : setPromptValue;
2244
- if (key.backspace || key.delete) {
2245
- setter((v) => v.slice(0, -1));
2246
- } else if (char && !key.ctrl && !key.meta) {
2247
- setter((v) => v + char);
2248
- }
2249
- });
2250
- 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")));
2251
- };
2252
- var FilterPanel = ({ allEntries, active, onUpdate, onClose, theme }) => {
2253
- const options = useMemo3(() => {
2254
- const opts = [];
2255
- for (const cat of FILTER_CATEGORIES) {
2256
- if (cat === "starred") {
2257
- const count = allEntries.filter((e) => {
2258
- try {
2259
- return JSON.parse(e.metadata).starred;
2260
- } catch {
2261
- return false;
2262
- }
2263
- }).length;
2264
- opts.push({ category: "starred", label: "\u2605 Only starred", count, active: !!active.starred });
2265
- } else if (cat === "quality") {
2266
- const qActive = active.minQuality !== void 0;
2267
- opts.push({ category: "quality", label: `Q \u2265 ${active.minQuality ?? "?"}`, count: 0, active: qActive });
2268
- for (const v of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
2269
- const c = allEntries.filter((e) => {
2270
- try {
2271
- return (JSON.parse(e.metadata).quality ?? 0) >= v;
2272
- } catch {
2273
- return false;
2274
- }
2275
- }).length;
2276
- opts.push({ category: "quality", label: `Q \u2265 ${v}`, count: c, active: active.minQuality === v });
2277
- }
2278
- } else if (cat === "relevance") {
2279
- const rActive = active.minRelevance !== void 0;
2280
- opts.push({ category: "relevance", label: `R \u2265 ${active.minRelevance ?? "?"}`, count: 0, active: rActive });
2281
- for (const v of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
2282
- const c = allEntries.filter((e) => {
2283
- try {
2284
- return (JSON.parse(e.metadata).relevance ?? 0) >= v;
2285
- } catch {
2286
- return false;
2287
- }
2288
- }).length;
2289
- opts.push({ category: "relevance", label: `R \u2265 ${v}`, count: c, active: active.minRelevance === v });
2290
- }
2291
- } else {
2292
- const vals = getDistinctValues(allEntries, cat);
2293
- for (const v of vals) {
2294
- const c = allEntries.filter((e) => {
2295
- try {
2296
- const m = JSON.parse(e.metadata);
2297
- if (cat === "project") return m.project === v;
2298
- if (cat === "language") return m.language === v;
2299
- if (cat === "role") return m.role === v;
2300
- if (cat === "tool") return e.tool === v;
2301
- if (cat === "tag") return m.tags?.includes(v);
2302
- return false;
2303
- } catch {
2304
- return false;
2305
- }
2306
- }).length;
2307
- const currentVal = active[cat];
2308
- opts.push({ category: cat, label: v, count: c, active: currentVal === v });
2309
- }
2310
- }
2311
- }
2312
- return opts;
2313
- }, [allEntries, active]);
2314
- const [cursor, setCursor] = useState2(0);
2315
- const visibleCount = Math.min(options.length, 16);
2316
- const scrollOffset = Math.max(0, Math.min(cursor - Math.floor(visibleCount / 2), options.length - visibleCount));
2317
- const visible = options.slice(Math.max(0, scrollOffset), scrollOffset + visibleCount);
2318
- useInput2((char, key) => {
3225
+ });
3226
+ const ROLES = "debug \xB7 refactor \xB7 explain \xB7 review \xB7 architect \xB7 test \xB7 docs \xB7 generate \xB7 research";
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")));
3228
+ };
3229
+ var RerunView = ({ entry, onConfirm, onClose, theme }) => {
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);
3234
+ const git = meta.git_context;
3235
+ useInput3((char, key) => {
2319
3236
  if (key.escape) {
2320
3237
  onClose();
2321
3238
  return;
2322
3239
  }
2323
- if (char === "c") {
2324
- onUpdate({});
3240
+ if (key.return) {
3241
+ onConfirm(toolValue.trim(), promptValue.trim());
2325
3242
  return;
2326
3243
  }
2327
- if (key.return || char === " ") {
2328
- const opt = options[cursor];
2329
- if (!opt) return;
2330
- if (opt.category === "starred") {
2331
- onUpdate({ ...active, starred: !opt.active || void 0 });
2332
- } else if (opt.category === "quality") {
2333
- const v = parseInt(opt.label.replace("Q \u2265 ", ""), 10);
2334
- onUpdate({ ...active, minQuality: opt.active ? void 0 : v });
2335
- } else if (opt.category === "relevance") {
2336
- const v = parseInt(opt.label.replace("R \u2265 ", ""), 10);
2337
- onUpdate({ ...active, minRelevance: opt.active ? void 0 : v });
2338
- } else {
2339
- const key2 = opt.category;
2340
- if (opt.active) {
2341
- const updated = { ...active };
2342
- delete updated[key2];
2343
- onUpdate(updated);
2344
- } else {
2345
- onUpdate({ ...active, [key2]: opt.label });
2346
- }
2347
- }
3244
+ if (key.tab) {
3245
+ setField((f) => f === "tool" ? "prompt" : "tool");
2348
3246
  return;
2349
3247
  }
2350
- if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
2351
- if (key.downArrow) setCursor((c) => Math.min(options.length - 1, c + 1));
2352
- if (key.pageUp) setCursor((c) => Math.max(0, c - visibleCount));
2353
- if (key.pageDown) setCursor((c) => Math.min(options.length - 1, c + visibleCount));
2354
- if (char && /^[a-z]$/.test(char)) {
2355
- const idx = options.findIndex((o, i) => i > cursor && o.category[0] === char);
2356
- 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);
2357
3274
  }
2358
3275
  });
2359
- const activeCount = Object.values(active).filter((v) => v !== void 0 && v !== false).length;
2360
- 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) => {
2361
- const absIdx = Math.max(0, scrollOffset) + i;
2362
- const isCur = absIdx === cursor;
2363
- 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";
2364
- 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"));
2365
- })), 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")));
2366
3277
  };
2367
3278
  var SETTING_FIELDS = [
2368
3279
  { key: "backgroundAnalysis", label: "Auto-analysis on capture", type: "boolean" },
@@ -2374,10 +3285,10 @@ var SETTING_FIELDS = [
2374
3285
  { key: "filterMinRelevance", label: "Min relevance filter", type: "number" }
2375
3286
  ];
2376
3287
  var SettingsView = ({ onClose, theme }) => {
2377
- const [cfg, setCfg] = useState2(() => load());
2378
- const [cursor, setCursor] = useState2(0);
2379
- const [dirty, setDirty] = useState2(false);
2380
- 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);
2381
3292
  const toggleField = (key) => {
2382
3293
  const updated = { ...cfg };
2383
3294
  if (key === "backgroundAnalysis") {
@@ -2393,7 +3304,7 @@ var SettingsView = ({ onClose, theme }) => {
2393
3304
  setSaved(true);
2394
3305
  setTimeout(() => setSaved(false), 2e3);
2395
3306
  };
2396
- useInput2((char, key) => {
3307
+ useInput3((char, key) => {
2397
3308
  if (key.escape) {
2398
3309
  onClose();
2399
3310
  return;
@@ -2416,7 +3327,7 @@ var SettingsView = ({ onClose, theme }) => {
2416
3327
  }
2417
3328
  }
2418
3329
  });
2419
- 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) => {
2420
3331
  const isCur = i === cursor;
2421
3332
  const val = cfg[field.key];
2422
3333
  let display;
@@ -2426,25 +3337,27 @@ var SettingsView = ({ onClose, theme }) => {
2426
3337
  display = val !== void 0 ? String(val) : "(default)";
2427
3338
  }
2428
3339
  const valColor = field.type === "boolean" ? val ? theme.success : theme.dim : val !== void 0 ? "white" : theme.dim;
2429
- 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]"));
2430
- })), /* @__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")));
2431
3342
  };
2432
3343
  var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2433
3344
  const { exit } = useApp();
2434
- const [allEntries, setAllEntries] = useState2(() => db.search({ limit: 1e3 }));
2435
- const [refreshKey, setRefreshKey] = useState2(0);
2436
- const [textFilter, setTextFilter] = useState2(initialTextFilter ?? "");
2437
- const [isTextFiltering, setTextFiltering] = useState2(false);
2438
- const [activeFilters, setActiveFilters] = useState2(initialFilters ?? {});
2439
- const [showFilterPanel, setFilterPanel] = useState2(false);
2440
- const [showSettings, setShowSettings] = useState2(false);
2441
- const [cursor, setCursor] = useState2(0);
2442
- const [detail, setDetail] = useState2(null);
2443
- const [editing, setEditing] = useState2(null);
2444
- const [rerunning, setRerunning] = useState2(null);
2445
- const [copiedId, setCopiedId] = useState2(null);
2446
- const [focusPane, setFocusPane] = useState2("list");
2447
- 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);
2448
3361
  const currentThemeName = "dark";
2449
3362
  const theme = THEMES3[currentThemeName] || THEMES3.dark;
2450
3363
  const { columns: termWidth, rows: termHeight } = useStdoutDimensions();
@@ -2452,15 +3365,15 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2452
3365
  const isWide = termWidth >= SPLIT_THRESHOLD;
2453
3366
  const leftPaneWidth = isWide ? Math.max(32, Math.min(50, Math.floor(termWidth * 0.33))) : termWidth;
2454
3367
  const PAGE_SIZE = Math.max(1, Math.floor((termHeight - 3) / 4));
2455
- const entries = useMemo3(
3368
+ const entries = useMemo4(
2456
3369
  () => applyFilters(allEntries, activeFilters, textFilter),
2457
3370
  // eslint-disable-next-line react-hooks/exhaustive-deps
2458
3371
  [allEntries, activeFilters, textFilter, refreshKey]
2459
3372
  );
2460
- useEffect2(() => {
3373
+ useEffect(() => {
2461
3374
  setCursor((c) => Math.min(c, Math.max(0, entries.length - 1)));
2462
3375
  }, [entries.length]);
2463
- useEffect2(() => {
3376
+ useEffect(() => {
2464
3377
  setScrollOffset((prev) => {
2465
3378
  const clamped = Math.max(0, Math.min(prev, Math.max(0, entries.length - PAGE_SIZE)));
2466
3379
  if (cursor < clamped) return cursor;
@@ -2472,7 +3385,7 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2472
3385
  const visible = entries.slice(pageStart, pageStart + PAGE_SIZE);
2473
3386
  const triggerRefresh = useCallback(() => setRefreshKey((k) => k + 1), []);
2474
3387
  const toggleStar = useCallback((entry) => {
2475
- const meta = parseMeta2(entry.metadata);
3388
+ const meta = parseMetadata(entry.metadata);
2476
3389
  meta.starred = !meta.starred;
2477
3390
  const newMeta = JSON.stringify(meta);
2478
3391
  db.updateMetadata(entry.id, newMeta);
@@ -2495,8 +3408,8 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2495
3408
  setCursor(0);
2496
3409
  setScrollOffset(0);
2497
3410
  }, []);
2498
- useInput2((char, key) => {
2499
- if (detail || editing || showFilterPanel || showSettings || rerunning) return;
3411
+ useInput3((char, key) => {
3412
+ if (detail || editing || showFilterPanel || showSettings || rerunning || providerPicking) return;
2500
3413
  if (isTextFiltering) {
2501
3414
  if (key.escape) {
2502
3415
  setTextFiltering(false);
@@ -2527,6 +3440,8 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2527
3440
  if (entries[cursor]) setEditing(entries[cursor]);
2528
3441
  } else if (char === "r") {
2529
3442
  if (entries[cursor]) setRerunning(entries[cursor]);
3443
+ } else if (char === "p") {
3444
+ if (entries[cursor]) setProviderPicking(entries[cursor]);
2530
3445
  } else if (char === "x") {
2531
3446
  if (entries[cursor]) handleDelete(entries[cursor]);
2532
3447
  } else if (char === "f") {
@@ -2541,7 +3456,7 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
2541
3456
  } else if (char === "C") {
2542
3457
  const entry = entries[cursor];
2543
3458
  if (!entry) return;
2544
- const meta = parseMeta2(entry.metadata);
3459
+ const meta = parseMetadata(entry.metadata);
2545
3460
  const project = meta.project;
2546
3461
  if (!project) return;
2547
3462
  const memories = db.searchMemories(project, 3);
@@ -2565,7 +3480,7 @@ ${mem.summary}`);
2565
3480
  if (recent.length > 0) {
2566
3481
  ctx.push("\n---\n## Recent Context");
2567
3482
  for (const p of recent) {
2568
- const pm = parseMeta2(p.metadata);
3483
+ const pm = parseMetadata(p.metadata);
2569
3484
  ctx.push(`
2570
3485
  - #${p.id}: ${pm.summary || p.prompt.slice(0, 100).replace(/\n/g, " ")}`);
2571
3486
  }
@@ -2611,7 +3526,7 @@ ${entry.prompt}`;
2611
3526
  });
2612
3527
  let mainContent;
2613
3528
  if (showSettings) {
2614
- mainContent = /* @__PURE__ */ React6.createElement(
3529
+ mainContent = /* @__PURE__ */ React7.createElement(
2615
3530
  SettingsView,
2616
3531
  {
2617
3532
  onClose: () => setShowSettings(false),
@@ -2619,7 +3534,7 @@ ${entry.prompt}`;
2619
3534
  }
2620
3535
  );
2621
3536
  } else if (showFilterPanel) {
2622
- mainContent = /* @__PURE__ */ React6.createElement(
3537
+ mainContent = /* @__PURE__ */ React7.createElement(
2623
3538
  FilterPanel,
2624
3539
  {
2625
3540
  allEntries,
@@ -2629,8 +3544,22 @@ ${entry.prompt}`;
2629
3544
  theme
2630
3545
  }
2631
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
+ );
2632
3561
  } else if (rerunning) {
2633
- mainContent = /* @__PURE__ */ React6.createElement(
3562
+ mainContent = /* @__PURE__ */ React7.createElement(
2634
3563
  RerunView,
2635
3564
  {
2636
3565
  entry: rerunning,
@@ -2644,7 +3573,7 @@ ${entry.prompt}`;
2644
3573
  }
2645
3574
  );
2646
3575
  } else if (editing) {
2647
- mainContent = /* @__PURE__ */ React6.createElement(
3576
+ mainContent = /* @__PURE__ */ React7.createElement(
2648
3577
  EditView,
2649
3578
  {
2650
3579
  entry: editing,
@@ -2654,7 +3583,7 @@ ${entry.prompt}`;
2654
3583
  }
2655
3584
  );
2656
3585
  } else if (detail && !isWide) {
2657
- mainContent = /* @__PURE__ */ React6.createElement(
3586
+ mainContent = /* @__PURE__ */ React7.createElement(
2658
3587
  DetailView,
2659
3588
  {
2660
3589
  entry: detail,
@@ -2671,10 +3600,10 @@ ${entry.prompt}`;
2671
3600
  } else if (isWide) {
2672
3601
  const listFocused = focusPane === "list";
2673
3602
  const previewFocused = focusPane === "preview";
2674
- 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) => {
2675
3604
  const absIdx = pageStart + i;
2676
3605
  const isNewSession = needsSessionSeparator(entries, absIdx);
2677
- 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(
2678
3607
  ListEntry,
2679
3608
  {
2680
3609
  entry,
@@ -2682,9 +3611,9 @@ ${entry.prompt}`;
2682
3611
  paneWidth: leftPaneWidth,
2683
3612
  theme
2684
3613
  }
2685
- ), 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)))));
2686
- })), /* @__PURE__ */ React6.createElement(
2687
- 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,
2688
3617
  {
2689
3618
  borderLeft: true,
2690
3619
  borderStyle: "single",
@@ -2692,9 +3621,10 @@ ${entry.prompt}`;
2692
3621
  flexGrow: 1,
2693
3622
  flexDirection: "column"
2694
3623
  },
2695
- /* @__PURE__ */ React6.createElement(
3624
+ /* @__PURE__ */ React7.createElement(
2696
3625
  PreviewPane,
2697
3626
  {
3627
+ key: entries[cursor]?.id ?? "empty",
2698
3628
  entry: entries[cursor] ?? null,
2699
3629
  paneWidth: termWidth - leftPaneWidth - 1,
2700
3630
  paneHeight: termHeight - 3,
@@ -2705,10 +3635,10 @@ ${entry.prompt}`;
2705
3635
  )
2706
3636
  ));
2707
3637
  } else {
2708
- 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) => {
2709
3639
  const absIdx = pageStart + i;
2710
3640
  const isNewSession = needsSessionSeparator(entries, absIdx);
2711
- 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(
2712
3642
  ListEntry,
2713
3643
  {
2714
3644
  entry,
@@ -2716,20 +3646,21 @@ ${entry.prompt}`;
2716
3646
  paneWidth: termWidth,
2717
3647
  theme
2718
3648
  }
2719
- ), 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)))));
2720
3650
  }));
2721
3651
  }
2722
- 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(
2723
3653
  Header,
2724
3654
  {
2725
3655
  entriesCount: entries.length,
2726
3656
  allEntriesCount: allEntries.length,
3657
+ archiveCount,
2727
3658
  activeFilters,
2728
3659
  textFilter,
2729
3660
  isTextFiltering: false,
2730
3661
  theme: currentThemeName
2731
3662
  }
2732
- ), /* @__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(
2733
3664
  Footer,
2734
3665
  {
2735
3666
  cursor,
@@ -2855,6 +3786,32 @@ function parseDate(s, label) {
2855
3786
  async function cmdSearch(db, cfg, args) {
2856
3787
  const { flags, positional } = parseFlags(args);
2857
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
+ }
2858
3815
  if (flags["semantic"] && query) {
2859
3816
  await cmdSearchSemantic(db, cfg, query, Number(flags["limit"] ?? 50), Boolean(flags["full"]));
2860
3817
  return;
@@ -3034,8 +3991,8 @@ ${meta.git_context.diff}
3034
3991
  }
3035
3992
 
3036
3993
  // src/commands/import.ts
3037
- import path7 from "path";
3038
- import os7 from "os";
3994
+ import path8 from "path";
3995
+ import os8 from "os";
3039
3996
 
3040
3997
  // src/importer/gemini.ts
3041
3998
  import fs5 from "fs";
@@ -3043,6 +4000,7 @@ import path4 from "path";
3043
4000
  import os4 from "os";
3044
4001
 
3045
4002
  // src/analyzer/index.ts
4003
+ import { createHash as createHash3 } from "crypto";
3046
4004
  var ANALYSIS_PROMPT = (text) => `Analyze this AI tool prompt and response and return ONLY a JSON object with these exact fields:
3047
4005
  {
3048
4006
  "project": "<project name if clearly identifiable, else empty string>",
@@ -3152,7 +4110,7 @@ function mergeMetadata(existing, result, force) {
3152
4110
  if (!merged.tags?.length) delete merged.tags;
3153
4111
  return merged;
3154
4112
  }
3155
- function parseMeta3(raw) {
4113
+ function parseMeta2(raw) {
3156
4114
  try {
3157
4115
  return JSON.parse(raw);
3158
4116
  } catch {
@@ -3160,20 +4118,28 @@ function parseMeta3(raw) {
3160
4118
  }
3161
4119
  }
3162
4120
  function hasMetadata(entry) {
3163
- const meta = parseMeta3(entry.metadata);
4121
+ const meta = parseMeta2(entry.metadata);
3164
4122
  return Boolean(meta.role || meta.tags?.length || meta.project || meta.language);
3165
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
+ }
3166
4130
  async function analyzeAll(entries, provider, db, opts = {}) {
3167
4131
  const { force = false, pruneBelow = 0, dryRun = false, onProgress, onPrune } = opts;
3168
4132
  const toProcess = force ? entries : entries.filter((e) => !hasMetadata(e));
3169
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;
3170
4136
  for (let i = 0; i < toProcess.length; i++) {
3171
4137
  const entry = toProcess[i];
3172
4138
  let result = null;
3173
4139
  let errMsg;
3174
4140
  try {
3175
4141
  result = await analyzePrompt(entry.prompt, provider);
3176
- const existing = parseMeta3(entry.metadata);
4142
+ const existing = parseMeta2(entry.metadata);
3177
4143
  const merged = mergeMetadata(existing, result, force);
3178
4144
  if (pruneBelow > 0 && result.relevance !== void 0 && result.relevance < pruneBelow) {
3179
4145
  onPrune?.(entry, result.relevance);
@@ -3184,11 +4150,25 @@ async function analyzeAll(entries, provider, db, opts = {}) {
3184
4150
  } else {
3185
4151
  db.updateMetadata(entry.id, JSON.stringify(merged));
3186
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
+ }
3187
4160
  db.upsertProjectMemory({
3188
4161
  project: result.project,
3189
4162
  prompt_id: entry.id,
3190
4163
  summary: result.summary,
3191
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 ?? [],
3192
4172
  technical_decisions: result.technical_decisions ?? []
3193
4173
  });
3194
4174
  }
@@ -3328,7 +4308,7 @@ async function importGeminiHistory(db, geminiDir, dryRun, analyzer, onProgress,
3328
4308
  if (response.length > 8e3) response = response.slice(0, 8e3) + "\n... (truncated)";
3329
4309
  }
3330
4310
  try {
3331
- const id = db.insert({
4311
+ const id = db.insert(createCaptureRecord({
3332
4312
  timestamp: new Date(msg.timestamp).toISOString(),
3333
4313
  tool: "gemini",
3334
4314
  prompt: content,
@@ -3337,8 +4317,8 @@ async function importGeminiHistory(db, geminiDir, dryRun, analyzer, onProgress,
3337
4317
  workdir: inferWorkDir(filePath, geminiDir),
3338
4318
  hostname,
3339
4319
  exit_code: 0,
3340
- metadata
3341
- });
4320
+ metadata: JSON.parse(metadata)
4321
+ }));
3342
4322
  filter?.registerHash(content, id);
3343
4323
  result.promptsImported++;
3344
4324
  } catch (e) {
@@ -3496,7 +4476,7 @@ async function importClaudeHistory(db, claudeDir, dryRun, analyzer, onProgress,
3496
4476
  } catch {
3497
4477
  }
3498
4478
  }
3499
- const id = db.insert({
4479
+ const id = db.insert(createCaptureRecord({
3500
4480
  timestamp: entry.timestamp,
3501
4481
  tool: "claude",
3502
4482
  prompt: entry.prompt,
@@ -3505,8 +4485,8 @@ async function importClaudeHistory(db, claudeDir, dryRun, analyzer, onProgress,
3505
4485
  workdir: entry.workdir,
3506
4486
  hostname,
3507
4487
  exit_code: 0,
3508
- metadata
3509
- });
4488
+ metadata: JSON.parse(metadata)
4489
+ }));
3510
4490
  filter?.registerHash(entry.prompt, id);
3511
4491
  result.promptsImported++;
3512
4492
  } catch (e) {
@@ -3679,7 +4659,7 @@ async function importOpenCodeHistory(db, opencodeDir, dryRun, analyzer, onProgre
3679
4659
  metadata.tokens = entry.tokens;
3680
4660
  }
3681
4661
  }
3682
- const id = db.insert({
4662
+ const id = db.insert(createCaptureRecord({
3683
4663
  timestamp: entry.timestamp,
3684
4664
  tool: "opencode",
3685
4665
  prompt: entry.prompt,
@@ -3688,8 +4668,8 @@ async function importOpenCodeHistory(db, opencodeDir, dryRun, analyzer, onProgre
3688
4668
  workdir: entry.workdir,
3689
4669
  hostname,
3690
4670
  exit_code: 0,
3691
- metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : "{}"
3692
- });
4671
+ metadata
4672
+ }));
3693
4673
  filter?.registerHash(entry.prompt, id);
3694
4674
  result.promptsImported++;
3695
4675
  } catch (e) {
@@ -3700,90 +4680,16 @@ async function importOpenCodeHistory(db, opencodeDir, dryRun, analyzer, onProgre
3700
4680
  return result;
3701
4681
  }
3702
4682
 
3703
- // src/ai/ollama.ts
3704
- var OllamaProvider = class {
3705
- constructor(url, model) {
3706
- this.url = url;
3707
- this.model = model;
3708
- }
3709
- name = "ollama";
3710
- async generate(prompt) {
3711
- const controller = new AbortController();
3712
- const timer = setTimeout(() => controller.abort(), 6e4);
3713
- let res;
3714
- try {
3715
- res = await fetch(`${this.url}/api/generate`, {
3716
- method: "POST",
3717
- headers: { "Content-Type": "application/json" },
3718
- body: JSON.stringify({ model: this.model, prompt, stream: false }),
3719
- signal: controller.signal
3720
- });
3721
- } catch (err) {
3722
- clearTimeout(timer);
3723
- const msg = err.message ?? String(err);
3724
- if (msg.includes("abort") || msg.includes("timeout")) {
3725
- throw new Error(`Ollama request timed out (60s). Is Ollama running at ${this.url}?`);
3726
- }
3727
- throw new Error(`Ollama unreachable at ${this.url}: ${msg}`);
3728
- } finally {
3729
- clearTimeout(timer);
3730
- }
3731
- if (!res.ok) {
3732
- const body = await res.text().catch(() => "");
3733
- throw new Error(`Ollama error ${res.status}: ${body}`);
3734
- }
3735
- const data = await res.json();
3736
- return data.response ?? "";
3737
- }
3738
- };
3739
-
3740
- // src/ai/gemini.ts
3741
- var GENERATE_API = "https://generativelanguage.googleapis.com/v1beta/models";
3742
- async function generateContent(prompt, model, apiKey) {
3743
- const res = await fetch(`${GENERATE_API}/${model}:generateContent?key=${apiKey}`, {
3744
- method: "POST",
3745
- headers: { "Content-Type": "application/json" },
3746
- body: JSON.stringify({
3747
- contents: [{ parts: [{ text: prompt }] }]
3748
- })
3749
- });
3750
- if (!res.ok) {
3751
- const err = await res.text();
3752
- throw new Error(`Gemini API error ${res.status}: ${err}`);
3753
- }
3754
- const data = await res.json();
3755
- return data.candidates[0]?.content?.parts[0]?.text ?? "";
3756
- }
3757
- var GeminiProvider = class {
3758
- constructor(model, apiKey) {
3759
- this.model = model;
3760
- this.apiKey = apiKey;
3761
- }
3762
- name = "gemini";
3763
- generate(prompt) {
3764
- return generateContent(prompt, this.model, this.apiKey);
3765
- }
3766
- };
3767
-
3768
- // src/ai/provider.ts
3769
- function getProvider(cfg) {
3770
- const provider = cfg.analyzeProvider ?? "ollama";
3771
- if (provider === "gemini") {
3772
- const apiKey = process.env.GEMINI_API_KEY ?? cfg.geminiApiKey;
3773
- if (!apiKey) return null;
3774
- return new GeminiProvider("gemini-2.5-flash", apiKey);
3775
- }
3776
- return new OllamaProvider(
3777
- cfg.ollamaUrl ?? "http://localhost:11434",
3778
- cfg.ollamaModel ?? "llama3.1:latest"
3779
- );
3780
- }
4683
+ // src/importer/codex.ts
4684
+ import fs8 from "fs";
4685
+ import path7 from "path";
4686
+ import os7 from "os";
3781
4687
 
3782
4688
  // src/filter/index.ts
3783
4689
  import { createHash as createHash4 } from "crypto";
3784
- 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;
3785
4691
  var ORPHAN_CODE_PATTERN = /^([{}()[\]<>|&!=\-+*/%^~#@\\:;.,\s]+|\.\.\.|console\.log.*|print.*)$/i;
3786
- 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]*$/;
3787
4693
  var FilterPipeline = class _FilterPipeline {
3788
4694
  minLength;
3789
4695
  minRelevance;
@@ -3856,11 +4762,209 @@ var FilterPipeline = class _FilterPipeline {
3856
4762
  }
3857
4763
  };
3858
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);
4946
+ }
4947
+ };
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
+
3859
4963
  // src/commands/import.ts
3860
4964
  async function cmdImport(db, cfg, args) {
3861
4965
  if (args.length === 0) {
3862
4966
  process.stderr.write("Usage: ph import <source> [--dry-run] [--analyze]\n");
3863
- process.stderr.write("Supported sources: gemini, claude, opencode\n");
4967
+ process.stderr.write("Supported sources: gemini, claude, opencode, codex\n");
3864
4968
  process.exit(1);
3865
4969
  }
3866
4970
  const source = args[0];
@@ -3868,7 +4972,8 @@ async function cmdImport(db, cfg, args) {
3868
4972
  const dryRun = Boolean(flags["dry-run"]);
3869
4973
  const analyze = Boolean(flags["analyze"]);
3870
4974
  const useFilter = Boolean(flags["filter"]);
3871
- const homeDir = os7.homedir();
4975
+ const transcriptFile = typeof flags.file === "string" ? flags.file : void 0;
4976
+ const homeDir = os8.homedir();
3872
4977
  if (dryRun) console.log("Dry run \u2014 nothing will be written to the database.");
3873
4978
  let analyzer;
3874
4979
  if (analyze) {
@@ -3903,16 +5008,19 @@ async function cmdImport(db, cfg, args) {
3903
5008
  let result;
3904
5009
  switch (source) {
3905
5010
  case "gemini":
3906
- 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);
3907
5012
  break;
3908
5013
  case "claude":
3909
- 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);
3910
5015
  break;
3911
5016
  case "opencode":
3912
- 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);
3913
5021
  break;
3914
5022
  default:
3915
- 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
3916
5024
  `);
3917
5025
  process.exit(1);
3918
5026
  }
@@ -4017,9 +5125,6 @@ ${e.response}` : e.prompt);
4017
5125
  console.log("\nDone!");
4018
5126
  }
4019
5127
 
4020
- // src/commands/log.ts
4021
- import os8 from "os";
4022
-
4023
5128
  // src/background/analyzer.ts
4024
5129
  import { spawn as spawn3 } from "child_process";
4025
5130
  function spawnBackgroundAnalysis(id, dbPath) {
@@ -4072,32 +5177,16 @@ async function cmdLog(dbPath, cfg, args) {
4072
5177
  process.exit(1);
4073
5178
  }
4074
5179
  }
4075
- const { rootDir, projectName } = detectProject(workdir);
4076
- const language = detectLanguage(rootDir);
4077
- const metaObj = { $schema_version: 1 };
4078
- if (projectName) metaObj.project = projectName;
4079
- if (language) metaObj.language = language;
4080
- const title = extractTopic(prompt);
4081
- if (title) metaObj.title = title;
5180
+ const record = createCaptureRecord({ tool, prompt, response, args: "", workdir });
4082
5181
  const db = new PhDB(dbPath);
4083
- const id = db.insert({
4084
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4085
- tool,
4086
- prompt,
4087
- response,
4088
- args: "",
4089
- workdir,
4090
- hostname: os8.hostname(),
4091
- exit_code: 0,
4092
- metadata: JSON.stringify(metaObj)
4093
- });
5182
+ const id = db.insert(record);
4094
5183
  db.close();
4095
5184
  if (cfg.backgroundAnalysis) {
4096
5185
  spawnBackgroundAnalysis(id, dbPath);
4097
5186
  }
4098
5187
  const pushUrl = process.env.PH_REMOTE_URL || cfg.remoteUrl;
4099
5188
  if (pushUrl) {
4100
- 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(() => {
4101
5190
  });
4102
5191
  }
4103
5192
  }
@@ -4188,6 +5277,13 @@ function cmdConfig(args) {
4188
5277
  }
4189
5278
 
4190
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
+ }
4191
5287
  async function cmdBackgroundAnalyze(dbPath, args) {
4192
5288
  const id = parseInt(args[0], 10);
4193
5289
  if (isNaN(id)) return;
@@ -4213,11 +5309,24 @@ async function cmdBackgroundAnalyze(dbPath, args) {
4213
5309
  const merged = mergeMetadata(existing, result, false);
4214
5310
  db.updateMetadata(id, JSON.stringify(merged));
4215
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
+ }
4216
5318
  db.upsertProjectMemory({
4217
5319
  project: result.project,
4218
5320
  prompt_id: id,
4219
5321
  summary: result.summary,
4220
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 ?? [],
4221
5330
  technical_decisions: result.technical_decisions ?? []
4222
5331
  });
4223
5332
  }
@@ -4231,6 +5340,11 @@ async function cmdBackgroundAnalyze(dbPath, args) {
4231
5340
  async function cmdCleanup(db, cfg, args) {
4232
5341
  const { flags } = parseFlags(args);
4233
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
+ }
4234
5348
  const minLength = flags["min-length"] ? Number(flags["min-length"]) : cfg.filterMinLength ?? 15;
4235
5349
  const minScore = flags["min-score"] ? Number(flags["min-score"]) : cfg.filterMinRelevance ?? 3;
4236
5350
  const days = flags["days"] ? Number(flags["days"]) : void 0;
@@ -4294,6 +5408,71 @@ Candidates for rule-based deletion: ${toDelete.length}`);
4294
5408
  console.log(`
4295
5409
  Deleted ${deleted} prompts.`);
4296
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
+ }
4297
5476
 
4298
5477
  // src/commands/vacuum.ts
4299
5478
  async function cmdVacuum(db) {
@@ -4317,75 +5496,20 @@ async function cmdContext(db, cfg, args) {
4317
5496
  process.stderr.write("ph: could not detect project. Use --project <name>\n");
4318
5497
  process.exit(1);
4319
5498
  }
4320
- const memories = !promptsOnly ? db.searchMemories(project, limit) : [];
4321
- let prompts = [];
4322
- if (!memoriesOnly) {
4323
- if (query) {
4324
- const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
4325
- const model = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
4326
- const [queryVec] = await getEmbeddings([query], ollamaUrl, model, 1);
4327
- if (queryVec) {
4328
- const semResults = db.searchSemantic(queryVec, limit * 3);
4329
- prompts = semResults.filter((e) => {
4330
- try {
4331
- const meta = JSON.parse(e.metadata);
4332
- return meta.project === project;
4333
- } catch {
4334
- return false;
4335
- }
4336
- }).slice(0, limit);
4337
- }
4338
- } else {
4339
- prompts = db.getProjectMemory(project, limit);
4340
- }
4341
- }
4342
- if (memories.length === 0 && prompts.length === 0) {
4343
- process.stdout.write(`No relevant context found for project "${project}".
4344
- `);
4345
- return;
4346
- }
4347
- const output = [];
4348
- if (memories.length > 0) {
4349
- output.push(`## Project Knowledge: ${project}
4350
- `);
4351
- for (const mem of memories) {
4352
- if (mem.summary) output.push(`${mem.summary}
4353
- `);
4354
- if (mem.key_insights.length > 0) {
4355
- output.push("Key Insights:");
4356
- for (const insight of mem.key_insights) output.push(` - ${insight}`);
4357
- output.push("");
4358
- }
4359
- if (mem.technical_decisions.length > 0) {
4360
- output.push("Technical Decisions:");
4361
- for (const dec of mem.technical_decisions) output.push(` - ${dec}`);
4362
- output.push("");
4363
- }
4364
- }
4365
- }
4366
- if (prompts.length > 0) {
4367
- output.push("---\n");
4368
- output.push(`## Recent Context
4369
- `);
4370
- for (const e of prompts) {
4371
- let meta = {};
4372
- try {
4373
- meta = JSON.parse(e.metadata);
4374
- } catch {
4375
- }
4376
- output.push(`### #${e.id}`);
4377
- if (meta.role) output.push(`Role: ${meta.role}`);
4378
- if (meta.summary) output.push(`${meta.summary}`);
4379
- if (meta.key_insights && meta.key_insights.length > 0) {
4380
- for (const i of meta.key_insights) output.push(` - ${i}`);
4381
- }
4382
- if (verbose) {
4383
- output.push("```\n" + e.prompt.slice(0, 1e3) + (e.prompt.length > 1e3 ? "\n..." : "") + "\n```");
4384
- }
4385
- output.push("");
4386
- }
4387
- }
4388
- 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");
4389
5513
  }
4390
5514
 
4391
5515
  // src/sessions/index.ts
@@ -4440,42 +5564,100 @@ function computeSessionCohesion(session, embeddings) {
4440
5564
  }
4441
5565
 
4442
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
+ }
4443
5649
  async function cmdSessions(db, args) {
4444
5650
  const { flags } = parseFlags(args);
4445
5651
  const gapHours = Number(flags["gap-hours"] ?? 2);
4446
5652
  const limit = Number(flags["limit"] ?? 20);
4447
5653
  const minSize = Number(flags["min-size"] ?? 1);
4448
5654
  const noCohesion = Boolean(flags["no-cohesion"]);
4449
- const C2 = {
4450
- reset: "\x1B[0m",
4451
- cyan: "\x1B[36m",
4452
- yellow: "\x1B[33m",
4453
- green: "\x1B[32m",
4454
- gray: "\x1B[90m",
4455
- red: "\x1B[31m",
4456
- blue: "\x1B[34m",
4457
- magenta: "\x1B[35m"
4458
- };
4459
- function fmtDate(ts) {
4460
- const d = new Date(ts);
4461
- const yr = d.getFullYear();
4462
- const mo = String(d.getMonth() + 1).padStart(2, "0");
4463
- const dy = String(d.getDate()).padStart(2, "0");
4464
- const hh = String(d.getHours()).padStart(2, "0");
4465
- const mm = String(d.getMinutes()).padStart(2, "0");
4466
- return `${yr}-${mo}-${dy} ${hh}:${mm}`;
4467
- }
4468
- function fmtTime(ts) {
4469
- const d = new Date(ts);
4470
- const hh = String(d.getHours()).padStart(2, "0");
4471
- const mm = String(d.getMinutes()).padStart(2, "0");
4472
- return `${hh}:${mm}`;
4473
- }
5655
+ const exportIndex = flags["export"] !== void 0 ? Number(flags["export"]) : void 0;
4474
5656
  const allEntries = db.search({ limit: 1e5 });
4475
5657
  const entriesAsc = [...allEntries].reverse();
4476
5658
  const sessions = groupIntoSessions(entriesAsc, gapHours);
4477
5659
  let embeddings = null;
4478
- if (!noCohesion) {
5660
+ if (!noCohesion && exportIndex === void 0) {
4479
5661
  embeddings = db.getAllEmbeddings();
4480
5662
  }
4481
5663
  for (const session of sessions) {
@@ -4483,21 +5665,37 @@ async function cmdSessions(db, args) {
4483
5665
  session.cohesion = computeSessionCohesion(session, embeddings);
4484
5666
  }
4485
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
+ }
4486
5678
  const filtered = sessions.filter((s) => s.entries.length >= minSize);
4487
5679
  const visible = filtered.slice(-limit);
4488
5680
  if (visible.length === 0) {
4489
5681
  process.stdout.write("No sessions found.\n");
4490
5682
  return;
4491
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
+ };
4492
5694
  for (const session of visible) {
4493
5695
  const projectCounts = /* @__PURE__ */ new Map();
4494
5696
  const languageCounts = /* @__PURE__ */ new Map();
4495
5697
  for (const entry of session.entries) {
4496
- let meta = {};
4497
- try {
4498
- meta = JSON.parse(entry.metadata);
4499
- } catch {
4500
- }
5698
+ const meta = parseMeta3(entry.metadata);
4501
5699
  if (meta.project) projectCounts.set(meta.project, (projectCounts.get(meta.project) ?? 0) + 1);
4502
5700
  if (meta.language) languageCounts.set(meta.language, (languageCounts.get(meta.language) ?? 0) + 1);
4503
5701
  }
@@ -4516,7 +5714,7 @@ async function cmdSessions(db, args) {
4516
5714
  break;
4517
5715
  }
4518
5716
  }
4519
- const startStr = fmtDate(session.startTime.toISOString());
5717
+ const startStr = formatTimestamp4(session.startTime.toISOString());
4520
5718
  const endStr = fmtTime(session.endTime.toISOString());
4521
5719
  const promptWord = session.entries.length === 1 ? "prompt" : "prompts";
4522
5720
  let header = `${C2.cyan}Session ${session.index}${C2.reset}`;
@@ -4534,11 +5732,7 @@ async function cmdSessions(db, args) {
4534
5732
  }
4535
5733
  process.stdout.write(header + "\n");
4536
5734
  for (const entry of session.entries) {
4537
- let meta = {};
4538
- try {
4539
- meta = JSON.parse(entry.metadata);
4540
- } catch {
4541
- }
5735
+ const meta = parseMeta3(entry.metadata);
4542
5736
  const idStr = `${C2.cyan}#${entry.id}${C2.reset}`;
4543
5737
  const toolStr = `${C2.yellow}${entry.tool.padEnd(8)}${C2.reset}`;
4544
5738
  const timeStr = `${C2.gray}${fmtTime(entry.timestamp)}${C2.reset}`;
@@ -4749,7 +5943,7 @@ ${C2.cyan}Cluster #${i + 1}${C2.reset} (${C2.yellow}${cluster.entryIds.length} p
4749
5943
  }
4750
5944
 
4751
5945
  // src/commands/analyze-reusability.ts
4752
- import fs8 from "fs";
5946
+ import fs9 from "fs";
4753
5947
 
4754
5948
  // src/analyzer/reusability.ts
4755
5949
  var ReusabilityAnalyzer = class _ReusabilityAnalyzer {
@@ -4934,7 +6128,7 @@ ${C2.bold}TOP 10 REMOVE (low score + duplicate):${C2.reset}`);
4934
6128
  console.log(` [${C2.red}${scoreStr}${C2.reset}] ${s.prompt.replace(/\n/g, " ").slice(0, 80)}...`);
4935
6129
  });
4936
6130
  if (exportCsv) {
4937
- fs8.writeFileSync(exportCsv, analyzer.toCSV(report));
6131
+ fs9.writeFileSync(exportCsv, analyzer.toCSV(report));
4938
6132
  console.log(`
4939
6133
  ${C2.green}\u2705 Report exported to ${exportCsv}${C2.reset}`);
4940
6134
  }
@@ -4942,7 +6136,7 @@ ${C2.green}\u2705 Report exported to ${exportCsv}${C2.reset}`);
4942
6136
  }
4943
6137
 
4944
6138
  // src/commands/cleanup-reusability.ts
4945
- import fs9 from "fs";
6139
+ import fs10 from "fs";
4946
6140
  async function cmdCleanupReusability(db, _cfg, args) {
4947
6141
  const { flags } = parseFlags(args);
4948
6142
  const threshold = flags["threshold"] ? parseFloat(flags["threshold"]) : 0.7;
@@ -5004,7 +6198,7 @@ Run with ${C2.bold}--force${C2.reset} or without ${C2.bold}--dry-run${C2.reset}
5004
6198
  process.stdout.write(`
5005
6199
  Continue and delete ${candidates.length} prompts? (y/N) `);
5006
6200
  const buffer = Buffer.alloc(16);
5007
- const bytesRead = fs9.readSync(0, buffer, 0, 16);
6201
+ const bytesRead = fs10.readSync(0, buffer, 0, 16, null);
5008
6202
  const answer = buffer.toString("utf8", 0, bytesRead).trim().toLowerCase();
5009
6203
  if (answer !== "y" && answer !== "yes") {
5010
6204
  console.log("Aborted.");
@@ -5022,8 +6216,137 @@ Deleted ${deleted} prompts, freed ~${(totalToFree / 1024 / 1024).toFixed(1)} MB`
5022
6216
  console.log("Database vacuumed.");
5023
6217
  }
5024
6218
 
6219
+ // src/commands/timeline.ts
6220
+ async function cmdTimeline(db, cfg, args) {
6221
+ const { flags, positional } = parseFlags(args);
6222
+ const project = flags["project"] || positional.join(" ") || detectProject(process.cwd()).projectName;
6223
+ if (!project) {
6224
+ process.stderr.write("ph: could not detect project. Use --project <name> or pass a project name.\n");
6225
+ process.exit(1);
6226
+ }
6227
+ const prompts = db.getAllPromptsByProject(project);
6228
+ const memories = db.getAllMemoriesByProject(project);
6229
+ if (prompts.length === 0 && memories.length === 0) {
6230
+ process.stdout.write(`No history found for project "${project}".
6231
+ `);
6232
+ return;
6233
+ }
6234
+ const output = [];
6235
+ output.push(`# Timeline: ${project}
6236
+ `);
6237
+ output.push(`**${prompts.length} prompts** \xB7 **${memories.length} memory entries**
6238
+ `);
6239
+ const events = [
6240
+ ...prompts.map((p) => ({ type: "prompt", timestamp: p.timestamp, prompt: p })),
6241
+ ...memories.map((m) => ({ type: "memory", timestamp: m.created_at, memory: m }))
6242
+ ];
6243
+ events.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
6244
+ let currentDate = "";
6245
+ for (const event of events) {
6246
+ const date = new Date(event.timestamp).toLocaleDateString("en-CA");
6247
+ if (date !== currentDate) {
6248
+ currentDate = date;
6249
+ output.push(`
6250
+ ## ${date}
6251
+ `);
6252
+ }
6253
+ if (event.type === "prompt" && event.prompt) {
6254
+ output.push(formatPromptEntry(event.prompt));
6255
+ }
6256
+ if (event.type === "memory" && event.memory) {
6257
+ output.push(formatMemoryEntry(event.memory));
6258
+ }
6259
+ }
6260
+ process.stdout.write(output.join("\n"));
6261
+ }
6262
+ function formatPromptEntry(e) {
6263
+ let meta = {};
6264
+ try {
6265
+ meta = JSON.parse(e.metadata);
6266
+ } catch {
6267
+ }
6268
+ const time = new Date(e.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
6269
+ const lines = [];
6270
+ lines.push(`### #${e.id} \u2014 ${e.tool} \u2014 ${time}`);
6271
+ if (meta.role) lines.push(`**Role**: ${meta.role}`);
6272
+ if (meta.title) lines.push(`**Title**: ${meta.title}`);
6273
+ if (meta.tags?.length) lines.push(`**Tags**: ${meta.tags.join(", ")}`);
6274
+ if (meta.summary) lines.push(`**Summary**: ${meta.summary}`);
6275
+ if (meta.relevance !== void 0) lines.push(`**Relevance**: ${meta.relevance}/10`);
6276
+ lines.push("");
6277
+ lines.push("**Prompt**:");
6278
+ lines.push("```");
6279
+ lines.push(e.prompt.slice(0, 500) + (e.prompt.length > 500 ? "\n..." : ""));
6280
+ lines.push("```");
6281
+ if (e.response) {
6282
+ lines.push("");
6283
+ lines.push("**Response**:");
6284
+ lines.push("```");
6285
+ lines.push(e.response.slice(0, 300) + (e.response.length > 300 ? "\n..." : ""));
6286
+ lines.push("```");
6287
+ }
6288
+ if (meta.key_insights?.length) {
6289
+ lines.push("");
6290
+ lines.push("**Insights**:");
6291
+ for (const i of meta.key_insights) lines.push(`- ${i}`);
6292
+ }
6293
+ lines.push("---\n");
6294
+ return lines.join("\n");
6295
+ }
6296
+ function formatMemoryEntry(m) {
6297
+ const time = new Date(m.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
6298
+ const lines = [];
6299
+ lines.push(`> \u{1F4CC} **Memory #${m.id} \u2014 ${time}**`);
6300
+ if (m.prompt_ids.length > 0) {
6301
+ lines.push(`> _Derived from prompts: #${m.prompt_ids.join(", #")}_`);
6302
+ }
6303
+ if (m.summary) lines.push(`> **Summary**: ${m.summary}`);
6304
+ if (m.key_insights.length > 0) {
6305
+ lines.push(`> **Insights**:`);
6306
+ for (const i of m.key_insights) lines.push(`> - ${i}`);
6307
+ }
6308
+ if (m.technical_decisions.length > 0) {
6309
+ lines.push(`> **Technical Decisions**:`);
6310
+ for (const d of m.technical_decisions) lines.push(`> - ${d}`);
6311
+ }
6312
+ lines.push("---\n");
6313
+ return lines.join("\n");
6314
+ }
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
+
5025
6348
  // src/commands/wrap.ts
5026
- import path8 from "path";
6349
+ import path9 from "path";
5027
6350
  import os9 from "os";
5028
6351
  async function cmdWrap(dbPath, tool, args, cfg) {
5029
6352
  let debugLog;
@@ -5032,7 +6355,7 @@ async function cmdWrap(dbPath, tool, args, cfg) {
5032
6355
  const cleanArgs = [];
5033
6356
  for (let i = 0; i < args.length; i++) {
5034
6357
  if (args[i] === "--ph-debug") {
5035
- debugLog = path8.join(os9.homedir(), ".ph_debug.log");
6358
+ debugLog = path9.join(os9.homedir(), ".ph_debug.log");
5036
6359
  process.stderr.write(`ph: debug log \u2192 ${debugLog}
5037
6360
  `);
5038
6361
  } else if (args[i] === "--ph-tag" && i + 1 < args.length) {
@@ -5061,28 +6384,17 @@ async function cmdWrap(dbPath, tool, args, cfg) {
5061
6384
  const db = new PhDB(dbPath);
5062
6385
  if (interactive) {
5063
6386
  const workdir = process.cwd();
5064
- const { rootDir, projectName } = detectProject(workdir);
5065
- const language = detectLanguage(rootDir);
5066
6387
  const gitContext = captureGitContext(workdir);
5067
6388
  const onPrompt = (prompt, ts) => {
5068
- const metaObj = { $schema_version: 1 };
5069
- if (projectName) metaObj.project = projectName;
5070
- if (language) metaObj.language = language;
5071
- if (role) metaObj.role = role;
5072
- if (tags.length > 0) metaObj.tags = tags;
5073
- if (gitContext) metaObj.git_context = gitContext;
5074
- const metadata = Object.keys(metaObj).length > 0 ? JSON.stringify(metaObj) : "{}";
5075
- return db.insert({
6389
+ return db.insert(createCaptureRecord({
5076
6390
  timestamp: ts.toISOString(),
5077
6391
  tool,
5078
6392
  prompt,
5079
- response: "",
5080
6393
  args: prompt,
5081
6394
  workdir,
5082
- hostname: os9.hostname(),
5083
- exit_code: 0,
5084
- metadata
5085
- });
6395
+ gitContext,
6396
+ metadata: { ...role ? { role } : {}, ...tags.length > 0 ? { tags } : {} }
6397
+ }));
5086
6398
  };
5087
6399
  const onResponse = (id, response) => {
5088
6400
  if (!response.trim()) return;
@@ -5122,41 +6434,14 @@ async function cmdChat(dbPath, cfg, args) {
5122
6434
  process.exit(1);
5123
6435
  }
5124
6436
  const db = new PhDB(dbPath);
5125
- const memories = db.searchMemories(project, 3);
5126
- const recentPrompts = db.getProjectMemory(project, 5);
5127
- const contextParts = [];
5128
- if (memories.length > 0) {
5129
- contextParts.push(`## Project Knowledge: ${project}`);
5130
- for (const mem of memories) {
5131
- if (mem.summary) contextParts.push(`
5132
- ${mem.summary}`);
5133
- if (mem.key_insights.length > 0) {
5134
- contextParts.push("\nKey Insights:");
5135
- for (const i of mem.key_insights) contextParts.push(` - ${i}`);
5136
- }
5137
- if (mem.technical_decisions.length > 0) {
5138
- contextParts.push("\nTechnical Decisions:");
5139
- for (const d of mem.technical_decisions) contextParts.push(` - ${d}`);
5140
- }
5141
- }
5142
- }
5143
- if (recentPrompts.length > 0) {
5144
- contextParts.push(`
5145
- ---
5146
- ## Recent Context`);
5147
- for (const p of recentPrompts) {
5148
- let meta = {};
5149
- try {
5150
- meta = JSON.parse(p.metadata);
5151
- } catch {
5152
- }
5153
- const summary = meta.summary || p.prompt.slice(0, 100).replace(/\n/g, " ");
5154
- contextParts.push(`
5155
- - #${p.id}: ${summary}`);
5156
- }
5157
- }
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
+ });
5158
6443
  db.close();
5159
- const contextStr = contextParts.join("\n");
6444
+ const contextStr = formatProjectContext(context);
5160
6445
  const fullPrompt = `Context from project "${project}":
5161
6446
 
5162
6447
  ${contextStr}
@@ -5185,13 +6470,16 @@ USAGE:
5185
6470
  ph analyze-reusability [options] Analyze prompt reusability
5186
6471
  ph star <id> Toggle star on a prompt
5187
6472
  ph export <id> [--format txt|json|md] Export a single prompt
5188
- ph import gemini [--dry-run] [--analyze] [--filter] Import from Gemini CLI sessions
5189
- 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
5190
6475
  ph analyze [--limit n] [--force] [--prune] [--dry-run] Analyze prompts with LLM
5191
6476
  ph mcp Start MCP server (Stdio)
5192
6477
  ph server [--port 3001] Start HTTP REST server for remote sync
6478
+ ph timeline [project] Show full project history with prompts and memories
6479
+ ph memory-migrate Merge existing memories into project_summaries
5193
6480
  ph remote push|pull|status Sync prompts with remote ph server
5194
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
5195
6483
  ph cleanup-reusability [--dry-run] [--threshold 0.7] [--force] Cleanup based on reusability
5196
6484
  ph embed-all Generate embeddings for all prompts
5197
6485
  ph log --tool <name> --prompt <text> [--response <text>] Log a prompt+response directly
@@ -5205,7 +6493,7 @@ WRAP FLAGS (placed before the tool name):
5205
6493
 
5206
6494
  SEARCH OPTIONS:
5207
6495
  -i, --interactive Open results in interactive TUI browser
5208
- --tool <name> Filter by tool name (claude, gemini, \u2026)
6496
+ --tool <name> Filter by tool name (claude, gemini, opencode, codex, \u2026)
5209
6497
  --project <name> Filter by project name
5210
6498
  --language <lang> Filter by language (go, typescript, python, \u2026)
5211
6499
  --role <role> Filter by role (debug, refactor, explain, \u2026)
@@ -5215,6 +6503,7 @@ SEARCH OPTIONS:
5215
6503
  --min-relevance <n> Filter by min relevance (0-10)
5216
6504
  --top Show only top quality prompts (quality >= 8)
5217
6505
  --semantic Use semantic search (requires embeddings)
6506
+ --archive Search archived prompts
5218
6507
  --since YYYY-MM-DD
5219
6508
  --until YYYY-MM-DD
5220
6509
  --limit <n> Max results (default 50)
@@ -5232,6 +6521,7 @@ SESSIONS OPTIONS:
5232
6521
  --limit <n> Max sessions to show (default 20)
5233
6522
  --min-size <n> Minimum prompts per session (default 1)
5234
6523
  --no-cohesion Skip semantic cohesion computation
6524
+ --export <n> Export session N as markdown
5235
6525
 
5236
6526
  CLUSTER OPTIONS:
5237
6527
  -k <number> Number of clusters (default 5)
@@ -5268,6 +6558,31 @@ EXAMPLES:
5268
6558
  ph config set ollama-model llama3.2:latest
5269
6559
  ph ollama-models List models available on the local Ollama instance
5270
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
+ }
5271
6586
  async function main() {
5272
6587
  const argv = process.argv.slice(2);
5273
6588
  const cfg = load();
@@ -5278,28 +6593,9 @@ async function main() {
5278
6593
  if (argv.length === 0) {
5279
6594
  if (isTerminal()) {
5280
6595
  const db = new PhDB(dbPath);
5281
- process.stdout.write("\x1B[?1049h");
5282
- let pendingRerun = null;
5283
- try {
5284
- const { waitUntilExit } = render(
5285
- React7.createElement(BrowseApp, {
5286
- db,
5287
- onRerun: (tool, prompt) => {
5288
- pendingRerun = { tool, prompt };
5289
- }
5290
- })
5291
- );
5292
- await waitUntilExit();
5293
- } finally {
5294
- process.stdout.write("\x1B[?1049l");
5295
- db.close();
5296
- }
5297
- if (pendingRerun) {
5298
- const { tool, prompt } = pendingRerun;
5299
- const realBin = resolveRealBinary(tool);
5300
- const child = spawnSync2(realBin, [prompt], { stdio: "inherit" });
5301
- process.exit(child.status ?? 0);
5302
- }
6596
+ const rerun = await openBrowser(db);
6597
+ db.close();
6598
+ runRerun(rerun);
5303
6599
  process.exit(0);
5304
6600
  } else {
5305
6601
  process.stdout.write(USAGE);
@@ -5328,39 +6624,21 @@ async function main() {
5328
6624
  const query = positional.join(" ");
5329
6625
  if (flags["i"] || flags["interactive"]) {
5330
6626
  const db2 = new PhDB(dbPath);
5331
- process.stdout.write("\x1B[?1049h");
5332
- let pendingRerun = null;
5333
- try {
5334
- const { waitUntilExit } = render(
5335
- React7.createElement(BrowseApp, {
5336
- db: db2,
5337
- initialTextFilter: query,
5338
- initialFilters: {
5339
- tool: flags["tool"],
5340
- project: flags["project"],
5341
- language: flags["language"],
5342
- role: flags["role"],
5343
- tag: flags["tag"],
5344
- starred: Boolean(flags["starred"]),
5345
- minQuality: flags["top"] ? 8 : flags["min-quality"] ? Number(flags["min-quality"]) : void 0,
5346
- minRelevance: flags["min-relevance"] ? Number(flags["min-relevance"]) : void 0
5347
- },
5348
- onRerun: (tool, prompt) => {
5349
- pendingRerun = { tool, prompt };
5350
- }
5351
- })
5352
- );
5353
- await waitUntilExit();
5354
- } finally {
5355
- process.stdout.write("\x1B[?1049l");
5356
- db2.close();
5357
- }
5358
- if (pendingRerun) {
5359
- const { tool, prompt } = pendingRerun;
5360
- const realBin = resolveRealBinary(tool);
5361
- const child = spawnSync2(realBin, [prompt], { stdio: "inherit" });
5362
- process.exit(child.status ?? 0);
5363
- }
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);
5364
6642
  break;
5365
6643
  }
5366
6644
  const db = new PhDB(dbPath);
@@ -5434,6 +6712,12 @@ async function main() {
5434
6712
  db.close();
5435
6713
  break;
5436
6714
  }
6715
+ case "memory-migrate": {
6716
+ const db = new PhDB(dbPath);
6717
+ cmdMemoryMigrate(db);
6718
+ db.close();
6719
+ break;
6720
+ }
5437
6721
  case "mcp": {
5438
6722
  await runMCPServer();
5439
6723
  break;
@@ -5480,34 +6764,21 @@ async function main() {
5480
6764
  }
5481
6765
  case "browse": {
5482
6766
  const db = new PhDB(dbPath);
5483
- process.stdout.write("\x1B[?1049h");
5484
- let pendingRerun = null;
5485
- try {
5486
- const { waitUntilExit } = render(
5487
- React7.createElement(BrowseApp, {
5488
- db,
5489
- onRerun: (tool, prompt) => {
5490
- pendingRerun = { tool, prompt };
5491
- }
5492
- })
5493
- );
5494
- await waitUntilExit();
5495
- } finally {
5496
- process.stdout.write("\x1B[?1049l");
5497
- db.close();
5498
- }
5499
- if (pendingRerun) {
5500
- const { tool, prompt } = pendingRerun;
5501
- const realBin = resolveRealBinary(tool);
5502
- const child = spawnSync2(realBin, [prompt], { stdio: "inherit" });
5503
- process.exit(child.status ?? 0);
5504
- }
6767
+ const rerun = await openBrowser(db);
6768
+ db.close();
6769
+ runRerun(rerun);
5505
6770
  break;
5506
6771
  }
5507
6772
  case "chat": {
5508
6773
  await cmdChat(dbPath, cfg, cmdArgs);
5509
6774
  break;
5510
6775
  }
6776
+ case "timeline": {
6777
+ const db = new PhDB(dbPath);
6778
+ await cmdTimeline(db, cfg, cmdArgs);
6779
+ db.close();
6780
+ break;
6781
+ }
5511
6782
  case "ollama-models": {
5512
6783
  const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
5513
6784
  try {