@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.
- package/dist/host-fs.d.mts +0 -1
- package/dist/host-fs.mjs +4 -4
- package/dist/index.d.mts +62 -31
- package/dist/index.mjs +239 -184
- package/dist/{storage-D7EhVuLY.d.mts → storage-ylShLr2v.d.mts} +13 -16
- package/dist/storage.d.mts +1 -1
- package/dist/storage.mjs +1 -1
- package/package.json +3 -3
package/dist/host-fs.d.mts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
183
|
-
stderr:
|
|
184
|
-
output:
|
|
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
|
-
|
|
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:
|
|
195
|
-
stderr:
|
|
196
|
-
output:
|
|
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:
|
|
204
|
-
stderr:
|
|
205
|
-
output:
|
|
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<
|
|
228
|
-
status(input: ShellStatusInput): Promise<
|
|
229
|
-
write(input: ShellWriteInput): Promise<
|
|
230
|
-
abort(input: ShellAbortInput): Promise<
|
|
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
|
|
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,
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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`:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
143
|
-
const rendered = this.list().map((command) =>
|
|
141
|
+
renderHelp() {
|
|
142
|
+
const rendered = this.list().map((command) => renderCommandHelp(command)).join("\n\n");
|
|
144
143
|
if (!rendered) return rendered;
|
|
145
|
-
return `${
|
|
144
|
+
return `${COMMAND_HELP_DEFAULTS}\n\n${rendered}`;
|
|
146
145
|
}
|
|
147
146
|
};
|
|
148
|
-
const
|
|
149
|
-
function parseCommandInput(root, argv, stdin =
|
|
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 === "
|
|
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 ??
|
|
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(`${
|
|
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
|
|
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(
|
|
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 (
|
|
309
|
-
}
|
|
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)
|
|
516
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
631
|
-
storageFor(
|
|
632
|
-
const existing = this.
|
|
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,
|
|
635
|
-
this.
|
|
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(
|
|
640
|
-
return this.released.has(this.key(
|
|
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(
|
|
644
|
-
if (this.isReleased(
|
|
645
|
-
this.storageFor(
|
|
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(
|
|
649
|
-
this.released.add(this.key(
|
|
650
|
-
await this.storageFor(
|
|
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(
|
|
653
|
-
return `${
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
722
|
-
this.
|
|
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
|
-
|
|
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)
|
|
755
|
-
if (code > 127) hasHighByte = true;
|
|
757
|
+
if (code > 255) hasWideChar = true;
|
|
758
|
+
else if (code > 127) hasHighByte = true;
|
|
756
759
|
}
|
|
757
|
-
if (
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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 =
|
|
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
|
-
|
|
803
|
-
if (
|
|
804
|
-
if (input.
|
|
805
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
922
|
-
const cwd = this.host.defaultCwd;
|
|
923
|
-
const fs = new HostBackedFileSystem(this.host, { lookup: (path) => this.lookupVirtualArtifact(
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
|
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 (
|
|
1411
|
-
|
|
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.
|
|
1469
|
+
return this.commandStatus(record, input);
|
|
1434
1470
|
}
|
|
1435
1471
|
async collectAborted(session, record, foreground, input = {}) {
|
|
1436
|
-
if (record.status !== "running") return this.
|
|
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.
|
|
1484
|
+
return this.commandStatus(record, input);
|
|
1449
1485
|
}
|
|
1450
1486
|
collectAbortedWithoutForeground(session, record, input = {}) {
|
|
1451
|
-
if (record.status !== "running") return this.
|
|
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.
|
|
1499
|
+
return this.commandStatus(record, input);
|
|
1464
1500
|
}
|
|
1465
|
-
|
|
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 =
|
|
1475
|
-
const stderr =
|
|
1476
|
-
const output =
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1513
|
-
return
|
|
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(
|
|
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(
|
|
1568
|
+
async commandArtifactIds(commandStorageId) {
|
|
1529
1569
|
const ids = /* @__PURE__ */ new Set();
|
|
1530
|
-
for (const record of this.commandsById.values()) if (record.
|
|
1531
|
-
const keys = await this.artifacts.storageFor(
|
|
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(
|
|
1574
|
+
if (match && !this.artifacts.isReleased(commandStorageId, match[1])) ids.add(match[1]);
|
|
1535
1575
|
}
|
|
1536
1576
|
return [...ids];
|
|
1537
1577
|
}
|
|
1538
|
-
async commandArtifact(
|
|
1539
|
-
if (this.artifacts.isReleased(
|
|
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?.
|
|
1581
|
+
if (record?.commandStorageId === commandStorageId) {
|
|
1542
1582
|
this.syncRunningRecord(record);
|
|
1543
|
-
return
|
|
1583
|
+
return commandArtifactFromRecord(record);
|
|
1544
1584
|
}
|
|
1545
|
-
const value = await this.artifacts.storageFor(
|
|
1546
|
-
return
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
renderHelp(): string;
|
|
91
88
|
}
|
|
92
|
-
declare const
|
|
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
|
|
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 _,
|
|
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 };
|
package/dist/storage.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as AgentSessionCommandStorage } from "./storage-
|
|
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 =
|
|
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.
|
|
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/
|
|
23
|
-
"@demicodes/
|
|
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",
|