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