@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/web/assets/{chat-tab-DvNEQYEe.js → chat-tab-SfXtOm9d.js} +1 -1
  3. package/dist/web/assets/{code-editor-CoT017Ah.js → code-editor-DAZvtAlT.js} +1 -1
  4. package/dist/web/assets/database-viewer-C5fco1jm.js +1 -0
  5. package/dist/web/assets/{diff-viewer-D0tuen4I.js → diff-viewer-ShRSPvsf.js} +1 -1
  6. package/dist/web/assets/{extension-webview-Ba5aeo9r.js → extension-webview-CWJRMPfV.js} +1 -1
  7. package/dist/web/assets/{git-graph-BnJrVPxJ.js → git-graph-h0QmXMdZ.js} +1 -1
  8. package/dist/web/assets/{index-DUQgLj0D.js → index-CDlrGSwd.js} +4 -4
  9. package/dist/web/assets/{index-BEfMoc_W.css → index-DVuSY0BZ.css} +1 -1
  10. package/dist/web/assets/keybindings-store-wbHg-S_v.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-BuGSrE3y.js → markdown-renderer-CSEmmMWt.js} +1 -1
  12. package/dist/web/assets/{port-forwarding-tab-DsbrWNUP.js → port-forwarding-tab-Cts6tMFn.js} +1 -1
  13. package/dist/web/assets/{postgres-viewer-Bh6YmZPq.js → postgres-viewer-CiQC1sf9.js} +1 -1
  14. package/dist/web/assets/{settings-tab-BnzFtexC.js → settings-tab-CQx6aHtO.js} +1 -1
  15. package/dist/web/assets/sqlite-viewer-FQfCkjU6.js +1 -0
  16. package/dist/web/assets/{terminal-tab-fnZvscaH.js → terminal-tab-C2SnOqxn.js} +1 -1
  17. package/dist/web/assets/{use-monaco-theme-BdcKAZ69.js → use-monaco-theme-VPgvhMpB.js} +1 -1
  18. package/dist/web/index.html +2 -2
  19. package/dist/web/sw.js +1 -1
  20. package/docs/codebase-summary.md +52 -13
  21. package/docs/project-changelog.md +45 -1
  22. package/docs/project-roadmap.md +1 -1
  23. package/docs/system-architecture.md +121 -9
  24. package/package.json +1 -1
  25. package/src/cli/commands/bot-cmd.ts +144 -240
  26. package/src/server/routes/database.ts +31 -0
  27. package/src/server/routes/settings.ts +13 -0
  28. package/src/server/routes/sqlite.ts +14 -0
  29. package/src/services/database/postgres-adapter.ts +8 -0
  30. package/src/services/database/sqlite-adapter.ts +5 -0
  31. package/src/services/db.service.ts +109 -1
  32. package/src/services/postgres.service.ts +12 -0
  33. package/src/services/ppmbot/ppmbot-delegation.ts +112 -0
  34. package/src/services/ppmbot/ppmbot-service.ts +194 -369
  35. package/src/services/ppmbot/ppmbot-session.ts +85 -108
  36. package/src/services/ppmbot/ppmbot-telegram.ts +5 -16
  37. package/src/services/sqlite.service.ts +10 -0
  38. package/src/types/config.ts +1 -3
  39. package/src/types/database.ts +3 -0
  40. package/src/types/ppmbot.ts +21 -0
  41. package/src/web/components/database/database-viewer.tsx +50 -8
  42. package/src/web/components/database/use-database.ts +13 -1
  43. package/src/web/components/settings/ppmbot-settings-section.tsx +87 -26
  44. package/src/web/components/sqlite/sqlite-data-grid.tsx +55 -8
  45. package/src/web/components/sqlite/sqlite-viewer.tsx +1 -0
  46. package/src/web/components/sqlite/use-sqlite.ts +16 -1
  47. package/dist/web/assets/database-viewer-C3wK7cDk.js +0 -1
  48. package/dist/web/assets/keybindings-store-CkGFjxkX.js +0 -1
  49. package/dist/web/assets/sqlite-viewer-Cu3_hf07.js +0 -1
  50. package/docs/streaming-input-guide.md +0 -267
  51. package/snapshot-state.md +0 -1526
  52. package/test-session-ops.mjs +0 -444
  53. package/test-tokens.mjs +0 -212
@@ -193,14 +193,21 @@ Tab IDs are deterministic: `{type}:{identifier}` (e.g., `editor:src/index.ts`, `
193
193
  | **AccountService** | Account CRUD, token encryption/decryption | getAccounts, createAccount, updateAccount, deleteAccount |
194
194
  | **AccountSelectorService** | Select active account based on config | getActiveAccount, setActiveAccount, selectByProject |
195
195
  | **UpgradeService** | Version checking, installation, self-replace signaling | checkForUpdate, applyUpgrade, getInstallMethod, compareSemver |
196
- | **ClawBotService** | Telegram bot orchestrator | start, stop, handleMessage, routeToChat |
197
- | **ClawBotTelegramService** | Telegram long-polling, message ops | getUpdates, sendMessage, editMessage, setTyping, handleCommands |
198
- | **ClawBotSessionService** | chatID PPM sessionID mapping | mapSession, getSession, deleteSession |
199
- | **ClawBotMemoryService** | FTS5 memory persistence | saveMemory, recallMemories, decayMemories, searchByProject |
200
- | **ClawBotFormatterService** | Markdown Telegram HTML + chunking | formatMarkdown, chunkMessage |
201
- | **ClawBotStreamerService** | ChatEventprogressive Telegram edits | streamMessageEdits |
202
-
203
- **Key Files:** `src/services/*.service.ts`, `src/services/clawbot/*.ts`
196
+ | **PPMBotService** | Coordinator orchestrator (team leader, delegation mgmt) | start, stop, handleUpdate, checkPendingTasks |
197
+ | **PPMBotSessionManager** | Coordinator session per chat, project resolver | getCoordinatorSession, rotateCoordinatorSession, resolveProject |
198
+ | **PPMBotTelegramService** | Telegram long-polling, message ops | getUpdates, sendMessage, editMessage, setTyping, handleCommands |
199
+ | **PPMBotMemoryService** | SQLite project memory persistence | saveMemory, recallMemories, searchByProject |
200
+ | **executeDelegation()** | Task execution in isolated session, result capture | (async function, manages ChatService + result storage) |
201
+ | **PPMBotFormatterService** | Markdown → Telegram HTML + chunking | formatMarkdown, chunkMessage |
202
+ | **PPMBotStreamerService** | ChatEvent → progressive Telegram edits | streamMessageEdits |
203
+ | **ClawBotService** | LEGACY Telegram bot (deprecated v0.9.11) | (direct-chat model, replaced by coordinator) |
204
+ | **ClawBotTelegramService** | LEGACY Telegram API | (deprecated v0.9.11) |
205
+ | **ClawBotSessionService** | LEGACY chatID mapping | (deprecated v0.9.11) |
206
+ | **ClawBotMemoryService** | LEGACY FTS5 memory | (deprecated v0.9.11) |
207
+ | **ClawBotFormatterService** | LEGACY formatter | (deprecated v0.9.11) |
208
+ | **ClawBotStreamerService** | LEGACY streamer | (deprecated v0.9.11) |
209
+
210
+ **Key Files:** `src/services/*.service.ts`, `src/services/ppmbot/*.ts`, `src/cli/commands/bot-cmd.ts`
204
211
 
205
212
  ---
206
213
 
@@ -269,7 +276,112 @@ PPM supports multiple AI providers through a generic `AIProvider` interface and
269
276
 
270
277
  ---
271
278
 
272
- ### ClawBot Service Layer (Telegram Bot Integration)
279
+ ### PPMBot Coordinator Service Layer (Telegram-based Team Leader)
280
+ **Component:** PPMBot coordinator orchestrator + delegation executor
281
+
282
+ **Responsibilities:**
283
+ - Manage single persistent coordinator session per Telegram chat in `~/.ppm/bot/` workspace
284
+ - Route incoming Telegram messages to coordinator (ask/answer) or delegation tracking
285
+ - Decide when to answer directly vs. delegate to subagents (based on project context)
286
+ - Execute delegated tasks in isolated project sessions
287
+ - Track task status and report results back to Telegram
288
+ - Format responses as Telegram HTML with progressive message editing
289
+
290
+ **Architecture:**
291
+ ```
292
+ Telegram → PPMBotTelegramService (polling) → PPMBotService (orchestrator)
293
+
294
+ PPMBotSessionManager (coordinator session per chat)
295
+ coordinatorSession.id → chatService.sendMessage()
296
+ Task Poller (5s interval)
297
+
298
+ executeDelegation(taskId, telegram, providerId)
299
+ ├─ getBotTask(taskId) → prompt
300
+ ├─ chatService.createSession(providerId, projectPath)
301
+ ├─ run async generator (abort, 900s timeout)
302
+ └─ updateBotTaskStatus(taskId, "completed", {result})
303
+
304
+ telegram.sendMessage(chatId, result summary)
305
+ ```
306
+
307
+ **Services (src/services/ppmbot/):**
308
+ - **PPMBotService** — Lifecycle (start/stop), message queue, Telegram polling loop, task poller loop
309
+ - **PPMBotSessionManager** — Coordinator session cache per chatID, project resolver (case-insensitive, prefix match)
310
+ - **PPMBotTelegramService** — Telegram Bot API (getUpdates polling, sendMessage, editMessage, setTyping)
311
+ - **PPMBotMemoryService** — SQLite project memories, contextual recall
312
+ - **executeDelegation()** — Task execution in isolated session, result capture, timeout/abort handling
313
+ - **PPMBotFormatterService** — Markdown → Telegram HTML, 4096-char chunking
314
+ - **PPMBotStreamerService** — ChatEvent → progressive Telegram message edits (1s throttle)
315
+
316
+ **Coordinator Identity (Persistent Cross-Provider):**
317
+ - Location: `~/.ppm/bot/coordinator.md` (loaded on startup, cached in `coordinatorIdentity`)
318
+ - Role definition: Team leader, project coordinator, decision-maker
319
+ - Decision framework: Answer directly (no project context) vs. Delegate (file access needed)
320
+ - Coordination tools: Bash-safe CLI commands (`ppm bot delegate`, `ppm bot task-status`, etc.)
321
+ - Cross-provider: Identity text injected as XML context block, works with Claude SDK + CLI providers
322
+
323
+ **Delegation Flow:**
324
+ 1. User asks task in Telegram
325
+ 2. Coordinator decides: delegate? → yes
326
+ 3. Coordinator calls bash: `ppm bot delegate --chat <chatId> --project <name> --prompt "<enriched>"`
327
+ 4. CLI creates `bot_tasks` row, returns taskId
328
+ 5. Service tells user: "Working on it, I'll notify you when done"
329
+ 6. Background poller (5s) detects pending task
330
+ 7. Executes: `chatService.createSession()` in target project
331
+ 8. Streams response, captures summary + full output
332
+ 9. Updates task status → "completed"
333
+ 10. Sends Telegram notification with result
334
+
335
+ **Task Execution (Isolation & Safety):**
336
+ - Each task = fresh isolated session (no shared context)
337
+ - Timeout: 900s default (configurable per task)
338
+ - Abort: AbortController on timeout, can be canceled mid-execution
339
+ - Result capture: Both summary (for notification) and full text (for detailed review)
340
+ - Error handling: Task status → "failed", error message stored, user notified
341
+
342
+ **Database Schema (v14):**
343
+ - `bot_tasks` — id (UUID), chatId, projectName, projectPath, prompt, status, resultSummary, resultFull, sessionId, error, reported, timeoutMs, createdAt, startedAt, completedAt
344
+ - Indexes: `idx_bot_tasks_status` (fast poller lookup), `idx_bot_tasks_chat` (history queries)
345
+
346
+ **Key Design Decisions:**
347
+ 1. **Single coordinator session** — Per chat, persistent, one identity (vs. per-task sessions in ClawBot)
348
+ 2. **Delegation via CLI** — Coordinator calls bash commands (safer than direct DB writes, auditable)
349
+ 3. **Isolated task execution** — Each delegated task spawns fresh session (no context bleed)
350
+ 4. **Background polling** — Task execution decoupled from message handler (non-blocking)
351
+ 5. **Result summary + full** — Notification shows short summary; user can fetch full output via CLI
352
+ 6. **Cross-provider identity** — Single `coordinator.md` works with any AI provider
353
+ 7. **Bash-safe tools only** — Coordinator restricted to Bash, Read, Write, Edit, Glob, Grep (safe delegation)
354
+
355
+ **CLI Expansion (ppm bot commands):**
356
+ ```
357
+ ppm bot delegate --chat <id> --project <name> --prompt "<text>" # Create task
358
+ ppm bot task-status <id> # Check status
359
+ ppm bot task-result <id> # Get full output
360
+ ppm bot tasks [--chat <id>] # List recent
361
+ ppm bot project list # Available projects
362
+ ppm bot project current # Active project
363
+ ppm bot project switch <name> # Switch project
364
+ ppm bot session new <title> # Create session
365
+ ppm bot session list # List sessions
366
+ ppm bot session resume <id> # Resume session
367
+ ppm bot session stop <id> # Stop session
368
+ ppm bot status # Bot health
369
+ ppm bot version # PPM version
370
+ ppm bot restart # Restart service
371
+ ppm bot help # Help
372
+ ```
373
+
374
+ **Settings UI (ppmbot-settings-section.tsx):**
375
+ - Enable/disable PPMBot
376
+ - Paired Telegram chats (approval management)
377
+ - Default project selection
378
+ - System prompt customization
379
+ - Task auto-refresh (poll interval, max history)
380
+ - Delegated tasks panel (status, result preview, delete)
381
+
382
+ ---
383
+
384
+ ### ClawBot Service Layer (Telegram Bot Integration) — LEGACY (v0.9.10)
273
385
  **Component:** Telegram bot service + subsidiary services
274
386
 
275
387
  **Responsibilities:**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.53",
3
+ "version": "0.9.55",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -32,20 +32,133 @@ export async function resolveChatId(chatOpt?: string): Promise<string> {
32
32
  }
33
33
 
34
34
  /**
35
- * `ppm bot` CLI — allows AI (via Bash tool) to manage PPMBot sessions,
36
- * projects, memories, and server operations through natural language.
37
- *
38
- * All session/project commands auto-detect the paired Telegram chat.
35
+ * `ppm bot` CLI — coordinator-era commands: delegation, memory, project list, status.
39
36
  */
40
37
  export function registerBotCommands(program: Command): void {
41
38
  const bot = program.command("bot").description("PPMBot utilities");
42
39
 
40
+ registerDelegationCommands(bot);
43
41
  registerMemoryCommands(bot);
44
42
  registerProjectCommands(bot);
45
- registerSessionCommands(bot);
46
43
  registerMiscCommands(bot);
47
44
  }
48
45
 
46
+ // ── Delegation ─────────────────────────────────────────────────────
47
+
48
+ function registerDelegationCommands(bot: Command): void {
49
+ bot
50
+ .command("delegate")
51
+ .description("Delegate a task to a project subagent")
52
+ .requiredOption("--chat <id>", "Telegram chat ID")
53
+ .requiredOption("--project <name>", "Project name")
54
+ .requiredOption("--prompt <text>", "Enriched task prompt")
55
+ .option("--timeout <ms>", "Timeout in milliseconds", "900000")
56
+ .action(async (opts: { chat: string; project: string; prompt: string; timeout: string }) => {
57
+ try {
58
+ const { PPMBotSessionManager } = await import("../../services/ppmbot/ppmbot-session.ts");
59
+ const sessions = new PPMBotSessionManager();
60
+ const project = sessions.resolveProject(opts.project);
61
+ if (!project) {
62
+ console.error(`${C.red}✗${C.reset} Project not found: ${opts.project}`);
63
+ process.exit(1);
64
+ }
65
+
66
+ const taskId = crypto.randomUUID();
67
+ const { createBotTask } = await import("../../services/db.service.ts");
68
+ createBotTask(taskId, opts.chat, project.name, project.path, opts.prompt, Number(opts.timeout) || 900000);
69
+
70
+ console.log(JSON.stringify({ taskId, project: project.name, status: "pending" }));
71
+ } catch (e) {
72
+ console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
73
+ process.exit(1);
74
+ }
75
+ });
76
+
77
+ bot
78
+ .command("task-status <id>")
79
+ .description("Get status of a delegated task")
80
+ .action(async (id: string) => {
81
+ try {
82
+ const { getBotTask } = await import("../../services/db.service.ts");
83
+ const task = getBotTask(id);
84
+ if (!task) {
85
+ console.error(`${C.red}✗${C.reset} Task not found: ${id}`);
86
+ process.exit(1);
87
+ }
88
+ const elapsed = task.startedAt
89
+ ? Math.round((Date.now() / 1000 - task.startedAt) / 60)
90
+ : 0;
91
+ console.log(JSON.stringify({
92
+ id: task.id,
93
+ status: task.status,
94
+ project: task.projectName,
95
+ prompt: task.prompt.slice(0, 100),
96
+ elapsed: `${elapsed}m`,
97
+ summary: task.resultSummary?.slice(0, 200) ?? null,
98
+ }));
99
+ } catch (e) {
100
+ console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
101
+ process.exit(1);
102
+ }
103
+ });
104
+
105
+ bot
106
+ .command("task-result <id>")
107
+ .description("Get full result of a completed task")
108
+ .action(async (id: string) => {
109
+ try {
110
+ const { getBotTask } = await import("../../services/db.service.ts");
111
+ const task = getBotTask(id);
112
+ if (!task) {
113
+ console.error(`${C.red}✗${C.reset} Task not found: ${id}`);
114
+ process.exit(1);
115
+ }
116
+ if (task.status === "completed") {
117
+ console.log(task.resultFull ?? "(no output)");
118
+ } else if (task.status === "failed") {
119
+ console.error(`Task failed: ${task.error ?? "unknown error"}`);
120
+ } else {
121
+ console.log(`Task status: ${task.status} (not completed yet)`);
122
+ }
123
+ } catch (e) {
124
+ console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
125
+ process.exit(1);
126
+ }
127
+ });
128
+
129
+ bot
130
+ .command("tasks")
131
+ .description("List recent delegated tasks")
132
+ .option("--chat <id>", "Telegram chat ID (auto-detected if single)")
133
+ .action(async (opts: { chat?: string }) => {
134
+ try {
135
+ const chatId = await resolveChatId(opts.chat);
136
+ const { getRecentBotTasks } = await import("../../services/db.service.ts");
137
+ const tasks = getRecentBotTasks(chatId, 20);
138
+
139
+ if (tasks.length === 0) {
140
+ console.log(`${C.dim}No delegated tasks found.${C.reset}`);
141
+ return;
142
+ }
143
+
144
+ const statusIcon: Record<string, string> = {
145
+ pending: "⏳", running: "🔄", completed: "✅", failed: "❌", timeout: "⏱",
146
+ };
147
+
148
+ for (const t of tasks) {
149
+ const icon = statusIcon[t.status] ?? "?";
150
+ const sid = t.id.slice(0, 8);
151
+ const prompt = t.prompt.slice(0, 50);
152
+ console.log(` ${icon} ${C.dim}${sid}${C.reset} ${C.bold}${t.projectName}${C.reset} — ${prompt}`);
153
+ }
154
+ console.log(`\n${C.dim}${tasks.length} tasks${C.reset}`);
155
+ } catch (e) {
156
+ console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
157
+ process.exit(1);
158
+ }
159
+ });
160
+ }
161
+
49
162
  // ── Memory ──────────────────────────────────────────────────────────
50
163
 
51
164
  function registerMemoryCommands(bot: Command): void {
@@ -147,202 +260,9 @@ function registerProjectCommands(bot: Command): void {
147
260
  return;
148
261
  }
149
262
 
150
- // Show current project if possible
151
- let current = "";
152
- try {
153
- const chatId = await resolveChatId();
154
- const active = sessions.getActiveSession(chatId);
155
- current = active?.projectName ?? "";
156
- } catch { /* no chat — skip marker */ }
157
-
158
263
  for (const name of projects) {
159
- const marker = name === current ? ` ${C.green}✓${C.reset}` : "";
160
- console.log(` ${name}${marker}`);
161
- }
162
- } catch (e) {
163
- console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
164
- process.exit(1);
165
- }
166
- });
167
-
168
- proj
169
- .command("switch <name>")
170
- .description("Switch to a different project")
171
- .option("--chat <id>", "Telegram chat ID (auto-detected if single)")
172
- .action(async (name: string, opts: { chat?: string }) => {
173
- try {
174
- const chatId = await resolveChatId(opts.chat);
175
- const { PPMBotSessionManager } = await import("../../services/ppmbot/ppmbot-session.ts");
176
- const sessions = new PPMBotSessionManager();
177
- const session = await sessions.switchProject(chatId, name);
178
- console.log(`${C.green}✓${C.reset} Switched to ${C.bold}${session.projectName}${C.reset}`);
179
- } catch (e) {
180
- console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
181
- process.exit(1);
182
- }
183
- });
184
-
185
- proj
186
- .command("current")
187
- .description("Show current project")
188
- .option("--chat <id>", "Telegram chat ID (auto-detected if single)")
189
- .action(async (opts: { chat?: string }) => {
190
- try {
191
- const chatId = await resolveChatId(opts.chat);
192
- const { PPMBotSessionManager } = await import("../../services/ppmbot/ppmbot-session.ts");
193
- const sessions = new PPMBotSessionManager();
194
- const active = sessions.getActiveSession(chatId);
195
-
196
- // Fallback: check DB for active session
197
- if (!active) {
198
- const { getActivePPMBotSession } = await import("../../services/db.service.ts");
199
- const { configService } = await import("../../services/config.service.ts");
200
- const projects = (configService.get("projects") as any[]) ?? [];
201
- for (const p of projects) {
202
- const dbSession = getActivePPMBotSession(chatId, p.name);
203
- if (dbSession) {
204
- console.log(dbSession.project_name);
205
- return;
206
- }
207
- }
208
- console.log(`${C.dim}No active project. Use: ppm bot project switch <name>${C.reset}`);
209
- return;
264
+ console.log(` ${name}`);
210
265
  }
211
- console.log(active.projectName);
212
- } catch (e) {
213
- console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
214
- process.exit(1);
215
- }
216
- });
217
- }
218
-
219
- // ── Session ─────────────────────────────────────────────────────────
220
-
221
- function registerSessionCommands(bot: Command): void {
222
- const sess = bot.command("session").description("Manage chat sessions");
223
-
224
- sess
225
- .command("new")
226
- .description("Start a fresh session (current project)")
227
- .option("--chat <id>", "Telegram chat ID (auto-detected if single)")
228
- .action(async (opts: { chat?: string }) => {
229
- try {
230
- const chatId = await resolveChatId(opts.chat);
231
- const { PPMBotSessionManager } = await import("../../services/ppmbot/ppmbot-session.ts");
232
- const sessions = new PPMBotSessionManager();
233
-
234
- // Get current project before closing
235
- const active = sessions.getActiveSession(chatId);
236
- const projectName = active?.projectName;
237
- await sessions.closeSession(chatId);
238
- const session = await sessions.getOrCreateSession(chatId, projectName ?? undefined);
239
- console.log(`${C.green}✓${C.reset} New session for ${C.bold}${session.projectName}${C.reset} (${session.sessionId.slice(0, 8)})`);
240
- } catch (e) {
241
- console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
242
- process.exit(1);
243
- }
244
- });
245
-
246
- sess
247
- .command("list")
248
- .description("List recent sessions")
249
- .option("--chat <id>", "Telegram chat ID (auto-detected if single)")
250
- .option("-l, --limit <n>", "Max results", "20")
251
- .option("--json", "Output as JSON")
252
- .action(async (opts: { chat?: string; limit: string; json?: boolean }) => {
253
- try {
254
- const chatId = await resolveChatId(opts.chat);
255
- const { getRecentPPMBotSessions, getSessionTitles, getPinnedSessionIds } = await import("../../services/db.service.ts");
256
- const allSessions = getRecentPPMBotSessions(chatId, Number(opts.limit) || 20);
257
-
258
- if (allSessions.length === 0) {
259
- console.log(`${C.dim}No sessions found.${C.reset}`);
260
- return;
261
- }
262
-
263
- const titles = getSessionTitles(allSessions.map((s) => s.session_id));
264
- const pinnedIds = getPinnedSessionIds();
265
-
266
- // Sort: pinned first, then by last_message_at desc
267
- const sorted = [...allSessions].sort((a, b) => {
268
- const aPin = pinnedIds.has(a.session_id) ? 1 : 0;
269
- const bPin = pinnedIds.has(b.session_id) ? 1 : 0;
270
- if (aPin !== bPin) return bPin - aPin;
271
- return b.last_message_at - a.last_message_at;
272
- });
273
-
274
- if (opts.json) {
275
- const jsonData = sorted.map((s, i) => ({
276
- index: i + 1,
277
- sessionId: s.session_id,
278
- project: s.project_name,
279
- title: titles[s.session_id]?.replace(/^\[PPM\]\s*/, "") || "",
280
- pinned: pinnedIds.has(s.session_id),
281
- active: !!s.is_active,
282
- lastMessage: new Date(s.last_message_at * 1000).toISOString(),
283
- }));
284
- console.log(JSON.stringify(jsonData, null, 2));
285
- return;
286
- }
287
-
288
- for (const [i, s] of sorted.entries()) {
289
- const pin = pinnedIds.has(s.session_id) ? "📌 " : " ";
290
- const activeDot = s.is_active ? ` ${C.green}⬤${C.reset}` : "";
291
- const rawTitle = titles[s.session_id]?.replace(/^\[PPM\]\s*/, "") || "";
292
- const title = rawTitle ? rawTitle.slice(0, 50) : `${C.dim}untitled${C.reset}`;
293
- const sid = s.session_id.slice(0, 8);
294
- const date = new Date(s.last_message_at * 1000).toLocaleString(undefined, {
295
- month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
296
- });
297
-
298
- console.log(`${pin}${i + 1}. ${title}${activeDot}`);
299
- console.log(` ${C.dim}${sid} · ${s.project_name} · ${date}${C.reset}`);
300
- }
301
- } catch (e) {
302
- console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
303
- process.exit(1);
304
- }
305
- });
306
-
307
- sess
308
- .command("resume <target>")
309
- .description("Resume a session by index number or session ID prefix")
310
- .option("--chat <id>", "Telegram chat ID (auto-detected if single)")
311
- .action(async (target: string, opts: { chat?: string }) => {
312
- try {
313
- const chatId = await resolveChatId(opts.chat);
314
- const { PPMBotSessionManager } = await import("../../services/ppmbot/ppmbot-session.ts");
315
- const sessions = new PPMBotSessionManager();
316
-
317
- const index = parseInt(target, 10);
318
- const isIndex = !isNaN(index) && index >= 1 && String(index) === target.trim();
319
-
320
- const session = isIndex
321
- ? await sessions.resumeSessionById(chatId, index)
322
- : await sessions.resumeSessionByIdPrefix(chatId, target.trim());
323
-
324
- if (!session) {
325
- console.log(`${C.yellow}Session not found: ${target}${C.reset}`);
326
- process.exit(1);
327
- }
328
- console.log(`${C.green}✓${C.reset} Resumed session ${C.dim}${session.sessionId.slice(0, 8)}${C.reset} (${C.bold}${session.projectName}${C.reset})`);
329
- } catch (e) {
330
- console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
331
- process.exit(1);
332
- }
333
- });
334
-
335
- sess
336
- .command("stop")
337
- .description("End the current session")
338
- .option("--chat <id>", "Telegram chat ID (auto-detected if single)")
339
- .action(async (opts: { chat?: string }) => {
340
- try {
341
- const chatId = await resolveChatId(opts.chat);
342
- const { PPMBotSessionManager } = await import("../../services/ppmbot/ppmbot-session.ts");
343
- const sessions = new PPMBotSessionManager();
344
- await sessions.closeSession(chatId);
345
- console.log(`${C.green}✓${C.reset} Session ended`);
346
266
  } catch (e) {
347
267
  console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
348
268
  process.exit(1);
@@ -355,45 +275,32 @@ function registerSessionCommands(bot: Command): void {
355
275
  function registerMiscCommands(bot: Command): void {
356
276
  bot
357
277
  .command("status")
358
- .description("Show current project and session info")
278
+ .description("Show current status and running tasks")
359
279
  .option("--chat <id>", "Telegram chat ID (auto-detected if single)")
360
280
  .option("--json", "Output as JSON")
361
281
  .action(async (opts: { chat?: string; json?: boolean }) => {
362
282
  try {
363
283
  const chatId = await resolveChatId(opts.chat);
364
- const { PPMBotSessionManager } = await import("../../services/ppmbot/ppmbot-session.ts");
365
- const sessions = new PPMBotSessionManager();
366
- const active = sessions.getActiveSession(chatId);
367
-
368
- // Fallback: check DB for any active session
369
- let project = active?.projectName ?? "";
370
- let provider = active?.providerId ?? "";
371
- let sessionId = active?.sessionId ?? "";
372
-
373
- if (!active) {
374
- const { getRecentPPMBotSessions } = await import("../../services/db.service.ts");
375
- const recent = getRecentPPMBotSessions(chatId, 1);
376
- if (recent.length > 0 && recent[0]!.is_active) {
377
- project = recent[0]!.project_name;
378
- provider = recent[0]!.provider_id;
379
- sessionId = recent[0]!.session_id;
380
- }
381
- }
284
+ const { getRecentBotTasks } = await import("../../services/db.service.ts");
285
+ const tasks = getRecentBotTasks(chatId, 10);
286
+ const active = tasks.filter((t) => t.status === "running" || t.status === "pending");
382
287
 
383
288
  if (opts.json) {
384
- console.log(JSON.stringify({ chatId, project, provider, sessionId }));
289
+ console.log(JSON.stringify({ chatId, activeTasks: active.length, recentTasks: tasks.length }));
385
290
  return;
386
291
  }
387
292
 
388
- if (!project) {
389
- console.log(`${C.dim}No active session. Use: ppm bot project switch <name>${C.reset}`);
390
- return;
391
- }
293
+ console.log(`Chat: ${chatId}`);
294
+ console.log(`Active tasks: ${active.length}`);
295
+ console.log(`Recent tasks: ${tasks.length}`);
392
296
 
393
- console.log(`Project: ${C.bold}${project}${C.reset}`);
394
- console.log(`Provider: ${provider}`);
395
- console.log(`Session: ${C.dim}${sessionId.slice(0, 12)}…${C.reset}`);
396
- console.log(`Chat: ${chatId}`);
297
+ if (active.length) {
298
+ console.log(`\n${C.cyan}Running:${C.reset}`);
299
+ for (const t of active) {
300
+ const elapsed = Math.round((Date.now() / 1000 - t.createdAt) / 60);
301
+ console.log(` 🔄 ${C.dim}${t.id.slice(0, 8)}${C.reset} ${C.bold}${t.projectName}${C.reset} — ${t.prompt.slice(0, 50)} (${elapsed}m)`);
302
+ }
303
+ }
397
304
  } catch (e) {
398
305
  console.error(`${C.red}✗${C.reset} ${e instanceof Error ? e.message : String(e)}`);
399
306
  process.exit(1);
@@ -442,28 +349,25 @@ function registerMiscCommands(bot: Command): void {
442
349
  .action(() => {
443
350
  console.log(`${C.bold}PPMBot CLI Commands${C.reset}
444
351
 
445
- ${C.cyan}Project:${C.reset}
446
- ppm bot project list List available projects
447
- ppm bot project switch <name> Switch to a project
448
- ppm bot project current Show current project
449
-
450
- ${C.cyan}Session:${C.reset}
451
- ppm bot session new Start fresh session
452
- ppm bot session list List recent sessions
453
- ppm bot session resume <n|id> Resume a session
454
- ppm bot session stop End current session
352
+ ${C.cyan}Delegation:${C.reset}
353
+ ppm bot delegate --chat <id> --project <name> --prompt "<task>"
354
+ ppm bot task-status <id> Check task status
355
+ ppm bot task-result <id> Get task result
356
+ ppm bot tasks List recent tasks
455
357
 
456
358
  ${C.cyan}Memory (cross-project):${C.reset}
457
359
  ppm bot memory save "<text>" Save a memory (-c category)
458
360
  ppm bot memory list List saved memories
459
361
  ppm bot memory forget "<topic>" Delete matching memories
460
362
 
363
+ ${C.cyan}Project:${C.reset}
364
+ ppm bot project list List available projects
365
+
461
366
  ${C.cyan}Server:${C.reset}
462
- ppm bot status Current project/session info
367
+ ppm bot status Current status + running tasks
463
368
  ppm bot version Show PPM version
464
369
  ppm bot restart Restart PPM server
465
370
 
466
- ${C.dim}Session/project commands auto-detect your Telegram chat.
467
- Use --chat <id> if multiple chats are paired.${C.reset}`);
371
+ ${C.dim}Use --chat <id> if multiple chats are paired.${C.reset}`);
468
372
  });
469
373
  }
@@ -302,6 +302,37 @@ databaseRoutes.put("/connections/:id/cell", async (c) => {
302
302
  }
303
303
  });
304
304
 
305
+ /** DELETE /api/db/connections/:id/row — body: { table, schema?, pkColumn, pkValue } — enforces readonly */
306
+ databaseRoutes.delete("/connections/:id/row", async (c) => {
307
+ try {
308
+ const conn = resolveConn(c.req.param("id"));
309
+ if (!conn) return c.json(err("Connection not found"), 404);
310
+
311
+ if (conn.readonly) {
312
+ return c.json(err("Connection is readonly — row deletion is disabled. Change this in PPM web UI."), 403);
313
+ }
314
+
315
+ const body = await c.req.json<{
316
+ table: string; schema?: string;
317
+ pkColumn: string; pkValue: unknown;
318
+ }>();
319
+ if (!body.table || !body.pkColumn || body.pkValue == null) {
320
+ return c.json(err("table, pkColumn, and pkValue are required"), 400);
321
+ }
322
+
323
+ const config = JSON.parse(conn.connection_config) as DbConnectionConfig;
324
+ const adapter = getAdapter(conn.type);
325
+ await adapter.deleteRow(config, body.table, {
326
+ schema: body.schema,
327
+ pkColumn: body.pkColumn,
328
+ pkValue: body.pkValue,
329
+ });
330
+ return c.json(ok({ deleted: true }));
331
+ } catch (e) {
332
+ return c.json(err((e as Error).message), 500);
333
+ }
334
+ });
335
+
305
336
  // ---------------------------------------------------------------------------
306
337
  // Search
307
338
  // ---------------------------------------------------------------------------
@@ -401,3 +401,16 @@ settingsRoutes.delete("/clawbot/memories/:id", (c) => {
401
401
  return c.json(err((e as Error).message), 500);
402
402
  }
403
403
  });
404
+
405
+ /** GET /settings/clawbot/tasks — list recent delegated tasks */
406
+ settingsRoutes.get("/clawbot/tasks", (c) => {
407
+ const limit = Number(c.req.query("limit")) || 20;
408
+ try {
409
+ const rows = getDb().query(
410
+ "SELECT * FROM bot_tasks ORDER BY created_at DESC LIMIT ?",
411
+ ).all(limit);
412
+ return c.json(ok(rows));
413
+ } catch (e) {
414
+ return c.json(ok([]));
415
+ }
416
+ });
@@ -73,3 +73,17 @@ sqliteRoutes.put("/cell", async (c) => {
73
73
  return c.json(err((e as Error).message), 500);
74
74
  }
75
75
  });
76
+
77
+ /** DELETE /sqlite/row — body: { path, table, rowid } */
78
+ sqliteRoutes.delete("/row", async (c) => {
79
+ try {
80
+ const body = await c.req.json<{ path: string; table: string; rowid: number }>();
81
+ if (!body.path || !body.table || body.rowid == null) {
82
+ return c.json(err("Missing required fields: path, table, rowid"), 400);
83
+ }
84
+ sqliteService.deleteRow(c.get("projectPath"), body.path, body.table, body.rowid);
85
+ return c.json(ok({ deleted: true }));
86
+ } catch (e) {
87
+ return c.json(err((e as Error).message), 500);
88
+ }
89
+ });