@platless/axom-cli 0.3.13 → 0.5.9

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 (235) hide show
  1. package/dist/axom-patch/cli/README.md +30 -8
  2. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  3. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  6. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  7. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  8. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  9. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  10. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  12. package/dist/axom-patch/cli/nightly-task.md +152 -0
  13. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  14. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  15. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  16. package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
  17. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
  18. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  19. package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
  20. package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
  21. package/dist/axom-patch/cli/qoder-steering.json +15 -15
  22. package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
  23. package/dist/axom-patch/cli/runtime.json +34 -8
  24. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  25. package/dist/axom-patch/cli/zcode-models.json +38 -0
  26. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  27. package/dist/cli.js +33 -6
  28. package/dist/keepers/acp/keeper.mjs +3083 -0
  29. package/dist/keepers/acp/manifest.json +9 -0
  30. package/dist/keepers/terminal/keeper.mjs +8469 -0
  31. package/dist/keepers/terminal/manifest.json +9 -0
  32. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  33. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  34. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  35. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  36. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  37. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  38. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  39. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  40. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  41. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  42. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  52. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  53. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  54. package/dist/test/keeperKit.js +67 -0
  55. package/dist/workstation/acp/acpAgents.js +2163 -611
  56. package/dist/workstation/acp/acpClient.js +60 -497
  57. package/dist/workstation/acp/adoption.js +10 -396
  58. package/dist/workstation/acp/binaryInstall.js +100 -27
  59. package/dist/workstation/acp/builds/gc.js +245 -0
  60. package/dist/workstation/acp/builds/install/binary.js +28 -0
  61. package/dist/workstation/acp/builds/install/npm.js +235 -0
  62. package/dist/workstation/acp/builds/install/uv.js +254 -0
  63. package/dist/workstation/acp/builds/migrate.js +195 -0
  64. package/dist/workstation/acp/builds/model.js +128 -0
  65. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  66. package/dist/workstation/acp/builds/store.js +352 -0
  67. package/dist/workstation/acp/commandTableProbe.js +68 -0
  68. package/dist/workstation/acp/configOptions.js +103 -1
  69. package/dist/workstation/acp/envSecrets.js +27 -0
  70. package/dist/workstation/acp/initializeResponse.js +285 -0
  71. package/dist/workstation/acp/installErrors.js +12 -2
  72. package/dist/workstation/acp/installProbe.js +1 -1
  73. package/dist/workstation/acp/managedPatches.js +76 -160
  74. package/dist/workstation/acp/outbox.js +330 -0
  75. package/dist/workstation/acp/patchAssets.js +20 -3
  76. package/dist/workstation/acp/permission.js +42 -10
  77. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  78. package/dist/workstation/acp/processor/commands.js +35 -0
  79. package/dist/workstation/acp/processor/control.js +355 -0
  80. package/dist/workstation/acp/processor/emit.js +26 -0
  81. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  82. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  83. package/dist/workstation/acp/processor/origins.js +48 -0
  84. package/dist/workstation/acp/processor/output.js +67 -0
  85. package/dist/workstation/acp/processor/projection.js +42 -0
  86. package/dist/workstation/acp/processor/record.js +128 -0
  87. package/dist/workstation/acp/processor/runtime.js +181 -0
  88. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  89. package/dist/workstation/acp/processor/session.js +525 -0
  90. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  91. package/dist/workstation/acp/processor/state.js +74 -0
  92. package/dist/workstation/acp/processor/step.js +459 -0
  93. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  94. package/dist/workstation/acp/processor/test/harness.js +120 -0
  95. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  96. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  97. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  98. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  99. package/dist/workstation/acp/processor/transport/local.js +269 -0
  100. package/dist/workstation/acp/processor/transport/types.js +78 -0
  101. package/dist/workstation/acp/processor/updates.js +364 -0
  102. package/dist/workstation/acp/processorSession.js +1512 -0
  103. package/dist/workstation/acp/qoderStartup.js +1 -1
  104. package/dist/workstation/acp/registry/catalog.js +154 -32
  105. package/dist/workstation/acp/registry/launch.js +61 -119
  106. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  107. package/dist/workstation/acp/steering.js +58 -0
  108. package/dist/workstation/acp/switching.js +114 -0
  109. package/dist/workstation/acp/terminalLogin.js +16 -42
  110. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  111. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  112. package/dist/workstation/acp/test/daemonLife.js +106 -0
  113. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  114. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  115. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  116. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  117. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  118. package/dist/workstation/acp/test/raw/sources.js +249 -0
  119. package/dist/workstation/acp/test/realAdapter.js +377 -0
  120. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  121. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  122. package/dist/workstation/acp/timedCommand.js +4 -2
  123. package/dist/workstation/acp/uvxInstall.js +72 -122
  124. package/dist/workstation/agentFlow.js +103 -0
  125. package/dist/workstation/agentSessions.js +1324 -474
  126. package/dist/workstation/channels/agent.js +7 -5
  127. package/dist/workstation/channels/computer.js +69 -21
  128. package/dist/workstation/channels/computerExecs.js +200 -0
  129. package/dist/workstation/channels/management.js +84 -4
  130. package/dist/workstation/daemon.js +288 -7
  131. package/dist/workstation/daemonctl.js +28 -2
  132. package/dist/workstation/exec/attach.js +126 -0
  133. package/dist/workstation/exec/command.js +40 -0
  134. package/dist/workstation/exec/durable.js +517 -0
  135. package/dist/workstation/exec/fallback.js +106 -0
  136. package/dist/workstation/exec/policy.js +837 -0
  137. package/dist/workstation/exec/shaping.js +211 -0
  138. package/dist/workstation/exec.js +3 -112
  139. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  140. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  141. package/dist/workstation/files/client.js +455 -0
  142. package/dist/workstation/files/errors.js +28 -0
  143. package/dist/workstation/files/fence.js +350 -0
  144. package/dist/workstation/files/find/fileIndex.js +153 -0
  145. package/dist/workstation/files/find/fuzzy.js +155 -0
  146. package/dist/workstation/files/host.js +122 -0
  147. package/dist/workstation/files/hostProtocol.js +2 -0
  148. package/dist/workstation/files/media/cache.js +620 -0
  149. package/dist/workstation/files/media/encoding.js +308 -0
  150. package/dist/workstation/files/media/estimator.js +100 -0
  151. package/dist/workstation/files/media/media.js +325 -0
  152. package/dist/workstation/files/media/packedAudio.js +23 -0
  153. package/dist/workstation/files/media/scheduler.js +551 -0
  154. package/dist/workstation/files/media/types.js +5 -0
  155. package/dist/workstation/files/scope.js +45 -0
  156. package/dist/workstation/files/structured.js +284 -0
  157. package/dist/workstation/{fileOps.js → files/text.js} +117 -38
  158. package/dist/workstation/files/uploads.js +351 -0
  159. package/dist/workstation/journal.js +65 -236
  160. package/dist/workstation/keepers/acp/client.js +101 -0
  161. package/dist/workstation/keepers/acp/envelope.js +62 -0
  162. package/dist/workstation/keepers/acp/host.js +419 -0
  163. package/dist/workstation/keepers/acp/main.js +18 -0
  164. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  165. package/dist/workstation/keepers/acp/params.js +137 -0
  166. package/dist/workstation/keepers/acp/protocol.js +66 -0
  167. package/dist/workstation/keepers/acp/recover.js +120 -0
  168. package/dist/workstation/keepers/acp/session.js +584 -0
  169. package/dist/workstation/keepers/acp/stderr.js +154 -0
  170. package/dist/workstation/keepers/kit/activity.js +103 -0
  171. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  172. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  173. package/dist/workstation/keepers/kit/bundle.js +120 -0
  174. package/dist/workstation/keepers/kit/client.js +432 -0
  175. package/dist/workstation/keepers/kit/identity.js +21 -0
  176. package/dist/workstation/keepers/kit/ipc.js +112 -0
  177. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  178. package/dist/workstation/keepers/kit/launcher.js +249 -0
  179. package/dist/workstation/keepers/kit/lock.js +123 -0
  180. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  181. package/dist/workstation/keepers/kit/paths.js +75 -0
  182. package/dist/workstation/keepers/kit/protocol.js +43 -0
  183. package/dist/workstation/keepers/kit/server.js +518 -0
  184. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  185. package/dist/workstation/keepers/legacy.js +296 -0
  186. package/dist/workstation/keepers/policy.js +576 -0
  187. package/dist/workstation/keepers/stop.js +34 -0
  188. package/dist/workstation/keepers/terminal/client.js +213 -0
  189. package/dist/workstation/keepers/terminal/exec.js +981 -0
  190. package/dist/workstation/keepers/terminal/generations.js +129 -0
  191. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  192. package/dist/workstation/keepers/terminal/main.js +43 -0
  193. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  194. package/dist/workstation/keepers/terminal/params.js +27 -0
  195. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  196. package/dist/workstation/keepers/terminal/pty.js +330 -0
  197. package/dist/workstation/protocol/accountLimits.js +47 -0
  198. package/dist/workstation/protocol/agent.js +48 -4
  199. package/dist/workstation/protocol/common.js +30 -0
  200. package/dist/workstation/protocol/computer.js +15 -4
  201. package/dist/workstation/protocol/management.js +50 -2
  202. package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
  203. package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
  204. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  205. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  206. package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
  207. package/dist/workstation/qoderAdapter/history.js +1 -3
  208. package/dist/workstation/qoderAdapter/main.js +4 -2
  209. package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
  210. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  211. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  212. package/dist/workstation/qoderAdapter/session.js +1 -1
  213. package/dist/workstation/qoderAdapter/subwork.js +15 -7
  214. package/dist/workstation/systemMetrics.js +110 -1
  215. package/dist/workstation/tools/client.js +9 -1
  216. package/dist/workstation/tools/info.js +50 -0
  217. package/dist/workstation/tools/keeperProcess.js +15 -5
  218. package/dist/workstation/tools/router.js +55 -0
  219. package/dist/workstation/tools/terminalRouter.js +222 -0
  220. package/dist/workstation/tools/validation.js +37 -1
  221. package/dist/workstation/update/coordinator.js +3 -0
  222. package/dist/workstation/update/ptyPreflight.js +21 -0
  223. package/dist/workstation/update/taskRegistry.js +105 -11
  224. package/package.json +10 -7
  225. package/dist/workstation/acp/acpSession.js +0 -1475
  226. package/dist/workstation/acp/contract.js +0 -499
  227. package/dist/workstation/acp/draftInterceptor.js +0 -163
  228. package/dist/workstation/acp/sessionScope.js +0 -19
  229. package/dist/workstation/filePaths.js +0 -174
  230. package/dist/workstation/fileWorker.js +0 -81
  231. package/dist/workstation/fileWorkerClient.js +0 -121
  232. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  233. package/dist/workstation/tools/core.js +0 -50
  234. package/dist/workstation/tools/files.js +0 -525
  235. package/dist/workstation/tools/terminal.js +0 -214
@@ -0,0 +1,284 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { base64, CHUNK_BYTES, identifier, integer, keys, string } from '../tools/validation.js';
4
+ import { openMediaSource, pinned, resolveAbsolute } from './fence.js';
5
+ import { configuredRoots, filesystemScope, inside, READ_ONLY } from './scope.js';
6
+ import { MAX_UPLOAD_BYTES, TEMP_PREFIX, UPLOAD_TTL_MS, UploadStore } from './uploads.js';
7
+ import { FileIndexCache } from './find/fileIndex.js';
8
+ import { rankPaths } from './find/fuzzy.js';
9
+ export const LIST_MAX_ENTRIES = 2000;
10
+ export const LIST_MAX_VISITED = 10000;
11
+ export const FIND_DEFAULT_LIMIT = 50;
12
+ export const FIND_MAX_LIMIT = 200;
13
+ /**
14
+ * The Files tab's file semantics (`files.*` tool requests): absolute paths in the `configured-roots` scope, or in the
15
+ * `read-only` scope when a request says `readOnly: true`. Errors are thrown with the messages callers see today
16
+ * (`tool_result.error`). Tools never compose a shell command.
17
+ */
18
+ export class StructuredFiles {
19
+ scope;
20
+ uploads;
21
+ // Home and its ancestors walk instead of asking git: a dotfiles repository at `~` (often ignoring
22
+ // `*`) would answer for the whole home with just its tracked dotfiles.
23
+ indexes = new FileIndexCache(name => name.startsWith(TEMP_PREFIX), Date.now, scope => { const relative = path.relative(scope, this.home); return !relative.startsWith('..') && !path.isAbsolute(relative); });
24
+ home;
25
+ roots;
26
+ downloads;
27
+ constructor(scope, ttl, stateDir) {
28
+ this.scope = scope;
29
+ this.home = scope.home;
30
+ this.roots = [...scope.roots];
31
+ this.downloads = path.join(scope.home, 'Downloads');
32
+ this.uploads = new UploadStore(scope.roots, ttl, stateDir);
33
+ }
34
+ static async create(home, roots, ttl = UPLOAD_TTL_MS, stateDir, filesystem = false) {
35
+ const scope = filesystem ? await filesystemScope(home) : await configuredRoots(home, roots);
36
+ if (stateDir)
37
+ await UploadStore.prepareStateDir(stateDir);
38
+ const files = new StructuredFiles(scope, ttl, stateDir);
39
+ await files.uploads.recover((raw, missingLeaf, mutation) => files.resolve(raw, missingLeaf, mutation));
40
+ return files;
41
+ }
42
+ /** Uploads in flight. */
43
+ get activeUploads() { return this.uploads.size; }
44
+ scopeFor(readOnly) { return readOnly ? READ_ONLY : this.scope; }
45
+ /** Validates an absolute path in this instance's configured-roots scope (or the read-only scope). */
46
+ resolve(raw, missingLeaf = false, mutation = false, readOnly = false, followLeaf = true) {
47
+ return resolveAbsolute(this.scopeFor(readOnly), raw, missingLeaf, mutation, followLeaf);
48
+ }
49
+ /** The descriptor stays pinned after cwd is restored; media children inherit it, never re-open the path. */
50
+ openMediaSource(raw, readOnly = false) {
51
+ return openMediaSource(this.scopeFor(readOnly), raw);
52
+ }
53
+ async dispatch(method, p) {
54
+ if (p.readOnly !== undefined && typeof p.readOnly !== 'boolean')
55
+ throw new Error('Invalid readOnly');
56
+ if (p.readOnly !== undefined && !['files.list', 'files.find', 'files.read'].includes(method))
57
+ throw new Error('Read-only file access cannot modify files');
58
+ await this.expire();
59
+ if (method === 'files.find')
60
+ return this.find(p);
61
+ // Any write may add, move or remove a path a cached search list still holds. Invalidate before and again after
62
+ // the mutation: a search building its list concurrently (find takes no pin) then never caches a pre-mutation list.
63
+ if (!['files.list', 'files.read'].includes(method)) {
64
+ this.indexes.invalidate();
65
+ try {
66
+ return await this.locate(method, p);
67
+ }
68
+ finally {
69
+ this.indexes.invalidate();
70
+ }
71
+ }
72
+ return this.locate(method, p);
73
+ }
74
+ async locate(method, p) {
75
+ let parent;
76
+ if (method === 'files.upload.chunk' || method === 'files.upload.finish' || method === 'files.upload.cancel') {
77
+ const upload = this.uploads.get(identifier(p, 'uploadId'));
78
+ if (!upload && method === 'files.upload.cancel') {
79
+ keys(p, ['uploadId']);
80
+ return { ok: true };
81
+ }
82
+ if (!upload)
83
+ throw new Error('Upload not found or expired');
84
+ parent = path.dirname(upload.target);
85
+ }
86
+ else {
87
+ const target = await this.resolve(string(p, 'path', 4096, method === 'files.list' ? this.home : undefined), method === 'files.mkdir' || method === 'files.upload.begin', !['files.list', 'files.read'].includes(method), p.readOnly === true, !['files.rename', 'files.delete'].includes(method));
88
+ parent = method === 'files.list' ? target : path.dirname(target);
89
+ }
90
+ return pinned(parent, async (pin) => {
91
+ if (method.startsWith('files.upload.') && method !== 'files.upload.begin') {
92
+ // Re-read: the upload may have finished, been cancelled or expired while this call waited for the pin.
93
+ const upload = this.uploads.get(identifier(p, 'uploadId'));
94
+ if (!upload && method === 'files.upload.cancel') {
95
+ keys(p, ['uploadId']);
96
+ return { ok: true };
97
+ }
98
+ if (!upload)
99
+ throw new Error('Upload not found or expired');
100
+ const stat = await fs.stat('.');
101
+ if (stat.dev !== upload.parent.dev || stat.ino !== upload.parent.ino) {
102
+ if (method === 'files.upload.cancel') {
103
+ await this.uploads.abandon(upload);
104
+ return { ok: true };
105
+ }
106
+ throw new Error('Upload parent changed');
107
+ }
108
+ }
109
+ return this.perform(method, p, pin);
110
+ });
111
+ }
112
+ async perform(method, p, pin) {
113
+ switch (method) {
114
+ case 'files.list': {
115
+ keys(p, ['path', 'readOnly']);
116
+ const target = await this.resolve(string(p, 'path', 4096, this.home), false, false, p.readOnly === true);
117
+ const displayedPath = this.scope.kind === 'filesystem' ? string(p, 'path', 4096, this.home) : target;
118
+ const listing = await fs.opendir(pin.leaf(target));
119
+ const entries = [];
120
+ let visited = 0;
121
+ try {
122
+ for await (const entry of listing) {
123
+ if (++visited > LIST_MAX_VISITED || entries.length >= LIST_MAX_ENTRIES)
124
+ throw new Error('Directory entry limit exceeded');
125
+ if (entry.name.startsWith(TEMP_PREFIX) || entry.isSymbolicLink())
126
+ continue;
127
+ const entryPath = path.join(target, entry.name);
128
+ let stat;
129
+ try {
130
+ await this.resolve(entryPath, false, false, p.readOnly === true);
131
+ stat = await fs.lstat(pin.leaf(entryPath));
132
+ }
133
+ catch {
134
+ continue;
135
+ }
136
+ if (!stat.isFile() && !stat.isDirectory())
137
+ continue;
138
+ entries.push({ name: entry.name, path: path.join(displayedPath, entry.name), directory: stat.isDirectory(), size: stat.size, modifiedAt: stat.mtimeMs, createdAt: stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.ctimeMs > 0 ? stat.ctimeMs : stat.mtimeMs,
139
+ ...(this.scope.kind === 'filesystem' ? { writable: await fs.access(entryPath, constants.W_OK).then(() => true, () => false) } : {}) });
140
+ }
141
+ }
142
+ finally {
143
+ await listing.close().catch(() => { });
144
+ }
145
+ entries.sort((a, b) => Number(b.directory) - Number(a.directory) || String(a.name).localeCompare(String(b.name)));
146
+ const parent = path.dirname(displayedPath);
147
+ return { path: displayedPath, parent: p.readOnly === true || this.scope.kind === 'filesystem' ? (parent === displayedPath ? null : parent) : this.roots.includes(target) || !this.roots.some(root => inside(parent, root)) ? null : parent, entries, roots: this.roots };
148
+ }
149
+ case 'files.read': {
150
+ keys(p, ['path', 'offset', 'length', 'readOnly']);
151
+ const offset = integer(p, 'offset', 0, Number.MAX_SAFE_INTEGER, 0);
152
+ const length = integer(p, 'length', 1, CHUNK_BYTES, CHUNK_BYTES);
153
+ const target = await this.resolve(string(p, 'path'), false, false, p.readOnly === true);
154
+ const handle = await pin.regularHandle(target);
155
+ try {
156
+ const stat = await handle.stat();
157
+ const buffer = Buffer.alloc(length);
158
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
159
+ return { data: buffer.subarray(0, bytesRead).toString('base64'), size: stat.size, eof: offset + bytesRead >= stat.size, name: path.basename(target),
160
+ ...(this.scope.kind === 'filesystem' ? { writable: await fs.access(pin.leaf(target), constants.W_OK).then(() => true, () => false) } : {}) };
161
+ }
162
+ finally {
163
+ await handle.close();
164
+ }
165
+ }
166
+ case 'files.mkdir': {
167
+ keys(p, ['path']);
168
+ const target = await this.resolve(string(p, 'path'), true, true);
169
+ await fs.mkdir(pin.leaf(target));
170
+ return { ok: true };
171
+ }
172
+ case 'files.rename': {
173
+ keys(p, ['path', 'name']);
174
+ const target = await this.resolve(string(p, 'path'), false, true, false, false);
175
+ const name = string(p, 'name', 255);
176
+ if (name === '.' || name === '..' || /[/\\]/.test(name) || name.startsWith(TEMP_PREFIX))
177
+ throw new Error('Invalid new name');
178
+ const destination = await this.resolve(path.join(path.dirname(target), name), true, true);
179
+ // Node rename() overwrites files. Hard-link commits are exclusive for files; for directories,
180
+ // reserve the destination with mkdir and replace only that empty reservation atomically.
181
+ const stat = await fs.lstat(pin.leaf(target));
182
+ if (stat.isFile()) {
183
+ await fs.link(pin.leaf(target), pin.leaf(destination));
184
+ await fs.unlink(pin.leaf(target));
185
+ }
186
+ else if (stat.isDirectory()) {
187
+ await fs.mkdir(pin.leaf(destination), { mode: stat.mode });
188
+ try {
189
+ await fs.rename(pin.leaf(target), pin.leaf(destination));
190
+ }
191
+ catch (error) {
192
+ await fs.rmdir(pin.leaf(destination)).catch(() => { });
193
+ throw error;
194
+ }
195
+ }
196
+ else
197
+ throw new Error('Not a regular file or directory');
198
+ return { ok: true };
199
+ }
200
+ case 'files.delete': {
201
+ keys(p, ['path']);
202
+ const target = await this.resolve(string(p, 'path'), false, true, false, false);
203
+ const stat = await fs.lstat(pin.leaf(target));
204
+ if (stat.isDirectory())
205
+ await fs.rmdir(pin.leaf(target));
206
+ else if (stat.isFile())
207
+ await fs.unlink(pin.leaf(target));
208
+ else
209
+ throw new Error('Not a regular file or directory');
210
+ return { ok: true };
211
+ }
212
+ case 'files.upload.begin': {
213
+ keys(p, ['path', 'size', 'overwrite']);
214
+ const size = integer(p, 'size', 0, MAX_UPLOAD_BYTES);
215
+ if (p.overwrite !== undefined && typeof p.overwrite !== 'boolean')
216
+ throw new Error('Invalid overwrite');
217
+ return this.uploads.begin(pin, () => this.resolve(string(p, 'path'), true, true), size, p.overwrite === true, this.scope.kind === 'filesystem', string(p, 'path'));
218
+ }
219
+ case 'files.upload.chunk': {
220
+ keys(p, ['uploadId', 'offset', 'data']);
221
+ const upload = this.uploads.use(identifier(p, 'uploadId'));
222
+ const offset = integer(p, 'offset', 0, MAX_UPLOAD_BYTES);
223
+ const bytes = base64(p.data);
224
+ return this.uploads.chunk(upload, offset, bytes);
225
+ }
226
+ case 'files.upload.finish': {
227
+ keys(p, ['uploadId']);
228
+ const upload = this.uploads.use(identifier(p, 'uploadId'));
229
+ return this.uploads.finish(pin, upload, (raw, missingLeaf, mutation) => this.resolve(raw, missingLeaf, mutation));
230
+ }
231
+ case 'files.upload.cancel': {
232
+ keys(p, ['uploadId']);
233
+ return this.uploads.cancel(identifier(p, 'uploadId'));
234
+ }
235
+ default: throw new Error('Unsupported file tool');
236
+ }
237
+ }
238
+ /**
239
+ * Fuzzy file-name search below one directory (`path`, default home). Ranking runs over the cached
240
+ * relative-path list; only the best candidates are re-validated against the same root/symlink fence
241
+ * as `files.list` before they are returned, so a path git still remembers but the disk lost, or one
242
+ * that became a symbolic link, never reaches the client. Uses no cwd: absolute paths throughout, so it
243
+ * never enters (or waits for) a `pinned()` section.
244
+ */
245
+ async find(p) {
246
+ keys(p, ['path', 'query', 'limit', 'readOnly']);
247
+ const readOnly = p.readOnly === true;
248
+ const query = string(p, 'query', 256).trim();
249
+ if (!query)
250
+ throw new Error('Invalid query');
251
+ const limit = integer(p, 'limit', 1, FIND_MAX_LIMIT, FIND_DEFAULT_LIMIT);
252
+ const scope = await this.resolve(string(p, 'path', 4096, this.home), false, false, readOnly);
253
+ const displayedScope = this.scope.kind === 'filesystem' ? string(p, 'path', 4096, this.home) : scope;
254
+ if (!(await fs.stat(scope)).isDirectory())
255
+ throw new Error('Search scope is not a directory');
256
+ const index = await this.indexes.get(scope);
257
+ const ranked = rankPaths(index.paths, query);
258
+ const entries = [];
259
+ let checked = 0;
260
+ // Bounded: a stale list full of vanished paths must not turn one search into a disk scan.
261
+ for (const hit of ranked.slice(0, limit * 4)) {
262
+ if (entries.length >= limit)
263
+ break;
264
+ checked++;
265
+ const relative = index.paths[hit.index];
266
+ const target = path.join(scope, relative);
267
+ let stat;
268
+ try {
269
+ await this.resolve(target, false, false, readOnly);
270
+ stat = await fs.lstat(target);
271
+ }
272
+ catch {
273
+ continue;
274
+ }
275
+ if (!stat.isFile() && !stat.isDirectory())
276
+ continue;
277
+ entries.push({ name: path.basename(relative), path: path.join(displayedScope, relative), directory: stat.isDirectory(), match: relative, positions: hit.positions });
278
+ }
279
+ // Candidates the fence dropped are not matches; the unchecked tail is counted as is.
280
+ return { path: displayedScope, entries, total: entries.length + ranked.length - checked, truncated: index.truncated, source: index.source };
281
+ }
282
+ async expire() { await this.uploads.expire(); }
283
+ async dispose() { await this.uploads.dispose(); }
284
+ }
@@ -2,27 +2,26 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { constants } from 'node:fs';
3
3
  import { link, lstat, mkdir, open, readdir, rename, stat, unlink } from 'node:fs/promises';
4
4
  import { basename, dirname, join } from 'node:path';
5
- import { ComputerFileError, computerFileError, resolveComputerDirCreatePath, resolveComputerDirPath, resolveComputerFilePath } from './filePaths.js';
5
+ import { canonicalRoot, FileError, pinned, resolvePinnedDir, resolvePinnedDirCreate, resolvePinnedLeaf, resolveRelativeDir, resolveRelativeDirCreate, resolveRelativeFile, throwIfAborted, toFileError, withinRoot, } from './fence.js';
6
+ import { assertScopeAllows } from './scope.js';
7
+ /** Text file semantics of the platform agent tools (`file_*_request`): request-root scope only. */
6
8
  export const MAX_READ_BYTES = 8 * 1024 * 1024;
7
9
  export const MAX_PAGE_BYTES = 256 * 1024;
8
10
  export const MAX_WRITE_BYTES = 1024 * 1024;
9
- // Transport-free primitives. Untrusted channel requests must use fileWorkerClient, which pins the
10
- // parent directory before invoking these with resolvePinnedLeafPath; pathname-only resolution here
11
- // is retained for unit tests and trusted callers, not as a security boundary against rename races.
11
+ // Transport-free engines (formerly fileOps.ts). Untrusted requests must go through the scope entry points at the
12
+ // bottom of this file (or the legacy fork-per-request worker), which pin the parent directory before invoking these
13
+ // with a pinned leaf resolver; pathname-only resolution here is retained for unit tests and trusted callers, not as a
14
+ // security boundary against rename races.
12
15
  const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
13
- function throwIfAborted(signal) {
14
- if (signal?.aborted)
15
- throw new ComputerFileError('workstation_offline', 'File operation was cancelled');
16
- }
17
16
  async function snapshotFile(file, signal) {
18
17
  let handle;
19
18
  try {
20
19
  handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
21
20
  const before = await handle.stat({ bigint: true });
22
21
  if (!before.isFile())
23
- throw new ComputerFileError('not_regular_file', 'Target is not a regular file');
22
+ throw new FileError('not_regular_file', 'Target is not a regular file');
24
23
  if (before.size > BigInt(MAX_READ_BYTES))
25
- throw new ComputerFileError('too_large', 'File exceeds 8 MiB');
24
+ throw new FileError('too_large', 'File exceeds 8 MiB');
26
25
  const parts = [];
27
26
  let total = 0;
28
27
  while (true) {
@@ -33,25 +32,25 @@ async function snapshotFile(file, signal) {
33
32
  break;
34
33
  total += bytesRead;
35
34
  if (total > MAX_READ_BYTES)
36
- throw new ComputerFileError('too_large', 'File exceeds 8 MiB');
35
+ throw new FileError('too_large', 'File exceeds 8 MiB');
37
36
  parts.push(chunk.subarray(0, bytesRead));
38
37
  }
39
38
  const after = await handle.stat({ bigint: true });
40
39
  if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || BigInt(total) !== after.size) {
41
- throw new ComputerFileError('conflict', 'File changed during read');
40
+ throw new FileError('conflict', 'File changed during read');
42
41
  }
43
42
  return Buffer.concat(parts, total);
44
43
  }
45
44
  catch (error) {
46
- throw computerFileError(error);
45
+ throw toFileError(error);
47
46
  }
48
47
  finally {
49
48
  await handle?.close();
50
49
  }
51
50
  }
52
- export async function readComputerFile(request, signal, resolvePath = resolveComputerFilePath) {
51
+ export async function readTextFile(request, signal, resolvePath = resolveRelativeFile) {
53
52
  if (!Number.isSafeInteger(request.startLine) || request.startLine < 1 || !Number.isSafeInteger(request.maxLines) || request.maxLines < 1 || request.maxLines > 2000) {
54
- throw new ComputerFileError('invalid_argument', 'Invalid line range');
53
+ throw new FileError('invalid_argument', 'Invalid line range');
55
54
  }
56
55
  const file = await resolvePath(request.root, request.path, false);
57
56
  let bytes;
@@ -59,7 +58,7 @@ export async function readComputerFile(request, signal, resolvePath = resolveCom
59
58
  bytes = await snapshotFile(file, signal);
60
59
  }
61
60
  catch (error) {
62
- if (!(error instanceof ComputerFileError) || error.code !== 'conflict')
61
+ if (!(error instanceof FileError) || error.code !== 'conflict')
63
62
  throw error;
64
63
  bytes = await snapshotFile(file, signal);
65
64
  }
@@ -68,7 +67,7 @@ export async function readComputerFile(request, signal, resolvePath = resolveCom
68
67
  source = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
69
68
  }
70
69
  catch {
71
- throw new ComputerFileError('invalid_encoding', 'File is not valid UTF-8');
70
+ throw new FileError('invalid_encoding', 'File is not valid UTF-8');
72
71
  }
73
72
  const lines = [];
74
73
  for (let begin = 0; begin < source.length;) {
@@ -84,7 +83,7 @@ export async function readComputerFile(request, signal, resolvePath = resolveCom
84
83
  for (let index = first; index < lines.length && selected < request.maxLines; index++) {
85
84
  const lineBytes = Buffer.byteLength(lines[index], 'utf8');
86
85
  if (lineBytes > MAX_PAGE_BYTES)
87
- throw new ComputerFileError('line_too_long', 'A line exceeds 256 KiB');
86
+ throw new FileError('line_too_long', 'A line exceeds 256 KiB');
88
87
  if (pageBytes + lineBytes > MAX_PAGE_BYTES)
89
88
  break;
90
89
  content += lines[index];
@@ -129,17 +128,17 @@ function validUnicode(content) {
129
128
  }
130
129
  return true;
131
130
  }
132
- export async function writeComputerFile(request, signal, resolvePath = resolveComputerFilePath) {
131
+ export async function writeTextFile(request, signal, resolvePath = resolveRelativeFile) {
133
132
  if (request.content.includes('\0') || !validUnicode(request.content)) {
134
- throw new ComputerFileError('invalid_encoding', 'Content must be valid UTF-8 text without NUL');
133
+ throw new FileError('invalid_encoding', 'Content must be valid UTF-8 text without NUL');
135
134
  }
136
135
  const bytes = Buffer.from(request.content, 'utf8');
137
136
  if (bytes.length > MAX_WRITE_BYTES)
138
- throw new ComputerFileError('too_large', 'Content exceeds 1 MiB');
137
+ throw new FileError('too_large', 'Content exceeds 1 MiB');
139
138
  if (request.mode !== 'create' && request.mode !== 'replace')
140
- throw new ComputerFileError('invalid_argument', 'Invalid write mode');
139
+ throw new FileError('invalid_argument', 'Invalid write mode');
141
140
  if (request.mode === 'create' ? request.expectedSha256 !== undefined : !/^[0-9a-f]{64}$/.test(request.expectedSha256 ?? '')) {
142
- throw new ComputerFileError('invalid_argument', 'create forbids expected_sha256; replace requires lowercase SHA-256');
141
+ throw new FileError('invalid_argument', 'create forbids expected_sha256; replace requires lowercase SHA-256');
143
142
  }
144
143
  throwIfAborted(signal);
145
144
  const file = await resolvePath(request.root, request.path, request.mode === 'create');
@@ -150,12 +149,12 @@ export async function writeComputerFile(request, signal, resolvePath = resolveCo
150
149
  if (request.mode === 'replace') {
151
150
  const current = await snapshotFile(file, signal);
152
151
  if (hash(current) !== request.expectedSha256)
153
- throw new ComputerFileError('conflict', 'File changed since it was read');
152
+ throw new FileError('conflict', 'File changed since it was read');
154
153
  try {
155
154
  mode = (await stat(file)).mode & 0o777;
156
155
  }
157
156
  catch (error) {
158
- throw computerFileError(error);
157
+ throw toFileError(error);
159
158
  }
160
159
  }
161
160
  const parent = dirname(file);
@@ -178,18 +177,18 @@ export async function writeComputerFile(request, signal, resolvePath = resolveCo
178
177
  return { path: request.path, sizeBytes: bytes.length, sha256: hash(bytes), created: request.mode === 'create' };
179
178
  }
180
179
  catch (error) {
181
- throw computerFileError(error);
180
+ throw toFileError(error);
182
181
  }
183
182
  finally {
184
183
  await handle?.close();
185
184
  await unlink(temporary).catch(error => { if (error.code !== 'ENOENT')
186
- throw computerFileError(error); });
185
+ throw toFileError(error); });
187
186
  }
188
187
  });
189
188
  }
190
- export async function listComputerDir(request, signal, resolvePath = resolveComputerDirPath) {
189
+ export async function listTextDir(request, signal, resolvePath = resolveRelativeDir) {
191
190
  if (!Number.isSafeInteger(request.maxEntries) || request.maxEntries < 1 || request.maxEntries > 2000) {
192
- throw new ComputerFileError('invalid_argument', 'Invalid entry range');
191
+ throw new FileError('invalid_argument', 'Invalid entry range');
193
192
  }
194
193
  throwIfAborted(signal);
195
194
  const dir = await resolvePath(request.root, request.path);
@@ -220,7 +219,7 @@ export async function listComputerDir(request, signal, resolvePath = resolveComp
220
219
  }
221
220
  catch (error) {
222
221
  if (error.code !== 'ENOENT')
223
- throw computerFileError(error);
222
+ throw toFileError(error);
224
223
  continue;
225
224
  }
226
225
  }
@@ -232,7 +231,7 @@ export async function listComputerDir(request, signal, resolvePath = resolveComp
232
231
  catch (error) {
233
232
  // A file that vanished between readdir and lstat simply drops out of the snapshot.
234
233
  if (error.code !== 'ENOENT')
235
- throw computerFileError(error);
234
+ throw toFileError(error);
236
235
  continue;
237
236
  }
238
237
  }
@@ -241,16 +240,16 @@ export async function listComputerDir(request, signal, resolvePath = resolveComp
241
240
  const nextAfter = truncated ? entries[entries.length - 1]?.name : undefined;
242
241
  return { path: request.path, entries, truncated, ...(nextAfter !== undefined ? { nextAfter } : {}) };
243
242
  }
244
- export async function createComputerDir(request, signal, resolvePlan = resolveComputerDirCreatePath) {
243
+ export async function createTextDir(request, signal, resolvePlan = resolveRelativeDirCreate) {
245
244
  throwIfAborted(signal);
246
245
  const plan = await resolvePlan(request.root, request.path, request.parents);
247
246
  if (plan.missing.length === 0) {
248
247
  if (!request.parents)
249
- throw new ComputerFileError('conflict', 'Target already exists');
248
+ throw new FileError('conflict', 'Target already exists');
250
249
  return { path: request.path, created: false };
251
250
  }
252
251
  if (!request.parents && plan.missing.length > 1) {
253
- throw new ComputerFileError('not_found', 'Parent directory not found');
252
+ throw new FileError('not_found', 'Parent directory not found');
254
253
  }
255
254
  let cursor = plan.anchor;
256
255
  let created = false;
@@ -263,15 +262,95 @@ export async function createComputerDir(request, signal, resolvePlan = resolveCo
263
262
  }
264
263
  catch (error) {
265
264
  if (error.code !== 'EEXIST')
266
- throw computerFileError(error);
265
+ throw toFileError(error);
267
266
  if (!request.parents)
268
- throw new ComputerFileError('conflict', 'Target already exists');
267
+ throw new FileError('conflict', 'Target already exists');
269
268
  }
270
269
  // A component we just made (or found racing us) must be a real directory, never a symlink stand-in.
271
- const entry = await lstat(target).catch(error => { throw computerFileError(error); });
270
+ const entry = await lstat(target).catch(error => { throw toFileError(error); });
272
271
  if (entry.isSymbolicLink() || !entry.isDirectory())
273
- throw new ComputerFileError('conflict', 'Path is not a directory');
272
+ throw new FileError('conflict', 'Path is not a directory');
274
273
  cursor = target;
275
274
  }
276
275
  return { path: request.path, created };
277
276
  }
277
+ const targetLocks = new Map();
278
+ async function serialTarget(target, action) {
279
+ const previous = targetLocks.get(target) ?? Promise.resolve();
280
+ let release;
281
+ const gate = new Promise(resolve => { release = resolve; });
282
+ const queued = previous.then(() => gate);
283
+ targetLocks.set(target, queued);
284
+ await previous;
285
+ try {
286
+ return await action();
287
+ }
288
+ finally {
289
+ release();
290
+ if (targetLocks.get(target) === queued)
291
+ targetLocks.delete(target);
292
+ }
293
+ }
294
+ // The worker client checked for cancellation after pathname validation, when it was about to start the operation.
295
+ async function throwIfAbortedThen(signal, run) {
296
+ throwIfAborted(signal);
297
+ return run();
298
+ }
299
+ function requireRequestRoot(scope, operation) {
300
+ if (scope?.kind !== 'request-root')
301
+ throw new FileError('invalid_argument', 'Text file operations require a request root');
302
+ assertScopeAllows(scope, operation);
303
+ }
304
+ /**
305
+ * Runs `run` with the cwd pinned to `anchor`, which must still be inside the canonical root after chdir. Cancellation
306
+ * keeps the file worker's contract: a request cancelled once it is under way reports `outcome_unknown` for mutations
307
+ * and `workstation_offline` for reads.
308
+ */
309
+ async function anchored(operation, root, anchor, signal, hooks, run) {
310
+ try {
311
+ await hooks?.afterValidation?.();
312
+ return await pinned(anchor, async () => {
313
+ await hooks?.afterPinned?.();
314
+ throwIfAborted(signal);
315
+ return run();
316
+ }, { verify: withinRoot(root), signal });
317
+ }
318
+ catch (error) {
319
+ if (signal?.aborted)
320
+ throw new FileError(operation === 'write' || operation === 'mkdir' ? 'outcome_unknown' : 'workstation_offline', 'File operation was cancelled');
321
+ throw toFileError(error);
322
+ }
323
+ }
324
+ export async function readText(scope, request, signal, hooks) {
325
+ requireRequestRoot(scope, 'read');
326
+ const root = await canonicalRoot(scope.root);
327
+ const target = await resolveRelativeFile(root, request.path, false);
328
+ throwIfAborted(signal);
329
+ // As the file worker did, the result names the leaf it read.
330
+ return anchored('read', root, dirname(target), signal, hooks, () => readTextFile({ ...request, root: '.', path: basename(target) }, signal, resolvePinnedLeaf));
331
+ }
332
+ export async function writeText(scope, request, signal, hooks) {
333
+ requireRequestRoot(scope, 'write');
334
+ const root = await canonicalRoot(scope.root);
335
+ const target = await resolveRelativeFile(root, request.path, request.mode === 'create');
336
+ return serialTarget(target, () => throwIfAbortedThen(signal, () => anchored('write', root, dirname(target), signal, hooks, () => writeTextFile({ ...request, root: '.', path: basename(target) }, signal, resolvePinnedLeaf))));
337
+ }
338
+ export async function listDir(scope, request, signal, hooks) {
339
+ requireRequestRoot(scope, 'list');
340
+ const root = await canonicalRoot(scope.root);
341
+ const target = await resolveRelativeDir(root, request.path);
342
+ throwIfAborted(signal);
343
+ // Listing the root pins the root itself; '' is the zero-component leaf.
344
+ const anchor = target === root ? root : dirname(target);
345
+ const leaf = target === root ? '' : basename(target);
346
+ const data = await anchored('list', root, anchor, signal, hooks, () => listTextDir({ ...request, root: '.', path: leaf }, signal, resolvePinnedDir));
347
+ return { ...data, path: request.path };
348
+ }
349
+ export async function makeDir(scope, request, signal, hooks) {
350
+ requireRequestRoot(scope, 'mkdir');
351
+ const root = await canonicalRoot(scope.root);
352
+ const plan = await resolveRelativeDirCreate(root, request.path, request.parents);
353
+ throwIfAborted(signal);
354
+ const data = await anchored('mkdir', root, plan.anchor, signal, hooks, () => createTextDir({ ...request, root: '.', path: plan.missing.join('/') }, signal, resolvePinnedDirCreate));
355
+ return { ...data, path: request.path };
356
+ }