@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
|
@@ -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
|
-
| **
|
|
197
|
-
| **
|
|
198
|
-
| **
|
|
199
|
-
| **
|
|
200
|
-
| **
|
|
201
|
-
| **
|
|
202
|
-
|
|
203
|
-
**
|
|
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
|
-
###
|
|
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
|
@@ -32,20 +32,133 @@ export async function resolveChatId(chatOpt?: string): Promise<string> {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* `ppm bot` CLI —
|
|
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
|
-
|
|
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
|
|
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 {
|
|
365
|
-
const
|
|
366
|
-
const active =
|
|
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,
|
|
289
|
+
console.log(JSON.stringify({ chatId, activeTasks: active.length, recentTasks: tasks.length }));
|
|
385
290
|
return;
|
|
386
291
|
}
|
|
387
292
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
293
|
+
console.log(`Chat: ${chatId}`);
|
|
294
|
+
console.log(`Active tasks: ${active.length}`);
|
|
295
|
+
console.log(`Recent tasks: ${tasks.length}`);
|
|
392
296
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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}
|
|
446
|
-
ppm bot project
|
|
447
|
-
ppm bot
|
|
448
|
-
ppm bot
|
|
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
|
|
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}
|
|
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
|
+
});
|