@hienlh/ppm 0.9.33 → 0.9.35

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 (34) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/web/assets/{browser-tab-B9nNKjZX.js → browser-tab-DnIsHiCc.js} +1 -1
  3. package/dist/web/assets/{chat-tab-6XGhEKaC.js → chat-tab-il6D4jql.js} +2 -2
  4. package/dist/web/assets/{code-editor-DMZMpzt2.js → code-editor-BUc1jBqm.js} +1 -1
  5. package/dist/web/assets/{database-viewer-CnP1FFS2.js → database-viewer-TjRo2b8_.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-Cvwd0XBO.js → diff-viewer-BMhCz0xk.js} +1 -1
  7. package/dist/web/assets/{extension-webview-DkhsRepr.js → extension-webview-DiVdlE2r.js} +1 -1
  8. package/dist/web/assets/{git-graph-C3670Nxm.js → git-graph-4eGJ8B1A.js} +1 -1
  9. package/dist/web/assets/{index-DjIQL8ar.js → index-BmcV1di6.js} +4 -4
  10. package/dist/web/assets/keybindings-store--5T5hsAj.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-Co04dDdI.js → markdown-renderer-IyEzLrC6.js} +1 -1
  12. package/dist/web/assets/{postgres-viewer-D8K1qnnA.js → postgres-viewer-CSynGGkJ.js} +1 -1
  13. package/dist/web/assets/{settings-tab-64ODAeQZ.js → settings-tab-BdI4HhRa.js} +1 -1
  14. package/dist/web/assets/{sqlite-viewer-ClX7FICB.js → sqlite-viewer-C5mviyU5.js} +1 -1
  15. package/dist/web/assets/{terminal-tab-Dw4IKWGM.js → terminal-tab-CDyC1grg.js} +1 -1
  16. package/dist/web/assets/{use-monaco-theme-DA7EyR70.js → use-monaco-theme-DcVicB_i.js} +1 -1
  17. package/dist/web/index.html +1 -1
  18. package/dist/web/sw.js +1 -1
  19. package/package.json +1 -1
  20. package/src/server/index.ts +4 -4
  21. package/src/server/routes/settings.ts +14 -14
  22. package/src/services/db.service.ts +29 -29
  23. package/src/services/{clawbot/clawbot-memory.ts → ppmbot/ppmbot-memory.ts} +29 -29
  24. package/src/services/{clawbot/clawbot-service.ts → ppmbot/ppmbot-service.ts} +55 -38
  25. package/src/services/{clawbot/clawbot-session.ts → ppmbot/ppmbot-session.ts} +48 -37
  26. package/src/services/{clawbot/clawbot-streamer.ts → ppmbot/ppmbot-streamer.ts} +114 -80
  27. package/src/services/{clawbot/clawbot-telegram.ts → ppmbot/ppmbot-telegram.ts} +46 -18
  28. package/src/types/config.ts +3 -3
  29. package/src/types/{clawbot.ts → ppmbot.ts} +10 -10
  30. package/src/web/components/chat/chat-history-bar.tsx +2 -2
  31. package/src/web/components/settings/{clawbot-settings-section.tsx → ppmbot-settings-section.tsx} +7 -7
  32. package/src/web/components/settings/settings-tab.tsx +3 -3
  33. package/dist/web/assets/keybindings-store-DHh6rwm-.js +0 -1
  34. /package/src/services/{clawbot/clawbot-formatter.ts → ppmbot/ppmbot-formatter.ts} +0 -0
@@ -1,19 +1,22 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
1
4
  import { chatService } from "../chat.service.ts";
2
5
  import { configService } from "../config.service.ts";
3
6
  import {
4
- getActiveClawBotSession,
5
- createClawBotSession,
6
- deactivateClawBotSession,
7
- touchClawBotSession,
8
- getRecentClawBotSessions,
7
+ getActivePPMBotSession,
8
+ createPPMBotSession,
9
+ deactivatePPMBotSession,
10
+ touchPPMBotSession,
11
+ getRecentPPMBotSessions,
9
12
  setSessionTitle,
10
13
  } from "../db.service.ts";
11
- import type { ClawBotActiveSession, ClawBotSessionRow } from "../../types/clawbot.ts";
12
- import type { ClawBotConfig, ProjectConfig } from "../../types/config.ts";
14
+ import type { PPMBotActiveSession, PPMBotSessionRow } from "../../types/ppmbot.ts";
15
+ import type { PPMBotConfig, ProjectConfig } from "../../types/config.ts";
13
16
 
14
- export class ClawBotSessionManager {
17
+ export class PPMBotSessionManager {
15
18
  /** In-memory cache: telegramChatId → active session */
16
- private activeSessions = new Map<string, ClawBotActiveSession>();
19
+ private activeSessions = new Map<string, PPMBotActiveSession>();
17
20
 
18
21
  /**
19
22
  * Get active session for chatId. If none exists, create one for the
@@ -22,21 +25,22 @@ export class ClawBotSessionManager {
22
25
  async getOrCreateSession(
23
26
  chatId: string,
24
27
  projectName?: string,
25
- ): Promise<ClawBotActiveSession> {
28
+ ): Promise<PPMBotActiveSession> {
26
29
  const cached = this.activeSessions.get(chatId);
27
30
  if (cached && (!projectName || cached.projectName === projectName)) {
28
- touchClawBotSession(cached.sessionId);
31
+ touchPPMBotSession(cached.sessionId);
29
32
  return cached;
30
33
  }
31
34
 
32
- const resolvedProject = this.resolveProject(
33
- projectName || this.getDefaultProject(),
34
- );
35
+ const input = projectName || this.getDefaultProject();
36
+ const resolvedProject = input
37
+ ? this.resolveProject(input)
38
+ : this.getFallbackProject();
35
39
  if (!resolvedProject) {
36
40
  throw new Error(`Project not found: "${projectName || "(default)"}"`);
37
41
  }
38
42
 
39
- const dbSession = getActiveClawBotSession(chatId, resolvedProject.name);
43
+ const dbSession = getActivePPMBotSession(chatId, resolvedProject.name);
40
44
  if (dbSession) {
41
45
  return this.resumeFromDb(chatId, dbSession, resolvedProject);
42
46
  }
@@ -48,7 +52,7 @@ export class ClawBotSessionManager {
48
52
  async switchProject(
49
53
  chatId: string,
50
54
  projectName: string,
51
- ): Promise<ClawBotActiveSession> {
55
+ ): Promise<PPMBotActiveSession> {
52
56
  await this.closeSession(chatId);
53
57
  return this.getOrCreateSession(chatId, projectName);
54
58
  }
@@ -57,27 +61,27 @@ export class ClawBotSessionManager {
57
61
  async closeSession(chatId: string): Promise<void> {
58
62
  const active = this.activeSessions.get(chatId);
59
63
  if (active) {
60
- deactivateClawBotSession(active.sessionId);
64
+ deactivatePPMBotSession(active.sessionId);
61
65
  this.activeSessions.delete(chatId);
62
66
  }
63
67
  }
64
68
 
65
69
  /** Get active session from cache (no DB hit) */
66
- getActiveSession(chatId: string): ClawBotActiveSession | null {
70
+ getActiveSession(chatId: string): PPMBotActiveSession | null {
67
71
  return this.activeSessions.get(chatId) ?? null;
68
72
  }
69
73
 
70
74
  /** List recent sessions for a chat (from DB) */
71
- listRecentSessions(chatId: string, limit = 10): ClawBotSessionRow[] {
72
- return getRecentClawBotSessions(chatId, limit);
75
+ listRecentSessions(chatId: string, limit = 10): PPMBotSessionRow[] {
76
+ return getRecentPPMBotSessions(chatId, limit);
73
77
  }
74
78
 
75
79
  /** Resume a specific session by 1-indexed position in history */
76
80
  async resumeSessionById(
77
81
  chatId: string,
78
82
  sessionIndex: number,
79
- ): Promise<ClawBotActiveSession | null> {
80
- const sessions = getRecentClawBotSessions(chatId, 20);
83
+ ): Promise<PPMBotActiveSession | null> {
84
+ const sessions = getRecentPPMBotSessions(chatId, 20);
81
85
  const target = sessions[sessionIndex - 1];
82
86
  if (!target) return null;
83
87
 
@@ -111,7 +115,7 @@ export class ClawBotSessionManager {
111
115
  /** Update session title (e.g. after first message) */
112
116
  updateSessionTitle(sessionId: string, firstMessage: string): void {
113
117
  const preview = firstMessage.slice(0, 60).replace(/\n/g, " ");
114
- const title = `[Claw] ${preview}`;
118
+ const title = `[PPM] ${preview}`;
115
119
  setSessionTitle(sessionId, title);
116
120
  }
117
121
 
@@ -124,30 +128,37 @@ export class ClawBotSessionManager {
124
128
  // ── Private ─────────────────────────────────────────────────────
125
129
 
126
130
  private getDefaultProject(): string {
127
- const clawbot = configService.get("clawbot") as ClawBotConfig | undefined;
128
- return clawbot?.default_project || "";
131
+ const cfg = configService.get("clawbot") as PPMBotConfig | undefined;
132
+ return cfg?.default_project || "";
133
+ }
134
+
135
+ /** Fallback project when nothing is configured: ~/.ppm/bot/ */
136
+ private getFallbackProject(): { name: string; path: string } {
137
+ const botDir = join(homedir(), ".ppm", "bot");
138
+ if (!existsSync(botDir)) mkdirSync(botDir, { recursive: true });
139
+ return { name: "bot", path: botDir };
129
140
  }
130
141
 
131
142
  private getDefaultProvider(): string {
132
- const clawbot = configService.get("clawbot") as ClawBotConfig | undefined;
133
- return clawbot?.default_provider || configService.get("ai").default_provider;
143
+ const cfg = configService.get("clawbot") as PPMBotConfig | undefined;
144
+ return cfg?.default_provider || configService.get("ai").default_provider;
134
145
  }
135
146
 
136
147
  private async createNewSession(
137
148
  chatId: string,
138
149
  project: { name: string; path: string },
139
- ): Promise<ClawBotActiveSession> {
150
+ ): Promise<PPMBotActiveSession> {
140
151
  const providerId = this.getDefaultProvider();
141
152
 
142
153
  const session = await chatService.createSession(providerId, {
143
154
  projectName: project.name,
144
155
  projectPath: project.path,
145
- title: `[Claw] New session`,
156
+ title: `[PPM] New session`,
146
157
  });
147
158
 
148
- createClawBotSession(chatId, session.id, providerId, project.name, project.path);
159
+ createPPMBotSession(chatId, session.id, providerId, project.name, project.path);
149
160
 
150
- const active: ClawBotActiveSession = {
161
+ const active: PPMBotActiveSession = {
151
162
  telegramChatId: chatId,
152
163
  sessionId: session.id,
153
164
  providerId,
@@ -161,20 +172,20 @@ export class ClawBotSessionManager {
161
172
 
162
173
  private async resumeFromDb(
163
174
  chatId: string,
164
- dbSession: ClawBotSessionRow,
175
+ dbSession: PPMBotSessionRow,
165
176
  project: { name: string; path: string },
166
- ): Promise<ClawBotActiveSession> {
177
+ ): Promise<PPMBotActiveSession> {
167
178
  try {
168
179
  await chatService.resumeSession(dbSession.provider_id, dbSession.session_id);
169
180
  } catch {
170
- console.warn(`[clawbot] Failed to resume session ${dbSession.session_id}, creating new`);
171
- deactivateClawBotSession(dbSession.session_id);
181
+ console.warn(`[ppmbot] Failed to resume session ${dbSession.session_id}, creating new`);
182
+ deactivatePPMBotSession(dbSession.session_id);
172
183
  return this.createNewSession(chatId, project);
173
184
  }
174
185
 
175
- touchClawBotSession(dbSession.session_id);
186
+ touchPPMBotSession(dbSession.session_id);
176
187
 
177
- const active: ClawBotActiveSession = {
188
+ const active: PPMBotActiveSession = {
178
189
  telegramChatId: chatId,
179
190
  sessionId: dbSession.session_id,
180
191
  providerId: dbSession.provider_id,
@@ -1,15 +1,44 @@
1
1
  import type { ChatEvent, ResultSubtype } from "../../types/chat.ts";
2
- import type { ClawBotTelegram } from "./clawbot-telegram.ts";
2
+ import type { PPMBotTelegram } from "./ppmbot-telegram.ts";
3
3
  import {
4
4
  markdownToTelegramHtml,
5
5
  chunkMessage,
6
6
  escapeHtml,
7
- } from "./clawbot-formatter.ts";
7
+ } from "./ppmbot-formatter.ts";
8
8
 
9
9
  const MAX_MSG_LEN = 4096;
10
10
  const TYPING_REFRESH_MS = 4000;
11
+ const EVENT_TIMEOUT_MS = 60_000; // 60s max wait per event
11
12
  const PLACEHOLDER = "\u2026"; // ellipsis
12
13
 
14
+ /**
15
+ * Wrap an async iterable with per-event timeout.
16
+ * If .next() doesn't resolve within timeoutMs, yields a timeout error.
17
+ */
18
+ async function* withEventTimeout<T>(
19
+ iterable: AsyncIterable<T>,
20
+ timeoutMs: number,
21
+ ): AsyncGenerator<T> {
22
+ const iterator = iterable[Symbol.asyncIterator]();
23
+ try {
24
+ while (true) {
25
+ const result = await Promise.race([
26
+ iterator.next(),
27
+ new Promise<{ done: true; value: undefined; timedOut: true }>((resolve) =>
28
+ setTimeout(() => resolve({ done: true, value: undefined, timedOut: true }), timeoutMs),
29
+ ),
30
+ ]);
31
+ if ("timedOut" in result) {
32
+ throw new Error("No response within 60 seconds");
33
+ }
34
+ if (result.done) break;
35
+ yield result.value;
36
+ }
37
+ } finally {
38
+ iterator.return?.();
39
+ }
40
+ }
41
+
13
42
  export interface StreamConfig {
14
43
  showToolCalls: boolean;
15
44
  showThinking: boolean;
@@ -18,30 +47,59 @@ export interface StreamConfig {
18
47
  export interface StreamResult {
19
48
  contextWindowPct?: number;
20
49
  resultSubtype?: ResultSubtype;
21
- /** All Telegram message IDs sent during this stream */
22
50
  messageIds: number[];
23
- /** New session ID if session was migrated */
24
51
  newSessionId?: string;
25
52
  }
26
53
 
27
54
  /**
28
- * Consume a ChatEvent stream and progressively send/edit Telegram messages.
29
- *
30
- * Flow:
31
- * 1. Send placeholder message
32
- * 2. Accumulate text/tool/thinking events
33
- * 3. Edit message every time ClawBotTelegram allows (1s throttle)
34
- * 4. When text exceeds 4096, finalize current msg, start new one
35
- * 5. On done/error, finalize and return result
55
+ * Segments of accumulated response.
56
+ * - "md" = raw markdown from AI (needs conversion)
57
+ * - "html" = pre-formatted HTML (tool calls, thinking, errors — already escaped)
36
58
  */
59
+ type Segment = { type: "md"; text: string } | { type: "html"; text: string };
60
+
61
+ /** Render segments into Telegram HTML */
62
+ function renderSegments(segments: Segment[]): string {
63
+ return segments
64
+ .map((s) => (s.type === "md" ? markdownToTelegramHtml(s.text) : s.text))
65
+ .join("");
66
+ }
67
+
68
+ /** Check if segments have meaningful content */
69
+ function hasContent(segments: Segment[]): boolean {
70
+ return segments.some((s) => s.text.trim().length > 0);
71
+ }
72
+
73
+ /** Get raw text length (approximation for split decisions) */
74
+ function segmentsLength(segments: Segment[]): number {
75
+ return segments.reduce((sum, s) => sum + s.text.length, 0);
76
+ }
77
+
78
+ /** Append markdown text — merges into last segment if also md */
79
+ function appendMd(segments: Segment[], text: string): void {
80
+ const last = segments[segments.length - 1];
81
+ if (last?.type === "md") {
82
+ last.text += text;
83
+ } else {
84
+ segments.push({ type: "md", text });
85
+ }
86
+ }
87
+
88
+ /** Append pre-formatted HTML */
89
+ function appendHtml(segments: Segment[], html: string): void {
90
+ segments.push({ type: "html", html: html } as any);
91
+ // fix: use correct field
92
+ segments[segments.length - 1] = { type: "html", text: html };
93
+ }
94
+
37
95
  export async function streamToTelegram(
38
96
  chatId: number | string,
39
97
  events: AsyncIterable<ChatEvent>,
40
- telegram: ClawBotTelegram,
98
+ telegram: PPMBotTelegram,
41
99
  config: StreamConfig,
42
100
  ): Promise<StreamResult> {
43
101
  const result: StreamResult = { messageIds: [] };
44
- let accumulated = "";
102
+ const segments: Segment[] = [];
45
103
  let currentMsgId: number | null = null;
46
104
  let lastTypingTime = 0;
47
105
 
@@ -62,62 +120,49 @@ export async function streamToTelegram(
62
120
  result.messageIds.push(currentMsgId);
63
121
  }
64
122
 
65
- const finalizeAndStartNew = async (text: string): Promise<void> => {
66
- if (currentMsgId && text.trim()) {
67
- const html = markdownToTelegramHtml(text);
68
- await telegram.editMessageFinal(chatId, currentMsgId, html);
69
- }
70
- accumulated = "";
71
- currentMsgId = null;
72
- };
73
-
74
- const sendNewMessage = async (text: string): Promise<void> => {
75
- const html = markdownToTelegramHtml(text);
76
- const chunks = chunkMessage(html, MAX_MSG_LEN);
77
- for (const chunk of chunks) {
78
- const sent = await telegram.sendMessage(chatId, chunk);
79
- if (sent) {
80
- currentMsgId = sent.message_id;
81
- result.messageIds.push(currentMsgId);
82
- }
83
- }
84
- };
85
-
86
123
  const editCurrent = async (): Promise<void> => {
87
- if (!currentMsgId || !accumulated.trim()) return;
124
+ if (!currentMsgId || !hasContent(segments)) return;
88
125
 
89
- const html = markdownToTelegramHtml(accumulated);
126
+ const html = renderSegments(segments);
90
127
  if (html.length > MAX_MSG_LEN) {
91
- const splitPoint = findSplitPoint(accumulated, MAX_MSG_LEN * 0.8);
92
- const first = accumulated.slice(0, splitPoint);
93
- const rest = accumulated.slice(splitPoint).trimStart();
94
-
95
- await finalizeAndStartNew(first);
96
- accumulated = rest;
97
- if (rest) {
98
- await sendNewMessage(rest);
128
+ // Finalize current message with what fits, start new one
129
+ await telegram.editMessageFinal(chatId, currentMsgId, html.slice(0, MAX_MSG_LEN));
130
+ currentMsgId = null;
131
+
132
+ const overflow = html.slice(MAX_MSG_LEN);
133
+ if (overflow.trim()) {
134
+ const chunks = chunkMessage(overflow, MAX_MSG_LEN);
135
+ for (const chunk of chunks) {
136
+ const sent = await telegram.sendMessage(chatId, chunk);
137
+ if (sent) {
138
+ currentMsgId = sent.message_id;
139
+ result.messageIds.push(currentMsgId);
140
+ }
141
+ }
99
142
  }
143
+ // Reset segments — only keep any un-rendered text
144
+ segments.length = 0;
100
145
  return;
101
146
  }
102
147
 
103
148
  await telegram.editMessage(chatId, currentMsgId, html);
104
149
  };
105
150
 
106
- // Process event stream
151
+ // Process event stream with per-event timeout
107
152
  try {
108
- for await (const event of events) {
153
+ for await (const event of withEventTimeout(events, EVENT_TIMEOUT_MS)) {
109
154
  await refreshTyping();
110
155
 
111
156
  switch (event.type) {
112
157
  case "text": {
113
- accumulated += event.content;
158
+ appendMd(segments, event.content);
114
159
  await editCurrent();
115
160
  break;
116
161
  }
117
162
 
118
163
  case "thinking": {
119
164
  if (config.showThinking && event.content) {
120
- accumulated += `\n<i>${escapeHtml(event.content)}</i>\n`;
165
+ appendHtml(segments, `\n<i>💭 ${escapeHtml(event.content)}</i>\n`);
121
166
  await editCurrent();
122
167
  }
123
168
  break;
@@ -127,7 +172,10 @@ export async function streamToTelegram(
127
172
  if (config.showToolCalls) {
128
173
  const toolName = event.tool;
129
174
  const inputPreview = formatToolInput(event.input);
130
- accumulated += `\n🔧 <code>${escapeHtml(toolName)}</code>(${escapeHtml(inputPreview)})\n`;
175
+ appendHtml(
176
+ segments,
177
+ `\n🔧 <code>${escapeHtml(toolName)}</code>(${escapeHtml(inputPreview)})\n`,
178
+ );
131
179
  await editCurrent();
132
180
  }
133
181
  break;
@@ -135,14 +183,17 @@ export async function streamToTelegram(
135
183
 
136
184
  case "tool_result": {
137
185
  if (config.showToolCalls && event.isError) {
138
- accumulated += `\n⚠️ <code>${escapeHtml(event.output.slice(0, 200))}</code>\n`;
186
+ appendHtml(
187
+ segments,
188
+ `\n⚠️ <code>${escapeHtml(event.output.slice(0, 200))}</code>\n`,
189
+ );
139
190
  await editCurrent();
140
191
  }
141
192
  break;
142
193
  }
143
194
 
144
195
  case "error": {
145
- accumulated += `\n\n❌ <b>Error:</b> ${escapeHtml(event.message)}`;
196
+ appendHtml(segments, `\n\n❌ <b>Error:</b> ${escapeHtml(event.message)}`);
146
197
  await editCurrent();
147
198
  break;
148
199
  }
@@ -159,23 +210,28 @@ export async function streamToTelegram(
159
210
  }
160
211
 
161
212
  case "account_retry": {
162
- accumulated += `\n⏳ <i>Switching account: ${escapeHtml(event.reason)}</i>\n`;
213
+ appendHtml(
214
+ segments,
215
+ `\n⏳ <i>Switching account: ${escapeHtml(event.reason)}</i>\n`,
216
+ );
163
217
  await editCurrent();
164
218
  break;
165
219
  }
166
220
 
167
221
  default:
168
- // Ignore unknown events (system, team_detected, account_info, etc.)
169
222
  break;
170
223
  }
171
224
  }
172
225
  } catch (err) {
173
- accumulated += `\n\n❌ <b>Stream error:</b> ${escapeHtml((err as Error).message)}`;
226
+ appendHtml(
227
+ segments,
228
+ `\n\n❌ <b>Stream error:</b> ${escapeHtml((err as Error).message)}`,
229
+ );
174
230
  }
175
231
 
176
232
  // Final edit with complete content
177
- if (currentMsgId && accumulated.trim()) {
178
- const html = markdownToTelegramHtml(accumulated);
233
+ if (currentMsgId && hasContent(segments)) {
234
+ const html = renderSegments(segments);
179
235
  const chunks = chunkMessage(html, MAX_MSG_LEN);
180
236
 
181
237
  if (chunks.length === 1) {
@@ -187,7 +243,7 @@ export async function streamToTelegram(
187
243
  if (sent) result.messageIds.push(sent.message_id);
188
244
  }
189
245
  }
190
- } else if (currentMsgId && !accumulated.trim()) {
246
+ } else if (currentMsgId && !hasContent(segments)) {
191
247
  await telegram.editMessageFinal(
192
248
  chatId,
193
249
  currentMsgId,
@@ -200,7 +256,6 @@ export async function streamToTelegram(
200
256
 
201
257
  // ── Helpers ─────────────────────────────────────────────────────
202
258
 
203
- /** Format tool input for compact display */
204
259
  function formatToolInput(input: unknown): string {
205
260
  if (!input) return "";
206
261
  if (typeof input === "string") return input.slice(0, 80);
@@ -222,24 +277,3 @@ function formatToolInput(input: unknown): string {
222
277
  return "";
223
278
  }
224
279
  }
225
-
226
- /**
227
- * Find a good split point in text, aiming for targetLen.
228
- * Prefers double newline > single newline > space.
229
- */
230
- function findSplitPoint(text: string, targetLen: number): number {
231
- if (text.length <= targetLen) return text.length;
232
-
233
- const window = text.slice(0, Math.floor(targetLen));
234
-
235
- let point = window.lastIndexOf("\n\n");
236
- if (point > targetLen * 0.3) return point;
237
-
238
- point = window.lastIndexOf("\n");
239
- if (point > targetLen * 0.3) return point;
240
-
241
- point = window.lastIndexOf(" ");
242
- if (point > targetLen * 0.3) return point;
243
-
244
- return Math.floor(targetLen);
245
- }
@@ -2,15 +2,15 @@ import type {
2
2
  TelegramUpdate,
3
3
  TelegramMessage,
4
4
  TelegramSentMessage,
5
- ClawBotCommand,
6
- } from "../../types/clawbot.ts";
5
+ PPMBotCommand,
6
+ } from "../../types/ppmbot.ts";
7
7
 
8
8
  const TELEGRAM_API = "https://api.telegram.org/bot";
9
9
  const POLL_TIMEOUT = 25;
10
10
  const MIN_EDIT_INTERVAL = 1000;
11
11
  const BOT_TOKEN_RE = /^\d+:[A-Za-z0-9_-]{30,50}$/;
12
12
 
13
- /** Known ClawBot slash commands */
13
+ /** Known PPMBot slash commands */
14
14
  const COMMANDS = new Set([
15
15
  "start", "project", "new", "sessions", "resume",
16
16
  "status", "stop", "memory", "forget", "remember", "help",
@@ -18,7 +18,7 @@ const COMMANDS = new Set([
18
18
 
19
19
  export type UpdateHandler = (update: TelegramUpdate) => Promise<void>;
20
20
 
21
- export class ClawBotTelegram {
21
+ export class PPMBotTelegram {
22
22
  private token: string;
23
23
  private offset = 0;
24
24
  private running = false;
@@ -37,12 +37,40 @@ export class ClawBotTelegram {
37
37
 
38
38
  // ── Polling ─────────────────────────────────────────────────────
39
39
 
40
+ /** Register bot commands with Telegram so they show in the menu */
41
+ async registerCommands(): Promise<void> {
42
+ try {
43
+ await this.callApi("setMyCommands", {
44
+ commands: [
45
+ { command: "start", description: "Greeting + list projects" },
46
+ { command: "project", description: "Switch project" },
47
+ { command: "new", description: "Fresh session (current project)" },
48
+ { command: "sessions", description: "List recent sessions" },
49
+ { command: "resume", description: "Resume a previous session" },
50
+ { command: "status", description: "Current project/session info" },
51
+ { command: "stop", description: "End current session" },
52
+ { command: "memory", description: "Show project memories" },
53
+ { command: "forget", description: "Remove matching memories" },
54
+ { command: "remember", description: "Save a fact" },
55
+ { command: "help", description: "Show all commands" },
56
+ ],
57
+ });
58
+ console.log("[ppmbot] Commands registered");
59
+ } catch (err) {
60
+ console.warn("[ppmbot] Failed to register commands:", (err as Error).message);
61
+ }
62
+ }
63
+
40
64
  /** Start long-polling loop. Calls handler for each update. */
41
65
  async startPolling(handler: UpdateHandler): Promise<void> {
42
66
  if (this.running) return;
43
67
  this.running = true;
44
68
  this.retryCount = 0;
45
- console.log("[clawbot] Polling started");
69
+
70
+ // Register commands on startup
71
+ await this.registerCommands();
72
+
73
+ console.log("[ppmbot] Polling started");
46
74
 
47
75
  while (this.running) {
48
76
  try {
@@ -51,24 +79,24 @@ export class ClawBotTelegram {
51
79
 
52
80
  for (const update of updates) {
53
81
  this.offset = update.update_id + 1;
54
- try {
55
- await handler(update);
56
- } catch (err) {
57
- console.error("[clawbot] Handler error:", (err as Error).message);
58
- }
82
+ // Fire-and-forget: don't block polling on handler execution
83
+ // Per-chatId serialization is handled by processing lock in service
84
+ handler(update).catch((err) => {
85
+ console.error("[ppmbot] Handler error:", (err as Error).message);
86
+ });
59
87
  }
60
88
  } catch (err) {
61
89
  if (!this.running) break;
62
90
  this.retryCount++;
63
91
  const delay = Math.min(1000 * 2 ** this.retryCount, 30_000);
64
92
  console.error(
65
- `[clawbot] Poll error (retry ${this.retryCount}): ${(err as Error).message}. Retrying in ${delay}ms`,
93
+ `[ppmbot] Poll error (retry ${this.retryCount}): ${(err as Error).message}. Retrying in ${delay}ms`,
66
94
  );
67
95
  await Bun.sleep(delay);
68
96
  }
69
97
  }
70
98
 
71
- console.log("[clawbot] Polling stopped");
99
+ console.log("[ppmbot] Polling stopped");
72
100
  }
73
101
 
74
102
  /** Stop polling gracefully */
@@ -128,12 +156,12 @@ export class ClawBotTelegram {
128
156
  });
129
157
  const json = (await res.json()) as { ok: boolean; result?: TelegramSentMessage; description?: string };
130
158
  if (!json.ok) {
131
- console.error(`[clawbot] sendMessage failed: ${json.description}`);
159
+ console.error(`[ppmbot] sendMessage failed: ${json.description}`);
132
160
  return null;
133
161
  }
134
162
  return json.result ?? null;
135
163
  } catch (err) {
136
- console.error(`[clawbot] sendMessage error: ${(err as Error).message}`);
164
+ console.error(`[ppmbot] sendMessage error: ${(err as Error).message}`);
137
165
  return null;
138
166
  }
139
167
  }
@@ -163,12 +191,12 @@ export class ClawBotTelegram {
163
191
  const json = (await res.json()) as { ok: boolean; description?: string };
164
192
  if (!json.ok) {
165
193
  if (json.description?.includes("not modified")) return true;
166
- console.error(`[clawbot] editMessage failed: ${json.description}`);
194
+ console.error(`[ppmbot] editMessage failed: ${json.description}`);
167
195
  return false;
168
196
  }
169
197
  return true;
170
198
  } catch (err) {
171
- console.error(`[clawbot] editMessage error: ${(err as Error).message}`);
199
+ console.error(`[ppmbot] editMessage error: ${(err as Error).message}`);
172
200
  return false;
173
201
  }
174
202
  }
@@ -211,8 +239,8 @@ export class ClawBotTelegram {
211
239
 
212
240
  // ── Command Parsing ─────────────────────────────────────────────
213
241
 
214
- /** Parse a Telegram message into a ClawBotCommand if it starts with / */
215
- static parseCommand(message: TelegramMessage): ClawBotCommand | null {
242
+ /** Parse a Telegram message into a PPMBotCommand if it starts with / */
243
+ static parseCommand(message: TelegramMessage): PPMBotCommand | null {
216
244
  const text = message.text ?? message.caption ?? "";
217
245
  if (!text.startsWith("/")) return null;
218
246
 
@@ -9,7 +9,7 @@ export interface TelegramConfig {
9
9
  chat_id: string;
10
10
  }
11
11
 
12
- export interface ClawBotConfig {
12
+ export interface PPMBotConfig {
13
13
  enabled: boolean;
14
14
  default_provider: string;
15
15
  default_project: string;
@@ -32,7 +32,7 @@ export interface PpmConfig {
32
32
  ai: AIConfig;
33
33
  push?: PushConfig;
34
34
  telegram?: TelegramConfig;
35
- clawbot?: ClawBotConfig;
35
+ clawbot?: PPMBotConfig;
36
36
  cloud_url?: string;
37
37
  }
38
38
 
@@ -105,7 +105,7 @@ export const DEFAULT_CONFIG: PpmConfig = {
105
105
  enabled: false,
106
106
  default_provider: "claude",
107
107
  default_project: "",
108
- system_prompt: "",
108
+ system_prompt: "You are PPMBot, a helpful AI coding assistant on Telegram. Keep responses concise and mobile-friendly. Use short paragraphs. When showing code, use compact examples. Be direct and helpful.",
109
109
  show_tool_calls: true,
110
110
  show_thinking: false,
111
111
  permission_mode: "bypassPermissions",