@hienlh/ppm 0.8.55 → 0.8.57
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/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/CHANGELOG.md +10 -0
- package/dist/web/assets/{chat-tab-BUOCxR2G.js → chat-tab-cawT08fh.js} +3 -3
- package/dist/web/assets/{code-editor-os78eUN8.js → code-editor-u6bm6bdq.js} +1 -1
- package/dist/web/assets/{database-viewer-DTwe0h8F.js → database-viewer-BgPBW1bJ.js} +1 -1
- package/dist/web/assets/{diff-viewer-CSyOOmS2.js → diff-viewer-Cho-kjse.js} +1 -1
- package/dist/web/assets/{git-graph-CwYW3F4P.js → git-graph-CktRdFwt.js} +1 -1
- package/dist/web/assets/{index-yMR7OUDx.js → index-CpOYx0qg.js} +7 -13
- package/dist/web/assets/keybindings-store-vOnSm10D.js +1 -0
- package/dist/web/assets/{markdown-renderer-DQWY7QvX.js → markdown-renderer-3_CTktzg.js} +1 -1
- package/dist/web/assets/{postgres-viewer-Ctv7NTI_.js → postgres-viewer-CH0JfEQ9.js} +1 -1
- package/dist/web/assets/{settings-tab-Daap0c_B.js → settings-tab-BI0n39LJ.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DtNk76CE.js → sqlite-viewer-DJyT7YZg.js} +1 -1
- package/dist/web/assets/{terminal-tab-JEpjt3RD.js → terminal-tab-OqCohyF0.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -0
- package/src/server/index.ts +15 -3
- package/src/server/routes/proxy.ts +53 -46
- package/src/services/proxy.service.ts +19 -4
- package/src/services/supervisor.ts +14 -1
- package/src/web/components/chat/usage-badge.tsx +38 -6
- package/src/web/components/settings/proxy-settings-section.tsx +42 -40
- package/dist/web/assets/keybindings-store-B-BLLKiZ.js +0 -1
- package/snapshot-state.md +0 -1526
- package/test-tokens.mjs +0 -212
|
@@ -60,6 +60,12 @@ class ProxyService {
|
|
|
60
60
|
|
|
61
61
|
// Ensure token is fresh for OAuth accounts
|
|
62
62
|
let token = account.accessToken;
|
|
63
|
+
if (!token) {
|
|
64
|
+
return new Response(
|
|
65
|
+
JSON.stringify({ type: "error", error: { type: "authentication_error", message: "Account has no access token (decryption may have failed)" } }),
|
|
66
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
67
|
+
);
|
|
68
|
+
}
|
|
63
69
|
if (token.startsWith("sk-ant-oat")) {
|
|
64
70
|
const fresh = await accountService.ensureFreshToken(account.id);
|
|
65
71
|
if (fresh) token = fresh.accessToken;
|
|
@@ -108,14 +114,22 @@ class ProxyService {
|
|
|
108
114
|
} else if (upstream.status === 401) {
|
|
109
115
|
accountSelector.onAuthError(account.id);
|
|
110
116
|
console.log(`[proxy] 401 from Anthropic — account ${account.email ?? account.id} auth error`);
|
|
117
|
+
} else if (upstream.status >= 400) {
|
|
118
|
+
console.log(`[proxy] ${upstream.status} from Anthropic — account ${account.email ?? account.id} (OAuth=${token.startsWith("sk-ant-oat")})`);
|
|
111
119
|
} else if (upstream.status >= 200 && upstream.status < 300) {
|
|
112
120
|
accountSelector.onSuccess(account.id);
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
// Stream response back as-is (preserves SSE for streaming)
|
|
116
124
|
const responseHeaders = new Headers();
|
|
117
|
-
// Forward
|
|
118
|
-
for (const key of [
|
|
125
|
+
// Forward all relevant response headers from Anthropic
|
|
126
|
+
for (const key of [
|
|
127
|
+
"content-type", "x-request-id", "request-id",
|
|
128
|
+
"anthropic-ratelimit-requests-limit", "anthropic-ratelimit-requests-remaining",
|
|
129
|
+
"anthropic-ratelimit-requests-reset", "anthropic-ratelimit-tokens-limit",
|
|
130
|
+
"anthropic-ratelimit-tokens-remaining", "anthropic-ratelimit-tokens-reset",
|
|
131
|
+
"retry-after",
|
|
132
|
+
]) {
|
|
119
133
|
const val = upstream.headers.get(key);
|
|
120
134
|
if (val) responseHeaders.set(key, val);
|
|
121
135
|
}
|
|
@@ -127,9 +141,10 @@ class ProxyService {
|
|
|
127
141
|
headers: responseHeaders,
|
|
128
142
|
});
|
|
129
143
|
} catch (e) {
|
|
130
|
-
|
|
144
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
145
|
+
console.error(`[proxy] Error forwarding to Anthropic:`, msg);
|
|
131
146
|
return new Response(
|
|
132
|
-
JSON.stringify({ type: "error", error: { type: "api_error", message:
|
|
147
|
+
JSON.stringify({ type: "error", error: { type: "api_error", message: msg || "Unknown proxy error" } }),
|
|
133
148
|
{ status: 502, headers: { "Content-Type": "application/json" } },
|
|
134
149
|
);
|
|
135
150
|
}
|
|
@@ -328,6 +328,16 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
|
|
|
328
328
|
const currentSupervisorPid = process.pid;
|
|
329
329
|
|
|
330
330
|
try {
|
|
331
|
+
// Kill server + tunnel children FIRST to free the port for the new supervisor
|
|
332
|
+
log("INFO", "Stopping server and tunnel before spawning new supervisor");
|
|
333
|
+
if (serverChild) { try { serverChild.kill(); } catch {} serverChild = null; }
|
|
334
|
+
if (tunnelChild) { try { tunnelChild.kill(); } catch {} tunnelChild = null; }
|
|
335
|
+
// Clear health timers so we don't try to respawn killed children
|
|
336
|
+
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
|
337
|
+
if (tunnelProbeTimer) { clearInterval(tunnelProbeTimer); tunnelProbeTimer = null; }
|
|
338
|
+
// Brief wait for port release
|
|
339
|
+
await Bun.sleep(500);
|
|
340
|
+
|
|
331
341
|
// Spawn new supervisor using saved argv
|
|
332
342
|
const cmd = originalArgv.slice();
|
|
333
343
|
const logFd = openSync(LOG_FILE, "a");
|
|
@@ -346,7 +356,10 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
|
|
|
346
356
|
const data = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
|
|
347
357
|
if (data.supervisorPid && data.supervisorPid !== currentSupervisorPid) {
|
|
348
358
|
log("INFO", `New supervisor detected (PID: ${data.supervisorPid}), old exiting`);
|
|
349
|
-
|
|
359
|
+
// Children already killed, just clear remaining timers and exit
|
|
360
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
361
|
+
if (upgradeCheckTimer) clearInterval(upgradeCheckTimer);
|
|
362
|
+
if (upgradeDelayTimer) clearTimeout(upgradeDelayTimer);
|
|
350
363
|
process.exit(0);
|
|
351
364
|
}
|
|
352
365
|
} catch {}
|
|
@@ -97,7 +97,7 @@ function BucketRow({ label, bucket }: { label: string; bucket?: LimitBucket }) {
|
|
|
97
97
|
<div className="flex items-center justify-between">
|
|
98
98
|
<span className="text-xs font-medium text-text-primary">{label}</span>
|
|
99
99
|
{reset && (
|
|
100
|
-
<span className="text-[10px] text-text-subtle">↻ {reset}</span>
|
|
100
|
+
<span className="text-[10px] text-text-subtle" title="Resets in">↻ {reset}</span>
|
|
101
101
|
)}
|
|
102
102
|
</div>
|
|
103
103
|
<div className="flex items-center gap-2">
|
|
@@ -115,6 +115,28 @@ function BucketRow({ label, bucket }: { label: string; bucket?: LimitBucket }) {
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function formatExpiry(expiresAt: number): string {
|
|
119
|
+
const diff = expiresAt - Date.now();
|
|
120
|
+
if (diff <= 0) return "expired";
|
|
121
|
+
const mins = Math.ceil(diff / 60_000);
|
|
122
|
+
const h = Math.floor(mins / 60);
|
|
123
|
+
const d = Math.floor(h / 24);
|
|
124
|
+
if (d > 0) return `${d}d ${h % 24}h`;
|
|
125
|
+
if (h > 0) return `${h}h ${mins % 60}m`;
|
|
126
|
+
return `${mins}m`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Derive a human-readable token status from account info */
|
|
130
|
+
function tokenStatus(info?: AccountInfo): { label: string; tip: string; color: string } {
|
|
131
|
+
if (!info) return { label: "unknown", tip: "No account info available", color: "text-text-subtle" };
|
|
132
|
+
if (!info.expiresAt) return { label: "key", tip: "API key (no expiry)", color: "text-text-subtle" };
|
|
133
|
+
const expired = info.expiresAt * 1000 < Date.now(); // expiresAt is seconds
|
|
134
|
+
if (expired && info.hasRefreshToken) return { label: "expired", tip: "Token expired but has refresh token — will auto-renew", color: "text-amber-500" };
|
|
135
|
+
if (expired) return { label: "expired", tip: "Token expired, no refresh token", color: "text-red-500" };
|
|
136
|
+
if (info.hasRefreshToken) return { label: "long-lived", tip: "OAuth token with refresh — long-lived", color: "text-green-500" };
|
|
137
|
+
return { label: "temp", tip: "Temporary token without refresh — will expire", color: "text-amber-500" };
|
|
138
|
+
}
|
|
139
|
+
|
|
118
140
|
function formatLastUpdated(ts: number | null | undefined): string | null {
|
|
119
141
|
if (!ts) return null;
|
|
120
142
|
const secs = Math.round((Date.now() - ts) / 1000);
|
|
@@ -193,11 +215,21 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, on
|
|
|
193
215
|
{entry.isOAuth ? "No usage data yet" : "Usage tracking not available for API keys"}
|
|
194
216
|
</p>
|
|
195
217
|
)}
|
|
196
|
-
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
218
|
+
{/* Footer: updated · expires · type */}
|
|
219
|
+
{(() => {
|
|
220
|
+
const ts = tokenStatus(accountInfo);
|
|
221
|
+
return (
|
|
222
|
+
<div className="flex items-center gap-1.5 text-[9px] text-text-subtle flex-wrap">
|
|
223
|
+
{usage.lastFetchedAt && (
|
|
224
|
+
<span title="Last usage data update">↻ {formatLastUpdated(new Date(usage.lastFetchedAt).getTime())}</span>
|
|
225
|
+
)}
|
|
226
|
+
{accountInfo?.expiresAt && accountInfo.expiresAt * 1000 > Date.now() && (
|
|
227
|
+
<span title="Token expires in">⏱ {formatExpiry(accountInfo.expiresAt * 1000)}</span>
|
|
228
|
+
)}
|
|
229
|
+
<span className={ts.color} title={ts.tip}>© {ts.label}</span>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
})()}
|
|
201
233
|
</div>
|
|
202
234
|
);
|
|
203
235
|
}
|
|
@@ -127,76 +127,78 @@ export function ProxySettingsSection() {
|
|
|
127
127
|
<div className="space-y-2 rounded-md border p-3 bg-muted/30">
|
|
128
128
|
<h4 className="text-[11px] font-medium">Connection Info</h4>
|
|
129
129
|
|
|
130
|
-
{/*
|
|
130
|
+
{/* Base URL */}
|
|
131
131
|
<div className="space-y-1">
|
|
132
|
-
<Label className="text-[10px] text-muted-foreground">
|
|
132
|
+
<Label className="text-[10px] text-muted-foreground">Base URL</Label>
|
|
133
133
|
<div className="flex gap-1.5 items-center">
|
|
134
134
|
<code className="text-[10px] font-mono bg-muted px-1.5 py-0.5 rounded flex-1 truncate">
|
|
135
|
-
{
|
|
135
|
+
{hasTunnel && settings.tunnelUrl
|
|
136
|
+
? `${settings.tunnelUrl}/proxy`
|
|
137
|
+
: `${window.location.origin}/proxy`}
|
|
136
138
|
</code>
|
|
137
139
|
<Button
|
|
138
140
|
variant="ghost"
|
|
139
141
|
size="sm"
|
|
140
142
|
className="h-6 px-1.5 cursor-pointer shrink-0"
|
|
141
|
-
onClick={() => copyToClipboard(
|
|
143
|
+
onClick={() => copyToClipboard(
|
|
144
|
+
hasTunnel && settings.tunnelUrl
|
|
145
|
+
? `${settings.tunnelUrl}/proxy`
|
|
146
|
+
: `${window.location.origin}/proxy`,
|
|
147
|
+
"baseurl",
|
|
148
|
+
)}
|
|
142
149
|
>
|
|
143
|
-
{copied === "
|
|
150
|
+
{copied === "baseurl" ? "Copied!" : <Copy className="size-3" />}
|
|
144
151
|
</Button>
|
|
145
152
|
</div>
|
|
146
153
|
</div>
|
|
147
154
|
|
|
148
|
-
{/* Tunnel endpoint */}
|
|
149
|
-
{hasTunnel && settings.proxyEndpoint && (
|
|
150
|
-
<div className="space-y-1">
|
|
151
|
-
<Label className="text-[10px] text-muted-foreground">Public Endpoint (Tunnel)</Label>
|
|
152
|
-
<div className="flex gap-1.5 items-center">
|
|
153
|
-
<code className="text-[10px] font-mono bg-muted px-1.5 py-0.5 rounded flex-1 truncate">
|
|
154
|
-
{settings.proxyEndpoint}
|
|
155
|
-
</code>
|
|
156
|
-
<Button
|
|
157
|
-
variant="ghost"
|
|
158
|
-
size="sm"
|
|
159
|
-
className="h-6 px-1.5 cursor-pointer shrink-0"
|
|
160
|
-
onClick={() => copyToClipboard(settings.proxyEndpoint!, "tunnel")}
|
|
161
|
-
>
|
|
162
|
-
{copied === "tunnel" ? "Copied!" : <Copy className="size-3" />}
|
|
163
|
-
</Button>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
)}
|
|
167
|
-
|
|
168
155
|
{!hasTunnel && (
|
|
169
156
|
<p className="text-[10px] text-muted-foreground">
|
|
170
157
|
Start a Cloudflare tunnel (Share) to get a public URL.
|
|
171
158
|
</p>
|
|
172
159
|
)}
|
|
173
160
|
|
|
174
|
-
{/*
|
|
161
|
+
{/* Claude Code CLI usage */}
|
|
162
|
+
<div className="space-y-1 pt-1">
|
|
163
|
+
<Label className="text-[10px] text-muted-foreground">Claude Code CLI</Label>
|
|
164
|
+
<div className="relative">
|
|
165
|
+
<pre className="text-[9px] font-mono bg-muted p-2 rounded overflow-x-auto whitespace-pre">
|
|
166
|
+
{`ANTHROPIC_BASE_URL=${hasTunnel && settings.tunnelUrl ? settings.tunnelUrl + "/proxy" : window.location.origin + "/proxy"} \\
|
|
167
|
+
ANTHROPIC_API_KEY=${settings.authKey} \\
|
|
168
|
+
claude`}
|
|
169
|
+
</pre>
|
|
170
|
+
<Button
|
|
171
|
+
variant="ghost"
|
|
172
|
+
size="sm"
|
|
173
|
+
className="absolute top-1 right-1 h-5 px-1 cursor-pointer"
|
|
174
|
+
onClick={() => copyToClipboard(
|
|
175
|
+
`ANTHROPIC_BASE_URL=${hasTunnel && settings.tunnelUrl ? settings.tunnelUrl + "/proxy" : window.location.origin + "/proxy"} ANTHROPIC_API_KEY=${settings.authKey} claude`,
|
|
176
|
+
"claude-cli",
|
|
177
|
+
)}
|
|
178
|
+
>
|
|
179
|
+
{copied === "claude-cli" ? "Copied!" : <Copy className="size-2.5" />}
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Generic env vars */}
|
|
175
185
|
<div className="space-y-1 pt-1">
|
|
176
|
-
<Label className="text-[10px] text-muted-foreground">
|
|
186
|
+
<Label className="text-[10px] text-muted-foreground">Environment Variables</Label>
|
|
177
187
|
<div className="relative">
|
|
178
188
|
<pre className="text-[9px] font-mono bg-muted p-2 rounded overflow-x-auto whitespace-pre">
|
|
179
|
-
{
|
|
180
|
-
|
|
181
|
-
ANTHROPIC_API_KEY=${settings.authKey}
|
|
182
|
-
|
|
183
|
-
# Or use curl
|
|
184
|
-
curl ${hasTunnel && settings.proxyEndpoint ? settings.proxyEndpoint : window.location.origin + "/proxy/v1/messages"} \\
|
|
185
|
-
-H "x-api-key: ${settings.authKey}" \\
|
|
186
|
-
-H "content-type: application/json" \\
|
|
187
|
-
-H "anthropic-version: 2023-06-01" \\
|
|
188
|
-
-d '{"model":"claude-sonnet-4-6","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'`}
|
|
189
|
+
{`export ANTHROPIC_BASE_URL=${hasTunnel && settings.tunnelUrl ? settings.tunnelUrl + "/proxy" : window.location.origin + "/proxy"}
|
|
190
|
+
export ANTHROPIC_API_KEY=${settings.authKey}`}
|
|
189
191
|
</pre>
|
|
190
192
|
<Button
|
|
191
193
|
variant="ghost"
|
|
192
194
|
size="sm"
|
|
193
195
|
className="absolute top-1 right-1 h-5 px-1 cursor-pointer"
|
|
194
196
|
onClick={() => copyToClipboard(
|
|
195
|
-
`ANTHROPIC_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy" : window.location.origin + "/proxy"}\
|
|
196
|
-
"
|
|
197
|
+
`export ANTHROPIC_BASE_URL=${hasTunnel && settings.tunnelUrl ? settings.tunnelUrl + "/proxy" : window.location.origin + "/proxy"}\nexport ANTHROPIC_API_KEY=${settings.authKey}`,
|
|
198
|
+
"envvars",
|
|
197
199
|
)}
|
|
198
200
|
>
|
|
199
|
-
{copied === "
|
|
201
|
+
{copied === "envvars" ? "Copied!" : <Copy className="size-2.5" />}
|
|
200
202
|
</Button>
|
|
201
203
|
</div>
|
|
202
204
|
</div>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import"./react-nm2Ru1Pt.js";import"./api-client-DpGMOZNf.js";import{T as e}from"./index-yMR7OUDx.js";export{e as useKeybindingsStore};
|