@deepseek-ai/dsh-api-workspace-files 0.1.5-alpha.1

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/lib/index.js ADDED
@@ -0,0 +1,551 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
3
+ import { Deque } from "@deepseek-ai/dsh-deque";
4
+ //#region lib/types/changes.js
5
+ /**
6
+ * Producer of the `changes` stream: every `fs/observed` emission whose target
7
+ * lies inside a generation's workspace root becomes one frame of that
8
+ * generation. Observations are emitted by tools after their own filesystem
9
+ * operation, so the feed covers Agent writes only; the OS is not watched.
10
+ * Each generation acknowledges its observation queue and resolved workspace
11
+ * root with `ready` before emitting any queued or live changes.
12
+ */
13
+ /** Owns `fs/observed` observation and every open `changes` generation. */
14
+ var WorkspaceChangeFeed = class {
15
+ ctx;
16
+ followers = /* @__PURE__ */ new Set();
17
+ /** @param ctx - Host context carrying the filesystem the observations come from. */
18
+ constructor(ctx) {
19
+ this.ctx = ctx;
20
+ ctx.on("fs/observed", (target, observation) => {
21
+ for (const follower of this.followers) follower.push([target, observation]);
22
+ });
23
+ ctx.effect(() => () => {
24
+ for (const follower of this.followers) follower.close();
25
+ this.followers.clear();
26
+ }, "workspace-files.changes");
27
+ }
28
+ /**
29
+ * Open one generation reporting observations inside `workspaceRoot`.
30
+ * @param workspaceRoot - the session's workspace root path.
31
+ * @param signal - generation cancellation.
32
+ * @returns `ready` after observation is active and the root resolves, then
33
+ * observations made after the generation was first pulled, in emission order.
34
+ */
35
+ async *follow(workspaceRoot, signal) {
36
+ signal.throwIfAborted();
37
+ const follower = new ChangeFollower();
38
+ this.followers.add(follower);
39
+ try {
40
+ const root = await this.ctx.fs.resolve(workspaceRoot, { signal }).catch((error) => {
41
+ if (signal.aborted) return void 0;
42
+ throw error;
43
+ });
44
+ if (root === void 0 || signal.aborted || follower.isClosed) return;
45
+ yield { kind: "ready" };
46
+ for await (const [target, observation] of follower.read(signal)) {
47
+ if (!this.ctx.fs.contains(root, target)) continue;
48
+ const absolutePath = this.ctx.fs.processPath(target);
49
+ yield {
50
+ kind: "change",
51
+ change: observation.kind === "present" ? {
52
+ absolutePath,
53
+ version: observation.version
54
+ } : {
55
+ absolutePath,
56
+ absent: true
57
+ }
58
+ };
59
+ }
60
+ } finally {
61
+ this.followers.delete(follower);
62
+ follower.close();
63
+ }
64
+ }
65
+ };
66
+ /** One generation's queue: observations wait here until its consumer pulls them. */
67
+ var ChangeFollower = class {
68
+ queue = new Deque();
69
+ wake;
70
+ closed = false;
71
+ /** Whether the generation was closed while its workspace root resolved. */
72
+ get isClosed() {
73
+ return this.closed;
74
+ }
75
+ push(observed) {
76
+ this.queue.pushBack(observed);
77
+ this.wake?.();
78
+ }
79
+ close() {
80
+ this.closed = true;
81
+ this.wake?.();
82
+ }
83
+ /** Drain until closed or aborted; anything still queued then is dropped with the generation. */
84
+ async *read(signal) {
85
+ const abort = () => {
86
+ this.close();
87
+ };
88
+ signal.addEventListener("abort", abort, { once: true });
89
+ if (signal.aborted) abort();
90
+ try {
91
+ while (!this.closed) {
92
+ const observed = this.queue.popFront();
93
+ if (observed !== void 0) {
94
+ yield observed;
95
+ continue;
96
+ }
97
+ await new Promise((resolve) => {
98
+ this.wake = resolve;
99
+ });
100
+ this.wake = void 0;
101
+ }
102
+ } finally {
103
+ signal.removeEventListener("abort", abort);
104
+ }
105
+ }
106
+ };
107
+ //#endregion
108
+ //#region lib/types/index.js
109
+ /**
110
+ * Workspace file service: paged text reads, byte-window reads, stats, directory
111
+ * listings, and the agent-write change feed inside one session's workspace
112
+ * root, exposed as the `workspaceFiles` Remote namespace.
113
+ *
114
+ * Reads through `ctx.fs` are deliberately unconfined — the sandboxing backend
115
+ * fences writes and edits only, and says so. Every constraint this service
116
+ * needs is therefore its own, and there are four:
117
+ *
118
+ * 1. The path is authorized by containment in the session's workspace root.
119
+ * 2. Containment is decided by {@link FileSystem.contains}, never by comparing
120
+ * path strings: `resolve` realpaths, so a prefix test cannot see a symlink
121
+ * that leaves the root. `lstat` rejects a link before that follow happens.
122
+ * 3. Every cap is validated Config, changeable per deployment. A page is cut by
123
+ * lines and refused, not shortened, when its bytes exceed the byte cap; a
124
+ * listing is cut by entries and says so.
125
+ * 4. Failures are one `RemoteError` per reason, declared in `./types`.
126
+ *
127
+ * A page is cut from `streamText`, which decodes and rejects non-UTF-8 as it
128
+ * goes, so the file is read only up to the first character past the page and
129
+ * never held whole in memory; the NUL scan runs on the page itself.
130
+ *
131
+ * This is NOT modelled on `session.openWorkspacePath`. That endpoint hands a
132
+ * path to the local opener and leaves the effect on the machine; this one sends
133
+ * file content across the wire, which is a different level of exposure.
134
+ */
135
+ var __runInitializers = function(thisArg, initializers, value) {
136
+ var useValue = arguments.length > 2;
137
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
138
+ return useValue ? value : void 0;
139
+ };
140
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
141
+ function accept(f) {
142
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
143
+ return f;
144
+ }
145
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
146
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
147
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
148
+ var _, done = false;
149
+ for (var i = decorators.length - 1; i >= 0; i--) {
150
+ var context = {};
151
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
152
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
153
+ context.addInitializer = function(f) {
154
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
155
+ extraInitializers.push(accept(f || null));
156
+ };
157
+ var result = (0, decorators[i])(kind === "accessor" ? {
158
+ get: descriptor.get,
159
+ set: descriptor.set
160
+ } : descriptor[key], context);
161
+ if (kind === "accessor") {
162
+ if (result === void 0) continue;
163
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
164
+ if (_ = accept(result.get)) descriptor.get = _;
165
+ if (_ = accept(result.set)) descriptor.set = _;
166
+ if (_ = accept(result.init)) initializers.unshift(_);
167
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
168
+ else descriptor[key] = _;
169
+ }
170
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
171
+ done = true;
172
+ };
173
+ /** The byte text never carries: its presence marks a page as binary. */
174
+ const NUL = String.fromCharCode(0);
175
+ /** Refuse anything the wire schema admits as a number but a window cannot use: only safe integers index a file. */
176
+ function integerAtLeast(value, min, name) {
177
+ if (!Number.isSafeInteger(value) || value < min) throw new RemoteError("gateway/bad-request", `${name} must be a safe integer of at least ${min}`, {});
178
+ return value;
179
+ }
180
+ /**
181
+ * Cut lines `offset` through `offset + limit - 1` from decoded chunks, stopping
182
+ * at the first character past the page so the rest of the file is never read.
183
+ * Lines before the page are counted, not kept, and the page is refused the
184
+ * moment its bytes exceed `maxBytes`, so one giant line cannot grow memory past
185
+ * the cap either.
186
+ */
187
+ async function cutPage(chunks, offset, limit, maxBytes, path) {
188
+ const last = offset + limit - 1;
189
+ const lines = [];
190
+ let current = "";
191
+ let bytes = 0;
192
+ let lineNumber = 1;
193
+ const admit = (size) => {
194
+ bytes += size;
195
+ if (bytes > maxBytes) throw new RemoteError("workspace-file/too-large", `lines ${offset}-${last} of "${path}" exceed the ${maxBytes} byte cap`, {
196
+ path,
197
+ limit: maxBytes
198
+ });
199
+ };
200
+ const complete = () => {
201
+ if (lines.length > 0) admit(1);
202
+ lines.push(current);
203
+ current = "";
204
+ };
205
+ for await (const chunk of chunks) {
206
+ let position = 0;
207
+ while (position < chunk.length) {
208
+ if (lineNumber > last) return {
209
+ text: lines.join("\n"),
210
+ lines: lines.length,
211
+ eof: false
212
+ };
213
+ const newline = chunk.indexOf("\n", position);
214
+ const segment = newline === -1 ? chunk.slice(position) : chunk.slice(position, newline);
215
+ if (lineNumber >= offset) {
216
+ admit(Buffer.byteLength(segment, "utf8"));
217
+ current += segment;
218
+ }
219
+ if (newline === -1) break;
220
+ if (lineNumber >= offset) complete();
221
+ lineNumber += 1;
222
+ position = newline + 1;
223
+ }
224
+ }
225
+ if (current.length > 0) complete();
226
+ return {
227
+ text: lines.join("\n"),
228
+ lines: lines.length,
229
+ eof: true
230
+ };
231
+ }
232
+ /**
233
+ * Workspace path of `target` relative to `root`, derived from the two canonical
234
+ * `file:` URIs so the answer is `/`-joined on every platform. Empty for the root.
235
+ */
236
+ function workspacePathOf(rootUrl, targetUrl) {
237
+ const root = new URL(rootUrl).pathname.replace(/\/+$/, "");
238
+ const target = new URL(targetUrl).pathname;
239
+ if (target === root) return "";
240
+ return target.slice(root.length + 1).split("/").map(decodeURIComponent).join("/");
241
+ }
242
+ /** Strip the resolved child target: the wire carries names and metadata only. */
243
+ function directoryEntry(child) {
244
+ return {
245
+ name: child.name,
246
+ type: child.type,
247
+ ...child.size === void 0 ? {} : { size: child.size }
248
+ };
249
+ }
250
+ /** Host Remote service over the composed filesystem, confined to one workspace. */
251
+ let WorkspaceFiles = (() => {
252
+ let _classSuper = TypertRemoteService;
253
+ let _instanceExtraInitializers = [];
254
+ let _read_decorators;
255
+ let _readBytes_decorators;
256
+ let _stat_decorators;
257
+ let _list_decorators;
258
+ let _changes_decorators;
259
+ return class WorkspaceFiles extends _classSuper {
260
+ static {
261
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
262
+ _read_decorators = [Remote];
263
+ _readBytes_decorators = [Remote];
264
+ _stat_decorators = [Remote];
265
+ _list_decorators = [Remote];
266
+ _changes_decorators = [Remote({ mode: "stream" })];
267
+ __esDecorate(this, null, _read_decorators, {
268
+ kind: "method",
269
+ name: "read",
270
+ static: false,
271
+ private: false,
272
+ access: {
273
+ has: (obj) => "read" in obj,
274
+ get: (obj) => obj.read
275
+ },
276
+ metadata: _metadata
277
+ }, null, _instanceExtraInitializers);
278
+ __esDecorate(this, null, _readBytes_decorators, {
279
+ kind: "method",
280
+ name: "readBytes",
281
+ static: false,
282
+ private: false,
283
+ access: {
284
+ has: (obj) => "readBytes" in obj,
285
+ get: (obj) => obj.readBytes
286
+ },
287
+ metadata: _metadata
288
+ }, null, _instanceExtraInitializers);
289
+ __esDecorate(this, null, _stat_decorators, {
290
+ kind: "method",
291
+ name: "stat",
292
+ static: false,
293
+ private: false,
294
+ access: {
295
+ has: (obj) => "stat" in obj,
296
+ get: (obj) => obj.stat
297
+ },
298
+ metadata: _metadata
299
+ }, null, _instanceExtraInitializers);
300
+ __esDecorate(this, null, _list_decorators, {
301
+ kind: "method",
302
+ name: "list",
303
+ static: false,
304
+ private: false,
305
+ access: {
306
+ has: (obj) => "list" in obj,
307
+ get: (obj) => obj.list
308
+ },
309
+ metadata: _metadata
310
+ }, null, _instanceExtraInitializers);
311
+ __esDecorate(this, null, _changes_decorators, {
312
+ kind: "method",
313
+ name: "changes",
314
+ static: false,
315
+ private: false,
316
+ access: {
317
+ has: (obj) => "changes" in obj,
318
+ get: (obj) => obj.changes
319
+ },
320
+ metadata: _metadata
321
+ }, null, _instanceExtraInitializers);
322
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
323
+ enumerable: true,
324
+ configurable: true,
325
+ writable: true,
326
+ value: _metadata
327
+ });
328
+ }
329
+ config = __runInitializers(this, _instanceExtraInitializers);
330
+ static inject = [
331
+ "fs",
332
+ "sandboxPolicy",
333
+ "typert"
334
+ ];
335
+ static Config = z.object({
336
+ maxBytes: z.number().step(1).min(1).default(2 * 1024 * 1024),
337
+ maxLines: z.number().step(1).min(1).default(5e3),
338
+ maxEntries: z.number().step(1).min(1).default(2e3)
339
+ });
340
+ feed;
341
+ /**
342
+ * @param ctx - Host context carrying the filesystem and the sandbox policy.
343
+ * @param config - deployment caps on one page or one listing.
344
+ */
345
+ constructor(ctx, config) {
346
+ super(ctx, "workspaceFiles");
347
+ this.config = config;
348
+ this.feed = new WorkspaceChangeFeed(ctx);
349
+ }
350
+ /**
351
+ * Read one page of lines from a UTF-8 text file inside the Agent's workspace.
352
+ * @param agent - target Agent resolved from the Session identity on the wire.
353
+ * @param path - workspace path, absolute or relative to the workspace root.
354
+ * @param range - the line window; omitted fields take the page defaults.
355
+ * @param signal - caller cancellation.
356
+ * @returns the page, the file's version at the stat before it, and whether it reaches the last line.
357
+ */
358
+ async read(agent, path, range, signal) {
359
+ const { offset, limit } = this.resolvePage(range);
360
+ const { target, info } = await this.locateFile(agent, path, signal);
361
+ const page = await this.cutPage(target, offset, limit, signal, path);
362
+ if (page.text.includes(NUL)) throw new RemoteError("workspace-file/not-text", `"${path}" contains NUL bytes`, { path });
363
+ return {
364
+ ...this.statOf(target, info),
365
+ offset,
366
+ text: page.text,
367
+ lines: page.lines,
368
+ eof: page.eof
369
+ };
370
+ }
371
+ /**
372
+ * Read one byte window of a regular file inside the Agent's workspace: raw
373
+ * bytes, no text decoding and no binary rejection.
374
+ * @param agent - target Agent resolved from the Session identity on the wire.
375
+ * @param path - workspace path, absolute or relative to the workspace root.
376
+ * @param range - the byte window; omitted fields take the window defaults.
377
+ * @param signal - caller cancellation.
378
+ * @returns the window in base64, the file's version and size at the stat before it, and whether it reaches the last byte.
379
+ */
380
+ async readBytes(agent, path, range, signal) {
381
+ const { offset, length } = this.resolveWindow(range, path);
382
+ const { target, info } = await this.locateFile(agent, path, signal);
383
+ const data = await this.ctx.fs.readByteRange(target, {
384
+ offset,
385
+ length
386
+ }, signal);
387
+ const eof = info.size === void 0 ? data.length < length : offset + data.length >= info.size;
388
+ return {
389
+ ...this.statOf(target, info),
390
+ offset,
391
+ data: Buffer.from(data).toString("base64"),
392
+ eof
393
+ };
394
+ }
395
+ /**
396
+ * Report one regular file's identity, version, and size without its content.
397
+ * @param agent - target Agent resolved from the Session identity on the wire.
398
+ * @param path - workspace path, absolute or relative to the workspace root.
399
+ * @param signal - caller cancellation.
400
+ * @returns the file's absolute path, current version, and byte size.
401
+ */
402
+ async stat(agent, path, signal) {
403
+ const { target, info } = await this.locateFile(agent, path, signal);
404
+ return this.statOf(target, info);
405
+ }
406
+ /**
407
+ * List the direct children of one directory inside the Agent's workspace.
408
+ * @param agent - target Agent resolved from the Session identity on the wire.
409
+ * @param path - workspace path, absolute or relative to the workspace root.
410
+ * @param signal - caller cancellation.
411
+ * @returns the directory's children in the backend's stable name order, bounded by the entry cap.
412
+ */
413
+ async list(agent, path, signal) {
414
+ const { root, workspaceRoot, entry } = await this.inspect(agent, path, signal);
415
+ if (entry.type !== "directory") throw new RemoteError("workspace-file/not-directory", `"${path}" is a ${entry.type}`, {
416
+ path,
417
+ kind: entry.type
418
+ });
419
+ const target = await this.confine(root, workspaceRoot, path, signal);
420
+ const children = await this.ctx.fs.listDir(target, signal);
421
+ return {
422
+ path: workspacePathOf(this.ctx.fs.fileUrl(root), this.ctx.fs.fileUrl(target)),
423
+ entries: children.slice(0, this.config.maxEntries).map(directoryEntry),
424
+ truncated: children.length > this.config.maxEntries
425
+ };
426
+ }
427
+ /**
428
+ * Stream every `fs/observed` observation of a file inside the Agent's
429
+ * workspace. Only Agent filesystem operations report here; the OS is not
430
+ * watched.
431
+ * @param agent - target Agent resolved from the Session identity on the wire.
432
+ * @param signal - generation cancellation.
433
+ * @returns `ready` once the Host observation queue is active and the workspace
434
+ * root is resolved, then queued and live observations in emission order.
435
+ */
436
+ changes(agent, signal) {
437
+ return this.feed.follow(this.workspaceRootOf(agent), signal);
438
+ }
439
+ /** Apply the page defaults and caps here, so the request never carries them implicitly. */
440
+ resolvePage(range) {
441
+ const offset = range.offset === void 0 ? 1 : integerAtLeast(range.offset, 1, "offset");
442
+ const limit = range.limit === void 0 ? this.config.maxLines : integerAtLeast(range.limit, 1, "limit");
443
+ if (limit > this.config.maxLines) throw new RemoteError("gateway/bad-request", `limit must be at most ${this.config.maxLines}`, {});
444
+ return {
445
+ offset,
446
+ limit
447
+ };
448
+ }
449
+ /** Apply the byte-window defaults and cap; a window above the cap is refused, not shortened. */
450
+ resolveWindow(range, path) {
451
+ const offset = range.offset === void 0 ? 0 : integerAtLeast(range.offset, 0, "offset");
452
+ const length = range.length === void 0 ? this.config.maxBytes : integerAtLeast(range.length, 1, "length");
453
+ if (offset + length > Number.MAX_SAFE_INTEGER) throw new RemoteError("gateway/bad-request", "offset plus length must stay a safe integer", {});
454
+ if (length > this.config.maxBytes) throw new RemoteError("workspace-file/too-large", `${length} bytes of "${path}" exceed the ${this.config.maxBytes} byte cap`, {
455
+ path,
456
+ limit: this.config.maxBytes
457
+ });
458
+ return {
459
+ offset,
460
+ length
461
+ };
462
+ }
463
+ /**
464
+ * The workspace root comes from the policy, not from the backend's own cwd
465
+ * default: the `minimal` preset shadows the host provider with a bare
466
+ * `fs-local` whose cwd differs, and resolving explicitly makes the answer
467
+ * the same whichever instance answers.
468
+ */
469
+ workspaceRootOf(agent) {
470
+ return this.ctx.sandboxPolicy.resolve({ session: agent.session }).workspaceRoot;
471
+ }
472
+ /**
473
+ * Gates 1 and 2 up to the point where the path's own type is known. The
474
+ * path is inspected before containment is decided, so a caller learns whether
475
+ * an outside path exists and what kind it is before `outside-workspace`
476
+ * refuses it; the caller is the Session's own owner, who can read the Host
477
+ * through the Agent anyway, and the accepted cost buys one `lstat` gate for
478
+ * every method instead of two resolution orders.
479
+ */
480
+ async inspect(agent, path, signal) {
481
+ if (path.length === 0) throw new RemoteError("gateway/bad-request", "path is required", {});
482
+ const workspaceRoot = this.workspaceRootOf(agent);
483
+ const root = await this.ctx.fs.resolve(workspaceRoot, { signal });
484
+ const entry = await this.ctx.fs.lstat(path, { cwd: workspaceRoot }, signal);
485
+ if (entry === void 0) throw new RemoteError("workspace-file/not-found", `no entry at "${path}"`, { path });
486
+ return {
487
+ root,
488
+ workspaceRoot,
489
+ entry
490
+ };
491
+ }
492
+ /** Resolve an inspected path and refuse it unless the workspace contains it. */
493
+ async confine(root, workspaceRoot, path, signal) {
494
+ const target = await this.ctx.fs.resolve(path, {
495
+ cwd: workspaceRoot,
496
+ signal
497
+ });
498
+ if (!this.ctx.fs.contains(root, target)) throw new RemoteError("workspace-file/outside-workspace", `"${path}" is outside the workspace`, { path });
499
+ return target;
500
+ }
501
+ /**
502
+ * All gates for a regular file, ending in the one stat that names its version
503
+ * and size. The stat re-checks what `lstat` saw: the file may have gone or
504
+ * changed kind in between.
505
+ */
506
+ async locateFile(agent, path, signal) {
507
+ const { root, workspaceRoot, entry } = await this.inspect(agent, path, signal);
508
+ if (entry.type !== "file") throw new RemoteError("workspace-file/not-regular-file", `"${path}" is a ${entry.type}`, {
509
+ path,
510
+ kind: entry.type
511
+ });
512
+ const target = await this.confine(root, workspaceRoot, path, signal);
513
+ const info = await this.ctx.fs.stat(target, signal);
514
+ if (info === void 0) throw new RemoteError("workspace-file/not-found", `no entry at "${path}"`, { path });
515
+ if (info.type !== "file") throw new RemoteError("workspace-file/not-regular-file", `"${path}" is a ${info.type}`, {
516
+ path,
517
+ kind: info.type
518
+ });
519
+ return {
520
+ target,
521
+ info
522
+ };
523
+ }
524
+ statOf(target, info) {
525
+ return {
526
+ absolutePath: this.ctx.fs.processPath(target),
527
+ version: info.version,
528
+ ...info.size === void 0 ? {} : { bytes: info.size }
529
+ };
530
+ }
531
+ /** Stream the file as text and cut the page, classifying the backend's non-text refusal. */
532
+ async cutPage(target, offset, limit, signal, path) {
533
+ try {
534
+ return await cutPage(await this.ctx.fs.streamText(target, signal), offset, limit, this.config.maxBytes, path);
535
+ } catch (error) {
536
+ if (isNotTextRefusal(error)) throw new RemoteError("workspace-file/not-text", `"${path}" is not UTF-8 text`, { path }, { cause: error });
537
+ throw error;
538
+ }
539
+ }
540
+ };
541
+ })();
542
+ /**
543
+ * The backend's non-text refusal, recognized by its code alone: the error class
544
+ * belongs to whichever `dsh-fs` instance the provider loaded, so no class
545
+ * identity is shared across the package boundary.
546
+ */
547
+ function isNotTextRefusal(error) {
548
+ return typeof error === "object" && error !== null && "code" in error && error.code === "FS_NOT_TEXT";
549
+ }
550
+ //#endregion
551
+ export { WorkspaceFiles, WorkspaceFiles as default };
@@ -0,0 +1,3 @@
1
+ /* Generated by @deepseek-ai/dsh-typert-generator from FaceModel — do not edit. */
2
+
3
+ export declare const TYPERT: unknown