@code-yeongyu/senpi 2026.11.15-2 → 2026.11.15-3

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 (132) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/bundle/chunks/{chunk-Q4RJ4WLE.js → chunk-225TDKTA.js} +4 -4
  3. package/dist/bundle/chunks/{chunk-QSU2456D.js → chunk-3KR3CDGM.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-AOTHPTMU.js → chunk-5GCZFK4X.js} +2 -2
  5. package/dist/bundle/chunks/{chunk-BMADOJHZ.js → chunk-5QIFH624.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-THYIV26N.js → chunk-GQEXLLWJ.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-3RILB2OD.js → chunk-OJOPOUAY.js} +9 -9
  8. package/dist/bundle/chunks/{chunk-64FJ4QQJ.js → chunk-PPCAJQDJ.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-74HCKLRH.js → chunk-TEXE4HT6.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-RRFDLGX6.js → chunk-TNYFCGI7.js} +156 -156
  11. package/dist/bundle/chunks/{cli-main-GNQY2LTU.js → cli-main-CV3ODX4A.js} +1 -1
  12. package/dist/bundle/chunks/{hand-entry-D3DNH6XL.js → hand-entry-2NN7ZTY6.js} +1 -1
  13. package/dist/bundle/chunks/{interactive-mode-ATBPY4M5.js → interactive-mode-TGHZWM2T.js} +1 -1
  14. package/dist/bundle/chunks/{main-OMFVSMUR.js → main-B7WEY4JW.js} +1 -1
  15. package/dist/bundle/chunks/{multi-session-host-ACQ5N4UB.js → multi-session-host-53YLL2OD.js} +1 -1
  16. package/dist/bundle/chunks/{package-manager-cli-2VMRV4HL.js → package-manager-cli-2U54AVKW.js} +1 -1
  17. package/dist/bundle/chunks/{role-BTH5PVME.js → role-4IXZZAL4.js} +1 -1
  18. package/dist/bundle/chunks/{rpc-mode-XUK5SYKH.js → rpc-mode-RRYXENML.js} +1 -1
  19. package/dist/bundle/chunks/{runtime-session-VO3X4OE3.js → runtime-session-KKHEYZOH.js} +1 -1
  20. package/dist/bundle/chunks/{session-control-endpoint-Y5EIFFPY.js → session-control-endpoint-HVULSIUR.js} +1 -1
  21. package/dist/bundle/chunks/session-worker.js +331 -331
  22. package/dist/bundle/cli.js +2 -2
  23. package/dist/bundle/index.js +1 -1
  24. package/dist/bundle/rpc-entry.js +1 -1
  25. package/dist/bundle/runtime-manifest.json +1 -1
  26. package/dist/core/agent-session.d.ts +6 -0
  27. package/dist/core/agent-session.js +34 -3
  28. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.d.ts +2 -0
  29. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +1 -0
  30. package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +2 -0
  31. package/dist/core/extensions/builtin/anthropic-subscription/failover.js +22 -4
  32. package/dist/core/extensions/builtin/anthropic-subscription/index.js +7 -2
  33. package/dist/core/extensions/builtin/anthropic-subscription/limit-recheck.d.ts +15 -0
  34. package/dist/core/extensions/builtin/anthropic-subscription/limit-recheck.js +55 -0
  35. package/dist/core/extensions/builtin/anthropic-subscription/session-stream.d.ts +1 -0
  36. package/dist/core/extensions/builtin/anthropic-subscription/session-stream.js +1 -0
  37. package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +8 -5
  38. package/dist/core/extensions/builtin/anthropic-subscription/stream.d.ts +1 -1
  39. package/dist/core/extensions/builtin/anthropic-subscription/stream.js +3 -1
  40. package/dist/core/extensions/builtin/terminal/extension-state.js +15 -0
  41. package/dist/core/extensions/builtin/terminal/notify.d.ts +2 -1
  42. package/dist/core/extensions/builtin/terminal/notify.js +15 -6
  43. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +5 -1
  44. package/dist/core/extensions/builtin/terminal/session-bundle.js +41 -1
  45. package/dist/core/extensions/builtin/terminal/tools/context.d.ts +4 -0
  46. package/dist/core/extensions/loader.js +5 -0
  47. package/dist/core/extensions/runner.d.ts +1 -0
  48. package/dist/core/extensions/runner.js +5 -0
  49. package/dist/core/extensions/types.d.ts +7 -0
  50. package/dist/core/run-background-jobs.d.ts +33 -0
  51. package/dist/core/run-background-jobs.js +47 -0
  52. package/dist/core/tools/bash-core.d.ts +50 -0
  53. package/dist/core/tools/bash-core.js +169 -0
  54. package/dist/core/tools/bash-schema.d.ts +48 -0
  55. package/dist/core/tools/bash-schema.js +34 -0
  56. package/dist/core/tools/bash.d.ts +4 -70
  57. package/dist/core/tools/bash.js +32 -409
  58. package/dist/core/tools/coding-tools-core.d.ts +24 -0
  59. package/dist/core/tools/coding-tools-core.js +14 -0
  60. package/dist/core/tools/edit-core.d.ts +56 -0
  61. package/dist/core/tools/edit-core.js +175 -0
  62. package/dist/core/tools/edit-diff.d.ts +0 -5
  63. package/dist/core/tools/edit-diff.js +0 -32
  64. package/dist/core/tools/edit-preview.d.ts +7 -0
  65. package/dist/core/tools/edit-preview.js +34 -0
  66. package/dist/core/tools/edit.d.ts +5 -43
  67. package/dist/core/tools/edit.js +10 -158
  68. package/dist/core/tools/encoding.d.ts +10 -0
  69. package/dist/core/tools/encoding.js +58 -0
  70. package/dist/core/tools/file-tool-host.d.ts +25 -0
  71. package/dist/core/tools/file-tool-host.js +109 -0
  72. package/dist/core/tools/fs-error-code.d.ts +2 -0
  73. package/dist/core/tools/fs-error-code.js +15 -0
  74. package/dist/core/tools/machine-output-spill.d.ts +11 -0
  75. package/dist/core/tools/machine-output-spill.js +65 -0
  76. package/dist/core/tools/machine.d.ts +4 -24
  77. package/dist/core/tools/machine.js +4 -44
  78. package/dist/core/tools/output-accumulator-core.d.ts +84 -0
  79. package/dist/core/tools/output-accumulator-core.js +200 -0
  80. package/dist/core/tools/output-accumulator.d.ts +9 -61
  81. package/dist/core/tools/output-accumulator.js +90 -265
  82. package/dist/core/tools/path-utils.js +3 -48
  83. package/dist/core/tools/posix-signals.d.ts +3 -0
  84. package/dist/core/tools/posix-signals.js +113 -0
  85. package/dist/core/tools/read-core.d.ts +64 -0
  86. package/dist/core/tools/read-core.js +180 -0
  87. package/dist/core/tools/read-text.d.ts +23 -0
  88. package/dist/core/tools/read-text.js +90 -0
  89. package/dist/core/tools/read.d.ts +6 -47
  90. package/dist/core/tools/read.js +13 -218
  91. package/dist/core/tools/renderers/bash.d.ts +0 -1
  92. package/dist/core/tools/renderers/bash.js +0 -1
  93. package/dist/core/tools/renderers/edit.d.ts +1 -1
  94. package/dist/core/tools/renderers/edit.js +1 -1
  95. package/dist/core/tools/secret-output-redactor.js +2 -1
  96. package/dist/core/tools/shell-operations.d.ts +53 -0
  97. package/dist/core/tools/shell-operations.js +115 -0
  98. package/dist/core/tools/shell-output.d.ts +20 -0
  99. package/dist/core/tools/shell-output.js +152 -0
  100. package/dist/core/tools/tail-window.js +5 -7
  101. package/dist/core/tools/tool-machine.d.ts +30 -0
  102. package/dist/core/tools/tool-machine.js +47 -0
  103. package/dist/core/tools/truncate.js +15 -14
  104. package/dist/core/tools/write-core.d.ts +47 -0
  105. package/dist/core/tools/write-core.js +108 -0
  106. package/dist/core/tools/write-result.d.ts +0 -1
  107. package/dist/core/tools/write-result.js +0 -13
  108. package/dist/core/tools/write.d.ts +6 -31
  109. package/dist/core/tools/write.js +25 -77
  110. package/dist/core/turn-origin.d.ts +4 -2
  111. package/dist/core/turn-origin.js +15 -8
  112. package/dist/modes/interactive/session-control-session-commands.js +1 -1
  113. package/dist/modes/rpc/connection-handler.js +2 -2
  114. package/dist/modes/rpc/effect-commands.d.ts +1 -4
  115. package/dist/modes/rpc/method-classes-schema-session.d.ts +1 -0
  116. package/dist/modes/rpc/method-classes-schema-session.js +5 -1
  117. package/dist/modes/rpc/method-classes-schema.d.ts +1 -0
  118. package/dist/modes/rpc/pause-command-validation.js +2 -0
  119. package/dist/modes/rpc/rpc-mutation-response-types.d.ts +2 -4
  120. package/dist/modes/rpc/rpc-session-command-types.d.ts +1 -0
  121. package/dist/modes/rpc/session-control-actions-data.d.ts +7 -3
  122. package/dist/modes/rpc/session-control-actions-data.js +12 -6
  123. package/dist/tools-core.d.ts +14 -0
  124. package/dist/tools-core.js +9 -0
  125. package/dist/utils/image-mime.d.ts +4 -0
  126. package/dist/utils/image-mime.js +99 -0
  127. package/dist/utils/mime.d.ts +1 -1
  128. package/dist/utils/mime.js +2 -97
  129. package/docs/extensions.md +7 -1
  130. package/docs/rpc.md +14 -0
  131. package/package.json +11 -7
  132. package/vendor/senpi-machine-ops/files/local-files.js +34 -5
@@ -0,0 +1,99 @@
1
+ /** Bytes from the start of a file that decide its image type. */
2
+ export const IMAGE_TYPE_SNIFF_BYTES = 4100;
3
+ const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
4
+ export function detectSupportedImageMimeType(buffer) {
5
+ if (startsWith(buffer, [0xff, 0xd8, 0xff])) {
6
+ return buffer[3] === 0xf7 ? null : "image/jpeg";
7
+ }
8
+ if (startsWith(buffer, PNG_SIGNATURE)) {
9
+ return isPng(buffer) && !isAnimatedPng(buffer) ? "image/png" : null;
10
+ }
11
+ if (startsWithAscii(buffer, 0, "GIF87a") || startsWithAscii(buffer, 0, "GIF89a")) {
12
+ return "image/gif";
13
+ }
14
+ if (startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP")) {
15
+ return "image/webp";
16
+ }
17
+ if (startsWithAscii(buffer, 0, "BM") && isBmp(buffer)) {
18
+ return "image/bmp";
19
+ }
20
+ return null;
21
+ }
22
+ function isPng(buffer) {
23
+ return (buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR"));
24
+ }
25
+ function isAnimatedPng(buffer) {
26
+ let offset = PNG_SIGNATURE.length;
27
+ while (offset + 8 <= buffer.length) {
28
+ const chunkLength = readUint32BE(buffer, offset);
29
+ const chunkTypeOffset = offset + 4;
30
+ if (startsWithAscii(buffer, chunkTypeOffset, "acTL"))
31
+ return true;
32
+ if (startsWithAscii(buffer, chunkTypeOffset, "IDAT"))
33
+ return false;
34
+ const nextOffset = offset + 8 + chunkLength + 4;
35
+ if (nextOffset <= offset || nextOffset > buffer.length)
36
+ return false;
37
+ offset = nextOffset;
38
+ }
39
+ return false;
40
+ }
41
+ function isBmp(buffer) {
42
+ if (buffer.length < 26)
43
+ return false;
44
+ const declaredFileSize = readUint32LE(buffer, 2);
45
+ const pixelDataOffset = readUint32LE(buffer, 10);
46
+ const dibHeaderSize = readUint32LE(buffer, 14);
47
+ if (declaredFileSize !== 0 && declaredFileSize < 26)
48
+ return false;
49
+ if (pixelDataOffset < 14 + dibHeaderSize)
50
+ return false;
51
+ if (declaredFileSize !== 0 && pixelDataOffset >= declaredFileSize)
52
+ return false;
53
+ let colorPlanes;
54
+ let bitsPerPixel;
55
+ if (dibHeaderSize === 12) {
56
+ colorPlanes = readUint16LE(buffer, 22);
57
+ bitsPerPixel = readUint16LE(buffer, 24);
58
+ }
59
+ else if (dibHeaderSize >= 40 && dibHeaderSize <= 124) {
60
+ if (buffer.length < 30)
61
+ return false;
62
+ colorPlanes = readUint16LE(buffer, 26);
63
+ bitsPerPixel = readUint16LE(buffer, 28);
64
+ }
65
+ else {
66
+ return false;
67
+ }
68
+ return colorPlanes === 1 && [1, 4, 8, 16, 24, 32].includes(bitsPerPixel);
69
+ }
70
+ function readUint16LE(buffer, offset) {
71
+ return (buffer[offset] ?? 0) + ((buffer[offset + 1] ?? 0) << 8);
72
+ }
73
+ function readUint32BE(buffer, offset) {
74
+ return ((buffer[offset] ?? 0) * 0x1000000 +
75
+ ((buffer[offset + 1] ?? 0) << 16) +
76
+ ((buffer[offset + 2] ?? 0) << 8) +
77
+ (buffer[offset + 3] ?? 0));
78
+ }
79
+ function readUint32LE(buffer, offset) {
80
+ return ((buffer[offset] ?? 0) +
81
+ ((buffer[offset + 1] ?? 0) << 8) +
82
+ ((buffer[offset + 2] ?? 0) << 16) +
83
+ (buffer[offset + 3] ?? 0) * 0x1000000);
84
+ }
85
+ function startsWith(buffer, bytes) {
86
+ if (buffer.length < bytes.length)
87
+ return false;
88
+ return bytes.every((byte, index) => buffer[index] === byte);
89
+ }
90
+ function startsWithAscii(buffer, offset, text) {
91
+ if (buffer.length < offset + text.length)
92
+ return false;
93
+ for (let index = 0; index < text.length; index++) {
94
+ if (buffer[offset + index] !== text.charCodeAt(index))
95
+ return false;
96
+ }
97
+ return true;
98
+ }
99
+ //# sourceMappingURL=image-mime.js.map
@@ -1,3 +1,3 @@
1
- export declare function detectSupportedImageMimeType(buffer: Uint8Array): string | null;
1
+ export { detectSupportedImageMimeType } from "./image-mime.ts";
2
2
  export declare function detectSupportedImageMimeTypeFromFile(filePath: string): Promise<string | null>;
3
3
  //# sourceMappingURL=mime.d.ts.map
@@ -1,24 +1,6 @@
1
1
  import { open } from "node:fs/promises";
2
- const IMAGE_TYPE_SNIFF_BYTES = 4100;
3
- const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
4
- export function detectSupportedImageMimeType(buffer) {
5
- if (startsWith(buffer, [0xff, 0xd8, 0xff])) {
6
- return buffer[3] === 0xf7 ? null : "image/jpeg";
7
- }
8
- if (startsWith(buffer, PNG_SIGNATURE)) {
9
- return isPng(buffer) && !isAnimatedPng(buffer) ? "image/png" : null;
10
- }
11
- if (startsWithAscii(buffer, 0, "GIF87a") || startsWithAscii(buffer, 0, "GIF89a")) {
12
- return "image/gif";
13
- }
14
- if (startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP")) {
15
- return "image/webp";
16
- }
17
- if (startsWithAscii(buffer, 0, "BM") && isBmp(buffer)) {
18
- return "image/bmp";
19
- }
20
- return null;
21
- }
2
+ import { detectSupportedImageMimeType, IMAGE_TYPE_SNIFF_BYTES } from "./image-mime.js";
3
+ export { detectSupportedImageMimeType } from "./image-mime.js";
22
4
  export async function detectSupportedImageMimeTypeFromFile(filePath) {
23
5
  const fileHandle = await open(filePath, "r");
24
6
  try {
@@ -30,81 +12,4 @@ export async function detectSupportedImageMimeTypeFromFile(filePath) {
30
12
  await fileHandle.close();
31
13
  }
32
14
  }
33
- function isPng(buffer) {
34
- return (buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR"));
35
- }
36
- function isAnimatedPng(buffer) {
37
- let offset = PNG_SIGNATURE.length;
38
- while (offset + 8 <= buffer.length) {
39
- const chunkLength = readUint32BE(buffer, offset);
40
- const chunkTypeOffset = offset + 4;
41
- if (startsWithAscii(buffer, chunkTypeOffset, "acTL"))
42
- return true;
43
- if (startsWithAscii(buffer, chunkTypeOffset, "IDAT"))
44
- return false;
45
- const nextOffset = offset + 8 + chunkLength + 4;
46
- if (nextOffset <= offset || nextOffset > buffer.length)
47
- return false;
48
- offset = nextOffset;
49
- }
50
- return false;
51
- }
52
- function isBmp(buffer) {
53
- if (buffer.length < 26)
54
- return false;
55
- const declaredFileSize = readUint32LE(buffer, 2);
56
- const pixelDataOffset = readUint32LE(buffer, 10);
57
- const dibHeaderSize = readUint32LE(buffer, 14);
58
- if (declaredFileSize !== 0 && declaredFileSize < 26)
59
- return false;
60
- if (pixelDataOffset < 14 + dibHeaderSize)
61
- return false;
62
- if (declaredFileSize !== 0 && pixelDataOffset >= declaredFileSize)
63
- return false;
64
- let colorPlanes;
65
- let bitsPerPixel;
66
- if (dibHeaderSize === 12) {
67
- colorPlanes = readUint16LE(buffer, 22);
68
- bitsPerPixel = readUint16LE(buffer, 24);
69
- }
70
- else if (dibHeaderSize >= 40 && dibHeaderSize <= 124) {
71
- if (buffer.length < 30)
72
- return false;
73
- colorPlanes = readUint16LE(buffer, 26);
74
- bitsPerPixel = readUint16LE(buffer, 28);
75
- }
76
- else {
77
- return false;
78
- }
79
- return colorPlanes === 1 && [1, 4, 8, 16, 24, 32].includes(bitsPerPixel);
80
- }
81
- function readUint16LE(buffer, offset) {
82
- return (buffer[offset] ?? 0) + ((buffer[offset + 1] ?? 0) << 8);
83
- }
84
- function readUint32BE(buffer, offset) {
85
- return ((buffer[offset] ?? 0) * 0x1000000 +
86
- ((buffer[offset + 1] ?? 0) << 16) +
87
- ((buffer[offset + 2] ?? 0) << 8) +
88
- (buffer[offset + 3] ?? 0));
89
- }
90
- function readUint32LE(buffer, offset) {
91
- return ((buffer[offset] ?? 0) +
92
- ((buffer[offset + 1] ?? 0) << 8) +
93
- ((buffer[offset + 2] ?? 0) << 16) +
94
- (buffer[offset + 3] ?? 0) * 0x1000000);
95
- }
96
- function startsWith(buffer, bytes) {
97
- if (buffer.length < bytes.length)
98
- return false;
99
- return bytes.every((byte, index) => buffer[index] === byte);
100
- }
101
- function startsWithAscii(buffer, offset, text) {
102
- if (buffer.length < offset + text.length)
103
- return false;
104
- for (let index = 0; index < text.length; index++) {
105
- if (buffer[offset + index] !== text.charCodeAt(index))
106
- return false;
107
- }
108
- return true;
109
- }
110
15
  //# sourceMappingURL=mime.js.map
@@ -493,6 +493,12 @@ journaled terminal. Later wakes tagged with the run are admitted again, fences f
493
493
  run that has no fence does nothing. A fence alone does not pause the session, so fencing or releasing only a fence
494
494
  emits no `session_paused`/`session_resumed` and no `session_hold_changed`; each pause gets exactly one resume.
495
495
 
496
+ `ctx.registerRunBackgroundJob({ kind, id, description?, stop })` records a background job under the turn running at
497
+ that moment (outside a turn it records nothing), and returns the release to call once the job ended on its own. An RPC
498
+ `interrupt` with `background: "stop"` calls `stop(turnId)` for each job that turn recorded and names it in its answer;
499
+ `stop` ends the job without waking the session and rejects with the reason it could not. The terminal builtin records
500
+ every background bash session this way.
501
+
496
502
  #### held_wakes_released
497
503
 
498
504
  `pi.on("held_wakes_released", (event, ctx) => ...)` receives
@@ -2573,7 +2579,7 @@ pi.registerShortcut("ctrl+shift+p", {
2573
2579
 
2574
2580
  Register the operation layer of the device this session's tools run on (a `ToolMachine`, for example from `createToolMachine({ ops })`). Only tools named in `OMO_TOOLS_VIA_DAEMON` use it, today `bash`, and the bash tool reads it when a command runs. With the flag on and no machine registered, bash fails with `DeviceUnavailableError` and never runs locally.
2575
2581
 
2576
- One registration per session: a second one, from any extension, throws `DeviceMachineConflictError` (`code: "device_machine_conflict"`, with `registeredBy` and `attemptedBy`). A registration made by an extension whose factory then throws is discarded with the rest of that load.
2582
+ One registration per session: a second one, from any extension, throws `DeviceMachineConflictError` (`code: "device_machine_conflict"`, with `registeredBy` and `attemptedBy`). A registration made by an extension whose factory then throws is discarded with the rest of that load. If another extension registers a device machine while this load's registration is still staged (for example from an event fired during this factory), the conflict is found when the load commits: the factory cannot catch it, and the extension fails to load as a whole with `DeviceMachineConflictError`.
2577
2583
 
2578
2584
  ```typescript
2579
2585
  import { createToolMachine } from "@code-yeongyu/senpi";
package/docs/rpc.md CHANGED
@@ -4678,6 +4678,20 @@ answers after the turn settled.
4678
4678
  {"type": "response", "command": "interrupt", "success": true, "data": {"interrupted": true, "turnId": "3"}}
4679
4679
  ```
4680
4680
 
4681
+ With `"background": "stop"` the command also stops the background jobs the interrupted turn started: each bash
4682
+ session that turn ran with `run_in_background`, or that auto-detached from the foreground, while that turn was running.
4683
+ Sessions an earlier turn started keep running. The stop happens after the turn settled, and the answer then carries
4684
+ `background: { stopped, failed }`: `stopped` names each job as `{ kind: "bash", id, description? }`, and `failed`
4685
+ names a job it could not stop with its `reason` (a session on the tool child under `--tool-runtime child`, or one
4686
+ still running after TERM then KILL). An idle session, or a `turnId` naming another turn, answers both lists empty.
4687
+ A stopped session's exit notice does not start a turn: the model reads `Background terminal session <id> was stopped
4688
+ when turn <turnId> was interrupted` at its next turn. Without the field nothing but the turn stops, as before.
4689
+
4690
+ ```json
4691
+ {"type": "interrupt", "turnId": "3", "background": "stop"}
4692
+ {"type": "response", "command": "interrupt", "success": true, "data": {"interrupted": true, "turnId": "3", "background": {"stopped": [{"kind": "bash", "id": "bash_1", "description": "sleep 60"}], "failed": []}}}
4693
+ ```
4694
+
4681
4695
  With `"intent": "stop"` (on `abort` or `interrupt`) the command is a user Stop: the session is paused before the
4682
4696
  turn is stopped, and automatic wakes that arrive later (terminal and monitor notices, detached eval cells, goal
4683
4697
  continuations, task and team deliveries) are kept as notices instead of starting a turn. Only a connection that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.11.15-2",
3
+ "version": "2026.11.15-3",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -63,6 +63,10 @@
63
63
  "./presenter-authority": {
64
64
  "types": "./dist/presenter-authority-slot.d.ts",
65
65
  "import": "./dist/presenter-authority-slot.js"
66
+ },
67
+ "./tools-core": {
68
+ "types": "./dist/tools-core.d.ts",
69
+ "import": "./dist/tools-core.js"
66
70
  }
67
71
  },
68
72
  "files": [
@@ -92,10 +96,10 @@
92
96
  },
93
97
  "dependencies": {
94
98
  "@earendil-works/chord": "0.99.1",
95
- "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.11.15-2",
96
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.11.15-2",
97
- "@earendil-works/pi-realm-registry": "npm:@code-yeongyu/senpi-realm-registry@2026.11.15-2",
98
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.11.15-2",
99
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.11.15-3",
100
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.11.15-3",
101
+ "@earendil-works/pi-realm-registry": "npm:@code-yeongyu/senpi-realm-registry@2026.11.15-3",
102
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.11.15-3",
99
103
  "@silvia-odwyer/photon-node": "0.3.4",
100
104
  "ajv": "8.20.0",
101
105
  "chalk": "6.0.0",
@@ -116,8 +120,8 @@
116
120
  "yaml": "2.9.1",
117
121
  "@anthropic-ai/claude-agent-sdk": "0.3.321",
118
122
  "@anthropic-ai/sdk": "0.127.0",
119
- "@code-yeongyu/senpi-codemode": "2026.11.15-2",
120
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.11.15-2",
123
+ "@code-yeongyu/senpi-codemode": "2026.11.15-3",
124
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.11.15-3",
121
125
  "@modelcontextprotocol/sdk": "1.32.1",
122
126
  "@mozilla/readability": "0.6.0",
123
127
  "linkedom": "0.18.13",
@@ -1,6 +1,6 @@
1
1
  import { constants } from "node:fs";
2
2
  import { access, mkdir, open, readdir, readFile, stat, writeFile } from "node:fs/promises";
3
- import { dirname, isAbsolute } from "node:path";
3
+ import { dirname, isAbsolute, resolve, sep } from "node:path";
4
4
  import { MachineOpError } from "../errors.js";
5
5
  import { checkPath, gated, resolveOpPath } from "../policy/gate.js";
6
6
  import { localGlob } from "./glob.js";
@@ -136,12 +136,20 @@ export function createLocalFileOps(options) {
136
136
  }));
137
137
  },
138
138
  async glob(req, ctx) {
139
- const cwd = resolveOpPath(req.cwd ?? ".", ctx);
140
- if (ctx.scope.root !== undefined && (isAbsolute(req.pattern) || req.pattern.split(/[\\/]/).includes(".."))) {
141
- throw new MachineOpError("policy_denied", "a glob pattern may not leave the scope root");
139
+ let cwd = resolveOpPath(req.cwd ?? ".", ctx);
140
+ let pattern = req.pattern;
141
+ if (ctx.scope.root !== undefined && (isAbsolute(pattern) || pattern.split(/[\\/]/).includes(".."))) {
142
+ // An absolute or ".." pattern is judged by where its fixed part resolves, like every other path: the
143
+ // check below allows it inside the scope root and refuses it outside.
144
+ const split = splitGlobBase(pattern, cwd);
145
+ if (split === undefined) {
146
+ throw new MachineOpError("policy_denied", "a glob pattern may not use '..' after a wildcard");
147
+ }
148
+ cwd = split.base;
149
+ pattern = split.rest;
142
150
  }
143
151
  await checkPath(cwd, "enumerate", "files.glob", ctx);
144
- return gated("files.glob", req, ctx, () => localGlob(req.pattern, cwd, req.limit, ctx.signal, options.resolveFd));
152
+ return gated("files.glob", req, ctx, () => localGlob(pattern, cwd, req.limit, ctx.signal, options.resolveFd));
145
153
  },
146
154
  async grep(req, ctx) {
147
155
  const path = resolveOpPath(req.path, ctx);
@@ -168,4 +176,25 @@ export function createLocalFileOps(options) {
168
176
  },
169
177
  };
170
178
  }
179
+ const GLOB_SYNTAX = /[*?[\]{}]/;
180
+ /**
181
+ * Split a glob pattern into the directory its fixed leading segments name (resolved against `cwd`) and the rest,
182
+ * which starts at the first segment with glob syntax. A pattern with no glob syntax splits before its last segment; one
183
+ * ending in a separator lists that directory.
184
+ * Undefined when ".." follows a wildcard: where that lands is known only after matching.
185
+ */
186
+ function splitGlobBase(pattern, cwd) {
187
+ const segments = pattern.split(/[\\/]/);
188
+ const firstGlob = segments.findIndex((segment) => GLOB_SYNTAX.test(segment));
189
+ const cut = firstGlob === -1 ? segments.length - 1 : firstGlob;
190
+ const fixed = segments.slice(0, cut);
191
+ const rest = segments.slice(cut);
192
+ if (rest.includes(".."))
193
+ return undefined;
194
+ // A pattern ending in a separator (`<root>/`, `../dir/`) names a directory: list its entries, as `*` would.
195
+ if (rest.join("/") === "")
196
+ return { base: isAbsolute(pattern) ? resolve(sep, ...fixed) : resolve(cwd, ...fixed), rest: "*" };
197
+ const base = isAbsolute(pattern) ? resolve(sep, ...fixed) : resolve(cwd, ...fixed);
198
+ return { base, rest: rest.join("/") };
199
+ }
171
200
  //# sourceMappingURL=local-files.js.map