claude-simple 0.2.38 → 0.2.39

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.
package/dist/index.html CHANGED
@@ -4,13 +4,13 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
6
6
  <title>Claude Code</title>
7
- <link rel="stylesheet" href="/style.0.2.38.min.css">
7
+ <link rel="stylesheet" href="/style.0.2.39.min.css">
8
8
  <link rel="stylesheet" href="/highlight.min.css">
9
9
  <script src="/marked.min.js"></script>
10
10
  <script src="/highlight.min.js"></script>
11
11
  </head>
12
12
  <body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
13
13
  <div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
14
- <script type="module" src="/app.0.2.38.min.js"></script>
14
+ <script type="module" src="/app.0.2.39.min.js"></script>
15
15
  </body>
16
16
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-simple",
3
- "version": "0.2.38",
3
+ "version": "0.2.39",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude-simple": "server.js",
@@ -5,6 +5,7 @@ import path from 'path';
5
5
  import os from 'os';
6
6
  import crypto from 'crypto';
7
7
  import { pathsEqual } from '../platform.js';
8
+ import { JSONL_LINE_PARSE_MAX, scanJsonlFile } from '../jsonl-scan.js';
8
9
 
9
10
  // ─── Claude ───────────────────────────────────────────────────────────────────
10
11
 
@@ -318,6 +319,7 @@ const CODEX_CATALOG_HEAD_BYTES = 256 * 1024;
318
319
  // session_meta includes git + base_instructions and is often 20–40KB.
319
320
  // Multi-MB tool outputs still exceed this and are skipped.
320
321
  const CODEX_LINE_PARSE_MAX = 256 * 1024;
322
+ const CODEX_TRANSCRIPT_CACHE_MAX = 64;
321
323
 
322
324
  const codexMetadata = new Map();
323
325
  async function readCodexMeta(filePath) {
@@ -445,7 +447,9 @@ export async function parseCodexMessages(filePath) {
445
447
  }
446
448
  codexTranscripts.delete(filePath);
447
449
  codexTranscripts.set(filePath, entry);
448
- while (codexTranscripts.size > 8) codexTranscripts.delete(codexTranscripts.keys().next().value);
450
+ while (codexTranscripts.size > CODEX_TRANSCRIPT_CACHE_MAX) {
451
+ codexTranscripts.delete(codexTranscripts.keys().next().value);
452
+ }
449
453
  if (entry.size === stat.size && entry.mtimeMs === stat.mtimeMs) return entry.messages;
450
454
  const loading = updateCodexTranscript(filePath, entry, stat);
451
455
  entry.loading = loading;
@@ -494,25 +498,16 @@ async function updateCodexTranscript(filePath, entry, stat) {
494
498
 
495
499
  try {
496
500
  if (stat.size <= entry.offset) return;
497
- const stream = fsSync.createReadStream(filePath, { start: entry.offset, end: stat.size - 1 });
498
- let pending = Buffer.alloc(0);
499
- for await (const chunk of stream) {
500
- pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
501
- let start = 0;
502
- let end;
503
- while ((end = pending.indexOf(10, start)) !== -1) {
504
- const lineBytes = end - start;
505
- entry.offset += lineBytes + 1;
506
- start = end + 1;
507
- // Tool outputs are multi-MB JSONL lines. AgentUI does not render them
508
- // and Hub must not receive them — skip without materializing the body.
509
- if (lineBytes > 64 * 1024) continue;
510
- const line = pending.subarray(start - lineBytes - 1, end).toString('utf8');
501
+ const scanned = await scanJsonlFile(filePath, {
502
+ start: entry.offset,
503
+ end: stat.size,
504
+ maxLineBytes: JSONL_LINE_PARSE_MAX,
505
+ onLine: (line) => {
511
506
  const head = line.slice(0, 400);
512
507
  if (!/"type"\s*:\s*"(?:response_item|event_msg)"/.test(head) ||
513
- !/"type"\s*:\s*"(?:message|agent_message|function_call|command_execution|custom_tool_call|task_started|task_complete|turn_aborted)"/.test(head)) continue;
508
+ !/"type"\s*:\s*"(?:message|agent_message|function_call|command_execution|custom_tool_call|task_started|task_complete|turn_aborted)"/.test(head)) return;
514
509
  let d;
515
- try { d = JSON.parse(line); } catch { continue; }
510
+ try { d = JSON.parse(line); } catch { return; }
516
511
 
517
512
  if (d.type === 'event_msg' && d.payload) {
518
513
  const p = d.payload;
@@ -544,13 +539,13 @@ async function updateCodexTranscript(filePath, entry, stat) {
544
539
  .filter(Boolean);
545
540
  pushAssistant(texts.join('\n'), d.timestamp);
546
541
  } else if (p.type === 'function_call') {
547
- if (p.call_id && entry.seenCalls.has(p.call_id)) continue;
542
+ if (p.call_id && entry.seenCalls.has(p.call_id)) return;
548
543
  if (p.call_id) entry.seenCalls.add(p.call_id);
549
544
  let input = p.arguments;
550
545
  try { input = typeof input === 'string' ? JSON.parse(input) : input; } catch {}
551
546
  messages.push({ role: 'tool', type: 'tool_use', name: p.name || 'function', id: p.call_id, input: input ?? {}, ts: d.timestamp });
552
547
  } else if (p.type === 'command_execution' || p.type === 'custom_tool_call') {
553
- if (p.call_id && entry.seenCalls.has(p.call_id)) continue;
548
+ if (p.call_id && entry.seenCalls.has(p.call_id)) return;
554
549
  if (p.call_id) entry.seenCalls.add(p.call_id);
555
550
  let input = p.input;
556
551
  try { input = typeof input === 'string' ? JSON.parse(input) : input; } catch {}
@@ -570,9 +565,9 @@ async function updateCodexTranscript(filePath, entry, stat) {
570
565
  const text = d.payload.message || d.payload.text;
571
566
  pushAssistant(text, d.timestamp);
572
567
  }
573
- }
574
- pending = pending.subarray(start);
575
- }
568
+ },
569
+ });
570
+ entry.offset = scanned.offset;
576
571
  entry.size = stat.size;
577
572
  entry.mtimeMs = stat.mtimeMs;
578
573
  } catch (error) {
@@ -1922,15 +1917,11 @@ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, cod
1922
1917
  const budget = Math.min(Math.max(1000, Number(maxChars) || 24000), 64000);
1923
1918
  const lines = [];
1924
1919
  let n = 0, chars = 0, hasMore = false, clipped = false;
1925
- const stream = fsSync.createReadStream(file, { encoding: 'utf8' });
1926
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
1927
- try {
1928
- for await (const line of rl) {
1920
+ await scanJsonlFile(file, {
1921
+ onLine: (line) => {
1929
1922
  const index = n++;
1930
- if (!tail && index < start) continue;
1931
- if (!tail && (lines.length >= limit || chars >= budget)) { hasMore = true; break; }
1932
- // Bound huge tool output rows as well as total response size. Clipping
1933
- // is explicit: a clipped line must not be mistaken for complete JSON.
1923
+ if (!tail && index < start) return;
1924
+ if (!tail && (lines.length >= limit || chars >= budget)) { hasMore = true; return; }
1934
1925
  const cap = tail ? budget : budget - chars;
1935
1926
  const text = line.slice(0, cap);
1936
1927
  if (text.length < line.length) clipped = true;
@@ -1938,8 +1929,8 @@ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, cod
1938
1929
  if (tail) while (lines.length > limit || (chars > budget && lines.length > 1)) {
1939
1930
  chars -= lines.shift().length;
1940
1931
  }
1941
- }
1942
- } finally { rl.close(); stream.destroy(); }
1932
+ },
1933
+ });
1943
1934
  const stat = await fs.stat(file).catch(() => null);
1944
1935
  const actualOffset = tail ? Math.max(0, n - lines.length) : start;
1945
1936
  return { agent: a, fileName: path.basename(file), offset: actualOffset, lines,
@@ -0,0 +1,71 @@
1
+ import fsSync from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+
4
+ export const JSONL_LINE_PARSE_MAX = 64 * 1024;
5
+
6
+ /**
7
+ * Sequential JSONL reader. Oversize records are skipped by searching for the
8
+ * next newline without keeping the body in a JS string.
9
+ */
10
+ export async function scanJsonlFile(filePath, {
11
+ start = 0,
12
+ end = null,
13
+ maxLineBytes = JSONL_LINE_PARSE_MAX,
14
+ onLine,
15
+ } = {}) {
16
+ const stat = await fs.stat(filePath);
17
+ const stop = end == null ? stat.size : Math.min(end, stat.size);
18
+ let offset = Math.max(0, start);
19
+ let skippedLines = 0;
20
+ if (offset >= stop) return { offset, skippedLines, size: stat.size };
21
+
22
+ const stream = fsSync.createReadStream(filePath, {
23
+ start: offset,
24
+ end: stop - 1,
25
+ highWaterMark: 64 * 1024,
26
+ });
27
+ let pending = Buffer.alloc(0);
28
+ let skip = false;
29
+ try {
30
+ for await (const chunk of stream) {
31
+ let buf = skip ? chunk : (pending.length ? Buffer.concat([pending, chunk]) : chunk);
32
+ pending = Buffer.alloc(0);
33
+ let pos = 0;
34
+ if (skip) {
35
+ const nl = buf.indexOf(10);
36
+ if (nl === -1) {
37
+ offset += buf.length;
38
+ continue;
39
+ }
40
+ offset += nl + 1;
41
+ skippedLines += 1;
42
+ skip = false;
43
+ pos = nl + 1;
44
+ }
45
+ while (pos < buf.length) {
46
+ const nl = buf.indexOf(10, pos);
47
+ if (nl === -1) break;
48
+ const len = nl - pos;
49
+ offset += len + 1;
50
+ if (len > maxLineBytes) {
51
+ skippedLines += 1;
52
+ } else if (typeof onLine === 'function') {
53
+ onLine(buf.subarray(pos, nl).toString('utf8'), { pendingBytes: buf.length, offset });
54
+ }
55
+ pos = nl + 1;
56
+ }
57
+ if (pos < buf.length) {
58
+ const leftover = buf.subarray(pos);
59
+ if (leftover.length > maxLineBytes) {
60
+ skip = true;
61
+ offset += leftover.length;
62
+ } else {
63
+ pending = leftover;
64
+ }
65
+ }
66
+ }
67
+ } finally {
68
+ stream.destroy();
69
+ }
70
+ return { offset, skippedLines, size: stat.size };
71
+ }
File without changes