@hienlh/ppm 0.9.48 → 0.9.49

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.49] - 2026-04-07
4
+
5
+ ### Fixed
6
+ - **/resume accepts session ID prefix**: `/resume fdc4ddaa` now works alongside `/resume 2` (index). Matches session by ID prefix from `/sessions` list.
7
+ - **/restart actually restarts**: Server now exits with code 42 (restart signal) instead of 0 (clean exit). Supervisor recognizes code 42 and respawns immediately without backoff.
8
+ - **Restart notification delivered**: Supervisor respawns after `/restart`, new server sends "PPM v0.9.49 restarted successfully." to all paired chats.
9
+ - **/project lists all projects**: `getProjectNames()` now merges config projects + unique project names from session history. Previously returned empty when no projects in config.
10
+
3
11
  ## [0.9.48] - 2026-04-06
4
12
 
5
13
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.48",
3
+ "version": "0.9.49",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -1022,6 +1022,13 @@ export function getRecentPPMBotSessions(
1022
1022
  ).all(telegramChatId, limit) as PPMBotSessionRow[];
1023
1023
  }
1024
1024
 
1025
+ export function getDistinctPPMBotProjectNames(): string[] {
1026
+ const rows = getDb().query(
1027
+ "SELECT DISTINCT project_name FROM clawbot_sessions ORDER BY project_name",
1028
+ ).all() as { project_name: string }[];
1029
+ return rows.map((r) => r.project_name);
1030
+ }
1031
+
1025
1032
  // ---------------------------------------------------------------------------
1026
1033
  // PPMBot memory helpers
1027
1034
  // ---------------------------------------------------------------------------
@@ -281,12 +281,19 @@ class PPMBotService {
281
281
  }
282
282
 
283
283
  private async cmdResume(chatId: string, args: string): Promise<void> {
284
- const index = parseInt(args, 10);
285
- if (!index || index < 1) {
286
- await this.telegram!.sendMessage(Number(chatId), "Usage: /resume &lt;number&gt;");
284
+ if (!args.trim()) {
285
+ await this.telegram!.sendMessage(Number(chatId), "Usage: /resume &lt;number or session-id&gt;");
287
286
  return;
288
287
  }
289
- const session = await this.sessions.resumeSessionById(chatId, index);
288
+
289
+ // Support both index (e.g. "2") and session ID prefix (e.g. "fdc4ddaa")
290
+ const index = parseInt(args, 10);
291
+ const isIndex = !isNaN(index) && index >= 1 && String(index) === args.trim();
292
+
293
+ const session = isIndex
294
+ ? await this.sessions.resumeSessionById(chatId, index)
295
+ : await this.sessions.resumeSessionByIdPrefix(chatId, args.trim());
296
+
290
297
  if (!session) {
291
298
  await this.telegram!.sendMessage(Number(chatId), "Session not found.");
292
299
  return;
@@ -368,8 +375,8 @@ class PPMBotService {
368
375
  const markerPath = join(homedir(), ".ppm", "restart-notify.json");
369
376
  writeFileSync(markerPath, JSON.stringify({ chatIds, ts: Date.now() }));
370
377
 
371
- console.log("[ppmbot] Restart requested via Telegram, exiting...");
372
- process.exit(0);
378
+ console.log("[ppmbot] Restart requested via Telegram, exiting with code 42...");
379
+ process.exit(42);
373
380
  }, 500);
374
381
  }
375
382
 
@@ -9,6 +9,7 @@ import {
9
9
  deactivatePPMBotSession,
10
10
  touchPPMBotSession,
11
11
  getRecentPPMBotSessions,
12
+ getDistinctPPMBotProjectNames,
12
13
  setSessionTitle,
13
14
  } from "../db.service.ts";
14
15
  import type { PPMBotActiveSession, PPMBotSessionRow } from "../../types/ppmbot.ts";
@@ -93,6 +94,23 @@ export class PPMBotSessionManager {
93
94
  return this.resumeFromDb(chatId, target, project);
94
95
  }
95
96
 
97
+ /** Resume a session by session ID prefix match */
98
+ async resumeSessionByIdPrefix(
99
+ chatId: string,
100
+ prefix: string,
101
+ ): Promise<PPMBotActiveSession | null> {
102
+ const sessions = getRecentPPMBotSessions(chatId, 20);
103
+ const target = sessions.find((s) => s.session_id.startsWith(prefix));
104
+ if (!target) return null;
105
+
106
+ await this.closeSession(chatId);
107
+
108
+ const project = this.resolveProject(target.project_name);
109
+ if (!project) return null;
110
+
111
+ return this.resumeFromDb(chatId, target, project);
112
+ }
113
+
96
114
  /**
97
115
  * Resolve a project name against configured projects.
98
116
  * Case-insensitive, supports prefix matching.
@@ -119,10 +137,12 @@ export class PPMBotSessionManager {
119
137
  setSessionTitle(sessionId, title);
120
138
  }
121
139
 
122
- /** Get list of available project names (for /start greeting) */
140
+ /** Get list of available project names (config + sessions history) */
123
141
  getProjectNames(): string[] {
124
- const projects = configService.get("projects") as ProjectConfig[];
125
- return projects?.map((p) => p.name) ?? [];
142
+ const configured = (configService.get("projects") as ProjectConfig[])?.map((p) => p.name) ?? [];
143
+ const fromSessions = getDistinctPPMBotProjectNames();
144
+ const merged = new Set([...configured, ...fromSessions]);
145
+ return [...merged].sort();
126
146
  }
127
147
 
128
148
  // ── Private ─────────────────────────────────────────────────────
@@ -130,11 +130,17 @@ export async function spawnServer(
130
130
  const exitCode = await serverChild.exited;
131
131
  serverChild = null;
132
132
 
133
- if (exitCode === 0 || shuttingDown) {
133
+ if (exitCode === 0 && shuttingDown) {
134
134
  log("INFO", `Server exited cleanly (code ${exitCode})`);
135
135
  return;
136
136
  }
137
137
 
138
+ // Exit code 42 = restart requested (e.g. /restart from Telegram)
139
+ if (exitCode === 42 || (exitCode === 0 && !shuttingDown)) {
140
+ log("INFO", `Server restart requested (code ${exitCode}), respawning immediately`);
141
+ return spawnServer(serverArgs, logFd);
142
+ }
143
+
138
144
  // SIGUSR2 restart — skip backoff, respawn immediately
139
145
  if (serverRestartRequested) {
140
146
  serverRestartRequested = false;