@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.
Files changed (2) hide show
  1. package/dist/cli.js +296 -45
  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,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.cwd && { cwd: options.cwd },
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
- callbacks.onSessionCreated({
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: (sys.slash_commands ?? []).map((cmd) => ({ name: cmd, description: "" })),
279
+ slashCommands: initCommands,
156
280
  skills: sys.skills ?? [],
157
281
  mcpServers: sys.mcp_servers ?? [],
158
282
  model: sys.model ?? "",
159
- cwd: sys.cwd ?? "",
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
- cwd: process.cwd(),
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 = 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");
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 readdir(CLAUDE_DIR);
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 = join(CLAUDE_DIR, projectDir, sessionFile);
492
+ const sessionPath = join2(CLAUDE_DIR, projectDir, sessionFile);
355
493
  try {
356
- const content = await readFile(sessionPath, "utf-8");
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 readdir(CLAUDE_DIR);
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 = join(CLAUDE_DIR, projectDir);
518
+ const projectPath = join2(CLAUDE_DIR, projectDir);
381
519
  let files;
382
520
  try {
383
- files = await readdir(projectPath);
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
- project: projectDir,
394
- 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)
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, cwd) {
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: cwd || process.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
- 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)");
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, cwd, images, options } = msg;
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, cwd, images, options },
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 blocked");
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, cwd } = msg;
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, cwd);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punkcode/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Control Claude Code from your phone",
5
5
  "type": "module",
6
6
  "bin": {