@gi4nks/ph 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +129 -6
- package/dist/cli.js +278 -1
- package/dist/cli.js.map +1 -1
- package/docs/ph-manual.md +160 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.5.0](https://github.com/gi4nks/ph/compare/v1.4.0...v1.5.0) (2026-05-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add timeline CLI command and two new MCP tools (get_project_timeline, check_project_knowledge) ([12d667c](https://github.com/gi4nks/ph/commit/12d667c69e35f4aff31a2e13018a00949587fb55))
|
|
7
|
+
|
|
1
8
|
# [1.4.0](https://github.com/gi4nks/ph/compare/v1.3.2...v1.4.0) (2026-05-16)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
|
+
### From npm
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g @gi4nks/ph
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### From source
|
|
24
|
+
|
|
17
25
|
```bash
|
|
18
|
-
# Clone and local build
|
|
19
26
|
git clone git@github.com:gi4nks/ph.git
|
|
20
27
|
cd ph
|
|
21
28
|
npm install
|
|
@@ -23,11 +30,6 @@ make build
|
|
|
23
30
|
make install
|
|
24
31
|
```
|
|
25
32
|
|
|
26
|
-
Or install from npm:
|
|
27
|
-
```bash
|
|
28
|
-
npm install -g @gi4nks/ph
|
|
29
|
-
```
|
|
30
|
-
|
|
31
33
|
## Quick Start
|
|
32
34
|
|
|
33
35
|
```bash
|
|
@@ -63,6 +65,127 @@ ph config set remote-url http://server:3001 # remote sync target
|
|
|
63
65
|
|
|
64
66
|
Environment variable `PH_REMOTE_URL` takes precedence over config.
|
|
65
67
|
|
|
68
|
+
| Option | Default | Description |
|
|
69
|
+
|--------|---------|-------------|
|
|
70
|
+
| `analyze-provider` | `ollama` | LLM provider for analysis (`ollama` or `gemini`) |
|
|
71
|
+
| `gemini-api-key` | — | API key for Gemini provider |
|
|
72
|
+
| `ollama-url` | `http://localhost:11434` | Ollama server URL |
|
|
73
|
+
| `ollama-model` | `llama3.1:latest` | Ollama model for analysis |
|
|
74
|
+
| `ollama-embed-model` | `nomic-embed-text-v2-moe` | Ollama model for embeddings |
|
|
75
|
+
| `background-analysis` | `false` | Auto-analyze after each `ph log` |
|
|
76
|
+
| `remote-url` | — | HTTP URL of remote ph server |
|
|
77
|
+
| `remote-api-key` | — | Optional API key for remote server |
|
|
78
|
+
| `db-path` | `~/.prompt_history.db` | Custom database path |
|
|
79
|
+
| `filter-min-length` | `15` | Ignore prompts shorter than N chars |
|
|
80
|
+
| `filter-min-relevance` | `3` | Minimum relevance score (0–10) |
|
|
81
|
+
|
|
82
|
+
## Remote Server Setup
|
|
83
|
+
|
|
84
|
+
### Ubuntu (systemd)
|
|
85
|
+
|
|
86
|
+
Install ph on your remote server:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
sudo npm install -g @gi4nks/ph
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Create `/etc/systemd/system/ph.service`:
|
|
93
|
+
|
|
94
|
+
```ini
|
|
95
|
+
[Unit]
|
|
96
|
+
Description=ph remote sync server
|
|
97
|
+
After=network.target
|
|
98
|
+
|
|
99
|
+
[Service]
|
|
100
|
+
Type=simple
|
|
101
|
+
ExecStart=<path-to-ph> server --port 3001 --host 0.0.0.0
|
|
102
|
+
Restart=always
|
|
103
|
+
RestartSec=5
|
|
104
|
+
User=<your-user>
|
|
105
|
+
Environment=NODE_ENV=production
|
|
106
|
+
StandardOutput=append:/var/log/ph-server.log
|
|
107
|
+
StandardError=append:/var/log/ph-server.log
|
|
108
|
+
|
|
109
|
+
[Install]
|
|
110
|
+
WantedBy=multi-user.target
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Find `<path-to-ph>` with `which ph` on the server. Start the service:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
sudo systemctl daemon-reload
|
|
117
|
+
sudo systemctl enable --now ph.service
|
|
118
|
+
sudo ufw allow 3001/tcp # if firewall is active
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### macOS (launchd)
|
|
122
|
+
|
|
123
|
+
Create `~/Library/LaunchAgents/com.gi4nks.ph.plist`:
|
|
124
|
+
|
|
125
|
+
```xml
|
|
126
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
127
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
128
|
+
<plist version="1.0">
|
|
129
|
+
<dict>
|
|
130
|
+
<key>Label</key>
|
|
131
|
+
<string>com.gi4nks.ph</string>
|
|
132
|
+
<key>ProgramArguments</key>
|
|
133
|
+
<array>
|
|
134
|
+
<string>/usr/local/bin/ph</string>
|
|
135
|
+
<string>server</string>
|
|
136
|
+
<string>--port</string>
|
|
137
|
+
<string>3001</string>
|
|
138
|
+
<string>--host</string>
|
|
139
|
+
<string>0.0.0.0</string>
|
|
140
|
+
</array>
|
|
141
|
+
<key>RunAtLoad</key>
|
|
142
|
+
<true/>
|
|
143
|
+
<key>KeepAlive</key>
|
|
144
|
+
<true/>
|
|
145
|
+
<key>StandardOutPath</key>
|
|
146
|
+
<string>/tmp/ph-server.log</string>
|
|
147
|
+
<key>StandardErrorPath</key>
|
|
148
|
+
<string>/tmp/ph-server.err</string>
|
|
149
|
+
</dict>
|
|
150
|
+
</plist>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
launchctl load ~/Library/LaunchAgents/com.gi4nks.ph.plist
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Client configuration
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Set remote URL (config or env)
|
|
161
|
+
ph config set remote-url http://your-server:3001
|
|
162
|
+
# export PH_REMOTE_URL=http://your-server:3001
|
|
163
|
+
|
|
164
|
+
# Sync
|
|
165
|
+
ph remote push # send local prompts to server
|
|
166
|
+
ph remote pull # fetch server prompts locally
|
|
167
|
+
ph remote status # check sync state
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## How Releases Work
|
|
171
|
+
|
|
172
|
+
Releases are automated via **semantic-release** on push to `main`:
|
|
173
|
+
|
|
174
|
+
1. Push conventional commits (`feat:`, `fix:`, `chore:`, etc.) to `main`
|
|
175
|
+
2. GitHub Actions runs `npx semantic-release`
|
|
176
|
+
3. Semantic-release analyzes commits since last release
|
|
177
|
+
4. Bumps version automatically (major/minor/patch)
|
|
178
|
+
5. Generates `CHANGELOG.md`
|
|
179
|
+
6. Creates a git tag
|
|
180
|
+
7. Publishes to npm with **OIDC trusted publishing** (provenance attestation)
|
|
181
|
+
8. Creates a GitHub Release
|
|
182
|
+
|
|
183
|
+
The published package is signed with provenance — you can verify it with:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npm audit signatures
|
|
187
|
+
```
|
|
188
|
+
|
|
66
189
|
## Storage
|
|
67
190
|
|
|
68
191
|
| Item | Default Path | Description |
|
package/dist/cli.js
CHANGED
|
@@ -485,6 +485,17 @@ var PhDB = class {
|
|
|
485
485
|
last_accessed: row.last_accessed
|
|
486
486
|
};
|
|
487
487
|
}
|
|
488
|
+
getAllPromptsByProject(project) {
|
|
489
|
+
return this.db.prepare(`
|
|
490
|
+
SELECT * FROM prompts
|
|
491
|
+
WHERE json_extract(metadata, '$.project') = ?
|
|
492
|
+
ORDER BY timestamp ASC
|
|
493
|
+
`).all(project);
|
|
494
|
+
}
|
|
495
|
+
getAllMemoriesByProject(project) {
|
|
496
|
+
const rows = this.db.prepare("SELECT * FROM memories WHERE project = ? ORDER BY created_at ASC").all(project);
|
|
497
|
+
return rows.map((r) => this.hydrateMemory(r));
|
|
498
|
+
}
|
|
488
499
|
close() {
|
|
489
500
|
this.db.close();
|
|
490
501
|
}
|
|
@@ -886,6 +897,30 @@ async function runMCPServer() {
|
|
|
886
897
|
},
|
|
887
898
|
required: ["query"]
|
|
888
899
|
}
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
name: "get_project_timeline",
|
|
903
|
+
description: "Get full chronological timeline of a project: all prompts, analysis summaries, key insights, and technical decisions in sequence.",
|
|
904
|
+
inputSchema: {
|
|
905
|
+
type: "object",
|
|
906
|
+
properties: {
|
|
907
|
+
project: { type: "string", description: "The project name" }
|
|
908
|
+
},
|
|
909
|
+
required: ["project"]
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
name: "check_project_knowledge",
|
|
914
|
+
description: "Before implementing a feature, check if it already exists in project history. Searches memories and prompts for previous implementations, discussions, and technical decisions related to a task.",
|
|
915
|
+
inputSchema: {
|
|
916
|
+
type: "object",
|
|
917
|
+
properties: {
|
|
918
|
+
project: { type: "string", description: "The project name" },
|
|
919
|
+
task: { type: "string", description: "Description of the feature or task to check" },
|
|
920
|
+
limit: { type: "number", description: "Max results per search (default 5)", default: 5 }
|
|
921
|
+
},
|
|
922
|
+
required: ["project", "task"]
|
|
923
|
+
}
|
|
889
924
|
}
|
|
890
925
|
]
|
|
891
926
|
};
|
|
@@ -1045,6 +1080,144 @@ async function runMCPServer() {
|
|
|
1045
1080
|
content: [{ type: "text", text: formatPromptList(filtered, query) }]
|
|
1046
1081
|
};
|
|
1047
1082
|
}
|
|
1083
|
+
if (name === "get_project_timeline") {
|
|
1084
|
+
const { project } = z.object({
|
|
1085
|
+
project: z.string()
|
|
1086
|
+
}).parse(args);
|
|
1087
|
+
const prompts = db.getAllPromptsByProject(project);
|
|
1088
|
+
const memories = db.getAllMemoriesByProject(project);
|
|
1089
|
+
if (prompts.length === 0 && memories.length === 0) {
|
|
1090
|
+
return {
|
|
1091
|
+
content: [{ type: "text", text: `No history found for project "${project}".` }]
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
const events = [];
|
|
1095
|
+
for (const p of prompts) {
|
|
1096
|
+
let meta = {};
|
|
1097
|
+
try {
|
|
1098
|
+
meta = JSON.parse(p.metadata);
|
|
1099
|
+
} catch {
|
|
1100
|
+
}
|
|
1101
|
+
events.push({
|
|
1102
|
+
timestamp: p.timestamp,
|
|
1103
|
+
text: [
|
|
1104
|
+
`### #${p.id} \u2014 ${p.tool}`,
|
|
1105
|
+
meta.role ? `Role: ${meta.role}` : "",
|
|
1106
|
+
meta.title ? `Title: ${meta.title}` : "",
|
|
1107
|
+
meta.tags?.length ? `Tags: ${meta.tags.join(", ")}` : "",
|
|
1108
|
+
meta.summary ? `Summary: ${meta.summary}` : "",
|
|
1109
|
+
`Prompt: ${p.prompt.slice(0, 500)}${p.prompt.length > 500 ? "..." : ""}`,
|
|
1110
|
+
p.response ? `Response: ${p.response.slice(0, 300)}${p.response.length > 300 ? "..." : ""}` : "",
|
|
1111
|
+
meta.key_insights?.length ? `Insights:
|
|
1112
|
+
${meta.key_insights.map((i) => `- ${i}`).join("\n")}` : ""
|
|
1113
|
+
].filter(Boolean).join("\n")
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
for (const m of memories) {
|
|
1117
|
+
events.push({
|
|
1118
|
+
timestamp: m.created_at,
|
|
1119
|
+
text: [
|
|
1120
|
+
`\u{1F4CC} Memory #${m.id}`,
|
|
1121
|
+
m.prompt_ids.length > 0 ? `From prompts: #${m.prompt_ids.join(", #")}` : "",
|
|
1122
|
+
m.summary ? `Summary: ${m.summary}` : "",
|
|
1123
|
+
m.key_insights.length > 0 ? `Key Insights:
|
|
1124
|
+
${m.key_insights.map((i) => `- ${i}`).join("\n")}` : "",
|
|
1125
|
+
m.technical_decisions.length > 0 ? `Technical Decisions:
|
|
1126
|
+
${m.technical_decisions.map((d) => `- ${d}`).join("\n")}` : ""
|
|
1127
|
+
].filter(Boolean).join("\n")
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
events.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
1131
|
+
const header = `# Timeline: ${project}
|
|
1132
|
+
|
|
1133
|
+
${prompts.length} prompts \xB7 ${memories.length} memory entries
|
|
1134
|
+
|
|
1135
|
+
`;
|
|
1136
|
+
const body = events.map((e) => {
|
|
1137
|
+
const date = new Date(e.timestamp).toLocaleDateString("en-CA");
|
|
1138
|
+
const time = new Date(e.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1139
|
+
return `## ${date} ${time}
|
|
1140
|
+
|
|
1141
|
+
${e.text}
|
|
1142
|
+
`;
|
|
1143
|
+
}).join("---\n");
|
|
1144
|
+
return {
|
|
1145
|
+
content: [{ type: "text", text: header + body }]
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
if (name === "check_project_knowledge") {
|
|
1149
|
+
const { project, task, limit = 5 } = z.object({
|
|
1150
|
+
project: z.string(),
|
|
1151
|
+
task: z.string(),
|
|
1152
|
+
limit: z.number().optional()
|
|
1153
|
+
}).parse(args);
|
|
1154
|
+
const parts = [];
|
|
1155
|
+
parts.push(`# Knowledge Check: "${task}" in ${project}
|
|
1156
|
+
`);
|
|
1157
|
+
const memories = db.searchMemories(project, limit);
|
|
1158
|
+
const relevantMemories = memories.filter(
|
|
1159
|
+
(m) => m.summary.toLowerCase().includes(task.toLowerCase()) || m.key_insights.some((k) => k.toLowerCase().includes(task.toLowerCase())) || m.technical_decisions.some((d) => d.toLowerCase().includes(task.toLowerCase()))
|
|
1160
|
+
);
|
|
1161
|
+
if (relevantMemories.length > 0) {
|
|
1162
|
+
parts.push(`## Found in Project Memories
|
|
1163
|
+
`);
|
|
1164
|
+
for (const m of relevantMemories) {
|
|
1165
|
+
parts.push(`### Memory #${m.id}`);
|
|
1166
|
+
if (m.summary) parts.push(`Summary: ${m.summary}`);
|
|
1167
|
+
if (m.key_insights.length > 0) {
|
|
1168
|
+
parts.push(`Key Insights:
|
|
1169
|
+
${m.key_insights.map((i) => `- ${i}`).join("\n")}`);
|
|
1170
|
+
}
|
|
1171
|
+
if (m.technical_decisions.length > 0) {
|
|
1172
|
+
parts.push(`Technical Decisions:
|
|
1173
|
+
${m.technical_decisions.map((d) => `- ${d}`).join("\n")}`);
|
|
1174
|
+
}
|
|
1175
|
+
parts.push("");
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
|
|
1179
|
+
const embedModel = cfg.ollamaEmbedModel ?? "nomic-embed-text-v2-moe";
|
|
1180
|
+
try {
|
|
1181
|
+
const [queryVec] = await getEmbeddings([`${project}: ${task}`], ollamaUrl, embedModel, 1);
|
|
1182
|
+
if (queryVec) {
|
|
1183
|
+
const results = db.searchSemantic(queryVec, limit * 3);
|
|
1184
|
+
const projectPrompts = results.filter((e) => {
|
|
1185
|
+
try {
|
|
1186
|
+
const meta = JSON.parse(e.metadata);
|
|
1187
|
+
return meta.project === project;
|
|
1188
|
+
} catch {
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
}).slice(0, limit);
|
|
1192
|
+
if (projectPrompts.length > 0) {
|
|
1193
|
+
parts.push(`## Related Prompts
|
|
1194
|
+
`);
|
|
1195
|
+
for (const p of projectPrompts) {
|
|
1196
|
+
let meta = {};
|
|
1197
|
+
try {
|
|
1198
|
+
meta = JSON.parse(p.metadata);
|
|
1199
|
+
} catch {
|
|
1200
|
+
}
|
|
1201
|
+
parts.push(`### #${p.id} \u2014 ${p.tool} (${new Date(p.timestamp).toLocaleDateString()})`);
|
|
1202
|
+
if (meta.role) parts.push(`Role: ${meta.role}`);
|
|
1203
|
+
if (meta.summary) parts.push(`Summary: ${meta.summary}`);
|
|
1204
|
+
parts.push(`Prompt: ${p.prompt.slice(0, 400)}${p.prompt.length > 400 ? "..." : ""}`);
|
|
1205
|
+
parts.push("");
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
} catch {
|
|
1210
|
+
parts.push("(Semantic search unavailable \u2014 Ollama may not be running)\n");
|
|
1211
|
+
}
|
|
1212
|
+
if (relevantMemories.length === 0 && !parts.some((p) => p.startsWith("## Related Prompts"))) {
|
|
1213
|
+
parts.push("No existing knowledge found for this task. This appears to be new work.\n");
|
|
1214
|
+
}
|
|
1215
|
+
parts.push("---\n");
|
|
1216
|
+
parts.push("**Recommendation**: Review the above before implementing. If a previous implementation exists, consider reusing or adapting it.\n");
|
|
1217
|
+
return {
|
|
1218
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1048
1221
|
throw new Error(`Unknown tool: ${name}`);
|
|
1049
1222
|
} catch (error) {
|
|
1050
1223
|
return {
|
|
@@ -1479,7 +1652,7 @@ import { AppHeader, THEMES } from "@gi4nks/ink";
|
|
|
1479
1652
|
// package.json
|
|
1480
1653
|
var package_default = {
|
|
1481
1654
|
name: "@gi4nks/ph",
|
|
1482
|
-
version: "
|
|
1655
|
+
version: "1.4.0",
|
|
1483
1656
|
description: "Transparent CLI wrapper that captures and indexes AI prompts",
|
|
1484
1657
|
type: "module",
|
|
1485
1658
|
license: "MIT",
|
|
@@ -5022,6 +5195,103 @@ Deleted ${deleted} prompts, freed ~${(totalToFree / 1024 / 1024).toFixed(1)} MB`
|
|
|
5022
5195
|
console.log("Database vacuumed.");
|
|
5023
5196
|
}
|
|
5024
5197
|
|
|
5198
|
+
// src/commands/timeline.ts
|
|
5199
|
+
async function cmdTimeline(db, cfg, args) {
|
|
5200
|
+
const { flags, positional } = parseFlags(args);
|
|
5201
|
+
const project = flags["project"] || positional.join(" ") || detectProject(process.cwd()).projectName;
|
|
5202
|
+
if (!project) {
|
|
5203
|
+
process.stderr.write("ph: could not detect project. Use --project <name> or pass a project name.\n");
|
|
5204
|
+
process.exit(1);
|
|
5205
|
+
}
|
|
5206
|
+
const prompts = db.getAllPromptsByProject(project);
|
|
5207
|
+
const memories = db.getAllMemoriesByProject(project);
|
|
5208
|
+
if (prompts.length === 0 && memories.length === 0) {
|
|
5209
|
+
process.stdout.write(`No history found for project "${project}".
|
|
5210
|
+
`);
|
|
5211
|
+
return;
|
|
5212
|
+
}
|
|
5213
|
+
const output = [];
|
|
5214
|
+
output.push(`# Timeline: ${project}
|
|
5215
|
+
`);
|
|
5216
|
+
output.push(`**${prompts.length} prompts** \xB7 **${memories.length} memory entries**
|
|
5217
|
+
`);
|
|
5218
|
+
const events = [
|
|
5219
|
+
...prompts.map((p) => ({ type: "prompt", timestamp: p.timestamp, prompt: p })),
|
|
5220
|
+
...memories.map((m) => ({ type: "memory", timestamp: m.created_at, memory: m }))
|
|
5221
|
+
];
|
|
5222
|
+
events.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
5223
|
+
let currentDate = "";
|
|
5224
|
+
for (const event of events) {
|
|
5225
|
+
const date = new Date(event.timestamp).toLocaleDateString("en-CA");
|
|
5226
|
+
if (date !== currentDate) {
|
|
5227
|
+
currentDate = date;
|
|
5228
|
+
output.push(`
|
|
5229
|
+
## ${date}
|
|
5230
|
+
`);
|
|
5231
|
+
}
|
|
5232
|
+
if (event.type === "prompt" && event.prompt) {
|
|
5233
|
+
output.push(formatPromptEntry(event.prompt));
|
|
5234
|
+
}
|
|
5235
|
+
if (event.type === "memory" && event.memory) {
|
|
5236
|
+
output.push(formatMemoryEntry(event.memory));
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
process.stdout.write(output.join("\n"));
|
|
5240
|
+
}
|
|
5241
|
+
function formatPromptEntry(e) {
|
|
5242
|
+
let meta = {};
|
|
5243
|
+
try {
|
|
5244
|
+
meta = JSON.parse(e.metadata);
|
|
5245
|
+
} catch {
|
|
5246
|
+
}
|
|
5247
|
+
const time = new Date(e.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
5248
|
+
const lines = [];
|
|
5249
|
+
lines.push(`### #${e.id} \u2014 ${e.tool} \u2014 ${time}`);
|
|
5250
|
+
if (meta.role) lines.push(`**Role**: ${meta.role}`);
|
|
5251
|
+
if (meta.title) lines.push(`**Title**: ${meta.title}`);
|
|
5252
|
+
if (meta.tags?.length) lines.push(`**Tags**: ${meta.tags.join(", ")}`);
|
|
5253
|
+
if (meta.summary) lines.push(`**Summary**: ${meta.summary}`);
|
|
5254
|
+
if (meta.relevance !== void 0) lines.push(`**Relevance**: ${meta.relevance}/10`);
|
|
5255
|
+
lines.push("");
|
|
5256
|
+
lines.push("**Prompt**:");
|
|
5257
|
+
lines.push("```");
|
|
5258
|
+
lines.push(e.prompt.slice(0, 500) + (e.prompt.length > 500 ? "\n..." : ""));
|
|
5259
|
+
lines.push("```");
|
|
5260
|
+
if (e.response) {
|
|
5261
|
+
lines.push("");
|
|
5262
|
+
lines.push("**Response**:");
|
|
5263
|
+
lines.push("```");
|
|
5264
|
+
lines.push(e.response.slice(0, 300) + (e.response.length > 300 ? "\n..." : ""));
|
|
5265
|
+
lines.push("```");
|
|
5266
|
+
}
|
|
5267
|
+
if (meta.key_insights?.length) {
|
|
5268
|
+
lines.push("");
|
|
5269
|
+
lines.push("**Insights**:");
|
|
5270
|
+
for (const i of meta.key_insights) lines.push(`- ${i}`);
|
|
5271
|
+
}
|
|
5272
|
+
lines.push("---\n");
|
|
5273
|
+
return lines.join("\n");
|
|
5274
|
+
}
|
|
5275
|
+
function formatMemoryEntry(m) {
|
|
5276
|
+
const time = new Date(m.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
5277
|
+
const lines = [];
|
|
5278
|
+
lines.push(`> \u{1F4CC} **Memory #${m.id} \u2014 ${time}**`);
|
|
5279
|
+
if (m.prompt_ids.length > 0) {
|
|
5280
|
+
lines.push(`> _Derived from prompts: #${m.prompt_ids.join(", #")}_`);
|
|
5281
|
+
}
|
|
5282
|
+
if (m.summary) lines.push(`> **Summary**: ${m.summary}`);
|
|
5283
|
+
if (m.key_insights.length > 0) {
|
|
5284
|
+
lines.push(`> **Insights**:`);
|
|
5285
|
+
for (const i of m.key_insights) lines.push(`> - ${i}`);
|
|
5286
|
+
}
|
|
5287
|
+
if (m.technical_decisions.length > 0) {
|
|
5288
|
+
lines.push(`> **Technical Decisions**:`);
|
|
5289
|
+
for (const d of m.technical_decisions) lines.push(`> - ${d}`);
|
|
5290
|
+
}
|
|
5291
|
+
lines.push("---\n");
|
|
5292
|
+
return lines.join("\n");
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5025
5295
|
// src/commands/wrap.ts
|
|
5026
5296
|
import path8 from "path";
|
|
5027
5297
|
import os9 from "os";
|
|
@@ -5190,6 +5460,7 @@ USAGE:
|
|
|
5190
5460
|
ph analyze [--limit n] [--force] [--prune] [--dry-run] Analyze prompts with LLM
|
|
5191
5461
|
ph mcp Start MCP server (Stdio)
|
|
5192
5462
|
ph server [--port 3001] Start HTTP REST server for remote sync
|
|
5463
|
+
ph timeline [project] Show full project history with prompts and memories
|
|
5193
5464
|
ph remote push|pull|status Sync prompts with remote ph server
|
|
5194
5465
|
ph cleanup [--dry-run] [--min-length N] [--min-score N] Remove useless prompts
|
|
5195
5466
|
ph cleanup-reusability [--dry-run] [--threshold 0.7] [--force] Cleanup based on reusability
|
|
@@ -5508,6 +5779,12 @@ async function main() {
|
|
|
5508
5779
|
await cmdChat(dbPath, cfg, cmdArgs);
|
|
5509
5780
|
break;
|
|
5510
5781
|
}
|
|
5782
|
+
case "timeline": {
|
|
5783
|
+
const db = new PhDB(dbPath);
|
|
5784
|
+
await cmdTimeline(db, cfg, cmdArgs);
|
|
5785
|
+
db.close();
|
|
5786
|
+
break;
|
|
5787
|
+
}
|
|
5511
5788
|
case "ollama-models": {
|
|
5512
5789
|
const ollamaUrl = cfg.ollamaUrl ?? "http://localhost:11434";
|
|
5513
5790
|
try {
|