@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.
- package/CHANGELOG.md +7 -0
- package/README.md +147 -58
- package/dist/cli.js +2292 -1021
- package/dist/cli.js.map +1 -1
- package/docs/evals/context-retrieval-golden.json +26 -0
- package/docs/evals/context-retrieval.md +27 -0
- package/docs/hooks.md +13 -7
- package/docs/ph-analysis.html +145 -0
- package/docs/ph-manual.md +352 -231
- package/docs/specs/SPEC-001-architecture-cli.md +82 -0
- package/docs/specs/SPEC-002-database-layer.md +105 -0
- package/docs/specs/SPEC-003-capture-modes.md +110 -0
- package/docs/specs/SPEC-004-filter-metadata.md +82 -0
- package/docs/specs/SPEC-005-search.md +102 -0
- package/docs/specs/SPEC-006-analysis-pipeline.md +98 -0
- package/docs/specs/SPEC-007-memory-summaries.md +96 -0
- package/docs/specs/SPEC-008-embeddings.md +90 -0
- package/docs/specs/SPEC-009-retention-archive.md +91 -0
- package/docs/specs/SPEC-010-remote-sync.md +106 -0
- package/docs/specs/SPEC-011-mcp-server.md +94 -0
- package/docs/specs/SPEC-012-tui-browser.md +98 -0
- package/docs/specs/SPEC-013-importers.md +109 -0
- package/docs/specs/SPEC-014-analytics-commands.md +109 -0
- package/docs/specs/SPEC-015-config-env.md +85 -0
- package/docs/specs/SPEC-INDEX.md +35 -0
- package/docs/specs/SPEC-ISSUES.md +122 -0
- package/docs/specs/_TEMPLATE.md +65 -0
- package/docs/superpowers/plans/2026-06-12-ph-evolution-phase1.md +466 -0
- package/docs/superpowers/plans/2026-06-12-ph-evolution-phase2.md +351 -0
- package/docs/superpowers/specs/2026-06-12-ph-evolution-design.md +173 -0
- package/hooks/codex/README.md +54 -0
- package/hooks/codex/ph-hook.sh +14 -0
- package/hooks/opencode/ph-plugin.ts +60 -57
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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/
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
const
|
|
798
|
-
const
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
960
|
-
if (
|
|
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
|
-
|
|
967
|
-
parts.push(`## ${mem.summary || "Project Memory"}
|
|
1572
|
+
parts.push(`## ${summary.summary || "Project Memory"}
|
|
968
1573
|
`);
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
|
1036
|
-
|
|
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
|
-
|
|
1039
|
-
return meta.project === project;
|
|
1674
|
+
meta = JSON.parse(p.metadata);
|
|
1040
1675
|
} catch {
|
|
1041
|
-
return false;
|
|
1042
1676
|
}
|
|
1043
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
|
1961
|
+
const path10 = url.pathname;
|
|
1188
1962
|
const method = req.method || "GET";
|
|
1189
|
-
if (method === "GET" &&
|
|
1190
|
-
return json(res, 200, { status: "ok", dbPath: db.
|
|
1963
|
+
if (method === "GET" && path10 === "/health") {
|
|
1964
|
+
return json(res, 200, { status: "ok", dbPath: db.dbPath });
|
|
1191
1965
|
}
|
|
1192
|
-
if (method === "POST" &&
|
|
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" &&
|
|
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" &&
|
|
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
|
|
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" &&
|
|
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 =
|
|
1239
|
-
const existing = db.
|
|
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" &&
|
|
2029
|
+
if (method === "POST" && path10 === "/api/sync/pull") {
|
|
1264
2030
|
const { since } = JSON.parse(body);
|
|
1265
2031
|
const limit = 1e4;
|
|
1266
|
-
|
|
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" &&
|
|
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" &&
|
|
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" &&
|
|
1286
|
-
const total
|
|
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} ${
|
|
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 =
|
|
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.
|
|
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
|
|
1471
|
-
import { Box as
|
|
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: "
|
|
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: "^
|
|
1541
|
-
react: "^19.2.
|
|
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.
|
|
1552
|
-
"@types/react": "^19.2.
|
|
1553
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
1554
|
-
"@typescript-eslint/parser": "^8.
|
|
1555
|
-
eslint: "^9.39.
|
|
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: "^
|
|
1563
|
-
vitest: "^4.1.
|
|
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/
|
|
1656
|
-
import {
|
|
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/
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
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
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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 (
|
|
1744
|
-
|
|
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
|
-
|
|
1747
|
-
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
2510
|
+
return result;
|
|
1748
2511
|
}
|
|
1749
|
-
function
|
|
1750
|
-
|
|
1751
|
-
if (
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
|
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
|
-
|
|
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 ?
|
|
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 ?
|
|
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__ */
|
|
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
|
|
1810
|
-
import { Box as
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
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.
|
|
1833
|
-
current = current.
|
|
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] =
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
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
|
-
}
|
|
1876
|
-
const meta =
|
|
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 =
|
|
2965
|
+
const allLines = useMemo3(() => {
|
|
1880
2966
|
if (!entry) return [];
|
|
1881
|
-
if (activeTab === "prompt") return
|
|
1882
|
-
if (activeTab === "response") return
|
|
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
|
-
|
|
1888
|
-
|
|
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
|
-
|
|
1892
|
-
|
|
2978
|
+
for (const l of wrapTextLines(mem.summary, contentWidth)) push2(l);
|
|
2979
|
+
push2("");
|
|
1893
2980
|
}
|
|
1894
2981
|
if (mem.key_insights.length > 0) {
|
|
1895
|
-
|
|
1896
|
-
for (const i of mem.key_insights)
|
|
1897
|
-
|
|
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
|
-
|
|
1901
|
-
for (const d of mem.technical_decisions)
|
|
1902
|
-
|
|
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
|
-
|
|
1906
|
-
|
|
2992
|
+
push2("\u2500\u2500 This Entry \u2500\u2500");
|
|
2993
|
+
push2("");
|
|
1907
2994
|
}
|
|
1908
2995
|
}
|
|
1909
2996
|
if (meta.summary) {
|
|
1910
|
-
|
|
1911
|
-
|
|
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
|
-
|
|
3002
|
+
for (const l of wrapTextLines(`\u2022 ${insight}`, contentWidth)) push2(l);
|
|
1916
3003
|
}
|
|
1917
|
-
|
|
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
|
-
|
|
3010
|
+
useInput2((_char, key) => {
|
|
1926
3011
|
if (!isFocused) return;
|
|
1927
|
-
if (_char === "1")
|
|
1928
|
-
if (_char === "2")
|
|
1929
|
-
if (_char === "3")
|
|
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__ */
|
|
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__ */
|
|
1951
|
-
|
|
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__ */
|
|
1960
|
-
|
|
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__ */
|
|
1969
|
-
|
|
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__ */
|
|
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] =
|
|
3086
|
+
const [dimensions, setDimensions] = useState3({
|
|
2030
3087
|
columns: process.stdout.columns || 120,
|
|
2031
3088
|
rows: process.stdout.rows || 24
|
|
2032
3089
|
});
|
|
2033
|
-
|
|
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 =
|
|
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 =
|
|
2127
|
-
const [activeTab, setActiveTab] =
|
|
2128
|
-
const [scrollOffset, setScrollOffset] =
|
|
2129
|
-
const [copied, setCopied] =
|
|
2130
|
-
|
|
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
|
-
}
|
|
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 =
|
|
3145
|
+
const lines = useMemo4(() => {
|
|
2136
3146
|
if (activeTab === "prompt" || activeTab === "response") {
|
|
2137
|
-
return
|
|
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
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
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
|
-
|
|
3157
|
+
push2("KEY INSIGHTS:");
|
|
2147
3158
|
for (const insight of meta.key_insights) {
|
|
2148
|
-
|
|
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
|
-
|
|
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")
|
|
2163
|
-
else if (char === "2")
|
|
2164
|
-
else if (char === "3")
|
|
2165
|
-
else
|
|
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__ */
|
|
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 =
|
|
2186
|
-
const [field, setField] =
|
|
2187
|
-
const [titleValue, setTitleValue] =
|
|
2188
|
-
const [roleValue, setRoleValue] =
|
|
2189
|
-
const [tagsValue, setTagsValue] =
|
|
2190
|
-
|
|
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__ */
|
|
2223
|
-
};
|
|
2224
|
-
var RerunView = ({ entry, onConfirm, onClose, theme }) => {
|
|
2225
|
-
const [field, setField] =
|
|
2226
|
-
const [toolValue, setToolValue] =
|
|
2227
|
-
const [promptValue, setPromptValue] =
|
|
2228
|
-
const meta =
|
|
2229
|
-
const git = meta.git_context;
|
|
2230
|
-
|
|
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 (
|
|
2324
|
-
|
|
3240
|
+
if (key.return) {
|
|
3241
|
+
onConfirm(toolValue.trim(), promptValue.trim());
|
|
2325
3242
|
return;
|
|
2326
3243
|
}
|
|
2327
|
-
if (key.
|
|
2328
|
-
|
|
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
|
-
|
|
2351
|
-
if (key.
|
|
2352
|
-
|
|
2353
|
-
if (key.
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
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
|
-
|
|
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] =
|
|
2378
|
-
const [cursor, setCursor] =
|
|
2379
|
-
const [dirty, setDirty] =
|
|
2380
|
-
const [saved, setSaved] =
|
|
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
|
-
|
|
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__ */
|
|
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__ */
|
|
2430
|
-
})), /* @__PURE__ */
|
|
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] =
|
|
2435
|
-
const [refreshKey, setRefreshKey] =
|
|
2436
|
-
const [
|
|
2437
|
-
const [
|
|
2438
|
-
const [
|
|
2439
|
-
const [
|
|
2440
|
-
const [
|
|
2441
|
-
const [
|
|
2442
|
-
const [
|
|
2443
|
-
const [
|
|
2444
|
-
const [
|
|
2445
|
-
const [
|
|
2446
|
-
const [
|
|
2447
|
-
const [
|
|
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 =
|
|
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
|
-
|
|
3373
|
+
useEffect(() => {
|
|
2461
3374
|
setCursor((c) => Math.min(c, Math.max(0, entries.length - 1)));
|
|
2462
3375
|
}, [entries.length]);
|
|
2463
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
2686
|
-
})), /* @__PURE__ */
|
|
2687
|
-
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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
|
|
3038
|
-
import
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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/
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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 {
|
|
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,
|
|
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
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
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
|
|
5126
|
-
const
|
|
5127
|
-
const
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
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 =
|
|
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
|
|
5189
|
-
|
|
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
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
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
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
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
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
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 {
|