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