@hienlh/ppm 0.9.53 → 0.9.55
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 +6 -0
- package/dist/web/assets/{chat-tab-DvNEQYEe.js → chat-tab-SfXtOm9d.js} +1 -1
- package/dist/web/assets/{code-editor-CoT017Ah.js → code-editor-DAZvtAlT.js} +1 -1
- package/dist/web/assets/database-viewer-C5fco1jm.js +1 -0
- package/dist/web/assets/{diff-viewer-D0tuen4I.js → diff-viewer-ShRSPvsf.js} +1 -1
- package/dist/web/assets/{extension-webview-Ba5aeo9r.js → extension-webview-CWJRMPfV.js} +1 -1
- package/dist/web/assets/{git-graph-BnJrVPxJ.js → git-graph-h0QmXMdZ.js} +1 -1
- package/dist/web/assets/{index-DUQgLj0D.js → index-CDlrGSwd.js} +4 -4
- package/dist/web/assets/{index-BEfMoc_W.css → index-DVuSY0BZ.css} +1 -1
- package/dist/web/assets/keybindings-store-wbHg-S_v.js +1 -0
- package/dist/web/assets/{markdown-renderer-BuGSrE3y.js → markdown-renderer-CSEmmMWt.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-DsbrWNUP.js → port-forwarding-tab-Cts6tMFn.js} +1 -1
- package/dist/web/assets/{postgres-viewer-Bh6YmZPq.js → postgres-viewer-CiQC1sf9.js} +1 -1
- package/dist/web/assets/{settings-tab-BnzFtexC.js → settings-tab-CQx6aHtO.js} +1 -1
- package/dist/web/assets/sqlite-viewer-FQfCkjU6.js +1 -0
- package/dist/web/assets/{terminal-tab-fnZvscaH.js → terminal-tab-C2SnOqxn.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-BdcKAZ69.js → use-monaco-theme-VPgvhMpB.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +52 -13
- package/docs/project-changelog.md +45 -1
- package/docs/project-roadmap.md +1 -1
- package/docs/system-architecture.md +121 -9
- package/package.json +1 -1
- package/src/cli/commands/bot-cmd.ts +144 -240
- package/src/server/routes/database.ts +31 -0
- package/src/server/routes/settings.ts +13 -0
- package/src/server/routes/sqlite.ts +14 -0
- package/src/services/database/postgres-adapter.ts +8 -0
- package/src/services/database/sqlite-adapter.ts +5 -0
- package/src/services/db.service.ts +109 -1
- package/src/services/postgres.service.ts +12 -0
- package/src/services/ppmbot/ppmbot-delegation.ts +112 -0
- package/src/services/ppmbot/ppmbot-service.ts +194 -369
- package/src/services/ppmbot/ppmbot-session.ts +85 -108
- package/src/services/ppmbot/ppmbot-telegram.ts +5 -16
- package/src/services/sqlite.service.ts +10 -0
- package/src/types/config.ts +1 -3
- package/src/types/database.ts +3 -0
- package/src/types/ppmbot.ts +21 -0
- package/src/web/components/database/database-viewer.tsx +50 -8
- package/src/web/components/database/use-database.ts +13 -1
- package/src/web/components/settings/ppmbot-settings-section.tsx +87 -26
- package/src/web/components/sqlite/sqlite-data-grid.tsx +55 -8
- package/src/web/components/sqlite/sqlite-viewer.tsx +1 -0
- package/src/web/components/sqlite/use-sqlite.ts +16 -1
- package/dist/web/assets/database-viewer-C3wK7cDk.js +0 -1
- package/dist/web/assets/keybindings-store-CkGFjxkX.js +0 -1
- package/dist/web/assets/sqlite-viewer-Cu3_hf07.js +0 -1
- package/docs/streaming-input-guide.md +0 -267
- package/snapshot-state.md +0 -1526
- package/test-session-ops.mjs +0 -444
- package/test-tokens.mjs +0 -212
|
@@ -1,20 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
1
4
|
import { configService } from "../config.service.ts";
|
|
2
5
|
import { chatService } from "../chat.service.ts";
|
|
3
6
|
import {
|
|
4
7
|
isPairedChat,
|
|
5
8
|
getPairingByChatId,
|
|
6
9
|
createPairingRequest,
|
|
7
|
-
getSessionTitles,
|
|
8
|
-
getPinnedSessionIds,
|
|
9
10
|
getApprovedPairedChats,
|
|
11
|
+
getRecentBotTasks,
|
|
12
|
+
getRunningBotTasks,
|
|
13
|
+
updateBotTaskStatus,
|
|
14
|
+
markBotTaskReported,
|
|
10
15
|
} from "../db.service.ts";
|
|
11
16
|
import { PPMBotTelegram } from "./ppmbot-telegram.ts";
|
|
12
|
-
import { PPMBotSessionManager } from "./ppmbot-session.ts";
|
|
17
|
+
import { PPMBotSessionManager, ensureCoordinatorWorkspace, DEFAULT_COORDINATOR_IDENTITY } from "./ppmbot-session.ts";
|
|
13
18
|
import { PPMBotMemory } from "./ppmbot-memory.ts";
|
|
14
19
|
import { streamToTelegram } from "./ppmbot-streamer.ts";
|
|
15
20
|
import { escapeHtml } from "./ppmbot-formatter.ts";
|
|
21
|
+
import { executeDelegation, getActiveDelegationCount } from "./ppmbot-delegation.ts";
|
|
16
22
|
import type { TelegramUpdate, PPMBotCommand } from "../../types/ppmbot.ts";
|
|
17
|
-
import type { PPMBotConfig, TelegramConfig, PermissionMode } from "../../types/config.ts";
|
|
23
|
+
import type { PPMBotConfig, TelegramConfig, ProjectConfig, PermissionMode } from "../../types/config.ts";
|
|
18
24
|
import type { SendMessageOpts } from "../../types/chat.ts";
|
|
19
25
|
|
|
20
26
|
const CONTEXT_WINDOW_THRESHOLD = 80;
|
|
@@ -25,6 +31,12 @@ class PPMBotService {
|
|
|
25
31
|
private memory = new PPMBotMemory();
|
|
26
32
|
private running = false;
|
|
27
33
|
|
|
34
|
+
/** Cached coordinator identity from coordinator.md */
|
|
35
|
+
private coordinatorIdentity = "";
|
|
36
|
+
|
|
37
|
+
/** Task polling interval */
|
|
38
|
+
private taskPoller: ReturnType<typeof setInterval> | null = null;
|
|
39
|
+
|
|
28
40
|
/** Debounce timers per chatId */
|
|
29
41
|
private debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
30
42
|
private debouncedTexts = new Map<string, string>();
|
|
@@ -35,12 +47,6 @@ class PPMBotService {
|
|
|
35
47
|
/** Message queue per chatId for concurrent messages */
|
|
36
48
|
private messageQueue = new Map<string, string[]>();
|
|
37
49
|
|
|
38
|
-
/** Sessions that already had their title set */
|
|
39
|
-
private titledSessions = new Set<string>();
|
|
40
|
-
|
|
41
|
-
/** Chat IDs that just received identity onboarding prompt */
|
|
42
|
-
private identityPending = new Set<string>();
|
|
43
|
-
|
|
44
50
|
// ── Lifecycle ─────────────────────────────────────────────────
|
|
45
51
|
|
|
46
52
|
async start(): Promise<void> {
|
|
@@ -57,13 +63,17 @@ class PPMBotService {
|
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
try {
|
|
66
|
+
ensureCoordinatorWorkspace();
|
|
67
|
+
|
|
60
68
|
this.telegram = new PPMBotTelegram(telegramConfig.bot_token);
|
|
61
69
|
this.running = true;
|
|
62
70
|
|
|
63
|
-
// Start polling (non-blocking)
|
|
64
71
|
this.telegram.startPolling((update) => this.handleUpdate(update));
|
|
65
72
|
|
|
66
|
-
//
|
|
73
|
+
// Task poller for delegation execution
|
|
74
|
+
this.taskPoller = setInterval(() => this.checkPendingTasks(), 5000);
|
|
75
|
+
this.cleanupStaleTasks();
|
|
76
|
+
|
|
67
77
|
await this.checkRestartNotification();
|
|
68
78
|
|
|
69
79
|
console.log("[ppmbot] Started");
|
|
@@ -77,12 +87,16 @@ class PPMBotService {
|
|
|
77
87
|
this.telegram?.stop();
|
|
78
88
|
this.telegram = null;
|
|
79
89
|
|
|
90
|
+
if (this.taskPoller) {
|
|
91
|
+
clearInterval(this.taskPoller);
|
|
92
|
+
this.taskPoller = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
for (const timer of this.debounceTimers.values()) clearTimeout(timer);
|
|
81
96
|
this.debounceTimers.clear();
|
|
82
97
|
this.debouncedTexts.clear();
|
|
83
98
|
this.processing.clear();
|
|
84
99
|
this.messageQueue.clear();
|
|
85
|
-
this.identityPending.clear();
|
|
86
100
|
|
|
87
101
|
console.log("[ppmbot] Stopped");
|
|
88
102
|
}
|
|
@@ -114,7 +128,6 @@ class PPMBotService {
|
|
|
114
128
|
if (!isPairedChat(chatId)) {
|
|
115
129
|
const pairing = getPairingByChatId(chatId);
|
|
116
130
|
if (!pairing) {
|
|
117
|
-
// First-time user — generate pairing code
|
|
118
131
|
const code = this.generatePairingCode();
|
|
119
132
|
createPairingRequest(chatId, String(userId), displayName, code);
|
|
120
133
|
await this.telegram!.sendMessage(
|
|
@@ -130,26 +143,22 @@ class PPMBotService {
|
|
|
130
143
|
);
|
|
131
144
|
return;
|
|
132
145
|
}
|
|
133
|
-
if (pairing.status === "revoked")
|
|
134
|
-
return; // Silently ignore
|
|
135
|
-
}
|
|
146
|
+
if (pairing.status === "revoked") return;
|
|
136
147
|
}
|
|
137
148
|
|
|
138
|
-
// Try parsing as command
|
|
139
149
|
const command = PPMBotTelegram.parseCommand(message);
|
|
140
150
|
if (command) {
|
|
141
151
|
await this.handleCommand(command);
|
|
142
152
|
return;
|
|
143
153
|
}
|
|
144
154
|
|
|
145
|
-
// Regular message
|
|
146
155
|
const text = message.text ?? message.caption ?? "";
|
|
147
156
|
if (!text.trim()) return;
|
|
148
157
|
|
|
149
158
|
await this.handleMessage(chatId, text);
|
|
150
159
|
}
|
|
151
160
|
|
|
152
|
-
// ── Command Handlers
|
|
161
|
+
// ── Command Handlers (3 commands + hidden restart) ──────────────
|
|
153
162
|
|
|
154
163
|
private async handleCommand(cmd: PPMBotCommand): Promise<void> {
|
|
155
164
|
const chatId = String(cmd.chatId);
|
|
@@ -158,19 +167,10 @@ class PPMBotService {
|
|
|
158
167
|
try {
|
|
159
168
|
switch (cmd.command) {
|
|
160
169
|
case "start": await this.cmdStart(chatId); break;
|
|
161
|
-
case "project": await this.cmdProject(chatId, cmd.args); break;
|
|
162
|
-
case "new": await this.cmdNew(chatId); break;
|
|
163
|
-
case "sessions": await this.cmdSessions(chatId, cmd.args); break;
|
|
164
|
-
case "resume": await this.cmdResume(chatId, cmd.args); break;
|
|
165
170
|
case "status": await this.cmdStatus(chatId); break;
|
|
166
|
-
case "stop": await this.cmdStop(chatId); break;
|
|
167
|
-
case "memory": await this.cmdMemory(chatId); break;
|
|
168
|
-
case "forget": await this.cmdForget(chatId, cmd.args); break;
|
|
169
|
-
case "remember": await this.cmdRemember(chatId, cmd.args); break;
|
|
170
171
|
case "restart": await this.cmdRestart(chatId); break;
|
|
171
|
-
case "version": await this.cmdVersion(chatId); break;
|
|
172
172
|
case "help": await this.cmdHelp(chatId); break;
|
|
173
|
-
default: await tg.sendMessage(Number(chatId), `
|
|
173
|
+
default: await tg.sendMessage(Number(chatId), `Just chat naturally — I'll handle it! Try /help`);
|
|
174
174
|
}
|
|
175
175
|
} catch (err) {
|
|
176
176
|
await tg.sendMessage(
|
|
@@ -182,348 +182,189 @@ class PPMBotService {
|
|
|
182
182
|
|
|
183
183
|
private async cmdStart(chatId: string): Promise<void> {
|
|
184
184
|
const projects = this.sessions.getProjectNames();
|
|
185
|
-
let text = "<b>🤖 PPMBot</b>\n\n";
|
|
186
|
-
text += "
|
|
187
|
-
text += "Ask me anything —
|
|
185
|
+
let text = "<b>🤖 PPMBot Coordinator</b>\n\n";
|
|
186
|
+
text += "I'm your AI project coordinator on Telegram.\n";
|
|
187
|
+
text += "Ask me anything — I'll answer directly or delegate to your projects.\n\n";
|
|
188
188
|
if (projects.length) {
|
|
189
189
|
text += "<b>Your projects:</b>\n";
|
|
190
190
|
for (const name of projects) {
|
|
191
191
|
text += ` • <code>${escapeHtml(name)}</code>\n`;
|
|
192
192
|
}
|
|
193
|
-
text += "\nSwitch: /project <name>";
|
|
194
|
-
} else {
|
|
195
|
-
text += "No projects configured — I'll use a default workspace.";
|
|
196
193
|
}
|
|
197
|
-
text += "\
|
|
194
|
+
text += "\nJust chat naturally — no commands needed!";
|
|
195
|
+
text += "\nType /help for more info.";
|
|
198
196
|
await this.telegram!.sendMessage(Number(chatId), text);
|
|
199
197
|
|
|
200
|
-
// Identity onboarding
|
|
198
|
+
// Identity onboarding
|
|
201
199
|
const globalMemories = this.memory.getSummary("_global", 50);
|
|
202
200
|
const hasIdentity = globalMemories.some((m) =>
|
|
203
201
|
m.category === "preference" && /identity|name|role/i.test(m.content),
|
|
204
202
|
);
|
|
205
203
|
if (!hasIdentity) {
|
|
206
|
-
this.identityPending.add(chatId);
|
|
207
204
|
await this.telegram!.sendMessage(
|
|
208
205
|
Number(chatId),
|
|
209
206
|
"📝 <b>Quick intro?</b>\n\n" +
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
"• What you work on (language, stack, role)\n" +
|
|
213
|
-
"• Preferred response language (English, Vietnamese, etc.)\n\n" +
|
|
214
|
-
"I'll remember your preferences for future chats.\n" +
|
|
215
|
-
"Or skip this and just start chatting!",
|
|
207
|
+
"Tell me your name, what you work on, and preferred language.\n" +
|
|
208
|
+
"I'll remember for future chats. Or just start chatting!",
|
|
216
209
|
);
|
|
217
210
|
}
|
|
218
211
|
}
|
|
219
212
|
|
|
220
|
-
private async cmdProject(chatId: string, args: string): Promise<void> {
|
|
221
|
-
if (!args) {
|
|
222
|
-
const active = this.sessions.getActiveSession(chatId);
|
|
223
|
-
const current = active?.projectName ?? "";
|
|
224
|
-
const projects = this.sessions.getProjectNames();
|
|
225
|
-
let text = "<b>Projects</b>\n\n";
|
|
226
|
-
if (projects.length === 0) {
|
|
227
|
-
text += "No projects configured.\nUsing default: <code>~/.ppm/bot/</code>\n";
|
|
228
|
-
} else {
|
|
229
|
-
for (const name of projects) {
|
|
230
|
-
const marker = name === current ? " ✓" : "";
|
|
231
|
-
text += `• <code>${escapeHtml(name)}</code>${marker}\n`;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
text += `\nCurrent: <b>${escapeHtml(current || "bot (default)")}</b>`;
|
|
235
|
-
text += "\nSwitch: /project <name>";
|
|
236
|
-
await this.telegram!.sendMessage(Number(chatId), text);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const session = await this.sessions.switchProject(chatId, args);
|
|
241
|
-
await this.telegram!.sendMessage(
|
|
242
|
-
Number(chatId),
|
|
243
|
-
`Switched to <b>${escapeHtml(session.projectName)}</b> ✓`,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private async cmdNew(chatId: string): Promise<void> {
|
|
248
|
-
const active = this.sessions.getActiveSession(chatId);
|
|
249
|
-
const projectName = active?.projectName;
|
|
250
|
-
await this.sessions.closeSession(chatId);
|
|
251
|
-
const session = await this.sessions.getOrCreateSession(chatId, projectName ?? undefined);
|
|
252
|
-
await this.telegram!.sendMessage(
|
|
253
|
-
Number(chatId),
|
|
254
|
-
`New session for <b>${escapeHtml(session.projectName)}</b> ✓`,
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
private async cmdSessions(chatId: string, args: string): Promise<void> {
|
|
259
|
-
const PAGE_SIZE = 8;
|
|
260
|
-
const page = Math.max(1, parseInt(args, 10) || 1);
|
|
261
|
-
|
|
262
|
-
const active = this.sessions.getActiveSession(chatId);
|
|
263
|
-
const project = active?.projectName;
|
|
264
|
-
|
|
265
|
-
// Fetch all sessions for this chat (enough for pagination)
|
|
266
|
-
const allSessions = this.sessions.listRecentSessions(chatId, 50);
|
|
267
|
-
// Filter by current project if one is active
|
|
268
|
-
const filtered = project
|
|
269
|
-
? allSessions.filter((s) => s.project_name === project)
|
|
270
|
-
: allSessions;
|
|
271
|
-
|
|
272
|
-
if (filtered.length === 0) {
|
|
273
|
-
await this.telegram!.sendMessage(Number(chatId), "No sessions yet. Send a message to start.");
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Enrich with titles and pin status
|
|
278
|
-
const titles = getSessionTitles(filtered.map((s) => s.session_id));
|
|
279
|
-
const pinnedIds = getPinnedSessionIds();
|
|
280
|
-
|
|
281
|
-
// Sort: pinned first, then by last_message_at desc
|
|
282
|
-
const sorted = [...filtered].sort((a, b) => {
|
|
283
|
-
const aPin = pinnedIds.has(a.session_id) ? 1 : 0;
|
|
284
|
-
const bPin = pinnedIds.has(b.session_id) ? 1 : 0;
|
|
285
|
-
if (aPin !== bPin) return bPin - aPin;
|
|
286
|
-
return b.last_message_at - a.last_message_at;
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const totalPages = Math.ceil(sorted.length / PAGE_SIZE);
|
|
290
|
-
const start = (page - 1) * PAGE_SIZE;
|
|
291
|
-
const pageItems = sorted.slice(start, start + PAGE_SIZE);
|
|
292
|
-
|
|
293
|
-
if (pageItems.length === 0) {
|
|
294
|
-
await this.telegram!.sendMessage(Number(chatId), `No sessions on page ${page}.`);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const header = project ? escapeHtml(project) : "All Projects";
|
|
299
|
-
let text = `<b>Sessions — ${header}</b>`;
|
|
300
|
-
if (totalPages > 1) text += ` <i>(${page}/${totalPages})</i>`;
|
|
301
|
-
text += "\n\n";
|
|
302
|
-
|
|
303
|
-
pageItems.forEach((s, i) => {
|
|
304
|
-
const pin = pinnedIds.has(s.session_id) ? "📌 " : "";
|
|
305
|
-
const activeDot = s.is_active ? " ⬤" : "";
|
|
306
|
-
const rawTitle = titles[s.session_id]?.replace(/^\[PPM\]\s*/, "") || "";
|
|
307
|
-
const title = rawTitle
|
|
308
|
-
? escapeHtml(rawTitle.slice(0, 45))
|
|
309
|
-
: "<i>untitled</i>";
|
|
310
|
-
const date = new Date(s.last_message_at * 1000).toLocaleString(undefined, {
|
|
311
|
-
month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
|
|
312
|
-
});
|
|
313
|
-
const sid = s.session_id.slice(0, 8);
|
|
314
|
-
const num = start + i + 1;
|
|
315
|
-
|
|
316
|
-
text += `${pin}${num}. ${title}${activeDot}\n`;
|
|
317
|
-
text += ` <code>${sid}</code> · ${date}\n\n`;
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
text += "Resume: /resume <n> or /resume <id>";
|
|
321
|
-
if (totalPages > 1 && page < totalPages) {
|
|
322
|
-
text += `\nNext: /sessions ${page + 1}`;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
await this.telegram!.sendMessage(Number(chatId), text);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
private async cmdResume(chatId: string, args: string): Promise<void> {
|
|
329
|
-
if (!args.trim()) {
|
|
330
|
-
await this.telegram!.sendMessage(Number(chatId), "Usage: /resume <number or session-id>");
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Support both index (e.g. "2") and session ID prefix (e.g. "fdc4ddaa")
|
|
335
|
-
const index = parseInt(args, 10);
|
|
336
|
-
const isIndex = !isNaN(index) && index >= 1 && String(index) === args.trim();
|
|
337
|
-
|
|
338
|
-
const session = isIndex
|
|
339
|
-
? await this.sessions.resumeSessionById(chatId, index)
|
|
340
|
-
: await this.sessions.resumeSessionByIdPrefix(chatId, args.trim());
|
|
341
|
-
|
|
342
|
-
if (!session) {
|
|
343
|
-
await this.telegram!.sendMessage(Number(chatId), "Session not found.");
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
await this.telegram!.sendMessage(
|
|
347
|
-
Number(chatId),
|
|
348
|
-
`Resumed session for <b>${escapeHtml(session.projectName)}</b> ✓`,
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
213
|
private async cmdStatus(chatId: string): Promise<void> {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
214
|
+
const tasks = getRecentBotTasks(chatId, 10);
|
|
215
|
+
const active = tasks.filter((t) => t.status === "running" || t.status === "pending");
|
|
216
|
+
const completed = tasks.filter((t) => t.status === "completed");
|
|
217
|
+
const delegationCount = getActiveDelegationCount();
|
|
218
|
+
|
|
219
|
+
let text = "<b>PPMBot Status</b>\n\n";
|
|
220
|
+
text += `Active delegations: ${delegationCount}\n`;
|
|
221
|
+
|
|
222
|
+
if (active.length) {
|
|
223
|
+
text += "\n<b>Running Tasks:</b>\n";
|
|
224
|
+
for (const t of active) {
|
|
225
|
+
const elapsed = Math.round((Date.now() / 1000 - t.createdAt) / 60);
|
|
226
|
+
text += ` 🔄 <code>${t.id.slice(0, 8)}</code> ${escapeHtml(t.projectName)} — ${escapeHtml(t.prompt.slice(0, 50))} (${elapsed}m)\n`;
|
|
227
|
+
}
|
|
357
228
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
private async cmdStop(chatId: string): Promise<void> {
|
|
366
|
-
await this.sessions.closeSession(chatId);
|
|
367
|
-
await this.telegram!.sendMessage(Number(chatId), "Session ended ✓");
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
private async cmdMemory(chatId: string): Promise<void> {
|
|
371
|
-
const memories = this.memory.getSummary("_global");
|
|
372
|
-
if (memories.length === 0) {
|
|
373
|
-
await this.telegram!.sendMessage(Number(chatId), "No memories stored. Use /remember to add.");
|
|
374
|
-
return;
|
|
229
|
+
if (completed.length) {
|
|
230
|
+
text += "\n<b>Recent Completed:</b>\n";
|
|
231
|
+
for (const t of completed.slice(0, 5)) {
|
|
232
|
+
text += ` ✅ <code>${t.id.slice(0, 8)}</code> ${escapeHtml(t.projectName)} — ${escapeHtml(t.prompt.slice(0, 50))}\n`;
|
|
233
|
+
}
|
|
375
234
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
text += `• [${mem.category}] ${escapeHtml(mem.content)}\n`;
|
|
235
|
+
if (!active.length && !completed.length) {
|
|
236
|
+
text += "No recent tasks.";
|
|
379
237
|
}
|
|
380
238
|
await this.telegram!.sendMessage(Number(chatId), text);
|
|
381
239
|
}
|
|
382
240
|
|
|
383
|
-
private async cmdForget(chatId: string, args: string): Promise<void> {
|
|
384
|
-
if (!args) {
|
|
385
|
-
await this.telegram!.sendMessage(Number(chatId), "Usage: /forget <topic>");
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
const count = this.memory.forget("_global", args);
|
|
389
|
-
await this.telegram!.sendMessage(Number(chatId), `Forgot ${count} memor${count === 1 ? "y" : "ies"} ✓`);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
private async cmdRemember(chatId: string, args: string): Promise<void> {
|
|
393
|
-
if (!args) {
|
|
394
|
-
await this.telegram!.sendMessage(Number(chatId), "Usage: /remember <fact>");
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const active = this.sessions.getActiveSession(chatId);
|
|
398
|
-
this.memory.saveOne("_global", args, "fact", active?.sessionId);
|
|
399
|
-
await this.telegram!.sendMessage(Number(chatId), "Remembered ✓ (cross-project)");
|
|
400
|
-
}
|
|
401
|
-
|
|
402
241
|
private async cmdRestart(chatId: string): Promise<void> {
|
|
403
242
|
await this.telegram!.sendMessage(Number(chatId), "🔄 Restarting PPM...");
|
|
404
|
-
|
|
405
|
-
// Schedule restart after a short delay so the response is sent
|
|
406
243
|
setTimeout(async () => {
|
|
407
|
-
const { join } = await import("node:path");
|
|
408
244
|
const { writeFileSync } = await import("node:fs");
|
|
409
|
-
const { homedir } = await import("node:os");
|
|
410
|
-
|
|
411
245
|
const approvedChats = getApprovedPairedChats();
|
|
412
246
|
const chatIds = approvedChats.map((c) => c.telegram_chat_id);
|
|
413
|
-
|
|
414
|
-
// Write restart marker so we can notify after restart
|
|
415
247
|
const markerPath = join(homedir(), ".ppm", "restart-notify.json");
|
|
416
248
|
writeFileSync(markerPath, JSON.stringify({ chatIds, ts: Date.now() }));
|
|
417
|
-
|
|
418
249
|
console.log("[ppmbot] Restart requested via Telegram, exiting with code 42...");
|
|
419
250
|
process.exit(42);
|
|
420
251
|
}, 500);
|
|
421
252
|
}
|
|
422
253
|
|
|
423
|
-
|
|
424
|
-
|
|
254
|
+
private async cmdHelp(chatId: string): Promise<void> {
|
|
255
|
+
const text = `<b>PPMBot Commands</b>
|
|
256
|
+
|
|
257
|
+
/start — Welcome + list projects
|
|
258
|
+
/status — Running tasks + delegations
|
|
259
|
+
/help — This message
|
|
260
|
+
|
|
261
|
+
<b>Everything else:</b> just chat naturally!
|
|
262
|
+
I'll answer directly or delegate to your project's AI.`;
|
|
263
|
+
await this.telegram!.sendMessage(Number(chatId), text);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Coordinator Context ─────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
private readCoordinatorIdentity(): string {
|
|
269
|
+
if (this.coordinatorIdentity) return this.coordinatorIdentity;
|
|
270
|
+
const identityPath = join(homedir(), ".ppm", "bot", "coordinator.md");
|
|
425
271
|
try {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
272
|
+
this.coordinatorIdentity = readFileSync(identityPath, "utf-8");
|
|
273
|
+
} catch {
|
|
274
|
+
this.coordinatorIdentity = DEFAULT_COORDINATOR_IDENTITY;
|
|
275
|
+
}
|
|
276
|
+
return this.coordinatorIdentity;
|
|
277
|
+
}
|
|
429
278
|
|
|
430
|
-
|
|
431
|
-
|
|
279
|
+
private buildCoordinatorContext(chatId: string): string {
|
|
280
|
+
const parts: string[] = [];
|
|
432
281
|
|
|
433
|
-
|
|
434
|
-
|
|
282
|
+
// Identity
|
|
283
|
+
const identity = this.readCoordinatorIdentity();
|
|
284
|
+
if (identity) {
|
|
285
|
+
parts.push("## Identity");
|
|
286
|
+
parts.push(identity);
|
|
287
|
+
}
|
|
435
288
|
|
|
436
|
-
|
|
437
|
-
|
|
289
|
+
// Session info
|
|
290
|
+
parts.push(`\n## Session Info`);
|
|
291
|
+
parts.push(`Chat ID: ${chatId}`);
|
|
438
292
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
} catch {}
|
|
293
|
+
// Custom system prompt (user overrides)
|
|
294
|
+
const config = this.getConfig();
|
|
295
|
+
if (config?.system_prompt) {
|
|
296
|
+
parts.push(`\n## Custom Instructions`);
|
|
297
|
+
parts.push(config.system_prompt);
|
|
298
|
+
}
|
|
446
299
|
|
|
447
|
-
|
|
448
|
-
|
|
300
|
+
// Project list
|
|
301
|
+
const projects = configService.get("projects") as ProjectConfig[];
|
|
302
|
+
if (projects?.length) {
|
|
303
|
+
parts.push("\n## Available Projects");
|
|
304
|
+
for (const p of projects) {
|
|
305
|
+
parts.push(`- ${p.name} (${p.path})`);
|
|
449
306
|
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
307
|
+
}
|
|
452
308
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
309
|
+
// Running/recent tasks
|
|
310
|
+
const tasks = getRecentBotTasks(chatId, 10);
|
|
311
|
+
const activeTasks = tasks.filter((t) => t.status === "running" || t.status === "pending");
|
|
312
|
+
if (activeTasks.length) {
|
|
313
|
+
parts.push("\n## Running Tasks");
|
|
314
|
+
for (const t of activeTasks) {
|
|
315
|
+
const elapsed = Math.round((Date.now() / 1000 - t.createdAt) / 60);
|
|
316
|
+
parts.push(`- ${t.id.slice(0, 8)}: ${t.projectName} — "${t.prompt.slice(0, 60)}" (${t.status}, ${elapsed}m ago)`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Completed tasks not yet reported
|
|
321
|
+
const completed = tasks.filter((t) => t.status === "completed" && !t.reported);
|
|
322
|
+
if (completed.length) {
|
|
323
|
+
parts.push("\n## Completed Tasks (notify user)");
|
|
324
|
+
for (const t of completed) {
|
|
325
|
+
parts.push(`- ${t.id.slice(0, 8)}: ${t.projectName} — "${t.prompt.slice(0, 60)}"`);
|
|
326
|
+
parts.push(` Summary: ${t.resultSummary ?? "(use ppm bot task-result to get details)"}`);
|
|
327
|
+
markBotTaskReported(t.id);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Memory recall
|
|
332
|
+
const memories = this.memory.getSummary("_global");
|
|
333
|
+
const memorySection = this.memory.buildRecallPrompt(memories);
|
|
334
|
+
if (memorySection) parts.push(memorySection);
|
|
335
|
+
|
|
336
|
+
return parts.join("\n");
|
|
462
337
|
}
|
|
463
338
|
|
|
464
|
-
|
|
465
|
-
const text = `<b>PPMBot Commands</b>
|
|
339
|
+
// ── Task Delegation Polling ─────────────────────────────────────
|
|
466
340
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
/version — Show PPM version
|
|
479
|
-
/help — This message`;
|
|
480
|
-
await this.telegram!.sendMessage(Number(chatId), text);
|
|
341
|
+
private checkPendingTasks(): void {
|
|
342
|
+
try {
|
|
343
|
+
const pending = getRunningBotTasks().filter((t) => t.status === "pending");
|
|
344
|
+
for (const task of pending) {
|
|
345
|
+
const config = this.getConfig();
|
|
346
|
+
const providerId = config?.default_provider || configService.get("ai").default_provider;
|
|
347
|
+
executeDelegation(task.id, this.telegram!, providerId);
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.error("[ppmbot] checkPendingTasks error:", (err as Error).message);
|
|
351
|
+
}
|
|
481
352
|
}
|
|
482
353
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
### Session
|
|
498
|
-
ppm bot session new — Start fresh session (current project)
|
|
499
|
-
ppm bot session list — List recent sessions
|
|
500
|
-
ppm bot session resume <n|id> — Resume a session by index or ID prefix
|
|
501
|
-
ppm bot session stop — End current session
|
|
502
|
-
|
|
503
|
-
### Memory (cross-project, persists across all projects)
|
|
504
|
-
ppm bot memory save "<content>" --category <category>
|
|
505
|
-
Categories: preference, fact, decision, architecture, issue
|
|
506
|
-
ppm bot memory list — List saved memories
|
|
507
|
-
ppm bot memory forget "<topic>" — Delete matching memories
|
|
508
|
-
|
|
509
|
-
### Server
|
|
510
|
-
ppm bot status — Current project/session info
|
|
511
|
-
ppm bot version — Show PPM version
|
|
512
|
-
ppm bot restart — Restart PPM server
|
|
513
|
-
|
|
514
|
-
### Natural Language Understanding
|
|
515
|
-
When the user says something like:
|
|
516
|
-
- "chuyển sang project X" or "switch to X" → ppm bot project switch X
|
|
517
|
-
- "tạo session mới" or "new session" → ppm bot session new
|
|
518
|
-
- "liệt kê sessions" or "show sessions" → ppm bot session list
|
|
519
|
-
- "quay lại session cũ" or "resume session 2" → ppm bot session resume 2
|
|
520
|
-
- "kết thúc session" or "stop" → ppm bot session stop
|
|
521
|
-
- "đang ở project nào?" or "current project?" → ppm bot project current
|
|
522
|
-
- "nhớ rằng..." or "remember that..." → ppm bot memory save "..." --category preference
|
|
523
|
-
- "quên đi..." or "forget about..." → ppm bot memory forget "..."
|
|
524
|
-
- "restart server" or "khởi động lại" → ppm bot restart
|
|
525
|
-
|
|
526
|
-
Always execute the appropriate CLI command — do NOT just describe what you would do.`;
|
|
354
|
+
private cleanupStaleTasks(): void {
|
|
355
|
+
try {
|
|
356
|
+
const stale = getRunningBotTasks().filter((t) => t.status === "running");
|
|
357
|
+
for (const task of stale) {
|
|
358
|
+
updateBotTaskStatus(task.id, "failed", { error: "Server restarted during execution" });
|
|
359
|
+
this.telegram?.sendMessage(
|
|
360
|
+
Number(task.chatId),
|
|
361
|
+
`⚠️ Task interrupted by server restart: <i>${escapeHtml(task.prompt.slice(0, 80))}</i>`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if (stale.length) console.log(`[ppmbot] Cleaned up ${stale.length} stale task(s)`);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error("[ppmbot] cleanupStaleTasks error:", (err as Error).message);
|
|
367
|
+
}
|
|
527
368
|
}
|
|
528
369
|
|
|
529
370
|
// ── Chat Message Pipeline ───────────────────────────────────────
|
|
@@ -551,7 +392,6 @@ Always execute the appropriate CLI command — do NOT just describe what you wou
|
|
|
551
392
|
this.debouncedTexts.delete(chatId);
|
|
552
393
|
if (!text.trim()) return;
|
|
553
394
|
|
|
554
|
-
// Queue if already processing
|
|
555
395
|
if (this.processing.has(chatId)) {
|
|
556
396
|
const queue = this.messageQueue.get(chatId) ?? [];
|
|
557
397
|
queue.push(text);
|
|
@@ -562,48 +402,16 @@ Always execute the appropriate CLI command — do NOT just describe what you wou
|
|
|
562
402
|
|
|
563
403
|
try {
|
|
564
404
|
const config = this.getConfig();
|
|
405
|
+
const session = await this.sessions.getCoordinatorSession(chatId);
|
|
565
406
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (!this.titledSessions.has(session.sessionId)) {
|
|
570
|
-
this.sessions.updateSessionTitle(session.sessionId, text);
|
|
571
|
-
this.titledSessions.add(session.sessionId);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Recall identity & preferences (global + project)
|
|
575
|
-
const memories = this.memory.getSummary(session.projectName);
|
|
576
|
-
|
|
577
|
-
// Build system prompt with identity/preferences
|
|
578
|
-
let systemPrompt = config?.system_prompt ?? "";
|
|
579
|
-
const memorySection = this.memory.buildRecallPrompt(memories);
|
|
580
|
-
if (memorySection) {
|
|
581
|
-
systemPrompt += memorySection;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Instruct AI to use CLI tools for session/project/memory management
|
|
585
|
-
systemPrompt += this.buildToolsPrompt(chatId);
|
|
407
|
+
// Build coordinator context (identity + projects + tasks + memories)
|
|
408
|
+
const context = this.buildCoordinatorContext(chatId);
|
|
409
|
+
const fullMessage = `<coordinator-context>\n${context}\n</coordinator-context>\n\n${text}`;
|
|
586
410
|
|
|
587
|
-
// Send message to AI (prepend system prompt + memory context)
|
|
588
411
|
const opts: SendMessageOpts = {
|
|
589
412
|
permissionMode: (config?.permission_mode ?? "bypassPermissions") as PermissionMode,
|
|
590
413
|
};
|
|
591
414
|
|
|
592
|
-
// Save identity BEFORE streaming — must persist even if streaming times out
|
|
593
|
-
let messageForAI = text;
|
|
594
|
-
if (this.identityPending.has(chatId)) {
|
|
595
|
-
this.identityPending.delete(chatId);
|
|
596
|
-
this.memory.saveOne("_global", `User identity: ${text}`, "preference", session.sessionId);
|
|
597
|
-
console.log("[ppmbot] Saved identity memory from onboarding");
|
|
598
|
-
// Tell AI this is an identity intro so it acknowledges warmly
|
|
599
|
-
messageForAI = `[User just introduced themselves in response to onboarding prompt. Acknowledge warmly and briefly.]\n\n${text}`;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
let fullMessage = messageForAI;
|
|
603
|
-
if (systemPrompt) {
|
|
604
|
-
fullMessage = `<system-context>\n${systemPrompt}\n</system-context>\n\n${messageForAI}`;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
415
|
const events = chatService.sendMessage(
|
|
608
416
|
session.providerId,
|
|
609
417
|
session.sessionId,
|
|
@@ -611,7 +419,6 @@ Always execute the appropriate CLI command — do NOT just describe what you wou
|
|
|
611
419
|
opts,
|
|
612
420
|
);
|
|
613
421
|
|
|
614
|
-
// Stream response to Telegram
|
|
615
422
|
const result = await streamToTelegram(
|
|
616
423
|
Number(chatId),
|
|
617
424
|
events,
|
|
@@ -622,12 +429,16 @@ Always execute the appropriate CLI command — do NOT just describe what you wou
|
|
|
622
429
|
},
|
|
623
430
|
);
|
|
624
431
|
|
|
625
|
-
//
|
|
432
|
+
// Context rotation
|
|
626
433
|
if (
|
|
627
434
|
result.contextWindowPct != null &&
|
|
628
435
|
result.contextWindowPct > CONTEXT_WINDOW_THRESHOLD
|
|
629
436
|
) {
|
|
630
|
-
await this.
|
|
437
|
+
await this.sessions.rotateCoordinatorSession(chatId);
|
|
438
|
+
await this.telegram?.sendMessage(
|
|
439
|
+
Number(chatId),
|
|
440
|
+
"<i>Context refreshed.</i>",
|
|
441
|
+
);
|
|
631
442
|
}
|
|
632
443
|
} catch (err) {
|
|
633
444
|
console.error(`[ppmbot] processMessage error for ${chatId}:`, (err as Error).message);
|
|
@@ -638,7 +449,6 @@ Always execute the appropriate CLI command — do NOT just describe what you wou
|
|
|
638
449
|
} finally {
|
|
639
450
|
this.processing.delete(chatId);
|
|
640
451
|
|
|
641
|
-
// Process queued messages
|
|
642
452
|
const queued = this.messageQueue.get(chatId);
|
|
643
453
|
if (queued && queued.length > 0) {
|
|
644
454
|
this.messageQueue.delete(chatId);
|
|
@@ -648,21 +458,36 @@ Always execute the appropriate CLI command — do NOT just describe what you wou
|
|
|
648
458
|
}
|
|
649
459
|
}
|
|
650
460
|
|
|
651
|
-
// ──
|
|
461
|
+
// ── Restart Notification ────────────────────────────────────────
|
|
652
462
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
463
|
+
async checkRestartNotification(): Promise<void> {
|
|
464
|
+
try {
|
|
465
|
+
const { existsSync, readFileSync: fsRead, unlinkSync } = await import("node:fs");
|
|
466
|
+
const markerPath = join(homedir(), ".ppm", "restart-notify.json");
|
|
467
|
+
if (!existsSync(markerPath)) return;
|
|
468
|
+
|
|
469
|
+
const data = JSON.parse(fsRead(markerPath, "utf-8"));
|
|
470
|
+
unlinkSync(markerPath);
|
|
471
|
+
|
|
472
|
+
if (Date.now() - data.ts > 60_000) return;
|
|
473
|
+
|
|
474
|
+
let version = "";
|
|
475
|
+
try {
|
|
476
|
+
const pkgPath = join(import.meta.dir, "../../../package.json");
|
|
477
|
+
const pkg = await Bun.file(pkgPath).json();
|
|
478
|
+
version = pkg.version ? ` v${pkg.version}` : "";
|
|
479
|
+
} catch {}
|
|
480
|
+
|
|
481
|
+
for (const cid of data.chatIds) {
|
|
482
|
+
await this.telegram?.sendMessage(Number(cid), `✅ PPM${version} restarted successfully.`);
|
|
483
|
+
}
|
|
484
|
+
} catch {}
|
|
660
485
|
}
|
|
661
486
|
|
|
662
487
|
// ── Helpers ─────────────────────────────────────────────────────
|
|
663
488
|
|
|
664
489
|
private generatePairingCode(): string {
|
|
665
|
-
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
490
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
666
491
|
const bytes = crypto.getRandomValues(new Uint8Array(6));
|
|
667
492
|
return Array.from(bytes, (b) => chars[b % chars.length]).join("");
|
|
668
493
|
}
|