@hienlh/ppm 0.9.42 → 0.9.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/web/assets/{chat-tab-BSQOFDle.js → chat-tab-DvNEQYEe.js} +1 -1
  3. package/dist/web/assets/{code-editor-eDYb_XML.js → code-editor-CoT017Ah.js} +1 -1
  4. package/dist/web/assets/{database-viewer-nP78XqEF.js → database-viewer-C3wK7cDk.js} +1 -1
  5. package/dist/web/assets/{diff-viewer-DTMtBxHM.js → diff-viewer-D0tuen4I.js} +1 -1
  6. package/dist/web/assets/{extension-webview-DzWz--CI.js → extension-webview-Ba5aeo9r.js} +1 -1
  7. package/dist/web/assets/{git-graph-D_6NTVVT.js → git-graph-BnJrVPxJ.js} +1 -1
  8. package/dist/web/assets/index-DUQgLj0D.js +30 -0
  9. package/dist/web/assets/keybindings-store-CkGFjxkX.js +1 -0
  10. package/dist/web/assets/{markdown-renderer-CxJg37If.js → markdown-renderer-BuGSrE3y.js} +1 -1
  11. package/dist/web/assets/{port-forwarding-tab-DBBJ3z8x.js → port-forwarding-tab-DsbrWNUP.js} +1 -1
  12. package/dist/web/assets/{postgres-viewer-CQ3coJ1p.js → postgres-viewer-Bh6YmZPq.js} +1 -1
  13. package/dist/web/assets/{settings-tab-CE8H5NiY.js → settings-tab-BnzFtexC.js} +1 -1
  14. package/dist/web/assets/{sqlite-viewer-Ccm-un47.js → sqlite-viewer-Cu3_hf07.js} +1 -1
  15. package/dist/web/assets/{terminal-tab-DnlFNbY6.js → terminal-tab-fnZvscaH.js} +1 -1
  16. package/dist/web/assets/{use-monaco-theme-hwg4tMW2.js → use-monaco-theme-BdcKAZ69.js} +1 -1
  17. package/dist/web/index.html +1 -1
  18. package/dist/web/sw.js +1 -1
  19. package/docs/streaming-input-guide.md +267 -0
  20. package/package.json +1 -1
  21. package/snapshot-state.md +1526 -0
  22. package/src/server/routes/settings.ts +20 -1
  23. package/src/services/ppmbot/ppmbot-service.ts +96 -12
  24. package/src/services/ppmbot/ppmbot-telegram.ts +3 -2
  25. package/src/web/components/settings/ppmbot-settings-section.tsx +84 -2
  26. package/test-session-ops.mjs +444 -0
  27. package/test-tokens.mjs +212 -0
  28. package/dist/web/assets/index-D48IQVYU.js +0 -30
  29. package/dist/web/assets/keybindings-store-BaWyhjXJ.js +0 -1
@@ -1,6 +1,6 @@
1
1
  import { Hono } from "hono";
2
2
  import { configService } from "../../services/config.service.ts";
3
- import { getConfigValue, setConfigValue, listPairedChats, getPairingByCode, approvePairing, revokePairing } from "../../services/db.service.ts";
3
+ import { getConfigValue, setConfigValue, listPairedChats, getPairingByCode, approvePairing, revokePairing, getPPMBotMemories, getDb } from "../../services/db.service.ts";
4
4
  import {
5
5
  validateAIProviderConfig,
6
6
  validateDefaultProvider,
@@ -382,3 +382,22 @@ settingsRoutes.delete("/clawbot/paired/:chatId", (c) => {
382
382
  revokePairing(c.req.param("chatId"));
383
383
  return c.json(ok({ revoked: true }));
384
384
  });
385
+
386
+ /** GET /settings/clawbot/memories?project=xxx — list memories for a project */
387
+ settingsRoutes.get("/clawbot/memories", (c) => {
388
+ const project = c.req.query("project") || "_global";
389
+ const memories = getPPMBotMemories(project, 50);
390
+ return c.json(ok(memories));
391
+ });
392
+
393
+ /** DELETE /settings/clawbot/memories/:id — delete a specific memory */
394
+ settingsRoutes.delete("/clawbot/memories/:id", (c) => {
395
+ const id = Number(c.req.param("id"));
396
+ if (!id) return c.json(err("Invalid memory ID"), 400);
397
+ try {
398
+ getDb().query("DELETE FROM clawbot_memories WHERE id = ?").run(id);
399
+ return c.json(ok({ deleted: id }));
400
+ } catch (e) {
401
+ return c.json(err((e as Error).message), 500);
402
+ }
403
+ });
@@ -5,6 +5,8 @@ import {
5
5
  getPairingByChatId,
6
6
  createPairingRequest,
7
7
  approvePairing,
8
+ getSessionTitles,
9
+ getApprovedPairedChats,
8
10
  } from "../db.service.ts";
9
11
  import { PPMBotTelegram } from "./ppmbot-telegram.ts";
10
12
  import { PPMBotSessionManager } from "./ppmbot-session.ts";
@@ -39,6 +41,9 @@ class PPMBotService {
39
41
  /** Chat IDs that just received identity onboarding prompt */
40
42
  private identityPending = new Set<string>();
41
43
 
44
+ /** Chat IDs where we've already checked for identity (once per session) */
45
+ private hasCheckedIdentity = new Set<string>();
46
+
42
47
  /** Message count per session for periodic memory save */
43
48
  private messageCount = new Map<string, number>();
44
49
 
@@ -70,6 +75,9 @@ class PPMBotService {
70
75
  // Start polling (non-blocking)
71
76
  this.telegram.startPolling((update) => this.handleUpdate(update));
72
77
 
78
+ // Check if this is a restart and notify users
79
+ await this.checkRestartNotification();
80
+
73
81
  console.log("[ppmbot] Started");
74
82
  } catch (err) {
75
83
  console.error("[ppmbot] Start failed:", (err as Error).message);
@@ -87,6 +95,7 @@ class PPMBotService {
87
95
  this.processing.clear();
88
96
  this.messageQueue.clear();
89
97
  this.identityPending.clear();
98
+ this.hasCheckedIdentity.clear();
90
99
  this.messageCount.clear();
91
100
 
92
101
  console.log("[ppmbot] Stopped");
@@ -172,6 +181,7 @@ class PPMBotService {
172
181
  case "memory": await this.cmdMemory(chatId); break;
173
182
  case "forget": await this.cmdForget(chatId, cmd.args); break;
174
183
  case "remember": await this.cmdRemember(chatId, cmd.args); break;
184
+ case "restart": await this.cmdRestart(chatId); break;
175
185
  case "help": await this.cmdHelp(chatId); break;
176
186
  default: await tg.sendMessage(Number(chatId), `Unknown command: /${cmd.command}`);
177
187
  }
@@ -201,8 +211,11 @@ class PPMBotService {
201
211
  await this.telegram!.sendMessage(Number(chatId), text);
202
212
 
203
213
  // Identity onboarding: if no identity memories exist, ask user
204
- const identityMemories = this.memory.recall("_global", "user identity name role");
205
- if (identityMemories.length === 0) {
214
+ const globalMemories = this.memory.getSummary("_global", 50);
215
+ const hasIdentity = globalMemories.some((m) =>
216
+ m.category === "preference" && /identity|name|role/i.test(m.content),
217
+ );
218
+ if (!hasIdentity) {
206
219
  this.identityPending.add(chatId);
207
220
  await this.telegram!.sendMessage(
208
221
  Number(chatId),
@@ -220,11 +233,20 @@ class PPMBotService {
220
233
  private async cmdProject(chatId: string, args: string): Promise<void> {
221
234
  if (!args) {
222
235
  const active = this.sessions.getActiveSession(chatId);
223
- const current = active?.projectName ?? "(none)";
224
- await this.telegram!.sendMessage(
225
- Number(chatId),
226
- `Current project: <b>${escapeHtml(current)}</b>\n\nUsage: /project &lt;name&gt;`,
227
- );
236
+ const current = active?.projectName ?? "";
237
+ const projects = this.sessions.getProjectNames();
238
+ let text = "<b>Projects</b>\n\n";
239
+ if (projects.length === 0) {
240
+ text += "No projects configured.\nUsing default: <code>~/.ppm/bot/</code>\n";
241
+ } else {
242
+ for (const name of projects) {
243
+ const marker = name === current ? " ✓" : "";
244
+ text += `• <code>${escapeHtml(name)}</code>${marker}\n`;
245
+ }
246
+ }
247
+ text += `\nCurrent: <b>${escapeHtml(current || "bot (default)")}</b>`;
248
+ text += "\nSwitch: /project &lt;name&gt;";
249
+ await this.telegram!.sendMessage(Number(chatId), text);
228
250
  return;
229
251
  }
230
252
 
@@ -254,13 +276,23 @@ class PPMBotService {
254
276
  await this.telegram!.sendMessage(Number(chatId), "No recent sessions.");
255
277
  return;
256
278
  }
279
+
280
+ // Fetch titles for all sessions
281
+ const titles = getSessionTitles(sessions.map((s) => s.session_id));
282
+
257
283
  let text = "<b>Recent Sessions</b>\n\n";
258
284
  sessions.forEach((s, i) => {
259
285
  const active = s.is_active ? " ⬤" : "";
260
- const date = new Date(s.last_message_at * 1000).toLocaleDateString();
261
- text += `${i + 1}. <code>${escapeHtml(s.project_name)}</code> ${date}${active}\n`;
286
+ const title = titles[s.session_id]?.replace(/^\[PPM\]\s*/, "") || "";
287
+ const preview = title ? ` — ${escapeHtml(title.slice(0, 40))}` : "";
288
+ const date = new Date(s.last_message_at * 1000).toLocaleString(undefined, {
289
+ month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
290
+ });
291
+ const sid = s.session_id.slice(0, 8);
292
+ text += `${i + 1}. <b>${escapeHtml(s.project_name)}</b>${preview}${active}\n`;
293
+ text += ` <code>${sid}</code> · ${date}\n\n`;
262
294
  });
263
- text += "\nResume: /resume &lt;number&gt;";
295
+ text += "Resume: /resume &lt;number&gt;";
264
296
  await this.telegram!.sendMessage(Number(chatId), text);
265
297
  }
266
298
 
@@ -338,11 +370,54 @@ class PPMBotService {
338
370
  await this.telegram!.sendMessage(Number(chatId), "Remembered ✓");
339
371
  }
340
372
 
373
+ private async cmdRestart(chatId: string): Promise<void> {
374
+ await this.telegram!.sendMessage(Number(chatId), "🔄 Restarting PPM...");
375
+
376
+ // Schedule restart after a short delay so the response is sent
377
+ setTimeout(async () => {
378
+ const { join } = await import("node:path");
379
+ const { writeFileSync } = await import("node:fs");
380
+ const { homedir } = await import("node:os");
381
+
382
+ const approvedChats = getApprovedPairedChats();
383
+ const chatIds = approvedChats.map((c) => c.telegram_chat_id);
384
+
385
+ // Write restart marker so we can notify after restart
386
+ const markerPath = join(homedir(), ".ppm", "restart-notify.json");
387
+ writeFileSync(markerPath, JSON.stringify({ chatIds, ts: Date.now() }));
388
+
389
+ console.log("[ppmbot] Restart requested via Telegram, exiting...");
390
+ process.exit(0);
391
+ }, 500);
392
+ }
393
+
394
+ /** Check for restart notification marker and send notification */
395
+ async checkRestartNotification(): Promise<void> {
396
+ try {
397
+ const { join } = await import("node:path");
398
+ const { existsSync, readFileSync, unlinkSync } = await import("node:fs");
399
+ const { homedir } = await import("node:os");
400
+
401
+ const markerPath = join(homedir(), ".ppm", "restart-notify.json");
402
+ if (!existsSync(markerPath)) return;
403
+
404
+ const data = JSON.parse(readFileSync(markerPath, "utf-8"));
405
+ unlinkSync(markerPath);
406
+
407
+ // Only notify if restart was recent (< 60s)
408
+ if (Date.now() - data.ts > 60_000) return;
409
+
410
+ for (const cid of data.chatIds) {
411
+ await this.telegram?.sendMessage(Number(cid), "✅ PPM restarted successfully.");
412
+ }
413
+ } catch {}
414
+ }
415
+
341
416
  private async cmdHelp(chatId: string): Promise<void> {
342
417
  const text = `<b>PPMBot Commands</b>
343
418
 
344
419
  /start — Greeting + list projects
345
- /project &lt;name&gt; — Switch project
420
+ /project &lt;name&gt; — Switch/list projects
346
421
  /new — Fresh session (current project)
347
422
  /sessions — List recent sessions
348
423
  /resume &lt;n&gt; — Resume session #n
@@ -351,6 +426,7 @@ class PPMBotService {
351
426
  /memory — Show project memories
352
427
  /forget &lt;topic&gt; — Remove matching memories
353
428
  /remember &lt;fact&gt; — Save a fact
429
+ /restart — Restart PPM server
354
430
  /help — This message`;
355
431
  await this.telegram!.sendMessage(Number(chatId), text);
356
432
  }
@@ -442,11 +518,19 @@ class PPMBotService {
442
518
  },
443
519
  );
444
520
 
445
- // Capture identity if onboarding was just shown
521
+ // Capture identity: save when onboarding was shown OR when no identity exists
522
+ // (handles server restarts losing the in-memory flag)
446
523
  if (this.identityPending.has(chatId)) {
447
524
  this.identityPending.delete(chatId);
448
525
  this.memory.saveOne("_global", `User identity: ${text}`, "preference", session.sessionId);
449
526
  console.log("[ppmbot] Saved identity memory from onboarding");
527
+ } else if (!this.hasCheckedIdentity.has(chatId)) {
528
+ this.hasCheckedIdentity.add(chatId);
529
+ const globalMems = this.memory.getSummary("_global", 50);
530
+ if (!globalMems.some((m) => m.category === "preference" && /identity/i.test(m.content))) {
531
+ this.memory.saveOne("_global", `User identity: ${text}`, "preference", session.sessionId);
532
+ console.log("[ppmbot] Saved identity memory (first message, no identity found)");
533
+ }
450
534
  }
451
535
 
452
536
  // Periodic memory extraction — fire-and-forget every N messages
@@ -13,7 +13,7 @@ const BOT_TOKEN_RE = /^\d+:[A-Za-z0-9_-]{30,50}$/;
13
13
  /** Known PPMBot slash commands */
14
14
  const COMMANDS = new Set([
15
15
  "start", "project", "new", "sessions", "resume",
16
- "status", "stop", "memory", "forget", "remember", "help",
16
+ "status", "stop", "memory", "forget", "remember", "restart", "help",
17
17
  ]);
18
18
 
19
19
  export type UpdateHandler = (update: TelegramUpdate) => Promise<void>;
@@ -43,7 +43,7 @@ export class PPMBotTelegram {
43
43
  await this.callApi("setMyCommands", {
44
44
  commands: [
45
45
  { command: "start", description: "Greeting + list projects" },
46
- { command: "project", description: "Switch project" },
46
+ { command: "project", description: "Switch/list projects" },
47
47
  { command: "new", description: "Fresh session (current project)" },
48
48
  { command: "sessions", description: "List recent sessions" },
49
49
  { command: "resume", description: "Resume a previous session" },
@@ -52,6 +52,7 @@ export class PPMBotTelegram {
52
52
  { command: "memory", description: "Show project memories" },
53
53
  { command: "forget", description: "Remove matching memories" },
54
54
  { command: "remember", description: "Save a fact" },
55
+ { command: "restart", description: "Restart PPM server" },
55
56
  { command: "help", description: "Show all commands" },
56
57
  ],
57
58
  });
@@ -3,7 +3,8 @@ import { Button } from "@/components/ui/button";
3
3
  import { Input } from "@/components/ui/input";
4
4
  import { Switch } from "@/components/ui/switch";
5
5
  import { api } from "@/lib/api-client";
6
- import { Trash2, CheckCircle, Clock, Send } from "lucide-react";
6
+ import { Trash2, CheckCircle, Clock, Send, Brain, RefreshCw } from "lucide-react";
7
+ import { Separator } from "@/components/ui/separator";
7
8
 
8
9
  interface PPMBotConfig {
9
10
  enabled: boolean;
@@ -20,6 +21,14 @@ interface TelegramConfig {
20
21
  bot_token: string;
21
22
  }
22
23
 
24
+ interface MemoryRow {
25
+ id: number;
26
+ project: string;
27
+ content: string;
28
+ category: string;
29
+ importance: number;
30
+ }
31
+
23
32
  interface PairedChat {
24
33
  id: number;
25
34
  telegram_chat_id: string;
@@ -53,6 +62,9 @@ export function PPMBotSettingsSection() {
53
62
  const [approving, setApproving] = useState(false);
54
63
  const [testing, setTesting] = useState(false);
55
64
 
65
+ const [memories, setMemories] = useState<MemoryRow[]>([]);
66
+ const [memoryProject, setMemoryProject] = useState("_global");
67
+
56
68
  const fetchPairedChats = useCallback(async () => {
57
69
  try {
58
70
  const data = await api.get<PairedChat[]>("/api/settings/clawbot/paired");
@@ -60,6 +72,20 @@ export function PPMBotSettingsSection() {
60
72
  } catch {}
61
73
  }, []);
62
74
 
75
+ const fetchMemories = useCallback(async (project = memoryProject) => {
76
+ try {
77
+ const data = await api.get<MemoryRow[]>(`/api/settings/clawbot/memories?project=${encodeURIComponent(project)}`);
78
+ setMemories(data);
79
+ } catch {}
80
+ }, [memoryProject]);
81
+
82
+ const deleteMemory = async (id: number) => {
83
+ try {
84
+ await api.del(`/api/settings/clawbot/memories/${id}`);
85
+ setMemories((prev) => prev.filter((m) => m.id !== id));
86
+ } catch {}
87
+ };
88
+
63
89
  useEffect(() => {
64
90
  api.get<PPMBotConfig>("/api/settings/clawbot").then((data) => {
65
91
  setConfig(data);
@@ -74,7 +100,8 @@ export function PPMBotSettingsSection() {
74
100
  setTokenConfigured(!!data.bot_token);
75
101
  }).catch(() => {});
76
102
  fetchPairedChats();
77
- }, [fetchPairedChats]);
103
+ fetchMemories("_global");
104
+ }, [fetchPairedChats, fetchMemories]);
78
105
 
79
106
  const saveToken = async () => {
80
107
  if (!tokenInput.trim()) return;
@@ -276,6 +303,61 @@ export function PPMBotSettingsSection() {
276
303
  )}
277
304
  </div>
278
305
 
306
+ <Separator />
307
+
308
+ {/* Memory & Identity */}
309
+ <div className="space-y-2">
310
+ <div className="flex items-center justify-between">
311
+ <div className="flex items-center gap-1.5">
312
+ <Brain className="size-3.5 text-muted-foreground" />
313
+ <p className="text-xs font-medium">Memory & Identity</p>
314
+ </div>
315
+ <Button
316
+ variant="ghost"
317
+ size="sm"
318
+ className="h-6 w-6 p-0 cursor-pointer"
319
+ onClick={() => fetchMemories(memoryProject)}
320
+ >
321
+ <RefreshCw className="size-3" />
322
+ </Button>
323
+ </div>
324
+ <p className="text-[10px] text-muted-foreground">
325
+ Facts the bot remembers across sessions. Use /remember on Telegram to add, or delete here.
326
+ </p>
327
+
328
+ {memories.length === 0 ? (
329
+ <p className="text-[10px] text-muted-foreground italic">
330
+ No memories stored yet. Send /start on Telegram and introduce yourself.
331
+ </p>
332
+ ) : (
333
+ <div className="space-y-1 max-h-[200px] overflow-y-auto">
334
+ {memories.map((mem) => (
335
+ <div
336
+ key={mem.id}
337
+ className="flex items-start justify-between rounded-md border p-2 gap-1"
338
+ >
339
+ <div className="min-w-0 flex-1">
340
+ <p className="text-[11px] break-words">{mem.content}</p>
341
+ <p className="text-[10px] text-muted-foreground">
342
+ {mem.category} · {mem.project}
343
+ </p>
344
+ </div>
345
+ <Button
346
+ variant="ghost"
347
+ size="sm"
348
+ className="h-6 w-6 p-0 text-destructive hover:text-destructive cursor-pointer shrink-0"
349
+ onClick={() => deleteMemory(mem.id)}
350
+ >
351
+ <Trash2 className="size-3" />
352
+ </Button>
353
+ </div>
354
+ ))}
355
+ </div>
356
+ )}
357
+ </div>
358
+
359
+ <Separator />
360
+
279
361
  {/* Default Project */}
280
362
  <div className="space-y-1.5">
281
363
  <label className="text-[11px] text-muted-foreground">Default Project</label>