@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
|
@@ -39,4 +39,12 @@ export const postgresAdapter: DatabaseAdapter = {
|
|
|
39
39
|
opts.pkColumn, opts.pkValue, opts.column, opts.value,
|
|
40
40
|
);
|
|
41
41
|
},
|
|
42
|
+
|
|
43
|
+
async deleteRow(config: DbConnectionConfig, table: string, opts): Promise<void> {
|
|
44
|
+
if (!config.connectionString) throw new Error("Missing connectionString");
|
|
45
|
+
await postgresService.deleteRow(
|
|
46
|
+
config.connectionString, table, opts.schema ?? "public",
|
|
47
|
+
opts.pkColumn, opts.pkValue,
|
|
48
|
+
);
|
|
49
|
+
},
|
|
42
50
|
};
|
|
@@ -51,4 +51,9 @@ export const sqliteAdapter: DatabaseAdapter = {
|
|
|
51
51
|
if (!config.path) throw new Error("Missing path");
|
|
52
52
|
sqliteService.updateCell(config.path, config.path, table, opts.pkValue as number, opts.column, opts.value, opts.pkColumn);
|
|
53
53
|
},
|
|
54
|
+
|
|
55
|
+
async deleteRow(config: DbConnectionConfig, table: string, opts): Promise<void> {
|
|
56
|
+
if (!config.path) throw new Error("Missing path");
|
|
57
|
+
sqliteService.deleteRow(config.path, config.path, table, opts.pkValue, opts.pkColumn);
|
|
58
|
+
},
|
|
54
59
|
};
|
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { mkdirSync, existsSync } from "node:fs";
|
|
5
5
|
|
|
6
6
|
const PPM_DIR = process.env.PPM_HOME || resolve(homedir(), ".ppm");
|
|
7
|
-
const CURRENT_SCHEMA_VERSION =
|
|
7
|
+
const CURRENT_SCHEMA_VERSION = 14;
|
|
8
8
|
|
|
9
9
|
let db: Database | null = null;
|
|
10
10
|
let dbProfile: string | null = null;
|
|
@@ -387,6 +387,33 @@ function runMigrations(database: Database): void {
|
|
|
387
387
|
PRAGMA user_version = 13;
|
|
388
388
|
`);
|
|
389
389
|
}
|
|
390
|
+
|
|
391
|
+
if (current < 14) {
|
|
392
|
+
database.exec(`
|
|
393
|
+
CREATE TABLE IF NOT EXISTS bot_tasks (
|
|
394
|
+
id TEXT PRIMARY KEY,
|
|
395
|
+
chat_id TEXT NOT NULL,
|
|
396
|
+
project_name TEXT NOT NULL,
|
|
397
|
+
project_path TEXT NOT NULL,
|
|
398
|
+
prompt TEXT NOT NULL,
|
|
399
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
400
|
+
result_summary TEXT,
|
|
401
|
+
result_full TEXT,
|
|
402
|
+
session_id TEXT,
|
|
403
|
+
error TEXT,
|
|
404
|
+
reported INTEGER NOT NULL DEFAULT 0,
|
|
405
|
+
timeout_ms INTEGER DEFAULT 900000,
|
|
406
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
407
|
+
started_at INTEGER,
|
|
408
|
+
completed_at INTEGER
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
CREATE INDEX IF NOT EXISTS idx_bot_tasks_status ON bot_tasks(status);
|
|
412
|
+
CREATE INDEX IF NOT EXISTS idx_bot_tasks_chat ON bot_tasks(chat_id, created_at DESC);
|
|
413
|
+
|
|
414
|
+
PRAGMA user_version = 14;
|
|
415
|
+
`);
|
|
416
|
+
}
|
|
390
417
|
}
|
|
391
418
|
|
|
392
419
|
// ---------------------------------------------------------------------------
|
|
@@ -1188,5 +1215,86 @@ export function isPairedChat(chatId: string): boolean {
|
|
|
1188
1215
|
return row != null;
|
|
1189
1216
|
}
|
|
1190
1217
|
|
|
1218
|
+
// ---------------------------------------------------------------------------
|
|
1219
|
+
// Bot Tasks helpers
|
|
1220
|
+
// ---------------------------------------------------------------------------
|
|
1221
|
+
|
|
1222
|
+
import type { BotTask, BotTaskStatus } from "../types/ppmbot.ts";
|
|
1223
|
+
|
|
1224
|
+
export function createBotTask(
|
|
1225
|
+
id: string, chatId: string, projectName: string, projectPath: string,
|
|
1226
|
+
prompt: string, timeoutMs = 900000,
|
|
1227
|
+
): void {
|
|
1228
|
+
getDb().query(
|
|
1229
|
+
`INSERT INTO bot_tasks (id, chat_id, project_name, project_path, prompt, timeout_ms)
|
|
1230
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
1231
|
+
).run(id, chatId, projectName, projectPath, prompt, timeoutMs);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
export function updateBotTaskStatus(
|
|
1235
|
+
id: string, status: BotTaskStatus,
|
|
1236
|
+
updates?: { sessionId?: string; resultSummary?: string; resultFull?: string; error?: string },
|
|
1237
|
+
): void {
|
|
1238
|
+
const sets = ["status = ?"];
|
|
1239
|
+
const params: (string | number | null)[] = [status];
|
|
1240
|
+
|
|
1241
|
+
if (status === "running") {
|
|
1242
|
+
sets.push("started_at = unixepoch()");
|
|
1243
|
+
}
|
|
1244
|
+
if (status === "completed" || status === "failed" || status === "timeout") {
|
|
1245
|
+
sets.push("completed_at = unixepoch()");
|
|
1246
|
+
}
|
|
1247
|
+
if (updates?.sessionId != null) { sets.push("session_id = ?"); params.push(updates.sessionId); }
|
|
1248
|
+
if (updates?.resultSummary != null) { sets.push("result_summary = ?"); params.push(updates.resultSummary); }
|
|
1249
|
+
if (updates?.resultFull != null) { sets.push("result_full = ?"); params.push(updates.resultFull); }
|
|
1250
|
+
if (updates?.error != null) { sets.push("error = ?"); params.push(updates.error); }
|
|
1251
|
+
|
|
1252
|
+
params.push(id);
|
|
1253
|
+
getDb().query(`UPDATE bot_tasks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
export function markBotTaskReported(id: string): void {
|
|
1257
|
+
getDb().query("UPDATE bot_tasks SET reported = 1 WHERE id = ?").run(id);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
export function getBotTask(id: string): BotTask | null {
|
|
1261
|
+
const row = getDb().query("SELECT * FROM bot_tasks WHERE id = ?").get(id) as Record<string, any> | null;
|
|
1262
|
+
return row ? mapBotTaskRow(row) : null;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
export function getRecentBotTasks(chatId: string, limit = 20): BotTask[] {
|
|
1266
|
+
const rows = getDb().query(
|
|
1267
|
+
"SELECT * FROM bot_tasks WHERE chat_id = ? ORDER BY created_at DESC LIMIT ?",
|
|
1268
|
+
).all(chatId, limit) as Record<string, any>[];
|
|
1269
|
+
return rows.map(mapBotTaskRow);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
export function getRunningBotTasks(): BotTask[] {
|
|
1273
|
+
const rows = getDb().query(
|
|
1274
|
+
"SELECT * FROM bot_tasks WHERE status IN ('pending', 'running') ORDER BY created_at ASC",
|
|
1275
|
+
).all() as Record<string, any>[];
|
|
1276
|
+
return rows.map(mapBotTaskRow);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function mapBotTaskRow(row: Record<string, any>): BotTask {
|
|
1280
|
+
return {
|
|
1281
|
+
id: row.id,
|
|
1282
|
+
chatId: row.chat_id,
|
|
1283
|
+
projectName: row.project_name,
|
|
1284
|
+
projectPath: row.project_path,
|
|
1285
|
+
prompt: row.prompt,
|
|
1286
|
+
status: row.status,
|
|
1287
|
+
resultSummary: row.result_summary,
|
|
1288
|
+
resultFull: row.result_full,
|
|
1289
|
+
sessionId: row.session_id,
|
|
1290
|
+
error: row.error,
|
|
1291
|
+
reported: !!row.reported,
|
|
1292
|
+
timeoutMs: row.timeout_ms,
|
|
1293
|
+
createdAt: row.created_at,
|
|
1294
|
+
startedAt: row.started_at,
|
|
1295
|
+
completedAt: row.completed_at,
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1191
1299
|
// Auto-close on process exit
|
|
1192
1300
|
process.on("beforeExit", closeDb);
|
|
@@ -161,6 +161,18 @@ class PostgresService {
|
|
|
161
161
|
);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/** Delete a row by primary key */
|
|
165
|
+
async deleteRow(
|
|
166
|
+
connectionString: string, table: string, schema = "public",
|
|
167
|
+
pkColumn: string, pkValue: unknown,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
const sql = this.connect(connectionString);
|
|
170
|
+
await sql.unsafe(
|
|
171
|
+
`DELETE FROM "${schema}"."${table}" WHERE "${pkColumn}" = $1`,
|
|
172
|
+
[pkValue as never],
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
164
176
|
/** Close all cached connections */
|
|
165
177
|
async closeAll() {
|
|
166
178
|
for (const key of this.cache.keys()) await this.disconnect(key);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { chatService } from "../chat.service.ts";
|
|
2
|
+
import { configService } from "../config.service.ts";
|
|
3
|
+
import { getBotTask, updateBotTaskStatus } from "../db.service.ts";
|
|
4
|
+
import { escapeHtml } from "./ppmbot-formatter.ts";
|
|
5
|
+
import type { PPMBotTelegram } from "./ppmbot-telegram.ts";
|
|
6
|
+
import type { ChatEvent } from "../../types/chat.ts";
|
|
7
|
+
import type { PermissionMode } from "../../types/config.ts";
|
|
8
|
+
|
|
9
|
+
/** Active background tasks: taskId -> AbortController */
|
|
10
|
+
const activeTasks = new Map<string, AbortController>();
|
|
11
|
+
|
|
12
|
+
export async function executeDelegation(
|
|
13
|
+
taskId: string,
|
|
14
|
+
telegram: PPMBotTelegram,
|
|
15
|
+
providerId: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const task = getBotTask(taskId);
|
|
18
|
+
if (!task || task.status !== "pending") return;
|
|
19
|
+
|
|
20
|
+
// Guard against double-execution
|
|
21
|
+
if (activeTasks.has(taskId)) return;
|
|
22
|
+
|
|
23
|
+
const abort = new AbortController();
|
|
24
|
+
activeTasks.set(taskId, abort);
|
|
25
|
+
|
|
26
|
+
updateBotTaskStatus(taskId, "running");
|
|
27
|
+
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
abort.abort();
|
|
30
|
+
activeTasks.delete(taskId);
|
|
31
|
+
updateBotTaskStatus(taskId, "timeout");
|
|
32
|
+
telegram.sendMessage(
|
|
33
|
+
Number(task.chatId),
|
|
34
|
+
`⏱ Task timed out: <i>${escapeHtml(task.prompt.slice(0, 80))}</i>`,
|
|
35
|
+
);
|
|
36
|
+
}, task.timeoutMs);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const session = await chatService.createSession(providerId, {
|
|
40
|
+
projectPath: task.projectPath,
|
|
41
|
+
projectName: task.projectName,
|
|
42
|
+
title: `[PPMBot] ${task.prompt.slice(0, 50)}`,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const opts = { permissionMode: "bypassPermissions" as PermissionMode };
|
|
46
|
+
const events = chatService.sendMessage(providerId, session.id, task.prompt, opts);
|
|
47
|
+
|
|
48
|
+
let fullText = "";
|
|
49
|
+
let lastAssistantText = "";
|
|
50
|
+
|
|
51
|
+
for await (const event of events) {
|
|
52
|
+
if (abort.signal.aborted) break;
|
|
53
|
+
|
|
54
|
+
if (event.type === "text") {
|
|
55
|
+
lastAssistantText = event.content;
|
|
56
|
+
fullText += event.content;
|
|
57
|
+
}
|
|
58
|
+
if (event.type === "done" && event.resultSubtype === "success") {
|
|
59
|
+
// done event — session finished
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
activeTasks.delete(taskId);
|
|
65
|
+
|
|
66
|
+
if (abort.signal.aborted) return;
|
|
67
|
+
|
|
68
|
+
const summary = lastAssistantText.slice(0, 500) || "Task completed (no text output)";
|
|
69
|
+
|
|
70
|
+
updateBotTaskStatus(taskId, "completed", {
|
|
71
|
+
sessionId: session.id,
|
|
72
|
+
resultSummary: summary,
|
|
73
|
+
resultFull: fullText.trim(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await telegram.sendMessage(
|
|
77
|
+
Number(task.chatId),
|
|
78
|
+
`✅ <b>${escapeHtml(task.projectName)}</b> task done\n\n` +
|
|
79
|
+
`<i>${escapeHtml(task.prompt.slice(0, 80))}</i>\n\n` +
|
|
80
|
+
`${escapeHtml(summary.slice(0, 300))}` +
|
|
81
|
+
(summary.length > 300 ? "…" : ""),
|
|
82
|
+
);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
activeTasks.delete(taskId);
|
|
86
|
+
|
|
87
|
+
if (abort.signal.aborted) return;
|
|
88
|
+
|
|
89
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
updateBotTaskStatus(taskId, "failed", { error: errorMsg });
|
|
91
|
+
|
|
92
|
+
await telegram.sendMessage(
|
|
93
|
+
Number(task.chatId),
|
|
94
|
+
`❌ Task failed (<b>${escapeHtml(task.projectName)}</b>)\n\n` +
|
|
95
|
+
`<i>${escapeHtml(task.prompt.slice(0, 80))}</i>\n\n` +
|
|
96
|
+
`Error: ${escapeHtml(errorMsg.slice(0, 200))}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function cancelDelegation(taskId: string): boolean {
|
|
102
|
+
const abort = activeTasks.get(taskId);
|
|
103
|
+
if (!abort) return false;
|
|
104
|
+
abort.abort();
|
|
105
|
+
activeTasks.delete(taskId);
|
|
106
|
+
updateBotTaskStatus(taskId, "failed", { error: "Cancelled by user" });
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getActiveDelegationCount(): number {
|
|
111
|
+
return activeTasks.size;
|
|
112
|
+
}
|