@code-yeongyu/senpi 2026.5.29-4 → 2026.6.2

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 (121) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/README.md +8 -2
  3. package/dist/cli/args.d.ts +1 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +13 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +9 -1
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-session.d.ts +5 -1
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +18 -2
  13. package/dist/core/agent-session.js.map +1 -1
  14. package/dist/core/compaction/branch-summarization.d.ts +3 -1
  15. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  16. package/dist/core/compaction/branch-summarization.js +9 -3
  17. package/dist/core/compaction/branch-summarization.js.map +1 -1
  18. package/dist/core/extensions/index.d.ts +1 -1
  19. package/dist/core/extensions/index.d.ts.map +1 -1
  20. package/dist/core/extensions/index.js.map +1 -1
  21. package/dist/core/extensions/runner.d.ts +4 -2
  22. package/dist/core/extensions/runner.d.ts.map +1 -1
  23. package/dist/core/extensions/runner.js +13 -1
  24. package/dist/core/extensions/runner.js.map +1 -1
  25. package/dist/core/extensions/types.d.ts +7 -1
  26. package/dist/core/extensions/types.d.ts.map +1 -1
  27. package/dist/core/extensions/types.js.map +1 -1
  28. package/dist/core/footer-data-provider.d.ts +2 -0
  29. package/dist/core/footer-data-provider.d.ts.map +1 -1
  30. package/dist/core/footer-data-provider.js +29 -1
  31. package/dist/core/footer-data-provider.js.map +1 -1
  32. package/dist/core/model-resolver.d.ts.map +1 -1
  33. package/dist/core/model-resolver.js +1 -0
  34. package/dist/core/model-resolver.js.map +1 -1
  35. package/dist/core/provider-attribution.d.ts +4 -0
  36. package/dist/core/provider-attribution.d.ts.map +1 -0
  37. package/dist/core/provider-attribution.js +73 -0
  38. package/dist/core/provider-attribution.js.map +1 -0
  39. package/dist/core/provider-display-names.d.ts.map +1 -1
  40. package/dist/core/provider-display-names.js +1 -0
  41. package/dist/core/provider-display-names.js.map +1 -1
  42. package/dist/core/sdk.d.ts.map +1 -1
  43. package/dist/core/sdk.js +8 -34
  44. package/dist/core/sdk.js.map +1 -1
  45. package/dist/core/session-manager.d.ts.map +1 -1
  46. package/dist/core/session-manager.js +92 -68
  47. package/dist/core/session-manager.js.map +1 -1
  48. package/dist/core/tools/edit.d.ts.map +1 -1
  49. package/dist/core/tools/edit.js +7 -10
  50. package/dist/core/tools/edit.js.map +1 -1
  51. package/dist/core/tools/find.d.ts.map +1 -1
  52. package/dist/core/tools/find.js.map +1 -1
  53. package/dist/core/tools/grep.d.ts.map +1 -1
  54. package/dist/core/tools/grep.js.map +1 -1
  55. package/dist/core/tools/ls.d.ts.map +1 -1
  56. package/dist/core/tools/ls.js +5 -7
  57. package/dist/core/tools/ls.js.map +1 -1
  58. package/dist/core/tools/read.d.ts.map +1 -1
  59. package/dist/core/tools/read.js +6 -7
  60. package/dist/core/tools/read.js.map +1 -1
  61. package/dist/core/tools/render-utils.d.ts +5 -2
  62. package/dist/core/tools/render-utils.d.ts.map +1 -1
  63. package/dist/core/tools/render-utils.js +17 -1
  64. package/dist/core/tools/render-utils.js.map +1 -1
  65. package/dist/core/tools/write.d.ts.map +1 -1
  66. package/dist/core/tools/write.js +5 -6
  67. package/dist/core/tools/write.js.map +1 -1
  68. package/dist/index.d.ts +2 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +2 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/main.d.ts.map +1 -1
  73. package/dist/main.js +8 -0
  74. package/dist/main.js.map +1 -1
  75. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  76. package/dist/modes/interactive/components/tool-execution.js +25 -1
  77. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  78. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  79. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  80. package/dist/modes/interactive/interactive-mode.js +39 -0
  81. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  82. package/dist/modes/print-mode.d.ts.map +1 -1
  83. package/dist/modes/print-mode.js +1 -0
  84. package/dist/modes/print-mode.js.map +1 -1
  85. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  86. package/dist/modes/rpc/rpc-mode.js +1 -0
  87. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  88. package/docs/extensions.md +35 -11
  89. package/docs/providers.md +2 -0
  90. package/docs/quickstart.md +1 -0
  91. package/docs/rpc.md +3 -2
  92. package/docs/session-format.md +1 -1
  93. package/docs/sessions.md +8 -0
  94. package/docs/settings.md +1 -1
  95. package/docs/terminal-setup.md +2 -0
  96. package/docs/tui.md +12 -3
  97. package/docs/usage.md +6 -1
  98. package/examples/extensions/custom-header.ts +1 -1
  99. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  100. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  101. package/examples/extensions/custom-provider-gitlab-duo/index.ts +53 -2
  102. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  103. package/examples/extensions/doom-overlay/index.ts +1 -1
  104. package/examples/extensions/handoff.ts +1 -1
  105. package/examples/extensions/interactive-shell.ts +1 -1
  106. package/examples/extensions/overlay-qa-tests.ts +152 -81
  107. package/examples/extensions/qna.ts +1 -1
  108. package/examples/extensions/question.ts +1 -1
  109. package/examples/extensions/questionnaire.ts +1 -1
  110. package/examples/extensions/sandbox/package-lock.json +2 -2
  111. package/examples/extensions/sandbox/package.json +1 -1
  112. package/examples/extensions/snake.ts +1 -1
  113. package/examples/extensions/space-invaders.ts +1 -1
  114. package/examples/extensions/summarize.ts +1 -1
  115. package/examples/extensions/tic-tac-toe.ts +1 -1
  116. package/examples/extensions/todo.ts +1 -1
  117. package/examples/extensions/tools.ts +5 -0
  118. package/examples/extensions/with-deps/package-lock.json +2 -2
  119. package/examples/extensions/with-deps/package.json +1 -1
  120. package/npm-shrinkwrap.json +12 -12
  121. package/package.json +4 -4
@@ -1,7 +1,9 @@
1
1
  import { randomBytes, randomUUID } from "crypto";
2
- import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, statSync, writeFileSync, } from "fs";
3
- import { readdir, readFile, stat } from "fs/promises";
2
+ import { appendFileSync, closeSync, createReadStream, existsSync, mkdirSync, openSync, readdirSync, readSync, statSync, writeFileSync, } from "fs";
3
+ import { readdir, stat } from "fs/promises";
4
4
  import { join, resolve } from "path";
5
+ import { createInterface } from "readline";
6
+ import { StringDecoder } from "string_decoder";
5
7
  import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
6
8
  import { normalizePath, resolvePath } from "../utils/paths.js";
7
9
  // Fork change: inlined UUIDv7 (upstream uses the `uuid` npm package). Keeps this
@@ -247,24 +249,52 @@ export function getDefaultSessionDir(cwd, agentDir = getDefaultAgentDir()) {
247
249
  }
248
250
  return sessionDir;
249
251
  }
252
+ const SESSION_READ_BUFFER_SIZE = 1024 * 1024;
253
+ function parseSessionEntryLine(line) {
254
+ if (!line.trim())
255
+ return null;
256
+ try {
257
+ return JSON.parse(line);
258
+ }
259
+ catch {
260
+ // Skip malformed lines
261
+ return null;
262
+ }
263
+ }
250
264
  /** Exported for testing */
251
265
  export function loadEntriesFromFile(filePath) {
252
266
  const resolvedFilePath = normalizePath(filePath);
253
267
  if (!existsSync(resolvedFilePath))
254
268
  return [];
255
- const content = readFileSync(resolvedFilePath, "utf8");
256
269
  const entries = [];
257
- const lines = content.trim().split("\n");
258
- for (const line of lines) {
259
- if (!line.trim())
260
- continue;
261
- try {
262
- const entry = JSON.parse(line);
263
- entries.push(entry);
264
- }
265
- catch {
266
- // Skip malformed lines
270
+ const fd = openSync(resolvedFilePath, "r");
271
+ try {
272
+ const decoder = new StringDecoder("utf8");
273
+ const buffer = Buffer.allocUnsafe(SESSION_READ_BUFFER_SIZE);
274
+ let pending = "";
275
+ while (true) {
276
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
277
+ if (bytesRead === 0)
278
+ break;
279
+ pending += decoder.write(buffer.subarray(0, bytesRead));
280
+ let lineStart = 0;
281
+ let newlineIndex = pending.indexOf("\n", lineStart);
282
+ while (newlineIndex !== -1) {
283
+ const entry = parseSessionEntryLine(pending.slice(lineStart, newlineIndex));
284
+ if (entry)
285
+ entries.push(entry);
286
+ lineStart = newlineIndex + 1;
287
+ newlineIndex = pending.indexOf("\n", lineStart);
288
+ }
289
+ pending = pending.slice(lineStart);
267
290
  }
291
+ pending += decoder.end();
292
+ const finalEntry = parseSessionEntryLine(pending);
293
+ if (finalEntry)
294
+ entries.push(finalEntry);
295
+ }
296
+ finally {
297
+ closeSync(fd);
268
298
  }
269
299
  // Validate session header
270
300
  if (entries.length === 0)
@@ -333,73 +363,53 @@ function extractTextContent(message) {
333
363
  .map((block) => block.text)
334
364
  .join(" ");
335
365
  }
336
- function getLastActivityTime(entries) {
337
- let lastActivityTime;
338
- for (const entry of entries) {
339
- if (entry.type !== "message")
340
- continue;
341
- const message = entry.message;
342
- if (!isMessageWithContent(message))
343
- continue;
344
- if (message.role !== "user" && message.role !== "assistant")
345
- continue;
346
- const msgTimestamp = message.timestamp;
347
- if (typeof msgTimestamp === "number") {
348
- lastActivityTime = Math.max(lastActivityTime ?? 0, msgTimestamp);
349
- continue;
350
- }
351
- const entryTimestamp = entry.timestamp;
352
- if (typeof entryTimestamp === "string") {
353
- const t = new Date(entryTimestamp).getTime();
354
- if (!Number.isNaN(t)) {
355
- lastActivityTime = Math.max(lastActivityTime ?? 0, t);
356
- }
357
- }
358
- }
359
- return lastActivityTime;
360
- }
361
- function getSessionModifiedDate(entries, header, statsMtime) {
362
- const lastActivityTime = getLastActivityTime(entries);
363
- if (typeof lastActivityTime === "number" && lastActivityTime > 0) {
364
- return new Date(lastActivityTime);
366
+ function getMessageActivityTime(entry) {
367
+ const message = entry.message;
368
+ if (!isMessageWithContent(message))
369
+ return undefined;
370
+ if (message.role !== "user" && message.role !== "assistant")
371
+ return undefined;
372
+ const msgTimestamp = message.timestamp;
373
+ if (typeof msgTimestamp === "number") {
374
+ return msgTimestamp;
365
375
  }
366
- const headerTime = typeof header.timestamp === "string" ? new Date(header.timestamp).getTime() : NaN;
367
- return !Number.isNaN(headerTime) ? new Date(headerTime) : statsMtime;
376
+ const t = new Date(entry.timestamp).getTime();
377
+ return Number.isNaN(t) ? undefined : t;
368
378
  }
369
379
  async function buildSessionInfo(filePath) {
370
380
  try {
371
- const content = await readFile(filePath, "utf8");
372
- const entries = [];
373
- const lines = content.trim().split("\n");
374
- for (const line of lines) {
375
- if (!line.trim())
376
- continue;
377
- try {
378
- entries.push(JSON.parse(line));
379
- }
380
- catch {
381
- // Skip malformed lines
382
- }
383
- }
384
- if (entries.length === 0)
385
- return null;
386
- const header = entries[0];
387
- if (header.type !== "session")
388
- return null;
389
381
  const stats = await stat(filePath);
382
+ let header = null;
390
383
  let messageCount = 0;
391
384
  let firstMessage = "";
392
385
  const allMessages = [];
393
386
  let name;
394
- for (const entry of entries) {
387
+ let lastActivityTime;
388
+ const rl = createInterface({
389
+ input: createReadStream(filePath, { encoding: "utf8" }),
390
+ crlfDelay: Infinity,
391
+ });
392
+ for await (const line of rl) {
393
+ const entry = parseSessionEntryLine(line);
394
+ if (!entry)
395
+ continue;
396
+ if (!header) {
397
+ if (entry.type !== "session")
398
+ return null;
399
+ header = entry;
400
+ continue;
401
+ }
395
402
  // Extract session name (use latest, including explicit clears)
396
403
  if (entry.type === "session_info") {
397
- const infoEntry = entry;
398
- name = infoEntry.name?.trim() || undefined;
404
+ name = entry.name?.trim() || undefined;
399
405
  }
400
406
  if (entry.type !== "message")
401
407
  continue;
402
408
  messageCount++;
409
+ const activityTime = getMessageActivityTime(entry);
410
+ if (typeof activityTime === "number") {
411
+ lastActivityTime = Math.max(lastActivityTime ?? 0, activityTime);
412
+ }
403
413
  const message = entry.message;
404
414
  if (!isMessageWithContent(message))
405
415
  continue;
@@ -413,9 +423,16 @@ async function buildSessionInfo(filePath) {
413
423
  firstMessage = textContent;
414
424
  }
415
425
  }
426
+ if (!header)
427
+ return null;
416
428
  const cwd = typeof header.cwd === "string" ? header.cwd : "";
417
429
  const parentSessionPath = header.parentSession;
418
- const modified = getSessionModifiedDate(entries, header, stats.mtime);
430
+ const headerTime = typeof header.timestamp === "string" ? new Date(header.timestamp).getTime() : NaN;
431
+ const modified = typeof lastActivityTime === "number" && lastActivityTime > 0
432
+ ? new Date(lastActivityTime)
433
+ : !Number.isNaN(headerTime)
434
+ ? new Date(headerTime)
435
+ : stats.mtime;
419
436
  return {
420
437
  path: filePath,
421
438
  id: header.id,
@@ -608,8 +625,15 @@ export class SessionManager {
608
625
  _rewriteFile() {
609
626
  if (!this.persist || !this.sessionFile)
610
627
  return;
611
- const content = `${this.fileEntries.map((e) => JSON.stringify(e)).join("\n")}\n`;
612
- writeFileSync(this.sessionFile, content);
628
+ const fd = openSync(this.sessionFile, "w");
629
+ try {
630
+ for (const entry of this.fileEntries) {
631
+ writeFileSync(fd, `${JSON.stringify(entry)}\n`);
632
+ }
633
+ }
634
+ finally {
635
+ closeSync(fd);
636
+ }
613
637
  }
614
638
  isPersisted() {
615
639
  return this.persist;