@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.
Files changed (2) hide show
  1. package/dist/cli.js +282 -86
  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
  }
@@ -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 collectDeviceInfo(deviceId, customName, customTags) {
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
- cwd: process.cwd(),
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 = join(homedir(), ".claude", "projects");
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 readdir(CLAUDE_DIR);
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 = join(CLAUDE_DIR, projectDir, sessionFile);
503
+ const sessionPath = join2(CLAUDE_DIR, projectDir, sessionFile);
405
504
  try {
406
- const content = await readFile(sessionPath, "utf-8");
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 readdir(CLAUDE_DIR);
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 = join(CLAUDE_DIR, projectDir);
529
+ const projectPath = join2(CLAUDE_DIR, projectDir);
431
530
  let files;
432
531
  try {
433
- files = await readdir(projectPath);
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
- project: projectDir,
444
- filePath: join(projectPath, file)
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, cwd) {
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: cwd || process.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-session-info", (msg) => {
970
- if (msg.type === "get-session-info") {
971
- handleGetSessionInfo(socket, msg);
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
- logger.error({ err }, "Connection error");
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, cwd, images, options } = msg;
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, cwd, images, options },
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 blocked");
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 ?? process.cwd();
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 handleGetSessionInfo(socket, msg) {
1113
- const { id, sessionId, cwd } = msg;
1114
- const log2 = createChildLogger({ sessionId });
1115
- log2.info("Getting session info...");
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 data = await getSessionInfo(sessionId, cwd);
1118
- send(socket, "response", { type: "session_info", data, requestId: id });
1119
- log2.info("Session info retrieved");
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 }, "Session info error");
1318
+ log2.error({ err }, "Commands error");
1124
1319
  }
1125
1320
  }
1126
- async function handleGetContext(socket, msg) {
1127
- const { id, sessionId, cwd } = msg;
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, cwd);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punkcode/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Control Claude Code from your phone",
5
5
  "type": "module",
6
6
  "bin": {