@punkcode/cli 0.1.16 → 0.1.18
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/dist/App-GTPBJCS2.js +785 -0
- package/dist/chunk-NW32U73H.js +1775 -0
- package/dist/cli.js +106 -1726
- package/package.json +8 -2
|
@@ -0,0 +1,1775 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/version.ts
|
|
4
|
+
var version = "0.1.18";
|
|
5
|
+
|
|
6
|
+
// src/lib/auth.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import os from "os";
|
|
10
|
+
var FIREBASE_API_KEY = "AIzaSyDI5_jEY2s4UDB04av_p3RNkgZu3G7Sl18";
|
|
11
|
+
var AUTH_FILE = path.join(os.homedir(), ".punk", "auth.json");
|
|
12
|
+
function loadAuth() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function saveAuth(auth) {
|
|
20
|
+
const dir = path.dirname(AUTH_FILE);
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2) + "\n", "utf-8");
|
|
23
|
+
}
|
|
24
|
+
function clearAuth() {
|
|
25
|
+
try {
|
|
26
|
+
fs.unlinkSync(AUTH_FILE);
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function signIn(email, password) {
|
|
31
|
+
const res = await fetch(
|
|
32
|
+
`https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${FIREBASE_API_KEY}`,
|
|
33
|
+
{
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify({ email, password, returnSecureToken: true })
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const body = await res.json().catch(() => ({}));
|
|
41
|
+
const code = body?.error?.message ?? `HTTP ${res.status}`;
|
|
42
|
+
throw new Error(`Login failed: ${code}`);
|
|
43
|
+
}
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
const auth = {
|
|
46
|
+
idToken: data.idToken,
|
|
47
|
+
refreshToken: data.refreshToken,
|
|
48
|
+
expiresAt: Date.now() + parseInt(data.expiresIn, 10) * 1e3,
|
|
49
|
+
email: data.email,
|
|
50
|
+
uid: data.localId
|
|
51
|
+
};
|
|
52
|
+
saveAuth(auth);
|
|
53
|
+
return auth;
|
|
54
|
+
}
|
|
55
|
+
async function refreshIdToken() {
|
|
56
|
+
const auth = loadAuth();
|
|
57
|
+
if (!auth) {
|
|
58
|
+
throw new Error("Not logged in. Run `punk login` first.");
|
|
59
|
+
}
|
|
60
|
+
if (auth.expiresAt - Date.now() > 5 * 60 * 1e3) {
|
|
61
|
+
return auth.idToken;
|
|
62
|
+
}
|
|
63
|
+
const res = await fetch(
|
|
64
|
+
`https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`,
|
|
65
|
+
{
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/json" },
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
grant_type: "refresh_token",
|
|
70
|
+
refresh_token: auth.refreshToken
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const body = await res.json().catch(() => ({}));
|
|
76
|
+
const code = body?.error?.message ?? `HTTP ${res.status}`;
|
|
77
|
+
throw new Error(`Token refresh failed: ${code}. Run \`punk login\` again.`);
|
|
78
|
+
}
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
const updated = {
|
|
81
|
+
...auth,
|
|
82
|
+
idToken: data.id_token,
|
|
83
|
+
refreshToken: data.refresh_token,
|
|
84
|
+
expiresAt: Date.now() + parseInt(data.expires_in, 10) * 1e3
|
|
85
|
+
};
|
|
86
|
+
saveAuth(updated);
|
|
87
|
+
return updated.idToken;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/lib/connection.ts
|
|
91
|
+
import { io } from "socket.io-client";
|
|
92
|
+
import fs3 from "fs";
|
|
93
|
+
import os3 from "os";
|
|
94
|
+
import path3 from "path";
|
|
95
|
+
|
|
96
|
+
// src/lib/claude-sdk.ts
|
|
97
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
98
|
+
import { readdir, readFile } from "fs/promises";
|
|
99
|
+
import { join } from "path";
|
|
100
|
+
import { homedir } from "os";
|
|
101
|
+
|
|
102
|
+
// src/utils/logger.ts
|
|
103
|
+
import pino from "pino";
|
|
104
|
+
var level = process.env.LOG_LEVEL ?? "info";
|
|
105
|
+
var format = process.env.PUNK_LOG_FORMAT ?? (process.stdout.isTTY ? "pretty" : "json");
|
|
106
|
+
var transport = format === "pretty" ? pino.transport({
|
|
107
|
+
target: "pino-pretty",
|
|
108
|
+
options: { colorize: true }
|
|
109
|
+
}) : void 0;
|
|
110
|
+
var logger = pino({ level }, transport);
|
|
111
|
+
function createChildLogger(bindings) {
|
|
112
|
+
return logger.child(bindings);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/lib/claude-sdk.ts
|
|
116
|
+
async function* promptWithImages(text, images, sessionId) {
|
|
117
|
+
yield {
|
|
118
|
+
type: "user",
|
|
119
|
+
message: {
|
|
120
|
+
role: "user",
|
|
121
|
+
content: [
|
|
122
|
+
...images.map((img) => ({
|
|
123
|
+
type: "image",
|
|
124
|
+
source: {
|
|
125
|
+
type: "base64",
|
|
126
|
+
media_type: img.media_type,
|
|
127
|
+
data: img.data
|
|
128
|
+
}
|
|
129
|
+
})),
|
|
130
|
+
...text ? [{ type: "text", text }] : []
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
parent_tool_use_id: null,
|
|
134
|
+
session_id: sessionId
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function loadGlobalSkills(cwd) {
|
|
138
|
+
const claudeDir = join(homedir(), ".claude");
|
|
139
|
+
const skills = [];
|
|
140
|
+
async function collectSkillsFromDir(dir) {
|
|
141
|
+
const result = [];
|
|
142
|
+
try {
|
|
143
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
146
|
+
try {
|
|
147
|
+
const md = await readFile(join(dir, entry.name, "SKILL.md"), "utf-8");
|
|
148
|
+
const fmMatch = md.match(/^---\n([\s\S]*?)(\n---|\n*$)/);
|
|
149
|
+
if (!fmMatch) continue;
|
|
150
|
+
const fm = fmMatch[1];
|
|
151
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
152
|
+
if (!nameMatch) continue;
|
|
153
|
+
let description = "";
|
|
154
|
+
const descMatch = fm.match(/^description:\s*(.+)$/m);
|
|
155
|
+
if (descMatch) {
|
|
156
|
+
description = descMatch[1].trim();
|
|
157
|
+
} else {
|
|
158
|
+
const blockMatch = fm.match(/^description:\s*\n((?:[ \t]+.+\n?)+)/m);
|
|
159
|
+
if (blockMatch) {
|
|
160
|
+
description = blockMatch[1].replace(/^[ \t]+/gm, "").trim().replace(/\n/g, " ");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
result.push({ name: nameMatch[1].trim(), description });
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
const globalSkills = await collectSkillsFromDir(join(claudeDir, "skills"));
|
|
172
|
+
const projectSkills = cwd ? await collectSkillsFromDir(join(cwd, ".claude", "skills")) : [];
|
|
173
|
+
const projectNames = new Set(projectSkills.map((s) => s.name));
|
|
174
|
+
for (const s of globalSkills) {
|
|
175
|
+
if (!projectNames.has(s.name)) {
|
|
176
|
+
skills.push(s);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
skills.push(...projectSkills);
|
|
180
|
+
try {
|
|
181
|
+
const settings = JSON.parse(await readFile(join(claudeDir, "settings.json"), "utf-8"));
|
|
182
|
+
const plugins = settings.enabledPlugins;
|
|
183
|
+
if (plugins && typeof plugins === "object") {
|
|
184
|
+
for (const [key, enabled] of Object.entries(plugins)) {
|
|
185
|
+
if (!enabled) continue;
|
|
186
|
+
const [name, source] = key.split("@");
|
|
187
|
+
if (!name) continue;
|
|
188
|
+
let description = "";
|
|
189
|
+
if (source) {
|
|
190
|
+
try {
|
|
191
|
+
const cacheDir = join(claudeDir, "plugins", "cache", source, name);
|
|
192
|
+
const versions = await readdir(cacheDir);
|
|
193
|
+
const latest = versions.filter((v) => !v.startsWith(".")).sort().pop();
|
|
194
|
+
if (latest) {
|
|
195
|
+
const md = await readFile(join(cacheDir, latest, "skills", name, "SKILL.md"), "utf-8");
|
|
196
|
+
const descMatch = md.match(/^description:\s*(.+)$/m);
|
|
197
|
+
if (descMatch) description = descMatch[1].trim();
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
skills.push({ name, description });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
return skills;
|
|
208
|
+
}
|
|
209
|
+
async function getProjectCommands(workingDirectory) {
|
|
210
|
+
const q = query({
|
|
211
|
+
prompt: "/load-session-info",
|
|
212
|
+
options: {
|
|
213
|
+
persistSession: false,
|
|
214
|
+
...workingDirectory && { cwd: workingDirectory }
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
try {
|
|
218
|
+
const [commands, skills] = await Promise.all([
|
|
219
|
+
q.supportedCommands(),
|
|
220
|
+
loadGlobalSkills(workingDirectory)
|
|
221
|
+
]);
|
|
222
|
+
const slashCommands = commands.map((c) => ({ name: c.name, description: c.description }));
|
|
223
|
+
const knownNames = new Set(slashCommands.map((c) => c.name));
|
|
224
|
+
for (const skill of skills) {
|
|
225
|
+
if (!knownNames.has(skill.name)) {
|
|
226
|
+
slashCommands.push(skill);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
logger.info({ commands: slashCommands.length }, "Project commands retrieved");
|
|
230
|
+
return slashCommands;
|
|
231
|
+
} finally {
|
|
232
|
+
q.close();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function runClaude(options, callbacks) {
|
|
236
|
+
const opts = options.options || {};
|
|
237
|
+
const isBypass = opts.permissionMode === "bypassPermissions";
|
|
238
|
+
const pendingPermissions = /* @__PURE__ */ new Map();
|
|
239
|
+
let q;
|
|
240
|
+
try {
|
|
241
|
+
q = query({
|
|
242
|
+
prompt: options.images?.length ? promptWithImages(options.prompt, options.images, options.sessionId || "") : options.prompt,
|
|
243
|
+
options: {
|
|
244
|
+
permissionMode: opts.permissionMode || "default",
|
|
245
|
+
settingSources: ["user", "project"],
|
|
246
|
+
...isBypass && { allowDangerouslySkipPermissions: true },
|
|
247
|
+
...opts.model && { model: opts.model },
|
|
248
|
+
...opts.allowedTools && { allowedTools: opts.allowedTools },
|
|
249
|
+
...opts.disallowedTools && { disallowedTools: opts.disallowedTools },
|
|
250
|
+
...opts.effort && { effort: opts.effort },
|
|
251
|
+
...opts.maxTurns && { maxTurns: opts.maxTurns },
|
|
252
|
+
systemPrompt: opts.systemPrompt ?? { type: "preset", preset: "claude_code" },
|
|
253
|
+
...options.workingDirectory && { cwd: options.workingDirectory },
|
|
254
|
+
...options.sessionId && { resume: options.sessionId },
|
|
255
|
+
thinking: { type: "adaptive" },
|
|
256
|
+
includePartialMessages: true,
|
|
257
|
+
canUseTool: async (toolName, input, toolOpts) => {
|
|
258
|
+
if (!callbacks.onPermissionRequest) {
|
|
259
|
+
return { behavior: "allow", updatedInput: input };
|
|
260
|
+
}
|
|
261
|
+
const promise = new Promise((resolve) => {
|
|
262
|
+
pendingPermissions.set(toolOpts.toolUseID, resolve);
|
|
263
|
+
});
|
|
264
|
+
callbacks.onPermissionRequest({
|
|
265
|
+
toolUseId: toolOpts.toolUseID,
|
|
266
|
+
toolName,
|
|
267
|
+
input,
|
|
268
|
+
reason: toolOpts.decisionReason,
|
|
269
|
+
blockedPath: toolOpts.blockedPath
|
|
270
|
+
});
|
|
271
|
+
const result = await promise;
|
|
272
|
+
pendingPermissions.delete(toolOpts.toolUseID);
|
|
273
|
+
if (!result.allow) {
|
|
274
|
+
return { behavior: "deny", message: result.feedback || "Denied by user" };
|
|
275
|
+
}
|
|
276
|
+
if (toolName === "AskUserQuestion" && result.answers) {
|
|
277
|
+
return {
|
|
278
|
+
behavior: "allow",
|
|
279
|
+
updatedInput: { ...input, answers: result.answers }
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return { behavior: "allow", updatedInput: input };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
} catch (err) {
|
|
287
|
+
callbacks.onError(`Failed to create query: ${err}`);
|
|
288
|
+
return {
|
|
289
|
+
abort: () => {
|
|
290
|
+
},
|
|
291
|
+
resolvePermission: () => {
|
|
292
|
+
},
|
|
293
|
+
setPermissionMode: async () => {
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
(async () => {
|
|
298
|
+
let sentCompactSummary = false;
|
|
299
|
+
try {
|
|
300
|
+
for await (const message of q) {
|
|
301
|
+
switch (message.type) {
|
|
302
|
+
case "assistant": {
|
|
303
|
+
const content = message.message?.content ?? [];
|
|
304
|
+
for (const block of content) {
|
|
305
|
+
if (block.type === "tool_use") {
|
|
306
|
+
const tb = block;
|
|
307
|
+
callbacks.onToolUse(tb.id, tb.name, tb.input, message.parent_tool_use_id);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case "stream_event": {
|
|
313
|
+
const evt = message.event;
|
|
314
|
+
if (evt.type === "content_block_delta") {
|
|
315
|
+
if (evt.delta.type === "text_delta") {
|
|
316
|
+
callbacks.onText(evt.delta.text);
|
|
317
|
+
} else if (evt.delta.type === "thinking_delta") {
|
|
318
|
+
callbacks.onThinking?.(evt.delta.thinking);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case "user": {
|
|
324
|
+
const userContent = message.message?.content;
|
|
325
|
+
if (typeof userContent === "string") {
|
|
326
|
+
const match = userContent.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
|
|
327
|
+
if (match) {
|
|
328
|
+
if (sentCompactSummary && match[1].trim() === "Compacted") {
|
|
329
|
+
sentCompactSummary = false;
|
|
330
|
+
} else {
|
|
331
|
+
callbacks.onSlashCommandOutput?.(match[1]);
|
|
332
|
+
}
|
|
333
|
+
} else if (userContent.startsWith("This session is being continued")) {
|
|
334
|
+
sentCompactSummary = true;
|
|
335
|
+
callbacks.onSlashCommandOutput?.(userContent);
|
|
336
|
+
}
|
|
337
|
+
} else if (Array.isArray(userContent)) {
|
|
338
|
+
for (const block of userContent) {
|
|
339
|
+
if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_result") {
|
|
340
|
+
const tr = block;
|
|
341
|
+
callbacks.onToolResult(
|
|
342
|
+
tr.tool_use_id,
|
|
343
|
+
tr.content,
|
|
344
|
+
tr.is_error === true
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case "system": {
|
|
352
|
+
const sys = message;
|
|
353
|
+
if (sys.subtype === "init" && callbacks.onSessionCreated) {
|
|
354
|
+
const initCommands = (sys.slash_commands ?? []).map((cmd) => ({ name: cmd, description: "" }));
|
|
355
|
+
const globalSkills = await loadGlobalSkills(sys.cwd);
|
|
356
|
+
const knownNames = new Set(initCommands.map((c) => c.name));
|
|
357
|
+
for (const skill of globalSkills) {
|
|
358
|
+
if (!knownNames.has(skill.name)) {
|
|
359
|
+
initCommands.push(skill);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const sessionInfo = {
|
|
363
|
+
sessionId: sys.session_id ?? "",
|
|
364
|
+
tools: sys.tools ?? [],
|
|
365
|
+
slashCommands: initCommands,
|
|
366
|
+
skills: sys.skills ?? [],
|
|
367
|
+
mcpServers: sys.mcp_servers ?? [],
|
|
368
|
+
model: sys.model ?? "",
|
|
369
|
+
workingDirectory: sys.cwd ?? "",
|
|
370
|
+
claudeCodeVersion: sys.claude_code_version ?? "",
|
|
371
|
+
permissionMode: sys.permissionMode ?? "default"
|
|
372
|
+
};
|
|
373
|
+
logger.info({ sessionId: sessionInfo.sessionId, commands: sessionInfo.slashCommands.length }, "New chat session info");
|
|
374
|
+
callbacks.onSessionCreated(sessionInfo);
|
|
375
|
+
} else if (sys.subtype === "task_started" && callbacks.onTaskStarted) {
|
|
376
|
+
callbacks.onTaskStarted(sys.task_id, sys.description ?? "", sys.tool_use_id);
|
|
377
|
+
} else if (sys.subtype === "task_notification" && callbacks.onTaskNotification) {
|
|
378
|
+
callbacks.onTaskNotification(sys.task_id, sys.status ?? "completed", sys.summary ?? "", sys.tool_use_id);
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case "tool_use_summary": {
|
|
383
|
+
const summary = message.summary;
|
|
384
|
+
if (summary) {
|
|
385
|
+
callbacks.onSlashCommandOutput?.(summary);
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
case "result": {
|
|
390
|
+
const resultText = message.subtype === "success" ? message.result : void 0;
|
|
391
|
+
callbacks.onResult(message.session_id, resultText);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
callbacks.onError(`Query error: ${err}`);
|
|
398
|
+
}
|
|
399
|
+
})();
|
|
400
|
+
return {
|
|
401
|
+
abort: () => {
|
|
402
|
+
try {
|
|
403
|
+
q.close();
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
resolvePermission: (toolUseId, allow, answers, feedback) => {
|
|
408
|
+
pendingPermissions.get(toolUseId)?.({ allow, answers, feedback });
|
|
409
|
+
},
|
|
410
|
+
setPermissionMode: (mode) => q.setPermissionMode(mode)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/lib/device-info.ts
|
|
415
|
+
import os2 from "os";
|
|
416
|
+
import path2 from "path";
|
|
417
|
+
import fs2 from "fs";
|
|
418
|
+
import crypto from "crypto";
|
|
419
|
+
import { execaSync } from "execa";
|
|
420
|
+
var PUNK_DIR = path2.join(os2.homedir(), ".punk");
|
|
421
|
+
var CONFIG_FILE = path2.join(PUNK_DIR, "config.json");
|
|
422
|
+
function getOrCreateDeviceId() {
|
|
423
|
+
try {
|
|
424
|
+
const config2 = JSON.parse(fs2.readFileSync(CONFIG_FILE, "utf-8"));
|
|
425
|
+
if (config2.deviceId) return config2.deviceId;
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
const id = crypto.randomUUID();
|
|
429
|
+
fs2.mkdirSync(PUNK_DIR, { recursive: true });
|
|
430
|
+
let config = {};
|
|
431
|
+
try {
|
|
432
|
+
config = JSON.parse(fs2.readFileSync(CONFIG_FILE, "utf-8"));
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
config.deviceId = id;
|
|
436
|
+
fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
437
|
+
return id;
|
|
438
|
+
}
|
|
439
|
+
function getDefaultWorkingDirectory() {
|
|
440
|
+
try {
|
|
441
|
+
const config = JSON.parse(fs2.readFileSync(CONFIG_FILE, "utf-8"));
|
|
442
|
+
if (config.defaultWorkingDirectory) return config.defaultWorkingDirectory;
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
return path2.join(os2.homedir(), "punk");
|
|
446
|
+
}
|
|
447
|
+
function collectDeviceInfo(deviceId, customName, customTags, defaultCwd) {
|
|
448
|
+
if (customName) {
|
|
449
|
+
saveConfigField("deviceName", customName);
|
|
450
|
+
}
|
|
451
|
+
if (customTags && customTags.length > 0) {
|
|
452
|
+
saveConfigField("tags", customTags);
|
|
453
|
+
}
|
|
454
|
+
const cpus = os2.cpus();
|
|
455
|
+
return {
|
|
456
|
+
deviceId,
|
|
457
|
+
name: customName || getDeviceName(),
|
|
458
|
+
tags: customTags && customTags.length > 0 ? customTags : getTags(),
|
|
459
|
+
platform: process.platform,
|
|
460
|
+
arch: process.arch,
|
|
461
|
+
username: os2.userInfo().username,
|
|
462
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
463
|
+
defaultWorkingDirectory: defaultCwd || getDefaultWorkingDirectory(),
|
|
464
|
+
model: getModel(),
|
|
465
|
+
cpuModel: cpus.length > 0 ? cpus[0].model : "Unknown",
|
|
466
|
+
memoryGB: Math.round(os2.totalmem() / 1024 ** 3),
|
|
467
|
+
battery: parseBattery(),
|
|
468
|
+
claudeCodeVersion: getClaudeVersion()
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function getDeviceName() {
|
|
472
|
+
try {
|
|
473
|
+
const config = JSON.parse(fs2.readFileSync(CONFIG_FILE, "utf-8"));
|
|
474
|
+
if (config.deviceName) return config.deviceName;
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
if (process.platform === "darwin") {
|
|
478
|
+
try {
|
|
479
|
+
const { stdout } = execaSync("scutil", ["--get", "ComputerName"], { timeout: 3e3 });
|
|
480
|
+
const name = stdout.trim();
|
|
481
|
+
if (name) return name;
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return os2.hostname();
|
|
486
|
+
}
|
|
487
|
+
function getTags() {
|
|
488
|
+
try {
|
|
489
|
+
const config = JSON.parse(fs2.readFileSync(CONFIG_FILE, "utf-8"));
|
|
490
|
+
if (Array.isArray(config.tags)) return config.tags;
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
function saveConfigField(key, value) {
|
|
496
|
+
fs2.mkdirSync(PUNK_DIR, { recursive: true });
|
|
497
|
+
let config = {};
|
|
498
|
+
try {
|
|
499
|
+
config = JSON.parse(fs2.readFileSync(CONFIG_FILE, "utf-8"));
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
config[key] = value;
|
|
503
|
+
fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
504
|
+
}
|
|
505
|
+
function parseBattery() {
|
|
506
|
+
try {
|
|
507
|
+
if (process.platform === "darwin") {
|
|
508
|
+
const { stdout: out } = execaSync("pmset", ["-g", "batt"], { timeout: 3e3 });
|
|
509
|
+
const match = out.match(/(\d+)%;\s*(charging|discharging|charged|finishing charge)/i);
|
|
510
|
+
if (match) {
|
|
511
|
+
return {
|
|
512
|
+
level: parseInt(match[1], 10),
|
|
513
|
+
charging: match[2].toLowerCase() !== "discharging"
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
} else if (process.platform === "linux") {
|
|
517
|
+
const capacity = fs2.readFileSync("/sys/class/power_supply/BAT0/capacity", "utf-8").trim();
|
|
518
|
+
const status = fs2.readFileSync("/sys/class/power_supply/BAT0/status", "utf-8").trim();
|
|
519
|
+
return {
|
|
520
|
+
level: parseInt(capacity, 10),
|
|
521
|
+
charging: status.toLowerCase() !== "discharging"
|
|
522
|
+
};
|
|
523
|
+
} else if (process.platform === "win32") {
|
|
524
|
+
const { stdout: out } = execaSync("wmic", [
|
|
525
|
+
"path",
|
|
526
|
+
"Win32_Battery",
|
|
527
|
+
"get",
|
|
528
|
+
"EstimatedChargeRemaining,BatteryStatus",
|
|
529
|
+
"/format:csv"
|
|
530
|
+
], { timeout: 3e3 });
|
|
531
|
+
const lines = out.trim().split("\n").filter(Boolean);
|
|
532
|
+
if (lines.length >= 2) {
|
|
533
|
+
const parts = lines[lines.length - 1].split(",");
|
|
534
|
+
if (parts.length >= 3) {
|
|
535
|
+
return {
|
|
536
|
+
level: parseInt(parts[2], 10),
|
|
537
|
+
charging: parts[1] !== "1"
|
|
538
|
+
// 1 = discharging
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
function getClaudeVersion() {
|
|
548
|
+
try {
|
|
549
|
+
const { stdout } = execaSync("claude", ["--version"], { timeout: 5e3 });
|
|
550
|
+
return stdout.trim() || null;
|
|
551
|
+
} catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function getModel() {
|
|
556
|
+
try {
|
|
557
|
+
if (process.platform === "darwin") {
|
|
558
|
+
return execaSync("sysctl", ["-n", "hw.model"], { timeout: 3e3 }).stdout.trim() || null;
|
|
559
|
+
} else if (process.platform === "linux") {
|
|
560
|
+
return fs2.readFileSync("/sys/devices/virtual/dmi/id/product_name", "utf-8").trim() || null;
|
|
561
|
+
} else if (process.platform === "win32") {
|
|
562
|
+
const { stdout: out } = execaSync("wmic", ["csproduct", "get", "name", "/format:csv"], { timeout: 3e3 });
|
|
563
|
+
const lines = out.trim().split("\n").filter(Boolean);
|
|
564
|
+
if (lines.length >= 2) {
|
|
565
|
+
const parts = lines[lines.length - 1].split(",");
|
|
566
|
+
return parts.length >= 2 ? parts[1].trim() || null : null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
} catch {
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/lib/session.ts
|
|
575
|
+
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
576
|
+
import { join as join2 } from "path";
|
|
577
|
+
import { homedir as homedir2 } from "os";
|
|
578
|
+
import { listSessions as sdkListSessions } from "@anthropic-ai/claude-agent-sdk";
|
|
579
|
+
var CLAUDE_DIR = join2(homedir2(), ".claude", "projects");
|
|
580
|
+
async function loadSession(sessionId) {
|
|
581
|
+
const sessionFile = `${sessionId}.jsonl`;
|
|
582
|
+
let projectDirs;
|
|
583
|
+
try {
|
|
584
|
+
projectDirs = await readdir2(CLAUDE_DIR);
|
|
585
|
+
} catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
for (const projectDir of projectDirs) {
|
|
589
|
+
const sessionPath = join2(CLAUDE_DIR, projectDir, sessionFile);
|
|
590
|
+
try {
|
|
591
|
+
const content = await readFile2(sessionPath, "utf-8");
|
|
592
|
+
const messages = parseSessionFile(content);
|
|
593
|
+
const subagentsDir = join2(CLAUDE_DIR, projectDir, sessionId, "subagents");
|
|
594
|
+
await attachSubagentData(messages, subagentsDir);
|
|
595
|
+
return messages;
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
var AGENT_ID_RE = /agentId: (\w+)/;
|
|
602
|
+
async function attachSubagentData(messages, subagentsDir) {
|
|
603
|
+
const taskBlocks = [];
|
|
604
|
+
for (const msg of messages) {
|
|
605
|
+
if (msg.role !== "assistant") continue;
|
|
606
|
+
const blocks = msg.content;
|
|
607
|
+
if (!Array.isArray(blocks)) continue;
|
|
608
|
+
for (const block of blocks) {
|
|
609
|
+
if (block.type !== "tool_use" || block.name !== "Task") continue;
|
|
610
|
+
const result = block.result;
|
|
611
|
+
if (typeof result !== "string") continue;
|
|
612
|
+
const match = result.match(AGENT_ID_RE);
|
|
613
|
+
if (match) {
|
|
614
|
+
taskBlocks.push({ block, agentId: match[1] });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (taskBlocks.length === 0) return;
|
|
619
|
+
await Promise.all(taskBlocks.map(async ({ block, agentId }) => {
|
|
620
|
+
try {
|
|
621
|
+
const content = await readFile2(join2(subagentsDir, `agent-${agentId}.jsonl`), "utf-8");
|
|
622
|
+
block.subagentMessages = parseSessionFile(content);
|
|
623
|
+
} catch {
|
|
624
|
+
}
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
async function listSessions(workingDirectory) {
|
|
628
|
+
try {
|
|
629
|
+
const sdkSessions = await sdkListSessions({
|
|
630
|
+
...workingDirectory && { dir: workingDirectory },
|
|
631
|
+
limit: 50
|
|
632
|
+
});
|
|
633
|
+
return sdkSessions.map((s) => ({
|
|
634
|
+
sessionId: s.sessionId,
|
|
635
|
+
project: workingDirectory ?? s.cwd ?? "",
|
|
636
|
+
title: s.summary,
|
|
637
|
+
lastModified: s.lastModified,
|
|
638
|
+
cwd: s.cwd,
|
|
639
|
+
gitBranch: s.gitBranch,
|
|
640
|
+
fileSize: s.fileSize
|
|
641
|
+
}));
|
|
642
|
+
} catch {
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
var TOOL_RESULT_PREVIEW_BYTES = 2048;
|
|
647
|
+
var ANSI_RE = /\u001b\[\d*m/g;
|
|
648
|
+
function stripAnsi(text) {
|
|
649
|
+
return text.replace(ANSI_RE, "");
|
|
650
|
+
}
|
|
651
|
+
function parseSessionFile(content) {
|
|
652
|
+
const messages = [];
|
|
653
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
654
|
+
const metaUuids = /* @__PURE__ */ new Set();
|
|
655
|
+
const taskNotifications = /* @__PURE__ */ new Map();
|
|
656
|
+
for (const line of lines) {
|
|
657
|
+
try {
|
|
658
|
+
const entry = JSON.parse(line);
|
|
659
|
+
if (entry.isMeta || entry.parentUuid && metaUuids.has(entry.parentUuid)) {
|
|
660
|
+
if (entry.uuid && entry.message?.role !== "assistant") {
|
|
661
|
+
metaUuids.add(entry.uuid);
|
|
662
|
+
}
|
|
663
|
+
if (entry.message?.role === "user" && typeof entry.message.content === "string") {
|
|
664
|
+
const content2 = entry.message.content;
|
|
665
|
+
const cmdMatch = content2.match(/<command-name>\/(.+?)<\/command-name>/);
|
|
666
|
+
if (cmdMatch) {
|
|
667
|
+
messages.push({
|
|
668
|
+
role: "user",
|
|
669
|
+
content: [{ type: "text", text: `/${cmdMatch[1]}` }],
|
|
670
|
+
timestamp: entry.timestamp,
|
|
671
|
+
isMeta: entry.isMeta,
|
|
672
|
+
uuid: entry.uuid,
|
|
673
|
+
parentUuid: entry.parentUuid,
|
|
674
|
+
type: entry.type
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const outMatch = content2.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
|
|
678
|
+
if (outMatch && outMatch[1].trim()) {
|
|
679
|
+
const text = stripAnsi(outMatch[1].trim());
|
|
680
|
+
if (text) {
|
|
681
|
+
messages.push({
|
|
682
|
+
role: "assistant",
|
|
683
|
+
content: [{ type: "text", text }],
|
|
684
|
+
timestamp: entry.timestamp,
|
|
685
|
+
isMeta: entry.isMeta,
|
|
686
|
+
uuid: entry.uuid,
|
|
687
|
+
parentUuid: entry.parentUuid,
|
|
688
|
+
type: entry.type
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (entry.type === "system" && entry.subtype === "compact_boundary") {
|
|
696
|
+
messages.push({
|
|
697
|
+
role: "system",
|
|
698
|
+
content: [],
|
|
699
|
+
timestamp: entry.timestamp,
|
|
700
|
+
type: "system",
|
|
701
|
+
subtype: entry.subtype,
|
|
702
|
+
uuid: entry.uuid,
|
|
703
|
+
parentUuid: entry.parentUuid
|
|
704
|
+
});
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (entry.isCompactSummary && entry.message) {
|
|
708
|
+
const summaryContent = entry.message.content;
|
|
709
|
+
let summaryText = "";
|
|
710
|
+
if (typeof summaryContent === "string") {
|
|
711
|
+
summaryText = summaryContent;
|
|
712
|
+
} else if (Array.isArray(summaryContent)) {
|
|
713
|
+
summaryText = summaryContent.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
|
714
|
+
}
|
|
715
|
+
if (summaryText) {
|
|
716
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
717
|
+
if (messages[i].subtype === "compact_boundary") {
|
|
718
|
+
messages[i].content = [{ type: "text", text: summaryText }];
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (entry.type === "user" && typeof entry.message?.content === "string") {
|
|
726
|
+
const raw = entry.message.content;
|
|
727
|
+
const notifMatch = raw.match(/<task-notification>([\s\S]*?)<\/task-notification>/);
|
|
728
|
+
if (notifMatch) {
|
|
729
|
+
const inner = notifMatch[1];
|
|
730
|
+
const toolUseId = inner.match(/<tool-use-id>(.*?)<\/tool-use-id>/)?.[1];
|
|
731
|
+
if (toolUseId) {
|
|
732
|
+
const taskId = inner.match(/<task-id>(.*?)<\/task-id>/)?.[1] ?? "";
|
|
733
|
+
const status = inner.match(/<status>(.*?)<\/status>/)?.[1] ?? "completed";
|
|
734
|
+
const summary = inner.match(/<summary>([\s\S]*?)<\/summary>/)?.[1]?.trim() ?? "";
|
|
735
|
+
taskNotifications.set(toolUseId, { taskId, status, summary });
|
|
736
|
+
}
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if ((entry.type === "user" || entry.type === "assistant") && entry.message) {
|
|
741
|
+
const msgContent = entry.message.content;
|
|
742
|
+
messages.push({
|
|
743
|
+
role: entry.message.role,
|
|
744
|
+
content: typeof msgContent === "string" ? [{ type: "text", text: msgContent }] : msgContent,
|
|
745
|
+
timestamp: entry.timestamp,
|
|
746
|
+
uuid: entry.uuid,
|
|
747
|
+
parentUuid: entry.parentUuid,
|
|
748
|
+
type: entry.type
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
} catch {
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
for (let i = 0; i < messages.length; i++) {
|
|
755
|
+
const msg = messages[i];
|
|
756
|
+
if (msg.role !== "user") continue;
|
|
757
|
+
const blocks = msg.content;
|
|
758
|
+
if (!Array.isArray(blocks)) continue;
|
|
759
|
+
for (const block of blocks) {
|
|
760
|
+
if (block.type !== "tool_result") continue;
|
|
761
|
+
const content2 = block.content;
|
|
762
|
+
let dataUri;
|
|
763
|
+
if (Array.isArray(content2)) {
|
|
764
|
+
const imgBlock = content2.find(
|
|
765
|
+
(b) => b?.type === "image" && b?.source?.type === "base64" && b?.source?.data
|
|
766
|
+
);
|
|
767
|
+
if (imgBlock) {
|
|
768
|
+
dataUri = `data:${imgBlock.source.media_type};base64,${imgBlock.source.data}`;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
let resultText;
|
|
772
|
+
if (typeof content2 === "string") {
|
|
773
|
+
resultText = content2 || void 0;
|
|
774
|
+
} else if (Array.isArray(content2)) {
|
|
775
|
+
const texts = content2.filter((b) => b?.type === "text" && typeof b?.text === "string").map((b) => b.text);
|
|
776
|
+
resultText = texts.join("\n") || void 0;
|
|
777
|
+
}
|
|
778
|
+
if (!dataUri && !resultText) continue;
|
|
779
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
780
|
+
if (messages[j].role !== "assistant") continue;
|
|
781
|
+
const aBlocks = messages[j].content;
|
|
782
|
+
if (!Array.isArray(aBlocks)) break;
|
|
783
|
+
const toolUse = aBlocks.find(
|
|
784
|
+
(b) => b.type === "tool_use" && b.id === block.tool_use_id
|
|
785
|
+
);
|
|
786
|
+
if (toolUse) {
|
|
787
|
+
if (dataUri) toolUse.imageUri = dataUri;
|
|
788
|
+
if (resultText) toolUse.result = resultText.length > TOOL_RESULT_PREVIEW_BYTES ? resultText.slice(0, TOOL_RESULT_PREVIEW_BYTES) + "\u2026" : resultText;
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (taskNotifications.size > 0) {
|
|
795
|
+
for (const msg of messages) {
|
|
796
|
+
if (msg.role !== "assistant") continue;
|
|
797
|
+
const blocks = msg.content;
|
|
798
|
+
if (!Array.isArray(blocks)) continue;
|
|
799
|
+
for (const block of blocks) {
|
|
800
|
+
if (block.type !== "tool_use" || block.name !== "Task") continue;
|
|
801
|
+
const notif = taskNotifications.get(block.id);
|
|
802
|
+
if (notif) {
|
|
803
|
+
block.taskStatus = notif.status;
|
|
804
|
+
block.taskSummary = notif.summary;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const merged = [];
|
|
810
|
+
for (const msg of messages) {
|
|
811
|
+
if (msg.role === "user") {
|
|
812
|
+
const blocks = msg.content;
|
|
813
|
+
const hasText = blocks.some((b) => b.type === "text" && b.text?.trim());
|
|
814
|
+
if (!hasText) continue;
|
|
815
|
+
}
|
|
816
|
+
const prev = merged[merged.length - 1];
|
|
817
|
+
if (msg.role === "assistant" && prev?.role === "assistant") {
|
|
818
|
+
prev.content = [...prev.content, ...msg.content];
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
merged.push({ ...msg, content: [...msg.content] });
|
|
822
|
+
}
|
|
823
|
+
return merged;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/lib/context.ts
|
|
827
|
+
import { execa } from "execa";
|
|
828
|
+
var log = createChildLogger({ component: "context" });
|
|
829
|
+
async function getContext(sessionId, workingDirectory) {
|
|
830
|
+
let stdout;
|
|
831
|
+
try {
|
|
832
|
+
const result = await execa("claude", [
|
|
833
|
+
"-p",
|
|
834
|
+
"--output-format",
|
|
835
|
+
"json",
|
|
836
|
+
"--verbose",
|
|
837
|
+
"--resume",
|
|
838
|
+
sessionId,
|
|
839
|
+
"/context"
|
|
840
|
+
], {
|
|
841
|
+
cwd: workingDirectory || process.cwd(),
|
|
842
|
+
timeout: 3e4,
|
|
843
|
+
stdin: "ignore"
|
|
844
|
+
});
|
|
845
|
+
stdout = result.stdout;
|
|
846
|
+
if (result.stderr) {
|
|
847
|
+
log.warn({ stderr: result.stderr.trim() }, "Command stderr");
|
|
848
|
+
}
|
|
849
|
+
} catch (err) {
|
|
850
|
+
const execErr = err;
|
|
851
|
+
log.error({
|
|
852
|
+
exitCode: execErr.exitCode ?? null,
|
|
853
|
+
stderr: execErr.stderr?.trim(),
|
|
854
|
+
stdout: execErr.stdout?.slice(0, 500)
|
|
855
|
+
}, "Command failed");
|
|
856
|
+
throw err;
|
|
857
|
+
}
|
|
858
|
+
log.debug({ chars: stdout.length }, "Raw stdout");
|
|
859
|
+
const markdown = extractMarkdown(stdout);
|
|
860
|
+
log.debug({ chars: markdown.length }, "Parsed markdown");
|
|
861
|
+
return parseContextMarkdown(markdown);
|
|
862
|
+
}
|
|
863
|
+
function extractMarkdown(stdout) {
|
|
864
|
+
const parsed = JSON.parse(stdout);
|
|
865
|
+
const block = parsed[1];
|
|
866
|
+
const content = block?.message?.content ?? "";
|
|
867
|
+
const match = content.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
|
|
868
|
+
return match ? match[1] : content;
|
|
869
|
+
}
|
|
870
|
+
function parseTokenValue(raw) {
|
|
871
|
+
const trimmed = raw.trim().replace(/,/g, "");
|
|
872
|
+
const kMatch = trimmed.match(/^([\d.]+)k$/i);
|
|
873
|
+
if (kMatch) {
|
|
874
|
+
return Math.round(parseFloat(kMatch[1]) * 1e3);
|
|
875
|
+
}
|
|
876
|
+
return parseInt(trimmed, 10) || 0;
|
|
877
|
+
}
|
|
878
|
+
function parseContextMarkdown(markdown) {
|
|
879
|
+
const data = {
|
|
880
|
+
model: "",
|
|
881
|
+
totalTokens: 0,
|
|
882
|
+
contextWindow: 0,
|
|
883
|
+
usedPercentage: 0,
|
|
884
|
+
categories: [],
|
|
885
|
+
mcpTools: [],
|
|
886
|
+
memoryFiles: [],
|
|
887
|
+
skills: [],
|
|
888
|
+
rawMarkdown: markdown
|
|
889
|
+
};
|
|
890
|
+
const modelMatch = markdown.match(/\*\*Model:\*\*\s*(.+)/);
|
|
891
|
+
if (modelMatch) {
|
|
892
|
+
data.model = modelMatch[1].trim();
|
|
893
|
+
}
|
|
894
|
+
const tokenMatch = markdown.match(/\*\*Tokens:\*\*\s*([\d.,]+k?)\s*\/\s*([\d.,]+k?)\s*\((\d+)%\)/i);
|
|
895
|
+
if (tokenMatch) {
|
|
896
|
+
data.totalTokens = parseTokenValue(tokenMatch[1]);
|
|
897
|
+
data.contextWindow = parseTokenValue(tokenMatch[2]);
|
|
898
|
+
data.usedPercentage = parseInt(tokenMatch[3], 10);
|
|
899
|
+
}
|
|
900
|
+
data.categories = parseTable(markdown, "Estimated usage by category", ["category", "tokens", "percentage"]);
|
|
901
|
+
data.mcpTools = parseTable(markdown, "MCP Tools", ["tool", "server", "tokens"]);
|
|
902
|
+
data.memoryFiles = parseTable(markdown, "Memory Files", ["type", "path", "tokens"]);
|
|
903
|
+
data.skills = parseTable(markdown, "Skills", ["skill", "source", "tokens"]);
|
|
904
|
+
return data;
|
|
905
|
+
}
|
|
906
|
+
function parseTable(markdown, sectionHeader, keys) {
|
|
907
|
+
const headerPattern = new RegExp(`#{2,3}\\s*${escapeRegex(sectionHeader)}`, "i");
|
|
908
|
+
const headerMatch = markdown.match(headerPattern);
|
|
909
|
+
if (!headerMatch || headerMatch.index === void 0) return [];
|
|
910
|
+
const afterHeader = markdown.slice(headerMatch.index + headerMatch[0].length);
|
|
911
|
+
const nextSection = afterHeader.search(/\n#{2,3}\s/);
|
|
912
|
+
const sectionText = nextSection !== -1 ? afterHeader.slice(0, nextSection) : afterHeader;
|
|
913
|
+
const lines = sectionText.split("\n").filter((line) => line.trim().startsWith("|"));
|
|
914
|
+
if (lines.length < 3) return [];
|
|
915
|
+
const dataRows = lines.slice(2);
|
|
916
|
+
return dataRows.map((row) => {
|
|
917
|
+
const cells = row.split("|").slice(1, -1).map((c) => c.trim());
|
|
918
|
+
const obj = {};
|
|
919
|
+
keys.forEach((key, i) => {
|
|
920
|
+
const cell = cells[i] ?? "";
|
|
921
|
+
if (key === "tokens") {
|
|
922
|
+
obj[key] = parseTokenValue(cell);
|
|
923
|
+
} else if (key === "percentage") {
|
|
924
|
+
obj[key] = parseInt(cell.replace("%", ""), 10) || 0;
|
|
925
|
+
} else {
|
|
926
|
+
obj[key] = cell;
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
return obj;
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
function escapeRegex(str) {
|
|
933
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// src/lib/directory-discovery.ts
|
|
937
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
938
|
+
async function findProjectDirectory(description, searchRoot, rejections, signal) {
|
|
939
|
+
let prompt = `Find up to 3 project directories on this machine that best match: "${description}"
|
|
940
|
+
|
|
941
|
+
Search starting from ${searchRoot}. Rank by relevance. Include a brief reason for each match.`;
|
|
942
|
+
if (rejections?.length) {
|
|
943
|
+
prompt += "\n\nThe user rejected these previous suggestions:";
|
|
944
|
+
for (const r of rejections) {
|
|
945
|
+
prompt += `
|
|
946
|
+
- ${r.path}${r.feedback ? ` (user said: "${r.feedback}")` : ""}`;
|
|
947
|
+
}
|
|
948
|
+
prompt += "\n\nFind different matches based on their feedback.";
|
|
949
|
+
}
|
|
950
|
+
const q = query2({
|
|
951
|
+
prompt,
|
|
952
|
+
options: {
|
|
953
|
+
systemPrompt: {
|
|
954
|
+
type: "preset",
|
|
955
|
+
preset: "claude_code",
|
|
956
|
+
append: "IMPORTANT: When searching for directories, always try listing likely parent directories with ls FIRST (e.g. ls ~/github, ls ~/projects). Only use find as a last resort, and always with -maxdepth 3. Never scan the entire home directory."
|
|
957
|
+
},
|
|
958
|
+
permissionMode: "bypassPermissions",
|
|
959
|
+
persistSession: false,
|
|
960
|
+
cwd: searchRoot,
|
|
961
|
+
outputFormat: {
|
|
962
|
+
type: "json_schema",
|
|
963
|
+
schema: {
|
|
964
|
+
type: "object",
|
|
965
|
+
properties: {
|
|
966
|
+
suggestions: {
|
|
967
|
+
type: "array",
|
|
968
|
+
items: {
|
|
969
|
+
type: "object",
|
|
970
|
+
properties: {
|
|
971
|
+
path: { type: "string", description: "Absolute path to the project directory" },
|
|
972
|
+
name: { type: "string", description: "Human-readable project name" },
|
|
973
|
+
reason: { type: "string", description: 'Brief reason why this matches (e.g. "Direct match under github folder")' }
|
|
974
|
+
},
|
|
975
|
+
required: ["path", "name", "reason"]
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
required: ["suggestions"]
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
const onAbort = () => q.close();
|
|
985
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
986
|
+
try {
|
|
987
|
+
for await (const msg of q) {
|
|
988
|
+
if (msg.type === "result") {
|
|
989
|
+
const resultMsg = msg;
|
|
990
|
+
if (resultMsg.subtype === "success") {
|
|
991
|
+
const structured = resultMsg.structured_output;
|
|
992
|
+
if (structured?.suggestions?.length) {
|
|
993
|
+
logger.info({ count: structured.suggestions.length }, "Project directories found (structured)");
|
|
994
|
+
return structured.suggestions;
|
|
995
|
+
}
|
|
996
|
+
const result = resultMsg.result?.trim();
|
|
997
|
+
if (result && result !== "null" && result.startsWith("/")) {
|
|
998
|
+
const path4 = result.split("\n")[0].trim();
|
|
999
|
+
const name = path4.split("/").pop() ?? path4;
|
|
1000
|
+
logger.info({ path: path4, name }, "Project directory found (text fallback)");
|
|
1001
|
+
return [{ path: path4, name, reason: "Best match" }];
|
|
1002
|
+
}
|
|
1003
|
+
logger.info("No matching directories found");
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
logger.warn({ subtype: resultMsg.subtype }, "Directory search query failed");
|
|
1007
|
+
return [];
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} finally {
|
|
1011
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1012
|
+
q.close();
|
|
1013
|
+
}
|
|
1014
|
+
return [];
|
|
1015
|
+
}
|
|
1016
|
+
async function suggestProjectLocation(description, searchRoot, name, signal) {
|
|
1017
|
+
const prompt = `Suggest up to 3 suitable locations on this machine to create a new project.
|
|
1018
|
+
|
|
1019
|
+
User's description: "${description}"
|
|
1020
|
+
${name ? `Desired project name: "${name}"` : ""}
|
|
1021
|
+
|
|
1022
|
+
Search starting from ${searchRoot}. Look at common project directories (e.g. ~/github, ~/projects, ~/code, ~/Desktop).
|
|
1023
|
+
For each suggestion, provide the full path WHERE the project folder would be created (including the project name as the last segment), a human-readable name, and a brief reason.`;
|
|
1024
|
+
const q = query2({
|
|
1025
|
+
prompt,
|
|
1026
|
+
options: {
|
|
1027
|
+
systemPrompt: {
|
|
1028
|
+
type: "preset",
|
|
1029
|
+
preset: "claude_code",
|
|
1030
|
+
append: "IMPORTANT: When searching for directories, always try listing likely parent directories with ls FIRST (e.g. ls ~/github, ls ~/projects). Only use find as a last resort, and always with -maxdepth 3. Never scan the entire home directory. The path in each suggestion must be the FULL path including the new project folder name."
|
|
1031
|
+
},
|
|
1032
|
+
permissionMode: "bypassPermissions",
|
|
1033
|
+
persistSession: false,
|
|
1034
|
+
cwd: searchRoot,
|
|
1035
|
+
outputFormat: {
|
|
1036
|
+
type: "json_schema",
|
|
1037
|
+
schema: {
|
|
1038
|
+
type: "object",
|
|
1039
|
+
properties: {
|
|
1040
|
+
suggestions: {
|
|
1041
|
+
type: "array",
|
|
1042
|
+
items: {
|
|
1043
|
+
type: "object",
|
|
1044
|
+
properties: {
|
|
1045
|
+
path: { type: "string", description: "Absolute path for the new project directory (including project folder name)" },
|
|
1046
|
+
name: { type: "string", description: "Human-readable project name" },
|
|
1047
|
+
reason: { type: "string", description: "Brief reason why this location is suitable" }
|
|
1048
|
+
},
|
|
1049
|
+
required: ["path", "name", "reason"]
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
required: ["suggestions"]
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
const onAbort = () => q.close();
|
|
1059
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1060
|
+
try {
|
|
1061
|
+
for await (const msg of q) {
|
|
1062
|
+
if (msg.type === "result") {
|
|
1063
|
+
const resultMsg = msg;
|
|
1064
|
+
if (resultMsg.subtype === "success") {
|
|
1065
|
+
const structured = resultMsg.structured_output;
|
|
1066
|
+
if (structured?.suggestions?.length) {
|
|
1067
|
+
logger.info({ count: structured.suggestions.length }, "Location suggestions found (structured)");
|
|
1068
|
+
return structured.suggestions;
|
|
1069
|
+
}
|
|
1070
|
+
logger.info("No location suggestions found");
|
|
1071
|
+
return [];
|
|
1072
|
+
}
|
|
1073
|
+
logger.warn({ subtype: resultMsg.subtype }, "Location suggestion query failed");
|
|
1074
|
+
return [];
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
} finally {
|
|
1078
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1079
|
+
q.close();
|
|
1080
|
+
}
|
|
1081
|
+
return [];
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/lib/sleep-inhibitor.ts
|
|
1085
|
+
import { spawn } from "child_process";
|
|
1086
|
+
function preventIdleSleep() {
|
|
1087
|
+
const platform = process.platform;
|
|
1088
|
+
let child = null;
|
|
1089
|
+
if (platform === "darwin") {
|
|
1090
|
+
child = spawn("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
1091
|
+
stdio: "ignore",
|
|
1092
|
+
detached: false
|
|
1093
|
+
});
|
|
1094
|
+
} else if (platform === "linux") {
|
|
1095
|
+
child = spawn(
|
|
1096
|
+
"systemd-inhibit",
|
|
1097
|
+
[
|
|
1098
|
+
"--what=idle",
|
|
1099
|
+
"--who=punk-connect",
|
|
1100
|
+
"--why=Device connected for remote access",
|
|
1101
|
+
"cat"
|
|
1102
|
+
],
|
|
1103
|
+
{ stdio: ["pipe", "ignore", "ignore"], detached: false }
|
|
1104
|
+
);
|
|
1105
|
+
} else if (platform === "win32") {
|
|
1106
|
+
const script = `
|
|
1107
|
+
$sig = '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags);';
|
|
1108
|
+
$t = Add-Type -MemberDefinition $sig -Name WinAPI -Namespace Punk -PassThru;
|
|
1109
|
+
while($true) {
|
|
1110
|
+
$t::SetThreadExecutionState(0x80000001) | Out-Null;
|
|
1111
|
+
try { $null = Read-Host } catch { break };
|
|
1112
|
+
Start-Sleep -Seconds 30;
|
|
1113
|
+
}`.trim();
|
|
1114
|
+
child = spawn("powershell", ["-NoProfile", "-Command", script], {
|
|
1115
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
1116
|
+
detached: false
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
child?.unref();
|
|
1120
|
+
child?.on("error", () => {
|
|
1121
|
+
});
|
|
1122
|
+
return {
|
|
1123
|
+
release: () => {
|
|
1124
|
+
if (child && !child.killed) {
|
|
1125
|
+
child.kill();
|
|
1126
|
+
child = null;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/lib/connection.ts
|
|
1133
|
+
var DEFAULT_HISTORY_LIMIT = 30;
|
|
1134
|
+
var MAX_PAYLOAD_BYTES = 19.5 * 1024 * 1024;
|
|
1135
|
+
var PunkConnection = class {
|
|
1136
|
+
socket;
|
|
1137
|
+
activeSessions = /* @__PURE__ */ new Map();
|
|
1138
|
+
store;
|
|
1139
|
+
sleepLock;
|
|
1140
|
+
heartbeatInterval;
|
|
1141
|
+
refreshInterval;
|
|
1142
|
+
deviceId;
|
|
1143
|
+
defaultCwd;
|
|
1144
|
+
options;
|
|
1145
|
+
constructor(server, idToken, options, store) {
|
|
1146
|
+
this.store = store;
|
|
1147
|
+
this.options = options;
|
|
1148
|
+
this.deviceId = options.deviceId || getOrCreateDeviceId();
|
|
1149
|
+
this.defaultCwd = options.cwd || getDefaultWorkingDirectory();
|
|
1150
|
+
fs3.mkdirSync(this.defaultCwd, { recursive: true });
|
|
1151
|
+
const url = buildUrl(server);
|
|
1152
|
+
store.getState().setReconnecting();
|
|
1153
|
+
store.getState().addActivity({ icon: "\u25CF", message: "Connecting..." });
|
|
1154
|
+
this.socket = io(url, {
|
|
1155
|
+
path: "/socket.io",
|
|
1156
|
+
transports: ["websocket"],
|
|
1157
|
+
auth: (cb) => {
|
|
1158
|
+
refreshIdToken().then((token) => cb({ token })).catch(() => cb({ token: idToken }));
|
|
1159
|
+
},
|
|
1160
|
+
reconnection: true,
|
|
1161
|
+
reconnectionAttempts: Infinity,
|
|
1162
|
+
reconnectionDelay: 1e3,
|
|
1163
|
+
reconnectionDelayMax: 5e3,
|
|
1164
|
+
perMessageDeflate: { threshold: 1024 }
|
|
1165
|
+
});
|
|
1166
|
+
this.sleepLock = preventIdleSleep();
|
|
1167
|
+
this.heartbeatInterval = setInterval(() => {
|
|
1168
|
+
if (this.socket.connected) this.socket.emit("heartbeat");
|
|
1169
|
+
}, 3e4);
|
|
1170
|
+
this.refreshInterval = setInterval(async () => {
|
|
1171
|
+
try {
|
|
1172
|
+
const token = await refreshIdToken();
|
|
1173
|
+
this.socket.emit("re-auth", { token });
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
}, 50 * 60 * 1e3);
|
|
1177
|
+
this.attachSocketHandlers();
|
|
1178
|
+
this.attachSignalHandlers();
|
|
1179
|
+
}
|
|
1180
|
+
disconnect() {
|
|
1181
|
+
this.cleanup();
|
|
1182
|
+
}
|
|
1183
|
+
reconnect() {
|
|
1184
|
+
this.socket.disconnect().connect();
|
|
1185
|
+
this.store.getState().setReconnecting();
|
|
1186
|
+
this.store.getState().addActivity({ icon: "\u27F3", message: "Reconnecting..." });
|
|
1187
|
+
}
|
|
1188
|
+
resolvePermission(requestId, toolUseId, allow, answers, feedback, permissionMode) {
|
|
1189
|
+
const session = this.activeSessions.get(requestId);
|
|
1190
|
+
if (session) {
|
|
1191
|
+
if (permissionMode) {
|
|
1192
|
+
session.setPermissionMode(permissionMode).catch(() => {
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
session.resolvePermission(toolUseId, allow, answers, feedback);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
// --- Private ---
|
|
1199
|
+
attachSocketHandlers() {
|
|
1200
|
+
const { socket, store } = this;
|
|
1201
|
+
socket.on("connect", () => {
|
|
1202
|
+
store.getState().setConnected();
|
|
1203
|
+
const deviceInfo = collectDeviceInfo(this.deviceId, this.options.name, this.options.tag, this.defaultCwd);
|
|
1204
|
+
store.getState().setDeviceInfo(deviceInfo);
|
|
1205
|
+
store.getState().addActivity({ icon: "\u25CF", message: `Connected to ${store.getState().server}` });
|
|
1206
|
+
socket.emit("register", deviceInfo, (response) => {
|
|
1207
|
+
if (response.success) {
|
|
1208
|
+
store.getState().addActivity({ icon: "\u2713", message: `Registered as ${deviceInfo.name}` });
|
|
1209
|
+
} else {
|
|
1210
|
+
setTimeout(() => {
|
|
1211
|
+
if (socket.connected) {
|
|
1212
|
+
socket.emit("register", collectDeviceInfo(this.deviceId, this.options.name, this.options.tag, this.defaultCwd), (r) => {
|
|
1213
|
+
if (r.success) store.getState().addActivity({ icon: "\u2713", message: "Registered (retry)" });
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}, 2e3);
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
});
|
|
1220
|
+
socket.on("disconnect", (reason) => {
|
|
1221
|
+
store.getState().setDisconnected(reason);
|
|
1222
|
+
store.getState().addActivity({ icon: "\u2717", message: `Disconnected: ${reason}` });
|
|
1223
|
+
});
|
|
1224
|
+
socket.io.on("reconnect_attempt", () => {
|
|
1225
|
+
store.getState().setReconnecting();
|
|
1226
|
+
store.getState().addActivity({ icon: "\u27F3", message: "Reconnecting..." });
|
|
1227
|
+
});
|
|
1228
|
+
socket.on("connect_error", (err) => {
|
|
1229
|
+
const { reason } = formatConnectionError(err);
|
|
1230
|
+
store.getState().setError(`Connection error: ${reason}`);
|
|
1231
|
+
store.getState().addActivity({ icon: "\u2717", message: `Connection error: ${reason}` });
|
|
1232
|
+
});
|
|
1233
|
+
socket.on("error", (err) => {
|
|
1234
|
+
store.getState().addActivity({ icon: "\u2717", message: `Socket error: ${err.message}` });
|
|
1235
|
+
});
|
|
1236
|
+
socket.on("prompt", (msg) => {
|
|
1237
|
+
if (msg.type === "prompt") this.handlePrompt(msg);
|
|
1238
|
+
});
|
|
1239
|
+
socket.on("load-session", (msg) => {
|
|
1240
|
+
if (msg.type === "load-session") this.handleLoadSession(msg);
|
|
1241
|
+
});
|
|
1242
|
+
socket.on("list-sessions", (msg) => {
|
|
1243
|
+
if (msg.type === "list-sessions") this.handleListSessions(msg);
|
|
1244
|
+
});
|
|
1245
|
+
socket.on("get-context", (msg) => {
|
|
1246
|
+
if (msg.type === "get-context") this.handleGetContext(msg);
|
|
1247
|
+
});
|
|
1248
|
+
socket.on("get-commands", (msg) => {
|
|
1249
|
+
if (msg.type === "get-commands") this.handleGetCommands(msg);
|
|
1250
|
+
});
|
|
1251
|
+
socket.on("find-project", (msg) => {
|
|
1252
|
+
if (msg.type === "find-project") this.handleFindProject(msg);
|
|
1253
|
+
});
|
|
1254
|
+
socket.on("suggest-project-location", (msg) => {
|
|
1255
|
+
if (msg.type === "suggest-project-location") this.handleSuggestProjectLocation(msg);
|
|
1256
|
+
});
|
|
1257
|
+
socket.on("create-project", (msg) => {
|
|
1258
|
+
if (msg.type === "create-project") this.handleCreateProject(msg);
|
|
1259
|
+
});
|
|
1260
|
+
socket.on("check-path", (msg) => {
|
|
1261
|
+
if (msg.type === "check-path") this.handleCheckPath(msg);
|
|
1262
|
+
});
|
|
1263
|
+
socket.on("cancel", (msg) => {
|
|
1264
|
+
this.handleCancel(msg.id);
|
|
1265
|
+
});
|
|
1266
|
+
socket.on("permission-response", (msg) => {
|
|
1267
|
+
const icon = msg.allow ? "\u2713" : "\u2717";
|
|
1268
|
+
this.store.getState().addActivity({
|
|
1269
|
+
icon,
|
|
1270
|
+
message: msg.allow ? "Permission accepted" : "Permission denied",
|
|
1271
|
+
sessionId: msg.requestId
|
|
1272
|
+
});
|
|
1273
|
+
this.resolvePermission(msg.requestId, msg.toolUseId, msg.allow, msg.answers, msg.feedback, msg.permissionMode);
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
attachSignalHandlers() {
|
|
1277
|
+
const onShutdown = () => {
|
|
1278
|
+
this.store.getState().addActivity({ icon: "\u25CF", message: "Shutting down..." });
|
|
1279
|
+
this.cleanup();
|
|
1280
|
+
process.exit(0);
|
|
1281
|
+
};
|
|
1282
|
+
process.on("SIGINT", onShutdown);
|
|
1283
|
+
process.on("SIGTERM", onShutdown);
|
|
1284
|
+
process.on("SIGCONT", () => {
|
|
1285
|
+
this.reconnect();
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
send(event, msg) {
|
|
1289
|
+
if (!this.socket.connected) return;
|
|
1290
|
+
this.socket.emit(event, msg);
|
|
1291
|
+
}
|
|
1292
|
+
handlePrompt(msg) {
|
|
1293
|
+
const { id, prompt, sessionId, workingDirectory, images, options } = msg;
|
|
1294
|
+
const { store } = this;
|
|
1295
|
+
const projectName = workingDirectory ? path3.basename(workingDirectory) : id.slice(0, 8);
|
|
1296
|
+
store.getState().registerSession(id, projectName);
|
|
1297
|
+
store.getState().addSession(id, {
|
|
1298
|
+
requestId: id,
|
|
1299
|
+
prompt,
|
|
1300
|
+
workingDirectory,
|
|
1301
|
+
status: "running",
|
|
1302
|
+
startedAt: Date.now(),
|
|
1303
|
+
model: options?.model,
|
|
1304
|
+
effort: options?.effort
|
|
1305
|
+
});
|
|
1306
|
+
store.getState().addActivity({
|
|
1307
|
+
icon: "\u25B6",
|
|
1308
|
+
message: "Session started",
|
|
1309
|
+
sessionId: id,
|
|
1310
|
+
detail: workingDirectory ? path3.basename(workingDirectory) : void 0,
|
|
1311
|
+
data: { prompt: prompt.slice(0, 500), workingDirectory, model: options?.model, effort: options?.effort }
|
|
1312
|
+
});
|
|
1313
|
+
const handle = runClaude(
|
|
1314
|
+
{ prompt, sessionId, workingDirectory, images, options },
|
|
1315
|
+
{
|
|
1316
|
+
onSessionCreated: (info) => {
|
|
1317
|
+
this.send("response", { type: "session_created", data: info, requestId: id });
|
|
1318
|
+
},
|
|
1319
|
+
onText: (text) => {
|
|
1320
|
+
this.send("response", { type: "text", text, requestId: id });
|
|
1321
|
+
},
|
|
1322
|
+
onSlashCommandOutput: (output) => {
|
|
1323
|
+
this.send("response", { type: "command_output", output, requestId: id });
|
|
1324
|
+
},
|
|
1325
|
+
onThinking: (thinking) => {
|
|
1326
|
+
this.send("response", { type: "thinking", thinking, requestId: id });
|
|
1327
|
+
},
|
|
1328
|
+
onToolUse: (toolId, name, input, parentToolUseId) => {
|
|
1329
|
+
this.send("response", { type: "tool_use", id: toolId, name, input, requestId: id, parent_tool_use_id: parentToolUseId ?? null });
|
|
1330
|
+
const inputStr = typeof input === "string" ? input : JSON.stringify(input);
|
|
1331
|
+
store.getState().addActivity({
|
|
1332
|
+
icon: "\u25B6",
|
|
1333
|
+
message: `Tool: ${name}`,
|
|
1334
|
+
sessionId: id,
|
|
1335
|
+
data: { toolName: name, input: inputStr?.slice(0, 500) }
|
|
1336
|
+
});
|
|
1337
|
+
},
|
|
1338
|
+
onToolResult: (toolUseId, content, isError) => {
|
|
1339
|
+
this.send("response", { type: "tool_result", tool_use_id: toolUseId, content, is_error: isError, requestId: id });
|
|
1340
|
+
},
|
|
1341
|
+
onResult: (sid, result) => {
|
|
1342
|
+
this.send("response", { type: "result", session_id: sid, ...result && { result }, requestId: id });
|
|
1343
|
+
this.activeSessions.delete(id);
|
|
1344
|
+
store.getState().removeSession(id);
|
|
1345
|
+
store.getState().addActivity({ icon: "\u2713", message: "Session done", sessionId: id });
|
|
1346
|
+
},
|
|
1347
|
+
onError: (message) => {
|
|
1348
|
+
this.send("response", { type: "error", message, requestId: id });
|
|
1349
|
+
this.activeSessions.delete(id);
|
|
1350
|
+
store.getState().updateSession(id, { status: "error" });
|
|
1351
|
+
store.getState().addActivity({ icon: "\u2717", message: `Error: ${message}`, sessionId: id, data: { error: message } });
|
|
1352
|
+
},
|
|
1353
|
+
onPermissionRequest: (req) => {
|
|
1354
|
+
store.getState().addActivity({
|
|
1355
|
+
icon: "\u25CF",
|
|
1356
|
+
message: `Permission: ${req.toolName}`,
|
|
1357
|
+
sessionId: id,
|
|
1358
|
+
data: { toolName: req.toolName, input: JSON.stringify(req.input).slice(0, 500), reason: req.reason, blockedPath: req.blockedPath }
|
|
1359
|
+
});
|
|
1360
|
+
this.socket.emit("permission-request", {
|
|
1361
|
+
requestId: id,
|
|
1362
|
+
toolUseId: req.toolUseId,
|
|
1363
|
+
toolName: req.toolName,
|
|
1364
|
+
input: req.input,
|
|
1365
|
+
reason: req.reason,
|
|
1366
|
+
blockedPath: req.blockedPath
|
|
1367
|
+
});
|
|
1368
|
+
},
|
|
1369
|
+
onTaskStarted: (taskId, description, toolUseId) => {
|
|
1370
|
+
this.send("response", { type: "task_started", taskId, description, toolUseId, requestId: id });
|
|
1371
|
+
},
|
|
1372
|
+
onTaskNotification: (taskId, status, summary, toolUseId) => {
|
|
1373
|
+
this.send("response", { type: "task_notification", taskId, status, summary, toolUseId, requestId: id });
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
);
|
|
1377
|
+
this.activeSessions.set(id, handle);
|
|
1378
|
+
}
|
|
1379
|
+
handleCancel(id) {
|
|
1380
|
+
const session = this.activeSessions.get(id);
|
|
1381
|
+
if (session) {
|
|
1382
|
+
session.abort();
|
|
1383
|
+
this.activeSessions.delete(id);
|
|
1384
|
+
this.store.getState().removeSession(id);
|
|
1385
|
+
this.store.getState().addActivity({ icon: "\u2717", message: "Session cancelled", sessionId: id });
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
async handleListSessions(msg) {
|
|
1389
|
+
const workingDirectory = msg.workingDirectory ?? this.defaultCwd;
|
|
1390
|
+
const sessions = await listSessions(workingDirectory);
|
|
1391
|
+
this.send("response", { type: "sessions_list", sessions, requestId: msg.id });
|
|
1392
|
+
}
|
|
1393
|
+
async handleLoadSession(msg) {
|
|
1394
|
+
const { id, sessionId, limit = DEFAULT_HISTORY_LIMIT } = msg;
|
|
1395
|
+
const all = await loadSession(sessionId);
|
|
1396
|
+
if (all) {
|
|
1397
|
+
const sliced = limit > 0 && all.length > limit ? all.slice(-limit) : all;
|
|
1398
|
+
const messages = fitToPayloadLimit(sliced);
|
|
1399
|
+
this.send("response", { type: "history", messages, total: all.length, requestId: id });
|
|
1400
|
+
} else {
|
|
1401
|
+
this.send("response", { type: "session_not_found", session_id: sessionId, requestId: id });
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async handleGetCommands(msg) {
|
|
1405
|
+
try {
|
|
1406
|
+
const commands = await getProjectCommands(msg.workingDirectory);
|
|
1407
|
+
this.send("response", { type: "commands", commands, requestId: msg.id });
|
|
1408
|
+
} catch (err) {
|
|
1409
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1410
|
+
this.send("response", { type: "error", message, requestId: msg.id });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
async handleFindProject(msg) {
|
|
1414
|
+
const { id, description, rootDirectory, rejections } = msg;
|
|
1415
|
+
const searchRoot = rootDirectory ?? os3.homedir();
|
|
1416
|
+
const ac = new AbortController();
|
|
1417
|
+
const handle = { abort: () => ac.abort(), resolvePermission: () => {
|
|
1418
|
+
}, setPermissionMode: async () => {
|
|
1419
|
+
} };
|
|
1420
|
+
this.activeSessions.set(id, handle);
|
|
1421
|
+
try {
|
|
1422
|
+
const suggestions = await findProjectDirectory(description, searchRoot, rejections, ac.signal);
|
|
1423
|
+
this.send("response", { type: "project_suggestions", suggestions, requestId: id });
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
if (!ac.signal.aborted) {
|
|
1426
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1427
|
+
this.send("response", { type: "error", message, requestId: id });
|
|
1428
|
+
}
|
|
1429
|
+
} finally {
|
|
1430
|
+
this.activeSessions.delete(id);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
async handleSuggestProjectLocation(msg) {
|
|
1434
|
+
const { id, description, name, rootDirectory } = msg;
|
|
1435
|
+
const searchRoot = rootDirectory ?? os3.homedir();
|
|
1436
|
+
const ac = new AbortController();
|
|
1437
|
+
const handle = { abort: () => ac.abort(), resolvePermission: () => {
|
|
1438
|
+
}, setPermissionMode: async () => {
|
|
1439
|
+
} };
|
|
1440
|
+
this.activeSessions.set(id, handle);
|
|
1441
|
+
try {
|
|
1442
|
+
const suggestions = await suggestProjectLocation(description, searchRoot, name, ac.signal);
|
|
1443
|
+
this.send("response", { type: "project_suggestions", suggestions, requestId: id });
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
if (!ac.signal.aborted) {
|
|
1446
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1447
|
+
this.send("response", { type: "error", message, requestId: id });
|
|
1448
|
+
}
|
|
1449
|
+
} finally {
|
|
1450
|
+
this.activeSessions.delete(id);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
handleCreateProject(msg) {
|
|
1454
|
+
const { id, path: projectPath } = msg;
|
|
1455
|
+
try {
|
|
1456
|
+
fs3.mkdirSync(projectPath, { recursive: true });
|
|
1457
|
+
this.send("response", { type: "project_created", path: projectPath, requestId: id });
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1460
|
+
this.send("response", { type: "error", message, requestId: id });
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
handleCheckPath(msg) {
|
|
1464
|
+
const { id, path: checkTarget } = msg;
|
|
1465
|
+
let exists = false;
|
|
1466
|
+
let isDirectory = false;
|
|
1467
|
+
let parentExists = false;
|
|
1468
|
+
try {
|
|
1469
|
+
const stat = fs3.statSync(checkTarget);
|
|
1470
|
+
exists = true;
|
|
1471
|
+
isDirectory = stat.isDirectory();
|
|
1472
|
+
} catch {
|
|
1473
|
+
}
|
|
1474
|
+
try {
|
|
1475
|
+
const parentStat = fs3.statSync(path3.dirname(checkTarget));
|
|
1476
|
+
parentExists = parentStat.isDirectory();
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1479
|
+
this.send("response", { type: "path_check", exists, isDirectory, parentExists, requestId: id });
|
|
1480
|
+
}
|
|
1481
|
+
async handleGetContext(msg) {
|
|
1482
|
+
const { id, sessionId } = msg;
|
|
1483
|
+
const workingDirectory = msg.workingDirectory ?? this.defaultCwd;
|
|
1484
|
+
try {
|
|
1485
|
+
const data = await getContext(sessionId, workingDirectory);
|
|
1486
|
+
this.send("response", { type: "context", data, requestId: id });
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1489
|
+
if (message.includes("not found") || message.includes("No such session")) {
|
|
1490
|
+
this.send("response", { type: "session_not_found", session_id: sessionId, requestId: id });
|
|
1491
|
+
} else {
|
|
1492
|
+
this.send("response", { type: "error", message, requestId: id });
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
cleanup() {
|
|
1497
|
+
clearInterval(this.refreshInterval);
|
|
1498
|
+
clearInterval(this.heartbeatInterval);
|
|
1499
|
+
this.sleepLock.release();
|
|
1500
|
+
for (const session of this.activeSessions.values()) {
|
|
1501
|
+
session.abort();
|
|
1502
|
+
}
|
|
1503
|
+
this.socket.disconnect();
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
function buildUrl(server) {
|
|
1507
|
+
const url = new URL(server);
|
|
1508
|
+
if (!url.pathname.endsWith("/device")) {
|
|
1509
|
+
url.pathname = url.pathname.replace(/\/$/, "") + "/device";
|
|
1510
|
+
}
|
|
1511
|
+
return url.origin + url.pathname;
|
|
1512
|
+
}
|
|
1513
|
+
function formatConnectionError(err) {
|
|
1514
|
+
const errRecord = err;
|
|
1515
|
+
const description = errRecord.description;
|
|
1516
|
+
const message = description?.message ?? description?.error?.message ?? err.message;
|
|
1517
|
+
const result = { message };
|
|
1518
|
+
let reason = "unknown";
|
|
1519
|
+
if (errRecord.type === "TransportError" && description) {
|
|
1520
|
+
const target = description.target;
|
|
1521
|
+
const req = target?._req;
|
|
1522
|
+
const res = req?.res;
|
|
1523
|
+
const statusCode = res?.statusCode;
|
|
1524
|
+
if (statusCode) {
|
|
1525
|
+
result.statusCode = statusCode;
|
|
1526
|
+
if (statusCode === 401 || statusCode === 403) reason = "authentication failed";
|
|
1527
|
+
else if (statusCode >= 500) reason = "server unavailable";
|
|
1528
|
+
else reason = `server responded ${statusCode}`;
|
|
1529
|
+
} else if (/ENOTFOUND|ECONNREFUSED|EAI_AGAIN/.test(message)) {
|
|
1530
|
+
reason = "server unreachable";
|
|
1531
|
+
} else {
|
|
1532
|
+
reason = "transport error";
|
|
1533
|
+
}
|
|
1534
|
+
} else if (err.message === "timeout") {
|
|
1535
|
+
reason = "timed out";
|
|
1536
|
+
} else if (errRecord.data) {
|
|
1537
|
+
result.data = errRecord.data;
|
|
1538
|
+
reason = "rejected by server";
|
|
1539
|
+
} else if (err.message.includes("v2.x")) {
|
|
1540
|
+
reason = "server version mismatch";
|
|
1541
|
+
} else {
|
|
1542
|
+
reason = "failed";
|
|
1543
|
+
}
|
|
1544
|
+
return { reason, ...result };
|
|
1545
|
+
}
|
|
1546
|
+
function fitToPayloadLimit(messages) {
|
|
1547
|
+
if (Buffer.byteLength(JSON.stringify(messages), "utf8") <= MAX_PAYLOAD_BYTES) {
|
|
1548
|
+
return messages;
|
|
1549
|
+
}
|
|
1550
|
+
let lo = 0;
|
|
1551
|
+
let hi = messages.length;
|
|
1552
|
+
while (lo < hi) {
|
|
1553
|
+
const mid = Math.floor((lo + hi + 1) / 2);
|
|
1554
|
+
const slice = messages.slice(messages.length - mid);
|
|
1555
|
+
if (Buffer.byteLength(JSON.stringify(slice), "utf8") <= MAX_PAYLOAD_BYTES) {
|
|
1556
|
+
lo = mid;
|
|
1557
|
+
} else {
|
|
1558
|
+
hi = mid - 1;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return messages.slice(messages.length - lo);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// src/ui/store.ts
|
|
1565
|
+
import { createStore, useStore } from "zustand";
|
|
1566
|
+
var MAX_ACTIVITY_ENTRIES = 500;
|
|
1567
|
+
var SESSION_COLORS = ["magenta", "blue", "magentaBright", "blueBright", "white", "gray"];
|
|
1568
|
+
var createPunkStore = (server) => createStore((set) => ({
|
|
1569
|
+
// Initial state
|
|
1570
|
+
screen: "login",
|
|
1571
|
+
auth: null,
|
|
1572
|
+
connection: "disconnected",
|
|
1573
|
+
connectionError: void 0,
|
|
1574
|
+
deviceInfo: null,
|
|
1575
|
+
server,
|
|
1576
|
+
connectedAt: null,
|
|
1577
|
+
sessions: {},
|
|
1578
|
+
activityLog: [],
|
|
1579
|
+
sessionProjects: {},
|
|
1580
|
+
// Actions
|
|
1581
|
+
setScreen: (screen) => set({ screen }),
|
|
1582
|
+
setAuth: (auth) => set({ auth }),
|
|
1583
|
+
setConnected: () => set({ connection: "connected", connectedAt: Date.now(), connectionError: void 0 }),
|
|
1584
|
+
setDisconnected: (reason) => set({ connection: "disconnected", connectionError: reason, connectedAt: null }),
|
|
1585
|
+
setReconnecting: () => set({ connection: "connecting" }),
|
|
1586
|
+
setError: (message) => set({ connection: "error", connectionError: message }),
|
|
1587
|
+
setDeviceInfo: (info) => set({ deviceInfo: info }),
|
|
1588
|
+
addSession: (id, session) => set((s) => ({ sessions: { ...s.sessions, [id]: session } })),
|
|
1589
|
+
updateSession: (id, updates) => set((s) => {
|
|
1590
|
+
const existing = s.sessions[id];
|
|
1591
|
+
if (!existing) return s;
|
|
1592
|
+
return { sessions: { ...s.sessions, [id]: { ...existing, ...updates } } };
|
|
1593
|
+
}),
|
|
1594
|
+
removeSession: (id) => set((s) => {
|
|
1595
|
+
const rest = { ...s.sessions };
|
|
1596
|
+
delete rest[id];
|
|
1597
|
+
return { sessions: rest };
|
|
1598
|
+
}),
|
|
1599
|
+
addActivity: (entry) => set((s) => ({
|
|
1600
|
+
activityLog: [
|
|
1601
|
+
...s.activityLog.slice(-(MAX_ACTIVITY_ENTRIES - 1)),
|
|
1602
|
+
{ ...entry, timestamp: Date.now() }
|
|
1603
|
+
]
|
|
1604
|
+
})),
|
|
1605
|
+
clearActivity: () => set({ activityLog: [] }),
|
|
1606
|
+
registerSession: (id, project) => set((s) => {
|
|
1607
|
+
if (s.sessionProjects[id]) return s;
|
|
1608
|
+
const colorIdx = Object.keys(s.sessionProjects).length % SESSION_COLORS.length;
|
|
1609
|
+
return {
|
|
1610
|
+
sessionProjects: {
|
|
1611
|
+
...s.sessionProjects,
|
|
1612
|
+
[id]: { project, color: SESSION_COLORS[colorIdx] }
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
})
|
|
1616
|
+
}));
|
|
1617
|
+
function usePunkStore(store, selector) {
|
|
1618
|
+
return useStore(store, selector);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// src/lib/qr-login.ts
|
|
1622
|
+
import qrcode from "qrcode-terminal";
|
|
1623
|
+
var DEFAULT_BACKEND_URL = "https://api.punkcode.dev";
|
|
1624
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
1625
|
+
var MAX_POLL_ATTEMPTS = 100;
|
|
1626
|
+
function resolveBackendUrl(backendUrl) {
|
|
1627
|
+
return backendUrl ?? process.env.BACKEND_URL ?? DEFAULT_BACKEND_URL;
|
|
1628
|
+
}
|
|
1629
|
+
async function initQrSession(backendUrl) {
|
|
1630
|
+
const baseUrl = resolveBackendUrl(backendUrl);
|
|
1631
|
+
const res = await fetch(`${baseUrl}/auth/qr/init`, {
|
|
1632
|
+
method: "POST",
|
|
1633
|
+
headers: { "Content-Type": "application/json" }
|
|
1634
|
+
});
|
|
1635
|
+
if (!res.ok) {
|
|
1636
|
+
const body = await res.json().catch(() => ({}));
|
|
1637
|
+
throw new Error(body?.message ?? `Failed to init QR login: HTTP ${res.status}`);
|
|
1638
|
+
}
|
|
1639
|
+
const { pairingCode, secret, qrPayload } = await res.json();
|
|
1640
|
+
let qrString = "";
|
|
1641
|
+
qrcode.generate(qrPayload, { small: true }, (code) => {
|
|
1642
|
+
qrString = code;
|
|
1643
|
+
});
|
|
1644
|
+
async function* poll(signal) {
|
|
1645
|
+
let attempts = 0;
|
|
1646
|
+
let backoffMs = POLL_INTERVAL_MS;
|
|
1647
|
+
while (attempts < MAX_POLL_ATTEMPTS) {
|
|
1648
|
+
await sleep(backoffMs);
|
|
1649
|
+
if (signal?.aborted) return;
|
|
1650
|
+
try {
|
|
1651
|
+
const pollRes = await fetch(
|
|
1652
|
+
`${baseUrl}/auth/qr/poll?pairingCode=${pairingCode}&secret=${secret}`,
|
|
1653
|
+
{ signal }
|
|
1654
|
+
);
|
|
1655
|
+
if (!pollRes.ok) {
|
|
1656
|
+
if (pollRes.status === 429) {
|
|
1657
|
+
backoffMs = Math.min(backoffMs * 2, 3e4);
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
throw new Error(`Poll failed: HTTP ${pollRes.status}`);
|
|
1661
|
+
}
|
|
1662
|
+
const result = await pollRes.json();
|
|
1663
|
+
backoffMs = POLL_INTERVAL_MS;
|
|
1664
|
+
attempts++;
|
|
1665
|
+
yield result;
|
|
1666
|
+
if (result.status !== "pending") return;
|
|
1667
|
+
} catch {
|
|
1668
|
+
if (signal?.aborted) return;
|
|
1669
|
+
backoffMs = Math.min(backoffMs * 2, 3e4);
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
yield { status: "expired" };
|
|
1674
|
+
}
|
|
1675
|
+
return { pairingCode, qrPayload, qrString, poll };
|
|
1676
|
+
}
|
|
1677
|
+
var QR_COLORS = [
|
|
1678
|
+
"\x1B[36m",
|
|
1679
|
+
// cyan
|
|
1680
|
+
"\x1B[35m",
|
|
1681
|
+
// magenta
|
|
1682
|
+
"\x1B[34m",
|
|
1683
|
+
// blue
|
|
1684
|
+
"\x1B[32m",
|
|
1685
|
+
// green
|
|
1686
|
+
"\x1B[33m",
|
|
1687
|
+
// yellow
|
|
1688
|
+
"\x1B[91m",
|
|
1689
|
+
// bright red
|
|
1690
|
+
"\x1B[96m",
|
|
1691
|
+
// bright cyan
|
|
1692
|
+
"\x1B[95m"
|
|
1693
|
+
// bright magenta
|
|
1694
|
+
];
|
|
1695
|
+
var RESET = "\x1B[0m";
|
|
1696
|
+
async function loginWithQr(backendUrl) {
|
|
1697
|
+
const session = await initQrSession(backendUrl);
|
|
1698
|
+
const qrLines = session.qrString.split("\n").filter(Boolean);
|
|
1699
|
+
console.log();
|
|
1700
|
+
console.log(" Scan this QR code with the Punk app:");
|
|
1701
|
+
console.log();
|
|
1702
|
+
let colorIdx = 0;
|
|
1703
|
+
for (const line of qrLines) {
|
|
1704
|
+
console.log(` ${QR_COLORS[0]}${line}${RESET}`);
|
|
1705
|
+
}
|
|
1706
|
+
console.log();
|
|
1707
|
+
const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1708
|
+
let spinIdx = 0;
|
|
1709
|
+
const recolorQr = () => {
|
|
1710
|
+
colorIdx = (colorIdx + 1) % QR_COLORS.length;
|
|
1711
|
+
const color = QR_COLORS[colorIdx];
|
|
1712
|
+
const moveUp = qrLines.length + 1;
|
|
1713
|
+
process.stdout.write(`\x1B[${moveUp}A`);
|
|
1714
|
+
for (const line of qrLines) {
|
|
1715
|
+
process.stdout.write(`\r ${color}${line}${RESET}
|
|
1716
|
+
`);
|
|
1717
|
+
}
|
|
1718
|
+
process.stdout.write("\n");
|
|
1719
|
+
};
|
|
1720
|
+
const colorTimer = setInterval(recolorQr, 800);
|
|
1721
|
+
const abort = new AbortController();
|
|
1722
|
+
const onSigint = () => {
|
|
1723
|
+
abort.abort();
|
|
1724
|
+
clearInterval(colorTimer);
|
|
1725
|
+
process.stdout.write("\r\x1B[K");
|
|
1726
|
+
console.log(" Login cancelled.");
|
|
1727
|
+
process.exit(0);
|
|
1728
|
+
};
|
|
1729
|
+
process.on("SIGINT", onSigint);
|
|
1730
|
+
try {
|
|
1731
|
+
for await (const result of session.poll(abort.signal)) {
|
|
1732
|
+
process.stdout.write(`\r Waiting for confirmation... ${spinner[spinIdx++ % spinner.length]} `);
|
|
1733
|
+
if (result.status === "pending") continue;
|
|
1734
|
+
if (result.status === "expired") {
|
|
1735
|
+
process.stdout.write("\r\x1B[K");
|
|
1736
|
+
throw new Error("QR code expired. Run `punk login` to try again.");
|
|
1737
|
+
}
|
|
1738
|
+
if (result.status === "confirmed") {
|
|
1739
|
+
process.stdout.write("\r\x1B[K");
|
|
1740
|
+
saveAuth({
|
|
1741
|
+
idToken: result.idToken,
|
|
1742
|
+
refreshToken: result.refreshToken,
|
|
1743
|
+
expiresAt: Date.now() + parseInt(result.expiresIn, 10) * 1e3,
|
|
1744
|
+
email: result.email,
|
|
1745
|
+
uid: result.uid
|
|
1746
|
+
});
|
|
1747
|
+
logger.info({ success: true, email: result.email }, "Logged in");
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
process.stdout.write("\r\x1B[K");
|
|
1752
|
+
throw new Error("QR code expired. Run `punk login` to try again.");
|
|
1753
|
+
} finally {
|
|
1754
|
+
clearInterval(colorTimer);
|
|
1755
|
+
process.removeListener("SIGINT", onSigint);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function sleep(ms) {
|
|
1759
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
export {
|
|
1763
|
+
version,
|
|
1764
|
+
loadAuth,
|
|
1765
|
+
saveAuth,
|
|
1766
|
+
clearAuth,
|
|
1767
|
+
signIn,
|
|
1768
|
+
refreshIdToken,
|
|
1769
|
+
logger,
|
|
1770
|
+
initQrSession,
|
|
1771
|
+
loginWithQr,
|
|
1772
|
+
PunkConnection,
|
|
1773
|
+
createPunkStore,
|
|
1774
|
+
usePunkStore
|
|
1775
|
+
};
|