@punkcode/cli 0.1.13 → 0.1.15

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 +85 -144
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -493,13 +493,11 @@ function getModel() {
493
493
  }
494
494
 
495
495
  // src/lib/session.ts
496
- import { readdir as readdir2, readFile as readFile2, stat, open } from "fs/promises";
496
+ import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
497
497
  import { join as join2 } from "path";
498
498
  import { homedir as homedir2 } from "os";
499
+ import { listSessions as sdkListSessions } from "@anthropic-ai/claude-agent-sdk";
499
500
  var CLAUDE_DIR = join2(homedir2(), ".claude", "projects");
500
- function pathToProjectDir(dir) {
501
- return dir.replace(/\//g, "-");
502
- }
503
501
  async function loadSession(sessionId) {
504
502
  const sessionFile = `${sessionId}.jsonl`;
505
503
  let projectDirs;
@@ -547,143 +545,26 @@ async function attachSubagentData(messages, subagentsDir) {
547
545
  }
548
546
  }));
549
547
  }
550
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
551
- function isValidSessionUUID(name) {
552
- return UUID_RE.test(name);
553
- }
554
548
  async function listSessions(workingDirectory) {
555
- let projectDirs;
556
549
  try {
557
- if (workingDirectory) {
558
- projectDirs = [pathToProjectDir(workingDirectory)];
559
- } else {
560
- projectDirs = await readdir2(CLAUDE_DIR);
561
- }
550
+ const sdkSessions = await sdkListSessions({
551
+ ...workingDirectory && { dir: workingDirectory },
552
+ limit: 50
553
+ });
554
+ return sdkSessions.map((s) => ({
555
+ sessionId: s.sessionId,
556
+ project: workingDirectory ?? s.cwd ?? "",
557
+ title: s.summary,
558
+ lastModified: s.lastModified,
559
+ cwd: s.cwd,
560
+ gitBranch: s.gitBranch,
561
+ fileSize: s.fileSize
562
+ }));
562
563
  } catch {
563
564
  return [];
564
565
  }
565
- const candidates = [];
566
- for (const projectDir of projectDirs) {
567
- const projectPath = join2(CLAUDE_DIR, projectDir);
568
- let files;
569
- try {
570
- files = await readdir2(projectPath);
571
- } catch {
572
- continue;
573
- }
574
- for (const file of files) {
575
- if (!file.endsWith(".jsonl")) continue;
576
- const sessionId = file.replace(".jsonl", "");
577
- if (!isValidSessionUUID(sessionId)) continue;
578
- candidates.push({
579
- sessionId,
580
- // When a workingDirectory is provided, return the original path so it
581
- // matches the device's defaultWorkingDirectory on the mobile side.
582
- project: workingDirectory ?? projectDir,
583
- filePath: join2(projectPath, file)
584
- });
585
- }
586
- }
587
- const results = await Promise.all(
588
- candidates.map(async (c) => {
589
- try {
590
- const fileStat = await stat(c.filePath);
591
- const titleInfo = await extractTitle(c.filePath);
592
- return {
593
- sessionId: c.sessionId,
594
- project: c.project,
595
- title: titleInfo.title,
596
- lastModified: fileStat.mtimeMs,
597
- ...titleInfo.summary && { summary: titleInfo.summary }
598
- };
599
- } catch {
600
- return null;
601
- }
602
- })
603
- );
604
- const sessions = results.filter((s) => s !== null);
605
- sessions.sort((a, b) => b.lastModified - a.lastModified);
606
- return sessions.slice(0, 50);
607
- }
608
- var HEAD_READ_BYTES = 8192;
609
- var TAIL_READ_BYTES = 16384;
610
- async function extractTitle(filePath) {
611
- const fh = await open(filePath, "r");
612
- try {
613
- const tailResult = await extractFromTail(fh, filePath);
614
- if (tailResult) return tailResult;
615
- const firstMsg = await extractFirstUserMessage(fh);
616
- return { title: firstMsg };
617
- } finally {
618
- await fh.close();
619
- }
620
- }
621
- async function extractFromTail(fh, filePath) {
622
- const fileStat = await stat(filePath);
623
- const fileSize = fileStat.size;
624
- if (fileSize === 0) return null;
625
- const readSize = Math.min(TAIL_READ_BYTES, fileSize);
626
- const offset = fileSize - readSize;
627
- const buf = Buffer.alloc(readSize);
628
- const { bytesRead } = await fh.read(buf, 0, readSize, offset);
629
- const chunk = buf.toString("utf-8", 0, bytesRead);
630
- const lines = chunk.split("\n");
631
- let customTitle = null;
632
- let summary = null;
633
- for (let i = lines.length - 1; i >= 0; i--) {
634
- const line = lines[i].trim();
635
- if (!line) continue;
636
- try {
637
- const entry = JSON.parse(line);
638
- if (entry.type === "custom-title" && entry.title && !customTitle) {
639
- customTitle = entry.title;
640
- }
641
- if (entry.type === "summary" && entry.summary && !summary) {
642
- summary = typeof entry.summary === "string" ? entry.summary : null;
643
- }
644
- if (customTitle && summary) break;
645
- } catch {
646
- }
647
- }
648
- if (customTitle) return { title: customTitle, summary: summary ?? void 0 };
649
- if (summary) return { title: summary, summary };
650
- return null;
651
- }
652
- async function extractFirstUserMessage(fh) {
653
- const buf = Buffer.alloc(HEAD_READ_BYTES);
654
- const { bytesRead } = await fh.read(buf, 0, HEAD_READ_BYTES, 0);
655
- const chunk = buf.toString("utf-8", 0, bytesRead);
656
- const lines = chunk.split("\n");
657
- const metaUuids = /* @__PURE__ */ new Set();
658
- for (const line of lines.slice(0, 20)) {
659
- if (!line.trim()) continue;
660
- try {
661
- const entry = JSON.parse(line);
662
- if (entry.isMeta || entry.parentUuid && metaUuids.has(entry.parentUuid)) {
663
- if (entry.uuid && entry.message?.role !== "assistant") {
664
- metaUuids.add(entry.uuid);
665
- }
666
- continue;
667
- }
668
- if (entry.type === "user" && entry.message?.role === "user") {
669
- const content = entry.message.content;
670
- if (typeof content === "string" && content.trim()) {
671
- return content.trim().slice(0, 100);
672
- }
673
- if (Array.isArray(content)) {
674
- const textBlock = content.find(
675
- (b) => b.type === "text" && "text" in b && b.text && !b.text.startsWith("[Request interrupted")
676
- );
677
- if (textBlock && "text" in textBlock && typeof textBlock.text === "string") {
678
- return textBlock.text.slice(0, 100);
679
- }
680
- }
681
- }
682
- } catch {
683
- }
684
- }
685
- return "Untitled session";
686
566
  }
567
+ var TOOL_RESULT_PREVIEW_BYTES = 2048;
687
568
  var ANSI_RE = /\u001b\[\d*m/g;
688
569
  function stripAnsi(text) {
689
570
  return text.replace(ANSI_RE, "");
@@ -692,6 +573,7 @@ function parseSessionFile(content) {
692
573
  const messages = [];
693
574
  const lines = content.split("\n").filter((line) => line.trim());
694
575
  const metaUuids = /* @__PURE__ */ new Set();
576
+ const taskNotifications = /* @__PURE__ */ new Map();
695
577
  for (const line of lines) {
696
578
  try {
697
579
  const entry = JSON.parse(line);
@@ -761,6 +643,21 @@ function parseSessionFile(content) {
761
643
  }
762
644
  continue;
763
645
  }
646
+ if (entry.type === "user" && typeof entry.message?.content === "string") {
647
+ const raw = entry.message.content;
648
+ const notifMatch = raw.match(/<task-notification>([\s\S]*?)<\/task-notification>/);
649
+ if (notifMatch) {
650
+ const inner = notifMatch[1];
651
+ const toolUseId = inner.match(/<tool-use-id>(.*?)<\/tool-use-id>/)?.[1];
652
+ if (toolUseId) {
653
+ const taskId = inner.match(/<task-id>(.*?)<\/task-id>/)?.[1] ?? "";
654
+ const status = inner.match(/<status>(.*?)<\/status>/)?.[1] ?? "completed";
655
+ const summary = inner.match(/<summary>([\s\S]*?)<\/summary>/)?.[1]?.trim() ?? "";
656
+ taskNotifications.set(toolUseId, { taskId, status, summary });
657
+ }
658
+ continue;
659
+ }
660
+ }
764
661
  if ((entry.type === "user" || entry.type === "assistant") && entry.message) {
765
662
  const msgContent = entry.message.content;
766
663
  messages.push({
@@ -809,12 +706,27 @@ function parseSessionFile(content) {
809
706
  );
810
707
  if (toolUse) {
811
708
  if (dataUri) toolUse.imageUri = dataUri;
812
- if (resultText) toolUse.result = resultText;
709
+ if (resultText) toolUse.result = resultText.length > TOOL_RESULT_PREVIEW_BYTES ? resultText.slice(0, TOOL_RESULT_PREVIEW_BYTES) + "\u2026" : resultText;
813
710
  break;
814
711
  }
815
712
  }
816
713
  }
817
714
  }
715
+ if (taskNotifications.size > 0) {
716
+ for (const msg of messages) {
717
+ if (msg.role !== "assistant") continue;
718
+ const blocks = msg.content;
719
+ if (!Array.isArray(blocks)) continue;
720
+ for (const block of blocks) {
721
+ if (block.type !== "tool_use" || block.name !== "Task") continue;
722
+ const notif = taskNotifications.get(block.id);
723
+ if (notif) {
724
+ block.taskStatus = notif.status;
725
+ block.taskSummary = notif.summary;
726
+ }
727
+ }
728
+ }
729
+ }
818
730
  const merged = [];
819
731
  for (const msg of messages) {
820
732
  if (msg.role === "user") {
@@ -1261,7 +1173,8 @@ async function connect(server, options) {
1261
1173
  reconnection: true,
1262
1174
  reconnectionAttempts: Infinity,
1263
1175
  reconnectionDelay: 1e3,
1264
- reconnectionDelayMax: 5e3
1176
+ reconnectionDelayMax: 5e3,
1177
+ perMessageDeflate: { threshold: 1024 }
1265
1178
  });
1266
1179
  const activeSessions = /* @__PURE__ */ new Map();
1267
1180
  const sleepLock = preventIdleSleep();
@@ -1361,6 +1274,9 @@ async function connect(server, options) {
1361
1274
  logger.error(detail, `Connection error: ${reason}`);
1362
1275
  logger.debug({ err }, "Connection error (raw)");
1363
1276
  });
1277
+ socket.on("error", (err) => {
1278
+ logger.error({ err }, "Socket error");
1279
+ });
1364
1280
  const refreshInterval = setInterval(async () => {
1365
1281
  try {
1366
1282
  const token = await refreshIdToken();
@@ -1439,6 +1355,7 @@ function formatConnectionError(err) {
1439
1355
  return { reason, ...result };
1440
1356
  }
1441
1357
  function send(socket, event, msg) {
1358
+ if (!socket.connected) return;
1442
1359
  socket.emit(event, msg);
1443
1360
  }
1444
1361
  function handlePrompt(socket, msg, activeSessions) {
@@ -1518,14 +1435,38 @@ async function handleListSessions(socket, msg, defaultCwd) {
1518
1435
  send(socket, "response", { type: "sessions_list", sessions, requestId: id });
1519
1436
  logger.info({ count: sessions.length }, "Listed sessions");
1520
1437
  }
1438
+ var DEFAULT_HISTORY_LIMIT = 30;
1439
+ var MAX_PAYLOAD_BYTES = 19.5 * 1024 * 1024;
1440
+ function fitToPayloadLimit(messages) {
1441
+ if (Buffer.byteLength(JSON.stringify(messages), "utf8") <= MAX_PAYLOAD_BYTES) {
1442
+ return messages;
1443
+ }
1444
+ let lo = 0;
1445
+ let hi = messages.length;
1446
+ while (lo < hi) {
1447
+ const mid = Math.floor((lo + hi + 1) / 2);
1448
+ const slice = messages.slice(messages.length - mid);
1449
+ if (Buffer.byteLength(JSON.stringify(slice), "utf8") <= MAX_PAYLOAD_BYTES) {
1450
+ lo = mid;
1451
+ } else {
1452
+ hi = mid - 1;
1453
+ }
1454
+ }
1455
+ return messages.slice(messages.length - lo);
1456
+ }
1521
1457
  async function handleLoadSession(socket, msg) {
1522
- const { id, sessionId } = msg;
1458
+ const { id, sessionId, limit = DEFAULT_HISTORY_LIMIT } = msg;
1523
1459
  const log2 = createChildLogger({ sessionId });
1524
1460
  log2.info("Loading session...");
1525
- const messages = await loadSession(sessionId);
1526
- if (messages) {
1527
- send(socket, "response", { type: "history", messages, requestId: id });
1528
- log2.info({ count: messages.length }, "Session loaded");
1461
+ const all = await loadSession(sessionId);
1462
+ if (all) {
1463
+ const sliced = limit > 0 && all.length > limit ? all.slice(-limit) : all;
1464
+ const messages = fitToPayloadLimit(sliced);
1465
+ if (messages.length < sliced.length) {
1466
+ log2.warn({ requested: sliced.length, sent: messages.length }, "Session payload trimmed to fit size limit");
1467
+ }
1468
+ send(socket, "response", { type: "history", messages, total: all.length, requestId: id });
1469
+ log2.info({ count: messages.length, total: all.length }, "Session loaded");
1529
1470
  } else {
1530
1471
  send(socket, "response", { type: "session_not_found", session_id: sessionId, requestId: id });
1531
1472
  log2.warn("Session not found");
@@ -1627,9 +1568,9 @@ function handleCheckPath(socket, msg) {
1627
1568
  let isDirectory = false;
1628
1569
  let parentExists = false;
1629
1570
  try {
1630
- const stat2 = fs3.statSync(checkTarget);
1571
+ const stat = fs3.statSync(checkTarget);
1631
1572
  exists = true;
1632
- isDirectory = stat2.isDirectory();
1573
+ isDirectory = stat.isDirectory();
1633
1574
  } catch {
1634
1575
  }
1635
1576
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punkcode/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Control Claude Code from your phone",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,7 +53,7 @@
53
53
  "vitest": "^4.0.18"
54
54
  },
55
55
  "dependencies": {
56
- "@anthropic-ai/claude-agent-sdk": "^0.2.49",
56
+ "@anthropic-ai/claude-agent-sdk": "^0.2.62",
57
57
  "@anthropic-ai/sdk": "^0.78.0",
58
58
  "commander": "^14.0.3",
59
59
  "execa": "^9.6.1",