@hienlh/ppm 0.7.0 → 0.7.2
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 +12 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +61 -3
- package/src/server/index.ts +14 -0
- package/src/server/routes/chat.ts +5 -1
- package/src/server/ws/chat.ts +12 -9
- package/src/services/tunnel.service.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.2] - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Tunnel URL sync**: `ppm start --share` tunnel URL now synced into `tunnelService` on daemon startup — Share button in web UI shows correct URL instead of treating tunnel as inactive and starting a duplicate
|
|
7
|
+
|
|
8
|
+
## [0.7.1] - 2026-03-20
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Session rename persistence**: Use SDK `customTitle` field instead of volatile `summary` when listing/resuming sessions, so custom titles survive reloads
|
|
12
|
+
- **Session rename ID resolution**: Resolve PPM UUID → SDK session ID and pass project dir when renaming, ensuring SDK finds the correct session file
|
|
13
|
+
- **SDK crash recovery**: When SDK subprocess exits with code 1 during session resume, automatically retry as a fresh session instead of showing a cryptic error
|
|
14
|
+
|
|
3
15
|
## [0.7.0] - 2026-03-20
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ A mobile-first web IDE with AI chat, terminal, git, database tools, and file exp
|
|
|
9
9
|
```bash
|
|
10
10
|
# 1. Install Bun (if you don't have it)
|
|
11
11
|
curl -fsSL https://bun.sh/install | bash
|
|
12
|
+
# or via npm
|
|
13
|
+
npm install -g bun
|
|
12
14
|
|
|
13
15
|
# 2. Run directly (no install needed)
|
|
14
16
|
bunx @hienlh/ppm start
|
package/package.json
CHANGED
|
@@ -261,7 +261,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
261
261
|
const meta: Session = {
|
|
262
262
|
id: sessionId,
|
|
263
263
|
providerId: this.id,
|
|
264
|
-
title: found.summary ?? "Resumed Chat",
|
|
264
|
+
title: found.customTitle ?? found.summary ?? "Resumed Chat",
|
|
265
265
|
createdAt: new Date(found.lastModified).toISOString(),
|
|
266
266
|
};
|
|
267
267
|
this.activeSessions.set(sessionId, meta);
|
|
@@ -295,7 +295,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
295
295
|
return sdkSessions.map((s) => ({
|
|
296
296
|
id: s.sessionId,
|
|
297
297
|
providerId: this.id,
|
|
298
|
-
title: s.summary ?? s.firstPrompt ?? "Chat",
|
|
298
|
+
title: s.customTitle ?? s.summary ?? s.firstPrompt ?? "Chat",
|
|
299
299
|
createdAt: new Date(s.lastModified).toISOString(),
|
|
300
300
|
updatedAt: new Date(s.lastModified).toISOString(),
|
|
301
301
|
}));
|
|
@@ -806,7 +806,65 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
806
806
|
} catch (e) {
|
|
807
807
|
const msg = (e as Error).message ?? String(e);
|
|
808
808
|
console.error(`[sdk] error: ${msg}`);
|
|
809
|
-
if (
|
|
809
|
+
if (msg.includes("abort") || msg.includes("closed")) {
|
|
810
|
+
// User-initiated abort or WS closed — nothing to report
|
|
811
|
+
} else if (!isFirstMessage && msg.includes("exited with code")) {
|
|
812
|
+
// SDK subprocess crashed during session resume — retry as fresh session
|
|
813
|
+
console.warn(`[sdk] session resume failed, retrying as fresh session`);
|
|
814
|
+
try {
|
|
815
|
+
const providerConfig = this.getProviderConfig();
|
|
816
|
+
const effectiveCwd = meta.projectPath || homedir();
|
|
817
|
+
const queryEnv = { ...process.env, ...this.getProjectEnvOverrides(meta.projectPath) };
|
|
818
|
+
const retryQuery = query({
|
|
819
|
+
prompt: message,
|
|
820
|
+
options: {
|
|
821
|
+
cwd: effectiveCwd,
|
|
822
|
+
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
823
|
+
settingSources: ["user", "project"],
|
|
824
|
+
env: queryEnv,
|
|
825
|
+
settings: { permissions: { allow: [], deny: [] } },
|
|
826
|
+
allowedTools: [
|
|
827
|
+
"Read", "Write", "Edit", "Bash", "Glob", "Grep",
|
|
828
|
+
"WebSearch", "WebFetch", "AskUserQuestion",
|
|
829
|
+
"Agent", "Skill", "TodoWrite", "ToolSearch",
|
|
830
|
+
],
|
|
831
|
+
permissionMode: "bypassPermissions",
|
|
832
|
+
allowDangerouslySkipPermissions: true,
|
|
833
|
+
...(providerConfig.model && { model: providerConfig.model }),
|
|
834
|
+
maxTurns: providerConfig.max_turns ?? 100,
|
|
835
|
+
canUseTool,
|
|
836
|
+
includePartialMessages: true,
|
|
837
|
+
} as any,
|
|
838
|
+
});
|
|
839
|
+
this.activeQueries.set(sessionId, retryQuery);
|
|
840
|
+
for await (const retryMsg of retryQuery) {
|
|
841
|
+
if (retryMsg.type === "system") continue;
|
|
842
|
+
if (retryMsg.type === "result") {
|
|
843
|
+
const r = retryMsg as any;
|
|
844
|
+
if (r.subtype && r.subtype !== "success") {
|
|
845
|
+
yield { type: "error", message: r.error ?? `Agent stopped: ${r.subtype}` };
|
|
846
|
+
}
|
|
847
|
+
resultSubtype = r.subtype;
|
|
848
|
+
resultNumTurns = r.num_turns;
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
if ((retryMsg as any).type === "assistant") {
|
|
852
|
+
const content = (retryMsg as any).message?.content;
|
|
853
|
+
if (Array.isArray(content)) {
|
|
854
|
+
for (const block of content) {
|
|
855
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
856
|
+
yield { type: "text", content: block.text };
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
} catch (retryErr) {
|
|
863
|
+
const retryMsg = (retryErr as Error).message ?? String(retryErr);
|
|
864
|
+
console.error(`[sdk] retry also failed: ${retryMsg}`);
|
|
865
|
+
yield { type: "error", message: `SDK error: ${msg}` };
|
|
866
|
+
}
|
|
867
|
+
} else {
|
|
810
868
|
yield { type: "error", message: `SDK error: ${msg}` };
|
|
811
869
|
}
|
|
812
870
|
} finally {
|
package/src/server/index.ts
CHANGED
|
@@ -448,6 +448,20 @@ if (process.argv.includes("__serve__")) {
|
|
|
448
448
|
configService.load(configPath);
|
|
449
449
|
await setupLogFile();
|
|
450
450
|
|
|
451
|
+
// Sync externally-started tunnel URL (from `ppm start --share`) into tunnelService
|
|
452
|
+
// so GET /api/tunnel reflects the correct state and Share button doesn't start a duplicate.
|
|
453
|
+
try {
|
|
454
|
+
const { resolve: r } = await import("node:path");
|
|
455
|
+
const { homedir: h } = await import("node:os");
|
|
456
|
+
const { readFileSync: rf } = await import("node:fs");
|
|
457
|
+
const statusFile = r(h(), ".ppm", "status.json");
|
|
458
|
+
const status = JSON.parse(rf(statusFile, "utf-8"));
|
|
459
|
+
if (status.shareUrl) {
|
|
460
|
+
const { tunnelService } = await import("../services/tunnel.service.ts");
|
|
461
|
+
tunnelService.setExternalUrl(status.shareUrl);
|
|
462
|
+
}
|
|
463
|
+
} catch { /* status.json missing or no shareUrl — normal */ }
|
|
464
|
+
|
|
451
465
|
Bun.serve({
|
|
452
466
|
port,
|
|
453
467
|
hostname: host,
|
|
@@ -8,6 +8,7 @@ import { renameSession as sdkRenameSession } from "@anthropic-ai/claude-agent-sd
|
|
|
8
8
|
import { listSlashItems } from "../../services/slash-items.service.ts";
|
|
9
9
|
import { getCachedUsage, refreshUsageNow } from "../../services/claude-usage.service.ts";
|
|
10
10
|
import { getSessionLog } from "../../services/session-log.service.ts";
|
|
11
|
+
import { getSessionMapping } from "../../services/db.service.ts";
|
|
11
12
|
import { ok, err } from "../../types/api.ts";
|
|
12
13
|
|
|
13
14
|
type Env = { Variables: { projectPath: string; projectName: string } };
|
|
@@ -114,8 +115,11 @@ chatRoutes.patch("/sessions/:id", async (c) => {
|
|
|
114
115
|
const body = await c.req.json<{ title?: string }>();
|
|
115
116
|
if (!body.title?.trim()) return c.json(err("title is required"), 400);
|
|
116
117
|
const title = body.title.trim();
|
|
118
|
+
// Resolve PPM UUID → SDK session ID if mapped
|
|
119
|
+
const sdkId = getSessionMapping(id) ?? id;
|
|
120
|
+
const projectPath = c.get("projectPath");
|
|
117
121
|
// Persist to SDK so Claude Code CLI also sees the custom title
|
|
118
|
-
await sdkRenameSession(
|
|
122
|
+
await sdkRenameSession(sdkId, title, { dir: projectPath });
|
|
119
123
|
// Also update in-memory session
|
|
120
124
|
const session = chatService.getSession(id);
|
|
121
125
|
if (session) session.title = title;
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -160,11 +160,12 @@ async function runStreamLoop(sessionId: string, providerId: string, content: str
|
|
|
160
160
|
// Fire-and-forget: fetch updated session title from SDK summary
|
|
161
161
|
sdkListSessions({ dir: entry.projectPath, limit: 50 }).then((sessions) => {
|
|
162
162
|
const found = sessions.find((s) => s.sessionId === sessionId || s.sessionId === ev.sessionId);
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const title = found?.customTitle ?? found?.summary;
|
|
164
|
+
if (title) {
|
|
165
|
+
safeSend(sessionId, { type: "title_updated", title });
|
|
165
166
|
// Also update in-memory session title
|
|
166
167
|
const session = chatService.getSession(sessionId);
|
|
167
|
-
if (session) session.title =
|
|
168
|
+
if (session) session.title = title;
|
|
168
169
|
}
|
|
169
170
|
}).catch(() => {});
|
|
170
171
|
// Fire-and-forget notification broadcast (push + telegram)
|
|
@@ -298,9 +299,10 @@ export const chatWebSocket = {
|
|
|
298
299
|
if (!session?.title || session.title === "Chat" || session.title === "Resumed Chat") {
|
|
299
300
|
sdkListSessions({ dir: projectPath, limit: 50 }).then((sessions) => {
|
|
300
301
|
const found = sessions.find((s) => s.sessionId === sessionId);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
const title = found?.customTitle ?? found?.summary;
|
|
303
|
+
if (title) {
|
|
304
|
+
safeSend(sessionId, { type: "title_updated", title });
|
|
305
|
+
if (session) session.title = title;
|
|
304
306
|
}
|
|
305
307
|
}).catch(() => {});
|
|
306
308
|
}
|
|
@@ -330,9 +332,10 @@ export const chatWebSocket = {
|
|
|
330
332
|
if (!session?.title || session.title === "Chat" || session.title === "Resumed Chat") {
|
|
331
333
|
sdkListSessions({ dir: projectPath, limit: 50 }).then((sessions) => {
|
|
332
334
|
const found = sessions.find((s) => s.sessionId === sessionId);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
const title = found?.customTitle ?? found?.summary;
|
|
336
|
+
if (title) {
|
|
337
|
+
safeSend(sessionId, { type: "title_updated", title });
|
|
338
|
+
if (session) session.title = title;
|
|
336
339
|
}
|
|
337
340
|
}).catch(() => {});
|
|
338
341
|
}
|
|
@@ -98,6 +98,11 @@ class TunnelService {
|
|
|
98
98
|
getTunnelUrl(): string | null {
|
|
99
99
|
return this.url;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
/** Inject an externally-started tunnel URL (e.g. from daemon --share) */
|
|
103
|
+
setExternalUrl(url: string): void {
|
|
104
|
+
this.url = url;
|
|
105
|
+
}
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
/** Singleton tunnel service */
|