@punkcode/cli 0.1.8 → 0.1.10
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 +282 -86
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,24 @@ import { execaSync as execaSync2 } from "execa";
|
|
|
12
12
|
|
|
13
13
|
// src/lib/claude-sdk.ts
|
|
14
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
|
|
15
33
|
async function* promptWithImages(text, images, sessionId) {
|
|
16
34
|
yield {
|
|
17
35
|
type: "user",
|
|
@@ -33,32 +51,100 @@ async function* promptWithImages(text, images, sessionId) {
|
|
|
33
51
|
session_id: sessionId
|
|
34
52
|
};
|
|
35
53
|
}
|
|
36
|
-
async function
|
|
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) {
|
|
37
127
|
const q = query({
|
|
38
|
-
prompt: "",
|
|
128
|
+
prompt: "/load-session-info",
|
|
39
129
|
options: {
|
|
40
|
-
|
|
41
|
-
...
|
|
130
|
+
persistSession: false,
|
|
131
|
+
...workingDirectory && { cwd: workingDirectory }
|
|
42
132
|
}
|
|
43
133
|
});
|
|
44
134
|
try {
|
|
45
|
-
const [
|
|
46
|
-
q.initializationResult(),
|
|
135
|
+
const [commands, skills] = await Promise.all([
|
|
47
136
|
q.supportedCommands(),
|
|
48
|
-
|
|
137
|
+
loadGlobalSkills(workingDirectory)
|
|
49
138
|
]);
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
claudeCodeVersion: "",
|
|
60
|
-
permissionMode: "default"
|
|
61
|
-
};
|
|
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;
|
|
62
148
|
} finally {
|
|
63
149
|
q.close();
|
|
64
150
|
}
|
|
@@ -80,7 +166,7 @@ function runClaude(options, callbacks) {
|
|
|
80
166
|
...opts.disallowedTools && { disallowedTools: opts.disallowedTools },
|
|
81
167
|
...opts.maxTurns && { maxTurns: opts.maxTurns },
|
|
82
168
|
systemPrompt: opts.systemPrompt ?? { type: "preset", preset: "claude_code" },
|
|
83
|
-
...options.
|
|
169
|
+
...options.workingDirectory && { cwd: options.workingDirectory },
|
|
84
170
|
...options.sessionId && { resume: options.sessionId },
|
|
85
171
|
maxThinkingTokens: 1e4,
|
|
86
172
|
includePartialMessages: true,
|
|
@@ -179,25 +265,27 @@ function runClaude(options, callbacks) {
|
|
|
179
265
|
case "system": {
|
|
180
266
|
const sys = message;
|
|
181
267
|
if (sys.subtype === "init" && callbacks.onSessionCreated) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
268
|
+
const initCommands = (sys.slash_commands ?? []).map((cmd) => ({ name: cmd, description: "" }));
|
|
269
|
+
const globalSkills = await loadGlobalSkills(sys.cwd);
|
|
270
|
+
const knownNames = new Set(initCommands.map((c) => c.name));
|
|
271
|
+
for (const skill of globalSkills) {
|
|
272
|
+
if (!knownNames.has(skill.name)) {
|
|
273
|
+
initCommands.push(skill);
|
|
187
274
|
}
|
|
188
|
-
} catch {
|
|
189
275
|
}
|
|
190
|
-
|
|
276
|
+
const sessionInfo = {
|
|
191
277
|
sessionId: sys.session_id ?? "",
|
|
192
278
|
tools: sys.tools ?? [],
|
|
193
|
-
slashCommands,
|
|
279
|
+
slashCommands: initCommands,
|
|
194
280
|
skills: sys.skills ?? [],
|
|
195
281
|
mcpServers: sys.mcp_servers ?? [],
|
|
196
282
|
model: sys.model ?? "",
|
|
197
|
-
|
|
283
|
+
workingDirectory: sys.cwd ?? "",
|
|
198
284
|
claudeCodeVersion: sys.claude_code_version ?? "",
|
|
199
285
|
permissionMode: sys.permissionMode ?? "default"
|
|
200
|
-
}
|
|
286
|
+
};
|
|
287
|
+
logger.info({ sessionId: sessionInfo.sessionId, commands: sessionInfo.slashCommands.length }, "New chat session info");
|
|
288
|
+
callbacks.onSessionCreated(sessionInfo);
|
|
201
289
|
}
|
|
202
290
|
break;
|
|
203
291
|
}
|
|
@@ -232,6 +320,9 @@ function runClaude(options, callbacks) {
|
|
|
232
320
|
};
|
|
233
321
|
}
|
|
234
322
|
|
|
323
|
+
// src/commands/connect.ts
|
|
324
|
+
import fs3 from "fs";
|
|
325
|
+
|
|
235
326
|
// src/lib/device-info.ts
|
|
236
327
|
import os from "os";
|
|
237
328
|
import path from "path";
|
|
@@ -257,7 +348,15 @@ function getOrCreateDeviceId() {
|
|
|
257
348
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
258
349
|
return id;
|
|
259
350
|
}
|
|
260
|
-
function
|
|
351
|
+
function getDefaultWorkingDirectory() {
|
|
352
|
+
try {
|
|
353
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
354
|
+
if (config.defaultWorkingDirectory) return config.defaultWorkingDirectory;
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
return path.join(os.homedir(), "punk");
|
|
358
|
+
}
|
|
359
|
+
function collectDeviceInfo(deviceId, customName, customTags, defaultCwd) {
|
|
261
360
|
if (customName) {
|
|
262
361
|
saveConfigField("deviceName", customName);
|
|
263
362
|
}
|
|
@@ -273,7 +372,7 @@ function collectDeviceInfo(deviceId, customName, customTags) {
|
|
|
273
372
|
arch: process.arch,
|
|
274
373
|
username: os.userInfo().username,
|
|
275
374
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
276
|
-
|
|
375
|
+
defaultWorkingDirectory: defaultCwd || getDefaultWorkingDirectory(),
|
|
277
376
|
model: getModel(),
|
|
278
377
|
cpuModel: cpus.length > 0 ? cpus[0].model : "Unknown",
|
|
279
378
|
memoryGB: Math.round(os.totalmem() / 1024 ** 3),
|
|
@@ -385,10 +484,10 @@ function getModel() {
|
|
|
385
484
|
}
|
|
386
485
|
|
|
387
486
|
// src/lib/session.ts
|
|
388
|
-
import { readdir, readFile, stat, open } from "fs/promises";
|
|
389
|
-
import { join } from "path";
|
|
390
|
-
import { homedir } from "os";
|
|
391
|
-
var CLAUDE_DIR =
|
|
487
|
+
import { readdir as readdir2, readFile as readFile2, stat, open } from "fs/promises";
|
|
488
|
+
import { join as join2 } from "path";
|
|
489
|
+
import { homedir as homedir2 } from "os";
|
|
490
|
+
var CLAUDE_DIR = join2(homedir2(), ".claude", "projects");
|
|
392
491
|
function pathToProjectDir(dir) {
|
|
393
492
|
return dir.replace(/\//g, "-");
|
|
394
493
|
}
|
|
@@ -396,14 +495,14 @@ async function loadSession(sessionId) {
|
|
|
396
495
|
const sessionFile = `${sessionId}.jsonl`;
|
|
397
496
|
let projectDirs;
|
|
398
497
|
try {
|
|
399
|
-
projectDirs = await
|
|
498
|
+
projectDirs = await readdir2(CLAUDE_DIR);
|
|
400
499
|
} catch {
|
|
401
500
|
return null;
|
|
402
501
|
}
|
|
403
502
|
for (const projectDir of projectDirs) {
|
|
404
|
-
const sessionPath =
|
|
503
|
+
const sessionPath = join2(CLAUDE_DIR, projectDir, sessionFile);
|
|
405
504
|
try {
|
|
406
|
-
const content = await
|
|
505
|
+
const content = await readFile2(sessionPath, "utf-8");
|
|
407
506
|
return parseSessionFile(content);
|
|
408
507
|
} catch {
|
|
409
508
|
}
|
|
@@ -420,17 +519,17 @@ async function listSessions(workingDirectory) {
|
|
|
420
519
|
if (workingDirectory) {
|
|
421
520
|
projectDirs = [pathToProjectDir(workingDirectory)];
|
|
422
521
|
} else {
|
|
423
|
-
projectDirs = await
|
|
522
|
+
projectDirs = await readdir2(CLAUDE_DIR);
|
|
424
523
|
}
|
|
425
524
|
} catch {
|
|
426
525
|
return [];
|
|
427
526
|
}
|
|
428
527
|
const candidates = [];
|
|
429
528
|
for (const projectDir of projectDirs) {
|
|
430
|
-
const projectPath =
|
|
529
|
+
const projectPath = join2(CLAUDE_DIR, projectDir);
|
|
431
530
|
let files;
|
|
432
531
|
try {
|
|
433
|
-
files = await
|
|
532
|
+
files = await readdir2(projectPath);
|
|
434
533
|
} catch {
|
|
435
534
|
continue;
|
|
436
535
|
}
|
|
@@ -440,8 +539,10 @@ async function listSessions(workingDirectory) {
|
|
|
440
539
|
if (!isValidSessionUUID(sessionId)) continue;
|
|
441
540
|
candidates.push({
|
|
442
541
|
sessionId,
|
|
443
|
-
|
|
444
|
-
|
|
542
|
+
// When a workingDirectory is provided, return the original path so it
|
|
543
|
+
// matches the device's defaultWorkingDirectory on the mobile side.
|
|
544
|
+
project: workingDirectory ?? projectDir,
|
|
545
|
+
filePath: join2(projectPath, file)
|
|
445
546
|
});
|
|
446
547
|
}
|
|
447
548
|
}
|
|
@@ -695,23 +796,8 @@ function parseSessionFile(content) {
|
|
|
695
796
|
|
|
696
797
|
// src/lib/context.ts
|
|
697
798
|
import { execa } from "execa";
|
|
698
|
-
|
|
699
|
-
// src/utils/logger.ts
|
|
700
|
-
import pino from "pino";
|
|
701
|
-
var level = process.env.LOG_LEVEL ?? "info";
|
|
702
|
-
var format = process.env.PUNK_LOG_FORMAT ?? (process.stdout.isTTY ? "pretty" : "json");
|
|
703
|
-
var transport = format === "pretty" ? pino.transport({
|
|
704
|
-
target: "pino-pretty",
|
|
705
|
-
options: { colorize: true }
|
|
706
|
-
}) : void 0;
|
|
707
|
-
var logger = pino({ level }, transport);
|
|
708
|
-
function createChildLogger(bindings) {
|
|
709
|
-
return logger.child(bindings);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// src/lib/context.ts
|
|
713
799
|
var log = createChildLogger({ component: "context" });
|
|
714
|
-
async function getContext(sessionId,
|
|
800
|
+
async function getContext(sessionId, workingDirectory) {
|
|
715
801
|
let stdout;
|
|
716
802
|
try {
|
|
717
803
|
const result = await execa("claude", [
|
|
@@ -723,7 +809,7 @@ async function getContext(sessionId, cwd) {
|
|
|
723
809
|
sessionId,
|
|
724
810
|
"/context"
|
|
725
811
|
], {
|
|
726
|
-
cwd:
|
|
812
|
+
cwd: workingDirectory || process.cwd(),
|
|
727
813
|
timeout: 3e4,
|
|
728
814
|
stdin: "ignore"
|
|
729
815
|
});
|
|
@@ -902,6 +988,54 @@ async function refreshIdToken() {
|
|
|
902
988
|
return updated.idToken;
|
|
903
989
|
}
|
|
904
990
|
|
|
991
|
+
// src/lib/sleep-inhibitor.ts
|
|
992
|
+
import { spawn } from "child_process";
|
|
993
|
+
function preventIdleSleep() {
|
|
994
|
+
const platform = process.platform;
|
|
995
|
+
let child = null;
|
|
996
|
+
if (platform === "darwin") {
|
|
997
|
+
child = spawn("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
998
|
+
stdio: "ignore",
|
|
999
|
+
detached: false
|
|
1000
|
+
});
|
|
1001
|
+
} else if (platform === "linux") {
|
|
1002
|
+
child = spawn(
|
|
1003
|
+
"systemd-inhibit",
|
|
1004
|
+
[
|
|
1005
|
+
"--what=idle",
|
|
1006
|
+
"--who=punk-connect",
|
|
1007
|
+
"--why=Device connected for remote access",
|
|
1008
|
+
"cat"
|
|
1009
|
+
],
|
|
1010
|
+
{ stdio: ["pipe", "ignore", "ignore"], detached: false }
|
|
1011
|
+
);
|
|
1012
|
+
} else if (platform === "win32") {
|
|
1013
|
+
const script = `
|
|
1014
|
+
$sig = '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags);';
|
|
1015
|
+
$t = Add-Type -MemberDefinition $sig -Name WinAPI -Namespace Punk -PassThru;
|
|
1016
|
+
while($true) {
|
|
1017
|
+
$t::SetThreadExecutionState(0x80000001) | Out-Null;
|
|
1018
|
+
try { $null = Read-Host } catch { break };
|
|
1019
|
+
Start-Sleep -Seconds 30;
|
|
1020
|
+
}`.trim();
|
|
1021
|
+
child = spawn("powershell", ["-NoProfile", "-Command", script], {
|
|
1022
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
1023
|
+
detached: false
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
child?.unref();
|
|
1027
|
+
child?.on("error", () => {
|
|
1028
|
+
});
|
|
1029
|
+
return {
|
|
1030
|
+
release: () => {
|
|
1031
|
+
if (child && !child.killed) {
|
|
1032
|
+
child.kill();
|
|
1033
|
+
child = null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
905
1039
|
// src/commands/connect.ts
|
|
906
1040
|
async function connect(server, options) {
|
|
907
1041
|
logger.info("Checking prerequisites...");
|
|
@@ -914,6 +1048,8 @@ async function connect(server, options) {
|
|
|
914
1048
|
}
|
|
915
1049
|
logger.info("All checks passed");
|
|
916
1050
|
const deviceId = options.deviceId || getOrCreateDeviceId();
|
|
1051
|
+
const defaultCwd = options.cwd || getDefaultWorkingDirectory();
|
|
1052
|
+
fs3.mkdirSync(defaultCwd, { recursive: true });
|
|
917
1053
|
const url = buildUrl(server);
|
|
918
1054
|
let idToken;
|
|
919
1055
|
if (options.token) {
|
|
@@ -937,9 +1073,16 @@ async function connect(server, options) {
|
|
|
937
1073
|
reconnectionDelayMax: 5e3
|
|
938
1074
|
});
|
|
939
1075
|
const activeSessions = /* @__PURE__ */ new Map();
|
|
1076
|
+
const sleepLock = preventIdleSleep();
|
|
1077
|
+
logger.info("Sleep inhibitor active");
|
|
1078
|
+
const heartbeatInterval = setInterval(() => {
|
|
1079
|
+
if (socket.connected) {
|
|
1080
|
+
socket.emit("heartbeat");
|
|
1081
|
+
}
|
|
1082
|
+
}, 3e4);
|
|
940
1083
|
socket.on("connect", () => {
|
|
941
1084
|
logger.info("Connected");
|
|
942
|
-
const deviceInfo = collectDeviceInfo(deviceId, options.name, options.tag);
|
|
1085
|
+
const deviceInfo = collectDeviceInfo(deviceId, options.name, options.tag, defaultCwd);
|
|
943
1086
|
socket.emit("register", deviceInfo, (response) => {
|
|
944
1087
|
if (response.success) {
|
|
945
1088
|
logger.info({ deviceId }, "Registered");
|
|
@@ -958,23 +1101,25 @@ async function connect(server, options) {
|
|
|
958
1101
|
});
|
|
959
1102
|
socket.on("list-sessions", async (msg) => {
|
|
960
1103
|
if (msg.type === "list-sessions") {
|
|
961
|
-
handleListSessions(socket, msg);
|
|
1104
|
+
handleListSessions(socket, msg, defaultCwd);
|
|
962
1105
|
}
|
|
963
1106
|
});
|
|
964
1107
|
socket.on("get-context", (msg) => {
|
|
965
1108
|
if (msg.type === "get-context") {
|
|
966
|
-
handleGetContext(socket, msg);
|
|
1109
|
+
handleGetContext(socket, msg, defaultCwd);
|
|
967
1110
|
}
|
|
968
1111
|
});
|
|
969
|
-
socket.on("get-
|
|
970
|
-
if (msg.type === "get-
|
|
971
|
-
|
|
1112
|
+
socket.on("get-commands", (msg) => {
|
|
1113
|
+
if (msg.type === "get-commands") {
|
|
1114
|
+
handleGetCommands(socket, msg);
|
|
972
1115
|
}
|
|
973
1116
|
});
|
|
974
1117
|
socket.on("cancel", (msg) => {
|
|
975
1118
|
handleCancel(msg.id, activeSessions);
|
|
976
1119
|
});
|
|
977
1120
|
socket.on("permission-response", (msg) => {
|
|
1121
|
+
const log2 = createChildLogger({ sessionId: msg.requestId });
|
|
1122
|
+
log2.info({ toolUseId: msg.toolUseId, allowed: msg.allow }, msg.allow ? "Permission accepted" : "Permission denied");
|
|
978
1123
|
const session = activeSessions.get(msg.requestId);
|
|
979
1124
|
if (session) {
|
|
980
1125
|
session.resolvePermission(msg.toolUseId, msg.allow, msg.answers, msg.feedback);
|
|
@@ -992,10 +1137,12 @@ async function connect(server, options) {
|
|
|
992
1137
|
});
|
|
993
1138
|
socket.on("reconnect", (attemptNumber) => {
|
|
994
1139
|
logger.info({ attemptNumber }, "Reconnected");
|
|
995
|
-
socket.emit("register", collectDeviceInfo(deviceId, options.name, options.tag));
|
|
1140
|
+
socket.emit("register", collectDeviceInfo(deviceId, options.name, options.tag, defaultCwd));
|
|
996
1141
|
});
|
|
997
1142
|
socket.on("connect_error", (err) => {
|
|
998
|
-
|
|
1143
|
+
const { reason, ...detail } = formatConnectionError(err);
|
|
1144
|
+
logger.error(detail, `Connection error: ${reason}`);
|
|
1145
|
+
logger.debug({ err }, "Connection error (raw)");
|
|
999
1146
|
});
|
|
1000
1147
|
const refreshInterval = setInterval(async () => {
|
|
1001
1148
|
try {
|
|
@@ -1006,6 +1153,8 @@ async function connect(server, options) {
|
|
|
1006
1153
|
}, 50 * 60 * 1e3);
|
|
1007
1154
|
const cleanup = () => {
|
|
1008
1155
|
clearInterval(refreshInterval);
|
|
1156
|
+
clearInterval(heartbeatInterval);
|
|
1157
|
+
sleepLock.release();
|
|
1009
1158
|
for (const session of activeSessions.values()) {
|
|
1010
1159
|
session.abort();
|
|
1011
1160
|
}
|
|
@@ -1016,6 +1165,15 @@ async function connect(server, options) {
|
|
|
1016
1165
|
cleanup();
|
|
1017
1166
|
process.exit(0);
|
|
1018
1167
|
});
|
|
1168
|
+
process.on("SIGTERM", () => {
|
|
1169
|
+
logger.info("Shutting down...");
|
|
1170
|
+
cleanup();
|
|
1171
|
+
process.exit(0);
|
|
1172
|
+
});
|
|
1173
|
+
process.on("SIGCONT", () => {
|
|
1174
|
+
logger.info("Resumed from sleep, reconnecting...");
|
|
1175
|
+
socket.disconnect().connect();
|
|
1176
|
+
});
|
|
1019
1177
|
await new Promise(() => {
|
|
1020
1178
|
});
|
|
1021
1179
|
}
|
|
@@ -1026,15 +1184,52 @@ function buildUrl(server) {
|
|
|
1026
1184
|
}
|
|
1027
1185
|
return url.origin + url.pathname;
|
|
1028
1186
|
}
|
|
1187
|
+
function formatConnectionError(err) {
|
|
1188
|
+
const errRecord = err;
|
|
1189
|
+
const description = errRecord.description;
|
|
1190
|
+
const message = description?.message ?? description?.error?.message ?? err.message;
|
|
1191
|
+
const result = { message };
|
|
1192
|
+
let reason = "unknown";
|
|
1193
|
+
if (errRecord.type === "TransportError" && description) {
|
|
1194
|
+
const target = description.target;
|
|
1195
|
+
const req = target?._req;
|
|
1196
|
+
const res = req?.res;
|
|
1197
|
+
const statusCode = res?.statusCode;
|
|
1198
|
+
if (statusCode) {
|
|
1199
|
+
result.statusCode = statusCode;
|
|
1200
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
1201
|
+
reason = "authentication failed";
|
|
1202
|
+
} else if (statusCode >= 500) {
|
|
1203
|
+
reason = "server unavailable";
|
|
1204
|
+
} else {
|
|
1205
|
+
reason = `server responded ${statusCode}`;
|
|
1206
|
+
}
|
|
1207
|
+
} else if (/ENOTFOUND|ECONNREFUSED|EAI_AGAIN/.test(message)) {
|
|
1208
|
+
reason = "server unreachable";
|
|
1209
|
+
} else {
|
|
1210
|
+
reason = "transport error";
|
|
1211
|
+
}
|
|
1212
|
+
} else if (err.message === "timeout") {
|
|
1213
|
+
reason = "timed out";
|
|
1214
|
+
} else if (errRecord.data) {
|
|
1215
|
+
result.data = errRecord.data;
|
|
1216
|
+
reason = "rejected by server";
|
|
1217
|
+
} else if (err.message.includes("v2.x")) {
|
|
1218
|
+
reason = "server version mismatch";
|
|
1219
|
+
} else {
|
|
1220
|
+
reason = "failed";
|
|
1221
|
+
}
|
|
1222
|
+
return { reason, ...result };
|
|
1223
|
+
}
|
|
1029
1224
|
function send(socket, event, msg) {
|
|
1030
1225
|
socket.emit(event, msg);
|
|
1031
1226
|
}
|
|
1032
1227
|
function handlePrompt(socket, msg, activeSessions) {
|
|
1033
|
-
const { id, prompt: prompt2, sessionId,
|
|
1228
|
+
const { id, prompt: prompt2, sessionId, workingDirectory, images, options } = msg;
|
|
1034
1229
|
const log2 = createChildLogger({ sessionId: id });
|
|
1035
1230
|
log2.info({ prompt: prompt2.slice(0, 80) }, "Session started");
|
|
1036
1231
|
const handle = runClaude(
|
|
1037
|
-
{ prompt: prompt2, sessionId,
|
|
1232
|
+
{ prompt: prompt2, sessionId, workingDirectory, images, options },
|
|
1038
1233
|
{
|
|
1039
1234
|
onSessionCreated: (info) => {
|
|
1040
1235
|
send(socket, "response", { type: "session_created", data: info, requestId: id });
|
|
@@ -1066,7 +1261,7 @@ function handlePrompt(socket, msg, activeSessions) {
|
|
|
1066
1261
|
log2.error({ error: message }, "Session error");
|
|
1067
1262
|
},
|
|
1068
1263
|
onPermissionRequest: (req) => {
|
|
1069
|
-
log2.info({ toolName: req.toolName }, "Permission
|
|
1264
|
+
log2.info({ toolUseId: req.toolUseId, toolName: req.toolName }, "Permission requested");
|
|
1070
1265
|
socket.emit("permission-request", {
|
|
1071
1266
|
requestId: id,
|
|
1072
1267
|
toolUseId: req.toolUseId,
|
|
@@ -1088,9 +1283,9 @@ function handleCancel(id, activeSessions) {
|
|
|
1088
1283
|
logger.info({ sessionId: id }, "Session cancelled");
|
|
1089
1284
|
}
|
|
1090
1285
|
}
|
|
1091
|
-
async function handleListSessions(socket, msg) {
|
|
1286
|
+
async function handleListSessions(socket, msg, defaultCwd) {
|
|
1092
1287
|
const { id } = msg;
|
|
1093
|
-
const workingDirectory = msg.workingDirectory ??
|
|
1288
|
+
const workingDirectory = msg.workingDirectory ?? defaultCwd;
|
|
1094
1289
|
logger.info("Listing sessions...");
|
|
1095
1290
|
const sessions = await listSessions(workingDirectory);
|
|
1096
1291
|
send(socket, "response", { type: "sessions_list", sessions, requestId: id });
|
|
@@ -1109,26 +1304,27 @@ async function handleLoadSession(socket, msg) {
|
|
|
1109
1304
|
log2.warn("Session not found");
|
|
1110
1305
|
}
|
|
1111
1306
|
}
|
|
1112
|
-
async function
|
|
1113
|
-
const { id,
|
|
1114
|
-
const log2 = createChildLogger({
|
|
1115
|
-
log2.info("Getting
|
|
1307
|
+
async function handleGetCommands(socket, msg) {
|
|
1308
|
+
const { id, workingDirectory } = msg;
|
|
1309
|
+
const log2 = createChildLogger({ requestId: id });
|
|
1310
|
+
log2.info("Getting commands...");
|
|
1116
1311
|
try {
|
|
1117
|
-
const
|
|
1118
|
-
send(socket, "response", { type: "
|
|
1119
|
-
log2.info("
|
|
1312
|
+
const commands = await getProjectCommands(workingDirectory);
|
|
1313
|
+
send(socket, "response", { type: "commands", commands, requestId: id });
|
|
1314
|
+
log2.info({ count: commands.length }, "Commands retrieved");
|
|
1120
1315
|
} catch (err) {
|
|
1121
1316
|
const message = err instanceof Error ? err.message : String(err);
|
|
1122
1317
|
send(socket, "response", { type: "error", message, requestId: id });
|
|
1123
|
-
log2.error({ err }, "
|
|
1318
|
+
log2.error({ err }, "Commands error");
|
|
1124
1319
|
}
|
|
1125
1320
|
}
|
|
1126
|
-
async function handleGetContext(socket, msg) {
|
|
1127
|
-
const { id, sessionId
|
|
1321
|
+
async function handleGetContext(socket, msg, defaultCwd) {
|
|
1322
|
+
const { id, sessionId } = msg;
|
|
1323
|
+
const workingDirectory = msg.workingDirectory ?? defaultCwd;
|
|
1128
1324
|
const log2 = createChildLogger({ sessionId });
|
|
1129
1325
|
log2.info("Getting context...");
|
|
1130
1326
|
try {
|
|
1131
|
-
const data = await getContext(sessionId,
|
|
1327
|
+
const data = await getContext(sessionId, workingDirectory);
|
|
1132
1328
|
send(socket, "response", { type: "context", data, requestId: id });
|
|
1133
1329
|
log2.info({ totalTokens: data.totalTokens }, "Context retrieved");
|
|
1134
1330
|
} catch (err) {
|
|
@@ -1210,7 +1406,7 @@ function logout() {
|
|
|
1210
1406
|
|
|
1211
1407
|
// src/commands/index.ts
|
|
1212
1408
|
function registerCommands(program2) {
|
|
1213
|
-
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], []).action(connect);
|
|
1409
|
+
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);
|
|
1214
1410
|
program2.command("login").description("Log in with your email and password").action(login);
|
|
1215
1411
|
program2.command("logout").description("Log out and clear stored credentials").action(logout);
|
|
1216
1412
|
}
|