@gi4nks/ph 1.5.0 → 1.7.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 +9 -2
- package/LICENSE +1 -1
- package/README.md +155 -145
- package/dist/cli.js +2279 -1052
- 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 +375 -373
- 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/claude/README.md +1 -1
- package/hooks/codex/README.md +54 -0
- package/hooks/codex/ph-hook.sh +14 -0
- package/hooks/gemini/README.md +1 -1
- package/hooks/opencode/ph-plugin.ts +60 -57
- package/package.json +18 -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
|
|
@@ -21,6 +21,7 @@ function load() {
|
|
|
21
21
|
}
|
|
22
22
|
function save(cfg) {
|
|
23
23
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
24
|
+
fs.chmodSync(CONFIG_PATH, 384);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// src/db/index.ts
|
|
@@ -29,15 +30,103 @@ import path2 from "path";
|
|
|
29
30
|
import os2 from "os";
|
|
30
31
|
import { createHash } from "crypto";
|
|
31
32
|
import * as sqliteVec from "sqlite-vec";
|
|
33
|
+
|
|
34
|
+
// src/db/semantic-index.ts
|
|
35
|
+
var SemanticIndex = class {
|
|
36
|
+
constructor(db) {
|
|
37
|
+
this.db = db;
|
|
38
|
+
}
|
|
39
|
+
save(promptId, vector) {
|
|
40
|
+
const encoded = this.encode(vector);
|
|
41
|
+
this.db.prepare("INSERT OR REPLACE INTO embeddings (prompt_id, vector) VALUES (?, ?)").run(promptId, encoded);
|
|
42
|
+
this.db.prepare("INSERT OR REPLACE INTO vec_embeddings(rowid, embedding) VALUES (?, vec_f32(?))").run(BigInt(promptId), encoded);
|
|
43
|
+
}
|
|
44
|
+
search(query, limit, project) {
|
|
45
|
+
const encodedQuery = this.encode(query);
|
|
46
|
+
const projectFilter = project ? "AND json_extract(p.metadata, '$.project') = ?" : "";
|
|
47
|
+
const candidateLimit = project ? Math.max(1, this.db.prepare("SELECT count(*) AS count FROM vec_embeddings").get().count) : limit;
|
|
48
|
+
const sql = `
|
|
49
|
+
SELECT p.*, v.distance
|
|
50
|
+
FROM vec_embeddings v
|
|
51
|
+
JOIN prompts p ON p.id = v.rowid
|
|
52
|
+
WHERE v.embedding MATCH vec_f32(?) AND k = ?
|
|
53
|
+
${projectFilter}
|
|
54
|
+
ORDER BY v.distance ASC
|
|
55
|
+
`;
|
|
56
|
+
return this.db.prepare(project ? `${sql} LIMIT ?` : sql).all(
|
|
57
|
+
encodedQuery,
|
|
58
|
+
candidateLimit,
|
|
59
|
+
...project ? [project, limit] : []
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
loadAll() {
|
|
63
|
+
const rows = this.db.prepare("SELECT prompt_id, vector FROM embeddings").all();
|
|
64
|
+
return new Map(rows.map((row) => [row.prompt_id, this.decode(row.vector)]));
|
|
65
|
+
}
|
|
66
|
+
findMissingPrompts() {
|
|
67
|
+
return this.db.prepare(
|
|
68
|
+
`SELECT p.* FROM prompts p
|
|
69
|
+
LEFT JOIN vec_embeddings e ON e.rowid = p.id
|
|
70
|
+
WHERE e.rowid IS NULL`
|
|
71
|
+
).all();
|
|
72
|
+
}
|
|
73
|
+
encode(vector) {
|
|
74
|
+
const buffer = Buffer.alloc(vector.length * 4);
|
|
75
|
+
for (let i = 0; i < vector.length; i++) buffer.writeFloatLE(vector[i], i * 4);
|
|
76
|
+
return buffer;
|
|
77
|
+
}
|
|
78
|
+
decode(buffer) {
|
|
79
|
+
const vector = new Float32Array(buffer.length / 4);
|
|
80
|
+
for (let i = 0; i < vector.length; i++) vector[i] = buffer.readFloatLE(i * 4);
|
|
81
|
+
return vector;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/db/index.ts
|
|
32
86
|
function defaultPath() {
|
|
33
87
|
return path2.join(os2.homedir(), ".prompt_history.db");
|
|
34
88
|
}
|
|
35
|
-
var PhDB = class {
|
|
89
|
+
var PhDB = class _PhDB {
|
|
90
|
+
static PROJECT_SUMMARIES_SCHEMA = `
|
|
91
|
+
CREATE TABLE IF NOT EXISTS project_summaries (
|
|
92
|
+
project TEXT PRIMARY KEY,
|
|
93
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
94
|
+
key_insights TEXT NOT NULL DEFAULT '[]',
|
|
95
|
+
technical_decisions TEXT NOT NULL DEFAULT '[]',
|
|
96
|
+
prompt_count INTEGER NOT NULL DEFAULT 0,
|
|
97
|
+
first_analyzed TEXT NOT NULL,
|
|
98
|
+
last_analyzed TEXT NOT NULL,
|
|
99
|
+
git_context_snapshot TEXT
|
|
100
|
+
);
|
|
101
|
+
`;
|
|
102
|
+
static ARCHIVE_SCHEMA = `
|
|
103
|
+
CREATE TABLE IF NOT EXISTS prompts_archive (
|
|
104
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
105
|
+
timestamp TEXT NOT NULL,
|
|
106
|
+
tool TEXT NOT NULL,
|
|
107
|
+
prompt TEXT NOT NULL,
|
|
108
|
+
response TEXT NOT NULL DEFAULT '',
|
|
109
|
+
args TEXT NOT NULL DEFAULT '',
|
|
110
|
+
workdir TEXT NOT NULL DEFAULT '',
|
|
111
|
+
hostname TEXT NOT NULL DEFAULT '',
|
|
112
|
+
exit_code INTEGER NOT NULL DEFAULT 0,
|
|
113
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
114
|
+
archived_at TEXT NOT NULL,
|
|
115
|
+
original_id INTEGER
|
|
116
|
+
);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_archive_timestamp ON prompts_archive(timestamp);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_archive_original_id ON prompts_archive(original_id);
|
|
119
|
+
`;
|
|
36
120
|
db;
|
|
121
|
+
semanticIndex;
|
|
122
|
+
/** Absolute path of the SQLite file (used by the HTTP server health endpoint). */
|
|
123
|
+
dbPath;
|
|
37
124
|
constructor(dbPath) {
|
|
125
|
+
this.dbPath = dbPath;
|
|
38
126
|
this.db = new Database(dbPath);
|
|
39
127
|
this.db.pragma("journal_mode = WAL");
|
|
40
128
|
sqliteVec.load(this.db);
|
|
129
|
+
this.semanticIndex = new SemanticIndex(this.db);
|
|
41
130
|
this.migrate();
|
|
42
131
|
}
|
|
43
132
|
migrate() {
|
|
@@ -104,6 +193,8 @@ var PhDB = class {
|
|
|
104
193
|
);
|
|
105
194
|
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
|
|
106
195
|
`);
|
|
196
|
+
this.db.exec(_PhDB.PROJECT_SUMMARIES_SCHEMA);
|
|
197
|
+
this.db.exec(_PhDB.ARCHIVE_SCHEMA);
|
|
107
198
|
const rowsWithoutSchema = this.db.prepare("SELECT id, metadata FROM prompts WHERE json_extract(metadata, '$.\\$schema_version') IS NULL").all();
|
|
108
199
|
for (const row of rowsWithoutSchema) {
|
|
109
200
|
try {
|
|
@@ -163,6 +254,7 @@ var PhDB = class {
|
|
|
163
254
|
`);
|
|
164
255
|
}
|
|
165
256
|
}
|
|
257
|
+
/** `response` is optional at the call site (runtime defaults to ''). */
|
|
166
258
|
insert(entry) {
|
|
167
259
|
const meta = entry.metadata || "{}";
|
|
168
260
|
const response = entry.response ?? "";
|
|
@@ -281,45 +373,16 @@ var PhDB = class {
|
|
|
281
373
|
}
|
|
282
374
|
}
|
|
283
375
|
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);
|
|
376
|
+
this.semanticIndex.save(id, vector);
|
|
290
377
|
}
|
|
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);
|
|
378
|
+
searchSemantic(queryVector, limit, project) {
|
|
379
|
+
return this.semanticIndex.search(queryVector, limit, project);
|
|
304
380
|
}
|
|
305
381
|
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;
|
|
382
|
+
return this.semanticIndex.loadAll();
|
|
316
383
|
}
|
|
317
384
|
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();
|
|
385
|
+
return this.semanticIndex.findMissingPrompts();
|
|
323
386
|
}
|
|
324
387
|
getAllPrompts(limit = 1e6) {
|
|
325
388
|
return this.db.prepare("SELECT * FROM prompts ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
@@ -330,6 +393,15 @@ var PhDB = class {
|
|
|
330
393
|
getPromptCount() {
|
|
331
394
|
return this.db.prepare("SELECT count(*) as c FROM prompts").get().c;
|
|
332
395
|
}
|
|
396
|
+
getPromptCountSince(timestamp) {
|
|
397
|
+
return this.db.prepare("SELECT count(*) as c FROM prompts WHERE timestamp > ?").get(timestamp).c;
|
|
398
|
+
}
|
|
399
|
+
getStats() {
|
|
400
|
+
const total = this.db.prepare("SELECT count(*) as c FROM prompts").get().c;
|
|
401
|
+
const totalMemories = this.db.prepare("SELECT count(*) as c FROM memories").get().c;
|
|
402
|
+
const byTool = this.db.prepare("SELECT tool, count(*) as count FROM prompts GROUP BY tool ORDER BY count DESC").all();
|
|
403
|
+
return { total, totalMemories, byTool };
|
|
404
|
+
}
|
|
333
405
|
getPromptBySyncHash(hash) {
|
|
334
406
|
return this.db.prepare("SELECT * FROM prompts WHERE json_extract(metadata, '$.sync_hash') = ?").get(hash);
|
|
335
407
|
}
|
|
@@ -369,6 +441,72 @@ var PhDB = class {
|
|
|
369
441
|
}
|
|
370
442
|
return 0;
|
|
371
443
|
}
|
|
444
|
+
archivePrompts(ids) {
|
|
445
|
+
if (ids.length === 0) return 0;
|
|
446
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
447
|
+
const select = this.db.prepare(`
|
|
448
|
+
SELECT id, timestamp, tool, prompt, response, args, workdir, hostname, exit_code, metadata
|
|
449
|
+
FROM prompts WHERE id = ?
|
|
450
|
+
`);
|
|
451
|
+
const insert = this.db.prepare(`
|
|
452
|
+
INSERT INTO prompts_archive (timestamp, tool, prompt, response, args, workdir, hostname, exit_code, metadata, archived_at, original_id)
|
|
453
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
454
|
+
`);
|
|
455
|
+
const del = this.db.prepare("DELETE FROM prompts WHERE id = ?");
|
|
456
|
+
const transaction = this.db.transaction((toArchive) => {
|
|
457
|
+
let count = 0;
|
|
458
|
+
for (const id of toArchive) {
|
|
459
|
+
const row = select.get(id);
|
|
460
|
+
if (!row) continue;
|
|
461
|
+
insert.run(
|
|
462
|
+
row.timestamp,
|
|
463
|
+
row.tool,
|
|
464
|
+
row.prompt,
|
|
465
|
+
row.response,
|
|
466
|
+
row.args,
|
|
467
|
+
row.workdir,
|
|
468
|
+
row.hostname,
|
|
469
|
+
row.exit_code,
|
|
470
|
+
row.metadata,
|
|
471
|
+
now,
|
|
472
|
+
id
|
|
473
|
+
);
|
|
474
|
+
del.run(id);
|
|
475
|
+
count++;
|
|
476
|
+
}
|
|
477
|
+
return count;
|
|
478
|
+
});
|
|
479
|
+
return transaction(ids);
|
|
480
|
+
}
|
|
481
|
+
searchArchive(opts) {
|
|
482
|
+
let sql = "SELECT * FROM prompts_archive WHERE 1=1";
|
|
483
|
+
const params = [];
|
|
484
|
+
if (opts.since) {
|
|
485
|
+
sql += " AND timestamp >= ?";
|
|
486
|
+
params.push(opts.since);
|
|
487
|
+
}
|
|
488
|
+
if (opts.until) {
|
|
489
|
+
sql += " AND timestamp <= ?";
|
|
490
|
+
params.push(opts.until);
|
|
491
|
+
}
|
|
492
|
+
sql += " ORDER BY timestamp DESC";
|
|
493
|
+
if (opts.limit) {
|
|
494
|
+
sql += " LIMIT ?";
|
|
495
|
+
params.push(opts.limit);
|
|
496
|
+
}
|
|
497
|
+
return this.db.prepare(sql).all(...params);
|
|
498
|
+
}
|
|
499
|
+
purgeArchive(beforeDate) {
|
|
500
|
+
const result = this.db.prepare("DELETE FROM prompts_archive WHERE archived_at < ?").run(beforeDate);
|
|
501
|
+
return result.changes;
|
|
502
|
+
}
|
|
503
|
+
getArchiveStats() {
|
|
504
|
+
const row = this.db.prepare(`
|
|
505
|
+
SELECT COUNT(*) as total, MIN(timestamp) as oldest, MAX(timestamp) as newest
|
|
506
|
+
FROM prompts_archive
|
|
507
|
+
`).get();
|
|
508
|
+
return row;
|
|
509
|
+
}
|
|
372
510
|
vacuum() {
|
|
373
511
|
this.db.exec("VACUUM");
|
|
374
512
|
}
|
|
@@ -460,10 +598,62 @@ var PhDB = class {
|
|
|
460
598
|
last_accessed: void 0
|
|
461
599
|
});
|
|
462
600
|
}
|
|
601
|
+
getLastMemoryForProject(project) {
|
|
602
|
+
const row = this.db.prepare("SELECT * FROM memories WHERE project = ? ORDER BY updated_at DESC LIMIT 1").get(project);
|
|
603
|
+
if (!row) return void 0;
|
|
604
|
+
return this.hydrateMemory(row);
|
|
605
|
+
}
|
|
463
606
|
getAllProjectsWithMemories() {
|
|
464
607
|
const rows = this.db.prepare("SELECT DISTINCT project FROM memories ORDER BY project").all();
|
|
465
608
|
return rows.map((r) => r.project);
|
|
466
609
|
}
|
|
610
|
+
upsertProjectSummary(params) {
|
|
611
|
+
const existing = this.getProjectSummary(params.project);
|
|
612
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
613
|
+
if (existing) {
|
|
614
|
+
const allInsights = /* @__PURE__ */ new Set([...existing.key_insights, ...params.key_insights]);
|
|
615
|
+
const allDecisions = /* @__PURE__ */ new Set([...existing.technical_decisions, ...params.technical_decisions]);
|
|
616
|
+
this.db.prepare(`
|
|
617
|
+
UPDATE project_summaries
|
|
618
|
+
SET summary = ?,
|
|
619
|
+
key_insights = ?,
|
|
620
|
+
technical_decisions = ?,
|
|
621
|
+
prompt_count = prompt_count + 1,
|
|
622
|
+
last_analyzed = ?,
|
|
623
|
+
git_context_snapshot = COALESCE(?, git_context_snapshot)
|
|
624
|
+
WHERE project = ?
|
|
625
|
+
`).run(
|
|
626
|
+
params.summary,
|
|
627
|
+
JSON.stringify([...allInsights]),
|
|
628
|
+
JSON.stringify([...allDecisions]),
|
|
629
|
+
now,
|
|
630
|
+
params.git_context_snapshot || null,
|
|
631
|
+
params.project
|
|
632
|
+
);
|
|
633
|
+
} else {
|
|
634
|
+
this.db.prepare(`
|
|
635
|
+
INSERT INTO project_summaries (project, summary, key_insights, technical_decisions, prompt_count, first_analyzed, last_analyzed, git_context_snapshot)
|
|
636
|
+
VALUES (?, ?, ?, ?, 1, ?, ?, ?)
|
|
637
|
+
`).run(
|
|
638
|
+
params.project,
|
|
639
|
+
params.summary,
|
|
640
|
+
JSON.stringify(params.key_insights),
|
|
641
|
+
JSON.stringify(params.technical_decisions),
|
|
642
|
+
now,
|
|
643
|
+
now,
|
|
644
|
+
params.git_context_snapshot || null
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
getProjectSummary(project) {
|
|
649
|
+
const row = this.db.prepare("SELECT * FROM project_summaries WHERE project = ?").get(project);
|
|
650
|
+
if (!row) return null;
|
|
651
|
+
return this.hydrateProjectSummary(row);
|
|
652
|
+
}
|
|
653
|
+
getAllProjectsWithSummaries() {
|
|
654
|
+
const rows = this.db.prepare("SELECT project FROM project_summaries ORDER BY project").all();
|
|
655
|
+
return rows.map((r) => r.project);
|
|
656
|
+
}
|
|
467
657
|
recordMemoryAccess(id) {
|
|
468
658
|
this.db.prepare("UPDATE memories SET access_count = access_count + 1, last_accessed = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), id);
|
|
469
659
|
}
|
|
@@ -485,6 +675,18 @@ var PhDB = class {
|
|
|
485
675
|
last_accessed: row.last_accessed
|
|
486
676
|
};
|
|
487
677
|
}
|
|
678
|
+
hydrateProjectSummary(row) {
|
|
679
|
+
return {
|
|
680
|
+
project: row.project,
|
|
681
|
+
summary: row.summary,
|
|
682
|
+
key_insights: JSON.parse(row.key_insights),
|
|
683
|
+
technical_decisions: JSON.parse(row.technical_decisions),
|
|
684
|
+
prompt_count: row.prompt_count,
|
|
685
|
+
first_analyzed: row.first_analyzed,
|
|
686
|
+
last_analyzed: row.last_analyzed,
|
|
687
|
+
git_context_snapshot: row.git_context_snapshot
|
|
688
|
+
};
|
|
689
|
+
}
|
|
488
690
|
getAllPromptsByProject(project) {
|
|
489
691
|
return this.db.prepare(`
|
|
490
692
|
SELECT * FROM prompts
|
|
@@ -492,6 +694,20 @@ var PhDB = class {
|
|
|
492
694
|
ORDER BY timestamp ASC
|
|
493
695
|
`).all(project);
|
|
494
696
|
}
|
|
697
|
+
getPromptsByProjectPaginated(project, page, pageSize) {
|
|
698
|
+
const total = this.db.prepare("SELECT COUNT(*) as count FROM prompts WHERE json_extract(metadata, '$.project') = ?").get(project);
|
|
699
|
+
const entries = this.db.prepare(`
|
|
700
|
+
SELECT * FROM prompts
|
|
701
|
+
WHERE json_extract(metadata, '$.project') = ?
|
|
702
|
+
ORDER BY timestamp DESC
|
|
703
|
+
LIMIT ? OFFSET ?
|
|
704
|
+
`).all(project, pageSize, (page - 1) * pageSize);
|
|
705
|
+
return { entries, total: total.count };
|
|
706
|
+
}
|
|
707
|
+
getPromptCountByProject(project) {
|
|
708
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM prompts WHERE json_extract(metadata, '$.project') = ?").get(project);
|
|
709
|
+
return row.count;
|
|
710
|
+
}
|
|
495
711
|
getAllMemoriesByProject(project) {
|
|
496
712
|
const rows = this.db.prepare("SELECT * FROM memories WHERE project = ? ORDER BY created_at ASC").all(project);
|
|
497
713
|
return rows.map((r) => this.hydrateMemory(r));
|
|
@@ -515,6 +731,34 @@ function cosineSimilarity(a, b) {
|
|
|
515
731
|
// src/runner/inline.ts
|
|
516
732
|
import { spawn, execFileSync as execFileSync2 } from "child_process";
|
|
517
733
|
import fs3 from "fs";
|
|
734
|
+
|
|
735
|
+
// src/runner/git-context.ts
|
|
736
|
+
import { execFileSync } from "child_process";
|
|
737
|
+
function captureGitContext(workdir) {
|
|
738
|
+
try {
|
|
739
|
+
const branch = execFileSync("git", ["-C", workdir, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
740
|
+
encoding: "utf-8",
|
|
741
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
742
|
+
}).trim();
|
|
743
|
+
const filesRaw = execFileSync("git", ["-C", workdir, "diff", "--name-only", "HEAD"], {
|
|
744
|
+
encoding: "utf-8",
|
|
745
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
746
|
+
}).trim();
|
|
747
|
+
const files = filesRaw ? filesRaw.split("\n").filter(Boolean) : [];
|
|
748
|
+
let diff = execFileSync("git", ["-C", workdir, "diff", "HEAD"], {
|
|
749
|
+
encoding: "utf-8",
|
|
750
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
751
|
+
});
|
|
752
|
+
if (diff.length > 4e3) {
|
|
753
|
+
diff = diff.slice(0, 4e3) + "\n... (truncated)";
|
|
754
|
+
}
|
|
755
|
+
return { branch, files, diff };
|
|
756
|
+
} catch {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/capture/index.ts
|
|
518
762
|
import os3 from "os";
|
|
519
763
|
|
|
520
764
|
// src/runner/project.ts
|
|
@@ -561,30 +805,119 @@ function detectLanguage(rootDir) {
|
|
|
561
805
|
return "";
|
|
562
806
|
}
|
|
563
807
|
|
|
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)";
|
|
808
|
+
// src/utils/extractTopic.ts
|
|
809
|
+
function extractTopic(prompt) {
|
|
810
|
+
let text = prompt.trim();
|
|
811
|
+
if (!text) {
|
|
812
|
+
return "(empty prompt)";
|
|
813
|
+
}
|
|
814
|
+
if (text.startsWith("```") || text.startsWith("`")) {
|
|
815
|
+
return truncateAtWordBoundary(text, 72);
|
|
816
|
+
}
|
|
817
|
+
const originalTrimmed = text;
|
|
818
|
+
if (text.startsWith("#")) {
|
|
819
|
+
const headingMatch = text.match(/^#+\s+(.*?)(?:\s+#+)?(?:\n|$)/);
|
|
820
|
+
if (headingMatch && headingMatch[1]) {
|
|
821
|
+
text = headingMatch[1].trim();
|
|
583
822
|
}
|
|
584
|
-
return { branch, files, diff };
|
|
585
|
-
} catch {
|
|
586
|
-
return null;
|
|
587
823
|
}
|
|
824
|
+
const prefixes = [
|
|
825
|
+
// English
|
|
826
|
+
"please, ",
|
|
827
|
+
"please ",
|
|
828
|
+
"can you please ",
|
|
829
|
+
"could you please ",
|
|
830
|
+
"can you ",
|
|
831
|
+
"could you ",
|
|
832
|
+
"would you ",
|
|
833
|
+
"i need you to ",
|
|
834
|
+
"i need to ",
|
|
835
|
+
"i want you to ",
|
|
836
|
+
"i want to ",
|
|
837
|
+
"i'd like you to ",
|
|
838
|
+
"i'd like to ",
|
|
839
|
+
"help me to ",
|
|
840
|
+
"help me ",
|
|
841
|
+
"i need ",
|
|
842
|
+
"claude, ",
|
|
843
|
+
"gemini, ",
|
|
844
|
+
"chatgpt, ",
|
|
845
|
+
// Italiano
|
|
846
|
+
"puoi ",
|
|
847
|
+
"potresti ",
|
|
848
|
+
"potreste ",
|
|
849
|
+
"per favore ",
|
|
850
|
+
"per piacere ",
|
|
851
|
+
"ho bisogno che ",
|
|
852
|
+
"ho bisogno di ",
|
|
853
|
+
"voglio che ",
|
|
854
|
+
"vorrei che ",
|
|
855
|
+
"voglio ",
|
|
856
|
+
"vorrei ",
|
|
857
|
+
"aiutami a ",
|
|
858
|
+
"aiutami ",
|
|
859
|
+
"fammi vedere ",
|
|
860
|
+
"fammi ",
|
|
861
|
+
"si prega di ",
|
|
862
|
+
"si "
|
|
863
|
+
];
|
|
864
|
+
let changed = true;
|
|
865
|
+
while (changed) {
|
|
866
|
+
changed = false;
|
|
867
|
+
const lowerText = text.toLowerCase();
|
|
868
|
+
for (const prefix of prefixes) {
|
|
869
|
+
if (lowerText.startsWith(prefix.toLowerCase())) {
|
|
870
|
+
text = text.slice(prefix.length).trim();
|
|
871
|
+
changed = true;
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const sentenceEndMatch = text.match(/[\n.?!`]/);
|
|
877
|
+
if (sentenceEndMatch && sentenceEndMatch.index !== void 0) {
|
|
878
|
+
text = text.slice(0, sentenceEndMatch.index).trim();
|
|
879
|
+
}
|
|
880
|
+
if (!text) {
|
|
881
|
+
return truncateAtWordBoundary(originalTrimmed, 72);
|
|
882
|
+
}
|
|
883
|
+
text = truncateAtWordBoundary(text, 72);
|
|
884
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
885
|
+
}
|
|
886
|
+
function truncateAtWordBoundary(str, limit) {
|
|
887
|
+
const clean = str.replace(/\s+/g, " ").trim();
|
|
888
|
+
if (clean.length <= limit) return clean;
|
|
889
|
+
const sub = clean.slice(0, limit);
|
|
890
|
+
const lastSpace = sub.lastIndexOf(" ");
|
|
891
|
+
if (lastSpace > 0) {
|
|
892
|
+
return sub.slice(0, lastSpace);
|
|
893
|
+
}
|
|
894
|
+
return sub;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/capture/index.ts
|
|
898
|
+
function createCaptureRecord(input) {
|
|
899
|
+
const { rootDir, projectName } = detectProject(input.workdir);
|
|
900
|
+
const language = detectLanguage(rootDir);
|
|
901
|
+
const title = extractTopic(input.prompt);
|
|
902
|
+
const metadata = {
|
|
903
|
+
$schema_version: 1,
|
|
904
|
+
...projectName ? { project: projectName } : {},
|
|
905
|
+
...language ? { language } : {},
|
|
906
|
+
...title ? { title } : {},
|
|
907
|
+
...input.gitContext ? { git_context: input.gitContext } : {},
|
|
908
|
+
...input.metadata
|
|
909
|
+
};
|
|
910
|
+
return {
|
|
911
|
+
timestamp: input.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
912
|
+
tool: input.tool,
|
|
913
|
+
prompt: input.prompt,
|
|
914
|
+
response: input.response ?? "",
|
|
915
|
+
args: input.args ?? "",
|
|
916
|
+
workdir: input.workdir,
|
|
917
|
+
hostname: input.hostname ?? os3.hostname(),
|
|
918
|
+
exit_code: input.exit_code ?? 0,
|
|
919
|
+
metadata: JSON.stringify(metadata)
|
|
920
|
+
};
|
|
588
921
|
}
|
|
589
922
|
|
|
590
923
|
// src/runner/inline.ts
|
|
@@ -613,26 +946,15 @@ function resolveRealBinary(tool) {
|
|
|
613
946
|
}
|
|
614
947
|
async function runInline(realBin, args, db, tool, tags, role, onInserted) {
|
|
615
948
|
const workdir = process.cwd();
|
|
616
|
-
const { rootDir, projectName } = detectProject(workdir);
|
|
617
|
-
const language = detectLanguage(rootDir);
|
|
618
949
|
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(),
|
|
950
|
+
const id = db.insert(createCaptureRecord({
|
|
628
951
|
tool,
|
|
629
952
|
prompt: extractPrompt(args),
|
|
630
953
|
args: args.join(" "),
|
|
631
954
|
workdir,
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
});
|
|
955
|
+
gitContext,
|
|
956
|
+
metadata: { ...role ? { role } : {}, ...tags.length > 0 ? { tags } : {} }
|
|
957
|
+
}));
|
|
636
958
|
if (onInserted) onInserted(id);
|
|
637
959
|
const child = spawn(realBin, args, { stdio: "inherit" });
|
|
638
960
|
child.on("error", (err) => {
|
|
@@ -654,7 +976,7 @@ function cleanLine(bytes) {
|
|
|
654
976
|
const s = Buffer.from(bytes).toString("utf-8");
|
|
655
977
|
return s.trim();
|
|
656
978
|
}
|
|
657
|
-
function
|
|
979
|
+
function stripPtyControlSequences(text) {
|
|
658
980
|
return text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
|
|
659
981
|
}
|
|
660
982
|
async function runPTY(binary, args, onPrompt, onResponse, debugLog) {
|
|
@@ -707,7 +1029,7 @@ async function runPTY(binary, args, onPrompt, onResponse, debugLog) {
|
|
|
707
1029
|
log(`ENTER \u2192 prompt: ${JSON.stringify(prompt)}`);
|
|
708
1030
|
if (prompt) {
|
|
709
1031
|
if (currentPromptId !== null) {
|
|
710
|
-
onResponse(currentPromptId,
|
|
1032
|
+
onResponse(currentPromptId, stripPtyControlSequences(responseBuffer));
|
|
711
1033
|
}
|
|
712
1034
|
currentPromptId = onPrompt(prompt, /* @__PURE__ */ new Date());
|
|
713
1035
|
responseBuffer = "";
|
|
@@ -751,7 +1073,7 @@ async function runPTY(binary, args, onPrompt, onResponse, debugLog) {
|
|
|
751
1073
|
return new Promise((resolve) => {
|
|
752
1074
|
ptyProcess.onExit(({ exitCode }) => {
|
|
753
1075
|
if (currentPromptId !== null) {
|
|
754
|
-
onResponse(currentPromptId,
|
|
1076
|
+
onResponse(currentPromptId, stripPtyControlSequences(responseBuffer));
|
|
755
1077
|
}
|
|
756
1078
|
process.stdin.removeListener("data", onData);
|
|
757
1079
|
process.stdout.removeListener("resize", onResize);
|
|
@@ -801,26 +1123,169 @@ async function getEmbeddings(texts, ollamaUrl, model, batchSize = 20) {
|
|
|
801
1123
|
return results;
|
|
802
1124
|
}
|
|
803
1125
|
|
|
804
|
-
// src/
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
const
|
|
809
|
-
const
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1126
|
+
// src/context/index.ts
|
|
1127
|
+
async function getProjectContext(db, options, embedQuery) {
|
|
1128
|
+
const limit = Math.max(1, Math.floor(options.limit ?? 5));
|
|
1129
|
+
const memoryLimit = Math.max(1, Math.floor(options.memoryLimit ?? limit));
|
|
1130
|
+
const includeSummary = options.includeSummary ?? true;
|
|
1131
|
+
const includeMemories = options.includeMemories ?? true;
|
|
1132
|
+
const includePrompts = options.includePrompts ?? true;
|
|
1133
|
+
const summary = includeSummary ? db.getProjectSummary(options.project) : null;
|
|
1134
|
+
const memories = includeMemories ? db.searchMemories(options.project, memoryLimit) : [];
|
|
1135
|
+
let prompts = [];
|
|
1136
|
+
if (includePrompts) {
|
|
1137
|
+
const query = options.query?.trim();
|
|
1138
|
+
if (query) {
|
|
1139
|
+
if (!embedQuery) throw new Error("Semantic context requires an embedding provider.");
|
|
1140
|
+
const vector = await embedQuery(query);
|
|
1141
|
+
prompts = vector ? db.searchSemantic(vector, limit, options.project) : [];
|
|
1142
|
+
} else {
|
|
1143
|
+
prompts = db.getProjectMemory(options.project, limit);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return { project: options.project, summary, memories, prompts };
|
|
1147
|
+
}
|
|
1148
|
+
function formatProjectContext(context, options = {}) {
|
|
1149
|
+
const parts = [];
|
|
1150
|
+
const { summary, memories, prompts, project } = context;
|
|
1151
|
+
if (summary || memories.length > 0) {
|
|
1152
|
+
parts.push(`## Project Knowledge: ${project}
|
|
1153
|
+
`);
|
|
1154
|
+
if (summary?.summary) parts.push(`${summary.summary}
|
|
1155
|
+
`);
|
|
1156
|
+
if (summary?.key_insights.length) {
|
|
1157
|
+
parts.push("Key Insights:");
|
|
1158
|
+
for (const insight of summary.key_insights) parts.push(` - ${insight}`);
|
|
1159
|
+
parts.push("");
|
|
1160
|
+
}
|
|
1161
|
+
if (summary?.technical_decisions.length) {
|
|
1162
|
+
parts.push("Technical Decisions:");
|
|
1163
|
+
for (const decision of summary.technical_decisions) parts.push(` - ${decision}`);
|
|
1164
|
+
parts.push("");
|
|
1165
|
+
}
|
|
1166
|
+
if (!summary && memories.length > 0) {
|
|
1167
|
+
for (const memory of memories) {
|
|
1168
|
+
parts.push(`### Memory #${memory.id}`);
|
|
1169
|
+
if (memory.summary) parts.push(`${memory.summary}
|
|
1170
|
+
`);
|
|
1171
|
+
if (memory.key_insights.length) {
|
|
1172
|
+
parts.push("Key Insights:");
|
|
1173
|
+
for (const insight of memory.key_insights) parts.push(` - ${insight}`);
|
|
1174
|
+
parts.push("");
|
|
1175
|
+
}
|
|
1176
|
+
if (memory.technical_decisions.length) {
|
|
1177
|
+
parts.push("Technical Decisions:");
|
|
1178
|
+
for (const decision of memory.technical_decisions) parts.push(` - ${decision}`);
|
|
1179
|
+
parts.push("");
|
|
1180
|
+
}
|
|
818
1181
|
}
|
|
819
1182
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1183
|
+
if (summary && memories.length > 0) {
|
|
1184
|
+
parts.push("Recent Memory Entries:");
|
|
1185
|
+
for (const memory of memories) {
|
|
1186
|
+
if (memory.summary) parts.push(` - [memory #${memory.id}] ${memory.summary}`);
|
|
1187
|
+
}
|
|
1188
|
+
parts.push("");
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (prompts.length > 0) {
|
|
1192
|
+
if (parts.length > 0) parts.push("---\n");
|
|
1193
|
+
parts.push("## Recent Context\n");
|
|
1194
|
+
for (const entry of prompts) {
|
|
1195
|
+
let metadata = {};
|
|
1196
|
+
try {
|
|
1197
|
+
metadata = JSON.parse(entry.metadata);
|
|
1198
|
+
} catch {
|
|
1199
|
+
}
|
|
1200
|
+
parts.push(`### Interaction #${entry.id} \u2014 ${entry.tool} (${new Date(entry.timestamp).toLocaleDateString()})`);
|
|
1201
|
+
if (metadata.role) parts.push(`Role: ${metadata.role}`);
|
|
1202
|
+
const promptSummary = metadata.summary || (options.includePromptText || options.promptExcerptLength ? "" : entry.prompt.slice(0, 100).replace(/\n/g, " "));
|
|
1203
|
+
if (promptSummary) parts.push(promptSummary);
|
|
1204
|
+
if (metadata.key_insights?.length) {
|
|
1205
|
+
for (const insight of metadata.key_insights) parts.push(` - ${insight}`);
|
|
1206
|
+
}
|
|
1207
|
+
if (options.includePromptText) {
|
|
1208
|
+
parts.push("```\n" + entry.prompt.slice(0, 1e3) + (entry.prompt.length > 1e3 ? "\n..." : "") + "\n```");
|
|
1209
|
+
} else if (options.promptExcerptLength) {
|
|
1210
|
+
const excerpt = entry.prompt.slice(0, options.promptExcerptLength);
|
|
1211
|
+
parts.push(`Prompt: ${excerpt}${entry.prompt.length > options.promptExcerptLength ? "..." : ""}`);
|
|
1212
|
+
}
|
|
1213
|
+
if (options.responseExcerptLength && entry.response) {
|
|
1214
|
+
const response = entry.response.slice(0, options.responseExcerptLength);
|
|
1215
|
+
parts.push(`Response: ${response}${entry.response.length > options.responseExcerptLength ? "..." : ""}`);
|
|
1216
|
+
}
|
|
1217
|
+
parts.push("");
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (parts.length === 0) return options.emptyMessage ?? `No relevant context found for project "${project}".`;
|
|
1221
|
+
return parts.join("\n").trimEnd();
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// src/mcp/server.ts
|
|
1225
|
+
import { z } from "zod";
|
|
1226
|
+
function createMCPServer(db, cfg = load()) {
|
|
1227
|
+
const server = new Server(
|
|
1228
|
+
{
|
|
1229
|
+
name: "ph-memory",
|
|
1230
|
+
version: "1.0.0"
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
capabilities: {
|
|
1234
|
+
tools: {}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1239
|
+
return {
|
|
1240
|
+
tools: [
|
|
1241
|
+
{
|
|
1242
|
+
name: "list_prompts",
|
|
1243
|
+
description: "List prompts for a project with pagination. Use page=1, pageSize=10 to browse.",
|
|
1244
|
+
inputSchema: {
|
|
1245
|
+
type: "object",
|
|
1246
|
+
properties: {
|
|
1247
|
+
project: { type: "string", description: "Project name to list prompts for" },
|
|
1248
|
+
page: { type: "number", description: "Page number (1-based)", default: 1 },
|
|
1249
|
+
pageSize: { type: "number", description: "Results per page (max 100)", default: 20 }
|
|
1250
|
+
},
|
|
1251
|
+
required: ["project"]
|
|
1252
|
+
}
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
name: "save_decision",
|
|
1256
|
+
description: "Save an architectural decision or key insight to project memory.",
|
|
1257
|
+
inputSchema: {
|
|
1258
|
+
type: "object",
|
|
1259
|
+
properties: {
|
|
1260
|
+
project: { type: "string", description: "Project name" },
|
|
1261
|
+
summary: { type: "string", description: "Brief summary of the decision" },
|
|
1262
|
+
keyInsights: {
|
|
1263
|
+
type: "array",
|
|
1264
|
+
items: { type: "string" },
|
|
1265
|
+
description: "Key insights or takeaways"
|
|
1266
|
+
},
|
|
1267
|
+
technicalDecisions: {
|
|
1268
|
+
type: "array",
|
|
1269
|
+
items: { type: "string" },
|
|
1270
|
+
description: "Technical decisions made"
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
required: ["project", "summary"]
|
|
1274
|
+
}
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
name: "get_project_diff",
|
|
1278
|
+
description: "Compare project state between two points in time. Shows prompts and memory entries created in the date range.",
|
|
1279
|
+
inputSchema: {
|
|
1280
|
+
type: "object",
|
|
1281
|
+
properties: {
|
|
1282
|
+
project: { type: "string", description: "Project name" },
|
|
1283
|
+
since: { type: "string", description: "Start date (ISO 8601, e.g. 2026-01-01)" },
|
|
1284
|
+
until: { type: "string", description: "End date (ISO 8601, defaults to now)" }
|
|
1285
|
+
},
|
|
1286
|
+
required: ["project", "since"]
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
824
1289
|
{
|
|
825
1290
|
name: "search_project_memory",
|
|
826
1291
|
description: "Search for past interactions and technical decisions in a specific project using semantic search.",
|
|
@@ -904,7 +1369,10 @@ async function runMCPServer() {
|
|
|
904
1369
|
inputSchema: {
|
|
905
1370
|
type: "object",
|
|
906
1371
|
properties: {
|
|
907
|
-
project: { type: "string", description: "The project name" }
|
|
1372
|
+
project: { type: "string", description: "The project name" },
|
|
1373
|
+
since: { type: "string", description: "Start date (ISO 8601, optional)" },
|
|
1374
|
+
until: { type: "string", description: "End date (ISO 8601, optional)" },
|
|
1375
|
+
limit: { type: "number", description: "Max events to return (default 100)", default: 100 }
|
|
908
1376
|
},
|
|
909
1377
|
required: ["project"]
|
|
910
1378
|
}
|
|
@@ -928,6 +1396,115 @@ async function runMCPServer() {
|
|
|
928
1396
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
929
1397
|
const { name, arguments: args } = request.params;
|
|
930
1398
|
try {
|
|
1399
|
+
if (name === "list_prompts") {
|
|
1400
|
+
const { project, page = 1, pageSize = 20 } = z.object({
|
|
1401
|
+
project: z.string(),
|
|
1402
|
+
page: z.number().optional(),
|
|
1403
|
+
pageSize: z.number().max(100).optional()
|
|
1404
|
+
}).parse(args);
|
|
1405
|
+
const result = db.getPromptsByProjectPaginated(project, page, pageSize);
|
|
1406
|
+
const totalPages = Math.ceil(result.total / pageSize);
|
|
1407
|
+
const text = [
|
|
1408
|
+
`## Prompts for "${project}"`,
|
|
1409
|
+
`Page ${page}/${totalPages} (${result.total} total)
|
|
1410
|
+
`,
|
|
1411
|
+
...result.entries.map((e) => {
|
|
1412
|
+
let meta = {};
|
|
1413
|
+
try {
|
|
1414
|
+
meta = JSON.parse(e.metadata);
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
const date = new Date(e.timestamp).toLocaleString();
|
|
1418
|
+
const title = meta.title ? ` \u2014 ${meta.title}` : "";
|
|
1419
|
+
const roleStr = meta.role ? ` (${meta.role})` : "";
|
|
1420
|
+
return `### #${e.id} \u2014 ${e.tool}${roleStr} (${date})${title}
|
|
1421
|
+
${meta.summary ? `Summary: ${meta.summary}
|
|
1422
|
+
` : ""}Prompt: ${e.prompt.slice(0, 200)}${e.prompt.length > 200 ? "..." : ""}`;
|
|
1423
|
+
}).join("\n\n"),
|
|
1424
|
+
"",
|
|
1425
|
+
`Page ${page}/${totalPages} | ${result.total} total prompts`
|
|
1426
|
+
].join("\n");
|
|
1427
|
+
return { content: [{ type: "text", text }] };
|
|
1428
|
+
}
|
|
1429
|
+
if (name === "save_decision") {
|
|
1430
|
+
const { project, summary, keyInsights, technicalDecisions } = z.object({
|
|
1431
|
+
project: z.string(),
|
|
1432
|
+
summary: z.string(),
|
|
1433
|
+
keyInsights: z.array(z.string()).optional(),
|
|
1434
|
+
technicalDecisions: z.array(z.string()).optional()
|
|
1435
|
+
}).parse(args);
|
|
1436
|
+
const id = db.insertMemory({
|
|
1437
|
+
project,
|
|
1438
|
+
prompt_ids: [],
|
|
1439
|
+
summary,
|
|
1440
|
+
key_insights: keyInsights ?? [],
|
|
1441
|
+
technical_decisions: technicalDecisions ?? [],
|
|
1442
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1443
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1444
|
+
access_count: 0
|
|
1445
|
+
});
|
|
1446
|
+
db.upsertProjectSummary({
|
|
1447
|
+
project,
|
|
1448
|
+
summary,
|
|
1449
|
+
key_insights: keyInsights ?? [],
|
|
1450
|
+
technical_decisions: technicalDecisions ?? []
|
|
1451
|
+
});
|
|
1452
|
+
return {
|
|
1453
|
+
content: [{ type: "text", text: `Decision saved as memory #${id} for project "${project}".
|
|
1454
|
+
|
|
1455
|
+
Summary: ${summary}
|
|
1456
|
+
Key Insights: ${(keyInsights ?? []).length}
|
|
1457
|
+
Technical Decisions: ${(technicalDecisions ?? []).length}` }]
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
if (name === "get_project_diff") {
|
|
1461
|
+
const { project, since, until } = z.object({
|
|
1462
|
+
project: z.string(),
|
|
1463
|
+
since: z.string(),
|
|
1464
|
+
until: z.string().optional()
|
|
1465
|
+
}).parse(args);
|
|
1466
|
+
const sinceDate = new Date(since);
|
|
1467
|
+
const untilDate = until ? new Date(until) : /* @__PURE__ */ new Date();
|
|
1468
|
+
const prompts = db.search({
|
|
1469
|
+
project,
|
|
1470
|
+
since: sinceDate,
|
|
1471
|
+
until: untilDate,
|
|
1472
|
+
limit: 1e3
|
|
1473
|
+
});
|
|
1474
|
+
const memories = db.getAllMemoriesByProject(project).filter((m) => {
|
|
1475
|
+
const created = new Date(m.created_at);
|
|
1476
|
+
return created >= sinceDate && created <= untilDate;
|
|
1477
|
+
});
|
|
1478
|
+
const lines = [
|
|
1479
|
+
`## Project Diff: ${project}`,
|
|
1480
|
+
`From: ${since} To: ${until || "now"}`,
|
|
1481
|
+
`Prompts: ${prompts.length} Memories: ${memories.length}
|
|
1482
|
+
`
|
|
1483
|
+
];
|
|
1484
|
+
if (prompts.length > 0) {
|
|
1485
|
+
lines.push("### Prompts\n");
|
|
1486
|
+
for (const p of prompts.reverse()) {
|
|
1487
|
+
let meta = {};
|
|
1488
|
+
try {
|
|
1489
|
+
meta = JSON.parse(p.metadata);
|
|
1490
|
+
} catch {
|
|
1491
|
+
}
|
|
1492
|
+
const title = meta.title ? ` \u2014 ${meta.title}` : "";
|
|
1493
|
+
const roleStr = meta.role ? ` (${meta.role})` : "";
|
|
1494
|
+
lines.push(`- **#${p.id}** ${p.tool}${roleStr}${title} (${new Date(p.timestamp).toLocaleDateString()})`);
|
|
1495
|
+
if (meta.summary) lines.push(` Summary: ${meta.summary}`);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (memories.length > 0) {
|
|
1499
|
+
lines.push("\n### Memory Entries\n");
|
|
1500
|
+
for (const m of memories) {
|
|
1501
|
+
lines.push(`- **Memory #${m.id}**: ${m.summary}`);
|
|
1502
|
+
for (const i of m.key_insights) lines.push(` - Insight: ${i}`);
|
|
1503
|
+
for (const d of m.technical_decisions) lines.push(` - Decision: ${d}`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1507
|
+
}
|
|
931
1508
|
if (name === "search_project_memory") {
|
|
932
1509
|
const { query, project, limit = 5 } = z.object({
|
|
933
1510
|
query: z.string(),
|
|
@@ -940,15 +1517,30 @@ async function runMCPServer() {
|
|
|
940
1517
|
if (!queryVec) {
|
|
941
1518
|
throw new Error("Failed to generate embedding for query");
|
|
942
1519
|
}
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1520
|
+
const filtered = db.searchSemantic(queryVec, limit, project);
|
|
1521
|
+
if (filtered.length === 0) {
|
|
1522
|
+
const merged = db.getProjectSummary(project);
|
|
1523
|
+
if (merged) {
|
|
1524
|
+
const parts = [
|
|
1525
|
+
`## Project Knowledge: ${project}
|
|
1526
|
+
`,
|
|
1527
|
+
`Based on ${merged.prompt_count} interactions.
|
|
1528
|
+
`
|
|
1529
|
+
];
|
|
1530
|
+
if (merged.summary) parts.push(`${merged.summary}
|
|
1531
|
+
`);
|
|
1532
|
+
if (merged.key_insights.length > 0) {
|
|
1533
|
+
parts.push("\n**Key Insights:**");
|
|
1534
|
+
for (const i of merged.key_insights) parts.push(`- ${i}`);
|
|
1535
|
+
}
|
|
1536
|
+
if (merged.technical_decisions.length > 0) {
|
|
1537
|
+
parts.push("\n**Technical Decisions:**");
|
|
1538
|
+
for (const d of merged.technical_decisions) parts.push(`- ${d}`);
|
|
1539
|
+
}
|
|
1540
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
950
1541
|
}
|
|
951
|
-
|
|
1542
|
+
return { content: [{ type: "text", text: `No relevant memory found for project "${project}".` }] };
|
|
1543
|
+
}
|
|
952
1544
|
return {
|
|
953
1545
|
content: [{ type: "text", text: formatResultsAsMarkdown(filtered) }]
|
|
954
1546
|
};
|
|
@@ -958,59 +1550,39 @@ async function runMCPServer() {
|
|
|
958
1550
|
project: z.string(),
|
|
959
1551
|
limit: z.number().optional()
|
|
960
1552
|
}).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
|
-
}
|
|
1553
|
+
const context = await getProjectContext(db, { project, limit, memoryLimit: 3 });
|
|
986
1554
|
return {
|
|
987
|
-
content: [{ type: "text", text:
|
|
1555
|
+
content: [{ type: "text", text: formatProjectContext(context, {
|
|
1556
|
+
promptExcerptLength: 300,
|
|
1557
|
+
responseExcerptLength: 200,
|
|
1558
|
+
emptyMessage: "No context found for this project."
|
|
1559
|
+
}) }]
|
|
988
1560
|
};
|
|
989
1561
|
}
|
|
990
1562
|
if (name === "get_project_summary") {
|
|
991
1563
|
const { project } = z.object({
|
|
992
1564
|
project: z.string()
|
|
993
1565
|
}).parse(args);
|
|
994
|
-
const
|
|
995
|
-
if (
|
|
1566
|
+
const summary = db.getProjectSummary(project);
|
|
1567
|
+
if (!summary) {
|
|
996
1568
|
return {
|
|
997
1569
|
content: [{ type: "text", text: `No accumulated knowledge for project "${project}". Run "ph analyze" to generate insights.` }]
|
|
998
1570
|
};
|
|
999
1571
|
}
|
|
1000
1572
|
const parts = [];
|
|
1001
|
-
|
|
1002
|
-
parts.push(`## ${mem.summary || "Project Memory"}
|
|
1573
|
+
parts.push(`## ${summary.summary || "Project Memory"}
|
|
1003
1574
|
`);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
}
|
|
1575
|
+
parts.push(`Based on ${summary.prompt_count} interactions.
|
|
1576
|
+
`);
|
|
1577
|
+
if (summary.key_insights.length > 0) {
|
|
1578
|
+
parts.push("**Key Insights:**");
|
|
1579
|
+
for (const i of summary.key_insights) parts.push(`- ${i}`);
|
|
1580
|
+
parts.push("");
|
|
1581
|
+
}
|
|
1582
|
+
if (summary.technical_decisions.length > 0) {
|
|
1583
|
+
parts.push("**Technical Decisions:**");
|
|
1584
|
+
for (const d of summary.technical_decisions) parts.push(`- ${d}`);
|
|
1585
|
+
parts.push("");
|
|
1014
1586
|
}
|
|
1015
1587
|
return {
|
|
1016
1588
|
content: [{ type: "text", text: parts.join("\n") }]
|
|
@@ -1067,32 +1639,37 @@ async function runMCPServer() {
|
|
|
1067
1639
|
if (!queryVec) {
|
|
1068
1640
|
throw new Error("Failed to generate embedding for query");
|
|
1069
1641
|
}
|
|
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);
|
|
1642
|
+
const filtered = db.searchSemantic(queryVec, limit, project);
|
|
1079
1643
|
return {
|
|
1080
1644
|
content: [{ type: "text", text: formatPromptList(filtered, query) }]
|
|
1081
1645
|
};
|
|
1082
1646
|
}
|
|
1083
1647
|
if (name === "get_project_timeline") {
|
|
1084
|
-
const { project } = z.object({
|
|
1085
|
-
project: z.string()
|
|
1648
|
+
const { project, since, until, limit = 100 } = z.object({
|
|
1649
|
+
project: z.string(),
|
|
1650
|
+
since: z.string().optional(),
|
|
1651
|
+
until: z.string().optional(),
|
|
1652
|
+
limit: z.number().optional()
|
|
1086
1653
|
}).parse(args);
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (
|
|
1654
|
+
let allPrompts = db.getAllPromptsByProject(project);
|
|
1655
|
+
let memories = db.getAllMemoriesByProject(project);
|
|
1656
|
+
if (since) {
|
|
1657
|
+
const sinceDate = new Date(since);
|
|
1658
|
+
allPrompts = allPrompts.filter((p) => new Date(p.timestamp) >= sinceDate);
|
|
1659
|
+
memories = memories.filter((m) => new Date(m.created_at) >= sinceDate);
|
|
1660
|
+
}
|
|
1661
|
+
if (until) {
|
|
1662
|
+
const untilDate = new Date(until);
|
|
1663
|
+
allPrompts = allPrompts.filter((p) => new Date(p.timestamp) <= untilDate);
|
|
1664
|
+
memories = memories.filter((m) => new Date(m.created_at) <= untilDate);
|
|
1665
|
+
}
|
|
1666
|
+
if (allPrompts.length === 0 && memories.length === 0) {
|
|
1090
1667
|
return {
|
|
1091
|
-
content: [{ type: "text", text: `No history found for project "${project}".` }]
|
|
1668
|
+
content: [{ type: "text", text: `No history found for project "${project}"${since ? ` since ${since}` : ""}${until ? ` until ${until}` : ""}.` }]
|
|
1092
1669
|
};
|
|
1093
1670
|
}
|
|
1094
1671
|
const events = [];
|
|
1095
|
-
for (const p of
|
|
1672
|
+
for (const p of allPrompts.slice(0, limit)) {
|
|
1096
1673
|
let meta = {};
|
|
1097
1674
|
try {
|
|
1098
1675
|
meta = JSON.parse(p.metadata);
|
|
@@ -1128,9 +1705,10 @@ ${m.technical_decisions.map((d) => `- ${d}`).join("\n")}` : ""
|
|
|
1128
1705
|
});
|
|
1129
1706
|
}
|
|
1130
1707
|
events.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
1131
|
-
const
|
|
1708
|
+
const range = since || until ? ` (${since || "\u2026"} \u2192 ${until || "now"})` : "";
|
|
1709
|
+
const header = `# Timeline: ${project}${range}
|
|
1132
1710
|
|
|
1133
|
-
${
|
|
1711
|
+
${allPrompts.length} prompts \xB7 ${memories.length} memory entries
|
|
1134
1712
|
|
|
1135
1713
|
`;
|
|
1136
1714
|
const body = events.map((e) => {
|
|
@@ -1180,15 +1758,7 @@ ${m.technical_decisions.map((d) => `- ${d}`).join("\n")}`);
|
|
|
1180
1758
|
try {
|
|
1181
1759
|
const [queryVec] = await getEmbeddings([`${project}: ${task}`], ollamaUrl, embedModel, 1);
|
|
1182
1760
|
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);
|
|
1761
|
+
const projectPrompts = db.searchSemantic(queryVec, limit, project);
|
|
1192
1762
|
if (projectPrompts.length > 0) {
|
|
1193
1763
|
parts.push(`## Related Prompts
|
|
1194
1764
|
`);
|
|
@@ -1226,6 +1796,13 @@ ${m.technical_decisions.map((d) => `- ${d}`).join("\n")}`);
|
|
|
1226
1796
|
};
|
|
1227
1797
|
}
|
|
1228
1798
|
});
|
|
1799
|
+
return server;
|
|
1800
|
+
}
|
|
1801
|
+
async function runMCPServer() {
|
|
1802
|
+
const cfg = load();
|
|
1803
|
+
const dbPath = process.env.PH_DB ?? cfg.dbPath ?? defaultPath();
|
|
1804
|
+
const db = new PhDB(dbPath);
|
|
1805
|
+
const server = createMCPServer(db, cfg);
|
|
1229
1806
|
const transport = new StdioServerTransport();
|
|
1230
1807
|
await server.connect(transport);
|
|
1231
1808
|
console.error("ph MCP server running on stdio");
|
|
@@ -1327,12 +1904,31 @@ function formatSinglePrompt(e) {
|
|
|
1327
1904
|
|
|
1328
1905
|
// src/server/index.ts
|
|
1329
1906
|
import http from "http";
|
|
1907
|
+
import { URL } from "url";
|
|
1908
|
+
|
|
1909
|
+
// src/utils/syncHash.ts
|
|
1330
1910
|
import { createHash as createHash2 } from "crypto";
|
|
1911
|
+
function syncHash(entry) {
|
|
1912
|
+
const body = entry.args ? `${entry.tool}|${entry.prompt}|${entry.response}|${entry.args}` : `${entry.tool}|${entry.prompt}|${entry.response}`;
|
|
1913
|
+
return createHash2("sha256").update(body).digest("hex");
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// src/server/index.ts
|
|
1917
|
+
import { timingSafeEqual } from "crypto";
|
|
1331
1918
|
async function runServer(port = 3001, host = "0.0.0.0") {
|
|
1332
1919
|
const cfg = load();
|
|
1333
1920
|
const dbPath = process.env.PH_DB ?? cfg.dbPath ?? defaultPath();
|
|
1334
1921
|
const db = new PhDB(dbPath);
|
|
1335
|
-
const server = http.createServer(
|
|
1922
|
+
const server = http.createServer(createRequestHandler(db, cfg));
|
|
1923
|
+
return new Promise((resolve) => {
|
|
1924
|
+
server.listen(port, host, () => {
|
|
1925
|
+
console.error(`ph server listening on http://${host}:${port}`);
|
|
1926
|
+
resolve();
|
|
1927
|
+
});
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
function createRequestHandler(db, cfg) {
|
|
1931
|
+
return async (req, res) => {
|
|
1336
1932
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1337
1933
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1338
1934
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
@@ -1341,28 +1937,34 @@ async function runServer(port = 3001, host = "0.0.0.0") {
|
|
|
1341
1937
|
res.end();
|
|
1342
1938
|
return;
|
|
1343
1939
|
}
|
|
1940
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
1941
|
+
if (url.pathname !== "/health" && !authorized(req, cfg)) {
|
|
1942
|
+
json(res, 401, { error: "unauthorized" });
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1344
1945
|
try {
|
|
1345
1946
|
const body = await readBody(req);
|
|
1346
1947
|
await route(req, res, body, db, cfg);
|
|
1347
1948
|
} catch (err) {
|
|
1348
1949
|
json(res, 500, { error: err.message });
|
|
1349
1950
|
}
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
function authorized(req, cfg) {
|
|
1954
|
+
if (!cfg.remoteApiKey) return true;
|
|
1955
|
+
const expected = `Bearer ${cfg.remoteApiKey}`;
|
|
1956
|
+
const a = Buffer.from(req.headers.authorization ?? "");
|
|
1957
|
+
const b = Buffer.from(expected);
|
|
1958
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
1357
1959
|
}
|
|
1358
1960
|
async function route(req, res, body, db, cfg) {
|
|
1359
1961
|
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
1360
|
-
const
|
|
1962
|
+
const path10 = url.pathname;
|
|
1361
1963
|
const method = req.method || "GET";
|
|
1362
|
-
if (method === "GET" &&
|
|
1363
|
-
return json(res, 200, { status: "ok", dbPath: db.
|
|
1964
|
+
if (method === "GET" && path10 === "/health") {
|
|
1965
|
+
return json(res, 200, { status: "ok", dbPath: db.dbPath });
|
|
1364
1966
|
}
|
|
1365
|
-
if (method === "POST" &&
|
|
1967
|
+
if (method === "POST" && path10 === "/api/prompts/search") {
|
|
1366
1968
|
const { query, tool, project, role, tag, since, until, limit = 10 } = JSON.parse(body);
|
|
1367
1969
|
const results = db.search({
|
|
1368
1970
|
query,
|
|
@@ -1376,31 +1978,23 @@ async function route(req, res, body, db, cfg) {
|
|
|
1376
1978
|
});
|
|
1377
1979
|
return json(res, 200, { prompts: results });
|
|
1378
1980
|
}
|
|
1379
|
-
if (method === "POST" &&
|
|
1981
|
+
if (method === "POST" && path10 === "/api/prompts/by-id") {
|
|
1380
1982
|
const { id } = JSON.parse(body);
|
|
1381
1983
|
const entry = db.getById(id);
|
|
1382
1984
|
if (!entry) return json(res, 404, { error: `Prompt #${id} not found` });
|
|
1383
1985
|
return json(res, 200, { prompt: entry });
|
|
1384
1986
|
}
|
|
1385
|
-
if (method === "POST" &&
|
|
1987
|
+
if (method === "POST" && path10 === "/api/prompts/semantic") {
|
|
1386
1988
|
const { query, project, limit = 5 } = JSON.parse(body);
|
|
1387
1989
|
if (!query) return json(res, 400, { error: "query is required" });
|
|
1388
1990
|
const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
|
|
1389
1991
|
const model = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
|
|
1390
1992
|
const [queryVec] = await getEmbeddings([query], ollamaUrl, model, 1);
|
|
1391
1993
|
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);
|
|
1994
|
+
const filtered = db.searchSemantic(queryVec, limit, project);
|
|
1401
1995
|
return json(res, 200, { prompts: filtered });
|
|
1402
1996
|
}
|
|
1403
|
-
if (method === "POST" &&
|
|
1997
|
+
if (method === "POST" && path10 === "/api/sync/push") {
|
|
1404
1998
|
const { prompts } = JSON.parse(body);
|
|
1405
1999
|
if (!Array.isArray(prompts)) return json(res, 400, { error: "prompts must be an array" });
|
|
1406
2000
|
let imported = 0;
|
|
@@ -1408,8 +2002,8 @@ async function route(req, res, body, db, cfg) {
|
|
|
1408
2002
|
const errors = [];
|
|
1409
2003
|
for (const p of prompts) {
|
|
1410
2004
|
try {
|
|
1411
|
-
const hash =
|
|
1412
|
-
const existing = db.
|
|
2005
|
+
const hash = syncHash(p);
|
|
2006
|
+
const existing = db.getPromptBySyncHash(hash);
|
|
1413
2007
|
if (existing) {
|
|
1414
2008
|
skipped++;
|
|
1415
2009
|
continue;
|
|
@@ -1433,35 +2027,28 @@ async function route(req, res, body, db, cfg) {
|
|
|
1433
2027
|
}
|
|
1434
2028
|
return json(res, 200, { imported, skipped, errors });
|
|
1435
2029
|
}
|
|
1436
|
-
if (method === "POST" &&
|
|
2030
|
+
if (method === "POST" && path10 === "/api/sync/pull") {
|
|
1437
2031
|
const { since } = JSON.parse(body);
|
|
1438
2032
|
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
|
-
}
|
|
2033
|
+
const results = since ? db.getPromptsSince(since, limit) : db.getAllPrompts(limit);
|
|
1445
2034
|
return json(res, 200, { prompts: results, hasMore: results.length >= limit });
|
|
1446
2035
|
}
|
|
1447
|
-
if (method === "POST" &&
|
|
2036
|
+
if (method === "POST" && path10 === "/api/memories/search") {
|
|
1448
2037
|
const { project, limit = 10 } = JSON.parse(body);
|
|
1449
2038
|
const memories = project ? db.searchMemories(project, limit) : [];
|
|
1450
2039
|
return json(res, 200, { memories });
|
|
1451
2040
|
}
|
|
1452
|
-
if (method === "POST" &&
|
|
2041
|
+
if (method === "POST" && path10 === "/api/memories/summary") {
|
|
1453
2042
|
const { project } = JSON.parse(body);
|
|
1454
2043
|
if (!project) return json(res, 400, { error: "project is required" });
|
|
1455
2044
|
const memories = db.searchMemories(project, 5);
|
|
1456
2045
|
return json(res, 200, { memories });
|
|
1457
2046
|
}
|
|
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();
|
|
2047
|
+
if (method === "GET" && path10 === "/api/stats") {
|
|
2048
|
+
const { total, totalMemories, byTool } = db.getStats();
|
|
1462
2049
|
return json(res, 200, { total, totalMemories, byTool });
|
|
1463
2050
|
}
|
|
1464
|
-
json(res, 404, { error: `Not found: ${method} ${
|
|
2051
|
+
json(res, 404, { error: `Not found: ${method} ${path10}` });
|
|
1465
2052
|
}
|
|
1466
2053
|
function json(res, status2, data) {
|
|
1467
2054
|
res.writeHead(status2, { "Content-Type": "application/json" });
|
|
@@ -1476,12 +2063,8 @@ function readBody(req) {
|
|
|
1476
2063
|
req.on("error", reject);
|
|
1477
2064
|
});
|
|
1478
2065
|
}
|
|
1479
|
-
function sha256(s) {
|
|
1480
|
-
return createHash2("sha256").update(s).digest("hex");
|
|
1481
|
-
}
|
|
1482
2066
|
|
|
1483
2067
|
// src/commands/remote.ts
|
|
1484
|
-
import { createHash as createHash3 } from "crypto";
|
|
1485
2068
|
function remoteUrl(cfg) {
|
|
1486
2069
|
return process.env.PH_REMOTE_URL || cfg.remoteUrl || "";
|
|
1487
2070
|
}
|
|
@@ -1584,7 +2167,7 @@ async function pull(dbPath, cfg, url) {
|
|
|
1584
2167
|
let imported = 0;
|
|
1585
2168
|
let skipped = 0;
|
|
1586
2169
|
for (const p of result.prompts) {
|
|
1587
|
-
const hash =
|
|
2170
|
+
const hash = syncHash(p);
|
|
1588
2171
|
const existing = db.getPromptBySyncHash(hash);
|
|
1589
2172
|
if (existing) {
|
|
1590
2173
|
skipped++;
|
|
@@ -1622,7 +2205,7 @@ async function status(dbPath, cfg, url) {
|
|
|
1622
2205
|
const localTotal = db.getPromptCount();
|
|
1623
2206
|
const lastPush = cfg.remoteLastPush || "never";
|
|
1624
2207
|
const lastPull = cfg.remoteLastPull || "never";
|
|
1625
|
-
const pending = cfg.remoteLastPush ? db.
|
|
2208
|
+
const pending = cfg.remoteLastPush ? db.getPromptCountSince(cfg.remoteLastPush) : localTotal;
|
|
1626
2209
|
process.stdout.write(`Remote: ${url}
|
|
1627
2210
|
`);
|
|
1628
2211
|
process.stdout.write(`Local total: ${localTotal} prompts
|
|
@@ -1635,13 +2218,10 @@ async function status(dbPath, cfg, url) {
|
|
|
1635
2218
|
`);
|
|
1636
2219
|
db.close();
|
|
1637
2220
|
}
|
|
1638
|
-
function sha2562(s) {
|
|
1639
|
-
return createHash3("sha256").update(s).digest("hex");
|
|
1640
|
-
}
|
|
1641
2221
|
|
|
1642
2222
|
// src/ui/BrowseApp.tsx
|
|
1643
|
-
import
|
|
1644
|
-
import { Box as
|
|
2223
|
+
import React7, { useState as useState3, useCallback, useEffect, useMemo as useMemo4 } from "react";
|
|
2224
|
+
import { Box as Box5, Text as Text6, useInput as useInput3, useApp } from "ink";
|
|
1645
2225
|
import { spawn as spawn2 } from "child_process";
|
|
1646
2226
|
|
|
1647
2227
|
// src/ui/Header.tsx
|
|
@@ -1652,10 +2232,13 @@ import { AppHeader, THEMES } from "@gi4nks/ink";
|
|
|
1652
2232
|
// package.json
|
|
1653
2233
|
var package_default = {
|
|
1654
2234
|
name: "@gi4nks/ph",
|
|
1655
|
-
version: "1.
|
|
2235
|
+
version: "1.6.0",
|
|
1656
2236
|
description: "Transparent CLI wrapper that captures and indexes AI prompts",
|
|
1657
2237
|
type: "module",
|
|
1658
2238
|
license: "MIT",
|
|
2239
|
+
engines: {
|
|
2240
|
+
node: ">=22"
|
|
2241
|
+
},
|
|
1659
2242
|
bin: {
|
|
1660
2243
|
ph: "dist/cli.js"
|
|
1661
2244
|
},
|
|
@@ -1703,37 +2286,40 @@ var package_default = {
|
|
|
1703
2286
|
dev: "tsx src/cli.ts",
|
|
1704
2287
|
start: "node dist/cli.js",
|
|
1705
2288
|
test: "vitest run",
|
|
1706
|
-
lint: "eslint src"
|
|
2289
|
+
lint: "eslint src",
|
|
2290
|
+
"eval:retrieval": "tsx scripts/eval-context-retrieval.ts"
|
|
1707
2291
|
},
|
|
1708
2292
|
dependencies: {
|
|
1709
2293
|
"@gi4nks/ink": "^1.5.0",
|
|
2294
|
+
"@gi4nks/wise": "^2.1.0",
|
|
1710
2295
|
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
1711
2296
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
2297
|
+
ai: "^7.0.136",
|
|
1712
2298
|
"better-sqlite3": "^12.8.0",
|
|
1713
|
-
ink: "^
|
|
1714
|
-
react: "^19.2.
|
|
2299
|
+
ink: "^7.0.5",
|
|
2300
|
+
react: "^19.2.7",
|
|
1715
2301
|
"sqlite-vec": "^0.1.9",
|
|
1716
2302
|
zod: "^4.3.6"
|
|
1717
2303
|
},
|
|
1718
2304
|
devDependencies: {
|
|
2305
|
+
"@eslint/js": "^9.39.5",
|
|
1719
2306
|
"@semantic-release/changelog": "^6.0.3",
|
|
1720
2307
|
"@semantic-release/git": "^10.0.1",
|
|
1721
2308
|
"@semantic-release/github": "^12.0.8",
|
|
1722
2309
|
"@semantic-release/npm": "^13.1.5",
|
|
1723
2310
|
"@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.
|
|
2311
|
+
"@types/node": "^25.9.3",
|
|
2312
|
+
"@types/react": "^19.2.7",
|
|
2313
|
+
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
|
2314
|
+
"@typescript-eslint/parser": "^8.58.2",
|
|
2315
|
+
eslint: "^9.39.5",
|
|
1729
2316
|
"eslint-plugin-react": "^7.37.5",
|
|
1730
2317
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
1731
2318
|
"semantic-release": "^25.0.3",
|
|
1732
|
-
"standard-version": "^9.5.0",
|
|
1733
2319
|
tsup: "^8.5.1",
|
|
1734
2320
|
tsx: "^4.21.0",
|
|
1735
|
-
typescript: "^
|
|
1736
|
-
vitest: "^4.1.
|
|
2321
|
+
typescript: "^6.0.3",
|
|
2322
|
+
vitest: "^4.1.8"
|
|
1737
2323
|
}
|
|
1738
2324
|
};
|
|
1739
2325
|
|
|
@@ -1753,6 +2339,7 @@ var FilterBadges = ({ active, textFilter }) => /* @__PURE__ */ React.createEleme
|
|
|
1753
2339
|
var Header = ({
|
|
1754
2340
|
entriesCount,
|
|
1755
2341
|
allEntriesCount,
|
|
2342
|
+
archiveCount,
|
|
1756
2343
|
activeFilters,
|
|
1757
2344
|
textFilter,
|
|
1758
2345
|
isTextFiltering,
|
|
@@ -1762,7 +2349,7 @@ var Header = ({
|
|
|
1762
2349
|
const version = package_default.version;
|
|
1763
2350
|
const rawCwd = process.cwd();
|
|
1764
2351
|
const cwd = rawCwd.replace(process.env.HOME || "", "~");
|
|
1765
|
-
const leftExtra = /* @__PURE__ */ React.createElement(Text, { color: "white", dimColor: true }, " ", entriesCount, entriesCount < allEntriesCount ? `/${allEntriesCount}` : "", " prompts");
|
|
2352
|
+
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
2353
|
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
2354
|
return /* @__PURE__ */ React.createElement(
|
|
1768
2355
|
AppHeader,
|
|
@@ -1804,6 +2391,7 @@ var Footer = ({
|
|
|
1804
2391
|
);
|
|
1805
2392
|
if (hasProject2) base.push({ key: "C", description: "chat" });
|
|
1806
2393
|
base.push({ key: "r", description: "rerun" });
|
|
2394
|
+
base.push({ key: "p", description: "model" });
|
|
1807
2395
|
base.push("|");
|
|
1808
2396
|
base.push(
|
|
1809
2397
|
{ key: "x", description: "del" },
|
|
@@ -1825,113 +2413,217 @@ var Footer = ({
|
|
|
1825
2413
|
);
|
|
1826
2414
|
};
|
|
1827
2415
|
|
|
1828
|
-
// src/ui/
|
|
1829
|
-
import {
|
|
2416
|
+
// src/ui/FilterPanel.tsx
|
|
2417
|
+
import React3, { useMemo as useMemo2, useState } from "react";
|
|
2418
|
+
import { Box, Text as Text2, useInput } from "ink";
|
|
1830
2419
|
|
|
1831
|
-
// src/ui/
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
2420
|
+
// src/ui/filtering.ts
|
|
2421
|
+
var FILTER_CATEGORIES = ["project", "language", "role", "tool", "tag", "starred", "quality", "relevance"];
|
|
2422
|
+
var ROLE_COLOR2 = {
|
|
2423
|
+
debug: "red",
|
|
2424
|
+
refactor: "yellow",
|
|
2425
|
+
explain: "blue",
|
|
2426
|
+
review: "magenta",
|
|
2427
|
+
architect: "green",
|
|
2428
|
+
test: "cyan",
|
|
2429
|
+
docs: "white",
|
|
2430
|
+
generate: "green",
|
|
2431
|
+
research: "blue"
|
|
1838
2432
|
};
|
|
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);
|
|
2433
|
+
function parseMetadata(raw) {
|
|
2434
|
+
try {
|
|
2435
|
+
return JSON.parse(raw);
|
|
2436
|
+
} catch {
|
|
2437
|
+
return {};
|
|
1852
2438
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2439
|
+
}
|
|
2440
|
+
var CODEX_RETRY_WINDOW_MS = 2 * 60 * 1e3;
|
|
2441
|
+
var CODEX_RETRY_MIN_PREFIX_LENGTH = 80;
|
|
2442
|
+
function displayTitle(entry) {
|
|
2443
|
+
return parseMetadata(entry.metadata).title || extractTopic(entry.prompt);
|
|
2444
|
+
}
|
|
2445
|
+
function normalizedPrompt(prompt) {
|
|
2446
|
+
return prompt.trim().replace(/\s+/g, " ");
|
|
2447
|
+
}
|
|
2448
|
+
function collapseCodexRetries(entries) {
|
|
2449
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
2450
|
+
for (const entry of entries) {
|
|
2451
|
+
if (entry.tool.toLowerCase() !== "codex") continue;
|
|
2452
|
+
const key = `${entry.workdir}\0${displayTitle(entry)}`;
|
|
2453
|
+
const bucket = buckets.get(key) ?? [];
|
|
2454
|
+
bucket.push(entry);
|
|
2455
|
+
buckets.set(key, bucket);
|
|
2456
|
+
}
|
|
2457
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
2458
|
+
for (const bucket of buckets.values()) {
|
|
2459
|
+
const incomplete = bucket.filter((entry) => !entry.response.trim());
|
|
2460
|
+
const completed = bucket.filter((entry) => !!entry.response.trim());
|
|
2461
|
+
for (const pending of incomplete) {
|
|
2462
|
+
const pendingPrompt = normalizedPrompt(pending.prompt);
|
|
2463
|
+
if (pendingPrompt.length < CODEX_RETRY_MIN_PREFIX_LENGTH) continue;
|
|
2464
|
+
const pendingTime = Date.parse(pending.timestamp);
|
|
2465
|
+
if (!Number.isFinite(pendingTime)) continue;
|
|
2466
|
+
const retried = completed.some((candidate) => {
|
|
2467
|
+
const candidateTime = Date.parse(candidate.timestamp);
|
|
2468
|
+
const candidatePrompt = normalizedPrompt(candidate.prompt);
|
|
2469
|
+
const delta = Math.abs(candidateTime - pendingTime);
|
|
2470
|
+
return Number.isFinite(candidateTime) && delta <= CODEX_RETRY_WINDOW_MS && candidatePrompt.length > pendingPrompt.length && candidatePrompt.startsWith(pendingPrompt);
|
|
2471
|
+
});
|
|
2472
|
+
if (retried) hidden.add(pending.id);
|
|
1858
2473
|
}
|
|
1859
2474
|
}
|
|
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
|
-
}
|
|
2475
|
+
return hidden.size ? entries.filter((entry) => !hidden.has(entry.id)) : entries;
|
|
2476
|
+
}
|
|
2477
|
+
function getDistinctValues(entries, category) {
|
|
2478
|
+
const values = /* @__PURE__ */ new Set();
|
|
2479
|
+
for (const entry of entries) {
|
|
2480
|
+
if (category === "tool") {
|
|
2481
|
+
values.add(entry.tool);
|
|
2482
|
+
continue;
|
|
1910
2483
|
}
|
|
2484
|
+
const metadata = parseMetadata(entry.metadata);
|
|
2485
|
+
if (category === "project" && metadata.project) values.add(metadata.project);
|
|
2486
|
+
if (category === "language" && metadata.language) values.add(metadata.language);
|
|
2487
|
+
if (category === "role" && metadata.role) values.add(metadata.role);
|
|
2488
|
+
if (category === "tag") metadata.tags?.forEach((tag) => values.add(tag));
|
|
1911
2489
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2490
|
+
return [...values].sort();
|
|
2491
|
+
}
|
|
2492
|
+
function applyFilters(entries, active, textFilter) {
|
|
2493
|
+
let result = collapseCodexRetries(entries);
|
|
2494
|
+
const hasActiveFilter = Object.values(active).some((value) => value !== void 0 && value !== false);
|
|
2495
|
+
if (hasActiveFilter) {
|
|
2496
|
+
result = result.filter((entry) => {
|
|
2497
|
+
const metadata = parseMetadata(entry.metadata);
|
|
2498
|
+
if (active.tool && entry.tool !== active.tool) return false;
|
|
2499
|
+
if (active.project && metadata.project !== active.project) return false;
|
|
2500
|
+
if (active.language && metadata.language !== active.language) return false;
|
|
2501
|
+
if (active.role && metadata.role !== active.role) return false;
|
|
2502
|
+
if (active.tag && !metadata.tags?.includes(active.tag)) return false;
|
|
2503
|
+
if (active.starred && !metadata.starred) return false;
|
|
2504
|
+
if (active.minQuality !== void 0 && (metadata.quality ?? 0) < active.minQuality) return false;
|
|
2505
|
+
if (active.minRelevance !== void 0 && (metadata.relevance ?? 0) < active.minRelevance) return false;
|
|
2506
|
+
return true;
|
|
2507
|
+
});
|
|
1915
2508
|
}
|
|
1916
|
-
if (
|
|
1917
|
-
|
|
2509
|
+
if (textFilter) {
|
|
2510
|
+
const query = textFilter.toLowerCase();
|
|
2511
|
+
result = result.filter((entry) => {
|
|
2512
|
+
const metadata = parseMetadata(entry.metadata);
|
|
2513
|
+
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);
|
|
2514
|
+
});
|
|
1918
2515
|
}
|
|
1919
|
-
|
|
1920
|
-
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
2516
|
+
return result;
|
|
1921
2517
|
}
|
|
1922
|
-
function
|
|
1923
|
-
|
|
1924
|
-
if (
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2518
|
+
function toggleFilter(active, option) {
|
|
2519
|
+
if (option.category === "starred") return { ...active, starred: !option.active || void 0 };
|
|
2520
|
+
if (option.category === "quality" || option.category === "relevance") {
|
|
2521
|
+
const value = parseInt(option.label.replace(option.category === "quality" ? "Q \u2265 " : "R \u2265 ", ""), 10);
|
|
2522
|
+
return option.category === "quality" ? { ...active, minQuality: option.active ? void 0 : value } : { ...active, minRelevance: option.active ? void 0 : value };
|
|
2523
|
+
}
|
|
2524
|
+
const filterKey = option.category;
|
|
2525
|
+
if (option.active) {
|
|
2526
|
+
const updated = { ...active };
|
|
2527
|
+
delete updated[filterKey];
|
|
2528
|
+
return updated;
|
|
2529
|
+
}
|
|
2530
|
+
return { ...active, [filterKey]: option.label };
|
|
2531
|
+
}
|
|
2532
|
+
function buildFilterOptions(entries, active) {
|
|
2533
|
+
const options = [];
|
|
2534
|
+
for (const category of FILTER_CATEGORIES) {
|
|
2535
|
+
if (category === "starred") {
|
|
2536
|
+
const count = entries.filter((entry) => parseMetadata(entry.metadata).starred).length;
|
|
2537
|
+
options.push({ category, label: "\u2605 Only starred", count, active: !!active.starred });
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
if (category === "quality" || category === "relevance") {
|
|
2541
|
+
const isQuality = category === "quality";
|
|
2542
|
+
const activeValue = isQuality ? active.minQuality : active.minRelevance;
|
|
2543
|
+
const prefix = isQuality ? "Q \u2265 " : "R \u2265 ";
|
|
2544
|
+
options.push({ category, label: `${prefix}${activeValue ?? "?"}`, count: 0, active: activeValue !== void 0 });
|
|
2545
|
+
for (const value of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
|
|
2546
|
+
const count = entries.filter((entry) => {
|
|
2547
|
+
const metadata = parseMetadata(entry.metadata);
|
|
2548
|
+
return ((isQuality ? metadata.quality : metadata.relevance) ?? 0) >= value;
|
|
2549
|
+
}).length;
|
|
2550
|
+
options.push({ category, label: `${prefix}${value}`, count, active: activeValue === value });
|
|
2551
|
+
}
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
for (const value of getDistinctValues(entries, category)) {
|
|
2555
|
+
const count = entries.filter((entry) => {
|
|
2556
|
+
const metadata = parseMetadata(entry.metadata);
|
|
2557
|
+
if (category === "project") return metadata.project === value;
|
|
2558
|
+
if (category === "language") return metadata.language === value;
|
|
2559
|
+
if (category === "role") return metadata.role === value;
|
|
2560
|
+
if (category === "tool") return entry.tool === value;
|
|
2561
|
+
if (category === "tag") return metadata.tags?.includes(value) ?? false;
|
|
2562
|
+
return false;
|
|
2563
|
+
}).length;
|
|
2564
|
+
const activeValue = active[category];
|
|
2565
|
+
options.push({ category, label: value, count, active: activeValue === value });
|
|
2566
|
+
}
|
|
1929
2567
|
}
|
|
1930
|
-
return
|
|
2568
|
+
return options;
|
|
1931
2569
|
}
|
|
1932
2570
|
|
|
2571
|
+
// src/ui/FilterPanel.tsx
|
|
2572
|
+
var FilterPanel = ({ allEntries, active, onUpdate, onClose, theme }) => {
|
|
2573
|
+
const options = useMemo2(() => buildFilterOptions(allEntries, active), [allEntries, active]);
|
|
2574
|
+
const [cursor, setCursor] = useState(0);
|
|
2575
|
+
const visibleCount = Math.min(options.length, 16);
|
|
2576
|
+
const scrollOffset = Math.max(0, Math.min(cursor - Math.floor(visibleCount / 2), options.length - visibleCount));
|
|
2577
|
+
const visible = options.slice(Math.max(0, scrollOffset), scrollOffset + visibleCount);
|
|
2578
|
+
useInput((char, key) => {
|
|
2579
|
+
if (key.escape) {
|
|
2580
|
+
onClose();
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
if (char === "c") {
|
|
2584
|
+
onUpdate({});
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
if (key.return || char === " ") {
|
|
2588
|
+
const option = options[cursor];
|
|
2589
|
+
if (!option) return;
|
|
2590
|
+
onUpdate(toggleFilter(active, option));
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
if (key.upArrow) setCursor((value) => Math.max(0, value - 1));
|
|
2594
|
+
if (key.downArrow) setCursor((value) => Math.min(options.length - 1, value + 1));
|
|
2595
|
+
if (key.pageUp) setCursor((value) => Math.max(0, value - visibleCount));
|
|
2596
|
+
if (key.pageDown) setCursor((value) => Math.min(options.length - 1, value + visibleCount));
|
|
2597
|
+
if (char && /^[a-z]$/.test(char)) {
|
|
2598
|
+
const index = options.findIndex((option, i) => i > cursor && option.category[0] === char);
|
|
2599
|
+
if (index !== -1) setCursor(index);
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2602
|
+
const activeCount = Object.values(active).filter((value) => value !== void 0 && value !== false).length;
|
|
2603
|
+
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) => {
|
|
2604
|
+
const absoluteIndex = Math.max(0, scrollOffset) + index;
|
|
2605
|
+
const isCurrent = absoluteIndex === cursor;
|
|
2606
|
+
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";
|
|
2607
|
+
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"));
|
|
2608
|
+
})), 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")));
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2611
|
+
// src/ui/themes.ts
|
|
2612
|
+
import { THEMES as THEMES3, THEME_NAMES } from "@gi4nks/ink";
|
|
2613
|
+
|
|
2614
|
+
// src/ui/SearchBar.tsx
|
|
2615
|
+
import React4 from "react";
|
|
2616
|
+
import { Box as Box2, Text as Text3 } from "ink";
|
|
2617
|
+
var SearchBar = ({ value, isActive, theme }) => {
|
|
2618
|
+
const isEmpty = value === "";
|
|
2619
|
+
const bgColor = isActive ? theme.primary : theme.dim;
|
|
2620
|
+
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" : ""));
|
|
2621
|
+
};
|
|
2622
|
+
|
|
1933
2623
|
// src/ui/ListEntry.tsx
|
|
1934
|
-
|
|
2624
|
+
import React5 from "react";
|
|
2625
|
+
import { Box as Box3, Text as Text4 } from "ink";
|
|
2626
|
+
var ROLE_COLOR3 = {
|
|
1935
2627
|
debug: "red",
|
|
1936
2628
|
refactor: "yellow",
|
|
1937
2629
|
explain: "blue",
|
|
@@ -1946,6 +2638,7 @@ function toolIndicator(tool) {
|
|
|
1946
2638
|
if (tool === "claude") return { char: "\u25CF", color: "#f5a623" };
|
|
1947
2639
|
if (tool === "gemini") return { char: "\u25CF", color: "#4fc3f7" };
|
|
1948
2640
|
if (tool === "opencode") return { char: "\u25CF", color: "#e040fb" };
|
|
2641
|
+
if (tool === "codex") return { char: "\u25CF", color: "#74aa9c" };
|
|
1949
2642
|
return { char: "\u25CB", color: "gray" };
|
|
1950
2643
|
}
|
|
1951
2644
|
function truncate(str, width) {
|
|
@@ -1966,32 +2659,195 @@ var ListEntry = ({ entry, isSelected, paneWidth, theme }) => {
|
|
|
1966
2659
|
hour12: false
|
|
1967
2660
|
});
|
|
1968
2661
|
const role = meta.role ? truncate(meta.role, 6) : "";
|
|
1969
|
-
const roleColor = meta.role ?
|
|
2662
|
+
const roleColor = meta.role ? ROLE_COLOR3[meta.role] || "white" : "white";
|
|
1970
2663
|
const hasAnalysis = Boolean(meta.summary || meta.role || meta.tags && meta.tags.length > 0);
|
|
1971
|
-
const roleBar = meta.role ?
|
|
2664
|
+
const roleBar = meta.role ? ROLE_COLOR3[meta.role] || theme.dim : theme.dim;
|
|
1972
2665
|
const rawTitle = meta.title || extractTopic(entry.prompt);
|
|
1973
2666
|
const displayPrompt = truncate(rawTitle, paneWidth - 5);
|
|
1974
2667
|
let summaryLine = "";
|
|
1975
2668
|
if (meta.summary) {
|
|
1976
2669
|
summaryLine = truncate(meta.summary.replace(/\n/g, " "), paneWidth - 5);
|
|
1977
2670
|
}
|
|
1978
|
-
return /* @__PURE__ */
|
|
2671
|
+
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
2672
|
};
|
|
1980
2673
|
|
|
1981
2674
|
// src/ui/PreviewPane.tsx
|
|
1982
|
-
import
|
|
1983
|
-
import { Box as
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2675
|
+
import React6, { useState as useState2, useMemo as useMemo3 } from "react";
|
|
2676
|
+
import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
|
|
2677
|
+
|
|
2678
|
+
// src/utils/syntaxHighlight.ts
|
|
2679
|
+
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
2680
|
+
"if",
|
|
2681
|
+
"else",
|
|
2682
|
+
"for",
|
|
2683
|
+
"while",
|
|
2684
|
+
"do",
|
|
2685
|
+
"switch",
|
|
2686
|
+
"case",
|
|
2687
|
+
"break",
|
|
2688
|
+
"continue",
|
|
2689
|
+
"return",
|
|
2690
|
+
"function",
|
|
2691
|
+
"class",
|
|
2692
|
+
"struct",
|
|
2693
|
+
"interface",
|
|
2694
|
+
"enum",
|
|
2695
|
+
"type",
|
|
2696
|
+
"const",
|
|
2697
|
+
"let",
|
|
2698
|
+
"var",
|
|
2699
|
+
"import",
|
|
2700
|
+
"export",
|
|
2701
|
+
"from",
|
|
2702
|
+
"default",
|
|
2703
|
+
"async",
|
|
2704
|
+
"await",
|
|
2705
|
+
"yield",
|
|
2706
|
+
"throw",
|
|
2707
|
+
"try",
|
|
2708
|
+
"catch",
|
|
2709
|
+
"finally",
|
|
2710
|
+
"new",
|
|
2711
|
+
"delete",
|
|
2712
|
+
"typeof",
|
|
2713
|
+
"instanceof",
|
|
2714
|
+
"void",
|
|
2715
|
+
"null",
|
|
2716
|
+
"undefined",
|
|
2717
|
+
"true",
|
|
2718
|
+
"false",
|
|
2719
|
+
"this",
|
|
2720
|
+
"super",
|
|
2721
|
+
"extends",
|
|
2722
|
+
"implements",
|
|
2723
|
+
"package",
|
|
2724
|
+
"namespace",
|
|
2725
|
+
"using",
|
|
2726
|
+
"def",
|
|
2727
|
+
"in",
|
|
2728
|
+
"not",
|
|
2729
|
+
"is",
|
|
2730
|
+
"and",
|
|
2731
|
+
"or",
|
|
2732
|
+
"with",
|
|
2733
|
+
"as",
|
|
2734
|
+
"pass",
|
|
2735
|
+
"raise",
|
|
2736
|
+
"lambda",
|
|
2737
|
+
"pub",
|
|
2738
|
+
"fn",
|
|
2739
|
+
"let",
|
|
2740
|
+
"mut",
|
|
2741
|
+
"impl",
|
|
2742
|
+
"trait",
|
|
2743
|
+
"use",
|
|
2744
|
+
"mod",
|
|
2745
|
+
"where",
|
|
2746
|
+
"ref",
|
|
2747
|
+
"func",
|
|
2748
|
+
"go",
|
|
2749
|
+
"defer",
|
|
2750
|
+
"select",
|
|
2751
|
+
"chan",
|
|
2752
|
+
"range",
|
|
2753
|
+
"map",
|
|
2754
|
+
"SELECT",
|
|
2755
|
+
"FROM",
|
|
2756
|
+
"WHERE",
|
|
2757
|
+
"INSERT",
|
|
2758
|
+
"UPDATE",
|
|
2759
|
+
"DELETE",
|
|
2760
|
+
"CREATE",
|
|
2761
|
+
"TABLE",
|
|
2762
|
+
"INTO",
|
|
2763
|
+
"VALUES",
|
|
2764
|
+
"SET",
|
|
2765
|
+
"JOIN",
|
|
2766
|
+
"LEFT",
|
|
2767
|
+
"RIGHT",
|
|
2768
|
+
"INNER",
|
|
2769
|
+
"OUTER",
|
|
2770
|
+
"ON",
|
|
2771
|
+
"AND",
|
|
2772
|
+
"OR",
|
|
2773
|
+
"NOT",
|
|
2774
|
+
"IN",
|
|
2775
|
+
"AS",
|
|
2776
|
+
"ORDER",
|
|
2777
|
+
"BY",
|
|
2778
|
+
"GROUP",
|
|
2779
|
+
"HAVING",
|
|
2780
|
+
"LIMIT",
|
|
2781
|
+
"OFFSET",
|
|
2782
|
+
"static",
|
|
2783
|
+
"public",
|
|
2784
|
+
"private",
|
|
2785
|
+
"protected",
|
|
2786
|
+
"readonly",
|
|
2787
|
+
"abstract",
|
|
2788
|
+
"virtual",
|
|
2789
|
+
"override",
|
|
2790
|
+
"final",
|
|
2791
|
+
"sealed",
|
|
2792
|
+
"internal",
|
|
2793
|
+
"extern",
|
|
2794
|
+
"module",
|
|
2795
|
+
"requires",
|
|
2796
|
+
"exports",
|
|
2797
|
+
"opens",
|
|
2798
|
+
"provides",
|
|
2799
|
+
"to",
|
|
2800
|
+
"transitive",
|
|
2801
|
+
"match",
|
|
2802
|
+
"enum",
|
|
2803
|
+
"macro",
|
|
2804
|
+
"unsafe"
|
|
2805
|
+
]);
|
|
2806
|
+
function tokenizeLine(line) {
|
|
2807
|
+
const segments = [];
|
|
2808
|
+
let i = 0;
|
|
2809
|
+
while (i < line.length) {
|
|
2810
|
+
const rest = line.slice(i);
|
|
2811
|
+
const strMatch = rest.match(/^("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/);
|
|
2812
|
+
if (strMatch) {
|
|
2813
|
+
segments.push({ text: strMatch[1], color: "green" });
|
|
2814
|
+
i += strMatch[1].length;
|
|
2815
|
+
continue;
|
|
2816
|
+
}
|
|
2817
|
+
const commentMatch = rest.match(/^(\/\/.*)/);
|
|
2818
|
+
if (commentMatch) {
|
|
2819
|
+
segments.push({ text: commentMatch[1], dim: true });
|
|
2820
|
+
i += commentMatch[1].length;
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
if (rest[0] === "#") {
|
|
2824
|
+
segments.push({ text: rest.slice(i), dim: true });
|
|
2825
|
+
break;
|
|
2826
|
+
}
|
|
2827
|
+
const numMatch = rest.match(/^(\d+(?:\.\d+)?)/);
|
|
2828
|
+
if (numMatch) {
|
|
2829
|
+
segments.push({ text: numMatch[1], color: "yellow" });
|
|
2830
|
+
i += numMatch[1].length;
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
const wordMatch = rest.match(/^([a-zA-Z_][a-zA-Z0-9_]*)/);
|
|
2834
|
+
if (wordMatch) {
|
|
2835
|
+
const word = wordMatch[1];
|
|
2836
|
+
if (KEYWORDS.has(word)) {
|
|
2837
|
+
segments.push({ text: word, color: "blue" });
|
|
2838
|
+
} else if (/^[A-Z]/.test(word)) {
|
|
2839
|
+
segments.push({ text: word, color: "cyan" });
|
|
2840
|
+
} else {
|
|
2841
|
+
segments.push({ text: word });
|
|
2842
|
+
}
|
|
2843
|
+
i += word.length;
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
segments.push({ text: rest[0] });
|
|
2847
|
+
i++;
|
|
2848
|
+
}
|
|
2849
|
+
return segments;
|
|
2850
|
+
}
|
|
1995
2851
|
function wrapTextLines(text, width) {
|
|
1996
2852
|
const lines = text.split("\n");
|
|
1997
2853
|
const result = [];
|
|
@@ -2002,13 +2858,77 @@ function wrapTextLines(text, width) {
|
|
|
2002
2858
|
}
|
|
2003
2859
|
let current = line;
|
|
2004
2860
|
while (current.length > width) {
|
|
2005
|
-
result.push(current.
|
|
2006
|
-
current = current.
|
|
2861
|
+
result.push(current.slice(0, width));
|
|
2862
|
+
current = current.slice(width);
|
|
2007
2863
|
}
|
|
2008
2864
|
if (current.length > 0) result.push(current);
|
|
2009
2865
|
}
|
|
2010
2866
|
return result;
|
|
2011
2867
|
}
|
|
2868
|
+
function wrapRichLines(lines, width) {
|
|
2869
|
+
const result = [];
|
|
2870
|
+
for (const line of lines) {
|
|
2871
|
+
const totalLen = line.segments.reduce((sum, s) => sum + s.text.length, 0);
|
|
2872
|
+
if (totalLen <= width) {
|
|
2873
|
+
result.push(line);
|
|
2874
|
+
continue;
|
|
2875
|
+
}
|
|
2876
|
+
let currentSegs = [];
|
|
2877
|
+
let currentLen = 0;
|
|
2878
|
+
for (const seg of line.segments) {
|
|
2879
|
+
let remaining = seg.text;
|
|
2880
|
+
while (remaining.length > 0) {
|
|
2881
|
+
const space = width - currentLen;
|
|
2882
|
+
if (remaining.length <= space) {
|
|
2883
|
+
currentSegs.push({ text: remaining, color: seg.color, dim: seg.dim });
|
|
2884
|
+
currentLen += remaining.length;
|
|
2885
|
+
remaining = "";
|
|
2886
|
+
} else {
|
|
2887
|
+
currentSegs.push({ text: remaining.slice(0, space), color: seg.color, dim: seg.dim });
|
|
2888
|
+
result.push({ segments: currentSegs });
|
|
2889
|
+
currentSegs = [];
|
|
2890
|
+
currentLen = 0;
|
|
2891
|
+
remaining = remaining.slice(space);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
if (currentLen > 0) {
|
|
2896
|
+
result.push({ segments: currentSegs });
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
return result;
|
|
2900
|
+
}
|
|
2901
|
+
function buildRichLines(text, width) {
|
|
2902
|
+
const rawLines = text.split("\n");
|
|
2903
|
+
let inCode = false;
|
|
2904
|
+
const parsed = [];
|
|
2905
|
+
for (const rawLine of rawLines) {
|
|
2906
|
+
const trimmed = rawLine.trim();
|
|
2907
|
+
if (trimmed.startsWith("```")) {
|
|
2908
|
+
inCode = !inCode;
|
|
2909
|
+
continue;
|
|
2910
|
+
}
|
|
2911
|
+
if (inCode) {
|
|
2912
|
+
parsed.push({ segments: tokenizeLine(rawLine) });
|
|
2913
|
+
} else {
|
|
2914
|
+
parsed.push({ segments: [{ text: rawLine }] });
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
return wrapRichLines(parsed, width);
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// src/ui/PreviewPane.tsx
|
|
2921
|
+
var ROLE_COLOR4 = {
|
|
2922
|
+
debug: "red",
|
|
2923
|
+
refactor: "yellow",
|
|
2924
|
+
explain: "blue",
|
|
2925
|
+
review: "magenta",
|
|
2926
|
+
architect: "green",
|
|
2927
|
+
test: "cyan",
|
|
2928
|
+
docs: "white",
|
|
2929
|
+
generate: "green",
|
|
2930
|
+
research: "blue"
|
|
2931
|
+
};
|
|
2012
2932
|
function formatTimestamp(ts) {
|
|
2013
2933
|
try {
|
|
2014
2934
|
const d = new Date(ts);
|
|
@@ -2037,80 +2957,78 @@ var PreviewPane = ({
|
|
|
2037
2957
|
theme,
|
|
2038
2958
|
getProjectMemories
|
|
2039
2959
|
}) => {
|
|
2040
|
-
const [activeTab, setActiveTab] =
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
useEffect(() => {
|
|
2960
|
+
const [activeTab, setActiveTab] = useState2(
|
|
2961
|
+
entry?.response ? "response" : "prompt"
|
|
2962
|
+
);
|
|
2963
|
+
const [scrollOffset, setScrollOffset] = useState2(0);
|
|
2964
|
+
const switchTab = (tab) => {
|
|
2965
|
+
setActiveTab(tab);
|
|
2047
2966
|
setScrollOffset(0);
|
|
2048
|
-
}
|
|
2049
|
-
const meta =
|
|
2967
|
+
};
|
|
2968
|
+
const meta = useMemo3(() => entry ? parseMeta(entry.metadata) : {}, [entry]);
|
|
2050
2969
|
const contentWidth = Math.max(10, paneWidth - 4);
|
|
2051
2970
|
const contentHeight = Math.max(3, paneHeight - 6);
|
|
2052
|
-
const allLines =
|
|
2971
|
+
const allLines = useMemo3(() => {
|
|
2053
2972
|
if (!entry) return [];
|
|
2054
|
-
if (activeTab === "prompt") return
|
|
2055
|
-
if (activeTab === "response") return
|
|
2973
|
+
if (activeTab === "prompt") return buildRichLines(entry.prompt, contentWidth);
|
|
2974
|
+
if (activeTab === "response") return buildRichLines(entry.response || "(no response captured)", contentWidth);
|
|
2056
2975
|
const lines = [];
|
|
2976
|
+
const push2 = (text) => lines.push({ segments: [{ text }] });
|
|
2057
2977
|
if (meta.project && getProjectMemories) {
|
|
2058
2978
|
const memories = getProjectMemories(meta.project);
|
|
2059
2979
|
if (memories.length > 0) {
|
|
2060
|
-
|
|
2061
|
-
|
|
2980
|
+
push2(`\u2500\u2500 Project Knowledge: ${meta.project} \u2500\u2500`);
|
|
2981
|
+
push2("");
|
|
2062
2982
|
for (const mem of memories) {
|
|
2063
2983
|
if (mem.summary) {
|
|
2064
|
-
|
|
2065
|
-
|
|
2984
|
+
for (const l of wrapTextLines(mem.summary, contentWidth)) push2(l);
|
|
2985
|
+
push2("");
|
|
2066
2986
|
}
|
|
2067
2987
|
if (mem.key_insights.length > 0) {
|
|
2068
|
-
|
|
2069
|
-
for (const i of mem.key_insights)
|
|
2070
|
-
|
|
2988
|
+
push2("Key Insights:");
|
|
2989
|
+
for (const i of mem.key_insights) for (const l of wrapTextLines(` \u2022 ${i}`, contentWidth)) push2(l);
|
|
2990
|
+
push2("");
|
|
2071
2991
|
}
|
|
2072
2992
|
if (mem.technical_decisions.length > 0) {
|
|
2073
|
-
|
|
2074
|
-
for (const d of mem.technical_decisions)
|
|
2075
|
-
|
|
2993
|
+
push2("Technical Decisions:");
|
|
2994
|
+
for (const d of mem.technical_decisions) for (const l of wrapTextLines(` \u2022 ${d}`, contentWidth)) push2(l);
|
|
2995
|
+
push2("");
|
|
2076
2996
|
}
|
|
2077
2997
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2998
|
+
push2("\u2500\u2500 This Entry \u2500\u2500");
|
|
2999
|
+
push2("");
|
|
2080
3000
|
}
|
|
2081
3001
|
}
|
|
2082
3002
|
if (meta.summary) {
|
|
2083
|
-
|
|
2084
|
-
|
|
3003
|
+
for (const l of wrapTextLines(meta.summary, contentWidth)) push2(l);
|
|
3004
|
+
push2("");
|
|
2085
3005
|
}
|
|
2086
3006
|
if (meta.key_insights && meta.key_insights.length > 0) {
|
|
2087
3007
|
for (const insight of meta.key_insights) {
|
|
2088
|
-
|
|
3008
|
+
for (const l of wrapTextLines(`\u2022 ${insight}`, contentWidth)) push2(l);
|
|
2089
3009
|
}
|
|
2090
|
-
|
|
2091
|
-
}
|
|
2092
|
-
if (lines.length === 0) {
|
|
2093
|
-
lines.push("(no AI analysis - run ph analyze)");
|
|
3010
|
+
push2("");
|
|
2094
3011
|
}
|
|
3012
|
+
if (lines.length === 0) push2("(no AI analysis - run ph analyze)");
|
|
2095
3013
|
return lines;
|
|
2096
3014
|
}, [entry, activeTab, contentWidth, meta, getProjectMemories]);
|
|
2097
3015
|
const maxScroll = Math.max(0, allLines.length - contentHeight);
|
|
2098
|
-
|
|
3016
|
+
useInput2((_char, key) => {
|
|
2099
3017
|
if (!isFocused) return;
|
|
2100
|
-
if (_char === "1")
|
|
2101
|
-
if (_char === "2")
|
|
2102
|
-
if (_char === "3")
|
|
3018
|
+
if (_char === "1") switchTab("prompt");
|
|
3019
|
+
if (_char === "2") switchTab("response");
|
|
3020
|
+
if (_char === "3") switchTab("memory");
|
|
2103
3021
|
if (key.upArrow) setScrollOffset((s) => Math.max(0, s - 1));
|
|
2104
3022
|
if (key.downArrow) setScrollOffset((s) => Math.min(maxScroll, s + 1));
|
|
2105
3023
|
if (key.pageUp) setScrollOffset((s) => Math.max(0, s - contentHeight));
|
|
2106
3024
|
if (key.pageDown) setScrollOffset((s) => Math.min(maxScroll, s + contentHeight));
|
|
2107
3025
|
});
|
|
2108
3026
|
if (!entry) {
|
|
2109
|
-
return /* @__PURE__ */
|
|
3027
|
+
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
3028
|
}
|
|
2111
3029
|
const visibleLines = allLines.slice(scrollOffset, scrollOffset + contentHeight);
|
|
2112
3030
|
const paddedLines = [...visibleLines];
|
|
2113
|
-
while (paddedLines.length < contentHeight) paddedLines.push("");
|
|
3031
|
+
while (paddedLines.length < contentHeight) paddedLines.push({ segments: [{ text: " " }] });
|
|
2114
3032
|
const topIndicator = scrollOffset > 0 ? ` \u2191 ${scrollOffset} above` : " ";
|
|
2115
3033
|
const remaining = allLines.length - scrollOffset - contentHeight;
|
|
2116
3034
|
const botIndicator = scrollOffset < maxScroll ? ` \u2193 ${remaining} more` : " ";
|
|
@@ -2120,8 +3038,8 @@ var PreviewPane = ({
|
|
|
2120
3038
|
const tabPromptColor = isPromptActive && isFocused ? theme.primary : theme.dim;
|
|
2121
3039
|
const tabResponseColor = isResponseActive && isFocused ? theme.primary : theme.dim;
|
|
2122
3040
|
const tabMemoryColor = isMemoryActive && isFocused ? theme.primary : theme.dim;
|
|
2123
|
-
return /* @__PURE__ */
|
|
2124
|
-
|
|
3041
|
+
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(
|
|
3042
|
+
Text5,
|
|
2125
3043
|
{
|
|
2126
3044
|
color: tabPromptColor,
|
|
2127
3045
|
bold: isPromptActive && isFocused,
|
|
@@ -2129,8 +3047,8 @@ var PreviewPane = ({
|
|
|
2129
3047
|
},
|
|
2130
3048
|
isPromptActive ? "\u25CF " : "\u25CB ",
|
|
2131
3049
|
"1:PROMPT"
|
|
2132
|
-
), /* @__PURE__ */
|
|
2133
|
-
|
|
3050
|
+
), /* @__PURE__ */ React6.createElement(Text5, null, " "), /* @__PURE__ */ React6.createElement(
|
|
3051
|
+
Text5,
|
|
2134
3052
|
{
|
|
2135
3053
|
color: tabResponseColor,
|
|
2136
3054
|
bold: isResponseActive && isFocused,
|
|
@@ -2138,8 +3056,8 @@ var PreviewPane = ({
|
|
|
2138
3056
|
},
|
|
2139
3057
|
isResponseActive ? "\u25CF " : "\u25CB ",
|
|
2140
3058
|
"2:RESPONSE"
|
|
2141
|
-
), /* @__PURE__ */
|
|
2142
|
-
|
|
3059
|
+
), /* @__PURE__ */ React6.createElement(Text5, null, " "), /* @__PURE__ */ React6.createElement(
|
|
3060
|
+
Text5,
|
|
2143
3061
|
{
|
|
2144
3062
|
color: tabMemoryColor,
|
|
2145
3063
|
bold: isMemoryActive && isFocused,
|
|
@@ -2147,22 +3065,56 @@ var PreviewPane = ({
|
|
|
2147
3065
|
},
|
|
2148
3066
|
isMemoryActive ? "\u25CF " : "\u25CB ",
|
|
2149
3067
|
"3:MEMORY"
|
|
2150
|
-
)), /* @__PURE__ */
|
|
3068
|
+
)), /* @__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
3069
|
};
|
|
2152
3070
|
|
|
3071
|
+
// src/config/provider-summary.ts
|
|
3072
|
+
function getProviderSummaries(cfg) {
|
|
3073
|
+
const active = cfg.analyzeProvider ?? "ollama";
|
|
3074
|
+
return [
|
|
3075
|
+
{
|
|
3076
|
+
id: "ollama",
|
|
3077
|
+
label: "Ollama",
|
|
3078
|
+
model: cfg.ollamaModel ?? "llama3.1:latest",
|
|
3079
|
+
url: cfg.ollamaUrl ?? "http://localhost:11434",
|
|
3080
|
+
credential: "not required",
|
|
3081
|
+
active: active === "ollama",
|
|
3082
|
+
ready: true
|
|
3083
|
+
},
|
|
3084
|
+
{
|
|
3085
|
+
id: "gemini",
|
|
3086
|
+
label: "Gemini",
|
|
3087
|
+
model: "gemini-2.5-flash",
|
|
3088
|
+
url: "(default)",
|
|
3089
|
+
credential: process.env.GEMINI_API_KEY || cfg.geminiApiKey ? "configured" : "not set",
|
|
3090
|
+
active: active === "gemini",
|
|
3091
|
+
ready: Boolean(process.env.GEMINI_API_KEY || cfg.geminiApiKey)
|
|
3092
|
+
},
|
|
3093
|
+
{
|
|
3094
|
+
id: "openrouter",
|
|
3095
|
+
label: "OpenRouter",
|
|
3096
|
+
model: process.env.OPENROUTER_MODEL ?? cfg.openrouterModel ?? "(not set)",
|
|
3097
|
+
url: process.env.OPENROUTER_BASE_URL ?? cfg.openrouterUrl ?? "https://openrouter.ai/api/v1",
|
|
3098
|
+
credential: process.env.OPENROUTER_API_KEY || cfg.openrouterApiKey ? "configured" : "not set",
|
|
3099
|
+
active: active === "openrouter",
|
|
3100
|
+
ready: Boolean((process.env.OPENROUTER_API_KEY || cfg.openrouterApiKey) && (process.env.OPENROUTER_MODEL || cfg.openrouterModel))
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
id: "mlx-serve",
|
|
3104
|
+
label: "MLX-Serve",
|
|
3105
|
+
model: process.env.MLX_SERVE_MODEL ?? cfg.mlxServeModel ?? "(not set)",
|
|
3106
|
+
url: process.env.MLX_SERVE_BASE_URL ?? cfg.mlxServeUrl ?? "http://127.0.0.1:11234/v1",
|
|
3107
|
+
credential: process.env.MLX_SERVE_API_KEY || process.env.GLB_OMLX_API_KEY || cfg.mlxServeApiKey ? "configured" : "not required",
|
|
3108
|
+
active: active === "mlx-serve",
|
|
3109
|
+
ready: Boolean(process.env.MLX_SERVE_MODEL || cfg.mlxServeModel)
|
|
3110
|
+
}
|
|
3111
|
+
];
|
|
3112
|
+
}
|
|
3113
|
+
function formatProviderSummary(provider) {
|
|
3114
|
+
return `${provider.active ? "*" : " "} ${provider.label}: model=${provider.model}, url=${provider.url}, key=${provider.credential}, setup=${provider.ready ? "ready" : "incomplete"}`;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
2153
3117
|
// 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
3118
|
function copyToClipboard(text) {
|
|
2167
3119
|
try {
|
|
2168
3120
|
const platform = process.platform;
|
|
@@ -2182,28 +3134,12 @@ function copyToClipboard(text) {
|
|
|
2182
3134
|
} catch {
|
|
2183
3135
|
}
|
|
2184
3136
|
}
|
|
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
3137
|
function useStdoutDimensions() {
|
|
2202
|
-
const [dimensions, setDimensions] =
|
|
3138
|
+
const [dimensions, setDimensions] = useState3({
|
|
2203
3139
|
columns: process.stdout.columns || 120,
|
|
2204
3140
|
rows: process.stdout.rows || 24
|
|
2205
3141
|
});
|
|
2206
|
-
|
|
3142
|
+
useEffect(() => {
|
|
2207
3143
|
const handler = () => {
|
|
2208
3144
|
setDimensions({
|
|
2209
3145
|
columns: process.stdout.columns || 120,
|
|
@@ -2226,16 +3162,9 @@ function formatTimestamp2(ts) {
|
|
|
2226
3162
|
return ts.slice(0, 16);
|
|
2227
3163
|
}
|
|
2228
3164
|
}
|
|
2229
|
-
function parseMeta2(raw) {
|
|
2230
|
-
try {
|
|
2231
|
-
return JSON.parse(raw);
|
|
2232
|
-
} catch {
|
|
2233
|
-
return {};
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
3165
|
function hasProject(entry) {
|
|
2237
3166
|
if (!entry) return false;
|
|
2238
|
-
const meta =
|
|
3167
|
+
const meta = parseMetadata(entry.metadata);
|
|
2239
3168
|
return Boolean(meta.project);
|
|
2240
3169
|
}
|
|
2241
3170
|
var SESSION_GAP_MS = 2 * 60 * 60 * 1e3;
|
|
@@ -2254,94 +3183,49 @@ function needsSessionSeparator(entries, idx) {
|
|
|
2254
3183
|
const curr = new Date(entries[idx].timestamp).getTime();
|
|
2255
3184
|
return prev - curr > SESSION_GAP_MS;
|
|
2256
3185
|
}
|
|
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
3186
|
var DetailView = ({ entry, onClose, onEdit, termWidth, termHeight, theme }) => {
|
|
2299
|
-
const meta =
|
|
2300
|
-
const [activeTab, setActiveTab] =
|
|
2301
|
-
const [scrollOffset, setScrollOffset] =
|
|
2302
|
-
const [copied, setCopied] =
|
|
2303
|
-
|
|
3187
|
+
const meta = parseMetadata(entry.metadata);
|
|
3188
|
+
const [activeTab, setActiveTab] = useState3(entry.response ? "response" : "prompt");
|
|
3189
|
+
const [scrollOffset, setScrollOffset] = useState3(0);
|
|
3190
|
+
const [copied, setCopied] = useState3(false);
|
|
3191
|
+
const switchTab = (tab) => {
|
|
3192
|
+
setActiveTab(tab);
|
|
2304
3193
|
setScrollOffset(0);
|
|
2305
|
-
}
|
|
3194
|
+
};
|
|
2306
3195
|
const contentWidth = Math.max(10, termWidth - 4);
|
|
2307
3196
|
const text = activeTab === "prompt" ? entry.prompt : entry.response || "(no response captured)";
|
|
2308
|
-
const lines =
|
|
3197
|
+
const lines = useMemo4(() => {
|
|
2309
3198
|
if (activeTab === "prompt" || activeTab === "response") {
|
|
2310
|
-
return
|
|
3199
|
+
return buildRichLines(text, contentWidth);
|
|
2311
3200
|
}
|
|
2312
3201
|
const memLines = [];
|
|
3202
|
+
const push2 = (t) => memLines.push({ segments: [{ text: t }] });
|
|
2313
3203
|
if (meta.summary) {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
3204
|
+
push2("SUMMARY:");
|
|
3205
|
+
for (const l of wrapTextLines(meta.summary, contentWidth)) push2(l);
|
|
3206
|
+
push2("");
|
|
2317
3207
|
}
|
|
2318
3208
|
if (meta.key_insights && meta.key_insights.length > 0) {
|
|
2319
|
-
|
|
3209
|
+
push2("KEY INSIGHTS:");
|
|
2320
3210
|
for (const insight of meta.key_insights) {
|
|
2321
|
-
|
|
3211
|
+
for (const l of wrapTextLines(`\u2022 ${insight}`, contentWidth)) push2(l);
|
|
2322
3212
|
}
|
|
2323
3213
|
}
|
|
2324
|
-
if (memLines.length === 0)
|
|
2325
|
-
memLines.push("(no AI analysis found - run ph analyze)");
|
|
2326
|
-
}
|
|
3214
|
+
if (memLines.length === 0) push2("(no AI analysis found - run ph analyze)");
|
|
2327
3215
|
return memLines;
|
|
2328
3216
|
}, [activeTab, text, contentWidth, meta]);
|
|
2329
3217
|
const contentHeight = Math.max(1, termHeight - 10);
|
|
2330
3218
|
const maxScroll = Math.max(0, lines.length - contentHeight);
|
|
2331
|
-
|
|
3219
|
+
useInput3((char, key) => {
|
|
2332
3220
|
if (key.escape || key.return) onClose();
|
|
2333
3221
|
else if (char === "e") onEdit();
|
|
2334
3222
|
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
|
-
});
|
|
3223
|
+
if (char === "1") switchTab("prompt");
|
|
3224
|
+
else if (char === "2") switchTab("response");
|
|
3225
|
+
else if (char === "3") switchTab("memory");
|
|
3226
|
+
else switchTab(activeTab === "prompt" ? "response" : activeTab === "response" ? "memory" : "prompt");
|
|
2343
3227
|
} else if (char === "y") {
|
|
2344
|
-
const copyText = activeTab === "memory" ? lines.join("\n") : text;
|
|
3228
|
+
const copyText = activeTab === "memory" ? lines.map((l) => l.segments.map((s) => s.text).join("")).join("\n") : text;
|
|
2345
3229
|
copyToClipboard(copyText);
|
|
2346
3230
|
setCopied(true);
|
|
2347
3231
|
setTimeout(() => setCopied(false), 2e3);
|
|
@@ -2352,15 +3236,15 @@ var DetailView = ({ entry, onClose, onEdit, termWidth, termHeight, theme }) => {
|
|
|
2352
3236
|
});
|
|
2353
3237
|
const visibleLines = lines.slice(scrollOffset, scrollOffset + contentHeight);
|
|
2354
3238
|
const remaining = lines.length - (scrollOffset + contentHeight);
|
|
2355
|
-
return /* @__PURE__ */
|
|
3239
|
+
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
3240
|
};
|
|
2357
3241
|
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
|
-
|
|
3242
|
+
const initMeta = parseMetadata(entry.metadata);
|
|
3243
|
+
const [field, setField] = useState3("title");
|
|
3244
|
+
const [titleValue, setTitleValue] = useState3(initMeta.title ?? extractTopic(entry.prompt));
|
|
3245
|
+
const [roleValue, setRoleValue] = useState3(initMeta.role ?? "");
|
|
3246
|
+
const [tagsValue, setTagsValue] = useState3(initMeta.tags?.join(", ") ?? "");
|
|
3247
|
+
useInput3((char, key) => {
|
|
2364
3248
|
if (key.escape) {
|
|
2365
3249
|
onClose();
|
|
2366
3250
|
return;
|
|
@@ -2392,15 +3276,15 @@ var EditView = ({ entry, onSave, onClose, theme }) => {
|
|
|
2392
3276
|
}
|
|
2393
3277
|
});
|
|
2394
3278
|
const ROLES = "debug \xB7 refactor \xB7 explain \xB7 review \xB7 architect \xB7 test \xB7 docs \xB7 generate \xB7 research";
|
|
2395
|
-
return /* @__PURE__ */
|
|
3279
|
+
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
3280
|
};
|
|
2397
3281
|
var RerunView = ({ entry, onConfirm, onClose, theme }) => {
|
|
2398
|
-
const [field, setField] =
|
|
2399
|
-
const [toolValue, setToolValue] =
|
|
2400
|
-
const [promptValue, setPromptValue] =
|
|
2401
|
-
const meta =
|
|
3282
|
+
const [field, setField] = useState3("prompt");
|
|
3283
|
+
const [toolValue, setToolValue] = useState3(entry.tool);
|
|
3284
|
+
const [promptValue, setPromptValue] = useState3(entry.prompt);
|
|
3285
|
+
const meta = parseMetadata(entry.metadata);
|
|
2402
3286
|
const git = meta.git_context;
|
|
2403
|
-
|
|
3287
|
+
useInput3((char, key) => {
|
|
2404
3288
|
if (key.escape) {
|
|
2405
3289
|
onClose();
|
|
2406
3290
|
return;
|
|
@@ -2420,137 +3304,36 @@ var RerunView = ({ entry, onConfirm, onClose, theme }) => {
|
|
|
2420
3304
|
setter((v) => v + char);
|
|
2421
3305
|
}
|
|
2422
3306
|
});
|
|
2423
|
-
return /* @__PURE__ */
|
|
3307
|
+
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")));
|
|
2424
3308
|
};
|
|
2425
|
-
var
|
|
2426
|
-
const
|
|
2427
|
-
|
|
2428
|
-
|
|
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) => {
|
|
2492
|
-
if (key.escape) {
|
|
3309
|
+
var ProviderPicker = ({ entry, onPick, onClose, theme }) => {
|
|
3310
|
+
const providers = getProviderSummaries(load());
|
|
3311
|
+
useInput3((char) => {
|
|
3312
|
+
if (char === "q" || char === "\x1B") {
|
|
2493
3313
|
onClose();
|
|
2494
3314
|
return;
|
|
2495
3315
|
}
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
if (key.return || char === " ") {
|
|
2501
|
-
const opt = options[cursor];
|
|
2502
|
-
if (!opt) return;
|
|
2503
|
-
if (opt.category === "starred") {
|
|
2504
|
-
onUpdate({ ...active, starred: !opt.active || void 0 });
|
|
2505
|
-
} else if (opt.category === "quality") {
|
|
2506
|
-
const v = parseInt(opt.label.replace("Q \u2265 ", ""), 10);
|
|
2507
|
-
onUpdate({ ...active, minQuality: opt.active ? void 0 : v });
|
|
2508
|
-
} else if (opt.category === "relevance") {
|
|
2509
|
-
const v = parseInt(opt.label.replace("R \u2265 ", ""), 10);
|
|
2510
|
-
onUpdate({ ...active, minRelevance: opt.active ? void 0 : v });
|
|
2511
|
-
} else {
|
|
2512
|
-
const key2 = opt.category;
|
|
2513
|
-
if (opt.active) {
|
|
2514
|
-
const updated = { ...active };
|
|
2515
|
-
delete updated[key2];
|
|
2516
|
-
onUpdate(updated);
|
|
2517
|
-
} else {
|
|
2518
|
-
onUpdate({ ...active, [key2]: opt.label });
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
return;
|
|
2522
|
-
}
|
|
2523
|
-
if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
|
|
2524
|
-
if (key.downArrow) setCursor((c) => Math.min(options.length - 1, c + 1));
|
|
2525
|
-
if (key.pageUp) setCursor((c) => Math.max(0, c - visibleCount));
|
|
2526
|
-
if (key.pageDown) setCursor((c) => Math.min(options.length - 1, c + visibleCount));
|
|
2527
|
-
if (char && /^[a-z]$/.test(char)) {
|
|
2528
|
-
const idx = options.findIndex((o, i) => i > cursor && o.category[0] === char);
|
|
2529
|
-
if (idx !== -1) setCursor(idx);
|
|
3316
|
+
const index = Number(char) - 1;
|
|
3317
|
+
const provider = providers[index];
|
|
3318
|
+
if (provider?.ready) {
|
|
3319
|
+
onPick(`wise:${provider.id}`, entry.prompt);
|
|
2530
3320
|
}
|
|
2531
3321
|
});
|
|
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")));
|
|
3322
|
+
return /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", padding: 2 }, /* @__PURE__ */ React7.createElement(Text6, { bold: true, color: theme.primary }, "Run prompt #", entry.id, " with an analysis provider:"), /* @__PURE__ */ React7.createElement(Box5, { marginTop: 1, flexDirection: "column" }, providers.map((provider, index) => /* @__PURE__ */ React7.createElement(Text6, { key: provider.id, color: provider.ready ? "white" : theme.dim }, /* @__PURE__ */ React7.createElement(Text6, { color: provider.ready ? theme.warning : theme.dim }, provider.ready ? index + 1 : " "), /* @__PURE__ */ React7.createElement(Text6, null, ": ", formatProviderSummary(provider).trim())))), /* @__PURE__ */ React7.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "Pick a configured provider \xB7 q to cancel \xB7 r reruns with the original CLI tool")));
|
|
2539
3323
|
};
|
|
2540
3324
|
var SETTING_FIELDS = [
|
|
2541
3325
|
{ key: "backgroundAnalysis", label: "Auto-analysis on capture", type: "boolean" },
|
|
2542
3326
|
{ key: "ollamaUrl", label: "Ollama URL", type: "string" },
|
|
2543
3327
|
{ key: "ollamaModel", label: "Ollama model (analysis)", type: "string" },
|
|
2544
3328
|
{ key: "ollamaEmbedModel", label: "Ollama model (embeddings)", type: "string" },
|
|
2545
|
-
{ key: "analyzeProvider", label: "Analysis provider", type: "string" },
|
|
2546
3329
|
{ key: "filterMinLength", label: "Min prompt length filter", type: "number" },
|
|
2547
3330
|
{ key: "filterMinRelevance", label: "Min relevance filter", type: "number" }
|
|
2548
3331
|
];
|
|
2549
3332
|
var SettingsView = ({ onClose, theme }) => {
|
|
2550
|
-
const [cfg, setCfg] =
|
|
2551
|
-
const [cursor, setCursor] =
|
|
2552
|
-
const [dirty, setDirty] =
|
|
2553
|
-
const [saved, setSaved] =
|
|
3333
|
+
const [cfg, setCfg] = useState3(() => load());
|
|
3334
|
+
const [cursor, setCursor] = useState3(0);
|
|
3335
|
+
const [dirty, setDirty] = useState3(false);
|
|
3336
|
+
const [saved, setSaved] = useState3(false);
|
|
2554
3337
|
const toggleField = (key) => {
|
|
2555
3338
|
const updated = { ...cfg };
|
|
2556
3339
|
if (key === "backgroundAnalysis") {
|
|
@@ -2566,7 +3349,7 @@ var SettingsView = ({ onClose, theme }) => {
|
|
|
2566
3349
|
setSaved(true);
|
|
2567
3350
|
setTimeout(() => setSaved(false), 2e3);
|
|
2568
3351
|
};
|
|
2569
|
-
|
|
3352
|
+
useInput3((char, key) => {
|
|
2570
3353
|
if (key.escape) {
|
|
2571
3354
|
onClose();
|
|
2572
3355
|
return;
|
|
@@ -2589,7 +3372,7 @@ var SettingsView = ({ onClose, theme }) => {
|
|
|
2589
3372
|
}
|
|
2590
3373
|
}
|
|
2591
3374
|
});
|
|
2592
|
-
return /* @__PURE__ */
|
|
3375
|
+
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 }, /* @__PURE__ */ React7.createElement(Text6, { bold: true, color: theme.primary }, "Analysis providers (* = active; edit with `ph config set`)"), getProviderSummaries(cfg).map((provider) => /* @__PURE__ */ React7.createElement(Text6, { key: provider.id, color: provider.active ? theme.success : theme.dim }, formatProviderSummary(provider))), /* @__PURE__ */ React7.createElement(Text6, null, " "), SETTING_FIELDS.map((field, i) => {
|
|
2593
3376
|
const isCur = i === cursor;
|
|
2594
3377
|
const val = cfg[field.key];
|
|
2595
3378
|
let display;
|
|
@@ -2599,25 +3382,27 @@ var SettingsView = ({ onClose, theme }) => {
|
|
|
2599
3382
|
display = val !== void 0 ? String(val) : "(default)";
|
|
2600
3383
|
}
|
|
2601
3384
|
const valColor = field.type === "boolean" ? val ? theme.success : theme.dim : val !== void 0 ? "white" : theme.dim;
|
|
2602
|
-
return /* @__PURE__ */
|
|
2603
|
-
})), /* @__PURE__ */
|
|
3385
|
+
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]"));
|
|
3386
|
+
})), /* @__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
3387
|
};
|
|
2605
3388
|
var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
|
|
2606
3389
|
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 [
|
|
3390
|
+
const [allEntries, setAllEntries] = useState3(() => db.search({ limit: 1e3 }));
|
|
3391
|
+
const [refreshKey, setRefreshKey] = useState3(0);
|
|
3392
|
+
const [archiveCount] = useState3(() => db.getArchiveStats().total);
|
|
3393
|
+
const [textFilter, setTextFilter] = useState3(initialTextFilter ?? "");
|
|
3394
|
+
const [isTextFiltering, setTextFiltering] = useState3(false);
|
|
3395
|
+
const [activeFilters, setActiveFilters] = useState3(initialFilters ?? {});
|
|
3396
|
+
const [showFilterPanel, setFilterPanel] = useState3(false);
|
|
3397
|
+
const [showSettings, setShowSettings] = useState3(false);
|
|
3398
|
+
const [cursor, setCursor] = useState3(0);
|
|
3399
|
+
const [detail, setDetail] = useState3(null);
|
|
3400
|
+
const [editing, setEditing] = useState3(null);
|
|
3401
|
+
const [rerunning, setRerunning] = useState3(null);
|
|
3402
|
+
const [providerPicking, setProviderPicking] = useState3(null);
|
|
3403
|
+
const [copiedId, setCopiedId] = useState3(null);
|
|
3404
|
+
const [focusPane, setFocusPane] = useState3("list");
|
|
3405
|
+
const [scrollOffset, setScrollOffset] = useState3(0);
|
|
2621
3406
|
const currentThemeName = "dark";
|
|
2622
3407
|
const theme = THEMES3[currentThemeName] || THEMES3.dark;
|
|
2623
3408
|
const { columns: termWidth, rows: termHeight } = useStdoutDimensions();
|
|
@@ -2625,15 +3410,15 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
|
|
|
2625
3410
|
const isWide = termWidth >= SPLIT_THRESHOLD;
|
|
2626
3411
|
const leftPaneWidth = isWide ? Math.max(32, Math.min(50, Math.floor(termWidth * 0.33))) : termWidth;
|
|
2627
3412
|
const PAGE_SIZE = Math.max(1, Math.floor((termHeight - 3) / 4));
|
|
2628
|
-
const entries =
|
|
3413
|
+
const entries = useMemo4(
|
|
2629
3414
|
() => applyFilters(allEntries, activeFilters, textFilter),
|
|
2630
3415
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2631
3416
|
[allEntries, activeFilters, textFilter, refreshKey]
|
|
2632
3417
|
);
|
|
2633
|
-
|
|
3418
|
+
useEffect(() => {
|
|
2634
3419
|
setCursor((c) => Math.min(c, Math.max(0, entries.length - 1)));
|
|
2635
3420
|
}, [entries.length]);
|
|
2636
|
-
|
|
3421
|
+
useEffect(() => {
|
|
2637
3422
|
setScrollOffset((prev) => {
|
|
2638
3423
|
const clamped = Math.max(0, Math.min(prev, Math.max(0, entries.length - PAGE_SIZE)));
|
|
2639
3424
|
if (cursor < clamped) return cursor;
|
|
@@ -2645,7 +3430,7 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
|
|
|
2645
3430
|
const visible = entries.slice(pageStart, pageStart + PAGE_SIZE);
|
|
2646
3431
|
const triggerRefresh = useCallback(() => setRefreshKey((k) => k + 1), []);
|
|
2647
3432
|
const toggleStar = useCallback((entry) => {
|
|
2648
|
-
const meta =
|
|
3433
|
+
const meta = parseMetadata(entry.metadata);
|
|
2649
3434
|
meta.starred = !meta.starred;
|
|
2650
3435
|
const newMeta = JSON.stringify(meta);
|
|
2651
3436
|
db.updateMetadata(entry.id, newMeta);
|
|
@@ -2668,8 +3453,8 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
|
|
|
2668
3453
|
setCursor(0);
|
|
2669
3454
|
setScrollOffset(0);
|
|
2670
3455
|
}, []);
|
|
2671
|
-
|
|
2672
|
-
if (detail || editing || showFilterPanel || showSettings || rerunning) return;
|
|
3456
|
+
useInput3((char, key) => {
|
|
3457
|
+
if (detail || editing || showFilterPanel || showSettings || rerunning || providerPicking) return;
|
|
2673
3458
|
if (isTextFiltering) {
|
|
2674
3459
|
if (key.escape) {
|
|
2675
3460
|
setTextFiltering(false);
|
|
@@ -2700,6 +3485,8 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
|
|
|
2700
3485
|
if (entries[cursor]) setEditing(entries[cursor]);
|
|
2701
3486
|
} else if (char === "r") {
|
|
2702
3487
|
if (entries[cursor]) setRerunning(entries[cursor]);
|
|
3488
|
+
} else if (char === "p") {
|
|
3489
|
+
if (entries[cursor]) setProviderPicking(entries[cursor]);
|
|
2703
3490
|
} else if (char === "x") {
|
|
2704
3491
|
if (entries[cursor]) handleDelete(entries[cursor]);
|
|
2705
3492
|
} else if (char === "f") {
|
|
@@ -2714,7 +3501,7 @@ var BrowseApp = ({ db, initialTextFilter, initialFilters, onRerun }) => {
|
|
|
2714
3501
|
} else if (char === "C") {
|
|
2715
3502
|
const entry = entries[cursor];
|
|
2716
3503
|
if (!entry) return;
|
|
2717
|
-
const meta =
|
|
3504
|
+
const meta = parseMetadata(entry.metadata);
|
|
2718
3505
|
const project = meta.project;
|
|
2719
3506
|
if (!project) return;
|
|
2720
3507
|
const memories = db.searchMemories(project, 3);
|
|
@@ -2738,7 +3525,7 @@ ${mem.summary}`);
|
|
|
2738
3525
|
if (recent.length > 0) {
|
|
2739
3526
|
ctx.push("\n---\n## Recent Context");
|
|
2740
3527
|
for (const p of recent) {
|
|
2741
|
-
const pm =
|
|
3528
|
+
const pm = parseMetadata(p.metadata);
|
|
2742
3529
|
ctx.push(`
|
|
2743
3530
|
- #${p.id}: ${pm.summary || p.prompt.slice(0, 100).replace(/\n/g, " ")}`);
|
|
2744
3531
|
}
|
|
@@ -2784,7 +3571,7 @@ ${entry.prompt}`;
|
|
|
2784
3571
|
});
|
|
2785
3572
|
let mainContent;
|
|
2786
3573
|
if (showSettings) {
|
|
2787
|
-
mainContent = /* @__PURE__ */
|
|
3574
|
+
mainContent = /* @__PURE__ */ React7.createElement(
|
|
2788
3575
|
SettingsView,
|
|
2789
3576
|
{
|
|
2790
3577
|
onClose: () => setShowSettings(false),
|
|
@@ -2792,7 +3579,7 @@ ${entry.prompt}`;
|
|
|
2792
3579
|
}
|
|
2793
3580
|
);
|
|
2794
3581
|
} else if (showFilterPanel) {
|
|
2795
|
-
mainContent = /* @__PURE__ */
|
|
3582
|
+
mainContent = /* @__PURE__ */ React7.createElement(
|
|
2796
3583
|
FilterPanel,
|
|
2797
3584
|
{
|
|
2798
3585
|
allEntries,
|
|
@@ -2802,8 +3589,22 @@ ${entry.prompt}`;
|
|
|
2802
3589
|
theme
|
|
2803
3590
|
}
|
|
2804
3591
|
);
|
|
3592
|
+
} else if (providerPicking) {
|
|
3593
|
+
mainContent = /* @__PURE__ */ React7.createElement(
|
|
3594
|
+
ProviderPicker,
|
|
3595
|
+
{
|
|
3596
|
+
entry: providerPicking,
|
|
3597
|
+
onPick: (tool, prompt) => {
|
|
3598
|
+
onRerun?.(tool, prompt);
|
|
3599
|
+
setProviderPicking(null);
|
|
3600
|
+
exit();
|
|
3601
|
+
},
|
|
3602
|
+
onClose: () => setProviderPicking(null),
|
|
3603
|
+
theme
|
|
3604
|
+
}
|
|
3605
|
+
);
|
|
2805
3606
|
} else if (rerunning) {
|
|
2806
|
-
mainContent = /* @__PURE__ */
|
|
3607
|
+
mainContent = /* @__PURE__ */ React7.createElement(
|
|
2807
3608
|
RerunView,
|
|
2808
3609
|
{
|
|
2809
3610
|
entry: rerunning,
|
|
@@ -2817,7 +3618,7 @@ ${entry.prompt}`;
|
|
|
2817
3618
|
}
|
|
2818
3619
|
);
|
|
2819
3620
|
} else if (editing) {
|
|
2820
|
-
mainContent = /* @__PURE__ */
|
|
3621
|
+
mainContent = /* @__PURE__ */ React7.createElement(
|
|
2821
3622
|
EditView,
|
|
2822
3623
|
{
|
|
2823
3624
|
entry: editing,
|
|
@@ -2827,7 +3628,7 @@ ${entry.prompt}`;
|
|
|
2827
3628
|
}
|
|
2828
3629
|
);
|
|
2829
3630
|
} else if (detail && !isWide) {
|
|
2830
|
-
mainContent = /* @__PURE__ */
|
|
3631
|
+
mainContent = /* @__PURE__ */ React7.createElement(
|
|
2831
3632
|
DetailView,
|
|
2832
3633
|
{
|
|
2833
3634
|
entry: detail,
|
|
@@ -2844,10 +3645,10 @@ ${entry.prompt}`;
|
|
|
2844
3645
|
} else if (isWide) {
|
|
2845
3646
|
const listFocused = focusPane === "list";
|
|
2846
3647
|
const previewFocused = focusPane === "preview";
|
|
2847
|
-
mainContent = /* @__PURE__ */
|
|
3648
|
+
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
3649
|
const absIdx = pageStart + i;
|
|
2849
3650
|
const isNewSession = needsSessionSeparator(entries, absIdx);
|
|
2850
|
-
return /* @__PURE__ */
|
|
3651
|
+
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
3652
|
ListEntry,
|
|
2852
3653
|
{
|
|
2853
3654
|
entry,
|
|
@@ -2855,9 +3656,9 @@ ${entry.prompt}`;
|
|
|
2855
3656
|
paneWidth: leftPaneWidth,
|
|
2856
3657
|
theme
|
|
2857
3658
|
}
|
|
2858
|
-
), i < visible.length - 1 && !needsSessionSeparator(entries, absIdx + 1) && /* @__PURE__ */
|
|
2859
|
-
})), /* @__PURE__ */
|
|
2860
|
-
|
|
3659
|
+
), 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)))));
|
|
3660
|
+
})), /* @__PURE__ */ React7.createElement(
|
|
3661
|
+
Box5,
|
|
2861
3662
|
{
|
|
2862
3663
|
borderLeft: true,
|
|
2863
3664
|
borderStyle: "single",
|
|
@@ -2865,9 +3666,10 @@ ${entry.prompt}`;
|
|
|
2865
3666
|
flexGrow: 1,
|
|
2866
3667
|
flexDirection: "column"
|
|
2867
3668
|
},
|
|
2868
|
-
/* @__PURE__ */
|
|
3669
|
+
/* @__PURE__ */ React7.createElement(
|
|
2869
3670
|
PreviewPane,
|
|
2870
3671
|
{
|
|
3672
|
+
key: entries[cursor]?.id ?? "empty",
|
|
2871
3673
|
entry: entries[cursor] ?? null,
|
|
2872
3674
|
paneWidth: termWidth - leftPaneWidth - 1,
|
|
2873
3675
|
paneHeight: termHeight - 3,
|
|
@@ -2878,10 +3680,10 @@ ${entry.prompt}`;
|
|
|
2878
3680
|
)
|
|
2879
3681
|
));
|
|
2880
3682
|
} else {
|
|
2881
|
-
mainContent = /* @__PURE__ */
|
|
3683
|
+
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
3684
|
const absIdx = pageStart + i;
|
|
2883
3685
|
const isNewSession = needsSessionSeparator(entries, absIdx);
|
|
2884
|
-
return /* @__PURE__ */
|
|
3686
|
+
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
3687
|
ListEntry,
|
|
2886
3688
|
{
|
|
2887
3689
|
entry,
|
|
@@ -2889,20 +3691,21 @@ ${entry.prompt}`;
|
|
|
2889
3691
|
paneWidth: termWidth,
|
|
2890
3692
|
theme
|
|
2891
3693
|
}
|
|
2892
|
-
), i < visible.length - 1 && !needsSessionSeparator(entries, absIdx + 1) && /* @__PURE__ */
|
|
3694
|
+
), 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
3695
|
}));
|
|
2894
3696
|
}
|
|
2895
|
-
return /* @__PURE__ */
|
|
3697
|
+
return /* @__PURE__ */ React7.createElement(Box5, { flexDirection: "column", height: termHeight }, /* @__PURE__ */ React7.createElement(
|
|
2896
3698
|
Header,
|
|
2897
3699
|
{
|
|
2898
3700
|
entriesCount: entries.length,
|
|
2899
3701
|
allEntriesCount: allEntries.length,
|
|
3702
|
+
archiveCount,
|
|
2900
3703
|
activeFilters,
|
|
2901
3704
|
textFilter,
|
|
2902
3705
|
isTextFiltering: false,
|
|
2903
3706
|
theme: currentThemeName
|
|
2904
3707
|
}
|
|
2905
|
-
), /* @__PURE__ */
|
|
3708
|
+
), /* @__PURE__ */ React7.createElement(SearchBar, { value: textFilter, isActive: isTextFiltering, theme }), /* @__PURE__ */ React7.createElement(Box5, { flexGrow: 1 }, mainContent), /* @__PURE__ */ React7.createElement(
|
|
2906
3709
|
Footer,
|
|
2907
3710
|
{
|
|
2908
3711
|
cursor,
|
|
@@ -3028,6 +3831,32 @@ function parseDate(s, label) {
|
|
|
3028
3831
|
async function cmdSearch(db, cfg, args) {
|
|
3029
3832
|
const { flags, positional } = parseFlags(args);
|
|
3030
3833
|
const query = positional.join(" ");
|
|
3834
|
+
if (flags["archive"]) {
|
|
3835
|
+
const archiveResults = db.searchArchive({
|
|
3836
|
+
since: flags["since"] ? new Date(flags["since"]).toISOString() : void 0,
|
|
3837
|
+
until: flags["until"] ? new Date(flags["until"]).toISOString() : void 0,
|
|
3838
|
+
limit: Number(flags["limit"] ?? 50)
|
|
3839
|
+
});
|
|
3840
|
+
if (archiveResults.length === 0) {
|
|
3841
|
+
console.log("No archived prompts found.");
|
|
3842
|
+
return;
|
|
3843
|
+
}
|
|
3844
|
+
console.log(`Found ${archiveResults.length} archived prompts:
|
|
3845
|
+
`);
|
|
3846
|
+
for (const r of archiveResults) {
|
|
3847
|
+
const date = new Date(r.timestamp).toLocaleString();
|
|
3848
|
+
const id = r.original_id ?? r.id;
|
|
3849
|
+
const prompt = r.prompt.replace(/\n/g, " ").slice(0, 120);
|
|
3850
|
+
console.log(` #${id} [${r.tool}] (${date})`);
|
|
3851
|
+
console.log(` ${prompt}${r.prompt.length > 120 ? "..." : ""}`);
|
|
3852
|
+
if (r.response) {
|
|
3853
|
+
const resp = r.response.replace(/\n/g, " ").slice(0, 80);
|
|
3854
|
+
console.log(` Response: ${resp}${r.response.length > 80 ? "..." : ""}`);
|
|
3855
|
+
}
|
|
3856
|
+
console.log();
|
|
3857
|
+
}
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3031
3860
|
if (flags["semantic"] && query) {
|
|
3032
3861
|
await cmdSearchSemantic(db, cfg, query, Number(flags["limit"] ?? 50), Boolean(flags["full"]));
|
|
3033
3862
|
return;
|
|
@@ -3207,8 +4036,8 @@ ${meta.git_context.diff}
|
|
|
3207
4036
|
}
|
|
3208
4037
|
|
|
3209
4038
|
// src/commands/import.ts
|
|
3210
|
-
import
|
|
3211
|
-
import
|
|
4039
|
+
import path8 from "path";
|
|
4040
|
+
import os8 from "os";
|
|
3212
4041
|
|
|
3213
4042
|
// src/importer/gemini.ts
|
|
3214
4043
|
import fs5 from "fs";
|
|
@@ -3216,6 +4045,7 @@ import path4 from "path";
|
|
|
3216
4045
|
import os4 from "os";
|
|
3217
4046
|
|
|
3218
4047
|
// src/analyzer/index.ts
|
|
4048
|
+
import { createHash as createHash3 } from "crypto";
|
|
3219
4049
|
var ANALYSIS_PROMPT = (text) => `Analyze this AI tool prompt and response and return ONLY a JSON object with these exact fields:
|
|
3220
4050
|
{
|
|
3221
4051
|
"project": "<project name if clearly identifiable, else empty string>",
|
|
@@ -3325,7 +4155,7 @@ function mergeMetadata(existing, result, force) {
|
|
|
3325
4155
|
if (!merged.tags?.length) delete merged.tags;
|
|
3326
4156
|
return merged;
|
|
3327
4157
|
}
|
|
3328
|
-
function
|
|
4158
|
+
function parseMeta2(raw) {
|
|
3329
4159
|
try {
|
|
3330
4160
|
return JSON.parse(raw);
|
|
3331
4161
|
} catch {
|
|
@@ -3333,20 +4163,28 @@ function parseMeta3(raw) {
|
|
|
3333
4163
|
}
|
|
3334
4164
|
}
|
|
3335
4165
|
function hasMetadata(entry) {
|
|
3336
|
-
const meta =
|
|
4166
|
+
const meta = parseMeta2(entry.metadata);
|
|
3337
4167
|
return Boolean(meta.role || meta.tags?.length || meta.project || meta.language);
|
|
3338
4168
|
}
|
|
4169
|
+
function gitFingerprint(ctx) {
|
|
4170
|
+
const hash = createHash3("sha256");
|
|
4171
|
+
hash.update(ctx.branch);
|
|
4172
|
+
hash.update(ctx.diff);
|
|
4173
|
+
return `git:${hash.digest("hex").slice(0, 14)}`;
|
|
4174
|
+
}
|
|
3339
4175
|
async function analyzeAll(entries, provider, db, opts = {}) {
|
|
3340
4176
|
const { force = false, pruneBelow = 0, dryRun = false, onProgress, onPrune } = opts;
|
|
3341
4177
|
const toProcess = force ? entries : entries.filter((e) => !hasMetadata(e));
|
|
3342
4178
|
const stats = { updated: 0, skipped: entries.length - toProcess.length, failed: 0, pruned: 0 };
|
|
4179
|
+
const gitCtx = captureGitContext(process.cwd());
|
|
4180
|
+
const gitFp = gitCtx ? gitFingerprint(gitCtx) : void 0;
|
|
3343
4181
|
for (let i = 0; i < toProcess.length; i++) {
|
|
3344
4182
|
const entry = toProcess[i];
|
|
3345
4183
|
let result = null;
|
|
3346
4184
|
let errMsg;
|
|
3347
4185
|
try {
|
|
3348
4186
|
result = await analyzePrompt(entry.prompt, provider);
|
|
3349
|
-
const existing =
|
|
4187
|
+
const existing = parseMeta2(entry.metadata);
|
|
3350
4188
|
const merged = mergeMetadata(existing, result, force);
|
|
3351
4189
|
if (pruneBelow > 0 && result.relevance !== void 0 && result.relevance < pruneBelow) {
|
|
3352
4190
|
onPrune?.(entry, result.relevance);
|
|
@@ -3357,11 +4195,25 @@ async function analyzeAll(entries, provider, db, opts = {}) {
|
|
|
3357
4195
|
} else {
|
|
3358
4196
|
db.updateMetadata(entry.id, JSON.stringify(merged));
|
|
3359
4197
|
if (result.project && result.summary) {
|
|
4198
|
+
if (gitFp) {
|
|
4199
|
+
const lastMem = db.getLastMemoryForProject(result.project);
|
|
4200
|
+
if (lastMem?.git_context_snapshot === gitFp) {
|
|
4201
|
+
stats.updated++;
|
|
4202
|
+
continue;
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
3360
4205
|
db.upsertProjectMemory({
|
|
3361
4206
|
project: result.project,
|
|
3362
4207
|
prompt_id: entry.id,
|
|
3363
4208
|
summary: result.summary,
|
|
3364
4209
|
key_insights: result.key_insights ?? [],
|
|
4210
|
+
technical_decisions: result.technical_decisions ?? [],
|
|
4211
|
+
git_context_snapshot: gitFp
|
|
4212
|
+
});
|
|
4213
|
+
db.upsertProjectSummary({
|
|
4214
|
+
project: result.project,
|
|
4215
|
+
summary: result.summary,
|
|
4216
|
+
key_insights: result.key_insights ?? [],
|
|
3365
4217
|
technical_decisions: result.technical_decisions ?? []
|
|
3366
4218
|
});
|
|
3367
4219
|
}
|
|
@@ -3501,7 +4353,7 @@ async function importGeminiHistory(db, geminiDir, dryRun, analyzer, onProgress,
|
|
|
3501
4353
|
if (response.length > 8e3) response = response.slice(0, 8e3) + "\n... (truncated)";
|
|
3502
4354
|
}
|
|
3503
4355
|
try {
|
|
3504
|
-
const id = db.insert({
|
|
4356
|
+
const id = db.insert(createCaptureRecord({
|
|
3505
4357
|
timestamp: new Date(msg.timestamp).toISOString(),
|
|
3506
4358
|
tool: "gemini",
|
|
3507
4359
|
prompt: content,
|
|
@@ -3510,8 +4362,8 @@ async function importGeminiHistory(db, geminiDir, dryRun, analyzer, onProgress,
|
|
|
3510
4362
|
workdir: inferWorkDir(filePath, geminiDir),
|
|
3511
4363
|
hostname,
|
|
3512
4364
|
exit_code: 0,
|
|
3513
|
-
metadata
|
|
3514
|
-
});
|
|
4365
|
+
metadata: JSON.parse(metadata)
|
|
4366
|
+
}));
|
|
3515
4367
|
filter?.registerHash(content, id);
|
|
3516
4368
|
result.promptsImported++;
|
|
3517
4369
|
} catch (e) {
|
|
@@ -3669,7 +4521,7 @@ async function importClaudeHistory(db, claudeDir, dryRun, analyzer, onProgress,
|
|
|
3669
4521
|
} catch {
|
|
3670
4522
|
}
|
|
3671
4523
|
}
|
|
3672
|
-
const id = db.insert({
|
|
4524
|
+
const id = db.insert(createCaptureRecord({
|
|
3673
4525
|
timestamp: entry.timestamp,
|
|
3674
4526
|
tool: "claude",
|
|
3675
4527
|
prompt: entry.prompt,
|
|
@@ -3678,8 +4530,8 @@ async function importClaudeHistory(db, claudeDir, dryRun, analyzer, onProgress,
|
|
|
3678
4530
|
workdir: entry.workdir,
|
|
3679
4531
|
hostname,
|
|
3680
4532
|
exit_code: 0,
|
|
3681
|
-
metadata
|
|
3682
|
-
});
|
|
4533
|
+
metadata: JSON.parse(metadata)
|
|
4534
|
+
}));
|
|
3683
4535
|
filter?.registerHash(entry.prompt, id);
|
|
3684
4536
|
result.promptsImported++;
|
|
3685
4537
|
} catch (e) {
|
|
@@ -3852,7 +4704,7 @@ async function importOpenCodeHistory(db, opencodeDir, dryRun, analyzer, onProgre
|
|
|
3852
4704
|
metadata.tokens = entry.tokens;
|
|
3853
4705
|
}
|
|
3854
4706
|
}
|
|
3855
|
-
const id = db.insert({
|
|
4707
|
+
const id = db.insert(createCaptureRecord({
|
|
3856
4708
|
timestamp: entry.timestamp,
|
|
3857
4709
|
tool: "opencode",
|
|
3858
4710
|
prompt: entry.prompt,
|
|
@@ -3861,8 +4713,8 @@ async function importOpenCodeHistory(db, opencodeDir, dryRun, analyzer, onProgre
|
|
|
3861
4713
|
workdir: entry.workdir,
|
|
3862
4714
|
hostname,
|
|
3863
4715
|
exit_code: 0,
|
|
3864
|
-
metadata
|
|
3865
|
-
});
|
|
4716
|
+
metadata
|
|
4717
|
+
}));
|
|
3866
4718
|
filter?.registerHash(entry.prompt, id);
|
|
3867
4719
|
result.promptsImported++;
|
|
3868
4720
|
} catch (e) {
|
|
@@ -3873,90 +4725,16 @@ async function importOpenCodeHistory(db, opencodeDir, dryRun, analyzer, onProgre
|
|
|
3873
4725
|
return result;
|
|
3874
4726
|
}
|
|
3875
4727
|
|
|
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
|
-
}
|
|
4728
|
+
// src/importer/codex.ts
|
|
4729
|
+
import fs8 from "fs";
|
|
4730
|
+
import path7 from "path";
|
|
4731
|
+
import os7 from "os";
|
|
3954
4732
|
|
|
3955
4733
|
// src/filter/index.ts
|
|
3956
4734
|
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;
|
|
4735
|
+
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
4736
|
var ORPHAN_CODE_PATTERN = /^([{}()[\]<>|&!=\-+*/%^~#@\\:;.,\s]+|\.\.\.|console\.log.*|print.*)$/i;
|
|
3959
|
-
var NON_PRINTABLE_PATTERN = /^[\x00-\x1f\x7f\x1b[\]()#;\d;A-Za-z]*$/;
|
|
4737
|
+
var NON_PRINTABLE_PATTERN = /^(?=.*[\x00-\x1f\x7f])[\x00-\x1f\x7f\x1b[\]()#;\d;A-Za-z]*$/;
|
|
3960
4738
|
var FilterPipeline = class _FilterPipeline {
|
|
3961
4739
|
minLength;
|
|
3962
4740
|
minRelevance;
|
|
@@ -4029,11 +4807,287 @@ var FilterPipeline = class _FilterPipeline {
|
|
|
4029
4807
|
}
|
|
4030
4808
|
};
|
|
4031
4809
|
|
|
4810
|
+
// src/importer/codex.ts
|
|
4811
|
+
function textFromContent(content) {
|
|
4812
|
+
if (typeof content === "string") return content;
|
|
4813
|
+
if (!Array.isArray(content)) return "";
|
|
4814
|
+
return content.flatMap((part) => {
|
|
4815
|
+
if (!part || typeof part !== "object") return [];
|
|
4816
|
+
const item = part;
|
|
4817
|
+
return typeof item.text === "string" && ["output_text", "input_text", "text"].includes(String(item.type)) ? [item.text] : [];
|
|
4818
|
+
}).join("");
|
|
4819
|
+
}
|
|
4820
|
+
function rolloutFiles(sessionsDir) {
|
|
4821
|
+
if (!fs8.existsSync(sessionsDir)) return [];
|
|
4822
|
+
const files = [];
|
|
4823
|
+
const visit = (dir) => {
|
|
4824
|
+
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
4825
|
+
const full = path7.join(dir, entry.name);
|
|
4826
|
+
if (entry.isDirectory()) visit(full);
|
|
4827
|
+
else if (entry.isFile() && /^rollout-.*\.jsonl$/.test(entry.name)) files.push(full);
|
|
4828
|
+
}
|
|
4829
|
+
};
|
|
4830
|
+
visit(sessionsDir);
|
|
4831
|
+
return files;
|
|
4832
|
+
}
|
|
4833
|
+
async function importCodexHistory(db, codexDir, dryRun, analyzer, onProgress, filter, transcriptFile) {
|
|
4834
|
+
const result = { filesScanned: 0, promptsFound: 0, promptsImported: 0, skipped: 0, filtered: 0, errors: [] };
|
|
4835
|
+
const dedupFilter = filter ?? new FilterPipeline({ minLength: 0, minRelevance: 0, existingHashes: db.getAllPromptHashes() });
|
|
4836
|
+
const files = transcriptFile ? [transcriptFile] : rolloutFiles(path7.join(codexDir, "sessions"));
|
|
4837
|
+
result.filesScanned = files.length;
|
|
4838
|
+
const hostname = os7.hostname();
|
|
4839
|
+
const entries = [];
|
|
4840
|
+
for (const file of files) {
|
|
4841
|
+
let lines;
|
|
4842
|
+
try {
|
|
4843
|
+
lines = fs8.readFileSync(file, "utf8").split("\n");
|
|
4844
|
+
} catch (error) {
|
|
4845
|
+
result.errors.push(`read ${file}: ${error.message}`);
|
|
4846
|
+
continue;
|
|
4847
|
+
}
|
|
4848
|
+
let workdir = "";
|
|
4849
|
+
let sessionId = path7.basename(file, ".jsonl").replace(/^rollout-.*?-/, "");
|
|
4850
|
+
const events = [];
|
|
4851
|
+
for (const line of lines) {
|
|
4852
|
+
if (!line.trim()) continue;
|
|
4853
|
+
try {
|
|
4854
|
+
const event = JSON.parse(line);
|
|
4855
|
+
events.push(event);
|
|
4856
|
+
if (event.type === "session_meta" && event.payload) {
|
|
4857
|
+
if (typeof event.payload.cwd === "string") workdir = event.payload.cwd;
|
|
4858
|
+
if (typeof event.payload.id === "string") sessionId = event.payload.id;
|
|
4859
|
+
}
|
|
4860
|
+
} catch {
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
for (let i = 0; i < events.length; i++) {
|
|
4864
|
+
const event = events[i];
|
|
4865
|
+
const payload = event.payload ?? {};
|
|
4866
|
+
const isUserMessage = event.type === "event_msg" && payload.type === "user_message" || event.type === "response_item" && payload.type === "message" && payload.role === "user";
|
|
4867
|
+
const prompt = !isUserMessage ? "" : typeof payload.message === "string" ? payload.message.trim() : textFromContent(payload.content).trim();
|
|
4868
|
+
if (!prompt) continue;
|
|
4869
|
+
const agentMessages = [];
|
|
4870
|
+
const assistantItems = [];
|
|
4871
|
+
const finalAssistantItems = [];
|
|
4872
|
+
for (let j = i + 1; j < events.length; j++) {
|
|
4873
|
+
const candidate = events[j].payload ?? {};
|
|
4874
|
+
const nextIsUserMessage = events[j].type === "event_msg" && candidate.type === "user_message" || events[j].type === "response_item" && candidate.type === "message" && candidate.role === "user";
|
|
4875
|
+
if (nextIsUserMessage) break;
|
|
4876
|
+
if (events[j].type === "event_msg" && candidate.type === "agent_message" && typeof candidate.message === "string") agentMessages.push(candidate.message);
|
|
4877
|
+
if (events[j].type === "response_item" && candidate.type === "message" && candidate.role === "assistant") {
|
|
4878
|
+
const text = textFromContent(candidate.content);
|
|
4879
|
+
assistantItems.push(text);
|
|
4880
|
+
if (candidate.phase === "final_answer") finalAssistantItems.push(text);
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
const response = agentMessages.length > 0 ? agentMessages.join("") : finalAssistantItems.length > 0 ? finalAssistantItems.join("") : assistantItems.join("");
|
|
4884
|
+
entries.push({ prompt, response: response.slice(0, 8e3), timestamp: event.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(), workdir, sessionId });
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
for (let i = 0; i < entries.length; i++) {
|
|
4888
|
+
const entry = entries[i];
|
|
4889
|
+
result.promptsFound++;
|
|
4890
|
+
onProgress?.(i + 1, result.promptsImported, entries.length, entry.prompt.slice(0, 60));
|
|
4891
|
+
if (dryRun) {
|
|
4892
|
+
result.promptsImported++;
|
|
4893
|
+
continue;
|
|
4894
|
+
}
|
|
4895
|
+
const rules = dedupFilter.checkRules(entry.prompt);
|
|
4896
|
+
if (!rules.keep || !dedupFilter.checkDuplicate(entry.prompt).keep) {
|
|
4897
|
+
result.filtered++;
|
|
4898
|
+
continue;
|
|
4899
|
+
}
|
|
4900
|
+
try {
|
|
4901
|
+
let metadata = {};
|
|
4902
|
+
if (analyzer) {
|
|
4903
|
+
try {
|
|
4904
|
+
metadata = mergeMetadata({}, await analyzePrompt(entry.prompt, analyzer), false);
|
|
4905
|
+
} catch {
|
|
4906
|
+
}
|
|
4907
|
+
}
|
|
4908
|
+
const id = db.insert(createCaptureRecord({
|
|
4909
|
+
timestamp: entry.timestamp,
|
|
4910
|
+
tool: "codex",
|
|
4911
|
+
prompt: entry.prompt,
|
|
4912
|
+
response: entry.response,
|
|
4913
|
+
args: entry.prompt,
|
|
4914
|
+
workdir: entry.workdir,
|
|
4915
|
+
hostname,
|
|
4916
|
+
exit_code: 0,
|
|
4917
|
+
metadata: { ...metadata }
|
|
4918
|
+
}));
|
|
4919
|
+
dedupFilter.registerHash(entry.prompt, id);
|
|
4920
|
+
result.promptsImported++;
|
|
4921
|
+
} catch (error) {
|
|
4922
|
+
result.errors.push(`insert session ${entry.sessionId}: ${error.message}`);
|
|
4923
|
+
result.skipped++;
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
return result;
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
// src/ai/ollama.ts
|
|
4930
|
+
var OllamaProvider = class {
|
|
4931
|
+
constructor(url, model) {
|
|
4932
|
+
this.url = url;
|
|
4933
|
+
this.model = model;
|
|
4934
|
+
}
|
|
4935
|
+
name = "ollama";
|
|
4936
|
+
async generate(prompt) {
|
|
4937
|
+
const controller = new AbortController();
|
|
4938
|
+
const timer = setTimeout(() => controller.abort(), 6e4);
|
|
4939
|
+
let res;
|
|
4940
|
+
try {
|
|
4941
|
+
res = await fetch(`${this.url}/api/generate`, {
|
|
4942
|
+
method: "POST",
|
|
4943
|
+
headers: { "Content-Type": "application/json" },
|
|
4944
|
+
body: JSON.stringify({ model: this.model, prompt, stream: false }),
|
|
4945
|
+
signal: controller.signal
|
|
4946
|
+
});
|
|
4947
|
+
} catch (err) {
|
|
4948
|
+
clearTimeout(timer);
|
|
4949
|
+
const msg = err.message ?? String(err);
|
|
4950
|
+
if (msg.includes("abort") || msg.includes("timeout")) {
|
|
4951
|
+
throw new Error(`Ollama request timed out (60s). Is Ollama running at ${this.url}?`, { cause: err });
|
|
4952
|
+
}
|
|
4953
|
+
throw new Error(`Ollama unreachable at ${this.url}: ${msg}`, { cause: err });
|
|
4954
|
+
} finally {
|
|
4955
|
+
clearTimeout(timer);
|
|
4956
|
+
}
|
|
4957
|
+
if (!res.ok) {
|
|
4958
|
+
const body = await res.text().catch(() => "");
|
|
4959
|
+
throw new Error(`Ollama error ${res.status}: ${body}`);
|
|
4960
|
+
}
|
|
4961
|
+
const data = await res.json();
|
|
4962
|
+
return data.response ?? "";
|
|
4963
|
+
}
|
|
4964
|
+
};
|
|
4965
|
+
|
|
4966
|
+
// src/ai/gemini.ts
|
|
4967
|
+
var GENERATE_API = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
4968
|
+
async function generateContent(prompt, model, apiKey) {
|
|
4969
|
+
const res = await fetch(`${GENERATE_API}/${model}:generateContent?key=${apiKey}`, {
|
|
4970
|
+
method: "POST",
|
|
4971
|
+
headers: { "Content-Type": "application/json" },
|
|
4972
|
+
body: JSON.stringify({
|
|
4973
|
+
contents: [{ parts: [{ text: prompt }] }]
|
|
4974
|
+
})
|
|
4975
|
+
});
|
|
4976
|
+
if (!res.ok) {
|
|
4977
|
+
const err = await res.text();
|
|
4978
|
+
throw new Error(`Gemini API error ${res.status}: ${err}`);
|
|
4979
|
+
}
|
|
4980
|
+
const data = await res.json();
|
|
4981
|
+
return data.candidates[0]?.content?.parts[0]?.text ?? "";
|
|
4982
|
+
}
|
|
4983
|
+
var GeminiProvider = class {
|
|
4984
|
+
constructor(model, apiKey) {
|
|
4985
|
+
this.model = model;
|
|
4986
|
+
this.apiKey = apiKey;
|
|
4987
|
+
}
|
|
4988
|
+
name = "gemini";
|
|
4989
|
+
generate(prompt) {
|
|
4990
|
+
return generateContent(prompt, this.model, this.apiKey);
|
|
4991
|
+
}
|
|
4992
|
+
};
|
|
4993
|
+
|
|
4994
|
+
// src/ai/wise.ts
|
|
4995
|
+
import { generateText } from "ai";
|
|
4996
|
+
import { createAIModel } from "@gi4nks/wise";
|
|
4997
|
+
var OPENROUTER_DEFAULT_URL = "https://openrouter.ai/api/v1";
|
|
4998
|
+
var MLX_SERVE_DEFAULT_URL = "http://127.0.0.1:11234/v1";
|
|
4999
|
+
var WiseLLMProvider = class {
|
|
5000
|
+
name;
|
|
5001
|
+
model;
|
|
5002
|
+
constructor(wiseProvider, displayName, modelId, config) {
|
|
5003
|
+
this.name = `${displayName} (${modelId})`;
|
|
5004
|
+
this.model = createAIModel(wiseProvider, modelId, config);
|
|
5005
|
+
}
|
|
5006
|
+
async generate(prompt) {
|
|
5007
|
+
const result = await generateText({
|
|
5008
|
+
model: this.model,
|
|
5009
|
+
prompt,
|
|
5010
|
+
// OpenRouter requests can incur cost. Avoid hidden automatic replays.
|
|
5011
|
+
maxRetries: 0
|
|
5012
|
+
});
|
|
5013
|
+
return result.text;
|
|
5014
|
+
}
|
|
5015
|
+
};
|
|
5016
|
+
async function discoverModels(provider, apiKey, baseUrl) {
|
|
5017
|
+
if (provider === "mlx-serve") {
|
|
5018
|
+
const url2 = (baseUrl || MLX_SERVE_DEFAULT_URL).replace(/\/+$/, "");
|
|
5019
|
+
const response2 = await fetch(`${url2}/models`, {
|
|
5020
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
5021
|
+
signal: AbortSignal.timeout(15e3)
|
|
5022
|
+
});
|
|
5023
|
+
if (!response2.ok) throw new Error(`HTTP ${response2.status}`);
|
|
5024
|
+
const data2 = await response2.json();
|
|
5025
|
+
return (data2.data ?? []).filter((model) => typeof model.id === "string" && model.id.length > 0).map((model) => ({ id: model.id, name: model.name || model.id, loaded: model.loaded, state: model.state }));
|
|
5026
|
+
}
|
|
5027
|
+
const url = (baseUrl || OPENROUTER_DEFAULT_URL).replace(/\/+$/, "");
|
|
5028
|
+
const response = await fetch(`${url}/models`, {
|
|
5029
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
5030
|
+
signal: AbortSignal.timeout(15e3)
|
|
5031
|
+
});
|
|
5032
|
+
if (!response.ok) {
|
|
5033
|
+
throw new Error(`HTTP ${response.status}`);
|
|
5034
|
+
}
|
|
5035
|
+
const data = await response.json();
|
|
5036
|
+
return (data.data ?? []).filter((model) => typeof model.id === "string" && model.id.length > 0).map((model) => ({ id: model.id, name: model.name || model.id }));
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
// src/ai/provider.ts
|
|
5040
|
+
function getProviderSetupError(cfg, selectedProvider) {
|
|
5041
|
+
const provider = selectedProvider ?? cfg.analyzeProvider ?? "ollama";
|
|
5042
|
+
if (provider === "openrouter") {
|
|
5043
|
+
if (!(process.env.OPENROUTER_API_KEY || cfg.openrouterApiKey)) return "Set a key with `ph config set openrouter-api-key` or OPENROUTER_API_KEY.";
|
|
5044
|
+
if (!(process.env.OPENROUTER_MODEL || cfg.openrouterModel)) return "Choose a model with `ph config set openrouter-model <provider/model>`.";
|
|
5045
|
+
}
|
|
5046
|
+
if (provider === "mlx-serve" && !(process.env.MLX_SERVE_MODEL || cfg.mlxServeModel)) {
|
|
5047
|
+
return "Choose an MLX-Serve model with `ph config set mlx-serve-model <model-id>`.";
|
|
5048
|
+
}
|
|
5049
|
+
return null;
|
|
5050
|
+
}
|
|
5051
|
+
function getProvider(cfg, selectedProvider) {
|
|
5052
|
+
const provider = selectedProvider ?? cfg.analyzeProvider ?? "ollama";
|
|
5053
|
+
if (provider === "openrouter") {
|
|
5054
|
+
const apiKey = process.env.OPENROUTER_API_KEY ?? cfg.openrouterApiKey;
|
|
5055
|
+
const model = process.env.OPENROUTER_MODEL ?? cfg.openrouterModel;
|
|
5056
|
+
if (!apiKey || !model) return null;
|
|
5057
|
+
return new WiseLLMProvider("openai", "OpenRouter", model, {
|
|
5058
|
+
openai: {
|
|
5059
|
+
apiKey,
|
|
5060
|
+
baseUrl: process.env.OPENROUTER_BASE_URL ?? cfg.openrouterUrl ?? OPENROUTER_DEFAULT_URL
|
|
5061
|
+
}
|
|
5062
|
+
});
|
|
5063
|
+
}
|
|
5064
|
+
if (provider === "mlx-serve") {
|
|
5065
|
+
const model = process.env.MLX_SERVE_MODEL ?? cfg.mlxServeModel;
|
|
5066
|
+
if (!model) return null;
|
|
5067
|
+
const apiKey = process.env.MLX_SERVE_API_KEY ?? process.env.GLB_OMLX_API_KEY ?? cfg.mlxServeApiKey ?? "local";
|
|
5068
|
+
return new WiseLLMProvider("omlx", "MLX-Serve", model, {
|
|
5069
|
+
omlx: {
|
|
5070
|
+
apiKey,
|
|
5071
|
+
baseUrl: process.env.MLX_SERVE_BASE_URL ?? cfg.mlxServeUrl ?? MLX_SERVE_DEFAULT_URL
|
|
5072
|
+
}
|
|
5073
|
+
});
|
|
5074
|
+
}
|
|
5075
|
+
if (provider === "gemini") {
|
|
5076
|
+
const apiKey = process.env.GEMINI_API_KEY ?? cfg.geminiApiKey;
|
|
5077
|
+
if (!apiKey) return null;
|
|
5078
|
+
return new GeminiProvider("gemini-2.5-flash", apiKey);
|
|
5079
|
+
}
|
|
5080
|
+
return new OllamaProvider(
|
|
5081
|
+
cfg.ollamaUrl ?? "http://localhost:11434",
|
|
5082
|
+
cfg.ollamaModel ?? "llama3.1:latest"
|
|
5083
|
+
);
|
|
5084
|
+
}
|
|
5085
|
+
|
|
4032
5086
|
// src/commands/import.ts
|
|
4033
5087
|
async function cmdImport(db, cfg, args) {
|
|
4034
5088
|
if (args.length === 0) {
|
|
4035
5089
|
process.stderr.write("Usage: ph import <source> [--dry-run] [--analyze]\n");
|
|
4036
|
-
process.stderr.write("Supported sources: gemini, claude, opencode\n");
|
|
5090
|
+
process.stderr.write("Supported sources: gemini, claude, opencode, codex\n");
|
|
4037
5091
|
process.exit(1);
|
|
4038
5092
|
}
|
|
4039
5093
|
const source = args[0];
|
|
@@ -4041,13 +5095,16 @@ async function cmdImport(db, cfg, args) {
|
|
|
4041
5095
|
const dryRun = Boolean(flags["dry-run"]);
|
|
4042
5096
|
const analyze = Boolean(flags["analyze"]);
|
|
4043
5097
|
const useFilter = Boolean(flags["filter"]);
|
|
4044
|
-
const
|
|
5098
|
+
const transcriptFile = typeof flags.file === "string" ? flags.file : void 0;
|
|
5099
|
+
const homeDir = os8.homedir();
|
|
4045
5100
|
if (dryRun) console.log("Dry run \u2014 nothing will be written to the database.");
|
|
4046
5101
|
let analyzer;
|
|
4047
5102
|
if (analyze) {
|
|
4048
5103
|
analyzer = getProvider(cfg);
|
|
4049
5104
|
if (!analyzer) {
|
|
4050
|
-
|
|
5105
|
+
const detail = getProviderSetupError(cfg) ?? "No LLM provider is configured. Run: ph config set analyze-provider ollama";
|
|
5106
|
+
process.stderr.write(`ph: ${detail}
|
|
5107
|
+
`);
|
|
4051
5108
|
process.exit(1);
|
|
4052
5109
|
}
|
|
4053
5110
|
console.log(`Analyzing with ${analyzer.name} during import...`);
|
|
@@ -4076,16 +5133,19 @@ async function cmdImport(db, cfg, args) {
|
|
|
4076
5133
|
let result;
|
|
4077
5134
|
switch (source) {
|
|
4078
5135
|
case "gemini":
|
|
4079
|
-
result = await importGeminiHistory(db,
|
|
5136
|
+
result = await importGeminiHistory(db, path8.join(homeDir, ".gemini"), dryRun, analyzer, onProgress, filter);
|
|
4080
5137
|
break;
|
|
4081
5138
|
case "claude":
|
|
4082
|
-
result = await importClaudeHistory(db,
|
|
5139
|
+
result = await importClaudeHistory(db, path8.join(homeDir, ".claude"), dryRun, analyzer, onProgress, filter);
|
|
4083
5140
|
break;
|
|
4084
5141
|
case "opencode":
|
|
4085
|
-
result = await importOpenCodeHistory(db,
|
|
5142
|
+
result = await importOpenCodeHistory(db, path8.join(homeDir, ".local", "share", "opencode", "storage"), dryRun, analyzer, onProgress, filter);
|
|
5143
|
+
break;
|
|
5144
|
+
case "codex":
|
|
5145
|
+
result = await importCodexHistory(db, path8.join(homeDir, ".codex"), dryRun, analyzer, onProgress, filter, transcriptFile);
|
|
4086
5146
|
break;
|
|
4087
5147
|
default:
|
|
4088
|
-
process.stderr.write(`ph: unknown import source "${source}". Supported: gemini, claude, opencode
|
|
5148
|
+
process.stderr.write(`ph: unknown import source "${source}". Supported: gemini, claude, opencode, codex
|
|
4089
5149
|
`);
|
|
4090
5150
|
process.exit(1);
|
|
4091
5151
|
}
|
|
@@ -4117,7 +5177,9 @@ async function cmdAnalyze(db, cfg, args) {
|
|
|
4117
5177
|
const pruneBelow = prune ? flags["min-score"] ? Number(flags["min-score"]) : cfg.filterMinRelevance ?? 3 : 0;
|
|
4118
5178
|
const provider = getProvider(cfg);
|
|
4119
5179
|
if (!provider) {
|
|
4120
|
-
|
|
5180
|
+
const detail = getProviderSetupError(cfg) ?? "No LLM provider is configured. Set analyze-provider in config.";
|
|
5181
|
+
process.stderr.write(`ph: ${detail}
|
|
5182
|
+
`);
|
|
4121
5183
|
process.exit(1);
|
|
4122
5184
|
}
|
|
4123
5185
|
const allRows = db.search({ limit: 1e4 });
|
|
@@ -4190,9 +5252,6 @@ ${e.response}` : e.prompt);
|
|
|
4190
5252
|
console.log("\nDone!");
|
|
4191
5253
|
}
|
|
4192
5254
|
|
|
4193
|
-
// src/commands/log.ts
|
|
4194
|
-
import os8 from "os";
|
|
4195
|
-
|
|
4196
5255
|
// src/background/analyzer.ts
|
|
4197
5256
|
import { spawn as spawn3 } from "child_process";
|
|
4198
5257
|
function spawnBackgroundAnalysis(id, dbPath) {
|
|
@@ -4245,32 +5304,16 @@ async function cmdLog(dbPath, cfg, args) {
|
|
|
4245
5304
|
process.exit(1);
|
|
4246
5305
|
}
|
|
4247
5306
|
}
|
|
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;
|
|
5307
|
+
const record = createCaptureRecord({ tool, prompt, response, args: "", workdir });
|
|
4255
5308
|
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
|
-
});
|
|
5309
|
+
const id = db.insert(record);
|
|
4267
5310
|
db.close();
|
|
4268
5311
|
if (cfg.backgroundAnalysis) {
|
|
4269
5312
|
spawnBackgroundAnalysis(id, dbPath);
|
|
4270
5313
|
}
|
|
4271
5314
|
const pushUrl = process.env.PH_REMOTE_URL || cfg.remoteUrl;
|
|
4272
5315
|
if (pushUrl) {
|
|
4273
|
-
pushToRemote(pushUrl, cfg.remoteApiKey,
|
|
5316
|
+
pushToRemote(pushUrl, cfg.remoteApiKey, record).catch(() => {
|
|
4274
5317
|
});
|
|
4275
5318
|
}
|
|
4276
5319
|
}
|
|
@@ -4290,17 +5333,29 @@ async function pushToRemote(url, apiKey, entry) {
|
|
|
4290
5333
|
}
|
|
4291
5334
|
|
|
4292
5335
|
// src/commands/config.ts
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
5336
|
+
var SECRET_KEYS = ["gemini-api-key", "openrouter-api-key", "mlx-serve-api-key", "remote-api-key"];
|
|
5337
|
+
async function cmdConfig(args) {
|
|
5338
|
+
if (args[0] === "show") {
|
|
5339
|
+
const cfg2 = load();
|
|
5340
|
+
console.log("Analysis providers (* = active; credentials are never shown):");
|
|
5341
|
+
for (const provider of getProviderSummaries(cfg2)) console.log(formatProviderSummary(provider));
|
|
5342
|
+
console.log("Configure with: ph config set analyze-provider <ollama|gemini|openrouter|mlx-serve>");
|
|
5343
|
+
return;
|
|
5344
|
+
}
|
|
5345
|
+
if (args[0] !== "set" || args.length < 2) {
|
|
5346
|
+
console.log("Usage: ph config show | ph config set <key> [value]");
|
|
4296
5347
|
console.log("Keys: gemini-api-key, db-path, analyze-provider, ollama-url, ollama-model,");
|
|
5348
|
+
console.log(" openrouter-model, openrouter-url, openrouter-api-key,");
|
|
5349
|
+
console.log(" mlx-serve-model, mlx-serve-url, mlx-serve-api-key,");
|
|
4297
5350
|
console.log(" ollama-embed-model, filter-min-length, filter-min-relevance, background-analysis,");
|
|
4298
5351
|
console.log(" remote-url, remote-api-key");
|
|
4299
|
-
|
|
5352
|
+
return;
|
|
4300
5353
|
}
|
|
4301
5354
|
const cfg = load();
|
|
4302
5355
|
const key = args[1];
|
|
4303
|
-
|
|
5356
|
+
let val = args.slice(2).join(" ");
|
|
5357
|
+
if (!val && SECRET_KEYS.includes(key)) val = await readSecret(`${key}: `);
|
|
5358
|
+
if (!val) throw new Error(`A value is required for ${key}.`);
|
|
4304
5359
|
switch (key) {
|
|
4305
5360
|
case "gemini-api-key":
|
|
4306
5361
|
cfg.geminiApiKey = val;
|
|
@@ -4309,10 +5364,7 @@ function cmdConfig(args) {
|
|
|
4309
5364
|
cfg.dbPath = val;
|
|
4310
5365
|
break;
|
|
4311
5366
|
case "analyze-provider":
|
|
4312
|
-
if (
|
|
4313
|
-
process.stderr.write('ph: analyze-provider must be "ollama" or "gemini"\n');
|
|
4314
|
-
process.exit(1);
|
|
4315
|
-
}
|
|
5367
|
+
if (!["ollama", "gemini", "openrouter", "mlx-serve"].includes(val)) throw new Error("analyze-provider must be ollama, gemini, openrouter, or mlx-serve");
|
|
4316
5368
|
cfg.analyzeProvider = val;
|
|
4317
5369
|
break;
|
|
4318
5370
|
case "ollama-url":
|
|
@@ -4321,24 +5373,36 @@ function cmdConfig(args) {
|
|
|
4321
5373
|
case "ollama-model":
|
|
4322
5374
|
cfg.ollamaModel = val;
|
|
4323
5375
|
break;
|
|
5376
|
+
case "openrouter-model":
|
|
5377
|
+
cfg.openrouterModel = val;
|
|
5378
|
+
break;
|
|
5379
|
+
case "openrouter-url":
|
|
5380
|
+
cfg.openrouterUrl = val;
|
|
5381
|
+
break;
|
|
5382
|
+
case "openrouter-api-key":
|
|
5383
|
+
cfg.openrouterApiKey = val;
|
|
5384
|
+
break;
|
|
5385
|
+
case "mlx-serve-model":
|
|
5386
|
+
cfg.mlxServeModel = val;
|
|
5387
|
+
break;
|
|
5388
|
+
case "mlx-serve-url":
|
|
5389
|
+
cfg.mlxServeUrl = val;
|
|
5390
|
+
break;
|
|
5391
|
+
case "mlx-serve-api-key":
|
|
5392
|
+
cfg.mlxServeApiKey = val;
|
|
5393
|
+
break;
|
|
4324
5394
|
case "ollama-embed-model":
|
|
4325
5395
|
cfg.ollamaEmbedModel = val;
|
|
4326
5396
|
break;
|
|
4327
5397
|
case "filter-min-length": {
|
|
4328
5398
|
const n = Number(val);
|
|
4329
|
-
if (
|
|
4330
|
-
process.stderr.write("ph: filter-min-length must be a non-negative integer\n");
|
|
4331
|
-
process.exit(1);
|
|
4332
|
-
}
|
|
5399
|
+
if (!Number.isInteger(n) || n < 0) throw new Error("filter-min-length must be a non-negative integer");
|
|
4333
5400
|
cfg.filterMinLength = n;
|
|
4334
5401
|
break;
|
|
4335
5402
|
}
|
|
4336
5403
|
case "filter-min-relevance": {
|
|
4337
5404
|
const n = Number(val);
|
|
4338
|
-
if (
|
|
4339
|
-
process.stderr.write("ph: filter-min-relevance must be 0-10\n");
|
|
4340
|
-
process.exit(1);
|
|
4341
|
-
}
|
|
5405
|
+
if (!Number.isInteger(n) || n < 0 || n > 10) throw new Error("filter-min-relevance must be 0-10");
|
|
4342
5406
|
cfg.filterMinRelevance = n;
|
|
4343
5407
|
break;
|
|
4344
5408
|
}
|
|
@@ -4352,15 +5416,53 @@ function cmdConfig(args) {
|
|
|
4352
5416
|
cfg.remoteApiKey = val;
|
|
4353
5417
|
break;
|
|
4354
5418
|
default:
|
|
4355
|
-
|
|
4356
|
-
`);
|
|
4357
|
-
process.exit(1);
|
|
5419
|
+
throw new Error(`unknown config key "${key}"`);
|
|
4358
5420
|
}
|
|
4359
5421
|
save(cfg);
|
|
4360
|
-
console.log(`Config "${key}" updated successfully.`);
|
|
5422
|
+
console.log(SECRET_KEYS.includes(key) ? `Config "${key}" updated (value hidden).` : `Config "${key}" updated successfully.`);
|
|
5423
|
+
}
|
|
5424
|
+
function readSecret(prompt) {
|
|
5425
|
+
if (!process.stdin.isTTY) throw new Error(`No value supplied for ${prompt.trim()}; use an environment variable in non-interactive shells.`);
|
|
5426
|
+
return new Promise((resolve, reject) => {
|
|
5427
|
+
const input = process.stdin;
|
|
5428
|
+
const previousRawMode = input.isRaw ?? false;
|
|
5429
|
+
let value = "";
|
|
5430
|
+
process.stderr.write(prompt);
|
|
5431
|
+
input.setRawMode(true);
|
|
5432
|
+
input.resume();
|
|
5433
|
+
const cleanup = () => {
|
|
5434
|
+
input.removeListener("data", onData);
|
|
5435
|
+
input.setRawMode(previousRawMode);
|
|
5436
|
+
process.stderr.write("\n");
|
|
5437
|
+
};
|
|
5438
|
+
const onData = (chunk) => {
|
|
5439
|
+
for (const byte of chunk) {
|
|
5440
|
+
if (byte === 3) {
|
|
5441
|
+
cleanup();
|
|
5442
|
+
reject(new Error("Input cancelled."));
|
|
5443
|
+
return;
|
|
5444
|
+
}
|
|
5445
|
+
if (byte === 13 || byte === 10) {
|
|
5446
|
+
cleanup();
|
|
5447
|
+
resolve(value);
|
|
5448
|
+
return;
|
|
5449
|
+
}
|
|
5450
|
+
if (byte === 127 || byte === 8) value = value.slice(0, -1);
|
|
5451
|
+
else if (byte >= 32) value += String.fromCharCode(byte);
|
|
5452
|
+
}
|
|
5453
|
+
};
|
|
5454
|
+
input.on("data", onData);
|
|
5455
|
+
});
|
|
4361
5456
|
}
|
|
4362
5457
|
|
|
4363
5458
|
// src/commands/background-analyze.ts
|
|
5459
|
+
import { createHash as createHash5 } from "crypto";
|
|
5460
|
+
function gitFingerprint2(ctx) {
|
|
5461
|
+
const hash = createHash5("sha256");
|
|
5462
|
+
hash.update(ctx.branch);
|
|
5463
|
+
hash.update(ctx.diff);
|
|
5464
|
+
return `git:${hash.digest("hex").slice(0, 14)}`;
|
|
5465
|
+
}
|
|
4364
5466
|
async function cmdBackgroundAnalyze(dbPath, args) {
|
|
4365
5467
|
const id = parseInt(args[0], 10);
|
|
4366
5468
|
if (isNaN(id)) return;
|
|
@@ -4386,11 +5488,24 @@ async function cmdBackgroundAnalyze(dbPath, args) {
|
|
|
4386
5488
|
const merged = mergeMetadata(existing, result, false);
|
|
4387
5489
|
db.updateMetadata(id, JSON.stringify(merged));
|
|
4388
5490
|
if (result.project && result.summary) {
|
|
5491
|
+
const gitCtx = captureGitContext(process.cwd());
|
|
5492
|
+
const gitFp = gitCtx ? gitFingerprint2(gitCtx) : void 0;
|
|
5493
|
+
if (gitFp) {
|
|
5494
|
+
const lastMem = db.getLastMemoryForProject(result.project);
|
|
5495
|
+
if (lastMem?.git_context_snapshot === gitFp) return;
|
|
5496
|
+
}
|
|
4389
5497
|
db.upsertProjectMemory({
|
|
4390
5498
|
project: result.project,
|
|
4391
5499
|
prompt_id: id,
|
|
4392
5500
|
summary: result.summary,
|
|
4393
5501
|
key_insights: result.key_insights ?? [],
|
|
5502
|
+
technical_decisions: result.technical_decisions ?? [],
|
|
5503
|
+
git_context_snapshot: gitFp
|
|
5504
|
+
});
|
|
5505
|
+
db.upsertProjectSummary({
|
|
5506
|
+
project: result.project,
|
|
5507
|
+
summary: result.summary,
|
|
5508
|
+
key_insights: result.key_insights ?? [],
|
|
4394
5509
|
technical_decisions: result.technical_decisions ?? []
|
|
4395
5510
|
});
|
|
4396
5511
|
}
|
|
@@ -4404,6 +5519,11 @@ async function cmdBackgroundAnalyze(dbPath, args) {
|
|
|
4404
5519
|
async function cmdCleanup(db, cfg, args) {
|
|
4405
5520
|
const { flags } = parseFlags(args);
|
|
4406
5521
|
const dryRun = Boolean(flags["dry-run"]);
|
|
5522
|
+
const retention = Boolean(flags["retention"]);
|
|
5523
|
+
if (retention) {
|
|
5524
|
+
await runRetention(db, cfg, dryRun);
|
|
5525
|
+
return;
|
|
5526
|
+
}
|
|
4407
5527
|
const minLength = flags["min-length"] ? Number(flags["min-length"]) : cfg.filterMinLength ?? 15;
|
|
4408
5528
|
const minScore = flags["min-score"] ? Number(flags["min-score"]) : cfg.filterMinRelevance ?? 3;
|
|
4409
5529
|
const days = flags["days"] ? Number(flags["days"]) : void 0;
|
|
@@ -4467,6 +5587,71 @@ Candidates for rule-based deletion: ${toDelete.length}`);
|
|
|
4467
5587
|
console.log(`
|
|
4468
5588
|
Deleted ${deleted} prompts.`);
|
|
4469
5589
|
}
|
|
5590
|
+
async function runRetention(db, cfg, dryRun) {
|
|
5591
|
+
const retentionDays = cfg.retentionDays ?? 90;
|
|
5592
|
+
const minStarred = cfg.retentionMinStarred ?? true;
|
|
5593
|
+
const minAnalyzed = cfg.retentionMinAnalyzed ?? true;
|
|
5594
|
+
const minRelevance = cfg.retentionMinRelevance ?? 3;
|
|
5595
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
5596
|
+
cutoff.setDate(cutoff.getDate() - retentionDays);
|
|
5597
|
+
const cutoffStr = cutoff.toISOString();
|
|
5598
|
+
const allEntries = db.search({ limit: 1e5 });
|
|
5599
|
+
const stats = db.getArchiveStats();
|
|
5600
|
+
console.log(`Retention policy: ${retentionDays} days, minRelevance=${minRelevance}, keepStarred=${minStarred}, keepAnalyzed=${minAnalyzed}`);
|
|
5601
|
+
console.log(`Archive: ${stats.total} entries (${stats.oldest ? `oldest ${stats.oldest}` : "empty"})`);
|
|
5602
|
+
console.log(`Scanning ${allEntries.length} prompts...
|
|
5603
|
+
`);
|
|
5604
|
+
const toArchive = [];
|
|
5605
|
+
for (const entry of allEntries) {
|
|
5606
|
+
let meta = {};
|
|
5607
|
+
try {
|
|
5608
|
+
meta = JSON.parse(entry.metadata);
|
|
5609
|
+
} catch {
|
|
5610
|
+
}
|
|
5611
|
+
const age = entry.timestamp;
|
|
5612
|
+
const isOld = age < cutoffStr;
|
|
5613
|
+
const isStarred = meta.starred === true;
|
|
5614
|
+
const isAnalyzed = Boolean(meta.summary || meta.role);
|
|
5615
|
+
const relevance = meta.relevance ?? 5;
|
|
5616
|
+
if (!isOld) continue;
|
|
5617
|
+
if (relevance < minRelevance) {
|
|
5618
|
+
toArchive.push({ id: entry.id, prompt: entry.prompt, reason: `low_relevance(${relevance})`, meta });
|
|
5619
|
+
continue;
|
|
5620
|
+
}
|
|
5621
|
+
if (!(minStarred && isStarred) && !(minAnalyzed && isAnalyzed)) {
|
|
5622
|
+
toArchive.push({ id: entry.id, prompt: entry.prompt, reason: `old(>${retentionDays}d)`, meta });
|
|
5623
|
+
}
|
|
5624
|
+
}
|
|
5625
|
+
if (toArchive.length === 0) {
|
|
5626
|
+
console.log("No prompts match retention criteria.");
|
|
5627
|
+
return;
|
|
5628
|
+
}
|
|
5629
|
+
console.log(`Candidates for archiving: ${toArchive.length}`);
|
|
5630
|
+
for (const item of toArchive.slice(0, 20)) {
|
|
5631
|
+
const short = item.prompt.replace(/\n/g, " ").slice(0, 60);
|
|
5632
|
+
const starred = item.meta.starred ? " *" : "";
|
|
5633
|
+
const analyzed = item.meta.summary ? " [A]" : "";
|
|
5634
|
+
console.log(` #${String(item.id).padEnd(5)} [${item.reason}]${starred}${analyzed} "${short}"`);
|
|
5635
|
+
}
|
|
5636
|
+
if (toArchive.length > 20) {
|
|
5637
|
+
console.log(` ... and ${toArchive.length - 20} more`);
|
|
5638
|
+
}
|
|
5639
|
+
if (dryRun) {
|
|
5640
|
+
console.log(`
|
|
5641
|
+
(dry-run) Would archive ${toArchive.length} prompts. Run without --dry-run to apply.`);
|
|
5642
|
+
return;
|
|
5643
|
+
}
|
|
5644
|
+
const ids = toArchive.map((e) => e.id);
|
|
5645
|
+
const archived = db.archivePrompts(ids);
|
|
5646
|
+
console.log(`
|
|
5647
|
+
Archived ${archived} prompts.`);
|
|
5648
|
+
const purgeCutoff = /* @__PURE__ */ new Date();
|
|
5649
|
+
purgeCutoff.setDate(purgeCutoff.getDate() - retentionDays * 2);
|
|
5650
|
+
const purged = db.purgeArchive(purgeCutoff.toISOString());
|
|
5651
|
+
if (purged > 0) {
|
|
5652
|
+
console.log(`Purged ${purged} archived entries older than ${retentionDays * 2} days.`);
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
4470
5655
|
|
|
4471
5656
|
// src/commands/vacuum.ts
|
|
4472
5657
|
async function cmdVacuum(db) {
|
|
@@ -4490,75 +5675,20 @@ async function cmdContext(db, cfg, args) {
|
|
|
4490
5675
|
process.stderr.write("ph: could not detect project. Use --project <name>\n");
|
|
4491
5676
|
process.exit(1);
|
|
4492
5677
|
}
|
|
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"));
|
|
5678
|
+
const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
|
|
5679
|
+
const model = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
|
|
5680
|
+
const context = await getProjectContext(db, {
|
|
5681
|
+
project,
|
|
5682
|
+
query,
|
|
5683
|
+
limit,
|
|
5684
|
+
includeSummary: !promptsOnly,
|
|
5685
|
+
includeMemories: !promptsOnly,
|
|
5686
|
+
includePrompts: !memoriesOnly
|
|
5687
|
+
}, async (text) => {
|
|
5688
|
+
const [vector] = await getEmbeddings([text], ollamaUrl, model, 1);
|
|
5689
|
+
return vector;
|
|
5690
|
+
});
|
|
5691
|
+
process.stdout.write(formatProjectContext(context, { includePromptText: verbose }) + "\n");
|
|
4562
5692
|
}
|
|
4563
5693
|
|
|
4564
5694
|
// src/sessions/index.ts
|
|
@@ -4613,42 +5743,100 @@ function computeSessionCohesion(session, embeddings) {
|
|
|
4613
5743
|
}
|
|
4614
5744
|
|
|
4615
5745
|
// src/commands/sessions.ts
|
|
5746
|
+
function formatTimestamp4(ts) {
|
|
5747
|
+
const d = new Date(ts);
|
|
5748
|
+
const Y = d.getFullYear();
|
|
5749
|
+
const M = String(d.getMonth() + 1).padStart(2, "0");
|
|
5750
|
+
const D = String(d.getDate()).padStart(2, "0");
|
|
5751
|
+
const h = String(d.getHours()).padStart(2, "0");
|
|
5752
|
+
const m = String(d.getMinutes()).padStart(2, "0");
|
|
5753
|
+
return `${Y}-${M}-${D} ${h}:${m}`;
|
|
5754
|
+
}
|
|
5755
|
+
function fmtTime(ts) {
|
|
5756
|
+
const d = new Date(ts);
|
|
5757
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
5758
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
5759
|
+
return `${hh}:${mm}`;
|
|
5760
|
+
}
|
|
5761
|
+
function parseMeta3(raw) {
|
|
5762
|
+
try {
|
|
5763
|
+
return JSON.parse(raw);
|
|
5764
|
+
} catch {
|
|
5765
|
+
return {};
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
function exportSessionMarkdown(session) {
|
|
5769
|
+
const lines = [];
|
|
5770
|
+
const dateStr = formatTimestamp4(session.startTime.toISOString());
|
|
5771
|
+
const endStr = fmtTime(session.endTime.toISOString());
|
|
5772
|
+
lines.push(`# Session ${session.index} \u2014 ${dateStr} \u2192 ${endStr}`);
|
|
5773
|
+
lines.push("");
|
|
5774
|
+
lines.push(`**${session.entries.length} prompts**`);
|
|
5775
|
+
lines.push("");
|
|
5776
|
+
lines.push("---");
|
|
5777
|
+
lines.push("");
|
|
5778
|
+
for (const entry of session.entries) {
|
|
5779
|
+
const meta = parseMeta3(entry.metadata);
|
|
5780
|
+
const ts = formatTimestamp4(entry.timestamp);
|
|
5781
|
+
const title = meta.title || entry.prompt.replace(/\n/g, " ").slice(0, 60);
|
|
5782
|
+
lines.push(`## #${entry.id} \u2014 ${entry.tool} \u2014 ${ts}`);
|
|
5783
|
+
if (title) lines.push(`**${title}**`);
|
|
5784
|
+
lines.push("");
|
|
5785
|
+
const metaLine = [];
|
|
5786
|
+
if (meta.role) metaLine.push(`Role: ${meta.role}`);
|
|
5787
|
+
if (meta.project) metaLine.push(`Project: ${meta.project}`);
|
|
5788
|
+
if (meta.language) metaLine.push(`Language: ${meta.language}`);
|
|
5789
|
+
if (meta.tags?.length) metaLine.push(`Tags: ${meta.tags.join(", ")}`);
|
|
5790
|
+
if (meta.relevance !== void 0) metaLine.push(`Relevance: ${meta.relevance}/10`);
|
|
5791
|
+
if (meta.quality !== void 0) metaLine.push(`Quality: ${meta.quality}/10`);
|
|
5792
|
+
if (metaLine.length > 0) {
|
|
5793
|
+
lines.push(metaLine.join(" \xB7 "));
|
|
5794
|
+
lines.push("");
|
|
5795
|
+
}
|
|
5796
|
+
if (meta.summary) {
|
|
5797
|
+
lines.push(`> ${meta.summary}`);
|
|
5798
|
+
lines.push("");
|
|
5799
|
+
}
|
|
5800
|
+
lines.push("### Prompt");
|
|
5801
|
+
lines.push("");
|
|
5802
|
+
lines.push("```");
|
|
5803
|
+
lines.push(entry.prompt);
|
|
5804
|
+
lines.push("```");
|
|
5805
|
+
lines.push("");
|
|
5806
|
+
if (entry.response) {
|
|
5807
|
+
lines.push("### Response");
|
|
5808
|
+
lines.push("");
|
|
5809
|
+
lines.push("```");
|
|
5810
|
+
lines.push(entry.response.slice(0, 2e3));
|
|
5811
|
+
lines.push("```");
|
|
5812
|
+
lines.push("");
|
|
5813
|
+
}
|
|
5814
|
+
if (meta.key_insights?.length) {
|
|
5815
|
+
lines.push("**Insights:**");
|
|
5816
|
+
for (const ins of meta.key_insights) lines.push(`- ${ins}`);
|
|
5817
|
+
lines.push("");
|
|
5818
|
+
}
|
|
5819
|
+
if (meta.git_context) {
|
|
5820
|
+
lines.push(`*Git: ${meta.git_context.branch}, ${meta.git_context.files?.length || 0} files modified*`);
|
|
5821
|
+
lines.push("");
|
|
5822
|
+
}
|
|
5823
|
+
lines.push("---");
|
|
5824
|
+
lines.push("");
|
|
5825
|
+
}
|
|
5826
|
+
return lines.join("\n");
|
|
5827
|
+
}
|
|
4616
5828
|
async function cmdSessions(db, args) {
|
|
4617
5829
|
const { flags } = parseFlags(args);
|
|
4618
5830
|
const gapHours = Number(flags["gap-hours"] ?? 2);
|
|
4619
5831
|
const limit = Number(flags["limit"] ?? 20);
|
|
4620
5832
|
const minSize = Number(flags["min-size"] ?? 1);
|
|
4621
5833
|
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
|
-
}
|
|
5834
|
+
const exportIndex = flags["export"] !== void 0 ? Number(flags["export"]) : void 0;
|
|
4647
5835
|
const allEntries = db.search({ limit: 1e5 });
|
|
4648
5836
|
const entriesAsc = [...allEntries].reverse();
|
|
4649
5837
|
const sessions = groupIntoSessions(entriesAsc, gapHours);
|
|
4650
5838
|
let embeddings = null;
|
|
4651
|
-
if (!noCohesion) {
|
|
5839
|
+
if (!noCohesion && exportIndex === void 0) {
|
|
4652
5840
|
embeddings = db.getAllEmbeddings();
|
|
4653
5841
|
}
|
|
4654
5842
|
for (const session of sessions) {
|
|
@@ -4656,21 +5844,37 @@ async function cmdSessions(db, args) {
|
|
|
4656
5844
|
session.cohesion = computeSessionCohesion(session, embeddings);
|
|
4657
5845
|
}
|
|
4658
5846
|
}
|
|
5847
|
+
if (exportIndex !== void 0) {
|
|
5848
|
+
const session = sessions.find((s) => s.index === exportIndex);
|
|
5849
|
+
if (!session) {
|
|
5850
|
+
process.stderr.write(`Session ${exportIndex} not found.
|
|
5851
|
+
`);
|
|
5852
|
+
return;
|
|
5853
|
+
}
|
|
5854
|
+
process.stdout.write(exportSessionMarkdown(session));
|
|
5855
|
+
return;
|
|
5856
|
+
}
|
|
4659
5857
|
const filtered = sessions.filter((s) => s.entries.length >= minSize);
|
|
4660
5858
|
const visible = filtered.slice(-limit);
|
|
4661
5859
|
if (visible.length === 0) {
|
|
4662
5860
|
process.stdout.write("No sessions found.\n");
|
|
4663
5861
|
return;
|
|
4664
5862
|
}
|
|
5863
|
+
const C2 = {
|
|
5864
|
+
reset: "\x1B[0m",
|
|
5865
|
+
cyan: "\x1B[36m",
|
|
5866
|
+
yellow: "\x1B[33m",
|
|
5867
|
+
green: "\x1B[32m",
|
|
5868
|
+
gray: "\x1B[90m",
|
|
5869
|
+
red: "\x1B[31m",
|
|
5870
|
+
blue: "\x1B[34m",
|
|
5871
|
+
magenta: "\x1B[35m"
|
|
5872
|
+
};
|
|
4665
5873
|
for (const session of visible) {
|
|
4666
5874
|
const projectCounts = /* @__PURE__ */ new Map();
|
|
4667
5875
|
const languageCounts = /* @__PURE__ */ new Map();
|
|
4668
5876
|
for (const entry of session.entries) {
|
|
4669
|
-
|
|
4670
|
-
try {
|
|
4671
|
-
meta = JSON.parse(entry.metadata);
|
|
4672
|
-
} catch {
|
|
4673
|
-
}
|
|
5877
|
+
const meta = parseMeta3(entry.metadata);
|
|
4674
5878
|
if (meta.project) projectCounts.set(meta.project, (projectCounts.get(meta.project) ?? 0) + 1);
|
|
4675
5879
|
if (meta.language) languageCounts.set(meta.language, (languageCounts.get(meta.language) ?? 0) + 1);
|
|
4676
5880
|
}
|
|
@@ -4689,7 +5893,7 @@ async function cmdSessions(db, args) {
|
|
|
4689
5893
|
break;
|
|
4690
5894
|
}
|
|
4691
5895
|
}
|
|
4692
|
-
const startStr =
|
|
5896
|
+
const startStr = formatTimestamp4(session.startTime.toISOString());
|
|
4693
5897
|
const endStr = fmtTime(session.endTime.toISOString());
|
|
4694
5898
|
const promptWord = session.entries.length === 1 ? "prompt" : "prompts";
|
|
4695
5899
|
let header = `${C2.cyan}Session ${session.index}${C2.reset}`;
|
|
@@ -4707,11 +5911,7 @@ async function cmdSessions(db, args) {
|
|
|
4707
5911
|
}
|
|
4708
5912
|
process.stdout.write(header + "\n");
|
|
4709
5913
|
for (const entry of session.entries) {
|
|
4710
|
-
|
|
4711
|
-
try {
|
|
4712
|
-
meta = JSON.parse(entry.metadata);
|
|
4713
|
-
} catch {
|
|
4714
|
-
}
|
|
5914
|
+
const meta = parseMeta3(entry.metadata);
|
|
4715
5915
|
const idStr = `${C2.cyan}#${entry.id}${C2.reset}`;
|
|
4716
5916
|
const toolStr = `${C2.yellow}${entry.tool.padEnd(8)}${C2.reset}`;
|
|
4717
5917
|
const timeStr = `${C2.gray}${fmtTime(entry.timestamp)}${C2.reset}`;
|
|
@@ -4922,7 +6122,7 @@ ${C2.cyan}Cluster #${i + 1}${C2.reset} (${C2.yellow}${cluster.entryIds.length} p
|
|
|
4922
6122
|
}
|
|
4923
6123
|
|
|
4924
6124
|
// src/commands/analyze-reusability.ts
|
|
4925
|
-
import
|
|
6125
|
+
import fs9 from "fs";
|
|
4926
6126
|
|
|
4927
6127
|
// src/analyzer/reusability.ts
|
|
4928
6128
|
var ReusabilityAnalyzer = class _ReusabilityAnalyzer {
|
|
@@ -5107,7 +6307,7 @@ ${C2.bold}TOP 10 REMOVE (low score + duplicate):${C2.reset}`);
|
|
|
5107
6307
|
console.log(` [${C2.red}${scoreStr}${C2.reset}] ${s.prompt.replace(/\n/g, " ").slice(0, 80)}...`);
|
|
5108
6308
|
});
|
|
5109
6309
|
if (exportCsv) {
|
|
5110
|
-
|
|
6310
|
+
fs9.writeFileSync(exportCsv, analyzer.toCSV(report));
|
|
5111
6311
|
console.log(`
|
|
5112
6312
|
${C2.green}\u2705 Report exported to ${exportCsv}${C2.reset}`);
|
|
5113
6313
|
}
|
|
@@ -5115,7 +6315,7 @@ ${C2.green}\u2705 Report exported to ${exportCsv}${C2.reset}`);
|
|
|
5115
6315
|
}
|
|
5116
6316
|
|
|
5117
6317
|
// src/commands/cleanup-reusability.ts
|
|
5118
|
-
import
|
|
6318
|
+
import fs10 from "fs";
|
|
5119
6319
|
async function cmdCleanupReusability(db, _cfg, args) {
|
|
5120
6320
|
const { flags } = parseFlags(args);
|
|
5121
6321
|
const threshold = flags["threshold"] ? parseFloat(flags["threshold"]) : 0.7;
|
|
@@ -5177,7 +6377,7 @@ Run with ${C2.bold}--force${C2.reset} or without ${C2.bold}--dry-run${C2.reset}
|
|
|
5177
6377
|
process.stdout.write(`
|
|
5178
6378
|
Continue and delete ${candidates.length} prompts? (y/N) `);
|
|
5179
6379
|
const buffer = Buffer.alloc(16);
|
|
5180
|
-
const bytesRead =
|
|
6380
|
+
const bytesRead = fs10.readSync(0, buffer, 0, 16, null);
|
|
5181
6381
|
const answer = buffer.toString("utf8", 0, bytesRead).trim().toLowerCase();
|
|
5182
6382
|
if (answer !== "y" && answer !== "yes") {
|
|
5183
6383
|
console.log("Aborted.");
|
|
@@ -5292,8 +6492,40 @@ function formatMemoryEntry(m) {
|
|
|
5292
6492
|
return lines.join("\n");
|
|
5293
6493
|
}
|
|
5294
6494
|
|
|
6495
|
+
// src/commands/memory-migrate.ts
|
|
6496
|
+
function cmdMemoryMigrate(db) {
|
|
6497
|
+
const projects = db.getAllProjectsWithMemories();
|
|
6498
|
+
if (projects.length === 0) {
|
|
6499
|
+
console.log("No memories found. Nothing to migrate.");
|
|
6500
|
+
return;
|
|
6501
|
+
}
|
|
6502
|
+
let count = 0;
|
|
6503
|
+
for (const project of projects) {
|
|
6504
|
+
const memories = db.searchMemories(project, 1e3);
|
|
6505
|
+
const allInsights = /* @__PURE__ */ new Set();
|
|
6506
|
+
const allDecisions = /* @__PURE__ */ new Set();
|
|
6507
|
+
let lastSummary = "";
|
|
6508
|
+
let gitCtx;
|
|
6509
|
+
for (const mem of memories) {
|
|
6510
|
+
if (mem.summary) lastSummary = mem.summary;
|
|
6511
|
+
for (const i of mem.key_insights) allInsights.add(i);
|
|
6512
|
+
for (const d of mem.technical_decisions) allDecisions.add(d);
|
|
6513
|
+
if (mem.git_context_snapshot) gitCtx = mem.git_context_snapshot;
|
|
6514
|
+
}
|
|
6515
|
+
db.upsertProjectSummary({
|
|
6516
|
+
project,
|
|
6517
|
+
summary: lastSummary,
|
|
6518
|
+
key_insights: [...allInsights],
|
|
6519
|
+
technical_decisions: [...allDecisions],
|
|
6520
|
+
git_context_snapshot: gitCtx
|
|
6521
|
+
});
|
|
6522
|
+
count++;
|
|
6523
|
+
}
|
|
6524
|
+
console.log(`Migrated ${count} project(s) into project_summaries.`);
|
|
6525
|
+
}
|
|
6526
|
+
|
|
5295
6527
|
// src/commands/wrap.ts
|
|
5296
|
-
import
|
|
6528
|
+
import path9 from "path";
|
|
5297
6529
|
import os9 from "os";
|
|
5298
6530
|
async function cmdWrap(dbPath, tool, args, cfg) {
|
|
5299
6531
|
let debugLog;
|
|
@@ -5302,7 +6534,7 @@ async function cmdWrap(dbPath, tool, args, cfg) {
|
|
|
5302
6534
|
const cleanArgs = [];
|
|
5303
6535
|
for (let i = 0; i < args.length; i++) {
|
|
5304
6536
|
if (args[i] === "--ph-debug") {
|
|
5305
|
-
debugLog =
|
|
6537
|
+
debugLog = path9.join(os9.homedir(), ".ph_debug.log");
|
|
5306
6538
|
process.stderr.write(`ph: debug log \u2192 ${debugLog}
|
|
5307
6539
|
`);
|
|
5308
6540
|
} else if (args[i] === "--ph-tag" && i + 1 < args.length) {
|
|
@@ -5331,28 +6563,17 @@ async function cmdWrap(dbPath, tool, args, cfg) {
|
|
|
5331
6563
|
const db = new PhDB(dbPath);
|
|
5332
6564
|
if (interactive) {
|
|
5333
6565
|
const workdir = process.cwd();
|
|
5334
|
-
const { rootDir, projectName } = detectProject(workdir);
|
|
5335
|
-
const language = detectLanguage(rootDir);
|
|
5336
6566
|
const gitContext = captureGitContext(workdir);
|
|
5337
6567
|
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({
|
|
6568
|
+
return db.insert(createCaptureRecord({
|
|
5346
6569
|
timestamp: ts.toISOString(),
|
|
5347
6570
|
tool,
|
|
5348
6571
|
prompt,
|
|
5349
|
-
response: "",
|
|
5350
6572
|
args: prompt,
|
|
5351
6573
|
workdir,
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
});
|
|
6574
|
+
gitContext,
|
|
6575
|
+
metadata: { ...role ? { role } : {}, ...tags.length > 0 ? { tags } : {} }
|
|
6576
|
+
}));
|
|
5356
6577
|
};
|
|
5357
6578
|
const onResponse = (id, response) => {
|
|
5358
6579
|
if (!response.trim()) return;
|
|
@@ -5392,41 +6613,14 @@ async function cmdChat(dbPath, cfg, args) {
|
|
|
5392
6613
|
process.exit(1);
|
|
5393
6614
|
}
|
|
5394
6615
|
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
|
-
}
|
|
6616
|
+
const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
|
|
6617
|
+
const model = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
|
|
6618
|
+
const context = await getProjectContext(db, { project, limit: 5, memoryLimit: 3 }, async (query) => {
|
|
6619
|
+
const [vector] = await getEmbeddings([query], ollamaUrl, model, 1);
|
|
6620
|
+
return vector;
|
|
6621
|
+
});
|
|
5428
6622
|
db.close();
|
|
5429
|
-
const contextStr =
|
|
6623
|
+
const contextStr = formatProjectContext(context);
|
|
5430
6624
|
const fullPrompt = `Context from project "${project}":
|
|
5431
6625
|
|
|
5432
6626
|
${contextStr}
|
|
@@ -5439,6 +6633,40 @@ ${userPrompt}`;
|
|
|
5439
6633
|
process.exit(child.status ?? 0);
|
|
5440
6634
|
}
|
|
5441
6635
|
|
|
6636
|
+
// src/commands/models.ts
|
|
6637
|
+
async function cmdModels(cfg, args) {
|
|
6638
|
+
const provider = args[0] ?? cfg.analyzeProvider;
|
|
6639
|
+
if (provider !== "openrouter" && provider !== "mlx-serve") {
|
|
6640
|
+
throw new Error("Choose a provider: `ph models openrouter` or `ph models mlx-serve`.");
|
|
6641
|
+
}
|
|
6642
|
+
const cfgKey = provider === "openrouter" ? cfg.openrouterApiKey : cfg.mlxServeApiKey;
|
|
6643
|
+
if (provider === "openrouter" && !(process.env.OPENROUTER_API_KEY || cfgKey)) {
|
|
6644
|
+
throw new Error("Configure a key with `ph config set openrouter-api-key` or set OPENROUTER_API_KEY.");
|
|
6645
|
+
}
|
|
6646
|
+
const apiKey = provider === "openrouter" ? process.env.OPENROUTER_API_KEY ?? cfg.openrouterApiKey : process.env.MLX_SERVE_API_KEY ?? process.env.GLB_OMLX_API_KEY ?? cfg.mlxServeApiKey ?? "local";
|
|
6647
|
+
const baseUrl = provider === "openrouter" ? process.env.OPENROUTER_BASE_URL ?? cfg.openrouterUrl ?? OPENROUTER_DEFAULT_URL : process.env.MLX_SERVE_BASE_URL ?? cfg.mlxServeUrl ?? MLX_SERVE_DEFAULT_URL;
|
|
6648
|
+
let models;
|
|
6649
|
+
try {
|
|
6650
|
+
models = await discoverModels(provider, apiKey, baseUrl);
|
|
6651
|
+
} catch (error) {
|
|
6652
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
6653
|
+
const tunnel = provider === "mlx-serve" && /127\.0\.0\.1|localhost/.test(baseUrl) ? "\nParmenide: apri in un altro terminale `ssh -N -L 11234:127.0.0.1:11234 parmenide`, poi riprova. Puoi impostare un URL raggiungibile con `ph config set mlx-serve-url <url>`." : "";
|
|
6654
|
+
throw new Error(`Cannot query ${provider} at ${baseUrl}: ${detail}${tunnel}`);
|
|
6655
|
+
}
|
|
6656
|
+
if (models.length === 0) {
|
|
6657
|
+
const tunnel = provider === "mlx-serve" ? "\nSe il CLI gira sul Mac e il server su Parmenide, apri: ssh -N -L 11234:127.0.0.1:11234 parmenide" : "";
|
|
6658
|
+
throw new Error(`No models returned by ${provider} at ${baseUrl}. Check connectivity and credentials.${tunnel}`);
|
|
6659
|
+
}
|
|
6660
|
+
console.log(`Models listed by ${provider} at ${baseUrl}:`);
|
|
6661
|
+
for (const model of models) {
|
|
6662
|
+
const status2 = model.loaded === void 0 ? "" : model.loaded ? " [loaded]" : ` [${model.state ?? "unloaded"}]`;
|
|
6663
|
+
console.log(` ${model.id}${model.name !== model.id ? ` \u2014 ${model.name}` : ""}${status2}`);
|
|
6664
|
+
}
|
|
6665
|
+
if (provider === "mlx-serve") {
|
|
6666
|
+
console.log("\nph can list installed models even when they are unloaded. Select a model marked [loaded] to run it immediately.");
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6669
|
+
|
|
5442
6670
|
// src/cli.ts
|
|
5443
6671
|
var USAGE = `ph \u2014 prompt history tracker
|
|
5444
6672
|
|
|
@@ -5455,18 +6683,22 @@ USAGE:
|
|
|
5455
6683
|
ph analyze-reusability [options] Analyze prompt reusability
|
|
5456
6684
|
ph star <id> Toggle star on a prompt
|
|
5457
6685
|
ph export <id> [--format txt|json|md] Export a single prompt
|
|
5458
|
-
ph import
|
|
5459
|
-
|
|
6686
|
+
ph import <source> [--dry-run] [--analyze] [--filter] [--file <path>]
|
|
6687
|
+
Import from Gemini, Claude, OpenCode, or Codex history
|
|
5460
6688
|
ph analyze [--limit n] [--force] [--prune] [--dry-run] Analyze prompts with LLM
|
|
6689
|
+
ph models [openrouter|mlx-serve] List models from an AI provider
|
|
5461
6690
|
ph mcp Start MCP server (Stdio)
|
|
5462
6691
|
ph server [--port 3001] Start HTTP REST server for remote sync
|
|
5463
6692
|
ph timeline [project] Show full project history with prompts and memories
|
|
6693
|
+
ph memory-migrate Merge existing memories into project_summaries
|
|
5464
6694
|
ph remote push|pull|status Sync prompts with remote ph server
|
|
5465
6695
|
ph cleanup [--dry-run] [--min-length N] [--min-score N] Remove useless prompts
|
|
6696
|
+
ph cleanup --retention [--dry-run] Archive prompts per retention policy
|
|
5466
6697
|
ph cleanup-reusability [--dry-run] [--threshold 0.7] [--force] Cleanup based on reusability
|
|
5467
6698
|
ph embed-all Generate embeddings for all prompts
|
|
5468
6699
|
ph log --tool <name> --prompt <text> [--response <text>] Log a prompt+response directly
|
|
5469
|
-
ph config
|
|
6700
|
+
ph config show Show active AI provider configuration
|
|
6701
|
+
ph config set <key> [value] Save config value (secret keys prompt hidden)
|
|
5470
6702
|
ph browse Interactive TUI browser
|
|
5471
6703
|
|
|
5472
6704
|
WRAP FLAGS (placed before the tool name):
|
|
@@ -5476,7 +6708,7 @@ WRAP FLAGS (placed before the tool name):
|
|
|
5476
6708
|
|
|
5477
6709
|
SEARCH OPTIONS:
|
|
5478
6710
|
-i, --interactive Open results in interactive TUI browser
|
|
5479
|
-
--tool <name> Filter by tool name (claude, gemini, \u2026)
|
|
6711
|
+
--tool <name> Filter by tool name (claude, gemini, opencode, codex, \u2026)
|
|
5480
6712
|
--project <name> Filter by project name
|
|
5481
6713
|
--language <lang> Filter by language (go, typescript, python, \u2026)
|
|
5482
6714
|
--role <role> Filter by role (debug, refactor, explain, \u2026)
|
|
@@ -5486,6 +6718,7 @@ SEARCH OPTIONS:
|
|
|
5486
6718
|
--min-relevance <n> Filter by min relevance (0-10)
|
|
5487
6719
|
--top Show only top quality prompts (quality >= 8)
|
|
5488
6720
|
--semantic Use semantic search (requires embeddings)
|
|
6721
|
+
--archive Search archived prompts
|
|
5489
6722
|
--since YYYY-MM-DD
|
|
5490
6723
|
--until YYYY-MM-DD
|
|
5491
6724
|
--limit <n> Max results (default 50)
|
|
@@ -5503,6 +6736,7 @@ SESSIONS OPTIONS:
|
|
|
5503
6736
|
--limit <n> Max sessions to show (default 20)
|
|
5504
6737
|
--min-size <n> Minimum prompts per session (default 1)
|
|
5505
6738
|
--no-cohesion Skip semantic cohesion computation
|
|
6739
|
+
--export <n> Export session N as markdown
|
|
5506
6740
|
|
|
5507
6741
|
CLUSTER OPTIONS:
|
|
5508
6742
|
-k <number> Number of clusters (default 5)
|
|
@@ -5538,7 +6772,46 @@ EXAMPLES:
|
|
|
5538
6772
|
ph config set background-analysis true
|
|
5539
6773
|
ph config set ollama-model llama3.2:latest
|
|
5540
6774
|
ph ollama-models List models available on the local Ollama instance
|
|
6775
|
+
ph models openrouter Discover OpenRouter models
|
|
6776
|
+
ph models mlx-serve Discover models on MLX-Serve (default Parmenide tunnel)
|
|
5541
6777
|
`;
|
|
6778
|
+
async function openBrowser(db, opts = {}) {
|
|
6779
|
+
let pendingRerun = null;
|
|
6780
|
+
process.stdout.write("\x1B[?1049h");
|
|
6781
|
+
try {
|
|
6782
|
+
const { waitUntilExit } = render(
|
|
6783
|
+
React8.createElement(BrowseApp, {
|
|
6784
|
+
db,
|
|
6785
|
+
...opts,
|
|
6786
|
+
onRerun: (tool, prompt) => {
|
|
6787
|
+
pendingRerun = { tool, prompt };
|
|
6788
|
+
}
|
|
6789
|
+
})
|
|
6790
|
+
);
|
|
6791
|
+
await waitUntilExit();
|
|
6792
|
+
} finally {
|
|
6793
|
+
process.stdout.write("\x1B[?1049l");
|
|
6794
|
+
}
|
|
6795
|
+
return pendingRerun;
|
|
6796
|
+
}
|
|
6797
|
+
async function runRerun(rerun, dbPath, cfg) {
|
|
6798
|
+
if (!rerun) return;
|
|
6799
|
+
if (rerun.tool.startsWith("wise:")) {
|
|
6800
|
+
const providerId = rerun.tool.slice("wise:".length);
|
|
6801
|
+
const setupError = getProviderSetupError(cfg, providerId);
|
|
6802
|
+
if (setupError) throw new Error(setupError);
|
|
6803
|
+
const provider = getProvider(cfg, providerId);
|
|
6804
|
+
if (!provider) throw new Error(`Cannot create ${providerId} provider from the saved configuration.`);
|
|
6805
|
+
const response = await provider.generate(rerun.prompt);
|
|
6806
|
+
process.stdout.write(`${response}
|
|
6807
|
+
`);
|
|
6808
|
+
await cmdLog(dbPath, cfg, ["--tool", `wise:${providerId}`, "--prompt", rerun.prompt, "--response", response]);
|
|
6809
|
+
return;
|
|
6810
|
+
}
|
|
6811
|
+
const realBin = resolveRealBinary(rerun.tool);
|
|
6812
|
+
const child = spawnSync2(realBin, [rerun.prompt], { stdio: "inherit" });
|
|
6813
|
+
process.exit(child.status ?? 0);
|
|
6814
|
+
}
|
|
5542
6815
|
async function main() {
|
|
5543
6816
|
const argv = process.argv.slice(2);
|
|
5544
6817
|
const cfg = load();
|
|
@@ -5549,28 +6822,9 @@ async function main() {
|
|
|
5549
6822
|
if (argv.length === 0) {
|
|
5550
6823
|
if (isTerminal()) {
|
|
5551
6824
|
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
|
-
}
|
|
6825
|
+
const rerun = await openBrowser(db);
|
|
6826
|
+
db.close();
|
|
6827
|
+
await runRerun(rerun, dbPath, cfg);
|
|
5574
6828
|
process.exit(0);
|
|
5575
6829
|
} else {
|
|
5576
6830
|
process.stdout.write(USAGE);
|
|
@@ -5599,39 +6853,21 @@ async function main() {
|
|
|
5599
6853
|
const query = positional.join(" ");
|
|
5600
6854
|
if (flags["i"] || flags["interactive"]) {
|
|
5601
6855
|
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
|
-
}
|
|
6856
|
+
const rerun = await openBrowser(db2, {
|
|
6857
|
+
initialTextFilter: query,
|
|
6858
|
+
initialFilters: {
|
|
6859
|
+
tool: flags["tool"],
|
|
6860
|
+
project: flags["project"],
|
|
6861
|
+
language: flags["language"],
|
|
6862
|
+
role: flags["role"],
|
|
6863
|
+
tag: flags["tag"],
|
|
6864
|
+
starred: Boolean(flags["starred"]),
|
|
6865
|
+
minQuality: flags["top"] ? 8 : flags["min-quality"] ? Number(flags["min-quality"]) : void 0,
|
|
6866
|
+
minRelevance: flags["min-relevance"] ? Number(flags["min-relevance"]) : void 0
|
|
6867
|
+
}
|
|
6868
|
+
});
|
|
6869
|
+
db2.close();
|
|
6870
|
+
await runRerun(rerun, dbPath, cfg);
|
|
5635
6871
|
break;
|
|
5636
6872
|
}
|
|
5637
6873
|
const db = new PhDB(dbPath);
|
|
@@ -5705,6 +6941,16 @@ async function main() {
|
|
|
5705
6941
|
db.close();
|
|
5706
6942
|
break;
|
|
5707
6943
|
}
|
|
6944
|
+
case "models": {
|
|
6945
|
+
await cmdModels(cfg, cmdArgs);
|
|
6946
|
+
break;
|
|
6947
|
+
}
|
|
6948
|
+
case "memory-migrate": {
|
|
6949
|
+
const db = new PhDB(dbPath);
|
|
6950
|
+
cmdMemoryMigrate(db);
|
|
6951
|
+
db.close();
|
|
6952
|
+
break;
|
|
6953
|
+
}
|
|
5708
6954
|
case "mcp": {
|
|
5709
6955
|
await runMCPServer();
|
|
5710
6956
|
break;
|
|
@@ -5743,7 +6989,7 @@ async function main() {
|
|
|
5743
6989
|
break;
|
|
5744
6990
|
}
|
|
5745
6991
|
case "config":
|
|
5746
|
-
cmdConfig(cmdArgs);
|
|
6992
|
+
await cmdConfig(cmdArgs);
|
|
5747
6993
|
break;
|
|
5748
6994
|
case "_bg-analyze": {
|
|
5749
6995
|
await cmdBackgroundAnalyze(dbPath, cmdArgs);
|
|
@@ -5751,28 +6997,9 @@ async function main() {
|
|
|
5751
6997
|
}
|
|
5752
6998
|
case "browse": {
|
|
5753
6999
|
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
|
-
}
|
|
7000
|
+
const rerun = await openBrowser(db);
|
|
7001
|
+
db.close();
|
|
7002
|
+
await runRerun(rerun, dbPath, cfg);
|
|
5776
7003
|
break;
|
|
5777
7004
|
}
|
|
5778
7005
|
case "chat": {
|