lark-remote 0.1.6 → 0.1.8

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 (216) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +71 -34
  5. package/dist/bridge/approval-coordinator.js +225 -41
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +97 -13
  9. package/dist/bridge/index.js +227 -87
  10. package/dist/bridge/queue-manager.d.ts +2 -1
  11. package/dist/bridge/queue-manager.js +1 -0
  12. package/dist/card/approval-render.js +294 -36
  13. package/dist/card/bash-card-session.d.ts +2 -24
  14. package/dist/card/bash-card-session.js +6 -73
  15. package/dist/card/bash-renderer.js +2 -3
  16. package/dist/card/card-budget.js +29 -75
  17. package/dist/card/card-session.d.ts +48 -0
  18. package/dist/card/card-session.js +86 -0
  19. package/dist/card/card-shared.d.ts +7 -16
  20. package/dist/card/card-shared.js +7 -30
  21. package/dist/card/run-card-session.d.ts +2 -55
  22. package/dist/card/run-card-session.js +6 -104
  23. package/dist/card/run-renderer.d.ts +7 -0
  24. package/dist/card/run-renderer.js +116 -238
  25. package/dist/card/run-state.d.ts +13 -0
  26. package/dist/card/run-state.js +25 -16
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/index.d.ts +112 -24
  40. package/dist/config/index.js +139 -37
  41. package/dist/connector/index.d.ts +69 -2
  42. package/dist/connector/index.js +107 -1
  43. package/dist/index.js +150 -118
  44. package/dist/instance-lock.js +4 -12
  45. package/dist/order/alias-resolve.d.ts +13 -0
  46. package/dist/order/alias-resolve.js +22 -0
  47. package/dist/order/alias-store.d.ts +38 -0
  48. package/dist/order/alias-store.js +106 -0
  49. package/dist/persistence/atomic-write.d.ts +8 -2
  50. package/dist/persistence/atomic-write.js +23 -7
  51. package/dist/restart.js +2 -1
  52. package/dist/router/card-action-payload.d.ts +30 -0
  53. package/dist/router/card-action-payload.js +39 -0
  54. package/dist/router/card-helpers.d.ts +63 -0
  55. package/dist/router/card-helpers.js +146 -0
  56. package/dist/router/config/claude.js +30 -1
  57. package/dist/router/config/codex.js +29 -50
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +5 -4
  61. package/dist/router/config/dsh.d.ts +51 -0
  62. package/dist/router/config/dsh.js +179 -0
  63. package/dist/router/config/index.d.ts +12 -0
  64. package/dist/router/config/index.js +28 -1
  65. package/dist/router/config/kimi.d.ts +4 -1
  66. package/dist/router/config/kimi.js +27 -0
  67. package/dist/router/config/opencode.js +18 -5
  68. package/dist/router/config/pi.js +4 -5
  69. package/dist/router/config/types.d.ts +17 -2
  70. package/dist/router/index.d.ts +76 -22
  71. package/dist/router/index.js +479 -548
  72. package/dist/router/utils.d.ts +2 -0
  73. package/dist/router/utils.js +16 -2
  74. package/dist/runner/agent-choices-common.d.ts +16 -0
  75. package/dist/runner/agent-choices-common.js +34 -0
  76. package/dist/runner/claude/runner.d.ts +47 -10
  77. package/dist/runner/claude/runner.js +117 -46
  78. package/dist/runner/claude/session.d.ts +166 -0
  79. package/dist/runner/claude/session.js +768 -0
  80. package/dist/runner/codex/app-server/protocol-types.d.ts +66 -39
  81. package/dist/runner/codex/app-server/protocol-types.js +2 -13
  82. package/dist/runner/codex/app-server/runner.d.ts +52 -59
  83. package/dist/runner/codex/app-server/runner.js +216 -338
  84. package/dist/runner/codex/app-server/translator.d.ts +8 -21
  85. package/dist/runner/codex/app-server/translator.js +45 -18
  86. package/dist/runner/codex/index.d.ts +0 -2
  87. package/dist/runner/codex/index.js +0 -2
  88. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  89. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  90. package/dist/runner/common/acp/protocol-types.d.ts +271 -0
  91. package/dist/runner/common/acp/protocol-types.js +77 -0
  92. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  93. package/dist/runner/common/connection-based-runner.js +261 -0
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  95. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  96. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  97. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  98. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  99. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  100. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  101. package/dist/runner/common/jsonrpc/types.js +9 -0
  102. package/dist/runner/common/process-stopper.d.ts +2 -1
  103. package/dist/runner/common/process-stopper.js +1 -0
  104. package/dist/runner/common/runner-utils.d.ts +6 -13
  105. package/dist/runner/common/runner-utils.js +6 -47
  106. package/dist/runner/common/spawning-runner.d.ts +24 -70
  107. package/dist/runner/common/spawning-runner.js +98 -152
  108. package/dist/runner/dsh/client.d.ts +95 -0
  109. package/dist/runner/dsh/client.js +418 -0
  110. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  111. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  112. package/dist/runner/dsh/index.d.ts +3 -0
  113. package/dist/runner/dsh/index.js +4 -0
  114. package/dist/runner/dsh/runner.d.ts +57 -0
  115. package/dist/runner/dsh/runner.js +191 -0
  116. package/dist/runner/dsh/translator.d.ts +28 -0
  117. package/dist/runner/dsh/translator.js +98 -0
  118. package/dist/runner/dsh/types.d.ts +155 -0
  119. package/dist/runner/dsh/types.js +14 -0
  120. package/dist/runner/index.d.ts +5 -4
  121. package/dist/runner/index.js +5 -4
  122. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  123. package/dist/runner/kimi/acp/runner.js +516 -0
  124. package/dist/runner/kimi/acp/translator.d.ts +138 -0
  125. package/dist/runner/kimi/acp/translator.js +441 -0
  126. package/dist/runner/kimi/index.d.ts +1 -1
  127. package/dist/runner/kimi/index.js +1 -1
  128. package/dist/runner/opencode/acp/index.d.ts +1 -0
  129. package/dist/runner/opencode/acp/index.js +2 -0
  130. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  131. package/dist/runner/opencode/acp/runner.js +429 -0
  132. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  133. package/dist/runner/opencode/acp/translator.js +345 -0
  134. package/dist/runner/opencode/index.d.ts +1 -1
  135. package/dist/runner/opencode/index.js +1 -1
  136. package/dist/runner/pi/index.d.ts +1 -1
  137. package/dist/runner/pi/index.js +1 -1
  138. package/dist/runner/pi/rpc/client.d.ts +52 -0
  139. package/dist/runner/pi/rpc/client.js +114 -0
  140. package/dist/runner/pi/rpc/index.d.ts +4 -0
  141. package/dist/runner/pi/rpc/index.js +4 -0
  142. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  143. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  144. package/dist/runner/pi/rpc/runner.d.ts +74 -0
  145. package/dist/runner/pi/rpc/runner.js +190 -0
  146. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  147. package/dist/runner/pi/rpc/translator.js +258 -0
  148. package/dist/runner/probe.js +6 -1
  149. package/dist/runner/question-common.d.ts +26 -0
  150. package/dist/runner/question-common.js +42 -0
  151. package/dist/runner/registry.d.ts +7 -8
  152. package/dist/runner/registry.js +3 -4
  153. package/dist/runner/resolve-agent-choices.js +18 -59
  154. package/dist/runner/sync-agent-choices.js +16 -67
  155. package/dist/runner/types.d.ts +97 -14
  156. package/dist/session/claude/session-index.js +2 -3
  157. package/dist/session/claude/session-reader.js +3 -6
  158. package/dist/session/claude/sessions.js +43 -28
  159. package/dist/session/codex/rollout-reader.d.ts +2 -2
  160. package/dist/session/codex/rollout-reader.js +14 -17
  161. package/dist/session/codex/sessions.d.ts +1 -1
  162. package/dist/session/codex/sessions.js +2 -2
  163. package/dist/session/common/content-blocks.d.ts +7 -0
  164. package/dist/session/common/content-blocks.js +13 -9
  165. package/dist/session/common/jsonl.d.ts +8 -25
  166. package/dist/session/common/jsonl.js +126 -261
  167. package/dist/session/common/pagination.d.ts +28 -0
  168. package/dist/session/common/pagination.js +33 -0
  169. package/dist/session/dsh/index.d.ts +1 -0
  170. package/dist/session/dsh/index.js +2 -0
  171. package/dist/session/dsh/sessions.d.ts +39 -0
  172. package/dist/session/dsh/sessions.js +237 -0
  173. package/dist/session/index.d.ts +1 -0
  174. package/dist/session/index.js +1 -0
  175. package/dist/session/kimi/sessions.d.ts +34 -1
  176. package/dist/session/kimi/sessions.js +208 -98
  177. package/dist/session/opencode/sessions.d.ts +17 -3
  178. package/dist/session/opencode/sessions.js +29 -29
  179. package/dist/session/pi/sessions.d.ts +5 -1
  180. package/dist/session/pi/sessions.js +12 -16
  181. package/dist/session/session-store.d.ts +11 -1
  182. package/dist/session/session-store.js +75 -178
  183. package/dist/update/index.d.ts +2 -2
  184. package/dist/update/index.js +2 -2
  185. package/dist/update/install.d.ts +20 -2
  186. package/dist/update/install.js +51 -7
  187. package/dist/workspace/index.d.ts +4 -0
  188. package/dist/workspace/index.js +8 -1
  189. package/package.json +2 -1
  190. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  191. package/dist/runner/codex/argv.d.ts +0 -33
  192. package/dist/runner/codex/argv.js +0 -40
  193. package/dist/runner/codex/factory.d.ts +0 -29
  194. package/dist/runner/codex/factory.js +0 -39
  195. package/dist/runner/codex/jsonl.d.ts +0 -37
  196. package/dist/runner/codex/jsonl.js +0 -221
  197. package/dist/runner/codex/runner.d.ts +0 -66
  198. package/dist/runner/codex/runner.js +0 -129
  199. package/dist/runner/common/exec-translator.d.ts +0 -73
  200. package/dist/runner/common/exec-translator.js +0 -105
  201. package/dist/runner/kimi/jsonl.d.ts +0 -28
  202. package/dist/runner/kimi/jsonl.js +0 -143
  203. package/dist/runner/kimi/runner.d.ts +0 -49
  204. package/dist/runner/kimi/runner.js +0 -105
  205. package/dist/runner/opencode/argv.d.ts +0 -32
  206. package/dist/runner/opencode/argv.js +0 -35
  207. package/dist/runner/opencode/jsonl.d.ts +0 -68
  208. package/dist/runner/opencode/jsonl.js +0 -295
  209. package/dist/runner/opencode/runner.d.ts +0 -61
  210. package/dist/runner/opencode/runner.js +0 -93
  211. package/dist/runner/pi/jsonl.d.ts +0 -128
  212. package/dist/runner/pi/jsonl.js +0 -249
  213. package/dist/runner/pi/runner.d.ts +0 -36
  214. package/dist/runner/pi/runner.js +0 -76
  215. package/dist/session/common/catch-up.d.ts +0 -12
  216. package/dist/session/common/catch-up.js +0 -31
@@ -1,66 +1,135 @@
1
1
  import fs from 'node:fs';
2
2
  /**
3
- * Find the first line in a JSONL file that matches a predicate using streaming reads.
4
- * Stops reading as soon as a match is found.
3
+ * Stream a JSONL file line-by-line using chunked reads.
5
4
  *
6
- * @param filePath Absolute path to a `.jsonl` file.
7
- * @param pred Predicate applied to each trimmed non-empty line.
8
- * @returns The matching trimmed line, or null if no match / file not found / read error.
5
+ * Opens the file, reads in 64KB chunks, splits on `\n`, and invokes `onLine`
6
+ * for each non-empty trimmed line. Handles files without a trailing newline.
7
+ *
8
+ * `onLine` may return `true` to **stop early** (no further reads); the file
9
+ * descriptor is always closed in a `finally` block.
10
+ *
11
+ * @returns `true` if the file was opened and scanned, `false` on open error.
9
12
  */
10
- export function findJsonlLine(filePath, pred) {
13
+ function forEachJsonlLine(filePath, onLine) {
14
+ return forEachJsonlLineCore(filePath, { onLine });
15
+ }
16
+ /**
17
+ * Variant of `forEachJsonlLine` that also tracks **byte offsets** for each line.
18
+ *
19
+ * `onLine` receives `(line, offset)` where `offset` is the starting byte offset
20
+ * of the line within the file. May return `true` to stop early.
21
+ *
22
+ * @returns `true` if the file was opened and scanned, `false` on open error.
23
+ */
24
+ function forEachJsonlLineWithOffset(filePath, onLine) {
25
+ return forEachJsonlLineCore(filePath, {
26
+ onLine: (line, offset) => onLine(line, offset ?? 0),
27
+ trackOffsets: true,
28
+ });
29
+ }
30
+ /**
31
+ * Variant of `forEachJsonlLine` that starts reading from a byte `offset`.
32
+ *
33
+ * @param filePath Absolute path to a `.jsonl` file.
34
+ * @param offset Byte offset to seek to before reading. **Must fall on a line
35
+ * boundary**; a mid-line offset will include a leading partial line.
36
+ * @param onLine Called for each non-empty trimmed line. May return `true` to stop early.
37
+ * @returns `true` if the file was opened and scanned, `false` on open error.
38
+ */
39
+ function forEachJsonlLineFromOffset(filePath, offset, onLine) {
40
+ if (offset < 0)
41
+ return false;
42
+ return forEachJsonlLineCore(filePath, { startOffset: offset, onLine });
43
+ }
44
+ /**
45
+ * Core chunked JSONL reader shared by the three public variants. Reads the
46
+ * file in 64KB chunks, splitting on `\n` at the byte level (UTF-8 multi-byte
47
+ * characters never contain 0x0A, so this never truncates a multi-byte seq).
48
+ *
49
+ * @param opts.startOffset Byte offset to seek to before reading (0 = start).
50
+ * @param opts.trackOffsets When true, pass each line's starting byte offset to onLine.
51
+ * @param opts.onLine Called per non-empty trimmed line; may return `true` to stop.
52
+ */
53
+ function forEachJsonlLineCore(filePath, opts) {
11
54
  let fd;
12
55
  try {
13
56
  fd = fs.openSync(filePath, 'r');
14
57
  }
15
58
  catch {
16
- return null;
59
+ return false;
17
60
  }
18
- const chunk = Buffer.alloc(65536);
61
+ const CHUNK_SIZE = 65536;
62
+ const chunk = Buffer.alloc(CHUNK_SIZE);
63
+ // Raw byte buffer holding the not-yet-decoded tail (everything after the
64
+ // last consumed `\n`). UTF-8 multi-byte characters never contain 0x0A,
65
+ // so splitting on `\n` at the byte level never truncates a multi-byte seq.
19
66
  let buf = Buffer.alloc(0);
67
+ let position = opts.startOffset ?? 0;
68
+ let baseOffset = 0;
69
+ let stop = false;
20
70
  try {
21
71
  for (;;) {
22
- const n = fs.readSync(fd, chunk, 0, chunk.length, null);
23
- if (n === 0)
72
+ const nread = fs.readSync(fd, chunk, 0, CHUNK_SIZE, position);
73
+ if (nread === 0)
24
74
  break;
25
- buf = Buffer.concat([buf, chunk.subarray(0, n)]);
26
- // Find the last newline so we only decode complete lines. UTF-8 multi-byte
27
- // characters never contain the 0x0A byte, so splitting on `\n` at the raw
28
- // byte level is always safe and never truncates a multi-byte sequence.
75
+ buf = Buffer.concat([buf, chunk.subarray(0, nread)]);
29
76
  let nlIdx;
30
77
  while ((nlIdx = buf.lastIndexOf(0x0a)) !== -1) {
31
- // Decode everything before the last `\n` and split into lines.
32
- const text = buf.subarray(0, nlIdx).toString('utf-8');
33
- // Keep the tail after the last `\n` as the new buffer.
78
+ const complete = buf.subarray(0, nlIdx);
79
+ const text = complete.toString('utf-8');
34
80
  buf = buf.subarray(nlIdx + 1);
35
81
  const parts = text.split('\n');
36
- // All elements except the last are guaranteed complete (they had their
37
- // own trailing `\n`). The last element is whatever followed the final
38
- // `\n` in `text`; it may be empty or a partial line — re-emit it as part
39
- // of the next buffer by prepending it back. But since we sliced on the
40
- // *last* newline in buf, everything before it is fully complete lines
41
- // joined by `\n`; the trailing fragment is already excluded by keeping
42
- // only buf.subarray(nlIdx+1). So `parts` here is the complete lines,
43
- // and `parts[parts.length-1]` is the final segment before the last `\n`
44
- // — which IS complete (the last `\n` we split on was its terminator).
82
+ let cursor = (opts.startOffset ?? 0) + baseOffset;
45
83
  for (const part of parts) {
46
84
  const trimmed = part.trim();
47
- if (trimmed && pred(trimmed)) {
48
- return trimmed;
85
+ if (trimmed) {
86
+ if (opts.onLine(trimmed, opts.trackOffsets ? cursor : undefined) === true) {
87
+ stop = true;
88
+ break;
89
+ }
49
90
  }
91
+ cursor += Buffer.byteLength(part, 'utf-8') + 1;
50
92
  }
51
- break;
93
+ baseOffset += complete.length + 1;
94
+ break; // only process up to last complete newline per chunk
52
95
  }
96
+ if (stop)
97
+ break;
98
+ position += nread;
53
99
  }
54
- // Handle last remaining line (file without trailing newline).
55
- const trimmed = buf.toString('utf-8').trim();
56
- if (trimmed && pred(trimmed)) {
57
- return trimmed;
100
+ // Handle remaining tail (file without trailing newline).
101
+ if (!stop) {
102
+ const tail = buf.toString('utf-8').trim();
103
+ if (tail) {
104
+ opts.onLine(tail, opts.trackOffsets ? (opts.startOffset ?? 0) + baseOffset : undefined);
105
+ }
58
106
  }
59
107
  }
60
108
  finally {
61
109
  fs.closeSync(fd);
62
110
  }
63
- return null;
111
+ return true;
112
+ }
113
+ // ===========================================================================
114
+ // Public API
115
+ // ===========================================================================
116
+ /**
117
+ * Find the first line in a JSONL file that matches a predicate using streaming reads.
118
+ * Stops reading as soon as a match is found.
119
+ *
120
+ * @param filePath Absolute path to a `.jsonl` file.
121
+ * @param pred Predicate applied to each trimmed non-empty line.
122
+ * @returns The matching trimmed line, or null if no match / file not found / read error.
123
+ */
124
+ export function findJsonlLine(filePath, pred) {
125
+ let found = null;
126
+ forEachJsonlLine(filePath, (line) => {
127
+ if (pred(line)) {
128
+ found = line;
129
+ return true; // stop early
130
+ }
131
+ });
132
+ return found;
64
133
  }
65
134
  /**
66
135
  * Read the last non-empty line from a JSONL file using streaming reads.
@@ -69,58 +138,17 @@ export function findJsonlLine(filePath, pred) {
69
138
  * @returns The last trimmed non-empty line, or null if the file is empty / not found.
70
139
  */
71
140
  export function readLastJsonlLine(filePath) {
72
- let fd;
73
- try {
74
- fd = fs.openSync(filePath, 'r');
75
- }
76
- catch {
77
- return null;
78
- }
79
- const chunk = Buffer.alloc(65536);
80
- let buf = Buffer.alloc(0);
81
141
  let last = null;
82
- try {
83
- for (;;) {
84
- const n = fs.readSync(fd, chunk, 0, chunk.length, null);
85
- if (n === 0)
86
- break;
87
- buf = Buffer.concat([buf, chunk.subarray(0, n)]);
88
- let nlIdx;
89
- while ((nlIdx = buf.lastIndexOf(0x0a)) !== -1) {
90
- const text = buf.subarray(0, nlIdx).toString('utf-8');
91
- buf = buf.subarray(nlIdx + 1);
92
- const parts = text.split('\n');
93
- for (const part of parts) {
94
- const trimmed = part.trim();
95
- if (trimmed) {
96
- last = trimmed;
97
- }
98
- }
99
- break;
100
- }
101
- }
102
- const trimmed = buf.toString('utf-8').trim();
103
- if (trimmed) {
104
- last = trimmed;
105
- }
106
- }
107
- finally {
108
- fs.closeSync(fd);
109
- }
142
+ forEachJsonlLine(filePath, (line) => {
143
+ last = line;
144
+ });
110
145
  return last;
111
146
  }
112
147
  /**
113
148
  * Read the last N non-empty lines from a JSONL file using streaming reads.
114
149
  *
115
150
  * Unlike `readJsonlLines` (which materializes the entire file into a `string[]`),
116
- * this only retains the last `n` lines — memory is O(n), not O(whole file). The
117
- * file is still read front-to-back in chunks (no reverse seek), but earlier
118
- * lines are discarded as soon as the buffer exceeds `n`, so a multi-MB file
119
- * costs only ~`n` line strings of resident memory.
120
- *
121
- * Used by tail-only read paths (P2-5) such as `isSessionActive`, which only
122
- * need to inspect the file's tail (e.g. the last ~20 loop events) but were
123
- * previously full-slurping the whole wire log.
151
+ * this only retains the last `n` lines — memory is O(n), not O(whole file).
124
152
  *
125
153
  * @param filePath Absolute path to a `.jsonl` file.
126
154
  * @param n Maximum number of trailing non-empty lines to retain.
@@ -130,222 +158,59 @@ export function readLastJsonlLine(filePath) {
130
158
  export function readLastNJsonlLines(filePath, n) {
131
159
  if (n <= 0)
132
160
  return [];
133
- let fd;
134
- try {
135
- fd = fs.openSync(filePath, 'r');
136
- }
137
- catch {
138
- return [];
139
- }
140
- const chunk = Buffer.alloc(65536);
141
- let buf = Buffer.alloc(0);
142
161
  const lines = [];
143
- try {
144
- for (;;) {
145
- const nread = fs.readSync(fd, chunk, 0, chunk.length, null);
146
- if (nread === 0)
147
- break;
148
- buf = Buffer.concat([buf, chunk.subarray(0, nread)]);
149
- let nlIdx;
150
- while ((nlIdx = buf.lastIndexOf(0x0a)) !== -1) {
151
- const text = buf.subarray(0, nlIdx).toString('utf-8');
152
- buf = buf.subarray(nlIdx + 1);
153
- const parts = text.split('\n');
154
- for (const part of parts) {
155
- if (part.trim()) {
156
- lines.push(part);
157
- if (lines.length > n) {
158
- lines.splice(0, lines.length - n);
159
- }
160
- }
161
- }
162
- break;
163
- }
162
+ forEachJsonlLine(filePath, (line) => {
163
+ lines.push(line);
164
+ if (lines.length > n) {
165
+ lines.splice(0, lines.length - n);
164
166
  }
165
- const tail = buf.toString('utf-8');
166
- if (tail.trim()) {
167
- lines.push(tail);
168
- if (lines.length > n) {
169
- lines.splice(0, lines.length - n);
170
- }
171
- }
172
- }
173
- finally {
174
- fs.closeSync(fd);
175
- }
167
+ });
176
168
  return lines;
177
169
  }
178
170
  /**
179
171
  * Stream-scan a JSONL file once, invoking `onLine` for each non-empty line
180
172
  * with the line's raw string and its **starting byte offset** within the file.
181
173
  *
182
- * Unlike `readJsonlLines` this retains NOTHING — memory is O(chunk), not
183
- * O(whole file). The callback receives the byte offset so callers can record
184
- * where a line of interest (e.g. the last user message) begins, then re-read
185
- * only the tail from that offset via `readJsonlLinesFromOffset` (P2-5
186
- * byte-offset two-pass: full-file scalar scan without materializing a
187
- * `string[]`, then tail-only re-parse for events).
174
+ * Memory is O(chunk), not O(whole file). The callback receives the byte offset
175
+ * so callers can record where a line of interest begins, then re-read only the
176
+ * tail from that offset via `readJsonlLinesFromOffset`.
188
177
  *
189
178
  * @param filePath Absolute path to a `.jsonl` file.
190
- * @param onLine Called for each non-empty line with `(line, offset)` where
191
- * `offset` is the byte offset of the line's first character.
179
+ * @param onLine Called for each non-empty line with `(line, offset)`.
192
180
  * @returns `true` if the file was opened and scanned, `false` on open error.
193
181
  */
194
182
  export function scanJsonlLines(filePath, onLine) {
195
- let fd;
196
- try {
197
- fd = fs.openSync(filePath, 'r');
198
- }
199
- catch {
200
- return false;
201
- }
202
- const chunk = Buffer.alloc(65536);
203
- // Raw byte buffer holding the not-yet-decoded tail (everything after the
204
- // last consumed `\n`). Byte offsets are tracked against the real file, so
205
- // they are immune to multi-byte UTF-8 decode artifacts.
206
- let buf = Buffer.alloc(0);
207
- let baseOffset = 0;
208
- try {
209
- for (;;) {
210
- const nread = fs.readSync(fd, chunk, 0, chunk.length, null);
211
- if (nread === 0)
212
- break;
213
- buf = Buffer.concat([buf, chunk.subarray(0, nread)]);
214
- // Decode all complete lines (everything up to the last `\n`). The tail
215
- // after the last `\n` stays in `buf` for the next iteration.
216
- let nlIdx;
217
- while ((nlIdx = buf.lastIndexOf(0x0a)) !== -1) {
218
- const complete = buf.subarray(0, nlIdx);
219
- const text = complete.toString('utf-8');
220
- buf = buf.subarray(nlIdx + 1);
221
- const parts = text.split('\n');
222
- let cursor = baseOffset;
223
- for (const part of parts) {
224
- if (part.trim()) {
225
- onLine(part, cursor);
226
- }
227
- // advance past this line + its '\n' (1 byte)
228
- cursor += Buffer.byteLength(part, 'utf-8') + 1;
229
- }
230
- baseOffset += complete.length + 1;
231
- break;
232
- }
233
- }
234
- const tail = buf.toString('utf-8');
235
- if (tail.trim()) {
236
- onLine(tail, baseOffset);
237
- }
238
- }
239
- finally {
240
- fs.closeSync(fd);
241
- }
242
- return true;
183
+ return forEachJsonlLineWithOffset(filePath, onLine);
243
184
  }
244
185
  /**
245
186
  * Read all non-empty lines starting from a byte `offset` in a JSONL file.
246
187
  *
247
- * Companion to `scanJsonlLines` for the P2-5 byte-offset two-pass pattern:
248
- * pass 1 records the offset where the tail begins (e.g. the byte right after
249
- * the last user message); pass 2 seeks there and reads/parses only the tail,
250
- * so raw line-string memory is O(tail) instead of O(whole file).
188
+ * Companion to `scanJsonlLines` for the byte-offset two-pass pattern.
251
189
  *
252
190
  * @param filePath Absolute path to a `.jsonl` file.
253
191
  * @param offset Byte offset to seek to before reading. **Must fall on a line
254
- * boundary** (e.g. 0, or the start of the line after the last
255
- * user message as recorded by `scanJsonlLines`); a mid-line
256
- * offset will include a leading partial line.
192
+ * boundary**; a mid-line offset will include a leading partial line.
257
193
  * @returns Array of raw line strings from `offset` to EOF (empty lines
258
194
  * excluded). Empty array on open/seek error or `offset` past EOF.
259
195
  */
260
196
  export function readJsonlLinesFromOffset(filePath, offset) {
261
- if (offset < 0)
262
- return [];
263
- let fd;
264
- try {
265
- fd = fs.openSync(filePath, 'r');
266
- }
267
- catch {
268
- return [];
269
- }
270
- const chunk = Buffer.alloc(65536);
271
- let buf = Buffer.alloc(0);
272
197
  const lines = [];
273
- try {
274
- let position = offset;
275
- for (;;) {
276
- const nread = fs.readSync(fd, chunk, 0, chunk.length, position);
277
- if (nread === 0)
278
- break;
279
- buf = Buffer.concat([buf, chunk.subarray(0, nread)]);
280
- let nlIdx;
281
- while ((nlIdx = buf.lastIndexOf(0x0a)) !== -1) {
282
- const text = buf.subarray(0, nlIdx).toString('utf-8');
283
- buf = buf.subarray(nlIdx + 1);
284
- const parts = text.split('\n');
285
- for (const part of parts) {
286
- if (part.trim()) {
287
- lines.push(part);
288
- }
289
- }
290
- break;
291
- }
292
- position += nread;
293
- }
294
- const tail = buf.toString('utf-8');
295
- if (tail.trim()) {
296
- lines.push(tail);
297
- }
298
- }
299
- finally {
300
- fs.closeSync(fd);
301
- }
198
+ forEachJsonlLineFromOffset(filePath, offset, (line) => {
199
+ lines.push(line);
200
+ });
302
201
  return lines;
303
202
  }
304
203
  /**
305
204
  * Read all non-empty lines from a JSONL file using streaming reads.
306
205
  *
307
206
  * @param filePath Absolute path to a `.jsonl` file.
308
- * @param chunkSize Buffer size for each `fs.readSync` call (default 65536).
309
207
  * @returns Array of raw line strings (empty lines excluded).
310
208
  */
311
- export function readJsonlLines(filePath, chunkSize = 65536) {
312
- let fd;
313
- try {
314
- fd = fs.openSync(filePath, 'r');
315
- }
316
- catch {
317
- return [];
318
- }
319
- const chunk = Buffer.alloc(chunkSize);
320
- let buf = Buffer.alloc(0);
209
+ export function readJsonlLines(filePath) {
321
210
  const lines = [];
322
- try {
323
- for (;;) {
324
- const n = fs.readSync(fd, chunk, 0, chunk.length, null);
325
- if (n === 0)
326
- break;
327
- buf = Buffer.concat([buf, chunk.subarray(0, n)]);
328
- let nlIdx;
329
- while ((nlIdx = buf.lastIndexOf(0x0a)) !== -1) {
330
- const text = buf.subarray(0, nlIdx).toString('utf-8');
331
- buf = buf.subarray(nlIdx + 1);
332
- const parts = text.split('\n');
333
- for (const part of parts) {
334
- if (part.trim()) {
335
- lines.push(part);
336
- }
337
- }
338
- break;
339
- }
340
- }
341
- const tail = buf.toString('utf-8');
342
- if (tail.trim()) {
343
- lines.push(tail);
344
- }
345
- }
346
- finally {
347
- fs.closeSync(fd);
348
- }
211
+ forEachJsonlLine(filePath, (line) => {
212
+ lines.push(line);
213
+ });
349
214
  return lines;
350
215
  }
351
216
  //# sourceMappingURL=jsonl.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared pagination / truncation helpers for agent session readers.
3
+ *
4
+ * The five readers (claude/codex/opencode/pi/kimi) all implement the same
5
+ * offset-clamp + slice + total pattern for `listSessions`, and the same
6
+ * maxEvents tail-cap for `readSessionContent`. These were duplicated (and had
7
+ * drifted on the default limit). Centralized here so the contract is defined
8
+ * once: default page limit is 20, matching the majority of readers.
9
+ */
10
+ /** Default page size for listSessions. */
11
+ export declare const DEFAULT_LIST_LIMIT = 20;
12
+ /**
13
+ * Paginate a (already-ordered) full item list by offset + limit.
14
+ * Clamps negative offset to 0; caps limit to the remaining items.
15
+ */
16
+ export declare function paginate<T>(items: T[], opts: {
17
+ limit?: number;
18
+ offset?: number;
19
+ }): {
20
+ items: T[];
21
+ total: number;
22
+ };
23
+ /**
24
+ * Apply the maxEvents tail-cap shared by readSessionContent:
25
+ * keep the LAST N events; maxEvents <= 0 returns [] (guards the
26
+ * `slice(-0) === slice(0)` full-array trap).
27
+ */
28
+ export declare function capEvents<T>(events: T[], maxEvents?: number): T[];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared pagination / truncation helpers for agent session readers.
3
+ *
4
+ * The five readers (claude/codex/opencode/pi/kimi) all implement the same
5
+ * offset-clamp + slice + total pattern for `listSessions`, and the same
6
+ * maxEvents tail-cap for `readSessionContent`. These were duplicated (and had
7
+ * drifted on the default limit). Centralized here so the contract is defined
8
+ * once: default page limit is 20, matching the majority of readers.
9
+ */
10
+ /** Default page size for listSessions. */
11
+ export const DEFAULT_LIST_LIMIT = 20;
12
+ /**
13
+ * Paginate a (already-ordered) full item list by offset + limit.
14
+ * Clamps negative offset to 0; caps limit to the remaining items.
15
+ */
16
+ export function paginate(items, opts) {
17
+ const limit = opts.limit ?? DEFAULT_LIST_LIMIT;
18
+ const offset = Math.max(0, opts.offset ?? 0);
19
+ return { items: items.slice(offset, offset + limit), total: items.length };
20
+ }
21
+ /**
22
+ * Apply the maxEvents tail-cap shared by readSessionContent:
23
+ * keep the LAST N events; maxEvents <= 0 returns [] (guards the
24
+ * `slice(-0) === slice(0)` full-array trap).
25
+ */
26
+ export function capEvents(events, maxEvents) {
27
+ if (maxEvents === undefined || maxEvents > events.length)
28
+ return events;
29
+ if (maxEvents <= 0)
30
+ return [];
31
+ return events.slice(-maxEvents);
32
+ }
33
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1 @@
1
+ export { DshSessionReader } from './sessions.js';
@@ -0,0 +1,2 @@
1
+ export { DshSessionReader } from './sessions.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * DshSessionReader — AgentSessionReader backed by the DSH Web Host over HTTP.
3
+ *
4
+ * The AgentSessionReader contract is fully synchronous (file-style readers),
5
+ * but DSH exposes only async HTTP. The reader therefore bridges with
6
+ * `spawnSync('curl', ...)` for its unary calls (session.list / session.history)
7
+ * — a synchronous HTTP round-trip to the local DSH host. This keeps the
8
+ * router/bridge interface contract intact without touching any upstream logic.
9
+ *
10
+ * listSessions: session.list → filter by cwd → sort by updatedAt desc → local
11
+ * pagination (DSH v1 has no cursor pagination).
12
+ * readSessionContent: session.history → map SessionEvents to display events +
13
+ * session-wide usage accumulation (assistant/message.usage).
14
+ */
15
+ import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
16
+ interface DshSessionReaderOptions {
17
+ host?: string;
18
+ /** Injectable synchronous unary transport. Defaults to spawnSync curl. */
19
+ syncRequest?: (baseUrl: string, method: string, payload: unknown) => unknown;
20
+ }
21
+ export declare class DshSessionReader implements AgentSessionReader {
22
+ private readonly baseUrl;
23
+ private readonly syncRequest;
24
+ constructor(opts?: DshSessionReaderOptions);
25
+ private unary;
26
+ listSessions(cwd: string, opts?: {
27
+ limit?: number;
28
+ offset?: number;
29
+ }): {
30
+ sessions: AgentSession[];
31
+ total: number;
32
+ };
33
+ getNewestSession(cwd: string): AgentSession | null;
34
+ readSessionContent(sessionId: string, cwd: string, opts?: {
35
+ maxEvents?: number;
36
+ }): SessionContent;
37
+ isSessionActive(sessionId: string, cwd: string): boolean;
38
+ }
39
+ export {};