@punkcode/cli 0.1.8 → 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.
Files changed (2) hide show
  1. package/dist/cli.js +259 -77
  2. 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 getSessionInfo(sessionId, cwd) {
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
- resume: sessionId,
41
- ...cwd && { cwd }
130
+ persistSession: false,
131
+ ...workingDirectory && { cwd: workingDirectory }
42
132
  }
43
133
  });
44
134
  try {
45
- const [init, commands, mcpServers] = await Promise.all([
46
- q.initializationResult(),
135
+ const [commands, skills] = await Promise.all([
47
136
  q.supportedCommands(),
48
- q.mcpServerStatus()
137
+ loadGlobalSkills(workingDirectory)
49
138
  ]);
50
- const cmdList = commands.length >= init.commands.length ? commands : init.commands;
51
- return {
52
- sessionId,
53
- tools: [],
54
- slashCommands: cmdList.map((c) => ({ name: c.name, description: c.description })),
55
- skills: [],
56
- mcpServers: mcpServers.map((s) => ({ name: s.name, status: s.status })),
57
- model: init.models?.[0]?.value ?? "",
58
- cwd: cwd ?? "",
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.cwd && { cwd: options.cwd },
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
- let slashCommands = (sys.slash_commands ?? []).map((cmd) => ({ name: cmd, description: "" }));
183
- try {
184
- const cmds = await q.supportedCommands();
185
- if (cmds.length > 0) {
186
- slashCommands = cmds.map((c) => ({ name: c.name, description: c.description }));
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
- callbacks.onSessionCreated({
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
- cwd: sys.cwd ?? "",
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
  }
@@ -273,7 +361,7 @@ function collectDeviceInfo(deviceId, customName, customTags) {
273
361
  arch: process.arch,
274
362
  username: os.userInfo().username,
275
363
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
276
- cwd: process.cwd(),
364
+ defaultWorkingDirectory: process.cwd(),
277
365
  model: getModel(),
278
366
  cpuModel: cpus.length > 0 ? cpus[0].model : "Unknown",
279
367
  memoryGB: Math.round(os.totalmem() / 1024 ** 3),
@@ -385,10 +473,10 @@ function getModel() {
385
473
  }
386
474
 
387
475
  // 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 = join(homedir(), ".claude", "projects");
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");
392
480
  function pathToProjectDir(dir) {
393
481
  return dir.replace(/\//g, "-");
394
482
  }
@@ -396,14 +484,14 @@ async function loadSession(sessionId) {
396
484
  const sessionFile = `${sessionId}.jsonl`;
397
485
  let projectDirs;
398
486
  try {
399
- projectDirs = await readdir(CLAUDE_DIR);
487
+ projectDirs = await readdir2(CLAUDE_DIR);
400
488
  } catch {
401
489
  return null;
402
490
  }
403
491
  for (const projectDir of projectDirs) {
404
- const sessionPath = join(CLAUDE_DIR, projectDir, sessionFile);
492
+ const sessionPath = join2(CLAUDE_DIR, projectDir, sessionFile);
405
493
  try {
406
- const content = await readFile(sessionPath, "utf-8");
494
+ const content = await readFile2(sessionPath, "utf-8");
407
495
  return parseSessionFile(content);
408
496
  } catch {
409
497
  }
@@ -420,17 +508,17 @@ async function listSessions(workingDirectory) {
420
508
  if (workingDirectory) {
421
509
  projectDirs = [pathToProjectDir(workingDirectory)];
422
510
  } else {
423
- projectDirs = await readdir(CLAUDE_DIR);
511
+ projectDirs = await readdir2(CLAUDE_DIR);
424
512
  }
425
513
  } catch {
426
514
  return [];
427
515
  }
428
516
  const candidates = [];
429
517
  for (const projectDir of projectDirs) {
430
- const projectPath = join(CLAUDE_DIR, projectDir);
518
+ const projectPath = join2(CLAUDE_DIR, projectDir);
431
519
  let files;
432
520
  try {
433
- files = await readdir(projectPath);
521
+ files = await readdir2(projectPath);
434
522
  } catch {
435
523
  continue;
436
524
  }
@@ -440,8 +528,10 @@ async function listSessions(workingDirectory) {
440
528
  if (!isValidSessionUUID(sessionId)) continue;
441
529
  candidates.push({
442
530
  sessionId,
443
- project: projectDir,
444
- filePath: join(projectPath, file)
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)
445
535
  });
446
536
  }
447
537
  }
@@ -695,23 +785,8 @@ function parseSessionFile(content) {
695
785
 
696
786
  // src/lib/context.ts
697
787
  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
788
  var log = createChildLogger({ component: "context" });
714
- async function getContext(sessionId, cwd) {
789
+ async function getContext(sessionId, workingDirectory) {
715
790
  let stdout;
716
791
  try {
717
792
  const result = await execa("claude", [
@@ -723,7 +798,7 @@ async function getContext(sessionId, cwd) {
723
798
  sessionId,
724
799
  "/context"
725
800
  ], {
726
- cwd: cwd || process.cwd(),
801
+ cwd: workingDirectory || process.cwd(),
727
802
  timeout: 3e4,
728
803
  stdin: "ignore"
729
804
  });
@@ -902,6 +977,54 @@ async function refreshIdToken() {
902
977
  return updated.idToken;
903
978
  }
904
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
+
905
1028
  // src/commands/connect.ts
906
1029
  async function connect(server, options) {
907
1030
  logger.info("Checking prerequisites...");
@@ -937,6 +1060,13 @@ async function connect(server, options) {
937
1060
  reconnectionDelayMax: 5e3
938
1061
  });
939
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);
940
1070
  socket.on("connect", () => {
941
1071
  logger.info("Connected");
942
1072
  const deviceInfo = collectDeviceInfo(deviceId, options.name, options.tag);
@@ -966,15 +1096,17 @@ async function connect(server, options) {
966
1096
  handleGetContext(socket, msg);
967
1097
  }
968
1098
  });
969
- socket.on("get-session-info", (msg) => {
970
- if (msg.type === "get-session-info") {
971
- handleGetSessionInfo(socket, msg);
1099
+ socket.on("get-commands", (msg) => {
1100
+ if (msg.type === "get-commands") {
1101
+ handleGetCommands(socket, msg);
972
1102
  }
973
1103
  });
974
1104
  socket.on("cancel", (msg) => {
975
1105
  handleCancel(msg.id, activeSessions);
976
1106
  });
977
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");
978
1110
  const session = activeSessions.get(msg.requestId);
979
1111
  if (session) {
980
1112
  session.resolvePermission(msg.toolUseId, msg.allow, msg.answers, msg.feedback);
@@ -995,7 +1127,9 @@ async function connect(server, options) {
995
1127
  socket.emit("register", collectDeviceInfo(deviceId, options.name, options.tag));
996
1128
  });
997
1129
  socket.on("connect_error", (err) => {
998
- logger.error({ err }, "Connection error");
1130
+ const { reason, ...detail } = formatConnectionError(err);
1131
+ logger.error(detail, `Connection error: ${reason}`);
1132
+ logger.debug({ err }, "Connection error (raw)");
999
1133
  });
1000
1134
  const refreshInterval = setInterval(async () => {
1001
1135
  try {
@@ -1006,6 +1140,8 @@ async function connect(server, options) {
1006
1140
  }, 50 * 60 * 1e3);
1007
1141
  const cleanup = () => {
1008
1142
  clearInterval(refreshInterval);
1143
+ clearInterval(heartbeatInterval);
1144
+ sleepLock.release();
1009
1145
  for (const session of activeSessions.values()) {
1010
1146
  session.abort();
1011
1147
  }
@@ -1016,6 +1152,15 @@ async function connect(server, options) {
1016
1152
  cleanup();
1017
1153
  process.exit(0);
1018
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
+ });
1019
1164
  await new Promise(() => {
1020
1165
  });
1021
1166
  }
@@ -1026,15 +1171,52 @@ function buildUrl(server) {
1026
1171
  }
1027
1172
  return url.origin + url.pathname;
1028
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
+ }
1029
1211
  function send(socket, event, msg) {
1030
1212
  socket.emit(event, msg);
1031
1213
  }
1032
1214
  function handlePrompt(socket, msg, activeSessions) {
1033
- const { id, prompt: prompt2, sessionId, cwd, images, options } = msg;
1215
+ const { id, prompt: prompt2, sessionId, workingDirectory, images, options } = msg;
1034
1216
  const log2 = createChildLogger({ sessionId: id });
1035
1217
  log2.info({ prompt: prompt2.slice(0, 80) }, "Session started");
1036
1218
  const handle = runClaude(
1037
- { prompt: prompt2, sessionId, cwd, images, options },
1219
+ { prompt: prompt2, sessionId, workingDirectory, images, options },
1038
1220
  {
1039
1221
  onSessionCreated: (info) => {
1040
1222
  send(socket, "response", { type: "session_created", data: info, requestId: id });
@@ -1066,7 +1248,7 @@ function handlePrompt(socket, msg, activeSessions) {
1066
1248
  log2.error({ error: message }, "Session error");
1067
1249
  },
1068
1250
  onPermissionRequest: (req) => {
1069
- log2.info({ toolName: req.toolName }, "Permission blocked");
1251
+ log2.info({ toolUseId: req.toolUseId, toolName: req.toolName }, "Permission requested");
1070
1252
  socket.emit("permission-request", {
1071
1253
  requestId: id,
1072
1254
  toolUseId: req.toolUseId,
@@ -1109,26 +1291,26 @@ async function handleLoadSession(socket, msg) {
1109
1291
  log2.warn("Session not found");
1110
1292
  }
1111
1293
  }
1112
- async function handleGetSessionInfo(socket, msg) {
1113
- const { id, sessionId, cwd } = msg;
1114
- const log2 = createChildLogger({ sessionId });
1115
- log2.info("Getting session info...");
1294
+ async function handleGetCommands(socket, msg) {
1295
+ const { id, workingDirectory } = msg;
1296
+ const log2 = createChildLogger({ requestId: id });
1297
+ log2.info("Getting commands...");
1116
1298
  try {
1117
- const data = await getSessionInfo(sessionId, cwd);
1118
- send(socket, "response", { type: "session_info", data, requestId: id });
1119
- log2.info("Session info retrieved");
1299
+ const commands = await getProjectCommands(workingDirectory);
1300
+ send(socket, "response", { type: "commands", commands, requestId: id });
1301
+ log2.info({ count: commands.length }, "Commands retrieved");
1120
1302
  } catch (err) {
1121
1303
  const message = err instanceof Error ? err.message : String(err);
1122
1304
  send(socket, "response", { type: "error", message, requestId: id });
1123
- log2.error({ err }, "Session info error");
1305
+ log2.error({ err }, "Commands error");
1124
1306
  }
1125
1307
  }
1126
1308
  async function handleGetContext(socket, msg) {
1127
- const { id, sessionId, cwd } = msg;
1309
+ const { id, sessionId, workingDirectory } = msg;
1128
1310
  const log2 = createChildLogger({ sessionId });
1129
1311
  log2.info("Getting context...");
1130
1312
  try {
1131
- const data = await getContext(sessionId, cwd);
1313
+ const data = await getContext(sessionId, workingDirectory);
1132
1314
  send(socket, "response", { type: "context", data, requestId: id });
1133
1315
  log2.info({ totalTokens: data.totalTokens }, "Context retrieved");
1134
1316
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punkcode/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Control Claude Code from your phone",
5
5
  "type": "module",
6
6
  "bin": {