@demicodes/shell 0.10.3 → 0.12.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,5 @@
1
1
  import { t as Host } from "./host-DIg4RxcL.mjs";
2
2
  import { BufferEncoding, CpOptions, DirentEntry, FileContent, FsStat, IFileSystem, MkdirOptions, ReadFileOptions, RmOptions, WriteFileOptions } from "@demicodes/just-bash/fs/interface";
3
-
4
3
  //#region src/host-fs.d.ts
5
4
  type VirtualFileSystemNode = {
6
5
  kind: 'file';
package/dist/host-fs.mjs CHANGED
@@ -166,13 +166,13 @@ function encodingFrom(options) {
166
166
  function encodeContent(content, encoding) {
167
167
  if (content instanceof Uint8Array) return content;
168
168
  if (encoding === "binary" || encoding === "latin1") return latin1ToBytes(content);
169
- if (encoding === "base64") return base64ToBytes(content);
169
+ if (encoding === "base64") return base64ToBytes$1(content);
170
170
  if (encoding === "hex") return hexToBytes(content);
171
171
  return encodeUtf8(content);
172
172
  }
173
173
  function decodeBytes(bytes, encoding) {
174
174
  if (encoding === "binary" || encoding === "latin1") return bytesToLatin1(bytes);
175
- if (encoding === "base64") return bytesToBase64(bytes);
175
+ if (encoding === "base64") return bytesToBase64$1(bytes);
176
176
  if (encoding === "hex") return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
177
177
  return decodeUtf8(bytes);
178
178
  }
@@ -186,10 +186,10 @@ function bytesToLatin1(bytes) {
186
186
  for (let index = 0; index < bytes.length; index += 8192) result += String.fromCharCode(...bytes.subarray(index, index + 8192));
187
187
  return result;
188
188
  }
189
- function base64ToBytes(content) {
189
+ function base64ToBytes$1(content) {
190
190
  return latin1ToBytes(atob(content));
191
191
  }
192
- function bytesToBase64(bytes) {
192
+ function bytesToBase64$1(bytes) {
193
193
  return btoa(bytesToLatin1(bytes));
194
194
  }
195
195
  function hexToBytes(content) {
package/dist/index.d.mts CHANGED
@@ -1,21 +1,20 @@
1
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
2
  import { HostBackedFileSystem, VirtualFileSystemNode, VirtualFileSystemProvider, virtualDirectory, virtualFile } from "./host-fs.mjs";
3
- import { _ as runRegisteredCommand, a as CommandExecutionContext, c as CommandOutputSpec, d as CommandRunResult, f as CommandStdin, g as renderCommandPrompt, h as parseCommandInput, i as CommandAsset, l as CommandRegistry, m as ParsedCommandInput, n as COMMAND_PROMPT_DEFAULTS, o as CommandIO, p as CommandStorage, r as Command, s as CommandInputSpec, t as AgentSessionCommandStorage, u as CommandRunContext } from "./storage-D7EhVuLY.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-ylShLr2v.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";
7
-
8
7
  //#region src/environment-state.d.ts
9
8
  interface ExecAccumulator {
10
9
  stdout: string;
11
10
  stderr: string;
12
11
  audit: BashAuditEvent[];
13
12
  commandMetadata: CommandMetadataRecord[];
14
- assets: CommandAsset[];
15
13
  }
16
14
  interface ShellSession {
17
15
  id: string;
18
- commandScopeId: string;
16
+ agentSessionId: string | null;
17
+ commandStorageId: string;
19
18
  state: InterpreterState;
20
19
  fs: HostBackedFileSystem;
21
20
  interpreter: Interpreter;
@@ -57,6 +56,8 @@ interface ForegroundProcess {
57
56
  /** Everything the process wrote, including redirected output — this is what
58
57
  * the interpreter observes as the command's stdout/stderr. */
59
58
  rawStdoutBuffer: string;
59
+ /** Raw stdout byte chunks for byte-clean pipeline continuation. */
60
+ rawStdoutBytes: Uint8Array[];
60
61
  rawStderrBuffer: string;
61
62
  /** Output routed to the visible sinks only (redirections excluded) — this is
62
63
  * what command records and model previews show. */
@@ -90,15 +91,21 @@ interface BashEnvironmentOptions {
90
91
  shellIdFactory?: () => string;
91
92
  commandIdFactory?: () => string;
92
93
  initialEnv?: Record<string, string>;
94
+ /** Cap on a command's text stdout. Guards against log floods. */
95
+ maxOutputBytes?: number;
93
96
  /**
94
- * Per-exec env injection for agent-owned shells. Evaluated on every exec with
95
- * the owning agent session id; returned vars are set and exported, so both
96
- * registered commands (ctx.env) and spawned external processes observe them.
97
- * Lets a product harness expose session-scoped context (identity, routing)
98
- * that changes between execs, which static initialEnv cannot express.
97
+ * Cap on a final stdout stream that is raw bytes rather than text.
98
+ *
99
+ * Separate from `maxOutputBytes` because the two guard different risks. Text
100
+ * costs context roughly in proportion to its bytes, so a tight cap is what
101
+ * stops a stray `cat` of a log file from flooding a window. Raw bytes are
102
+ * carried to be looked at — a picture, a clip — and there a megabyte buys far
103
+ * more than a megabyte of text does, so the same number would forbid the
104
+ * ordinary case. This layer deliberately stops at that distinction: which
105
+ * modality the bytes are, and what a given model should be shown, is decided
106
+ * above, where models are known.
99
107
  */
100
- execEnv?: (agentSessionId: string) => Record<string, string>;
101
- maxOutputBytes?: number;
108
+ maxBinaryBytes?: number;
102
109
  }
103
110
  interface ShellExecInput {
104
111
  script: string;
@@ -107,6 +114,18 @@ interface ShellExecInput {
107
114
  timeoutMs?: number;
108
115
  maxOutputBytes?: number;
109
116
  signal?: AbortSignal;
117
+ /**
118
+ * Run in a dedicated one-shot shell instead of the session default shell, so
119
+ * cd/env side effects never leak into other execs sharing the session. The
120
+ * caller owns the shell and should `disposeShell(snapshot.shellId)` when done.
121
+ * Mutually exclusive with `shellId`.
122
+ */
123
+ ephemeral?: boolean;
124
+ /**
125
+ * Initial working directory of the shell this exec creates. Requires
126
+ * `ephemeral` — a persistent shell owns its cwd (that is what `cd` is for).
127
+ */
128
+ cwd?: string;
110
129
  }
111
130
  interface ShellStatusInput {
112
131
  commandId: string;
@@ -125,7 +144,7 @@ interface ShellAbortInput {
125
144
  commandId: string;
126
145
  maxOutputBytes?: number;
127
146
  }
128
- interface StreamArtifact {
147
+ interface ShellStreamView {
129
148
  path: string;
130
149
  offset: number;
131
150
  delta: string;
@@ -141,7 +160,7 @@ interface ShellOutputRecordChunk extends ShellOutputChunk {
141
160
  offset: number;
142
161
  bytes: number;
143
162
  }
144
- interface ShellOutputArtifact {
163
+ interface ShellOutputView {
145
164
  path: string;
146
165
  offset: number;
147
166
  text: string;
@@ -174,35 +193,46 @@ interface CommandMetadataRecord {
174
193
  args: string[];
175
194
  metadata: unknown;
176
195
  }
177
- type ShellCommandSnapshot = {
196
+ /** Final stdout stream that is not valid UTF-8: raw bytes for the boundary above. */
197
+ interface BinaryStdout {
198
+ data: Uint8Array;
199
+ /** True when the stream exceeded the applicable cap; data is capped. */
200
+ truncated: boolean;
201
+ /** Total byte count of the un-capped stream. */
202
+ totalBytes: number;
203
+ /** The byte ceiling that applied, so the boundary above can name it. */
204
+ limitBytes: number;
205
+ }
206
+ type ShellCommandStatus = {
178
207
  status: 'exited';
179
208
  shellId: string;
180
209
  commandId: string;
181
210
  exitCode: number;
182
- stdout: StreamArtifact;
183
- stderr: StreamArtifact;
184
- output: ShellOutputArtifact;
211
+ stdout: ShellStreamView;
212
+ stderr: ShellStreamView;
213
+ output: ShellOutputView;
185
214
  runningMs: number;
186
215
  idleMs: number;
187
216
  audit: BashAuditEvent[];
188
217
  commandMetadata?: CommandMetadataRecord[];
189
- assets?: CommandAsset[];
218
+ /** Present when the final stream was binary (bytes that are not valid UTF-8). */
219
+ binaryStdout?: BinaryStdout;
190
220
  } | {
191
221
  status: 'running';
192
222
  shellId: string;
193
223
  commandId: string;
194
- stdout: StreamArtifact;
195
- stderr: StreamArtifact;
196
- output: ShellOutputArtifact;
224
+ stdout: ShellStreamView;
225
+ stderr: ShellStreamView;
226
+ output: ShellOutputView;
197
227
  runningMs: number;
198
228
  idleMs: number;
199
229
  } | {
200
230
  status: 'aborted';
201
231
  shellId: string;
202
232
  commandId: string;
203
- stdout: StreamArtifact;
204
- stderr: StreamArtifact;
205
- output: ShellOutputArtifact;
233
+ stdout: ShellStreamView;
234
+ stderr: ShellStreamView;
235
+ output: ShellOutputView;
206
236
  runningMs: number;
207
237
  idleMs: number;
208
238
  };
@@ -214,20 +244,21 @@ declare class BashEnvironment {
214
244
  private readonly shellIdFactory;
215
245
  private readonly commandIdFactory;
216
246
  private readonly initialEnv;
217
- private readonly execEnv?;
218
247
  private readonly defaultOutputLimitBytes;
248
+ private readonly defaultBinaryLimitBytes;
219
249
  private readonly shells;
220
250
  private readonly defaultShellByAgentSessionId;
221
251
  private readonly commandsById;
222
252
  private readonly artifacts;
223
253
  constructor(options: BashEnvironmentOptions);
224
254
  getShell(shellId: string): ShellSession | null;
255
+ hasCommand(commandId: string): boolean;
225
256
  registerCommand(command: Command): void;
226
257
  registeredCommands(): Command[];
227
- exec(input: ShellExecInput): Promise<ShellCommandSnapshot>;
228
- status(input: ShellStatusInput): Promise<ShellCommandSnapshot>;
229
- write(input: ShellWriteInput): Promise<ShellCommandSnapshot>;
230
- abort(input: ShellAbortInput): Promise<ShellCommandSnapshot>;
258
+ exec(input: ShellExecInput): Promise<ShellCommandStatus>;
259
+ status(input: ShellStatusInput): Promise<ShellCommandStatus>;
260
+ write(input: ShellWriteInput): Promise<ShellCommandStatus>;
261
+ abort(input: ShellAbortInput): Promise<ShellCommandStatus>;
231
262
  releaseCommand(commandId: string): Promise<boolean>;
232
263
  disposeShell(shellId: string): Promise<boolean>;
233
264
  disposeAllShells(): Promise<void>;
@@ -251,7 +282,7 @@ declare class BashEnvironment {
251
282
  private finishExited;
252
283
  private collectAborted;
253
284
  private collectAbortedWithoutForeground;
254
- private snapshotCommand;
285
+ private commandStatus;
255
286
  private lookupVirtualArtifact;
256
287
  private commandArtifactIds;
257
288
  private commandArtifact;
@@ -290,4 +321,4 @@ declare function shellQuote(value: string): string;
290
321
  /** Picks a heredoc delimiter that does not collide with any line already in `body`. */
291
322
  declare function heredocDelimiter(body: string): string;
292
323
  //#endregion
293
- export { AgentSessionCommandStorage, BashAuditEvent, BashEnvironment, BashEnvironmentOptions, COMMAND_PROMPT_DEFAULTS, Command, CommandAsset, 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, ShellCommandSnapshot, ShellExecInput, ShellOutputArtifact, ShellOutputChunk, ShellOutputRecordChunk, ShellStatusInput, ShellWriteInput, StreamArtifact, VirtualFileSystemNode, VirtualFileSystemProvider, heredocDelimiter, parseCommandInput, renderCommandPrompt, runRegisteredCommand, shellQuote, virtualDirectory, virtualFile };
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 };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { HostBackedFileSystem, virtualDirectory, virtualFile } from "./host-fs.mjs";
2
2
  import { AgentSessionCommandStorage } from "./storage.mjs";
3
- import { asError, concatBytes, decodeUtf8, encodeUtf8, tail, utf8Bytes, utf8Slice } from "@demicodes/utils";
3
+ import { asError, base64ToBytes, bytesToBase64, 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";
@@ -10,26 +10,19 @@ import { LexerError } from "@demicodes/just-bash/parser/lexer";
10
10
  import { resolveLimits } from "@demicodes/just-bash/limits";
11
11
  //#region src/portable-commands.ts
12
12
  /**
13
- * Individually excluded even though just-bash calls them portable: each
14
- * regresses a real, already-passing test elsewhere in this monorepo when
15
- * dispatched through this package's fork-command wrapping specifically
16
- * (isolated just-bash unit tests for these same commands pass fine, so the
17
- * break is in how @demicodes/shell wraps them, not in just-bash itself):
13
+ * Deliberately routed to real OS processes (Host.process.spawn) even though
14
+ * just-bash ships portable versions. This is a semantics decision, not a
15
+ * missing feature:
18
16
  *
19
- * - `bash`, `sh`: nested "run a script string through the interpreter again".
20
- * Their stdout never reaches the caller here `sh -c "printf hi"` and
21
- * `bash -c "echo hi"` both exit 0 with empty output, while the exact same
22
- * scripts run un-nested work fine. This package's own `AgentServer` test
23
- * spawns `sh -c` for real and asserts on the real output.
24
- * - `sleep`: just-bash's version is a plain timer, not a real backgroundable,
25
- * abortable OS process. This package's own environment tests use `sleep 10`
26
- * specifically as a long-running foreground process to exercise abort/
27
- * timeout handling (`env.abort()` / status-while-`running`), which needs a
28
- * real process to abort.
29
- *
30
- * Keeping these three off the list preserves their pre-existing
31
- * fall-through to Host.process.spawn, same as before this list started
32
- * tracking just-bash's registry.
17
+ * - `bash`, `sh`: scripts in real repositories expect a real interpreter
18
+ * (full bash semantics, real coreutils, real subprocess behavior). Routing
19
+ * them into just-bash's simulated interpreter would silently downgrade
20
+ * every `bash script.sh` a model runs. (The fork's nested-interpreter
21
+ * stdout also does not forward through this package's wrapping but even
22
+ * with that fixed, real spawn stays the right routing.)
23
+ * - `sleep`: must be a real, abortable OS process so foreground abort and
24
+ * timeout semantics (`env.abort()` while `running`) genuinely interrupt
25
+ * it; just-bash's version is a plain in-process timer.
33
26
  */
34
27
  const REAL_SPAWN_DEPENDENT_COMMANDS = /* @__PURE__ */ new Set([
35
28
  "bash",
@@ -115,6 +108,12 @@ const RESERVED_COMMAND_NAMES = /* @__PURE__ */ new Set([
115
108
  ]);
116
109
  //#endregion
117
110
  //#region src/command.ts
111
+ function emptyStdin() {
112
+ return {
113
+ text: "",
114
+ bytes: /* @__PURE__ */ new Uint8Array(0)
115
+ };
116
+ }
118
117
  const EXECUTION_ONLY_FIELDS = [
119
118
  "effects",
120
119
  "successOutput",
@@ -122,9 +121,9 @@ const EXECUTION_ONLY_FIELDS = [
122
121
  "input",
123
122
  "positionals",
124
123
  "stdinField",
125
- "output",
126
- "examples"
124
+ "output"
127
125
  ];
126
+ const COMMAND_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
128
127
  var CommandRegistry = class {
129
128
  commands = /* @__PURE__ */ new Map();
130
129
  register(command) {
@@ -139,14 +138,14 @@ var CommandRegistry = class {
139
138
  list() {
140
139
  return [...this.commands.values()];
141
140
  }
142
- renderPrompt() {
143
- const rendered = this.list().map((command) => renderCommandPrompt(command)).join("\n\n");
141
+ renderHelp() {
142
+ const rendered = this.list().map((command) => renderCommandHelp(command)).join("\n\n");
144
143
  if (!rendered) return rendered;
145
- return `${COMMAND_PROMPT_DEFAULTS}\n\n${rendered}`;
144
+ return `${COMMAND_HELP_DEFAULTS}\n\n${rendered}`;
146
145
  }
147
146
  };
148
- const COMMAND_PROMPT_DEFAULTS = "Unless a command states otherwise: success prints raw text on stdout, failure writes an error message to stderr and exits non-zero.";
149
- function parseCommandInput(root, argv, stdin = { text: "" }) {
147
+ const COMMAND_HELP_DEFAULTS = "Unless a command states otherwise: success prints raw text on stdout, failure writes an error message to stderr and exits non-zero. Pass --help at any level to print a command's documentation.";
148
+ function parseCommandInput(root, argv, stdin = emptyStdin()) {
150
149
  if (argv[0] !== root.name) throw new Error(`Expected command "${root.name}", received "${argv[0] ?? ""}"`);
151
150
  let node = root;
152
151
  const path = [root.name];
@@ -157,7 +156,7 @@ function parseCommandInput(root, argv, stdin = { text: "" }) {
157
156
  throw new Error(`Command "${path.join(" ")}" requires a subcommand`);
158
157
  }
159
158
  const token = argv[index];
160
- if (token === "prompt") return {
159
+ if (token === "--help") return {
161
160
  path: [...path],
162
161
  help: true,
163
162
  values: {},
@@ -182,6 +181,12 @@ function parseArgs(command, path, argv, startIndex, stdin) {
182
181
  let positionalIndex = 0;
183
182
  for (let i = startIndex; i < argv.length; i += 1) {
184
183
  const token = argv[i];
184
+ if (token === "--help") return {
185
+ path: [...path],
186
+ help: true,
187
+ values: {},
188
+ json: false
189
+ };
185
190
  if (token === "--json") {
186
191
  json = true;
187
192
  continue;
@@ -223,13 +228,13 @@ function resolveCommand(root, path) {
223
228
  return node;
224
229
  }
225
230
  async function runRegisteredCommand(root, ctx) {
226
- const stdin = ctx.stdin ?? { text: "" };
231
+ const stdin = ctx.stdin ?? emptyStdin();
227
232
  const parsed = parseCommandInput(root, ctx.argv, stdin);
228
233
  const displayPath = parsed.path.join(" ");
229
234
  if (parsed.help) {
230
235
  const node = resolveCommand(root, parsed.path);
231
236
  const parentPath = parsed.path.length > 1 ? parsed.path.slice(0, -1).join(" ") : "";
232
- await ctx.io.stdout(`${renderCommandPrompt(node, parentPath)}\n`);
237
+ await ctx.io.stdout(`${renderCommandHelp(node, parentPath)}\n`);
233
238
  return { exitCode: 0 };
234
239
  }
235
240
  const command = resolveCommand(root, parsed.path);
@@ -243,7 +248,8 @@ async function runRegisteredCommand(root, ctx) {
243
248
  env: ctx.env,
244
249
  cwd: ctx.cwd,
245
250
  io: parsed.json ? capture : ctx.io,
246
- storage: ctx.storage
251
+ storage: ctx.storage,
252
+ host: ctx.host
247
253
  });
248
254
  if (parsed.json && result.exitCode === 0) {
249
255
  const raw = capture.stdoutText();
@@ -262,7 +268,7 @@ async function runRegisteredCommand(root, ctx) {
262
268
  }
263
269
  return result;
264
270
  }
265
- function renderCommandPrompt(command, parentPath = "") {
271
+ function renderCommandHelp(command, parentPath = "") {
266
272
  const path = parentPath ? `${parentPath} ${command.name}` : command.name;
267
273
  const blocks = [];
268
274
  const lines = [`${path}: ${command.summary}`];
@@ -285,11 +291,6 @@ function renderCommandPrompt(command, parentPath = "") {
285
291
  if (command.stdinField) lines.push(` stdin/heredoc: ${command.stdinField}`);
286
292
  if (command.output?.json) lines.push(" --json: emits machine-readable JSON for this command");
287
293
  else lines.push(" --json: accepted only when this command defines JSON output");
288
- const examples = command.examples ?? [];
289
- if (examples.length > 0) {
290
- lines.push(" Examples:");
291
- for (const example of examples) lines.push(indent(example, 6));
292
- }
293
294
  }
294
295
  const children = command.subcommands ?? [];
295
296
  if (children.length > 0) {
@@ -297,16 +298,17 @@ function renderCommandPrompt(command, parentPath = "") {
297
298
  for (const child of children) lines.push(` ${path} ${child.name} — ${child.summary}`);
298
299
  }
299
300
  blocks.push(lines.join("\n"));
300
- for (const child of children) blocks.push(renderCommandPrompt(child, path));
301
+ for (const child of children) blocks.push(renderCommandHelp(child, path));
301
302
  return blocks.join("\n\n");
302
303
  }
303
304
  function validateCommandTree(command, path) {
305
+ if (!COMMAND_NAME_PATTERN.test(command.name)) throw new Error(`CommandRegistry: "${path}" has invalid name "${command.name}"; use letters, numbers, underscores, and hyphens`);
304
306
  const hasRun = typeof command.run === "function";
305
307
  const children = command.subcommands ?? [];
306
308
  if (!hasRun && children.length === 0) throw new Error(`CommandRegistry: "${path}" must have run() and/or subcommands`);
307
- if (hasRun) {
308
- if (!Array.isArray(command.examples)) throw new Error(`CommandRegistry: "${path}" defines run() but is missing examples[]`);
309
- } else for (const field of EXECUTION_ONLY_FIELDS) if (command[field] !== void 0) throw new Error(`CommandRegistry: "${path}" sets ${field} without run()`);
309
+ if (!hasRun) {
310
+ for (const field of EXECUTION_ONLY_FIELDS) if (command[field] !== void 0) throw new Error(`CommandRegistry: "${path}" sets ${field} without run()`);
311
+ }
310
312
  if (hasRun) {
311
313
  const input = command.input ?? {};
312
314
  if (command.stdinField && !(command.stdinField in input)) throw new Error(`CommandRegistry: "${path}" stdinField "${command.stdinField}" is not in input`);
@@ -314,7 +316,6 @@ function validateCommandTree(command, path) {
314
316
  }
315
317
  const seen = /* @__PURE__ */ new Set();
316
318
  for (const child of children) {
317
- if (child.name === "prompt") throw new Error(`CommandRegistry: "${path} prompt" is reserved for the help pseudo-subcommand`);
318
319
  if (seen.has(child.name)) throw new Error(`CommandRegistry: duplicate subcommand "${path} ${child.name}"`);
319
320
  seen.add(child.name);
320
321
  validateCommandTree(child, `${path} ${child.name}`);
@@ -357,10 +358,6 @@ function coerceValue(schema, value) {
357
358
  function formatField(field, schema, positional, stdin) {
358
359
  return `${positional ? `<${field}>` : `--${field}`}${stdin ? " (from stdin/heredoc)" : ""}${schema.description ? ` - ${schema.description}` : ""}`;
359
360
  }
360
- function indent(text, spaces) {
361
- const prefix = " ".repeat(spaces);
362
- return text.split("\n").map((line) => `${prefix}${line}`).join("\n");
363
- }
364
361
  function isArraySchema(schema) {
365
362
  const unwrapped = unwrapSchema(schema);
366
363
  return zodTypeName(unwrapped) === "array" || zodTypeName(unwrapped) === "ZodArray";
@@ -398,9 +395,6 @@ var CapturingIO = class {
398
395
  async stderr(data) {
399
396
  await this.target.stderr(data);
400
397
  }
401
- async asset(asset) {
402
- await this.target.asset(asset);
403
- }
404
398
  stdoutText() {
405
399
  return decodeUtf8(concatBytes(this.chunks));
406
400
  }
@@ -512,8 +506,10 @@ function createOutputSinks(fs, cwd, redirections) {
512
506
  function recordForegroundChunk(foreground, sourceFd, chunk) {
513
507
  const text = decodeUtf8(chunk);
514
508
  foreground.lastOutputAt = Date.now();
515
- if (sourceFd === 1) foreground.rawStdoutBuffer += text;
516
- else foreground.rawStderrBuffer += text;
509
+ if (sourceFd === 1) {
510
+ foreground.rawStdoutBuffer += text;
511
+ foreground.rawStdoutBytes.push(chunk);
512
+ } else foreground.rawStderrBuffer += text;
517
513
  const sink = foreground.outputSinks[sourceFd];
518
514
  if (sink.kind === "file" || sink.kind === "null") {
519
515
  sink.bytes.push(chunk);
@@ -615,62 +611,63 @@ function appendVisibleChunk(foreground, targetFd, text, byteLength) {
615
611
  //#endregion
616
612
  //#region src/command-artifact-store.ts
617
613
  /**
618
- * Owns persistence of shell command artifacts: a per-scope storage cache plus the
614
+ * Owns persistence of shell command artifacts: a per-storage-id cache plus the
619
615
  * set of released (tombstoned) commands. `BashEnvironment` delegates the storage
620
616
  * and release-tracking side of the `/@` virtual filesystem here, keeping only the
621
617
  * in-memory record lookups that need live command state.
622
618
  */
623
619
  var CommandArtifactStore = class {
624
620
  store;
625
- storageByScope = /* @__PURE__ */ new Map();
621
+ storageById = /* @__PURE__ */ new Map();
626
622
  released = /* @__PURE__ */ new Set();
627
623
  constructor(store) {
628
624
  this.store = store;
629
625
  }
630
- /** The artifact storage for a command scope, created (and cached) on first use. */
631
- storageFor(scopeId) {
632
- const existing = this.storageByScope.get(scopeId);
626
+ /** The artifact storage for one agent session or anonymous shell. */
627
+ storageFor(commandStorageId) {
628
+ const existing = this.storageById.get(commandStorageId);
633
629
  if (existing) return existing;
634
- const storage = new AgentSessionCommandStorage(this.store, scopeId);
635
- this.storageByScope.set(scopeId, storage);
630
+ const storage = new AgentSessionCommandStorage(this.store, commandStorageId);
631
+ this.storageById.set(commandStorageId, storage);
636
632
  return storage;
637
633
  }
638
634
  /** Whether a command's artifact has been released (tombstoned). */
639
- isReleased(scopeId, commandId) {
640
- return this.released.has(this.key(scopeId, commandId));
635
+ isReleased(commandStorageId, commandId) {
636
+ return this.released.has(this.key(commandStorageId, commandId));
641
637
  }
642
638
  /** Persists an artifact unless its command has already been released. */
643
- persist(scopeId, commandId, artifact) {
644
- if (this.isReleased(scopeId, commandId)) return;
645
- this.storageFor(scopeId).writeJson(`commands/${commandId}/artifact.json`, artifact).catch(() => {});
639
+ persist(commandStorageId, commandId, artifact) {
640
+ if (this.isReleased(commandStorageId, commandId)) return;
641
+ this.storageFor(commandStorageId).writeJson(`commands/${commandId}/artifact.json`, artifact).catch(() => {});
646
642
  }
647
643
  /** Tombstones a command and removes its persisted artifact. */
648
- async release(scopeId, commandId) {
649
- this.released.add(this.key(scopeId, commandId));
650
- await this.storageFor(scopeId).delete(`commands/${commandId}/artifact.json`).catch(() => {});
644
+ async release(commandStorageId, commandId) {
645
+ this.released.add(this.key(commandStorageId, commandId));
646
+ await this.storageFor(commandStorageId).delete(`commands/${commandId}/artifact.json`).catch(() => {});
651
647
  }
652
- key(scopeId, commandId) {
653
- return `${scopeId}\0${commandId}`;
648
+ key(commandStorageId, commandId) {
649
+ return `${commandStorageId}\0${commandId}`;
654
650
  }
655
651
  };
656
652
  //#endregion
657
653
  //#region src/registered-command-adapter.ts
658
- function commandToForkCommand(session, command, storage) {
654
+ function commandToForkCommand(session, command, storage, host) {
659
655
  return {
660
656
  name: command.name,
661
657
  consumesStdin: treeConsumesStdin(command),
662
658
  execute: async (args, ctx) => {
663
- const stdinText = decodeForkStdin(ctx.stdin);
659
+ const stdin = decodeForkStdin(ctx.stdin);
664
660
  const io = createForwardingIO();
665
661
  const argv = [command.name, ...args];
666
662
  try {
667
663
  const result = await runRegisteredCommand(command, {
668
664
  argv,
669
- stdin: { text: stdinText },
665
+ stdin,
670
666
  env: mapToRecord(ctx.env),
671
667
  cwd: ctx.cwd,
672
668
  io,
673
- storage
669
+ storage,
670
+ host
674
671
  });
675
672
  session.accumulator.audit.push({
676
673
  kind: "registered-command",
@@ -685,7 +682,8 @@ function commandToForkCommand(session, command, storage) {
685
682
  metadata: result.metadata
686
683
  });
687
684
  return {
688
- stdout: io.stdoutText(),
685
+ stdout: io.stdoutLatin1(),
686
+ stdoutKind: "bytes",
689
687
  stderr: io.stderrText(),
690
688
  exitCode: result.exitCode
691
689
  };
@@ -698,38 +696,31 @@ function commandToForkCommand(session, command, storage) {
698
696
  exitCode: 1
699
697
  });
700
698
  return {
701
- stdout: io.stdoutText(),
699
+ stdout: io.stdoutLatin1(),
700
+ stdoutKind: "bytes",
702
701
  stderr: `${io.stderrText()}${command.name}: ${message}\n`,
703
702
  exitCode: 1
704
703
  };
705
- } finally {
706
- if (io.assets().length > 0) session.accumulator.assets.push(...io.assets());
707
704
  }
708
705
  }
709
706
  };
710
707
  }
708
+ /** Collects command output as raw bytes; stdout stays byte-clean for the pipe. */
711
709
  var ForwardingIO = class {
712
710
  stdoutChunks = [];
713
711
  stderrChunks = [];
714
- assetItems = [];
715
712
  async stdout(data) {
716
713
  this.stdoutChunks.push(typeof data === "string" ? encodeUtf8(data) : data);
717
714
  }
718
715
  async stderr(data) {
719
716
  this.stderrChunks.push(typeof data === "string" ? encodeUtf8(data) : data);
720
717
  }
721
- asset(asset) {
722
- this.assetItems.push(asset);
723
- }
724
- stdoutText() {
725
- return decodeUtf8(concatBytes(this.stdoutChunks));
718
+ stdoutLatin1() {
719
+ return decodeLatin1(concatBytes(this.stdoutChunks));
726
720
  }
727
721
  stderrText() {
728
722
  return decodeUtf8(concatBytes(this.stderrChunks));
729
723
  }
730
- assets() {
731
- return this.assetItems;
732
- }
733
724
  };
734
725
  function createForwardingIO() {
735
726
  return new ForwardingIO();
@@ -743,26 +734,52 @@ function mapToRecord(map) {
743
734
  for (const [key, value] of map) record[key] = value;
744
735
  return record;
745
736
  }
737
+ /** Pipes hand stdin over as a latin1-packed byte string; expose bytes and a UTF-8 text view. */
746
738
  function decodeForkStdin(stdin) {
747
- if (!stdin) return "";
748
- if (stdin instanceof Uint8Array) return decodeUtf8(stdin);
739
+ if (!stdin) return {
740
+ text: "",
741
+ bytes: /* @__PURE__ */ new Uint8Array(0)
742
+ };
743
+ if (stdin instanceof Uint8Array) return {
744
+ text: decodeUtf8(stdin),
745
+ bytes: stdin
746
+ };
749
747
  const latin1 = stdin;
750
- if (!latin1) return "";
748
+ if (!latin1) return {
749
+ text: "",
750
+ bytes: /* @__PURE__ */ new Uint8Array(0)
751
+ };
752
+ const bytes = encodeLatin1(latin1);
751
753
  let hasHighByte = false;
754
+ let hasWideChar = false;
752
755
  for (let i = 0; i < latin1.length; i += 1) {
753
756
  const code = latin1.charCodeAt(i);
754
- if (code > 255) return latin1;
755
- if (code > 127) hasHighByte = true;
757
+ if (code > 255) hasWideChar = true;
758
+ else if (code > 127) hasHighByte = true;
756
759
  }
757
- if (!hasHighByte) return latin1;
758
- const bytes = new Uint8Array(latin1.length);
759
- for (let i = 0; i < latin1.length; i += 1) bytes[i] = latin1.charCodeAt(i);
760
- return decodeUtf8(bytes);
760
+ if (hasWideChar) return {
761
+ text: latin1,
762
+ bytes: encodeUtf8(latin1)
763
+ };
764
+ if (!hasHighByte) return {
765
+ text: latin1,
766
+ bytes
767
+ };
768
+ return {
769
+ text: decodeUtf8(bytes),
770
+ bytes
771
+ };
761
772
  }
762
773
  //#endregion
763
774
  //#region src/environment.ts
764
775
  const DEFAULT_TIMEOUT_MS = 1e4;
765
- const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024;
776
+ const DEFAULT_OUTPUT_LIMIT_BYTES = 1048576;
777
+ /**
778
+ * Ceiling for a raw-byte final stream. Sized so an ordinary viewing-grade clip
779
+ * survives the shell and reaches the layer that decides what to do with it,
780
+ * while still bounding a runaway producer.
781
+ */
782
+ const DEFAULT_BINARY_LIMIT_BYTES = 16777216;
766
783
  /** Upper bound for a single exec observation window (also the command-bridge wait ceiling). */
767
784
  const MAX_TIMEOUT_MS = 6e5;
768
785
  var BashEnvironment = class {
@@ -771,8 +788,8 @@ var BashEnvironment = class {
771
788
  shellIdFactory;
772
789
  commandIdFactory;
773
790
  initialEnv;
774
- execEnv;
775
791
  defaultOutputLimitBytes;
792
+ defaultBinaryLimitBytes;
776
793
  shells = /* @__PURE__ */ new Map();
777
794
  defaultShellByAgentSessionId = /* @__PURE__ */ new Map();
778
795
  commandsById = /* @__PURE__ */ new Map();
@@ -784,12 +801,15 @@ var BashEnvironment = class {
784
801
  this.shellIdFactory = options.shellIdFactory ?? (() => globalThis.crypto.randomUUID());
785
802
  this.commandIdFactory = options.commandIdFactory ?? (() => globalThis.crypto.randomUUID());
786
803
  this.initialEnv = options.initialEnv ?? {};
787
- this.execEnv = options.execEnv;
788
804
  this.defaultOutputLimitBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES;
805
+ this.defaultBinaryLimitBytes = options.maxBinaryBytes ?? DEFAULT_BINARY_LIMIT_BYTES;
789
806
  }
790
807
  getShell(shellId) {
791
808
  return this.shells.get(shellId) ?? null;
792
809
  }
810
+ hasCommand(commandId) {
811
+ return this.commandsById.has(commandId);
812
+ }
793
813
  registerCommand(command) {
794
814
  if (this.commands.get(command.name)) return;
795
815
  this.commands.register(command);
@@ -799,20 +819,13 @@ var BashEnvironment = class {
799
819
  }
800
820
  async exec(input) {
801
821
  const timeoutMs = normalizeTimeoutMs(input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
802
- const session = input.shellId ? this.requireShell(input.shellId) : this.availableDefaultShell(input.agentSessionId);
803
- if (session.exited) throw new Error(`Shell session "${session.id}" has exited`);
804
- if (input.agentSessionId) {
805
- session.state.env.set("DEMI_AGENT_SESSION_ID", input.agentSessionId);
806
- const extraEnv = this.execEnv?.(input.agentSessionId);
807
- if (extraEnv) {
808
- const exported = session.state.exportedVars ?? /* @__PURE__ */ new Set();
809
- session.state.exportedVars = exported;
810
- for (const [key, value] of Object.entries(extraEnv)) {
811
- session.state.env.set(key, value);
812
- exported.add(key);
813
- }
814
- }
822
+ if (input.shellId && input.ephemeral) throw new Error("ShellExecInput: \"shellId\" and \"ephemeral\" are mutually exclusive");
823
+ if (input.cwd !== void 0 && !input.ephemeral) throw new Error("ShellExecInput: \"cwd\" requires \"ephemeral\"; a persistent shell owns its cwd");
824
+ if (input.cwd !== void 0) {
825
+ if (!(await this.host.fs.stat(input.cwd).catch(() => null))?.isDirectory) throw new Error(`Shell exec cwd is not a directory: ${input.cwd}`);
815
826
  }
827
+ const session = input.shellId ? this.requireShell(input.shellId) : input.ephemeral ? this.createShell(input.agentSessionId, input.cwd) : this.availableDefaultShell(input.agentSessionId);
828
+ if (session.exited) throw new Error(`Shell session "${session.id}" has exited`);
816
829
  if (session.pendingExec || session.foreground) {
817
830
  const commandId = session.activeCommandId ?? session.foreground?.commandId ?? "unknown";
818
831
  throw new Error(`Shell session "${session.id}" is already running command "${commandId}"`);
@@ -824,7 +837,7 @@ var BashEnvironment = class {
824
837
  }
825
838
  async status(input) {
826
839
  const record = this.requireCommand(input.commandId);
827
- return this.snapshotCommand(record, input);
840
+ return this.commandStatus(record, input);
828
841
  }
829
842
  async write(input) {
830
843
  const record = this.requireCommand(input.commandId);
@@ -834,11 +847,11 @@ var BashEnvironment = class {
834
847
  const data = typeof input.stdin === "string" ? encodeUtf8(input.stdin) : input.stdin;
835
848
  if (data.byteLength === 0) throw new Error("shell_write field \"stdin\" must not be empty; use shell_status to poll");
836
849
  await foreground.handle.writeStdin(data);
837
- return this.snapshotCommand(record, input);
850
+ return this.commandStatus(record, input);
838
851
  }
839
852
  async abort(input) {
840
853
  const record = this.requireCommand(input.commandId);
841
- if (record.status !== "running") return this.snapshotCommand(record, input);
854
+ if (record.status !== "running") return this.commandStatus(record, input);
842
855
  const session = this.requireShell(record.shellId);
843
856
  const foreground = this.requireForegroundCommand(session, record.id);
844
857
  foreground.abortController.abort();
@@ -850,7 +863,7 @@ var BashEnvironment = class {
850
863
  const record = this.commandsById.get(commandId);
851
864
  if (!record || record.status === "running") return false;
852
865
  this.commandsById.delete(commandId);
853
- await this.artifacts.release(record.commandScopeId, commandId);
866
+ await this.artifacts.release(record.commandStorageId, commandId);
854
867
  return true;
855
868
  }
856
869
  async disposeShell(shellId) {
@@ -916,25 +929,22 @@ var BashEnvironment = class {
916
929
  if (!agentSessionId || shell.exited || !shell.pendingExec && !shell.foreground) return shell;
917
930
  return this.createShell(agentSessionId);
918
931
  }
919
- createShell(agentSessionId) {
932
+ createShell(agentSessionId, initialCwd) {
920
933
  const id = this.shellIdFactory();
921
- const commandScopeId = agentSessionId ?? id;
922
- const cwd = this.host.defaultCwd;
923
- const fs = new HostBackedFileSystem(this.host, { lookup: (path) => this.lookupVirtualArtifact(commandScopeId, path) });
934
+ const commandStorageId = agentSessionId ?? id;
935
+ const cwd = initialCwd ?? this.host.defaultCwd;
936
+ const fs = new HostBackedFileSystem(this.host, { lookup: (path) => this.lookupVirtualArtifact(commandStorageId, path) });
924
937
  const env = /* @__PURE__ */ new Map();
925
938
  for (const [key, value] of Object.entries(this.initialEnv)) env.set(key, value);
926
939
  env.set("PWD", cwd);
927
- env.set("DEMI_SESSION_ID", commandScopeId);
940
+ if (agentSessionId) env.set("DEMI_SESSION_ID", agentSessionId);
928
941
  env.set("DEMI_SHELL_ID", id);
929
942
  if (!env.has("IFS")) env.set("IFS", " \n");
930
943
  if (!env.has("PS1")) env.set("PS1", "");
931
944
  if (!env.has("PS2")) env.set("PS2", "> ");
932
945
  if (!env.has("SHLVL")) env.set("SHLVL", "1");
933
- const exportedVars = /* @__PURE__ */ new Set([
934
- "PWD",
935
- "DEMI_SESSION_ID",
936
- "DEMI_SHELL_ID"
937
- ]);
946
+ const exportedVars = /* @__PURE__ */ new Set(["PWD", "DEMI_SHELL_ID"]);
947
+ if (agentSessionId) exportedVars.add("DEMI_SESSION_ID");
938
948
  for (const key of env.keys()) if (key !== key.toLowerCase()) exportedVars.add(key);
939
949
  for (const key of Object.keys(this.initialEnv)) exportedVars.add(key);
940
950
  const state = {
@@ -995,7 +1005,8 @@ var BashEnvironment = class {
995
1005
  const forkCommands = /* @__PURE__ */ new Map();
996
1006
  const session = {
997
1007
  id,
998
- commandScopeId,
1008
+ agentSessionId: agentSessionId ?? null,
1009
+ commandStorageId,
999
1010
  state,
1000
1011
  fs,
1001
1012
  interpreter: void 0,
@@ -1004,8 +1015,7 @@ var BashEnvironment = class {
1004
1015
  stdout: "",
1005
1016
  stderr: "",
1006
1017
  audit: [],
1007
- commandMetadata: [],
1008
- assets: []
1018
+ commandMetadata: []
1009
1019
  },
1010
1020
  foregroundWaiters: /* @__PURE__ */ new Set(),
1011
1021
  backgroundJobs: /* @__PURE__ */ new Map(),
@@ -1013,19 +1023,20 @@ var BashEnvironment = class {
1013
1023
  exited: false
1014
1024
  };
1015
1025
  for (const command of createPortableCommands(session)) forkCommands.set(command.name, command);
1016
- const storage = new AgentSessionCommandStorage(this.host.store, commandScopeId);
1017
- for (const command of this.commands.list()) forkCommands.set(command.name, commandToForkCommand(session, command, storage));
1026
+ const storage = new AgentSessionCommandStorage(this.host.store, commandStorageId);
1027
+ for (const command of this.commands.list()) forkCommands.set(command.name, commandToForkCommand(session, command, storage, this.host));
1018
1028
  session.abortController = new AbortController();
1029
+ const limits = resolveLimits({
1030
+ maxOutputSize: 1073741824,
1031
+ maxCommandCount: 1e6,
1032
+ maxLoopIterations: 1e6,
1033
+ maxCallDepth: 1e3,
1034
+ maxGlobOperations: 1e6
1035
+ });
1019
1036
  session.interpreter = new Interpreter({
1020
1037
  fs,
1021
1038
  commands: forkCommands,
1022
- limits: resolveLimits({
1023
- maxOutputSize: 1024 * 1024 * 1024,
1024
- maxCommandCount: 1e6,
1025
- maxLoopIterations: 1e6,
1026
- maxCallDepth: 1e3,
1027
- maxGlobOperations: 1e6
1028
- }),
1039
+ limits,
1029
1040
  exec: async () => ({
1030
1041
  stdout: "",
1031
1042
  stderr: "",
@@ -1044,6 +1055,7 @@ var BashEnvironment = class {
1044
1055
  }
1045
1056
  async runScript(session, script, input) {
1046
1057
  const record = this.createCommandRecord(session, script);
1058
+ record.outputLimitBytes = input.maxOutputBytes ?? this.defaultOutputLimitBytes;
1047
1059
  let ast;
1048
1060
  try {
1049
1061
  ast = parse(script);
@@ -1055,7 +1067,7 @@ var BashEnvironment = class {
1055
1067
  record.exitCode = 2;
1056
1068
  session.state.lastExitCode = 2;
1057
1069
  session.activeCommandId = void 0;
1058
- return this.snapshotCommand(record, input);
1070
+ return this.commandStatus(record, input);
1059
1071
  }
1060
1072
  throw error;
1061
1073
  }
@@ -1063,8 +1075,7 @@ var BashEnvironment = class {
1063
1075
  stdout: "",
1064
1076
  stderr: "",
1065
1077
  audit: [],
1066
- commandMetadata: [],
1067
- assets: []
1078
+ commandMetadata: []
1068
1079
  };
1069
1080
  session.abortController = new AbortController();
1070
1081
  session.activeCommandId = record.id;
@@ -1087,7 +1098,7 @@ var BashEnvironment = class {
1087
1098
  const record = {
1088
1099
  id,
1089
1100
  shellId: session.id,
1090
- commandScopeId: session.commandScopeId,
1101
+ commandStorageId: session.commandStorageId,
1091
1102
  script,
1092
1103
  startedAt: now,
1093
1104
  lastOutputAt: now,
@@ -1100,7 +1111,7 @@ var BashEnvironment = class {
1100
1111
  outputOffset: 0,
1101
1112
  audit: [],
1102
1113
  commandMetadata: [],
1103
- assets: []
1114
+ outputLimitBytes: this.defaultOutputLimitBytes
1104
1115
  };
1105
1116
  this.commandsById.set(id, record);
1106
1117
  return record;
@@ -1221,7 +1232,7 @@ var BashEnvironment = class {
1221
1232
  foreground = outcome.foreground;
1222
1233
  continue;
1223
1234
  }
1224
- if (outcome.kind === "timeout") return this.snapshotCommand(record, input);
1235
+ if (outcome.kind === "timeout") return this.commandStatus(record, input);
1225
1236
  if (outcome.kind === "aborted") {
1226
1237
  const activeForeground = foreground ?? session.foreground;
1227
1238
  if (!activeForeground) return this.collectAbortedWithoutForeground(session, record, input);
@@ -1306,6 +1317,7 @@ var BashEnvironment = class {
1306
1317
  startedAt,
1307
1318
  lastOutputAt: startedAt,
1308
1319
  rawStdoutBuffer: "",
1320
+ rawStdoutBytes: [],
1309
1321
  rawStderrBuffer: "",
1310
1322
  stdoutBuffer: "",
1311
1323
  stderrBuffer: "",
@@ -1326,7 +1338,7 @@ var BashEnvironment = class {
1326
1338
  };
1327
1339
  session.foreground = foreground;
1328
1340
  notifyForegroundWaiters(session.foregroundWaiters, foreground);
1329
- if (opts.stdin && opts.stdin.length > 0) await handle.writeStdin(encodeUtf8(opts.stdin));
1341
+ if (opts.stdin && opts.stdin.length > 0) await handle.writeStdin(encodeLatin1(opts.stdin));
1330
1342
  if (opts.stdinProvided) await handle.closeStdin();
1331
1343
  if (handle.output) {
1332
1344
  foreground.stdoutPump = pumpOutputStream(handle.output, (chunk) => {
@@ -1339,7 +1351,7 @@ var BashEnvironment = class {
1339
1351
  }
1340
1352
  const exit = await foreground.exitPromise;
1341
1353
  await Promise.allSettled([foreground.stdoutPump, foreground.stderrPump]);
1342
- const stdout = foreground.rawStdoutBuffer;
1354
+ const stdout = decodeLatin1(concatBytes(foreground.rawStdoutBytes));
1343
1355
  const exitCode = exit.exitCode ?? 127;
1344
1356
  const stderr = exit.exitCode === null && foreground.rawStderrBuffer.length === 0 ? `${command}: ${exit.signal ?? "command not found"}\n` : foreground.rawStderrBuffer;
1345
1357
  foreground.audit[0] = {
@@ -1360,12 +1372,13 @@ var BashEnvironment = class {
1360
1372
  session.foreground = void 0;
1361
1373
  return {
1362
1374
  stdout,
1375
+ stdoutKind: "bytes",
1363
1376
  stderr,
1364
1377
  exitCode
1365
1378
  };
1366
1379
  }
1367
1380
  collectExited(session, record, resultOrError, foreground, input = {}) {
1368
- if (record.status !== "running") return this.snapshotCommand(record, input);
1381
+ if (record.status !== "running") return this.commandStatus(record, input);
1369
1382
  if (resultOrError instanceof Error) {
1370
1383
  if (resultOrError instanceof ExitError) {
1371
1384
  session.exited = true;
@@ -1403,12 +1416,36 @@ var BashEnvironment = class {
1403
1416
  appendRecordOutput(record, "stderr", text);
1404
1417
  return this.finishExited(session, record, 1, input);
1405
1418
  }
1406
- const stdoutText = foreground ? resultOrError.stdout : decodeBytesToUtf8(unsafeBytesFromLatin1(resultOrError.stdout));
1419
+ const raw = resultOrError.stdout;
1420
+ let stdoutText;
1421
+ let binary;
1422
+ if (hasWideChar(raw)) stdoutText = raw;
1423
+ else {
1424
+ const bytes = encodeLatin1(raw);
1425
+ const strict = decodeUtf8Strict(bytes);
1426
+ if (strict !== null) stdoutText = strict;
1427
+ else {
1428
+ const cap = this.defaultBinaryLimitBytes;
1429
+ const truncated = bytes.length > cap;
1430
+ binary = {
1431
+ data: truncated ? bytes.slice(0, cap) : bytes,
1432
+ truncated,
1433
+ totalBytes: bytes.length,
1434
+ limitBytes: cap
1435
+ };
1436
+ stdoutText = `<binary stdout: ${bytes.length} bytes${truncated ? `, exceeds the ${cap}-byte binary limit` : ""}; raw bytes at /@/commands/${record.id}/stdout.bin>\n`;
1437
+ }
1438
+ }
1407
1439
  const stderrText = foreground ? resultOrError.stderr : decodeBytesToUtf8(unsafeBytesFromLatin1(resultOrError.stderr));
1408
1440
  session.accumulator.stdout += stdoutText;
1409
1441
  session.accumulator.stderr += stderrText;
1410
- if (foreground) record.outputChunks = [...foreground.outputChunks];
1411
- else if (record.outputChunks.length === 0) {
1442
+ if (binary) record.binaryStdout = binary;
1443
+ if (foreground && !binary) record.outputChunks = [...foreground.outputChunks];
1444
+ else if (binary) {
1445
+ record.outputChunks = [];
1446
+ appendRecordOutput(record, "stdout", stdoutText);
1447
+ appendRecordOutput(record, "stderr", stderrText);
1448
+ } else if (record.outputChunks.length === 0) {
1412
1449
  appendRecordOutput(record, "stdout", stdoutText);
1413
1450
  appendRecordOutput(record, "stderr", stderrText);
1414
1451
  }
@@ -1427,13 +1464,12 @@ var BashEnvironment = class {
1427
1464
  record.exitCode = exitCode;
1428
1465
  record.audit = [...session.accumulator.audit];
1429
1466
  record.commandMetadata = [...session.accumulator.commandMetadata];
1430
- record.assets = [...session.accumulator.assets];
1431
1467
  session.pendingExec = void 0;
1432
1468
  if (session.activeCommandId === record.id) session.activeCommandId = void 0;
1433
- return this.snapshotCommand(record, input);
1469
+ return this.commandStatus(record, input);
1434
1470
  }
1435
1471
  async collectAborted(session, record, foreground, input = {}) {
1436
- if (record.status !== "running") return this.snapshotCommand(record, input);
1472
+ if (record.status !== "running") return this.commandStatus(record, input);
1437
1473
  foreground.abortController.abort();
1438
1474
  foreground.handle.kill("SIGTERM").catch(() => {});
1439
1475
  await flushForegroundSinks(session, foreground);
@@ -1445,10 +1481,10 @@ var BashEnvironment = class {
1445
1481
  session.foreground = void 0;
1446
1482
  session.pendingExec = void 0;
1447
1483
  if (session.activeCommandId === record.id) session.activeCommandId = void 0;
1448
- return this.snapshotCommand(record, input);
1484
+ return this.commandStatus(record, input);
1449
1485
  }
1450
1486
  collectAbortedWithoutForeground(session, record, input = {}) {
1451
- if (record.status !== "running") return this.snapshotCommand(record, input);
1487
+ if (record.status !== "running") return this.commandStatus(record, input);
1452
1488
  session.abortController?.abort();
1453
1489
  session.pendingExec = void 0;
1454
1490
  if (session.activeCommandId === record.id) session.activeCommandId = void 0;
@@ -1460,9 +1496,9 @@ var BashEnvironment = class {
1460
1496
  }
1461
1497
  record.lastOutputAt = Date.now();
1462
1498
  record.status = "aborted";
1463
- return this.snapshotCommand(record, input);
1499
+ return this.commandStatus(record, input);
1464
1500
  }
1465
- snapshotCommand(record, input = {}) {
1501
+ commandStatus(record, input = {}) {
1466
1502
  const foreground = this.shells.get(record.shellId)?.foreground;
1467
1503
  if (record.status === "running" && foreground?.commandId === record.id) {
1468
1504
  record.stdout = foreground.stdoutBuffer;
@@ -1471,9 +1507,9 @@ var BashEnvironment = class {
1471
1507
  record.lastOutputAt = foreground.lastOutputAt;
1472
1508
  }
1473
1509
  const maxOutputBytes = input.maxOutputBytes ?? this.defaultOutputLimitBytes;
1474
- const stdout = streamArtifact(record, "stdout", input.stdoutOffset, maxOutputBytes);
1475
- const stderr = streamArtifact(record, "stderr", input.stderrOffset, maxOutputBytes);
1476
- const output = streamOutputArtifact(record, input.outputOffset, maxOutputBytes);
1510
+ const stdout = streamView(record, "stdout", input.stdoutOffset, maxOutputBytes);
1511
+ const stderr = streamView(record, "stderr", input.stderrOffset, maxOutputBytes);
1512
+ const output = mergedOutputView(record, input.outputOffset, maxOutputBytes);
1477
1513
  const base = {
1478
1514
  shellId: record.shellId,
1479
1515
  commandId: record.id,
@@ -1492,7 +1528,7 @@ var BashEnvironment = class {
1492
1528
  audit: record.audit
1493
1529
  };
1494
1530
  if (record.commandMetadata.length > 0) result.commandMetadata = record.commandMetadata;
1495
- if (record.assets.length > 0) result.assets = record.assets;
1531
+ if (record.binaryStdout) result.binaryStdout = record.binaryStdout;
1496
1532
  return result;
1497
1533
  }
1498
1534
  if (record.status === "aborted") return {
@@ -1504,52 +1540,56 @@ var BashEnvironment = class {
1504
1540
  status: "running"
1505
1541
  };
1506
1542
  }
1507
- async lookupVirtualArtifact(scopeId, path) {
1543
+ async lookupVirtualArtifact(commandStorageId, path) {
1508
1544
  const parts = path.split("/").filter(Boolean);
1509
1545
  if (parts.length === 1 && parts[0] === "@") return virtualDirectory(["commands"]);
1510
- if (parts.length === 2 && parts[0] === "@" && parts[1] === "commands") return virtualDirectory(await this.commandArtifactIds(scopeId));
1546
+ if (parts.length === 2 && parts[0] === "@" && parts[1] === "commands") return virtualDirectory(await this.commandArtifactIds(commandStorageId));
1511
1547
  if (parts.length === 3 && parts[0] === "@" && parts[1] === "commands") {
1512
- if (!await this.commandArtifact(scopeId, parts[2])) return null;
1513
- return virtualDirectory([
1548
+ const artifact = await this.commandArtifact(commandStorageId, parts[2]);
1549
+ if (!artifact) return null;
1550
+ const entries = [
1514
1551
  "meta.json",
1515
1552
  "stderr.txt",
1516
1553
  "stdout.txt"
1517
- ]);
1554
+ ];
1555
+ if (artifact.stdoutBinary) entries.push("stdout.bin");
1556
+ return virtualDirectory(entries);
1518
1557
  }
1519
1558
  if (parts.length !== 4 || parts[0] !== "@" || parts[1] !== "commands") return null;
1520
- const artifact = await this.commandArtifact(scopeId, parts[2]);
1559
+ const artifact = await this.commandArtifact(commandStorageId, parts[2]);
1521
1560
  if (!artifact) return null;
1522
1561
  const fileName = parts[3];
1523
1562
  if (fileName === "stdout.txt") return virtualFile(encodeUtf8(artifact.stdout));
1563
+ if (fileName === "stdout.bin" && artifact.stdoutBinary) return virtualFile(base64ToBytes(artifact.stdoutBinary.base64));
1524
1564
  if (fileName === "stderr.txt") return virtualFile(encodeUtf8(artifact.stderr));
1525
1565
  if (fileName === "meta.json") return virtualFile(encodeUtf8(`${JSON.stringify(commandArtifactMeta(artifact), null, 2)}\n`));
1526
1566
  return null;
1527
1567
  }
1528
- async commandArtifactIds(scopeId) {
1568
+ async commandArtifactIds(commandStorageId) {
1529
1569
  const ids = /* @__PURE__ */ new Set();
1530
- for (const record of this.commandsById.values()) if (record.commandScopeId === scopeId && !this.artifacts.isReleased(scopeId, record.id)) ids.add(record.id);
1531
- const keys = await this.artifacts.storageFor(scopeId).list("commands").catch(() => []);
1570
+ for (const record of this.commandsById.values()) if (record.commandStorageId === commandStorageId && !this.artifacts.isReleased(commandStorageId, record.id)) ids.add(record.id);
1571
+ const keys = await this.artifacts.storageFor(commandStorageId).list("commands").catch(() => []);
1532
1572
  for (const key of keys) {
1533
1573
  const match = /^commands\/([^/]+)\/artifact\.json$/.exec(key);
1534
- if (match && !this.artifacts.isReleased(scopeId, match[1])) ids.add(match[1]);
1574
+ if (match && !this.artifacts.isReleased(commandStorageId, match[1])) ids.add(match[1]);
1535
1575
  }
1536
1576
  return [...ids];
1537
1577
  }
1538
- async commandArtifact(scopeId, commandId) {
1539
- if (this.artifacts.isReleased(scopeId, commandId)) return null;
1578
+ async commandArtifact(commandStorageId, commandId) {
1579
+ if (this.artifacts.isReleased(commandStorageId, commandId)) return null;
1540
1580
  const record = this.commandsById.get(commandId);
1541
- if (record?.commandScopeId === scopeId) {
1581
+ if (record?.commandStorageId === commandStorageId) {
1542
1582
  this.syncRunningRecord(record);
1543
- return persistedArtifactFromRecord(record);
1583
+ return commandArtifactFromRecord(record);
1544
1584
  }
1545
- const value = await this.artifacts.storageFor(scopeId).readJson(`commands/${commandId}/artifact.json`).catch(() => null);
1546
- return isPersistedShellCommandArtifact(value) ? value : null;
1585
+ const value = await this.artifacts.storageFor(commandStorageId).readJson(`commands/${commandId}/artifact.json`).catch(() => null);
1586
+ return isCommandArtifact(value) ? value : null;
1547
1587
  }
1548
1588
  persistCommandArtifact(record) {
1549
- const fingerprint = `${record.status}:${record.exitCode ?? ""}:${record.stdout.length}:${record.stderr.length}`;
1589
+ const fingerprint = `${record.status}:${record.exitCode ?? ""}:${record.stdout.length}:${record.stderr.length}:${record.binaryStdout?.totalBytes ?? ""}`;
1550
1590
  if (record.persistedFingerprint === fingerprint) return;
1551
1591
  record.persistedFingerprint = fingerprint;
1552
- this.artifacts.persist(record.commandScopeId, record.id, persistedArtifactFromRecord(record));
1592
+ this.artifacts.persist(record.commandStorageId, record.id, commandArtifactFromRecord(record));
1553
1593
  }
1554
1594
  syncRunningRecord(record) {
1555
1595
  const foreground = this.shells.get(record.shellId)?.foreground;
@@ -1577,11 +1617,16 @@ function createPortableCommands(session) {
1577
1617
  }
1578
1618
  }));
1579
1619
  }
1620
+ /** True when the string contains a char > 0xFF, i.e. already-decoded Unicode text. */
1621
+ function hasWideChar(value) {
1622
+ for (let i = 0; i < value.length; i += 1) if (value.charCodeAt(i) > 255) return true;
1623
+ return false;
1624
+ }
1580
1625
  function normalizeTimeoutMs(value) {
1581
1626
  if (!Number.isFinite(value) || value < 1 || value > 6e5) throw new Error(`timeoutMs must be between 1 and ${MAX_TIMEOUT_MS}`);
1582
1627
  return Math.floor(value);
1583
1628
  }
1584
- function streamArtifact(record, stream, explicitOffset, maxOutputBytes) {
1629
+ function streamView(record, stream, explicitOffset, maxOutputBytes) {
1585
1630
  const text = stream === "stdout" ? record.stdout : record.stderr;
1586
1631
  const totalBytes = utf8Bytes(text);
1587
1632
  const boundedOffset = clampOffset(explicitOffset ?? (stream === "stdout" ? record.stdoutOffset : record.stderrOffset), totalBytes);
@@ -1601,7 +1646,7 @@ function streamArtifact(record, stream, explicitOffset, maxOutputBytes) {
1601
1646
  truncated
1602
1647
  };
1603
1648
  }
1604
- function streamOutputArtifact(record, explicitOffset, maxOutputBytes) {
1649
+ function mergedOutputView(record, explicitOffset, maxOutputBytes) {
1605
1650
  const totalBytes = record.outputChunks.reduce((total, chunk) => total + chunk.bytes, 0);
1606
1651
  const offset = clampOffset(explicitOffset ?? record.outputOffset, totalBytes);
1607
1652
  const byteLimit = Math.max(0, Math.floor(maxOutputBytes));
@@ -1655,7 +1700,7 @@ function ensureRecordOutputCoverage(record) {
1655
1700
  appendRecordOutput(record, "stdout", record.stdout);
1656
1701
  appendRecordOutput(record, "stderr", record.stderr);
1657
1702
  }
1658
- function persistedArtifactFromRecord(record) {
1703
+ function commandArtifactFromRecord(record) {
1659
1704
  return {
1660
1705
  status: record.status,
1661
1706
  shellId: record.shellId,
@@ -1664,7 +1709,12 @@ function persistedArtifactFromRecord(record) {
1664
1709
  lastOutputAt: record.lastOutputAt,
1665
1710
  exitCode: record.exitCode ?? null,
1666
1711
  stdout: record.stdout,
1667
- stderr: record.stderr
1712
+ stderr: record.stderr,
1713
+ ...record.binaryStdout ? { stdoutBinary: {
1714
+ base64: bytesToBase64(record.binaryStdout.data),
1715
+ truncated: record.binaryStdout.truncated,
1716
+ totalBytes: record.binaryStdout.totalBytes
1717
+ } } : {}
1668
1718
  };
1669
1719
  }
1670
1720
  function commandArtifactMeta(artifact) {
@@ -1686,10 +1736,15 @@ function commandArtifactMeta(artifact) {
1686
1736
  stderr: {
1687
1737
  path: stderrPath,
1688
1738
  bytes: utf8Bytes(artifact.stderr)
1689
- }
1739
+ },
1740
+ ...artifact.stdoutBinary ? { stdoutBinary: {
1741
+ path: `/@/commands/${artifact.commandId}/stdout.bin`,
1742
+ bytes: artifact.stdoutBinary.totalBytes,
1743
+ truncated: artifact.stdoutBinary.truncated
1744
+ } } : {}
1690
1745
  };
1691
1746
  }
1692
- function isPersistedShellCommandArtifact(value) {
1747
+ function isCommandArtifact(value) {
1693
1748
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1694
1749
  const record = value;
1695
1750
  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";
@@ -1721,4 +1776,4 @@ function heredocDelimiter(body) {
1721
1776
  return delimiter;
1722
1777
  }
1723
1778
  //#endregion
1724
- export { AgentSessionCommandStorage, BashEnvironment, COMMAND_PROMPT_DEFAULTS, CommandRegistry, DEMI_PORTABLE_COMMANDS, HostBackedFileSystem, MAX_TIMEOUT_MS, RESERVED_COMMAND_NAMES, heredocDelimiter, parseCommandInput, renderCommandPrompt, runRegisteredCommand, shellQuote, virtualDirectory, virtualFile };
1779
+ 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 };
@@ -1,6 +1,5 @@
1
- import { u as HostStore } from "./host-DIg4RxcL.mjs";
1
+ import { t as Host, u as HostStore } from "./host-DIg4RxcL.mjs";
2
2
  import { z } from "zod";
3
-
4
3
  //#region src/command.d.ts
5
4
  type CommandInputSpec = Record<string, z.ZodType>;
6
5
  interface CommandOutputSpec {
@@ -27,8 +26,6 @@ interface Command {
27
26
  positionals?: string[];
28
27
  stdinField?: string;
29
28
  output?: CommandOutputSpec;
30
- /** Required when `run` is set (may be empty for fixtures). */
31
- examples?: string[];
32
29
  }
33
30
  interface ParsedCommandInput {
34
31
  /**
@@ -36,7 +33,7 @@ interface ParsedCommandInput {
36
33
  * For help: path of the node help was requested for.
37
34
  */
38
35
  path: string[];
39
- /** True when the invocation was `<path…> prompt`. */
36
+ /** True when the invocation requested `--help`. */
40
37
  help: boolean;
41
38
  values: Record<string, unknown>;
42
39
  json: boolean;
@@ -49,24 +46,23 @@ interface CommandRunContext {
49
46
  cwd: string;
50
47
  io: CommandIO;
51
48
  storage: CommandStorage;
49
+ /** Host of the BashEnvironment executing this command. */
50
+ host: Host;
52
51
  }
53
52
  interface CommandRunResult {
54
53
  exitCode: number;
55
54
  metadata?: unknown;
56
55
  }
57
56
  interface CommandStdin {
57
+ /** Stdin decoded as UTF-8 text (lossy for non-text input). */
58
58
  text: string;
59
+ /** Raw stdin bytes, byte-identical to what the pipe delivered. */
60
+ bytes: Uint8Array;
59
61
  }
60
- /** A non-text content item a command emits to the model, peer to stdout text. */
61
- type CommandAsset = {
62
- type: 'image';
63
- mediaType: string;
64
- data: string;
65
- };
62
+ declare function emptyStdin(): CommandStdin;
66
63
  interface CommandIO {
67
64
  stdout(data: string | Uint8Array): Promise<void> | void;
68
65
  stderr(data: string | Uint8Array): Promise<void> | void;
69
- asset(asset: CommandAsset): Promise<void> | void;
70
66
  }
71
67
  interface CommandStorage {
72
68
  readJson<T>(key: string): Promise<T | null>;
@@ -81,18 +77,19 @@ interface CommandExecutionContext {
81
77
  cwd: string;
82
78
  io: CommandIO;
83
79
  storage: CommandStorage;
80
+ host: Host;
84
81
  }
85
82
  declare class CommandRegistry {
86
83
  private readonly commands;
87
84
  register(command: Command): void;
88
85
  get(name: string): Command | null;
89
86
  list(): Command[];
90
- renderPrompt(): string;
87
+ renderHelp(): string;
91
88
  }
92
- declare const COMMAND_PROMPT_DEFAULTS = "Unless a command states otherwise: success prints raw text on stdout, failure writes an error message to stderr and exits non-zero.";
89
+ declare const COMMAND_HELP_DEFAULTS = "Unless a command states otherwise: success prints raw text on stdout, failure writes an error message to stderr and exits non-zero. Pass --help at any level to print a command's documentation.";
93
90
  declare function parseCommandInput(root: Command, argv: string[], stdin?: CommandStdin): ParsedCommandInput;
94
91
  declare function runRegisteredCommand(root: Command, ctx: CommandExecutionContext): Promise<CommandRunResult>;
95
- declare function renderCommandPrompt(command: Command, parentPath?: string): string;
92
+ declare function renderCommandHelp(command: Command, parentPath?: string): string;
96
93
  //#endregion
97
94
  //#region src/storage.d.ts
98
95
  declare class AgentSessionCommandStorage implements CommandStorage {
@@ -106,4 +103,4 @@ declare class AgentSessionCommandStorage implements CommandStorage {
106
103
  private key;
107
104
  }
108
105
  //#endregion
109
- export { runRegisteredCommand as _, CommandExecutionContext as a, CommandOutputSpec as c, CommandRunResult as d, CommandStdin as f, renderCommandPrompt as g, parseCommandInput as h, CommandAsset as i, CommandRegistry as l, ParsedCommandInput as m, COMMAND_PROMPT_DEFAULTS as n, CommandIO as o, CommandStorage as p, Command as r, CommandInputSpec as s, AgentSessionCommandStorage as t, CommandRunContext as u };
106
+ export { runRegisteredCommand as _, CommandIO as a, CommandRegistry as c, CommandStdin as d, CommandStorage as f, renderCommandHelp as g, parseCommandInput as h, CommandExecutionContext as i, CommandRunContext as l, emptyStdin as m, COMMAND_HELP_DEFAULTS as n, CommandInputSpec as o, ParsedCommandInput as p, Command as r, CommandOutputSpec as s, AgentSessionCommandStorage as t, CommandRunResult as u };
@@ -1,2 +1,2 @@
1
- import { t as AgentSessionCommandStorage } from "./storage-D7EhVuLY.mjs";
1
+ import { t as AgentSessionCommandStorage } from "./storage-ylShLr2v.mjs";
2
2
  export { AgentSessionCommandStorage };
package/dist/storage.mjs CHANGED
@@ -5,7 +5,7 @@ var AgentSessionCommandStorage = class {
5
5
  constructor(store, agentSessionId) {
6
6
  this.store = store;
7
7
  validateAgentSessionId(agentSessionId);
8
- this.agentSessionPrefix = `${agentSessionId}/`;
8
+ this.agentSessionPrefix = `agent-sessions/${agentSessionId}/`;
9
9
  }
10
10
  readJson(key) {
11
11
  return this.store.readJson(this.key(key));
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.10.3",
4
+ "version": "0.12.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/utils": "^0.10.3",
23
- "@demicodes/just-bash": "^3.1.0-demi.2",
22
+ "@demicodes/just-bash": "^3.0.1-demi.5",
23
+ "@demicodes/utils": "^0.12.0",
24
24
  "zod": "^4.0.0"
25
25
  },
26
26
  "license": "Apache-2.0",