toolcraft 0.0.864 → 0.0.865

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/composition.json CHANGED
@@ -138,7 +138,7 @@
138
138
  },
139
139
  {
140
140
  "name": "toolcraft",
141
- "version": "0.0.864",
141
+ "version": "0.0.865",
142
142
  "license": "MIT"
143
143
  },
144
144
  {
@@ -148,7 +148,7 @@
148
148
  },
149
149
  {
150
150
  "name": "toolcraft-schema",
151
- "version": "0.0.864",
151
+ "version": "0.0.865",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -138,7 +138,7 @@
138
138
  },
139
139
  {
140
140
  "name": "toolcraft",
141
- "version": "0.0.864",
141
+ "version": "0.0.865",
142
142
  "license": "MIT"
143
143
  },
144
144
  {
@@ -148,7 +148,7 @@
148
148
  },
149
149
  {
150
150
  "name": "toolcraft-schema",
151
- "version": "0.0.864",
151
+ "version": "0.0.865",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -119,6 +119,8 @@ identities without a default device-count budget. Their optional `maxDevices` ac
119
119
  `Infinity` or a positive safe integer. Explicit budgets remain enforced; device numbers
120
120
  must still fit the disjoint application/runtime ranges of the uint32 Python stat ABI.
121
121
 
122
+ Python filesystem directory cursors use `iterateDirectory` when available, share open-handle admission with files, and drain pending reads before cleanup. Backends without lazy enumeration retain the buffered `readdir` fallback.
123
+
122
124
  `withObjectFileDescriptors(fs, store)` supports large shell and Python descriptor
123
125
  writes when the host supplies `store.createStaging`: private externally backed
124
126
  pages keep working memory bounded without publishing the growing file after
@@ -0,0 +1,9 @@
1
+ import type { FileSystem, FsOptions } from '../contracts/filesystem.js';
2
+ /** One prefetched entry makes acquisition errors eager without buffering a listing. */
3
+ export declare class PythonDirectory {
4
+ #private;
5
+ constructor(fs: FileSystem, path: string, options: FsOptions, limit: number);
6
+ prepare(): Promise<void>;
7
+ next(): Promise<string | null>;
8
+ close(): Promise<void>;
9
+ }
@@ -0,0 +1,38 @@
1
+ import { FsError } from '../contracts/errors.js';
2
+ /** One prefetched entry makes acquisition errors eager without buffering a listing. */
3
+ export class PythonDirectory {
4
+ #first;
5
+ #tail = Promise.resolve();
6
+ #closing;
7
+ #count = 0;
8
+ #iterator;
9
+ #options;
10
+ #limit;
11
+ constructor(fs, path, options, limit) {
12
+ this.#options = options;
13
+ this.#limit = limit;
14
+ const source = fs.iterateDirectory ? fs.iterateDirectory(path, options) : (async function* () { yield* await fs.readdir(path, { ...options, ...limit === Infinity ? {} : { maxEntries: limit } }); })();
15
+ this.#iterator = source[Symbol.asyncIterator]();
16
+ }
17
+ async prepare() { this.#first = await this.#iterator.next(); }
18
+ next() {
19
+ if (this.#closing)
20
+ return Promise.reject(new FsError('EBADF'));
21
+ const work = this.#tail.then(async () => {
22
+ this.#options.signal?.throwIfAborted();
23
+ const value = this.#first ?? await this.#iterator.next();
24
+ this.#first = undefined;
25
+ this.#options.signal?.throwIfAborted();
26
+ if (value.done)
27
+ return null;
28
+ if (++this.#count > this.#limit)
29
+ throw new FsError('EFBIG', { syscall: 'readdir' });
30
+ return value.value.name;
31
+ });
32
+ this.#tail = work.catch(() => { });
33
+ return work;
34
+ }
35
+ close() {
36
+ return this.#closing ??= this.#tail.then(async () => { this.#first = undefined; await this.#iterator.return?.(); });
37
+ }
38
+ }
@@ -9,7 +9,7 @@ export type PythonFsRequest = {
9
9
  readonly op: "write";
10
10
  readonly args: readonly [number, Uint8Array, number | null];
11
11
  } | {
12
- readonly op: "fstat" | "close" | "position" | "descriptorCapabilities";
12
+ readonly op: "directoryNext" | "fstat" | "close" | "position" | "descriptorCapabilities";
13
13
  readonly args: readonly [number];
14
14
  } | {
15
15
  readonly op: "ftruncate";
@@ -18,7 +18,7 @@ export type PythonFsRequest = {
18
18
  readonly op: "sync";
19
19
  readonly args: readonly [number, boolean];
20
20
  } | {
21
- readonly op: "stat" | "lstat" | "readdir" | "realpath" | "readlink" | "rm" | "rmdir" | "rmtree" | "rmtreeSupported";
21
+ readonly op: "directoryOpen" | "stat" | "lstat" | "readdir" | "realpath" | "readlink" | "rm" | "rmdir" | "rmtree" | "rmtreeSupported";
22
22
  readonly args: readonly [string];
23
23
  } | {
24
24
  readonly op: "rename" | "symlink" | "link";
@@ -1,3 +1,4 @@
1
+ import { PythonDirectory } from "./directory.js";
1
2
  import { parsePythonFsRequest } from "./request.js";
2
3
  import { composeAbortSignals } from "../contracts/abort.js";
3
4
  import { FsError } from "../contracts/errors.js";
@@ -57,10 +58,37 @@ export class PythonFileSystem {
57
58
  }
58
59
  #handle(id) {
59
60
  const handle = this.#handles.get(id);
60
- if (!handle)
61
+ if (!handle || handle instanceof PythonDirectory)
61
62
  throw new FsError("EBADF");
62
63
  return handle;
63
64
  }
65
+ async #acquire(open) {
66
+ if (this.#handles.size + this.#acquiring >= this.#limit || !Number.isSafeInteger(this.#next))
67
+ throw new FsError("EMFILE", { syscall: "open" });
68
+ this.#acquiring++;
69
+ let handle;
70
+ try {
71
+ handle = await open();
72
+ if (handle instanceof PythonDirectory)
73
+ await handle.prepare();
74
+ this.#scope.signal.throwIfAborted();
75
+ const id = this.#next++;
76
+ this.#handles.set(id, handle);
77
+ return id;
78
+ }
79
+ catch (error) {
80
+ if (handle)
81
+ try {
82
+ await handle.close();
83
+ }
84
+ catch { /* Preserve acquisition failure. */ }
85
+ this.#scope.signal.throwIfAborted();
86
+ throw error;
87
+ }
88
+ finally {
89
+ this.#acquiring--;
90
+ }
91
+ }
64
92
  #size(size) {
65
93
  if (!Number.isSafeInteger(size) || size < 0)
66
94
  throw new FsError("EINVAL");
@@ -70,7 +98,9 @@ export class PythonFileSystem {
70
98
  async #execute(request) {
71
99
  if (request.op === "close") {
72
100
  const [id] = request.args;
73
- const handle = this.#handle(id);
101
+ const handle = this.#handles.get(id);
102
+ if (!handle)
103
+ throw new FsError("EBADF");
74
104
  this.#handles.delete(id);
75
105
  await handle.close({ signal: this.#scope.signal });
76
106
  return;
@@ -82,40 +112,25 @@ export class PythonFileSystem {
82
112
  switch (request.op) {
83
113
  case "open": {
84
114
  const [path, supplied] = request.args;
85
- if (this.#handles.size + this.#acquiring >= this.#limit || !Number.isSafeInteger(this.#next))
86
- throw new FsError("EMFILE", { syscall: "open" });
87
- if (!this.#open && !fs.open)
88
- throw new FsError("ENOTSUP", { syscall: "open", path });
89
- this.#acquiring++;
90
- let handle;
91
- try {
92
- // A following capability query cannot preflight exclusive final-entry acquisition.
93
- // The canonical open remains authoritative for selected-path policy and atomic refusal.
115
+ return this.#acquire(async () => {
116
+ if (!this.#open && !fs.open)
117
+ throw new FsError("ENOTSUP", { syscall: "open", path });
118
+ // Exclusive creation must reach the canonical atomic operation directly.
94
119
  const capabilities = supplied.creation !== "exclusive" && fs.capabilitiesFor ? await fs.capabilitiesFor(this.#path(path), { signal, ...(supplied.creation === "ifMissing" ? { create: true } : {}) }) : fs.capabilities;
95
120
  if (capabilities.readOnly === true && (supplied.access === "write" || supplied.access === "readwrite" || supplied.creation === "ifMissing" || supplied.creation === "exclusive" || supplied.truncate === true || supplied.append === true))
96
121
  throw new FsError("EROFS", { syscall: "open", path });
97
122
  if (capabilities.open === false)
98
123
  throw new FsError("ENOTSUP", { syscall: "open", path });
99
124
  signal.throwIfAborted();
100
- const open = this.#open ?? fs.open.bind(fs);
101
- handle = await open(this.#path(path), { ...supplied, signal });
102
- signal.throwIfAborted();
103
- const id = this.#next++;
104
- this.#handles.set(id, handle);
105
- return id;
106
- }
107
- catch (error) {
108
- if (handle)
109
- try {
110
- await handle.close();
111
- }
112
- catch { /* Preserve the acquisition failure. */ }
113
- signal.throwIfAborted();
114
- throw error;
115
- }
116
- finally {
117
- this.#acquiring--;
118
- }
125
+ return (this.#open ?? fs.open.bind(fs))(this.#path(path), { ...supplied, signal });
126
+ });
127
+ }
128
+ case "directoryOpen": return this.#acquire(() => new PythonDirectory(fs, this.#path(request.args[0]), options, this.#directoryLimit));
129
+ case "directoryNext": {
130
+ const handle = this.#handles.get(request.args[0]);
131
+ if (!(handle instanceof PythonDirectory))
132
+ throw new FsError("EBADF");
133
+ return handle.next();
119
134
  }
120
135
  case "descriptorCapabilities": return { ...this.#handle(request.args[0]).capabilities };
121
136
  case "fstat": return this.#handle(request.args[0]).stat(options);
@@ -2,6 +2,7 @@ import { FsError } from "../contracts/errors.js";
2
2
  const argumentKinds = {
3
3
  open: ["string", "object"], read: ["number", "number", "position"], write: ["number", "bytes", "position"],
4
4
  descriptorCapabilities: ["number"], fstat: ["number"], close: ["number"], position: ["number"], ftruncate: ["number", "number"], sync: ["number", "boolean"],
5
+ directoryOpen: ["string"], directoryNext: ["number"],
5
6
  stat: ["string"], lstat: ["string"], readdir: ["string"], realpath: ["string"], readlink: ["string"], rm: ["string"], rmdir: ["string"], rmtree: ["string"], rmtreeSupported: ["string"],
6
7
  rename: ["string", "string"], symlink: ["string", "string"], link: ["string", "string"], mkdir: ["string", "optionalObject"],
7
8
  chmod: ["string", "number"], truncate: ["string", "number"], access: ["string", "number"], utimes: ["string", "number", "number"],
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.864",
11
+ "version": "0.0.865",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.864",
3
+ "version": "0.0.865",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.864",
3
+ "version": "0.0.865",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -177,7 +177,7 @@
177
177
  "saxes"
178
178
  ],
179
179
  "optionalDependencies": {
180
- "toolcraft-schema": "0.0.864",
180
+ "toolcraft-schema": "0.0.865",
181
181
  "toolcraft-design": "*",
182
182
  "@poe-code/frontmatter": "*",
183
183
  "@poe-code/agent-mcp-config": "*",