claude-simple 0.2.37 → 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.37.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.37.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.37",
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
 
@@ -315,7 +316,10 @@ async function findCodexRollouts(codexHome) {
315
316
 
316
317
  const CODEX_CATALOG_FULL_SCAN_MAX = 2 * 1024 * 1024;
317
318
  const CODEX_CATALOG_HEAD_BYTES = 256 * 1024;
318
- const CODEX_LINE_PARSE_MAX = 16 * 1024;
319
+ // session_meta includes git + base_instructions and is often 20–40KB.
320
+ // Multi-MB tool outputs still exceed this and are skipped.
321
+ const CODEX_LINE_PARSE_MAX = 256 * 1024;
322
+ const CODEX_TRANSCRIPT_CACHE_MAX = 64;
319
323
 
320
324
  const codexMetadata = new Map();
321
325
  async function readCodexMeta(filePath) {
@@ -443,7 +447,9 @@ export async function parseCodexMessages(filePath) {
443
447
  }
444
448
  codexTranscripts.delete(filePath);
445
449
  codexTranscripts.set(filePath, entry);
446
- 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
+ }
447
453
  if (entry.size === stat.size && entry.mtimeMs === stat.mtimeMs) return entry.messages;
448
454
  const loading = updateCodexTranscript(filePath, entry, stat);
449
455
  entry.loading = loading;
@@ -492,25 +498,16 @@ async function updateCodexTranscript(filePath, entry, stat) {
492
498
 
493
499
  try {
494
500
  if (stat.size <= entry.offset) return;
495
- const stream = fsSync.createReadStream(filePath, { start: entry.offset, end: stat.size - 1 });
496
- let pending = Buffer.alloc(0);
497
- for await (const chunk of stream) {
498
- pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
499
- let start = 0;
500
- let end;
501
- while ((end = pending.indexOf(10, start)) !== -1) {
502
- const lineBytes = end - start;
503
- entry.offset += lineBytes + 1;
504
- start = end + 1;
505
- // Tool outputs are multi-MB JSONL lines. AgentUI does not render them
506
- // and Hub must not receive them — skip without materializing the body.
507
- if (lineBytes > 64 * 1024) continue;
508
- 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) => {
509
506
  const head = line.slice(0, 400);
510
507
  if (!/"type"\s*:\s*"(?:response_item|event_msg)"/.test(head) ||
511
- !/"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;
512
509
  let d;
513
- try { d = JSON.parse(line); } catch { continue; }
510
+ try { d = JSON.parse(line); } catch { return; }
514
511
 
515
512
  if (d.type === 'event_msg' && d.payload) {
516
513
  const p = d.payload;
@@ -542,13 +539,13 @@ async function updateCodexTranscript(filePath, entry, stat) {
542
539
  .filter(Boolean);
543
540
  pushAssistant(texts.join('\n'), d.timestamp);
544
541
  } else if (p.type === 'function_call') {
545
- if (p.call_id && entry.seenCalls.has(p.call_id)) continue;
542
+ if (p.call_id && entry.seenCalls.has(p.call_id)) return;
546
543
  if (p.call_id) entry.seenCalls.add(p.call_id);
547
544
  let input = p.arguments;
548
545
  try { input = typeof input === 'string' ? JSON.parse(input) : input; } catch {}
549
546
  messages.push({ role: 'tool', type: 'tool_use', name: p.name || 'function', id: p.call_id, input: input ?? {}, ts: d.timestamp });
550
547
  } else if (p.type === 'command_execution' || p.type === 'custom_tool_call') {
551
- if (p.call_id && entry.seenCalls.has(p.call_id)) continue;
548
+ if (p.call_id && entry.seenCalls.has(p.call_id)) return;
552
549
  if (p.call_id) entry.seenCalls.add(p.call_id);
553
550
  let input = p.input;
554
551
  try { input = typeof input === 'string' ? JSON.parse(input) : input; } catch {}
@@ -568,9 +565,9 @@ async function updateCodexTranscript(filePath, entry, stat) {
568
565
  const text = d.payload.message || d.payload.text;
569
566
  pushAssistant(text, d.timestamp);
570
567
  }
571
- }
572
- pending = pending.subarray(start);
573
- }
568
+ },
569
+ });
570
+ entry.offset = scanned.offset;
574
571
  entry.size = stat.size;
575
572
  entry.mtimeMs = stat.mtimeMs;
576
573
  } catch (error) {
@@ -1920,15 +1917,11 @@ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, cod
1920
1917
  const budget = Math.min(Math.max(1000, Number(maxChars) || 24000), 64000);
1921
1918
  const lines = [];
1922
1919
  let n = 0, chars = 0, hasMore = false, clipped = false;
1923
- const stream = fsSync.createReadStream(file, { encoding: 'utf8' });
1924
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
1925
- try {
1926
- for await (const line of rl) {
1920
+ await scanJsonlFile(file, {
1921
+ onLine: (line) => {
1927
1922
  const index = n++;
1928
- if (!tail && index < start) continue;
1929
- if (!tail && (lines.length >= limit || chars >= budget)) { hasMore = true; break; }
1930
- // Bound huge tool output rows as well as total response size. Clipping
1931
- // 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; }
1932
1925
  const cap = tail ? budget : budget - chars;
1933
1926
  const text = line.slice(0, cap);
1934
1927
  if (text.length < line.length) clipped = true;
@@ -1936,8 +1929,8 @@ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, cod
1936
1929
  if (tail) while (lines.length > limit || (chars > budget && lines.length > 1)) {
1937
1930
  chars -= lines.shift().length;
1938
1931
  }
1939
- }
1940
- } finally { rl.close(); stream.destroy(); }
1932
+ },
1933
+ });
1941
1934
  const stat = await fs.stat(file).catch(() => null);
1942
1935
  const actualOffset = tail ? Math.max(0, n - lines.length) : start;
1943
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