@demicodes/shell 0.14.1 → 0.15.0

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.
@@ -1,6 +1,15 @@
1
1
  //#region src/host.d.ts
2
2
  interface Host {
3
3
  defaultCwd: string;
4
+ /**
5
+ * Directory where command artifacts (stdout.txt / stderr.txt / stdout.bin /
6
+ * meta.json) are written as plain files, laid out as
7
+ * `<dir>/<storageId>/<commandId>/`. Contract: the path is reachable through
8
+ * `fs` AND visible to processes started via `process.spawn` — one shared
9
+ * filesystem namespace, so any tool (portable or real) can read and search
10
+ * artifacts with ordinary file operations.
11
+ */
12
+ commandArtifactsDir: string;
4
13
  fs: HostFileSystem;
5
14
  process: HostProcess;
6
15
  store: HostStore;
@@ -1,28 +1,21 @@
1
- import { t as Host } from "./host-DIg4RxcL.mjs";
1
+ import { t as Host } from "./host-B-oKG7x5.mjs";
2
2
  import { BufferEncoding, CpOptions, DirentEntry, FileContent, FsStat, IFileSystem, MkdirOptions, ReadFileOptions, RmOptions, WriteFileOptions } from "@demicodes/just-bash/fs/interface";
3
3
  //#region src/host-fs.d.ts
4
- type VirtualFileSystemNode = {
5
- kind: 'file';
6
- content: Uint8Array;
7
- mode?: number;
8
- mtime?: Date;
9
- } | {
10
- kind: 'directory';
11
- entries: DirentEntry[];
12
- mode?: number;
13
- mtime?: Date;
14
- };
15
- interface VirtualFileSystemProvider {
16
- lookup(path: string): Promise<VirtualFileSystemNode | null> | VirtualFileSystemNode | null;
4
+ interface HostBackedFileSystemOptions {
5
+ /**
6
+ * Ceiling for a single file read through this filesystem. Portable commands
7
+ * (cat/grep/rg/head/...) run in-process and load files whole, so one read of
8
+ * an oversized file becomes resident memory in the embedding host — a broad
9
+ * scan over a large tree can balloon the process by gigabytes. Oversized
10
+ * files fail loudly instead; real-process routes (`bash -c`) stay available
11
+ * for genuinely large files.
12
+ */
13
+ maxFileReadBytes?: number;
17
14
  }
18
- /** Builds a virtual directory node; entry names containing a dot are treated as files. */
19
- declare function virtualDirectory(names: string[]): VirtualFileSystemNode;
20
- /** Builds a virtual file node from raw bytes. */
21
- declare function virtualFile(content: Uint8Array): VirtualFileSystemNode;
22
15
  declare class HostBackedFileSystem implements IFileSystem {
23
16
  private readonly host;
24
- private readonly virtualProvider?;
25
- constructor(host: Host, virtualProvider?: VirtualFileSystemProvider | undefined);
17
+ private readonly maxFileReadBytes;
18
+ constructor(host: Host, options?: HostBackedFileSystemOptions);
26
19
  readFile(path: string, options?: ReadFileOptions | BufferEncoding): Promise<string>;
27
20
  readFileBuffer(path: string): Promise<Uint8Array>;
28
21
  writeFile(path: string, content: FileContent, options?: WriteFileOptions | BufferEncoding): Promise<void>;
@@ -44,8 +37,6 @@ declare class HostBackedFileSystem implements IFileSystem {
44
37
  readlink(path: string): Promise<string>;
45
38
  realpath(path: string): Promise<string>;
46
39
  utimes(path: string, atime: Date, mtime: Date): Promise<void>;
47
- private lookupVirtual;
48
- private assertWritablePath;
49
40
  }
50
41
  //#endregion
51
- export { HostBackedFileSystem, VirtualFileSystemNode, VirtualFileSystemProvider, virtualDirectory, virtualFile };
42
+ export { HostBackedFileSystem, HostBackedFileSystemOptions };
package/dist/host-fs.mjs CHANGED
@@ -1,78 +1,40 @@
1
1
  import { decodeUtf8, encodeUtf8, isAbsolutePath, normalizePath } from "@demicodes/utils";
2
2
  //#region src/host-fs.ts
3
- /** Builds a virtual directory node; entry names containing a dot are treated as files. */
4
- function virtualDirectory(names) {
5
- return {
6
- kind: "directory",
7
- entries: names.sort().map((name) => ({
8
- name,
9
- isFile: name.includes("."),
10
- isDirectory: !name.includes("."),
11
- isSymbolicLink: false
12
- }))
13
- };
14
- }
15
- /** Builds a virtual file node from raw bytes. */
16
- function virtualFile(content) {
17
- return {
18
- kind: "file",
19
- content
20
- };
21
- }
3
+ const DEFAULT_FILE_READ_LIMIT_BYTES = 67108864;
22
4
  var HostBackedFileSystem = class {
23
5
  host;
24
- virtualProvider;
25
- constructor(host, virtualProvider) {
6
+ maxFileReadBytes;
7
+ constructor(host, options = {}) {
26
8
  this.host = host;
27
- this.virtualProvider = virtualProvider;
9
+ this.maxFileReadBytes = options.maxFileReadBytes ?? DEFAULT_FILE_READ_LIMIT_BYTES;
28
10
  }
29
11
  async readFile(path, options) {
30
12
  return decodeBytes(await this.readFileBuffer(path), encodingFrom(options));
31
13
  }
32
14
  async readFileBuffer(path) {
33
- const virtual = await this.lookupVirtual(path);
34
- if (virtual) {
35
- if (virtual.kind !== "file") throw new Error(`EISDIR: illegal operation on a directory, read '${path}'`);
36
- return virtual.content;
37
- }
15
+ const stat = await this.host.fs.stat(path, { cwd: this.host.defaultCwd });
16
+ if (stat.isFile && stat.size > this.maxFileReadBytes) throw new Error(`EFBIG: file exceeds the ${this.maxFileReadBytes}-byte in-shell read limit (${stat.size} bytes), read '${path}'; read a slice instead, or process it with a real process via 'bash -c'`);
38
17
  return this.host.fs.readFile(path, { cwd: this.host.defaultCwd });
39
18
  }
40
19
  async writeFile(path, content, options) {
41
- this.assertWritablePath(path);
42
20
  await this.host.fs.writeFile(path, encodeContent(content, encodingFrom(options)), { cwd: this.host.defaultCwd });
43
21
  }
44
22
  async appendFile(path, content, options) {
45
- this.assertWritablePath(path);
46
23
  await this.host.fs.appendFile(path, encodeContent(content, encodingFrom(options)), { cwd: this.host.defaultCwd });
47
24
  }
48
25
  async exists(path) {
49
- if (isVirtualPath(path)) return await this.lookupVirtual(path) !== null;
50
26
  return this.host.fs.exists(path, { cwd: this.host.defaultCwd });
51
27
  }
52
28
  async stat(path) {
53
- const virtual = await this.lookupVirtual(path);
54
- if (virtual) return virtualStat(virtual);
55
29
  return toFsStat(await this.host.fs.stat(path, { cwd: this.host.defaultCwd }));
56
30
  }
57
31
  async lstat(path) {
58
- const virtual = await this.lookupVirtual(path);
59
- if (virtual) return virtualStat(virtual);
60
32
  return toFsStat(await this.host.fs.lstat(path, { cwd: this.host.defaultCwd }));
61
33
  }
62
34
  async readdir(path) {
63
- const virtual = await this.lookupVirtual(path);
64
- if (virtual) {
65
- if (virtual.kind !== "directory") throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
66
- return virtual.entries.map((entry) => entry.name);
67
- }
68
35
  return this.host.fs.readdir(path, { cwd: this.host.defaultCwd });
69
36
  }
70
37
  async readdirWithFileTypes(path) {
71
- const virtual = await this.lookupVirtual(path);
72
- if (virtual) {
73
- if (virtual.kind !== "directory") throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
74
- return virtual.entries;
75
- }
76
38
  return (await this.host.fs.readdir(path, {
77
39
  cwd: this.host.defaultCwd,
78
40
  withFileTypes: true
@@ -86,14 +48,12 @@ var HostBackedFileSystem = class {
86
48
  return [];
87
49
  }
88
50
  async mkdir(path, options) {
89
- this.assertWritablePath(path);
90
51
  await this.host.fs.mkdir(path, {
91
52
  cwd: this.host.defaultCwd,
92
53
  recursive: options?.recursive
93
54
  });
94
55
  }
95
56
  async rm(path, options) {
96
- this.assertWritablePath(path);
97
57
  await this.host.fs.rm(path, {
98
58
  cwd: this.host.defaultCwd,
99
59
  recursive: options?.recursive,
@@ -101,62 +61,32 @@ var HostBackedFileSystem = class {
101
61
  });
102
62
  }
103
63
  async cp(src, dest, options) {
104
- this.assertWritablePath(dest);
105
- if (isVirtualPath(src)) {
106
- const virtual = await this.lookupVirtual(src);
107
- if (!virtual) throw new Error(`ENOENT: no such file or directory, copyfile '${src}'`);
108
- if (virtual.kind !== "file") throw new Error(`EISDIR: illegal operation on a directory, copyfile '${src}'`);
109
- await this.host.fs.writeFile(dest, virtual.content, {
110
- cwd: this.host.defaultCwd,
111
- createParents: options?.recursive
112
- });
113
- return;
114
- }
115
64
  await this.host.fs.cp(src, dest, {
116
65
  cwd: this.host.defaultCwd,
117
66
  recursive: options?.recursive
118
67
  });
119
68
  }
120
69
  async mv(src, dest) {
121
- this.assertWritablePath(src);
122
- this.assertWritablePath(dest);
123
70
  await this.host.fs.mv(src, dest, { cwd: this.host.defaultCwd });
124
71
  }
125
72
  async chmod(path, mode) {
126
- this.assertWritablePath(path);
127
73
  await this.host.fs.chmod(path, mode, { cwd: this.host.defaultCwd });
128
74
  }
129
75
  async symlink(target, linkPath) {
130
- this.assertWritablePath(linkPath);
131
76
  await this.host.fs.symlink(target, linkPath, { cwd: this.host.defaultCwd });
132
77
  }
133
78
  async link(existingPath, newPath) {
134
- this.assertWritablePath(existingPath);
135
- this.assertWritablePath(newPath);
136
79
  await this.host.fs.link(existingPath, newPath, { cwd: this.host.defaultCwd });
137
80
  }
138
81
  async readlink(path) {
139
- if (isVirtualPath(path)) throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
140
82
  return this.host.fs.readlink(path, { cwd: this.host.defaultCwd });
141
83
  }
142
84
  async realpath(path) {
143
- if (isVirtualPath(path)) {
144
- if (!await this.lookupVirtual(path)) throw new Error(`ENOENT: no such file or directory, realpath '${path}'`);
145
- return normalizePath(path);
146
- }
147
85
  return this.host.fs.realpath(path, { cwd: this.host.defaultCwd });
148
86
  }
149
87
  async utimes(path, atime, mtime) {
150
- this.assertWritablePath(path);
151
88
  await this.host.fs.utimes(path, atime, mtime, { cwd: this.host.defaultCwd });
152
89
  }
153
- async lookupVirtual(path) {
154
- if (!this.virtualProvider || !isVirtualPath(path)) return null;
155
- return await this.virtualProvider.lookup(normalizePath(path)) ?? null;
156
- }
157
- assertWritablePath(path) {
158
- if (isVirtualPath(path)) throw new Error(`EROFS: read-only virtual filesystem, '${path}'`);
159
- }
160
90
  };
161
91
  function encodingFrom(options) {
162
92
  if (!options) return void 0;
@@ -166,13 +96,13 @@ function encodingFrom(options) {
166
96
  function encodeContent(content, encoding) {
167
97
  if (content instanceof Uint8Array) return content;
168
98
  if (encoding === "binary" || encoding === "latin1") return latin1ToBytes(content);
169
- if (encoding === "base64") return base64ToBytes$1(content);
99
+ if (encoding === "base64") return base64ToBytes(content);
170
100
  if (encoding === "hex") return hexToBytes(content);
171
101
  return encodeUtf8(content);
172
102
  }
173
103
  function decodeBytes(bytes, encoding) {
174
104
  if (encoding === "binary" || encoding === "latin1") return bytesToLatin1(bytes);
175
- if (encoding === "base64") return bytesToBase64$1(bytes);
105
+ if (encoding === "base64") return bytesToBase64(bytes);
176
106
  if (encoding === "hex") return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
177
107
  return decodeUtf8(bytes);
178
108
  }
@@ -186,10 +116,10 @@ function bytesToLatin1(bytes) {
186
116
  for (let index = 0; index < bytes.length; index += 8192) result += String.fromCharCode(...bytes.subarray(index, index + 8192));
187
117
  return result;
188
118
  }
189
- function base64ToBytes$1(content) {
119
+ function base64ToBytes(content) {
190
120
  return latin1ToBytes(atob(content));
191
121
  }
192
- function bytesToBase64$1(bytes) {
122
+ function bytesToBase64(bytes) {
193
123
  return btoa(bytesToLatin1(bytes));
194
124
  }
195
125
  function hexToBytes(content) {
@@ -215,28 +145,5 @@ function toDirentEntry(value) {
215
145
  isSymbolicLink: value.isSymbolicLink
216
146
  };
217
147
  }
218
- function virtualStat(node) {
219
- const now = /* @__PURE__ */ new Date(0);
220
- if (node.kind === "directory") return {
221
- isFile: false,
222
- isDirectory: true,
223
- isSymbolicLink: false,
224
- mode: node.mode ?? 365,
225
- size: 0,
226
- mtime: node.mtime ?? now
227
- };
228
- return {
229
- isFile: true,
230
- isDirectory: false,
231
- isSymbolicLink: false,
232
- mode: node.mode ?? 292,
233
- size: node.content.byteLength,
234
- mtime: node.mtime ?? now
235
- };
236
- }
237
- function isVirtualPath(path) {
238
- const normalized = normalizePath(path);
239
- return normalized === "/@" || normalized.startsWith("/@/");
240
- }
241
148
  //#endregion
242
- export { HostBackedFileSystem, virtualDirectory, virtualFile };
149
+ export { HostBackedFileSystem };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as HostProcess, c as HostSpawnHandle, i as HostFileSystem, l as HostSpawnParams, n as HostDirent, o as HostProcessOutputChunk, r as HostFileStat, s as HostSpawnExit, t as Host, u as HostStore } from "./host-DIg4RxcL.mjs";
2
- import { HostBackedFileSystem, VirtualFileSystemNode, VirtualFileSystemProvider, virtualDirectory, virtualFile } from "./host-fs.mjs";
3
- import { _ as runRegisteredCommand, a as CommandIO, c as CommandRegistry, d as CommandStdin, f as CommandStorage, g as renderCommandHelp, h as parseCommandInput, i as CommandExecutionContext, l as CommandRunContext, m as emptyStdin, n as COMMAND_HELP_DEFAULTS, o as CommandInputSpec, p as ParsedCommandInput, r as Command, s as CommandOutputSpec, t as AgentSessionCommandStorage, u as CommandRunResult } from "./storage-CNMBL9_j.mjs";
1
+ import { a as HostProcess, c as HostSpawnHandle, i as HostFileSystem, l as HostSpawnParams, n as HostDirent, o as HostProcessOutputChunk, r as HostFileStat, s as HostSpawnExit, t as Host, u as HostStore } from "./host-B-oKG7x5.mjs";
2
+ import { HostBackedFileSystem, HostBackedFileSystemOptions } from "./host-fs.mjs";
3
+ import { _ as runRegisteredCommand, a as CommandIO, c as CommandRegistry, d as CommandStdin, f as CommandStorage, g as renderCommandHelp, h as parseCommandInput, i as CommandExecutionContext, l as CommandRunContext, m as emptyStdin, n as COMMAND_HELP_DEFAULTS, o as CommandInputSpec, p as ParsedCommandInput, r as Command, s as CommandOutputSpec, t as AgentSessionCommandStorage, u as CommandRunResult } from "./storage-DYvEwSc9.mjs";
4
4
  import { CommandName } from "@demicodes/just-bash/commands";
5
5
  import { Interpreter, InterpreterState } from "@demicodes/just-bash/interpreter";
6
6
  import { CommandRegistry as CommandRegistry$1, ExecResult } from "@demicodes/just-bash/types";
@@ -38,6 +38,9 @@ interface BackgroundJob {
38
38
  handle: HostSpawnHandle;
39
39
  stdoutBuffer: string;
40
40
  stderrBuffer: string;
41
+ /** Chars discarded from the head of each buffer once it outgrew the capture limit. */
42
+ droppedStdoutChars: number;
43
+ droppedStderrChars: number;
41
44
  stdoutPump: Promise<void>;
42
45
  stderrPump: Promise<void>;
43
46
  exitPromise: Promise<{
@@ -66,6 +69,10 @@ interface ForegroundProcess {
66
69
  /** Interleaved visible chunks with running byte offsets, for merged replay. */
67
70
  outputChunks: ShellOutputRecordChunk[];
68
71
  outputBytes: number;
72
+ /** Total bytes ingested across both streams; the capture limit judges this. */
73
+ capturedBytes: number;
74
+ /** Set when the capture limit was breached: the process is killed and further chunks dropped. */
75
+ captureOverflowed: boolean;
69
76
  audit: BashAuditEvent[];
70
77
  stdoutPump: Promise<void>;
71
78
  stderrPump: Promise<void>;
@@ -106,6 +113,19 @@ interface BashEnvironmentOptions {
106
113
  * above, where models are known.
107
114
  */
108
115
  maxBinaryBytes?: number;
116
+ /**
117
+ * Ceiling for the bytes a single command's output capture may ingest.
118
+ *
119
+ * Distinct from `maxOutputBytes`, which sizes the rendered view: capture is
120
+ * what the environment holds in memory while a command runs, and it holds
121
+ * several copies (raw bytes for the pipe, text renders, replay chunks). A
122
+ * foreground process that produces more than this is SIGKILLed and the
123
+ * command fails with an explicit error; a background job keeps only the most
124
+ * recent bytes within the ceiling. Without this ceiling one stray
125
+ * `rg`/`cat` over a large tree balloons the embedding process by tens of
126
+ * gigabytes until the kernel OOM-kills it.
127
+ */
128
+ maxCaptureBytes?: number;
109
129
  }
110
130
  interface ShellExecInput {
111
131
  script: string;
@@ -207,6 +227,8 @@ type ShellCommandStatus = {
207
227
  status: 'exited';
208
228
  shellId: string;
209
229
  commandId: string;
230
+ /** Real directory holding this command's artifact files. */
231
+ artifactDir: string;
210
232
  exitCode: number;
211
233
  stdout: ShellStreamView;
212
234
  stderr: ShellStreamView;
@@ -221,6 +243,7 @@ type ShellCommandStatus = {
221
243
  status: 'running';
222
244
  shellId: string;
223
245
  commandId: string;
246
+ artifactDir: string;
224
247
  stdout: ShellStreamView;
225
248
  stderr: ShellStreamView;
226
249
  output: ShellOutputView;
@@ -230,6 +253,7 @@ type ShellCommandStatus = {
230
253
  status: 'aborted';
231
254
  shellId: string;
232
255
  commandId: string;
256
+ artifactDir: string;
233
257
  stdout: ShellStreamView;
234
258
  stderr: ShellStreamView;
235
259
  output: ShellOutputView;
@@ -246,6 +270,7 @@ declare class BashEnvironment {
246
270
  private readonly initialEnv;
247
271
  private readonly defaultOutputLimitBytes;
248
272
  private readonly defaultBinaryLimitBytes;
273
+ private readonly captureLimitBytes;
249
274
  private readonly shells;
250
275
  private readonly defaultShellByAgentSessionId;
251
276
  private readonly commandsById;
@@ -283,11 +308,7 @@ declare class BashEnvironment {
283
308
  private collectAborted;
284
309
  private collectAbortedWithoutForeground;
285
310
  private commandStatus;
286
- private lookupVirtualArtifact;
287
- private commandArtifactIds;
288
- private commandArtifact;
289
311
  private persistCommandArtifact;
290
- private syncRunningRecord;
291
312
  }
292
313
  //#endregion
293
314
  //#region src/portable-commands.d.ts
@@ -308,6 +329,15 @@ declare class BashEnvironment {
308
329
  * we call portable.
309
330
  */
310
331
  declare const DEMI_PORTABLE_COMMANDS: readonly CommandName[];
332
+ /**
333
+ * Portable commands routed real-binary-first (`preferHostSpawn`): recursive
334
+ * tree scanners whose in-process implementations read files whole and burn
335
+ * the embedding host's main thread for minutes on large trees. A real
336
+ * process runs off-thread with its output bounded by the capture limit;
337
+ * hosts without the binary fall back to the portable implementation on
338
+ * first use.
339
+ */
340
+ declare const HOST_PREFERRED_SCAN_COMMANDS: ReadonlySet<string>;
311
341
  /**
312
342
  * Names registered commands must not shadow, derived from the actual portable
313
343
  * command set plus interpreter builtins and pass-through system tools — not a
@@ -321,4 +351,4 @@ declare function shellQuote(value: string): string;
321
351
  /** Picks a heredoc delimiter that does not collide with any line already in `body`. */
322
352
  declare function heredocDelimiter(body: string): string;
323
353
  //#endregion
324
- export { AgentSessionCommandStorage, BashAuditEvent, BashEnvironment, BashEnvironmentOptions, BinaryStdout, COMMAND_HELP_DEFAULTS, Command, CommandExecutionContext, CommandIO, CommandInputSpec, CommandMetadataRecord, CommandOutputSpec, CommandRegistry, CommandRunContext, CommandRunResult, CommandStdin, CommandStorage, DEMI_PORTABLE_COMMANDS, Host, HostBackedFileSystem, HostDirent, HostFileStat, HostFileSystem, HostProcess, HostProcessOutputChunk, HostSpawnExit, HostSpawnHandle, HostSpawnParams, HostStore, MAX_TIMEOUT_MS, ParsedCommandInput, RESERVED_COMMAND_NAMES, ShellAbortInput, ShellCommandStatus, ShellExecInput, ShellOutputChunk, ShellOutputRecordChunk, ShellOutputView, ShellStatusInput, ShellStreamView, ShellWriteInput, VirtualFileSystemNode, VirtualFileSystemProvider, emptyStdin, heredocDelimiter, parseCommandInput, renderCommandHelp, runRegisteredCommand, shellQuote, virtualDirectory, virtualFile };
354
+ export { AgentSessionCommandStorage, BashAuditEvent, BashEnvironment, BashEnvironmentOptions, BinaryStdout, COMMAND_HELP_DEFAULTS, Command, CommandExecutionContext, CommandIO, CommandInputSpec, CommandMetadataRecord, CommandOutputSpec, CommandRegistry, CommandRunContext, CommandRunResult, CommandStdin, CommandStorage, DEMI_PORTABLE_COMMANDS, HOST_PREFERRED_SCAN_COMMANDS, Host, HostBackedFileSystem, HostBackedFileSystemOptions, HostDirent, HostFileStat, HostFileSystem, HostProcess, HostProcessOutputChunk, HostSpawnExit, HostSpawnHandle, HostSpawnParams, HostStore, MAX_TIMEOUT_MS, ParsedCommandInput, RESERVED_COMMAND_NAMES, ShellAbortInput, ShellCommandStatus, ShellExecInput, ShellOutputChunk, ShellOutputRecordChunk, ShellOutputView, ShellStatusInput, ShellStreamView, ShellWriteInput, emptyStdin, heredocDelimiter, parseCommandInput, renderCommandHelp, runRegisteredCommand, shellQuote };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { HostBackedFileSystem, virtualDirectory, virtualFile } from "./host-fs.mjs";
1
+ import { HostBackedFileSystem } from "./host-fs.mjs";
2
2
  import { AgentSessionCommandStorage } from "./storage.mjs";
3
- import { asError, base64ToBytes, bytesToBase64, concatBytes, decodeLatin1, decodeUtf8, decodeUtf8Strict, encodeLatin1, encodeUtf8, tail, utf8Bytes, utf8Slice } from "@demicodes/utils";
3
+ import { asError, concatBytes, decodeLatin1, decodeUtf8, decodeUtf8Strict, encodeLatin1, encodeUtf8, tail, utf8Bytes, utf8Slice } from "@demicodes/utils";
4
4
  import { createLazyCommands, getCommandNames } from "@demicodes/just-bash/commands";
5
5
  import { ArithmeticError, BadSubstitutionError, ExecutionLimitError, ExitError, Interpreter } from "@demicodes/just-bash/interpreter";
6
6
  import { decodeBytesToUtf8, unsafeBytesFromLatin1 } from "@demicodes/just-bash/encoding";
@@ -46,6 +46,19 @@ const REAL_SPAWN_DEPENDENT_COMMANDS = /* @__PURE__ */ new Set([
46
46
  * we call portable.
47
47
  */
48
48
  const DEMI_PORTABLE_COMMANDS = getCommandNames().filter((name) => !REAL_SPAWN_DEPENDENT_COMMANDS.has(name));
49
+ /**
50
+ * Portable commands routed real-binary-first (`preferHostSpawn`): recursive
51
+ * tree scanners whose in-process implementations read files whole and burn
52
+ * the embedding host's main thread for minutes on large trees. A real
53
+ * process runs off-thread with its output bounded by the capture limit;
54
+ * hosts without the binary fall back to the portable implementation on
55
+ * first use.
56
+ */
57
+ const HOST_PREFERRED_SCAN_COMMANDS = /* @__PURE__ */ new Set([
58
+ "rg",
59
+ "grep",
60
+ "find"
61
+ ]);
49
62
  /** Shell language words and builtins the interpreter itself owns. */
50
63
  const SHELL_BUILTIN_NAMES = [
51
64
  ".",
@@ -501,7 +514,16 @@ function createOutputSinks(fs, cwd, redirections) {
501
514
  }
502
515
  return routes;
503
516
  }
504
- function recordForegroundChunk(foreground, sourceFd, chunk) {
517
+ function recordForegroundChunk(foreground, sourceFd, chunk, captureLimitBytes) {
518
+ if (foreground.captureOverflowed) return;
519
+ if (foreground.capturedBytes + chunk.byteLength > captureLimitBytes) {
520
+ foreground.captureOverflowed = true;
521
+ foreground.rawStdoutBuffer = "";
522
+ foreground.rawStdoutBytes = [];
523
+ foreground.handle.kill("SIGKILL").catch(() => {});
524
+ return;
525
+ }
526
+ foreground.capturedBytes += chunk.byteLength;
505
527
  const text = decodeUtf8(chunk);
506
528
  foreground.lastOutputAt = Date.now();
507
529
  if (sourceFd === 1) {
@@ -609,39 +631,63 @@ function appendVisibleChunk(foreground, targetFd, text, byteLength) {
609
631
  //#endregion
610
632
  //#region src/command-artifact-store.ts
611
633
  /**
612
- * Owns persistence of shell command artifacts: a per-storage-id cache plus the
613
- * set of released (tombstoned) commands. `BashEnvironment` delegates the storage
614
- * and release-tracking side of the `/@` virtual filesystem here, keeping only the
615
- * in-memory record lookups that need live command state.
634
+ * Writes shell command artifacts as plain files under
635
+ * `host.commandArtifactsDir/<storageId>/<commandId>/` one shared filesystem
636
+ * namespace with the processes the shell spawns, so any tool (portable or
637
+ * real) reads and searches artifacts with ordinary file operations.
616
638
  */
617
639
  var CommandArtifactStore = class {
618
- store;
619
- storageById = /* @__PURE__ */ new Map();
640
+ host;
620
641
  released = /* @__PURE__ */ new Set();
621
- constructor(store) {
622
- this.store = store;
623
- }
624
- /** The artifact storage for one agent session or anonymous shell. */
625
- storageFor(commandStorageId) {
626
- const existing = this.storageById.get(commandStorageId);
627
- if (existing) return existing;
628
- const storage = new AgentSessionCommandStorage(this.store, commandStorageId);
629
- this.storageById.set(commandStorageId, storage);
630
- return storage;
631
- }
632
- /** Whether a command's artifact has been released (tombstoned). */
642
+ writeChains = /* @__PURE__ */ new Map();
643
+ constructor(host) {
644
+ this.host = host;
645
+ }
646
+ dirFor(commandStorageId, commandId) {
647
+ return `${this.host.commandArtifactsDir}/${commandStorageId}/${commandId}`;
648
+ }
649
+ /** Whether a command's artifact has been released (removed from disk). */
633
650
  isReleased(commandStorageId, commandId) {
634
651
  return this.released.has(this.key(commandStorageId, commandId));
635
652
  }
636
- /** Persists an artifact unless its command has already been released. */
637
- persist(commandStorageId, commandId, artifact) {
638
- if (this.isReleased(commandStorageId, commandId)) return;
639
- this.storageFor(commandStorageId).writeJson(`commands/${commandId}/artifact.json`, artifact).catch(() => {});
653
+ /** Persists the artifact files unless the command has already been released. */
654
+ persist(commandStorageId, commandId, files) {
655
+ const key = this.key(commandStorageId, commandId);
656
+ if (this.released.has(key)) return;
657
+ const dir = this.dirFor(commandStorageId, commandId);
658
+ this.chain(key, async () => {
659
+ if (this.released.has(key)) return;
660
+ const writeOptions = {
661
+ cwd: this.host.defaultCwd,
662
+ createParents: true
663
+ };
664
+ await this.host.fs.writeFile(`${dir}/meta.json`, encodeUtf8(files.meta), writeOptions);
665
+ await this.host.fs.writeFile(`${dir}/stdout.txt`, encodeUtf8(files.stdout), writeOptions);
666
+ await this.host.fs.writeFile(`${dir}/stderr.txt`, encodeUtf8(files.stderr), writeOptions);
667
+ if (files.stdoutBin) await this.host.fs.writeFile(`${dir}/stdout.bin`, files.stdoutBin, writeOptions);
668
+ });
640
669
  }
641
- /** Tombstones a command and removes its persisted artifact. */
670
+ /** Tombstones a command and removes its artifact directory. */
642
671
  async release(commandStorageId, commandId) {
643
- this.released.add(this.key(commandStorageId, commandId));
644
- await this.storageFor(commandStorageId).delete(`commands/${commandId}/artifact.json`).catch(() => {});
672
+ const key = this.key(commandStorageId, commandId);
673
+ this.released.add(key);
674
+ await new Promise((resolve) => {
675
+ this.chain(key, async () => {
676
+ await this.host.fs.rm(this.dirFor(commandStorageId, commandId), {
677
+ cwd: this.host.defaultCwd,
678
+ recursive: true,
679
+ force: true
680
+ }).catch(() => {});
681
+ resolve();
682
+ });
683
+ });
684
+ }
685
+ chain(key, work) {
686
+ const next = (this.writeChains.get(key) ?? Promise.resolve()).then(work).catch(() => {});
687
+ this.writeChains.set(key, next);
688
+ next.finally(() => {
689
+ if (this.writeChains.get(key) === next) this.writeChains.delete(key);
690
+ });
645
691
  }
646
692
  key(commandStorageId, commandId) {
647
693
  return `${commandStorageId}\0${commandId}`;
@@ -778,6 +824,13 @@ const DEFAULT_OUTPUT_LIMIT_BYTES = 1048576;
778
824
  * while still bounding a runaway producer.
779
825
  */
780
826
  const DEFAULT_BINARY_LIMIT_BYTES = 16777216;
827
+ /**
828
+ * Ceiling for a single command's in-memory output capture. Sized to the same
829
+ * order as the in-shell file read limit: the execution model buffers whole
830
+ * command outputs (in several copies), so this is a hard memory-safety bound
831
+ * on the embedding process, not a view budget.
832
+ */
833
+ const DEFAULT_CAPTURE_LIMIT_BYTES = 67108864;
781
834
  /** Upper bound for a single exec observation window (also the command-bridge wait ceiling). */
782
835
  const MAX_TIMEOUT_MS = 6e5;
783
836
  var BashEnvironment = class {
@@ -788,19 +841,21 @@ var BashEnvironment = class {
788
841
  initialEnv;
789
842
  defaultOutputLimitBytes;
790
843
  defaultBinaryLimitBytes;
844
+ captureLimitBytes;
791
845
  shells = /* @__PURE__ */ new Map();
792
846
  defaultShellByAgentSessionId = /* @__PURE__ */ new Map();
793
847
  commandsById = /* @__PURE__ */ new Map();
794
848
  artifacts;
795
849
  constructor(options) {
796
850
  this.host = options.host;
797
- this.artifacts = new CommandArtifactStore(this.host.store);
851
+ this.artifacts = new CommandArtifactStore(this.host);
798
852
  this.commands = options.commands ?? new CommandRegistry();
799
853
  this.shellIdFactory = options.shellIdFactory ?? (() => globalThis.crypto.randomUUID());
800
854
  this.commandIdFactory = options.commandIdFactory ?? (() => globalThis.crypto.randomUUID());
801
855
  this.initialEnv = options.initialEnv ?? {};
802
856
  this.defaultOutputLimitBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES;
803
857
  this.defaultBinaryLimitBytes = options.maxBinaryBytes ?? DEFAULT_BINARY_LIMIT_BYTES;
858
+ this.captureLimitBytes = options.maxCaptureBytes ?? DEFAULT_CAPTURE_LIMIT_BYTES;
804
859
  }
805
860
  getShell(shellId) {
806
861
  return this.shells.get(shellId) ?? null;
@@ -931,7 +986,7 @@ var BashEnvironment = class {
931
986
  const id = this.shellIdFactory();
932
987
  const commandStorageId = agentSessionId ?? id;
933
988
  const cwd = initialCwd ?? this.host.defaultCwd;
934
- const fs = new HostBackedFileSystem(this.host, { lookup: (path) => this.lookupVirtualArtifact(commandStorageId, path) });
989
+ const fs = new HostBackedFileSystem(this.host);
935
990
  const env = /* @__PURE__ */ new Map();
936
991
  for (const [key, value] of Object.entries(this.initialEnv)) env.set(key, value);
937
992
  env.set("PWD", cwd);
@@ -1025,7 +1080,7 @@ var BashEnvironment = class {
1025
1080
  for (const command of this.commands.list()) forkCommands.set(command.name, commandToForkCommand(session, command, storage, this.host));
1026
1081
  session.abortController = new AbortController();
1027
1082
  const limits = resolveLimits({
1028
- maxOutputSize: 1073741824,
1083
+ maxOutputSize: this.captureLimitBytes,
1029
1084
  maxCommandCount: 1e6,
1030
1085
  maxLoopIterations: 1e6,
1031
1086
  maxCallDepth: 1e3,
@@ -1097,6 +1152,7 @@ var BashEnvironment = class {
1097
1152
  id,
1098
1153
  shellId: session.id,
1099
1154
  commandStorageId: session.commandStorageId,
1155
+ artifactDir: this.artifacts.dirFor(session.commandStorageId, id),
1100
1156
  script,
1101
1157
  startedAt: now,
1102
1158
  lastOutputAt: now,
@@ -1135,15 +1191,26 @@ var BashEnvironment = class {
1135
1191
  handle,
1136
1192
  stdoutBuffer: "",
1137
1193
  stderrBuffer: "",
1194
+ droppedStdoutChars: 0,
1195
+ droppedStderrChars: 0,
1138
1196
  stdoutPump: Promise.resolve(),
1139
1197
  stderrPump: Promise.resolve(),
1140
1198
  exitPromise: handle.wait()
1141
1199
  };
1200
+ const retainLimit = this.defaultOutputLimitBytes;
1142
1201
  job.stdoutPump = pumpStream(handle.stdout, (chunk) => {
1143
1202
  job.stdoutBuffer += decodeUtf8(chunk);
1203
+ if (job.stdoutBuffer.length > retainLimit) {
1204
+ job.droppedStdoutChars += job.stdoutBuffer.length - retainLimit;
1205
+ job.stdoutBuffer = job.stdoutBuffer.slice(-retainLimit);
1206
+ }
1144
1207
  });
1145
1208
  job.stderrPump = pumpStream(handle.stderr, (chunk) => {
1146
1209
  job.stderrBuffer += decodeUtf8(chunk);
1210
+ if (job.stderrBuffer.length > retainLimit) {
1211
+ job.droppedStderrChars += job.stderrBuffer.length - retainLimit;
1212
+ job.stderrBuffer = job.stderrBuffer.slice(-retainLimit);
1213
+ }
1147
1214
  });
1148
1215
  session.backgroundJobs.set(id, job);
1149
1216
  session.state.lastBackgroundPid = id;
@@ -1191,7 +1258,8 @@ var BashEnvironment = class {
1191
1258
  await Promise.allSettled([job.stdoutPump, job.stderrPump]);
1192
1259
  session.backgroundJobs.delete(id);
1193
1260
  const exitCode = exit.exitCode ?? 127;
1194
- const stderr = exit.exitCode === null && job.stderrBuffer.length === 0 ? `${job.command}: ${exit.signal ?? "command not found"}\n` : job.stderrBuffer;
1261
+ const stdout = job.droppedStdoutChars > 0 ? `[... dropped ${job.droppedStdoutChars} chars of earlier stdout over the capture limit ...]\n${job.stdoutBuffer}` : job.stdoutBuffer;
1262
+ const stderr = exit.exitCode === null && job.stderrBuffer.length === 0 ? `${job.command}: ${exit.signal ?? "command not found"}\n` : job.droppedStderrChars > 0 ? `[... dropped ${job.droppedStderrChars} chars of earlier stderr over the capture limit ...]\n${job.stderrBuffer}` : job.stderrBuffer;
1195
1263
  session.accumulator.audit.push({
1196
1264
  kind: "system-command",
1197
1265
  name: job.command,
@@ -1200,7 +1268,7 @@ var BashEnvironment = class {
1200
1268
  exitCode
1201
1269
  });
1202
1270
  return {
1203
- stdout: job.stdoutBuffer,
1271
+ stdout,
1204
1272
  stderr,
1205
1273
  exitCode
1206
1274
  };
@@ -1321,6 +1389,8 @@ var BashEnvironment = class {
1321
1389
  stderrBuffer: "",
1322
1390
  outputChunks: [],
1323
1391
  outputBytes: 0,
1392
+ capturedBytes: 0,
1393
+ captureOverflowed: false,
1324
1394
  audit: [{
1325
1395
  kind: "system-command",
1326
1396
  name: command,
@@ -1340,18 +1410,18 @@ var BashEnvironment = class {
1340
1410
  if (opts.stdinProvided) await handle.closeStdin();
1341
1411
  if (handle.output) {
1342
1412
  foreground.stdoutPump = pumpOutputStream(handle.output, (chunk) => {
1343
- recordForegroundChunk(foreground, chunk.stream === "stdout" ? 1 : 2, chunk.chunk);
1413
+ recordForegroundChunk(foreground, chunk.stream === "stdout" ? 1 : 2, chunk.chunk, this.captureLimitBytes);
1344
1414
  });
1345
1415
  foreground.stderrPump = Promise.resolve();
1346
1416
  } else {
1347
- foreground.stdoutPump = pumpStream(handle.stdout, (chunk) => recordForegroundChunk(foreground, 1, chunk));
1348
- foreground.stderrPump = pumpStream(handle.stderr, (chunk) => recordForegroundChunk(foreground, 2, chunk));
1417
+ foreground.stdoutPump = pumpStream(handle.stdout, (chunk) => recordForegroundChunk(foreground, 1, chunk, this.captureLimitBytes));
1418
+ foreground.stderrPump = pumpStream(handle.stderr, (chunk) => recordForegroundChunk(foreground, 2, chunk, this.captureLimitBytes));
1349
1419
  }
1350
1420
  const exit = await foreground.exitPromise;
1351
1421
  await Promise.allSettled([foreground.stdoutPump, foreground.stderrPump]);
1352
- const stdout = decodeLatin1(concatBytes(foreground.rawStdoutBytes));
1353
- const exitCode = exit.exitCode ?? 127;
1354
- const stderr = exit.exitCode === null && foreground.rawStderrBuffer.length === 0 ? `${command}: ${exit.signal ?? "command not found"}\n` : foreground.rawStderrBuffer;
1422
+ const stdout = foreground.captureOverflowed ? "" : decodeLatin1(concatBytes(foreground.rawStdoutBytes));
1423
+ const exitCode = foreground.captureOverflowed ? 137 : exit.exitCode ?? 127;
1424
+ const stderr = foreground.captureOverflowed ? `${command}: output exceeded the ${this.captureLimitBytes}-byte capture limit and the process was killed; the shell buffers whole command outputs in memory — narrow the output at the source (filters, head, tighter paths)\n` : exit.exitCode === null && foreground.rawStderrBuffer.length === 0 ? `${command}: ${exit.signal ?? "command not found"}\n` : foreground.rawStderrBuffer;
1355
1425
  foreground.audit[0] = {
1356
1426
  kind: "system-command",
1357
1427
  name: command,
@@ -1431,7 +1501,8 @@ var BashEnvironment = class {
1431
1501
  totalBytes: bytes.length,
1432
1502
  limitBytes: cap
1433
1503
  };
1434
- stdoutText = `<binary stdout: ${bytes.length} bytes${truncated ? `, exceeds the ${cap}-byte binary limit` : ""}; raw bytes at /@/commands/${record.id}/stdout.bin>\n`;
1504
+ stdoutText = `<binary stdout: ${bytes.length} bytes${truncated ? `, exceeds the ${cap}-byte binary limit` : ""}; raw bytes at ${record.artifactDir}/stdout.bin>\n`;
1505
+ record.pendingBinaryArtifact = bytes;
1435
1506
  }
1436
1507
  }
1437
1508
  const stderrText = foreground ? resultOrError.stderr : decodeBytesToUtf8(unsafeBytesFromLatin1(resultOrError.stderr));
@@ -1511,6 +1582,7 @@ var BashEnvironment = class {
1511
1582
  const base = {
1512
1583
  shellId: record.shellId,
1513
1584
  commandId: record.id,
1585
+ artifactDir: record.artifactDir,
1514
1586
  stdout,
1515
1587
  stderr,
1516
1588
  output,
@@ -1538,70 +1610,24 @@ var BashEnvironment = class {
1538
1610
  status: "running"
1539
1611
  };
1540
1612
  }
1541
- async lookupVirtualArtifact(commandStorageId, path) {
1542
- const parts = path.split("/").filter(Boolean);
1543
- if (parts.length === 1 && parts[0] === "@") return virtualDirectory(["commands"]);
1544
- if (parts.length === 2 && parts[0] === "@" && parts[1] === "commands") return virtualDirectory(await this.commandArtifactIds(commandStorageId));
1545
- if (parts.length === 3 && parts[0] === "@" && parts[1] === "commands") {
1546
- const artifact = await this.commandArtifact(commandStorageId, parts[2]);
1547
- if (!artifact) return null;
1548
- const entries = [
1549
- "meta.json",
1550
- "stderr.txt",
1551
- "stdout.txt"
1552
- ];
1553
- if (artifact.stdoutBinary) entries.push("stdout.bin");
1554
- return virtualDirectory(entries);
1555
- }
1556
- if (parts.length !== 4 || parts[0] !== "@" || parts[1] !== "commands") return null;
1557
- const artifact = await this.commandArtifact(commandStorageId, parts[2]);
1558
- if (!artifact) return null;
1559
- const fileName = parts[3];
1560
- if (fileName === "stdout.txt") return virtualFile(encodeUtf8(artifact.stdout));
1561
- if (fileName === "stdout.bin" && artifact.stdoutBinary) return virtualFile(base64ToBytes(artifact.stdoutBinary.base64));
1562
- if (fileName === "stderr.txt") return virtualFile(encodeUtf8(artifact.stderr));
1563
- if (fileName === "meta.json") return virtualFile(encodeUtf8(`${JSON.stringify(commandArtifactMeta(artifact), null, 2)}\n`));
1564
- return null;
1565
- }
1566
- async commandArtifactIds(commandStorageId) {
1567
- const ids = /* @__PURE__ */ new Set();
1568
- for (const record of this.commandsById.values()) if (record.commandStorageId === commandStorageId && !this.artifacts.isReleased(commandStorageId, record.id)) ids.add(record.id);
1569
- const keys = await this.artifacts.storageFor(commandStorageId).list("commands").catch(() => []);
1570
- for (const key of keys) {
1571
- const match = /^commands\/([^/]+)\/artifact\.json$/.exec(key);
1572
- if (match && !this.artifacts.isReleased(commandStorageId, match[1])) ids.add(match[1]);
1573
- }
1574
- return [...ids];
1575
- }
1576
- async commandArtifact(commandStorageId, commandId) {
1577
- if (this.artifacts.isReleased(commandStorageId, commandId)) return null;
1578
- const record = this.commandsById.get(commandId);
1579
- if (record?.commandStorageId === commandStorageId) {
1580
- this.syncRunningRecord(record);
1581
- return commandArtifactFromRecord(record);
1582
- }
1583
- const value = await this.artifacts.storageFor(commandStorageId).readJson(`commands/${commandId}/artifact.json`).catch(() => null);
1584
- return isCommandArtifact(value) ? value : null;
1585
- }
1586
1613
  persistCommandArtifact(record) {
1587
1614
  const fingerprint = `${record.status}:${record.exitCode ?? ""}:${record.stdout.length}:${record.stderr.length}:${record.binaryStdout?.totalBytes ?? ""}`;
1588
1615
  if (record.persistedFingerprint === fingerprint) return;
1589
1616
  record.persistedFingerprint = fingerprint;
1590
- this.artifacts.persist(record.commandStorageId, record.id, commandArtifactFromRecord(record));
1591
- }
1592
- syncRunningRecord(record) {
1593
- const foreground = this.shells.get(record.shellId)?.foreground;
1594
- if (record.status === "running" && foreground?.commandId === record.id) {
1595
- record.stdout = foreground.stdoutBuffer;
1596
- record.stderr = foreground.stderrBuffer;
1597
- record.outputChunks = [...foreground.outputChunks];
1598
- record.lastOutputAt = foreground.lastOutputAt;
1599
- }
1617
+ const stdoutBin = record.pendingBinaryArtifact;
1618
+ record.pendingBinaryArtifact = void 0;
1619
+ this.artifacts.persist(record.commandStorageId, record.id, {
1620
+ meta: `${JSON.stringify(commandArtifactMeta(record), null, 2)}\n`,
1621
+ stdout: record.stdout,
1622
+ stderr: record.stderr,
1623
+ ...stdoutBin ? { stdoutBin } : {}
1624
+ });
1600
1625
  }
1601
1626
  };
1602
1627
  function createPortableCommands(session) {
1603
1628
  return createLazyCommands([...DEMI_PORTABLE_COMMANDS]).map((command) => ({
1604
1629
  ...command,
1630
+ preferHostSpawn: HOST_PREFERRED_SCAN_COMMANDS.has(command.name),
1605
1631
  execute: async (args, ctx) => {
1606
1632
  const result = await command.execute(args, ctx);
1607
1633
  session.accumulator.audit.push({
@@ -1636,7 +1662,7 @@ function streamView(record, stream, explicitOffset, maxOutputBytes) {
1636
1662
  if (explicitOffset === void 0) if (stream === "stdout") record.stdoutOffset = nextOffset;
1637
1663
  else record.stderrOffset = nextOffset;
1638
1664
  return {
1639
- path: `/@/commands/${record.id}/${stream}.txt`,
1665
+ path: `${record.artifactDir}/${stream}.txt`,
1640
1666
  offset: nextOffset,
1641
1667
  delta,
1642
1668
  tail: tailString(text),
@@ -1671,7 +1697,7 @@ function mergedOutputView(record, explicitOffset, maxOutputBytes) {
1671
1697
  const truncated = nextOffset < totalBytes;
1672
1698
  if (explicitOffset === void 0) record.outputOffset = nextOffset;
1673
1699
  return {
1674
- path: `demi://shell/${record.shellId}/commands/${record.id}/output`,
1700
+ path: record.artifactDir,
1675
1701
  offset: nextOffset,
1676
1702
  text,
1677
1703
  tail: tailOutputText(record.outputChunks),
@@ -1698,55 +1724,29 @@ function ensureRecordOutputCoverage(record) {
1698
1724
  appendRecordOutput(record, "stdout", record.stdout);
1699
1725
  appendRecordOutput(record, "stderr", record.stderr);
1700
1726
  }
1701
- function commandArtifactFromRecord(record) {
1727
+ function commandArtifactMeta(record) {
1702
1728
  return {
1703
1729
  status: record.status,
1704
1730
  shellId: record.shellId,
1705
1731
  commandId: record.id,
1732
+ script: record.script,
1706
1733
  startedAt: record.startedAt,
1707
1734
  lastOutputAt: record.lastOutputAt,
1708
1735
  exitCode: record.exitCode ?? null,
1709
- stdout: record.stdout,
1710
- stderr: record.stderr,
1711
- ...record.binaryStdout ? { stdoutBinary: {
1712
- base64: bytesToBase64(record.binaryStdout.data),
1713
- truncated: record.binaryStdout.truncated,
1714
- totalBytes: record.binaryStdout.totalBytes
1715
- } } : {}
1716
- };
1717
- }
1718
- function commandArtifactMeta(artifact) {
1719
- const stdoutPath = `/@/commands/${artifact.commandId}/stdout.txt`;
1720
- const stderrPath = `/@/commands/${artifact.commandId}/stderr.txt`;
1721
- return {
1722
- status: artifact.status,
1723
- shellId: artifact.shellId,
1724
- commandId: artifact.commandId,
1725
- startedAt: artifact.startedAt,
1726
- lastOutputAt: artifact.lastOutputAt,
1727
- runningMs: Date.now() - artifact.startedAt,
1728
- idleMs: Date.now() - artifact.lastOutputAt,
1729
- exitCode: artifact.exitCode,
1730
1736
  stdout: {
1731
- path: stdoutPath,
1732
- bytes: utf8Bytes(artifact.stdout)
1737
+ path: `${record.artifactDir}/stdout.txt`,
1738
+ bytes: utf8Bytes(record.stdout)
1733
1739
  },
1734
1740
  stderr: {
1735
- path: stderrPath,
1736
- bytes: utf8Bytes(artifact.stderr)
1741
+ path: `${record.artifactDir}/stderr.txt`,
1742
+ bytes: utf8Bytes(record.stderr)
1737
1743
  },
1738
- ...artifact.stdoutBinary ? { stdoutBinary: {
1739
- path: `/@/commands/${artifact.commandId}/stdout.bin`,
1740
- bytes: artifact.stdoutBinary.totalBytes,
1741
- truncated: artifact.stdoutBinary.truncated
1744
+ ...record.binaryStdout ? { stdoutBinary: {
1745
+ path: `${record.artifactDir}/stdout.bin`,
1746
+ bytes: record.binaryStdout.totalBytes
1742
1747
  } } : {}
1743
1748
  };
1744
1749
  }
1745
- function isCommandArtifact(value) {
1746
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1747
- const record = value;
1748
- return (record.status === "running" || record.status === "exited" || record.status === "aborted") && typeof record.shellId === "string" && typeof record.commandId === "string" && typeof record.startedAt === "number" && typeof record.lastOutputAt === "number" && (typeof record.exitCode === "number" || record.exitCode === null) && typeof record.stdout === "string" && typeof record.stderr === "string";
1749
- }
1750
1750
  function tailOutputText(chunks) {
1751
1751
  const maxChars = 4096;
1752
1752
  let text = "";
@@ -1774,4 +1774,4 @@ function heredocDelimiter(body) {
1774
1774
  return delimiter;
1775
1775
  }
1776
1776
  //#endregion
1777
- export { AgentSessionCommandStorage, BashEnvironment, COMMAND_HELP_DEFAULTS, CommandRegistry, DEMI_PORTABLE_COMMANDS, HostBackedFileSystem, MAX_TIMEOUT_MS, RESERVED_COMMAND_NAMES, emptyStdin, heredocDelimiter, parseCommandInput, renderCommandHelp, runRegisteredCommand, shellQuote, virtualDirectory, virtualFile };
1777
+ export { AgentSessionCommandStorage, BashEnvironment, COMMAND_HELP_DEFAULTS, CommandRegistry, DEMI_PORTABLE_COMMANDS, HOST_PREFERRED_SCAN_COMMANDS, HostBackedFileSystem, MAX_TIMEOUT_MS, RESERVED_COMMAND_NAMES, emptyStdin, heredocDelimiter, parseCommandInput, renderCommandHelp, runRegisteredCommand, shellQuote };
@@ -1,4 +1,4 @@
1
- import { t as Host, u as HostStore } from "./host-DIg4RxcL.mjs";
1
+ import { t as Host, u as HostStore } from "./host-B-oKG7x5.mjs";
2
2
  import { z } from "zod";
3
3
  //#region src/command.d.ts
4
4
  type CommandInputSpec = Record<string, z.ZodType>;
@@ -1,2 +1,2 @@
1
- import { t as AgentSessionCommandStorage } from "./storage-CNMBL9_j.mjs";
1
+ import { t as AgentSessionCommandStorage } from "./storage-DYvEwSc9.mjs";
2
2
  export { AgentSessionCommandStorage };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/shell",
3
3
  "description": "Sandboxable bash engine and Host contract for Demi.",
4
- "version": "0.14.1",
4
+ "version": "0.15.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -19,8 +19,8 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@demicodes/just-bash": "^3.0.1-demi.5",
23
- "@demicodes/utils": "^0.14.1",
22
+ "@demicodes/just-bash": "^3.1.0-demi.3",
23
+ "@demicodes/utils": "^0.15.0",
24
24
  "zod": "^4.0.0"
25
25
  },
26
26
  "license": "Apache-2.0",