@hienlh/ppm 0.8.58 → 0.8.60

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 (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/web/assets/chat-tab-C5H74y2z.js +7 -0
  3. package/dist/web/assets/{code-editor-u6bm6bdq.js → code-editor-DMw26mUm.js} +1 -1
  4. package/dist/web/assets/{database-viewer-BgPBW1bJ.js → database-viewer-gnj_8u4T.js} +1 -1
  5. package/dist/web/assets/{diff-viewer-Cho-kjse.js → diff-viewer-DVqfhdBN.js} +1 -1
  6. package/dist/web/assets/{git-graph-CktRdFwt.js → git-graph-CJy7tOAJ.js} +1 -1
  7. package/dist/web/assets/index-BAioKo_2.css +2 -0
  8. package/dist/web/assets/index-Dg6TQ3Iu.js +37 -0
  9. package/dist/web/assets/keybindings-store-DcxZ6WAa.js +1 -0
  10. package/dist/web/assets/{markdown-renderer-3_CTktzg.js → markdown-renderer--Ss7hHOm.js} +1 -1
  11. package/dist/web/assets/{postgres-viewer-CH0JfEQ9.js → postgres-viewer-DMcvp0H7.js} +1 -1
  12. package/dist/web/assets/{settings-tab-BI0n39LJ.js → settings-tab-lC12I-a1.js} +1 -1
  13. package/dist/web/assets/{sqlite-viewer-DJyT7YZg.js → sqlite-viewer-BK2emL4i.js} +1 -1
  14. package/dist/web/assets/{tab-store-dpsCvqhH.js → tab-store-DcIBZTD4.js} +1 -1
  15. package/dist/web/assets/{terminal-tab-OqCohyF0.js → terminal-tab--Ag9kqvS.js} +1 -1
  16. package/dist/web/index.html +3 -3
  17. package/dist/web/sw.js +1 -1
  18. package/package.json +1 -1
  19. package/snapshot-state.md +1526 -0
  20. package/src/index.ts +0 -0
  21. package/src/providers/claude-agent-sdk.ts +16 -14
  22. package/src/providers/mock-provider.ts +6 -1
  23. package/src/server/index.ts +3 -15
  24. package/src/server/routes/proxy.ts +46 -53
  25. package/src/server/ws/chat.ts +194 -139
  26. package/src/services/account-selector.service.ts +8 -6
  27. package/src/services/account.service.ts +1 -0
  28. package/src/services/claude-usage.service.ts +10 -4
  29. package/src/services/proxy.service.ts +4 -19
  30. package/src/types/api.ts +9 -1
  31. package/src/web/components/chat/chat-tab.tsx +14 -5
  32. package/src/web/components/chat/message-input.tsx +39 -12
  33. package/src/web/components/chat/message-list.tsx +15 -12
  34. package/src/web/components/layout/panel-layout.tsx +17 -1
  35. package/src/web/components/settings/proxy-settings-section.tsx +40 -42
  36. package/src/web/hooks/use-chat.ts +196 -203
  37. package/src/web/stores/panel-store.ts +10 -10
  38. package/test-tokens.mjs +212 -0
  39. package/.claude.bak/agent-memory/tester/MEMORY.md +0 -3
  40. package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +0 -32
  41. package/dist/web/assets/chat-tab-cawT08fh.js +0 -7
  42. package/dist/web/assets/index-CpOYx0qg.js +0 -31
  43. package/dist/web/assets/index-WKLuYsBY.css +0 -2
  44. package/dist/web/assets/keybindings-store-vOnSm10D.js +0 -1
package/src/index.ts CHANGED
File without changes
@@ -502,22 +502,24 @@ export class ClaudeAgentSdkProvider implements AIProvider {
502
502
 
503
503
  // Log all system events for debugging SDK lifecycle
504
504
  if (msg.type === "system") {
505
- console.log(`[sdk] session=${sessionId} system: subtype=${(msg as any).subtype ?? "none"} ${JSON.stringify(msg).slice(0, 500)}`);
506
- }
507
-
508
- // Capture SDK session metadata from init message
509
- if (msg.type === "system" && (msg as any).subtype === "init") {
510
- const initMsg = msg as any;
511
- // SDK may assign a different session_id than our UUID
512
- if (initMsg.session_id && initMsg.session_id !== sessionId) {
513
- // Persist mapping so resume works after server restart
514
- setSessionMapping(sessionId, initMsg.session_id);
515
- // Update our in-memory mapping
516
- const oldMeta = this.activeSessions.get(sessionId);
517
- if (oldMeta) {
518
- this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
505
+ const subtype = (msg as any).subtype ?? "none";
506
+ console.log(`[sdk] session=${sessionId} system: subtype=${subtype} ${JSON.stringify(msg).slice(0, 500)}`);
507
+
508
+ // Capture SDK session metadata from init message
509
+ if (subtype === "init") {
510
+ const initMsg = msg as any;
511
+ if (initMsg.session_id && initMsg.session_id !== sessionId) {
512
+ setSessionMapping(sessionId, initMsg.session_id);
513
+ const oldMeta = this.activeSessions.get(sessionId);
514
+ if (oldMeta) {
515
+ this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
516
+ }
519
517
  }
520
518
  }
519
+
520
+ // Yield system events so streaming loop can transition phases
521
+ // (e.g. connecting → thinking when hooks/init arrive)
522
+ yield { type: "system" as any, subtype } as any;
521
523
  continue;
522
524
  }
523
525
 
@@ -92,8 +92,13 @@ export class MockProvider implements AIProvider {
92
92
  const abortController = new AbortController();
93
93
  this.activeAborts.set(sessionId, abortController);
94
94
 
95
+ // Simulate SDK system events (hooks, init) — real SDK emits these before content
96
+ yield { type: "system" as any, subtype: "hook_started" } as any;
97
+ await sleep(50);
98
+ yield { type: "system" as any, subtype: "init" } as any;
99
+
95
100
  // Simulate thinking delay
96
- await sleep(300);
101
+ await sleep(250);
97
102
 
98
103
  // Pick a response
99
104
  const responseText =
@@ -13,7 +13,7 @@ import { postgresRoutes } from "./routes/postgres.ts";
13
13
  import { databaseRoutes } from "./routes/database.ts";
14
14
  import { fsBrowseRoutes } from "./routes/fs-browse.ts";
15
15
  import { accountsRoutes } from "./routes/accounts.ts";
16
- import { proxyRoutes, handleProxyRequest } from "./routes/proxy.ts";
16
+ import { proxyRoutes } from "./routes/proxy.ts";
17
17
  import { initAdapters } from "../services/database/init-adapters.ts";
18
18
  import { terminalWebSocket } from "./ws/terminal.ts";
19
19
  import { chatWebSocket } from "./ws/chat.ts";
@@ -344,15 +344,9 @@ export async function startServer(options: {
344
344
  const server = Bun.serve({
345
345
  port,
346
346
  hostname: host,
347
- async fetch(req, server) {
347
+ fetch(req, server) {
348
348
  const url = new URL(req.url);
349
349
 
350
- // Proxy: handle before Hono to avoid SPA catch-all conflict
351
- if (url.pathname.startsWith("/proxy")) {
352
- const proxyRes = await handleProxyRequest(req);
353
- if (proxyRes) return proxyRes;
354
- }
355
-
356
350
  // WebSocket upgrade: /ws/project/:projectName/terminal/:id
357
351
  if (url.pathname.startsWith("/ws/project/")) {
358
352
  const parts = url.pathname.split("/");
@@ -505,15 +499,9 @@ if (process.argv.includes("__serve__")) {
505
499
  Bun.serve({
506
500
  port,
507
501
  hostname: host,
508
- async fetch(req, server) {
502
+ fetch(req, server) {
509
503
  const url = new URL(req.url);
510
504
 
511
- // Proxy: handle before Hono to avoid SPA catch-all conflict
512
- if (url.pathname.startsWith("/proxy")) {
513
- const proxyRes = await handleProxyRequest(req);
514
- if (proxyRes) return proxyRes;
515
- }
516
-
517
505
  if (url.pathname === "/ws/health") {
518
506
  const upgraded = server.upgrade(req, { data: { type: "health" } });
519
507
  if (upgraded) return undefined;
@@ -1,86 +1,79 @@
1
1
  import { Hono } from "hono";
2
2
  import { proxyService } from "../../services/proxy.service.ts";
3
+ import { ok, err } from "../../types/api.ts";
3
4
 
4
5
  /**
5
6
  * Proxy routes — Anthropic-compatible API proxy.
6
- * External tools (Claude Code CLI, OpenCode, Cursor, etc.) send requests here
7
+ * External tools (opencode, cursor, etc.) send requests here
7
8
  * and PPM forwards them to Anthropic using account rotation.
8
9
  *
9
- * Mounted at /proxy — all paths are forwarded to api.anthropic.com.
10
+ * Mounted at /proxy — so /proxy/v1/messages maps to Anthropic's POST /v1/messages.
10
11
  * Uses its own auth (proxy auth key), NOT PPM's auth middleware.
11
- *
12
- * Usage with Claude Code CLI:
13
- * ANTHROPIC_BASE_URL=http://host:port/proxy
14
- * ANTHROPIC_API_KEY=<proxy-auth-key>
15
12
  */
13
+ export const proxyRoutes = new Hono();
16
14
 
17
- /** Validate proxy auth key from Authorization or x-api-key header */
15
+ /** Validate proxy auth key from Authorization header */
18
16
  function validateProxyAuth(authHeader: string | undefined): boolean {
19
17
  if (!authHeader) return false;
20
18
  const key = proxyService.getAuthKey();
21
19
  if (!key) return false;
20
+ // Accept both "Bearer <key>" and raw "<key>" (x-api-key style)
22
21
  const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
23
22
  return token === key;
24
23
  }
25
24
 
26
- const CORS_HEADERS = {
27
- "Access-Control-Allow-Origin": "*",
28
- "Access-Control-Allow-Methods": "POST, GET, PUT, DELETE, PATCH, OPTIONS",
29
- "Access-Control-Allow-Headers": "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta",
30
- "Access-Control-Max-Age": "86400",
31
- };
25
+ /** CORS preflight for external tools */
26
+ proxyRoutes.options("/*", (c) => {
27
+ return new Response(null, {
28
+ status: 204,
29
+ headers: {
30
+ "Access-Control-Allow-Origin": "*",
31
+ "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
32
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta",
33
+ "Access-Control-Max-Age": "86400",
34
+ },
35
+ });
36
+ });
32
37
 
33
- /**
34
- * Standalone proxy request handler — called directly from Bun.serve fetch,
35
- * bypassing Hono routing to avoid SPA catch-all conflicts.
36
- * Returns a Response for /proxy/* requests, or null if path doesn't match.
37
- */
38
- export async function handleProxyRequest(req: Request): Promise<Response | null> {
39
- const url = new URL(req.url);
40
- if (!url.pathname.startsWith("/proxy/") && url.pathname !== "/proxy") return null;
38
+ /** POST /proxy/v1/messages — Anthropic Messages API proxy */
39
+ proxyRoutes.post("/v1/messages", async (c) => {
40
+ if (!proxyService.isEnabled()) {
41
+ return c.json({ type: "error", error: { type: "api_error", message: "Proxy is disabled" } }, 503);
42
+ }
41
43
 
42
- // CORS preflight
43
- if (req.method === "OPTIONS") {
44
- return new Response(null, { status: 204, headers: CORS_HEADERS });
44
+ // Auth check — accept both Authorization and x-api-key headers
45
+ const authHeader = c.req.header("authorization") || c.req.header("x-api-key");
46
+ if (!validateProxyAuth(authHeader)) {
47
+ return c.json({ type: "error", error: { type: "authentication_error", message: "Invalid proxy auth key" } }, 401);
45
48
  }
46
49
 
50
+ const body = await c.req.text();
51
+ const headers: Record<string, string> = {};
52
+ for (const key of ["anthropic-version", "anthropic-beta", "content-type"]) {
53
+ const val = c.req.header(key);
54
+ if (val) headers[key] = val;
55
+ }
56
+
57
+ return proxyService.forward("/v1/messages", "POST", headers, body);
58
+ });
59
+
60
+ /** POST /proxy/v1/messages/count_tokens — token counting proxy */
61
+ proxyRoutes.post("/v1/messages/count_tokens", async (c) => {
47
62
  if (!proxyService.isEnabled()) {
48
- return Response.json(
49
- { type: "error", error: { type: "api_error", message: "Proxy is disabled" } },
50
- { status: 503, headers: { "Access-Control-Allow-Origin": "*" } },
51
- );
63
+ return c.json({ type: "error", error: { type: "api_error", message: "Proxy is disabled" } }, 503);
52
64
  }
53
65
 
54
- // Auth check
55
- const authHeader = req.headers.get("authorization") || req.headers.get("x-api-key") || undefined;
66
+ const authHeader = c.req.header("authorization") || c.req.header("x-api-key");
56
67
  if (!validateProxyAuth(authHeader)) {
57
- return Response.json(
58
- { type: "error", error: { type: "authentication_error", message: "Invalid proxy auth key" } },
59
- { status: 401, headers: { "Access-Control-Allow-Origin": "*" } },
60
- );
68
+ return c.json({ type: "error", error: { type: "authentication_error", message: "Invalid proxy auth key" } }, 401);
61
69
  }
62
70
 
63
- // Strip /proxy prefix to get the Anthropic API path
64
- const path = url.pathname.replace(/^\/proxy/, "") || "/";
65
- const method = req.method;
66
-
67
- // Collect relevant headers to forward
71
+ const body = await c.req.text();
68
72
  const headers: Record<string, string> = {};
69
- for (const key of ["anthropic-version", "anthropic-beta", "content-type", "accept"]) {
70
- const val = req.headers.get(key);
73
+ for (const key of ["anthropic-version", "anthropic-beta", "content-type"]) {
74
+ const val = c.req.header(key);
71
75
  if (val) headers[key] = val;
72
76
  }
73
77
 
74
- // Read body for methods that have one
75
- const body = ["POST", "PUT", "PATCH"].includes(method) ? await req.text() : null;
76
-
77
- return proxyService.forward(path, method, headers, body);
78
- }
79
-
80
- // Keep Hono sub-router for backward compat with app.route() + tests
81
- export const proxyRoutes = new Hono();
82
- proxyRoutes.all("/*", async (c) => {
83
- const res = await handleProxyRequest(c.req.raw);
84
- if (res) return res;
85
- return c.notFound();
78
+ return proxyService.forward("/v1/messages/count_tokens", "POST", headers, body);
86
79
  });