@punkcode/cli 0.1.7 → 0.1.9
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 +296 -45
- 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,6 +51,104 @@ async function* promptWithImages(text, images, sessionId) {
|
|
|
33
51
|
session_id: sessionId
|
|
34
52
|
};
|
|
35
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
|
+
}
|
|
36
152
|
function runClaude(options, callbacks) {
|
|
37
153
|
const opts = options.options || {};
|
|
38
154
|
const isBypass = opts.permissionMode === "bypassPermissions";
|
|
@@ -50,7 +166,7 @@ function runClaude(options, callbacks) {
|
|
|
50
166
|
...opts.disallowedTools && { disallowedTools: opts.disallowedTools },
|
|
51
167
|
...opts.maxTurns && { maxTurns: opts.maxTurns },
|
|
52
168
|
systemPrompt: opts.systemPrompt ?? { type: "preset", preset: "claude_code" },
|
|
53
|
-
...options.
|
|
169
|
+
...options.workingDirectory && { cwd: options.workingDirectory },
|
|
54
170
|
...options.sessionId && { resume: options.sessionId },
|
|
55
171
|
maxThinkingTokens: 1e4,
|
|
56
172
|
includePartialMessages: true,
|
|
@@ -149,17 +265,27 @@ function runClaude(options, callbacks) {
|
|
|
149
265
|
case "system": {
|
|
150
266
|
const sys = message;
|
|
151
267
|
if (sys.subtype === "init" && callbacks.onSessionCreated) {
|
|
152
|
-
|
|
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);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const sessionInfo = {
|
|
153
277
|
sessionId: sys.session_id ?? "",
|
|
154
278
|
tools: sys.tools ?? [],
|
|
155
|
-
slashCommands:
|
|
279
|
+
slashCommands: initCommands,
|
|
156
280
|
skills: sys.skills ?? [],
|
|
157
281
|
mcpServers: sys.mcp_servers ?? [],
|
|
158
282
|
model: sys.model ?? "",
|
|
159
|
-
|
|
283
|
+
workingDirectory: sys.cwd ?? "",
|
|
160
284
|
claudeCodeVersion: sys.claude_code_version ?? "",
|
|
161
285
|
permissionMode: sys.permissionMode ?? "default"
|
|
162
|
-
}
|
|
286
|
+
};
|
|
287
|
+
logger.info({ sessionId: sessionInfo.sessionId, commands: sessionInfo.slashCommands.length }, "New chat session info");
|
|
288
|
+
callbacks.onSessionCreated(sessionInfo);
|
|
163
289
|
}
|
|
164
290
|
break;
|
|
165
291
|
}
|
|
@@ -219,19 +345,23 @@ function getOrCreateDeviceId() {
|
|
|
219
345
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
220
346
|
return id;
|
|
221
347
|
}
|
|
222
|
-
function collectDeviceInfo(deviceId, customName) {
|
|
348
|
+
function collectDeviceInfo(deviceId, customName, customTags) {
|
|
223
349
|
if (customName) {
|
|
224
350
|
saveConfigField("deviceName", customName);
|
|
225
351
|
}
|
|
352
|
+
if (customTags && customTags.length > 0) {
|
|
353
|
+
saveConfigField("tags", customTags);
|
|
354
|
+
}
|
|
226
355
|
const cpus = os.cpus();
|
|
227
356
|
return {
|
|
228
357
|
deviceId,
|
|
229
358
|
name: customName || getDeviceName(),
|
|
359
|
+
tags: customTags && customTags.length > 0 ? customTags : getTags(),
|
|
230
360
|
platform: process.platform,
|
|
231
361
|
arch: process.arch,
|
|
232
362
|
username: os.userInfo().username,
|
|
233
363
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
234
|
-
|
|
364
|
+
defaultWorkingDirectory: process.cwd(),
|
|
235
365
|
model: getModel(),
|
|
236
366
|
cpuModel: cpus.length > 0 ? cpus[0].model : "Unknown",
|
|
237
367
|
memoryGB: Math.round(os.totalmem() / 1024 ** 3),
|
|
@@ -255,6 +385,14 @@ function getDeviceName() {
|
|
|
255
385
|
}
|
|
256
386
|
return os.hostname();
|
|
257
387
|
}
|
|
388
|
+
function getTags() {
|
|
389
|
+
try {
|
|
390
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
391
|
+
if (Array.isArray(config.tags)) return config.tags;
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
258
396
|
function saveConfigField(key, value) {
|
|
259
397
|
fs.mkdirSync(PUNK_DIR, { recursive: true });
|
|
260
398
|
let config = {};
|
|
@@ -335,10 +473,10 @@ function getModel() {
|
|
|
335
473
|
}
|
|
336
474
|
|
|
337
475
|
// src/lib/session.ts
|
|
338
|
-
import { readdir, readFile, stat, open } from "fs/promises";
|
|
339
|
-
import { join } from "path";
|
|
340
|
-
import { homedir } from "os";
|
|
341
|
-
var CLAUDE_DIR =
|
|
476
|
+
import { readdir as readdir2, readFile as readFile2, stat, open } from "fs/promises";
|
|
477
|
+
import { join as join2 } from "path";
|
|
478
|
+
import { homedir as homedir2 } from "os";
|
|
479
|
+
var CLAUDE_DIR = join2(homedir2(), ".claude", "projects");
|
|
342
480
|
function pathToProjectDir(dir) {
|
|
343
481
|
return dir.replace(/\//g, "-");
|
|
344
482
|
}
|
|
@@ -346,14 +484,14 @@ async function loadSession(sessionId) {
|
|
|
346
484
|
const sessionFile = `${sessionId}.jsonl`;
|
|
347
485
|
let projectDirs;
|
|
348
486
|
try {
|
|
349
|
-
projectDirs = await
|
|
487
|
+
projectDirs = await readdir2(CLAUDE_DIR);
|
|
350
488
|
} catch {
|
|
351
489
|
return null;
|
|
352
490
|
}
|
|
353
491
|
for (const projectDir of projectDirs) {
|
|
354
|
-
const sessionPath =
|
|
492
|
+
const sessionPath = join2(CLAUDE_DIR, projectDir, sessionFile);
|
|
355
493
|
try {
|
|
356
|
-
const content = await
|
|
494
|
+
const content = await readFile2(sessionPath, "utf-8");
|
|
357
495
|
return parseSessionFile(content);
|
|
358
496
|
} catch {
|
|
359
497
|
}
|
|
@@ -370,17 +508,17 @@ async function listSessions(workingDirectory) {
|
|
|
370
508
|
if (workingDirectory) {
|
|
371
509
|
projectDirs = [pathToProjectDir(workingDirectory)];
|
|
372
510
|
} else {
|
|
373
|
-
projectDirs = await
|
|
511
|
+
projectDirs = await readdir2(CLAUDE_DIR);
|
|
374
512
|
}
|
|
375
513
|
} catch {
|
|
376
514
|
return [];
|
|
377
515
|
}
|
|
378
516
|
const candidates = [];
|
|
379
517
|
for (const projectDir of projectDirs) {
|
|
380
|
-
const projectPath =
|
|
518
|
+
const projectPath = join2(CLAUDE_DIR, projectDir);
|
|
381
519
|
let files;
|
|
382
520
|
try {
|
|
383
|
-
files = await
|
|
521
|
+
files = await readdir2(projectPath);
|
|
384
522
|
} catch {
|
|
385
523
|
continue;
|
|
386
524
|
}
|
|
@@ -390,8 +528,10 @@ async function listSessions(workingDirectory) {
|
|
|
390
528
|
if (!isValidSessionUUID(sessionId)) continue;
|
|
391
529
|
candidates.push({
|
|
392
530
|
sessionId,
|
|
393
|
-
|
|
394
|
-
|
|
531
|
+
// When a workingDirectory is provided, return the original path so it
|
|
532
|
+
// matches the device's defaultWorkingDirectory on the mobile side.
|
|
533
|
+
project: workingDirectory ?? projectDir,
|
|
534
|
+
filePath: join2(projectPath, file)
|
|
395
535
|
});
|
|
396
536
|
}
|
|
397
537
|
}
|
|
@@ -645,23 +785,8 @@ function parseSessionFile(content) {
|
|
|
645
785
|
|
|
646
786
|
// src/lib/context.ts
|
|
647
787
|
import { execa } from "execa";
|
|
648
|
-
|
|
649
|
-
// src/utils/logger.ts
|
|
650
|
-
import pino from "pino";
|
|
651
|
-
var level = process.env.LOG_LEVEL ?? "info";
|
|
652
|
-
var format = process.env.PUNK_LOG_FORMAT ?? (process.stdout.isTTY ? "pretty" : "json");
|
|
653
|
-
var transport = format === "pretty" ? pino.transport({
|
|
654
|
-
target: "pino-pretty",
|
|
655
|
-
options: { colorize: true }
|
|
656
|
-
}) : void 0;
|
|
657
|
-
var logger = pino({ level }, transport);
|
|
658
|
-
function createChildLogger(bindings) {
|
|
659
|
-
return logger.child(bindings);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// src/lib/context.ts
|
|
663
788
|
var log = createChildLogger({ component: "context" });
|
|
664
|
-
async function getContext(sessionId,
|
|
789
|
+
async function getContext(sessionId, workingDirectory) {
|
|
665
790
|
let stdout;
|
|
666
791
|
try {
|
|
667
792
|
const result = await execa("claude", [
|
|
@@ -673,7 +798,7 @@ async function getContext(sessionId, cwd) {
|
|
|
673
798
|
sessionId,
|
|
674
799
|
"/context"
|
|
675
800
|
], {
|
|
676
|
-
cwd:
|
|
801
|
+
cwd: workingDirectory || process.cwd(),
|
|
677
802
|
timeout: 3e4,
|
|
678
803
|
stdin: "ignore"
|
|
679
804
|
});
|
|
@@ -852,6 +977,54 @@ async function refreshIdToken() {
|
|
|
852
977
|
return updated.idToken;
|
|
853
978
|
}
|
|
854
979
|
|
|
980
|
+
// src/lib/sleep-inhibitor.ts
|
|
981
|
+
import { spawn } from "child_process";
|
|
982
|
+
function preventIdleSleep() {
|
|
983
|
+
const platform = process.platform;
|
|
984
|
+
let child = null;
|
|
985
|
+
if (platform === "darwin") {
|
|
986
|
+
child = spawn("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
987
|
+
stdio: "ignore",
|
|
988
|
+
detached: false
|
|
989
|
+
});
|
|
990
|
+
} else if (platform === "linux") {
|
|
991
|
+
child = spawn(
|
|
992
|
+
"systemd-inhibit",
|
|
993
|
+
[
|
|
994
|
+
"--what=idle",
|
|
995
|
+
"--who=punk-connect",
|
|
996
|
+
"--why=Device connected for remote access",
|
|
997
|
+
"cat"
|
|
998
|
+
],
|
|
999
|
+
{ stdio: ["pipe", "ignore", "ignore"], detached: false }
|
|
1000
|
+
);
|
|
1001
|
+
} else if (platform === "win32") {
|
|
1002
|
+
const script = `
|
|
1003
|
+
$sig = '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags);';
|
|
1004
|
+
$t = Add-Type -MemberDefinition $sig -Name WinAPI -Namespace Punk -PassThru;
|
|
1005
|
+
while($true) {
|
|
1006
|
+
$t::SetThreadExecutionState(0x80000001) | Out-Null;
|
|
1007
|
+
try { $null = Read-Host } catch { break };
|
|
1008
|
+
Start-Sleep -Seconds 30;
|
|
1009
|
+
}`.trim();
|
|
1010
|
+
child = spawn("powershell", ["-NoProfile", "-Command", script], {
|
|
1011
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
1012
|
+
detached: false
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
child?.unref();
|
|
1016
|
+
child?.on("error", () => {
|
|
1017
|
+
});
|
|
1018
|
+
return {
|
|
1019
|
+
release: () => {
|
|
1020
|
+
if (child && !child.killed) {
|
|
1021
|
+
child.kill();
|
|
1022
|
+
child = null;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
855
1028
|
// src/commands/connect.ts
|
|
856
1029
|
async function connect(server, options) {
|
|
857
1030
|
logger.info("Checking prerequisites...");
|
|
@@ -887,9 +1060,16 @@ async function connect(server, options) {
|
|
|
887
1060
|
reconnectionDelayMax: 5e3
|
|
888
1061
|
});
|
|
889
1062
|
const activeSessions = /* @__PURE__ */ new Map();
|
|
1063
|
+
const sleepLock = preventIdleSleep();
|
|
1064
|
+
logger.info("Sleep inhibitor active");
|
|
1065
|
+
const heartbeatInterval = setInterval(() => {
|
|
1066
|
+
if (socket.connected) {
|
|
1067
|
+
socket.emit("heartbeat");
|
|
1068
|
+
}
|
|
1069
|
+
}, 3e4);
|
|
890
1070
|
socket.on("connect", () => {
|
|
891
1071
|
logger.info("Connected");
|
|
892
|
-
const deviceInfo = collectDeviceInfo(deviceId, options.name);
|
|
1072
|
+
const deviceInfo = collectDeviceInfo(deviceId, options.name, options.tag);
|
|
893
1073
|
socket.emit("register", deviceInfo, (response) => {
|
|
894
1074
|
if (response.success) {
|
|
895
1075
|
logger.info({ deviceId }, "Registered");
|
|
@@ -916,10 +1096,17 @@ async function connect(server, options) {
|
|
|
916
1096
|
handleGetContext(socket, msg);
|
|
917
1097
|
}
|
|
918
1098
|
});
|
|
1099
|
+
socket.on("get-commands", (msg) => {
|
|
1100
|
+
if (msg.type === "get-commands") {
|
|
1101
|
+
handleGetCommands(socket, msg);
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
919
1104
|
socket.on("cancel", (msg) => {
|
|
920
1105
|
handleCancel(msg.id, activeSessions);
|
|
921
1106
|
});
|
|
922
1107
|
socket.on("permission-response", (msg) => {
|
|
1108
|
+
const log2 = createChildLogger({ sessionId: msg.requestId });
|
|
1109
|
+
log2.info({ toolUseId: msg.toolUseId, allowed: msg.allow }, msg.allow ? "Permission accepted" : "Permission denied");
|
|
923
1110
|
const session = activeSessions.get(msg.requestId);
|
|
924
1111
|
if (session) {
|
|
925
1112
|
session.resolvePermission(msg.toolUseId, msg.allow, msg.answers, msg.feedback);
|
|
@@ -937,10 +1124,12 @@ async function connect(server, options) {
|
|
|
937
1124
|
});
|
|
938
1125
|
socket.on("reconnect", (attemptNumber) => {
|
|
939
1126
|
logger.info({ attemptNumber }, "Reconnected");
|
|
940
|
-
socket.emit("register", collectDeviceInfo(deviceId, options.name));
|
|
1127
|
+
socket.emit("register", collectDeviceInfo(deviceId, options.name, options.tag));
|
|
941
1128
|
});
|
|
942
1129
|
socket.on("connect_error", (err) => {
|
|
943
|
-
|
|
1130
|
+
const { reason, ...detail } = formatConnectionError(err);
|
|
1131
|
+
logger.error(detail, `Connection error: ${reason}`);
|
|
1132
|
+
logger.debug({ err }, "Connection error (raw)");
|
|
944
1133
|
});
|
|
945
1134
|
const refreshInterval = setInterval(async () => {
|
|
946
1135
|
try {
|
|
@@ -951,6 +1140,8 @@ async function connect(server, options) {
|
|
|
951
1140
|
}, 50 * 60 * 1e3);
|
|
952
1141
|
const cleanup = () => {
|
|
953
1142
|
clearInterval(refreshInterval);
|
|
1143
|
+
clearInterval(heartbeatInterval);
|
|
1144
|
+
sleepLock.release();
|
|
954
1145
|
for (const session of activeSessions.values()) {
|
|
955
1146
|
session.abort();
|
|
956
1147
|
}
|
|
@@ -961,6 +1152,15 @@ async function connect(server, options) {
|
|
|
961
1152
|
cleanup();
|
|
962
1153
|
process.exit(0);
|
|
963
1154
|
});
|
|
1155
|
+
process.on("SIGTERM", () => {
|
|
1156
|
+
logger.info("Shutting down...");
|
|
1157
|
+
cleanup();
|
|
1158
|
+
process.exit(0);
|
|
1159
|
+
});
|
|
1160
|
+
process.on("SIGCONT", () => {
|
|
1161
|
+
logger.info("Resumed from sleep, reconnecting...");
|
|
1162
|
+
socket.disconnect().connect();
|
|
1163
|
+
});
|
|
964
1164
|
await new Promise(() => {
|
|
965
1165
|
});
|
|
966
1166
|
}
|
|
@@ -971,15 +1171,52 @@ function buildUrl(server) {
|
|
|
971
1171
|
}
|
|
972
1172
|
return url.origin + url.pathname;
|
|
973
1173
|
}
|
|
1174
|
+
function formatConnectionError(err) {
|
|
1175
|
+
const errRecord = err;
|
|
1176
|
+
const description = errRecord.description;
|
|
1177
|
+
const message = description?.message ?? description?.error?.message ?? err.message;
|
|
1178
|
+
const result = { message };
|
|
1179
|
+
let reason = "unknown";
|
|
1180
|
+
if (errRecord.type === "TransportError" && description) {
|
|
1181
|
+
const target = description.target;
|
|
1182
|
+
const req = target?._req;
|
|
1183
|
+
const res = req?.res;
|
|
1184
|
+
const statusCode = res?.statusCode;
|
|
1185
|
+
if (statusCode) {
|
|
1186
|
+
result.statusCode = statusCode;
|
|
1187
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
1188
|
+
reason = "authentication failed";
|
|
1189
|
+
} else if (statusCode >= 500) {
|
|
1190
|
+
reason = "server unavailable";
|
|
1191
|
+
} else {
|
|
1192
|
+
reason = `server responded ${statusCode}`;
|
|
1193
|
+
}
|
|
1194
|
+
} else if (/ENOTFOUND|ECONNREFUSED|EAI_AGAIN/.test(message)) {
|
|
1195
|
+
reason = "server unreachable";
|
|
1196
|
+
} else {
|
|
1197
|
+
reason = "transport error";
|
|
1198
|
+
}
|
|
1199
|
+
} else if (err.message === "timeout") {
|
|
1200
|
+
reason = "timed out";
|
|
1201
|
+
} else if (errRecord.data) {
|
|
1202
|
+
result.data = errRecord.data;
|
|
1203
|
+
reason = "rejected by server";
|
|
1204
|
+
} else if (err.message.includes("v2.x")) {
|
|
1205
|
+
reason = "server version mismatch";
|
|
1206
|
+
} else {
|
|
1207
|
+
reason = "failed";
|
|
1208
|
+
}
|
|
1209
|
+
return { reason, ...result };
|
|
1210
|
+
}
|
|
974
1211
|
function send(socket, event, msg) {
|
|
975
1212
|
socket.emit(event, msg);
|
|
976
1213
|
}
|
|
977
1214
|
function handlePrompt(socket, msg, activeSessions) {
|
|
978
|
-
const { id, prompt: prompt2, sessionId,
|
|
1215
|
+
const { id, prompt: prompt2, sessionId, workingDirectory, images, options } = msg;
|
|
979
1216
|
const log2 = createChildLogger({ sessionId: id });
|
|
980
1217
|
log2.info({ prompt: prompt2.slice(0, 80) }, "Session started");
|
|
981
1218
|
const handle = runClaude(
|
|
982
|
-
{ prompt: prompt2, sessionId,
|
|
1219
|
+
{ prompt: prompt2, sessionId, workingDirectory, images, options },
|
|
983
1220
|
{
|
|
984
1221
|
onSessionCreated: (info) => {
|
|
985
1222
|
send(socket, "response", { type: "session_created", data: info, requestId: id });
|
|
@@ -1011,7 +1248,7 @@ function handlePrompt(socket, msg, activeSessions) {
|
|
|
1011
1248
|
log2.error({ error: message }, "Session error");
|
|
1012
1249
|
},
|
|
1013
1250
|
onPermissionRequest: (req) => {
|
|
1014
|
-
log2.info({ toolName: req.toolName }, "Permission
|
|
1251
|
+
log2.info({ toolUseId: req.toolUseId, toolName: req.toolName }, "Permission requested");
|
|
1015
1252
|
socket.emit("permission-request", {
|
|
1016
1253
|
requestId: id,
|
|
1017
1254
|
toolUseId: req.toolUseId,
|
|
@@ -1054,12 +1291,26 @@ async function handleLoadSession(socket, msg) {
|
|
|
1054
1291
|
log2.warn("Session not found");
|
|
1055
1292
|
}
|
|
1056
1293
|
}
|
|
1294
|
+
async function handleGetCommands(socket, msg) {
|
|
1295
|
+
const { id, workingDirectory } = msg;
|
|
1296
|
+
const log2 = createChildLogger({ requestId: id });
|
|
1297
|
+
log2.info("Getting commands...");
|
|
1298
|
+
try {
|
|
1299
|
+
const commands = await getProjectCommands(workingDirectory);
|
|
1300
|
+
send(socket, "response", { type: "commands", commands, requestId: id });
|
|
1301
|
+
log2.info({ count: commands.length }, "Commands retrieved");
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1304
|
+
send(socket, "response", { type: "error", message, requestId: id });
|
|
1305
|
+
log2.error({ err }, "Commands error");
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1057
1308
|
async function handleGetContext(socket, msg) {
|
|
1058
|
-
const { id, sessionId,
|
|
1309
|
+
const { id, sessionId, workingDirectory } = msg;
|
|
1059
1310
|
const log2 = createChildLogger({ sessionId });
|
|
1060
1311
|
log2.info("Getting context...");
|
|
1061
1312
|
try {
|
|
1062
|
-
const data = await getContext(sessionId,
|
|
1313
|
+
const data = await getContext(sessionId, workingDirectory);
|
|
1063
1314
|
send(socket, "response", { type: "context", data, requestId: id });
|
|
1064
1315
|
log2.info({ totalTokens: data.totalTokens }, "Context retrieved");
|
|
1065
1316
|
} catch (err) {
|
|
@@ -1141,7 +1392,7 @@ function logout() {
|
|
|
1141
1392
|
|
|
1142
1393
|
// src/commands/index.ts
|
|
1143
1394
|
function registerCommands(program2) {
|
|
1144
|
-
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").action(connect);
|
|
1395
|
+
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);
|
|
1145
1396
|
program2.command("login").description("Log in with your email and password").action(login);
|
|
1146
1397
|
program2.command("logout").description("Log out and clear stored credentials").action(logout);
|
|
1147
1398
|
}
|