@punkcode/cli 0.1.4 → 0.1.6

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 +165 -65
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -92,6 +92,7 @@ function runClaude(options, callbacks) {
92
92
  };
93
93
  }
94
94
  (async () => {
95
+ let sentCompactSummary = false;
95
96
  try {
96
97
  for await (const message of q) {
97
98
  switch (message.type) {
@@ -118,7 +119,19 @@ function runClaude(options, callbacks) {
118
119
  }
119
120
  case "user": {
120
121
  const userContent = message.message?.content;
121
- if (Array.isArray(userContent)) {
122
+ if (typeof userContent === "string") {
123
+ const match = userContent.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
124
+ if (match) {
125
+ if (sentCompactSummary && match[1].trim() === "Compacted") {
126
+ sentCompactSummary = false;
127
+ } else {
128
+ callbacks.onSlashCommandOutput?.(match[1]);
129
+ }
130
+ } else if (userContent.startsWith("This session is being continued")) {
131
+ sentCompactSummary = true;
132
+ callbacks.onSlashCommandOutput?.(userContent);
133
+ }
134
+ } else if (Array.isArray(userContent)) {
122
135
  for (const block of userContent) {
123
136
  if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_result") {
124
137
  const tr = block;
@@ -132,10 +145,35 @@ function runClaude(options, callbacks) {
132
145
  }
133
146
  break;
134
147
  }
135
- case "result": {
136
- callbacks.onResult(message.session_id);
148
+ case "system": {
149
+ const sys = message;
150
+ if (sys.subtype === "init" && callbacks.onSessionCreated) {
151
+ callbacks.onSessionCreated({
152
+ sessionId: sys.session_id ?? "",
153
+ tools: sys.tools ?? [],
154
+ slashCommands: (sys.slash_commands ?? []).map((cmd) => ({ name: cmd, description: "" })),
155
+ skills: sys.skills ?? [],
156
+ mcpServers: sys.mcp_servers ?? [],
157
+ model: sys.model ?? "",
158
+ cwd: sys.cwd ?? "",
159
+ claudeCodeVersion: sys.claude_code_version ?? "",
160
+ permissionMode: sys.permissionMode ?? "default"
161
+ });
162
+ }
137
163
  break;
138
164
  }
165
+ case "tool_use_summary": {
166
+ const summary = message.summary;
167
+ if (summary) {
168
+ callbacks.onSlashCommandOutput?.(summary);
169
+ }
170
+ break;
171
+ }
172
+ case "result": {
173
+ const resultText = message.subtype === "success" ? message.result : void 0;
174
+ callbacks.onResult(message.session_id, resultText);
175
+ return;
176
+ }
139
177
  }
140
178
  }
141
179
  } catch (err) {
@@ -180,11 +218,14 @@ function getOrCreateDeviceId() {
180
218
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
181
219
  return id;
182
220
  }
183
- function collectDeviceInfo(deviceId) {
221
+ function collectDeviceInfo(deviceId, customName) {
222
+ if (customName) {
223
+ saveConfigField("deviceName", customName);
224
+ }
184
225
  const cpus = os.cpus();
185
226
  return {
186
227
  deviceId,
187
- name: getDeviceName(),
228
+ name: customName || getDeviceName(),
188
229
  platform: process.platform,
189
230
  arch: process.arch,
190
231
  username: os.userInfo().username,
@@ -198,6 +239,11 @@ function collectDeviceInfo(deviceId) {
198
239
  };
199
240
  }
200
241
  function getDeviceName() {
242
+ try {
243
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
244
+ if (config.deviceName) return config.deviceName;
245
+ } catch {
246
+ }
201
247
  if (process.platform === "darwin") {
202
248
  try {
203
249
  const { stdout } = execaSync("scutil", ["--get", "ComputerName"], { timeout: 3e3 });
@@ -208,6 +254,16 @@ function getDeviceName() {
208
254
  }
209
255
  return os.hostname();
210
256
  }
257
+ function saveConfigField(key, value) {
258
+ fs.mkdirSync(PUNK_DIR, { recursive: true });
259
+ let config = {};
260
+ try {
261
+ config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
262
+ } catch {
263
+ }
264
+ config[key] = value;
265
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
266
+ }
211
267
  function parseBattery() {
212
268
  try {
213
269
  if (process.platform === "darwin") {
@@ -282,6 +338,9 @@ import { readdir, readFile, stat, open } from "fs/promises";
282
338
  import { join } from "path";
283
339
  import { homedir } from "os";
284
340
  var CLAUDE_DIR = join(homedir(), ".claude", "projects");
341
+ function pathToProjectDir(dir) {
342
+ return dir.replace(/\//g, "-");
343
+ }
285
344
  async function loadSession(sessionId) {
286
345
  const sessionFile = `${sessionId}.jsonl`;
287
346
  let projectDirs;
@@ -304,10 +363,14 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
304
363
  function isValidSessionUUID(name) {
305
364
  return UUID_RE.test(name);
306
365
  }
307
- async function listSessions() {
366
+ async function listSessions(workingDirectory) {
308
367
  let projectDirs;
309
368
  try {
310
- projectDirs = await readdir(CLAUDE_DIR);
369
+ if (workingDirectory) {
370
+ projectDirs = [pathToProjectDir(workingDirectory)];
371
+ } else {
372
+ projectDirs = await readdir(CLAUDE_DIR);
373
+ }
311
374
  } catch {
312
375
  return [];
313
376
  }
@@ -335,16 +398,12 @@ async function listSessions() {
335
398
  candidates.map(async (c) => {
336
399
  try {
337
400
  const fileStat = await stat(c.filePath);
338
- const [titleInfo, turnCount] = await Promise.all([
339
- extractTitle(c.filePath),
340
- countTurns(c.filePath)
341
- ]);
401
+ const titleInfo = await extractTitle(c.filePath);
342
402
  return {
343
403
  sessionId: c.sessionId,
344
404
  project: c.project,
345
405
  title: titleInfo.title,
346
406
  lastModified: fileStat.mtimeMs,
347
- turnCount,
348
407
  ...titleInfo.summary && { summary: titleInfo.summary }
349
408
  };
350
409
  } catch {
@@ -411,7 +470,9 @@ async function extractFirstUserMessage(fh) {
411
470
  try {
412
471
  const entry = JSON.parse(line);
413
472
  if (entry.isMeta || entry.parentUuid && metaUuids.has(entry.parentUuid)) {
414
- if (entry.uuid) metaUuids.add(entry.uuid);
473
+ if (entry.uuid && entry.message?.role !== "assistant") {
474
+ metaUuids.add(entry.uuid);
475
+ }
415
476
  continue;
416
477
  }
417
478
  if (entry.type === "user" && entry.message?.role === "user") {
@@ -421,9 +482,9 @@ async function extractFirstUserMessage(fh) {
421
482
  }
422
483
  if (Array.isArray(content)) {
423
484
  const textBlock = content.find(
424
- (b) => b.type === "text" && b.text && !b.text.startsWith("[Request interrupted")
485
+ (b) => b.type === "text" && "text" in b && b.text && !b.text.startsWith("[Request interrupted")
425
486
  );
426
- if (textBlock?.text) {
487
+ if (textBlock && "text" in textBlock && typeof textBlock.text === "string") {
427
488
  return textBlock.text.slice(0, 100);
428
489
  }
429
490
  }
@@ -433,41 +494,9 @@ async function extractFirstUserMessage(fh) {
433
494
  }
434
495
  return "Untitled session";
435
496
  }
436
- async function countTurns(filePath) {
437
- const content = await readFile(filePath, "utf-8");
438
- const lines = content.split("\n");
439
- let count = 0;
440
- const metaUuids = /* @__PURE__ */ new Set();
441
- for (const line of lines) {
442
- if (!line.trim()) continue;
443
- try {
444
- const entry = JSON.parse(line);
445
- if (entry.isMeta || entry.parentUuid && metaUuids.has(entry.parentUuid)) {
446
- if (entry.uuid) metaUuids.add(entry.uuid);
447
- continue;
448
- }
449
- if (entry.type !== "user" || !entry.message || entry.message.role !== "user") continue;
450
- const msgContent = entry.message.content;
451
- if (typeof msgContent === "string") {
452
- const trimmed = msgContent.trim();
453
- if (trimmed && !trimmed.startsWith("<") && !trimmed.startsWith("[") && !trimmed.startsWith("/")) {
454
- count++;
455
- }
456
- } else if (Array.isArray(msgContent)) {
457
- const textBlock = msgContent.find(
458
- (b) => b.type === "text" && b.text
459
- );
460
- if (textBlock?.text) {
461
- const text = textBlock.text.trim();
462
- if (text && !text.startsWith("<") && !text.startsWith("[") && !text.startsWith("/")) {
463
- count++;
464
- }
465
- }
466
- }
467
- } catch {
468
- }
469
- }
470
- return count;
497
+ var ANSI_RE = /\u001b\[\d*m/g;
498
+ function stripAnsi(text) {
499
+ return text.replace(ANSI_RE, "");
471
500
  }
472
501
  function parseSessionFile(content) {
473
502
  const messages = [];
@@ -477,7 +506,51 @@ function parseSessionFile(content) {
477
506
  try {
478
507
  const entry = JSON.parse(line);
479
508
  if (entry.isMeta || entry.parentUuid && metaUuids.has(entry.parentUuid)) {
480
- if (entry.uuid) metaUuids.add(entry.uuid);
509
+ if (entry.uuid && entry.message?.role !== "assistant") {
510
+ metaUuids.add(entry.uuid);
511
+ }
512
+ if (entry.message?.role === "user" && typeof entry.message.content === "string") {
513
+ const content2 = entry.message.content;
514
+ const cmdMatch = content2.match(/<command-name>\/(.+?)<\/command-name>/);
515
+ if (cmdMatch) {
516
+ messages.push({
517
+ role: "user",
518
+ content: [{ type: "text", text: `/${cmdMatch[1]}` }],
519
+ timestamp: entry.timestamp,
520
+ isMeta: entry.isMeta,
521
+ uuid: entry.uuid,
522
+ parentUuid: entry.parentUuid,
523
+ type: entry.type
524
+ });
525
+ }
526
+ const outMatch = content2.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
527
+ if (outMatch && outMatch[1].trim()) {
528
+ const text = stripAnsi(outMatch[1].trim());
529
+ if (text) {
530
+ messages.push({
531
+ role: "assistant",
532
+ content: [{ type: "text", text }],
533
+ timestamp: entry.timestamp,
534
+ isMeta: entry.isMeta,
535
+ uuid: entry.uuid,
536
+ parentUuid: entry.parentUuid,
537
+ type: entry.type
538
+ });
539
+ }
540
+ }
541
+ }
542
+ continue;
543
+ }
544
+ if (entry.type === "system" && entry.subtype === "compact_boundary") {
545
+ messages.push({
546
+ role: "assistant",
547
+ content: [],
548
+ timestamp: entry.timestamp,
549
+ type: "system",
550
+ subtype: entry.subtype,
551
+ uuid: entry.uuid,
552
+ parentUuid: entry.parentUuid
553
+ });
481
554
  continue;
482
555
  }
483
556
  if ((entry.type === "user" || entry.type === "assistant") && entry.message) {
@@ -485,7 +558,10 @@ function parseSessionFile(content) {
485
558
  messages.push({
486
559
  role: entry.message.role,
487
560
  content: typeof msgContent === "string" ? [{ type: "text", text: msgContent }] : msgContent,
488
- timestamp: entry.timestamp
561
+ timestamp: entry.timestamp,
562
+ uuid: entry.uuid,
563
+ parentUuid: entry.parentUuid,
564
+ type: entry.type
489
565
  });
490
566
  }
491
567
  } catch {
@@ -497,12 +573,25 @@ function parseSessionFile(content) {
497
573
  const blocks = msg.content;
498
574
  if (!Array.isArray(blocks)) continue;
499
575
  for (const block of blocks) {
500
- if (block.type !== "tool_result" || !Array.isArray(block.content)) continue;
501
- const imgBlock = block.content.find(
502
- (b) => b?.type === "image" && b?.source?.type === "base64" && b?.source?.data
503
- );
504
- if (!imgBlock) continue;
505
- const dataUri = `data:${imgBlock.source.media_type};base64,${imgBlock.source.data}`;
576
+ if (block.type !== "tool_result") continue;
577
+ const content2 = block.content;
578
+ let dataUri;
579
+ if (Array.isArray(content2)) {
580
+ const imgBlock = content2.find(
581
+ (b) => b?.type === "image" && b?.source?.type === "base64" && b?.source?.data
582
+ );
583
+ if (imgBlock) {
584
+ dataUri = `data:${imgBlock.source.media_type};base64,${imgBlock.source.data}`;
585
+ }
586
+ }
587
+ let resultText;
588
+ if (typeof content2 === "string") {
589
+ resultText = content2 || void 0;
590
+ } else if (Array.isArray(content2)) {
591
+ const texts = content2.filter((b) => b?.type === "text" && typeof b?.text === "string").map((b) => b.text);
592
+ resultText = texts.join("\n") || void 0;
593
+ }
594
+ if (!dataUri && !resultText) continue;
506
595
  for (let j = i - 1; j >= 0; j--) {
507
596
  if (messages[j].role !== "assistant") continue;
508
597
  const aBlocks = messages[j].content;
@@ -510,7 +599,10 @@ function parseSessionFile(content) {
510
599
  const toolUse = aBlocks.find(
511
600
  (b) => b.type === "tool_use" && b.id === block.tool_use_id
512
601
  );
513
- if (toolUse) toolUse.imageUri = dataUri;
602
+ if (toolUse) {
603
+ if (dataUri) toolUse.imageUri = dataUri;
604
+ if (resultText) toolUse.result = resultText;
605
+ }
514
606
  break;
515
607
  }
516
608
  }
@@ -778,7 +870,7 @@ async function connect(server, options) {
778
870
  const activeSessions = /* @__PURE__ */ new Map();
779
871
  socket.on("connect", () => {
780
872
  logger.info("Connected");
781
- const deviceInfo = collectDeviceInfo(deviceId);
873
+ const deviceInfo = collectDeviceInfo(deviceId, options.name);
782
874
  socket.emit("register", deviceInfo, (response) => {
783
875
  if (response.success) {
784
876
  logger.info({ deviceId }, "Registered");
@@ -826,7 +918,7 @@ async function connect(server, options) {
826
918
  });
827
919
  socket.on("reconnect", (attemptNumber) => {
828
920
  logger.info({ attemptNumber }, "Reconnected");
829
- socket.emit("register", collectDeviceInfo(deviceId));
921
+ socket.emit("register", collectDeviceInfo(deviceId, options.name));
830
922
  });
831
923
  socket.on("connect_error", (err) => {
832
924
  logger.error({ err }, "Connection error");
@@ -870,9 +962,16 @@ function handlePrompt(socket, msg, activeSessions) {
870
962
  const handle = runClaude(
871
963
  { prompt: prompt2, sessionId, cwd, images, options },
872
964
  {
965
+ onSessionCreated: (info) => {
966
+ send(socket, "response", { type: "session_created", data: info, requestId: id });
967
+ log2.info({ sessionId: info.sessionId }, "New session created");
968
+ },
873
969
  onText: (text) => {
874
970
  send(socket, "response", { type: "text", text, requestId: id });
875
971
  },
972
+ onSlashCommandOutput: (output) => {
973
+ send(socket, "response", { type: "command_output", output, requestId: id });
974
+ },
876
975
  onThinking: (thinking) => {
877
976
  send(socket, "response", { type: "thinking", thinking, requestId: id });
878
977
  },
@@ -882,8 +981,8 @@ function handlePrompt(socket, msg, activeSessions) {
882
981
  onToolResult: (toolUseId, content, isError) => {
883
982
  send(socket, "response", { type: "tool_result", tool_use_id: toolUseId, content, is_error: isError, requestId: id });
884
983
  },
885
- onResult: (sid) => {
886
- send(socket, "response", { type: "result", session_id: sid, requestId: id });
984
+ onResult: (sid, result) => {
985
+ send(socket, "response", { type: "result", session_id: sid, ...result && { result }, requestId: id });
887
986
  activeSessions.delete(id);
888
987
  log2.info("Session done");
889
988
  },
@@ -917,8 +1016,9 @@ function handleCancel(id, activeSessions) {
917
1016
  }
918
1017
  async function handleListSessions(socket, msg) {
919
1018
  const { id } = msg;
1019
+ const workingDirectory = msg.workingDirectory ?? process.cwd();
920
1020
  logger.info("Listing sessions...");
921
- const sessions = await listSessions();
1021
+ const sessions = await listSessions(workingDirectory);
922
1022
  send(socket, "response", { type: "sessions_list", sessions, requestId: id });
923
1023
  logger.info({ count: sessions.length }, "Listed sessions");
924
1024
  }
@@ -1022,7 +1122,7 @@ function logout() {
1022
1122
 
1023
1123
  // src/commands/index.ts
1024
1124
  function registerCommands(program2) {
1025
- 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)").action(connect);
1125
+ 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);
1026
1126
  program2.command("login").description("Log in with your email and password").action(login);
1027
1127
  program2.command("logout").description("Log out and clear stored credentials").action(logout);
1028
1128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punkcode/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Control Claude Code from your phone",
5
5
  "type": "module",
6
6
  "bin": {