@demicodes/shell 0.10.3 → 0.11.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 +235 -170
- package/dist/{storage-D7EhVuLY.d.mts → storage-DB9hjZ2G.d.mts} +14 -15
- 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-DB9hjZ2G.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",
|
|
@@ -125,6 +124,7 @@ const EXECUTION_ONLY_FIELDS = [
|
|
|
125
124
|
"output",
|
|
126
125
|
"examples"
|
|
127
126
|
];
|
|
127
|
+
const COMMAND_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
128
128
|
var CommandRegistry = class {
|
|
129
129
|
commands = /* @__PURE__ */ new Map();
|
|
130
130
|
register(command) {
|
|
@@ -139,14 +139,14 @@ var CommandRegistry = class {
|
|
|
139
139
|
list() {
|
|
140
140
|
return [...this.commands.values()];
|
|
141
141
|
}
|
|
142
|
-
|
|
143
|
-
const rendered = this.list().map((command) =>
|
|
142
|
+
renderHelp() {
|
|
143
|
+
const rendered = this.list().map((command) => renderCommandHelp(command)).join("\n\n");
|
|
144
144
|
if (!rendered) return rendered;
|
|
145
|
-
return `${
|
|
145
|
+
return `${COMMAND_HELP_DEFAULTS}\n\n${rendered}`;
|
|
146
146
|
}
|
|
147
147
|
};
|
|
148
|
-
const
|
|
149
|
-
function parseCommandInput(root, argv, stdin =
|
|
148
|
+
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.";
|
|
149
|
+
function parseCommandInput(root, argv, stdin = emptyStdin()) {
|
|
150
150
|
if (argv[0] !== root.name) throw new Error(`Expected command "${root.name}", received "${argv[0] ?? ""}"`);
|
|
151
151
|
let node = root;
|
|
152
152
|
const path = [root.name];
|
|
@@ -157,7 +157,7 @@ function parseCommandInput(root, argv, stdin = { text: "" }) {
|
|
|
157
157
|
throw new Error(`Command "${path.join(" ")}" requires a subcommand`);
|
|
158
158
|
}
|
|
159
159
|
const token = argv[index];
|
|
160
|
-
if (token === "
|
|
160
|
+
if (token === "--help") return {
|
|
161
161
|
path: [...path],
|
|
162
162
|
help: true,
|
|
163
163
|
values: {},
|
|
@@ -182,6 +182,12 @@ function parseArgs(command, path, argv, startIndex, stdin) {
|
|
|
182
182
|
let positionalIndex = 0;
|
|
183
183
|
for (let i = startIndex; i < argv.length; i += 1) {
|
|
184
184
|
const token = argv[i];
|
|
185
|
+
if (token === "--help") return {
|
|
186
|
+
path: [...path],
|
|
187
|
+
help: true,
|
|
188
|
+
values: {},
|
|
189
|
+
json: false
|
|
190
|
+
};
|
|
185
191
|
if (token === "--json") {
|
|
186
192
|
json = true;
|
|
187
193
|
continue;
|
|
@@ -223,13 +229,13 @@ function resolveCommand(root, path) {
|
|
|
223
229
|
return node;
|
|
224
230
|
}
|
|
225
231
|
async function runRegisteredCommand(root, ctx) {
|
|
226
|
-
const stdin = ctx.stdin ??
|
|
232
|
+
const stdin = ctx.stdin ?? emptyStdin();
|
|
227
233
|
const parsed = parseCommandInput(root, ctx.argv, stdin);
|
|
228
234
|
const displayPath = parsed.path.join(" ");
|
|
229
235
|
if (parsed.help) {
|
|
230
236
|
const node = resolveCommand(root, parsed.path);
|
|
231
237
|
const parentPath = parsed.path.length > 1 ? parsed.path.slice(0, -1).join(" ") : "";
|
|
232
|
-
await ctx.io.stdout(`${
|
|
238
|
+
await ctx.io.stdout(`${renderCommandHelp(node, parentPath)}\n`);
|
|
233
239
|
return { exitCode: 0 };
|
|
234
240
|
}
|
|
235
241
|
const command = resolveCommand(root, parsed.path);
|
|
@@ -243,7 +249,8 @@ async function runRegisteredCommand(root, ctx) {
|
|
|
243
249
|
env: ctx.env,
|
|
244
250
|
cwd: ctx.cwd,
|
|
245
251
|
io: parsed.json ? capture : ctx.io,
|
|
246
|
-
storage: ctx.storage
|
|
252
|
+
storage: ctx.storage,
|
|
253
|
+
host: ctx.host
|
|
247
254
|
});
|
|
248
255
|
if (parsed.json && result.exitCode === 0) {
|
|
249
256
|
const raw = capture.stdoutText();
|
|
@@ -262,7 +269,7 @@ async function runRegisteredCommand(root, ctx) {
|
|
|
262
269
|
}
|
|
263
270
|
return result;
|
|
264
271
|
}
|
|
265
|
-
function
|
|
272
|
+
function renderCommandHelp(command, parentPath = "") {
|
|
266
273
|
const path = parentPath ? `${parentPath} ${command.name}` : command.name;
|
|
267
274
|
const blocks = [];
|
|
268
275
|
const lines = [`${path}: ${command.summary}`];
|
|
@@ -297,10 +304,11 @@ function renderCommandPrompt(command, parentPath = "") {
|
|
|
297
304
|
for (const child of children) lines.push(` ${path} ${child.name} — ${child.summary}`);
|
|
298
305
|
}
|
|
299
306
|
blocks.push(lines.join("\n"));
|
|
300
|
-
for (const child of children) blocks.push(
|
|
307
|
+
for (const child of children) blocks.push(renderCommandHelp(child, path));
|
|
301
308
|
return blocks.join("\n\n");
|
|
302
309
|
}
|
|
303
310
|
function validateCommandTree(command, path) {
|
|
311
|
+
if (!COMMAND_NAME_PATTERN.test(command.name)) throw new Error(`CommandRegistry: "${path}" has invalid name "${command.name}"; use letters, numbers, underscores, and hyphens`);
|
|
304
312
|
const hasRun = typeof command.run === "function";
|
|
305
313
|
const children = command.subcommands ?? [];
|
|
306
314
|
if (!hasRun && children.length === 0) throw new Error(`CommandRegistry: "${path}" must have run() and/or subcommands`);
|
|
@@ -314,7 +322,6 @@ function validateCommandTree(command, path) {
|
|
|
314
322
|
}
|
|
315
323
|
const seen = /* @__PURE__ */ new Set();
|
|
316
324
|
for (const child of children) {
|
|
317
|
-
if (child.name === "prompt") throw new Error(`CommandRegistry: "${path} prompt" is reserved for the help pseudo-subcommand`);
|
|
318
325
|
if (seen.has(child.name)) throw new Error(`CommandRegistry: duplicate subcommand "${path} ${child.name}"`);
|
|
319
326
|
seen.add(child.name);
|
|
320
327
|
validateCommandTree(child, `${path} ${child.name}`);
|
|
@@ -398,9 +405,6 @@ var CapturingIO = class {
|
|
|
398
405
|
async stderr(data) {
|
|
399
406
|
await this.target.stderr(data);
|
|
400
407
|
}
|
|
401
|
-
async asset(asset) {
|
|
402
|
-
await this.target.asset(asset);
|
|
403
|
-
}
|
|
404
408
|
stdoutText() {
|
|
405
409
|
return decodeUtf8(concatBytes(this.chunks));
|
|
406
410
|
}
|
|
@@ -512,8 +516,10 @@ function createOutputSinks(fs, cwd, redirections) {
|
|
|
512
516
|
function recordForegroundChunk(foreground, sourceFd, chunk) {
|
|
513
517
|
const text = decodeUtf8(chunk);
|
|
514
518
|
foreground.lastOutputAt = Date.now();
|
|
515
|
-
if (sourceFd === 1)
|
|
516
|
-
|
|
519
|
+
if (sourceFd === 1) {
|
|
520
|
+
foreground.rawStdoutBuffer += text;
|
|
521
|
+
foreground.rawStdoutBytes.push(chunk);
|
|
522
|
+
} else foreground.rawStderrBuffer += text;
|
|
517
523
|
const sink = foreground.outputSinks[sourceFd];
|
|
518
524
|
if (sink.kind === "file" || sink.kind === "null") {
|
|
519
525
|
sink.bytes.push(chunk);
|
|
@@ -615,62 +621,63 @@ function appendVisibleChunk(foreground, targetFd, text, byteLength) {
|
|
|
615
621
|
//#endregion
|
|
616
622
|
//#region src/command-artifact-store.ts
|
|
617
623
|
/**
|
|
618
|
-
* Owns persistence of shell command artifacts: a per-
|
|
624
|
+
* Owns persistence of shell command artifacts: a per-storage-id cache plus the
|
|
619
625
|
* set of released (tombstoned) commands. `BashEnvironment` delegates the storage
|
|
620
626
|
* and release-tracking side of the `/@` virtual filesystem here, keeping only the
|
|
621
627
|
* in-memory record lookups that need live command state.
|
|
622
628
|
*/
|
|
623
629
|
var CommandArtifactStore = class {
|
|
624
630
|
store;
|
|
625
|
-
|
|
631
|
+
storageById = /* @__PURE__ */ new Map();
|
|
626
632
|
released = /* @__PURE__ */ new Set();
|
|
627
633
|
constructor(store) {
|
|
628
634
|
this.store = store;
|
|
629
635
|
}
|
|
630
|
-
/** The artifact storage for
|
|
631
|
-
storageFor(
|
|
632
|
-
const existing = this.
|
|
636
|
+
/** The artifact storage for one agent session or anonymous shell. */
|
|
637
|
+
storageFor(commandStorageId) {
|
|
638
|
+
const existing = this.storageById.get(commandStorageId);
|
|
633
639
|
if (existing) return existing;
|
|
634
|
-
const storage = new AgentSessionCommandStorage(this.store,
|
|
635
|
-
this.
|
|
640
|
+
const storage = new AgentSessionCommandStorage(this.store, commandStorageId);
|
|
641
|
+
this.storageById.set(commandStorageId, storage);
|
|
636
642
|
return storage;
|
|
637
643
|
}
|
|
638
644
|
/** Whether a command's artifact has been released (tombstoned). */
|
|
639
|
-
isReleased(
|
|
640
|
-
return this.released.has(this.key(
|
|
645
|
+
isReleased(commandStorageId, commandId) {
|
|
646
|
+
return this.released.has(this.key(commandStorageId, commandId));
|
|
641
647
|
}
|
|
642
648
|
/** Persists an artifact unless its command has already been released. */
|
|
643
|
-
persist(
|
|
644
|
-
if (this.isReleased(
|
|
645
|
-
this.storageFor(
|
|
649
|
+
persist(commandStorageId, commandId, artifact) {
|
|
650
|
+
if (this.isReleased(commandStorageId, commandId)) return;
|
|
651
|
+
this.storageFor(commandStorageId).writeJson(`commands/${commandId}/artifact.json`, artifact).catch(() => {});
|
|
646
652
|
}
|
|
647
653
|
/** Tombstones a command and removes its persisted artifact. */
|
|
648
|
-
async release(
|
|
649
|
-
this.released.add(this.key(
|
|
650
|
-
await this.storageFor(
|
|
654
|
+
async release(commandStorageId, commandId) {
|
|
655
|
+
this.released.add(this.key(commandStorageId, commandId));
|
|
656
|
+
await this.storageFor(commandStorageId).delete(`commands/${commandId}/artifact.json`).catch(() => {});
|
|
651
657
|
}
|
|
652
|
-
key(
|
|
653
|
-
return `${
|
|
658
|
+
key(commandStorageId, commandId) {
|
|
659
|
+
return `${commandStorageId}\0${commandId}`;
|
|
654
660
|
}
|
|
655
661
|
};
|
|
656
662
|
//#endregion
|
|
657
663
|
//#region src/registered-command-adapter.ts
|
|
658
|
-
function commandToForkCommand(session, command, storage) {
|
|
664
|
+
function commandToForkCommand(session, command, storage, host) {
|
|
659
665
|
return {
|
|
660
666
|
name: command.name,
|
|
661
667
|
consumesStdin: treeConsumesStdin(command),
|
|
662
668
|
execute: async (args, ctx) => {
|
|
663
|
-
const
|
|
669
|
+
const stdin = decodeForkStdin(ctx.stdin);
|
|
664
670
|
const io = createForwardingIO();
|
|
665
671
|
const argv = [command.name, ...args];
|
|
666
672
|
try {
|
|
667
673
|
const result = await runRegisteredCommand(command, {
|
|
668
674
|
argv,
|
|
669
|
-
stdin
|
|
675
|
+
stdin,
|
|
670
676
|
env: mapToRecord(ctx.env),
|
|
671
677
|
cwd: ctx.cwd,
|
|
672
678
|
io,
|
|
673
|
-
storage
|
|
679
|
+
storage,
|
|
680
|
+
host
|
|
674
681
|
});
|
|
675
682
|
session.accumulator.audit.push({
|
|
676
683
|
kind: "registered-command",
|
|
@@ -685,7 +692,8 @@ function commandToForkCommand(session, command, storage) {
|
|
|
685
692
|
metadata: result.metadata
|
|
686
693
|
});
|
|
687
694
|
return {
|
|
688
|
-
stdout: io.
|
|
695
|
+
stdout: io.stdoutLatin1(),
|
|
696
|
+
stdoutKind: "bytes",
|
|
689
697
|
stderr: io.stderrText(),
|
|
690
698
|
exitCode: result.exitCode
|
|
691
699
|
};
|
|
@@ -698,38 +706,31 @@ function commandToForkCommand(session, command, storage) {
|
|
|
698
706
|
exitCode: 1
|
|
699
707
|
});
|
|
700
708
|
return {
|
|
701
|
-
stdout: io.
|
|
709
|
+
stdout: io.stdoutLatin1(),
|
|
710
|
+
stdoutKind: "bytes",
|
|
702
711
|
stderr: `${io.stderrText()}${command.name}: ${message}\n`,
|
|
703
712
|
exitCode: 1
|
|
704
713
|
};
|
|
705
|
-
} finally {
|
|
706
|
-
if (io.assets().length > 0) session.accumulator.assets.push(...io.assets());
|
|
707
714
|
}
|
|
708
715
|
}
|
|
709
716
|
};
|
|
710
717
|
}
|
|
718
|
+
/** Collects command output as raw bytes; stdout stays byte-clean for the pipe. */
|
|
711
719
|
var ForwardingIO = class {
|
|
712
720
|
stdoutChunks = [];
|
|
713
721
|
stderrChunks = [];
|
|
714
|
-
assetItems = [];
|
|
715
722
|
async stdout(data) {
|
|
716
723
|
this.stdoutChunks.push(typeof data === "string" ? encodeUtf8(data) : data);
|
|
717
724
|
}
|
|
718
725
|
async stderr(data) {
|
|
719
726
|
this.stderrChunks.push(typeof data === "string" ? encodeUtf8(data) : data);
|
|
720
727
|
}
|
|
721
|
-
|
|
722
|
-
this.
|
|
723
|
-
}
|
|
724
|
-
stdoutText() {
|
|
725
|
-
return decodeUtf8(concatBytes(this.stdoutChunks));
|
|
728
|
+
stdoutLatin1() {
|
|
729
|
+
return decodeLatin1(concatBytes(this.stdoutChunks));
|
|
726
730
|
}
|
|
727
731
|
stderrText() {
|
|
728
732
|
return decodeUtf8(concatBytes(this.stderrChunks));
|
|
729
733
|
}
|
|
730
|
-
assets() {
|
|
731
|
-
return this.assetItems;
|
|
732
|
-
}
|
|
733
734
|
};
|
|
734
735
|
function createForwardingIO() {
|
|
735
736
|
return new ForwardingIO();
|
|
@@ -743,26 +744,52 @@ function mapToRecord(map) {
|
|
|
743
744
|
for (const [key, value] of map) record[key] = value;
|
|
744
745
|
return record;
|
|
745
746
|
}
|
|
747
|
+
/** Pipes hand stdin over as a latin1-packed byte string; expose bytes and a UTF-8 text view. */
|
|
746
748
|
function decodeForkStdin(stdin) {
|
|
747
|
-
if (!stdin) return
|
|
748
|
-
|
|
749
|
+
if (!stdin) return {
|
|
750
|
+
text: "",
|
|
751
|
+
bytes: /* @__PURE__ */ new Uint8Array(0)
|
|
752
|
+
};
|
|
753
|
+
if (stdin instanceof Uint8Array) return {
|
|
754
|
+
text: decodeUtf8(stdin),
|
|
755
|
+
bytes: stdin
|
|
756
|
+
};
|
|
749
757
|
const latin1 = stdin;
|
|
750
|
-
if (!latin1) return
|
|
758
|
+
if (!latin1) return {
|
|
759
|
+
text: "",
|
|
760
|
+
bytes: /* @__PURE__ */ new Uint8Array(0)
|
|
761
|
+
};
|
|
762
|
+
const bytes = encodeLatin1(latin1);
|
|
751
763
|
let hasHighByte = false;
|
|
764
|
+
let hasWideChar = false;
|
|
752
765
|
for (let i = 0; i < latin1.length; i += 1) {
|
|
753
766
|
const code = latin1.charCodeAt(i);
|
|
754
|
-
if (code > 255)
|
|
755
|
-
if (code > 127) hasHighByte = true;
|
|
767
|
+
if (code > 255) hasWideChar = true;
|
|
768
|
+
else if (code > 127) hasHighByte = true;
|
|
756
769
|
}
|
|
757
|
-
if (
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
770
|
+
if (hasWideChar) return {
|
|
771
|
+
text: latin1,
|
|
772
|
+
bytes: encodeUtf8(latin1)
|
|
773
|
+
};
|
|
774
|
+
if (!hasHighByte) return {
|
|
775
|
+
text: latin1,
|
|
776
|
+
bytes
|
|
777
|
+
};
|
|
778
|
+
return {
|
|
779
|
+
text: decodeUtf8(bytes),
|
|
780
|
+
bytes
|
|
781
|
+
};
|
|
761
782
|
}
|
|
762
783
|
//#endregion
|
|
763
784
|
//#region src/environment.ts
|
|
764
785
|
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
765
|
-
const DEFAULT_OUTPUT_LIMIT_BYTES =
|
|
786
|
+
const DEFAULT_OUTPUT_LIMIT_BYTES = 1048576;
|
|
787
|
+
/**
|
|
788
|
+
* Ceiling for a raw-byte final stream. Sized so an ordinary viewing-grade clip
|
|
789
|
+
* survives the shell and reaches the layer that decides what to do with it,
|
|
790
|
+
* while still bounding a runaway producer.
|
|
791
|
+
*/
|
|
792
|
+
const DEFAULT_BINARY_LIMIT_BYTES = 16777216;
|
|
766
793
|
/** Upper bound for a single exec observation window (also the command-bridge wait ceiling). */
|
|
767
794
|
const MAX_TIMEOUT_MS = 6e5;
|
|
768
795
|
var BashEnvironment = class {
|
|
@@ -771,8 +798,8 @@ var BashEnvironment = class {
|
|
|
771
798
|
shellIdFactory;
|
|
772
799
|
commandIdFactory;
|
|
773
800
|
initialEnv;
|
|
774
|
-
execEnv;
|
|
775
801
|
defaultOutputLimitBytes;
|
|
802
|
+
defaultBinaryLimitBytes;
|
|
776
803
|
shells = /* @__PURE__ */ new Map();
|
|
777
804
|
defaultShellByAgentSessionId = /* @__PURE__ */ new Map();
|
|
778
805
|
commandsById = /* @__PURE__ */ new Map();
|
|
@@ -784,12 +811,15 @@ var BashEnvironment = class {
|
|
|
784
811
|
this.shellIdFactory = options.shellIdFactory ?? (() => globalThis.crypto.randomUUID());
|
|
785
812
|
this.commandIdFactory = options.commandIdFactory ?? (() => globalThis.crypto.randomUUID());
|
|
786
813
|
this.initialEnv = options.initialEnv ?? {};
|
|
787
|
-
this.execEnv = options.execEnv;
|
|
788
814
|
this.defaultOutputLimitBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES;
|
|
815
|
+
this.defaultBinaryLimitBytes = options.maxBinaryBytes ?? DEFAULT_BINARY_LIMIT_BYTES;
|
|
789
816
|
}
|
|
790
817
|
getShell(shellId) {
|
|
791
818
|
return this.shells.get(shellId) ?? null;
|
|
792
819
|
}
|
|
820
|
+
hasCommand(commandId) {
|
|
821
|
+
return this.commandsById.has(commandId);
|
|
822
|
+
}
|
|
793
823
|
registerCommand(command) {
|
|
794
824
|
if (this.commands.get(command.name)) return;
|
|
795
825
|
this.commands.register(command);
|
|
@@ -799,20 +829,13 @@ var BashEnvironment = class {
|
|
|
799
829
|
}
|
|
800
830
|
async exec(input) {
|
|
801
831
|
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
|
-
}
|
|
832
|
+
if (input.shellId && input.ephemeral) throw new Error("ShellExecInput: \"shellId\" and \"ephemeral\" are mutually exclusive");
|
|
833
|
+
if (input.cwd !== void 0 && !input.ephemeral) throw new Error("ShellExecInput: \"cwd\" requires \"ephemeral\"; a persistent shell owns its cwd");
|
|
834
|
+
if (input.cwd !== void 0) {
|
|
835
|
+
if (!(await this.host.fs.stat(input.cwd).catch(() => null))?.isDirectory) throw new Error(`Shell exec cwd is not a directory: ${input.cwd}`);
|
|
815
836
|
}
|
|
837
|
+
const session = input.shellId ? this.requireShell(input.shellId) : input.ephemeral ? this.createShell(input.agentSessionId, input.cwd) : this.availableDefaultShell(input.agentSessionId);
|
|
838
|
+
if (session.exited) throw new Error(`Shell session "${session.id}" has exited`);
|
|
816
839
|
if (session.pendingExec || session.foreground) {
|
|
817
840
|
const commandId = session.activeCommandId ?? session.foreground?.commandId ?? "unknown";
|
|
818
841
|
throw new Error(`Shell session "${session.id}" is already running command "${commandId}"`);
|
|
@@ -824,7 +847,7 @@ var BashEnvironment = class {
|
|
|
824
847
|
}
|
|
825
848
|
async status(input) {
|
|
826
849
|
const record = this.requireCommand(input.commandId);
|
|
827
|
-
return this.
|
|
850
|
+
return this.commandStatus(record, input);
|
|
828
851
|
}
|
|
829
852
|
async write(input) {
|
|
830
853
|
const record = this.requireCommand(input.commandId);
|
|
@@ -834,11 +857,11 @@ var BashEnvironment = class {
|
|
|
834
857
|
const data = typeof input.stdin === "string" ? encodeUtf8(input.stdin) : input.stdin;
|
|
835
858
|
if (data.byteLength === 0) throw new Error("shell_write field \"stdin\" must not be empty; use shell_status to poll");
|
|
836
859
|
await foreground.handle.writeStdin(data);
|
|
837
|
-
return this.
|
|
860
|
+
return this.commandStatus(record, input);
|
|
838
861
|
}
|
|
839
862
|
async abort(input) {
|
|
840
863
|
const record = this.requireCommand(input.commandId);
|
|
841
|
-
if (record.status !== "running") return this.
|
|
864
|
+
if (record.status !== "running") return this.commandStatus(record, input);
|
|
842
865
|
const session = this.requireShell(record.shellId);
|
|
843
866
|
const foreground = this.requireForegroundCommand(session, record.id);
|
|
844
867
|
foreground.abortController.abort();
|
|
@@ -850,7 +873,7 @@ var BashEnvironment = class {
|
|
|
850
873
|
const record = this.commandsById.get(commandId);
|
|
851
874
|
if (!record || record.status === "running") return false;
|
|
852
875
|
this.commandsById.delete(commandId);
|
|
853
|
-
await this.artifacts.release(record.
|
|
876
|
+
await this.artifacts.release(record.commandStorageId, commandId);
|
|
854
877
|
return true;
|
|
855
878
|
}
|
|
856
879
|
async disposeShell(shellId) {
|
|
@@ -916,25 +939,22 @@ var BashEnvironment = class {
|
|
|
916
939
|
if (!agentSessionId || shell.exited || !shell.pendingExec && !shell.foreground) return shell;
|
|
917
940
|
return this.createShell(agentSessionId);
|
|
918
941
|
}
|
|
919
|
-
createShell(agentSessionId) {
|
|
942
|
+
createShell(agentSessionId, initialCwd) {
|
|
920
943
|
const id = this.shellIdFactory();
|
|
921
|
-
const
|
|
922
|
-
const cwd = this.host.defaultCwd;
|
|
923
|
-
const fs = new HostBackedFileSystem(this.host, { lookup: (path) => this.lookupVirtualArtifact(
|
|
944
|
+
const commandStorageId = agentSessionId ?? id;
|
|
945
|
+
const cwd = initialCwd ?? this.host.defaultCwd;
|
|
946
|
+
const fs = new HostBackedFileSystem(this.host, { lookup: (path) => this.lookupVirtualArtifact(commandStorageId, path) });
|
|
924
947
|
const env = /* @__PURE__ */ new Map();
|
|
925
948
|
for (const [key, value] of Object.entries(this.initialEnv)) env.set(key, value);
|
|
926
949
|
env.set("PWD", cwd);
|
|
927
|
-
env.set("DEMI_SESSION_ID",
|
|
950
|
+
if (agentSessionId) env.set("DEMI_SESSION_ID", agentSessionId);
|
|
928
951
|
env.set("DEMI_SHELL_ID", id);
|
|
929
952
|
if (!env.has("IFS")) env.set("IFS", " \n");
|
|
930
953
|
if (!env.has("PS1")) env.set("PS1", "");
|
|
931
954
|
if (!env.has("PS2")) env.set("PS2", "> ");
|
|
932
955
|
if (!env.has("SHLVL")) env.set("SHLVL", "1");
|
|
933
|
-
const exportedVars = /* @__PURE__ */ new Set([
|
|
934
|
-
|
|
935
|
-
"DEMI_SESSION_ID",
|
|
936
|
-
"DEMI_SHELL_ID"
|
|
937
|
-
]);
|
|
956
|
+
const exportedVars = /* @__PURE__ */ new Set(["PWD", "DEMI_SHELL_ID"]);
|
|
957
|
+
if (agentSessionId) exportedVars.add("DEMI_SESSION_ID");
|
|
938
958
|
for (const key of env.keys()) if (key !== key.toLowerCase()) exportedVars.add(key);
|
|
939
959
|
for (const key of Object.keys(this.initialEnv)) exportedVars.add(key);
|
|
940
960
|
const state = {
|
|
@@ -995,7 +1015,8 @@ var BashEnvironment = class {
|
|
|
995
1015
|
const forkCommands = /* @__PURE__ */ new Map();
|
|
996
1016
|
const session = {
|
|
997
1017
|
id,
|
|
998
|
-
|
|
1018
|
+
agentSessionId: agentSessionId ?? null,
|
|
1019
|
+
commandStorageId,
|
|
999
1020
|
state,
|
|
1000
1021
|
fs,
|
|
1001
1022
|
interpreter: void 0,
|
|
@@ -1004,8 +1025,7 @@ var BashEnvironment = class {
|
|
|
1004
1025
|
stdout: "",
|
|
1005
1026
|
stderr: "",
|
|
1006
1027
|
audit: [],
|
|
1007
|
-
commandMetadata: []
|
|
1008
|
-
assets: []
|
|
1028
|
+
commandMetadata: []
|
|
1009
1029
|
},
|
|
1010
1030
|
foregroundWaiters: /* @__PURE__ */ new Set(),
|
|
1011
1031
|
backgroundJobs: /* @__PURE__ */ new Map(),
|
|
@@ -1013,19 +1033,20 @@ var BashEnvironment = class {
|
|
|
1013
1033
|
exited: false
|
|
1014
1034
|
};
|
|
1015
1035
|
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));
|
|
1036
|
+
const storage = new AgentSessionCommandStorage(this.host.store, commandStorageId);
|
|
1037
|
+
for (const command of this.commands.list()) forkCommands.set(command.name, commandToForkCommand(session, command, storage, this.host));
|
|
1018
1038
|
session.abortController = new AbortController();
|
|
1039
|
+
const limits = resolveLimits({
|
|
1040
|
+
maxOutputSize: 1073741824,
|
|
1041
|
+
maxCommandCount: 1e6,
|
|
1042
|
+
maxLoopIterations: 1e6,
|
|
1043
|
+
maxCallDepth: 1e3,
|
|
1044
|
+
maxGlobOperations: 1e6
|
|
1045
|
+
});
|
|
1019
1046
|
session.interpreter = new Interpreter({
|
|
1020
1047
|
fs,
|
|
1021
1048
|
commands: forkCommands,
|
|
1022
|
-
limits
|
|
1023
|
-
maxOutputSize: 1024 * 1024 * 1024,
|
|
1024
|
-
maxCommandCount: 1e6,
|
|
1025
|
-
maxLoopIterations: 1e6,
|
|
1026
|
-
maxCallDepth: 1e3,
|
|
1027
|
-
maxGlobOperations: 1e6
|
|
1028
|
-
}),
|
|
1049
|
+
limits,
|
|
1029
1050
|
exec: async () => ({
|
|
1030
1051
|
stdout: "",
|
|
1031
1052
|
stderr: "",
|
|
@@ -1044,6 +1065,7 @@ var BashEnvironment = class {
|
|
|
1044
1065
|
}
|
|
1045
1066
|
async runScript(session, script, input) {
|
|
1046
1067
|
const record = this.createCommandRecord(session, script);
|
|
1068
|
+
record.outputLimitBytes = input.maxOutputBytes ?? this.defaultOutputLimitBytes;
|
|
1047
1069
|
let ast;
|
|
1048
1070
|
try {
|
|
1049
1071
|
ast = parse(script);
|
|
@@ -1055,7 +1077,7 @@ var BashEnvironment = class {
|
|
|
1055
1077
|
record.exitCode = 2;
|
|
1056
1078
|
session.state.lastExitCode = 2;
|
|
1057
1079
|
session.activeCommandId = void 0;
|
|
1058
|
-
return this.
|
|
1080
|
+
return this.commandStatus(record, input);
|
|
1059
1081
|
}
|
|
1060
1082
|
throw error;
|
|
1061
1083
|
}
|
|
@@ -1063,8 +1085,7 @@ var BashEnvironment = class {
|
|
|
1063
1085
|
stdout: "",
|
|
1064
1086
|
stderr: "",
|
|
1065
1087
|
audit: [],
|
|
1066
|
-
commandMetadata: []
|
|
1067
|
-
assets: []
|
|
1088
|
+
commandMetadata: []
|
|
1068
1089
|
};
|
|
1069
1090
|
session.abortController = new AbortController();
|
|
1070
1091
|
session.activeCommandId = record.id;
|
|
@@ -1087,7 +1108,7 @@ var BashEnvironment = class {
|
|
|
1087
1108
|
const record = {
|
|
1088
1109
|
id,
|
|
1089
1110
|
shellId: session.id,
|
|
1090
|
-
|
|
1111
|
+
commandStorageId: session.commandStorageId,
|
|
1091
1112
|
script,
|
|
1092
1113
|
startedAt: now,
|
|
1093
1114
|
lastOutputAt: now,
|
|
@@ -1100,7 +1121,7 @@ var BashEnvironment = class {
|
|
|
1100
1121
|
outputOffset: 0,
|
|
1101
1122
|
audit: [],
|
|
1102
1123
|
commandMetadata: [],
|
|
1103
|
-
|
|
1124
|
+
outputLimitBytes: this.defaultOutputLimitBytes
|
|
1104
1125
|
};
|
|
1105
1126
|
this.commandsById.set(id, record);
|
|
1106
1127
|
return record;
|
|
@@ -1221,7 +1242,7 @@ var BashEnvironment = class {
|
|
|
1221
1242
|
foreground = outcome.foreground;
|
|
1222
1243
|
continue;
|
|
1223
1244
|
}
|
|
1224
|
-
if (outcome.kind === "timeout") return this.
|
|
1245
|
+
if (outcome.kind === "timeout") return this.commandStatus(record, input);
|
|
1225
1246
|
if (outcome.kind === "aborted") {
|
|
1226
1247
|
const activeForeground = foreground ?? session.foreground;
|
|
1227
1248
|
if (!activeForeground) return this.collectAbortedWithoutForeground(session, record, input);
|
|
@@ -1306,6 +1327,7 @@ var BashEnvironment = class {
|
|
|
1306
1327
|
startedAt,
|
|
1307
1328
|
lastOutputAt: startedAt,
|
|
1308
1329
|
rawStdoutBuffer: "",
|
|
1330
|
+
rawStdoutBytes: [],
|
|
1309
1331
|
rawStderrBuffer: "",
|
|
1310
1332
|
stdoutBuffer: "",
|
|
1311
1333
|
stderrBuffer: "",
|
|
@@ -1326,7 +1348,7 @@ var BashEnvironment = class {
|
|
|
1326
1348
|
};
|
|
1327
1349
|
session.foreground = foreground;
|
|
1328
1350
|
notifyForegroundWaiters(session.foregroundWaiters, foreground);
|
|
1329
|
-
if (opts.stdin && opts.stdin.length > 0) await handle.writeStdin(
|
|
1351
|
+
if (opts.stdin && opts.stdin.length > 0) await handle.writeStdin(encodeLatin1(opts.stdin));
|
|
1330
1352
|
if (opts.stdinProvided) await handle.closeStdin();
|
|
1331
1353
|
if (handle.output) {
|
|
1332
1354
|
foreground.stdoutPump = pumpOutputStream(handle.output, (chunk) => {
|
|
@@ -1339,7 +1361,7 @@ var BashEnvironment = class {
|
|
|
1339
1361
|
}
|
|
1340
1362
|
const exit = await foreground.exitPromise;
|
|
1341
1363
|
await Promise.allSettled([foreground.stdoutPump, foreground.stderrPump]);
|
|
1342
|
-
const stdout = foreground.
|
|
1364
|
+
const stdout = decodeLatin1(concatBytes(foreground.rawStdoutBytes));
|
|
1343
1365
|
const exitCode = exit.exitCode ?? 127;
|
|
1344
1366
|
const stderr = exit.exitCode === null && foreground.rawStderrBuffer.length === 0 ? `${command}: ${exit.signal ?? "command not found"}\n` : foreground.rawStderrBuffer;
|
|
1345
1367
|
foreground.audit[0] = {
|
|
@@ -1360,12 +1382,13 @@ var BashEnvironment = class {
|
|
|
1360
1382
|
session.foreground = void 0;
|
|
1361
1383
|
return {
|
|
1362
1384
|
stdout,
|
|
1385
|
+
stdoutKind: "bytes",
|
|
1363
1386
|
stderr,
|
|
1364
1387
|
exitCode
|
|
1365
1388
|
};
|
|
1366
1389
|
}
|
|
1367
1390
|
collectExited(session, record, resultOrError, foreground, input = {}) {
|
|
1368
|
-
if (record.status !== "running") return this.
|
|
1391
|
+
if (record.status !== "running") return this.commandStatus(record, input);
|
|
1369
1392
|
if (resultOrError instanceof Error) {
|
|
1370
1393
|
if (resultOrError instanceof ExitError) {
|
|
1371
1394
|
session.exited = true;
|
|
@@ -1403,12 +1426,36 @@ var BashEnvironment = class {
|
|
|
1403
1426
|
appendRecordOutput(record, "stderr", text);
|
|
1404
1427
|
return this.finishExited(session, record, 1, input);
|
|
1405
1428
|
}
|
|
1406
|
-
const
|
|
1429
|
+
const raw = resultOrError.stdout;
|
|
1430
|
+
let stdoutText;
|
|
1431
|
+
let binary;
|
|
1432
|
+
if (hasWideChar(raw)) stdoutText = raw;
|
|
1433
|
+
else {
|
|
1434
|
+
const bytes = encodeLatin1(raw);
|
|
1435
|
+
const strict = decodeUtf8Strict(bytes);
|
|
1436
|
+
if (strict !== null) stdoutText = strict;
|
|
1437
|
+
else {
|
|
1438
|
+
const cap = this.defaultBinaryLimitBytes;
|
|
1439
|
+
const truncated = bytes.length > cap;
|
|
1440
|
+
binary = {
|
|
1441
|
+
data: truncated ? bytes.slice(0, cap) : bytes,
|
|
1442
|
+
truncated,
|
|
1443
|
+
totalBytes: bytes.length,
|
|
1444
|
+
limitBytes: cap
|
|
1445
|
+
};
|
|
1446
|
+
stdoutText = `<binary stdout: ${bytes.length} bytes${truncated ? `, exceeds the ${cap}-byte binary limit` : ""}; raw bytes at /@/commands/${record.id}/stdout.bin>\n`;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1407
1449
|
const stderrText = foreground ? resultOrError.stderr : decodeBytesToUtf8(unsafeBytesFromLatin1(resultOrError.stderr));
|
|
1408
1450
|
session.accumulator.stdout += stdoutText;
|
|
1409
1451
|
session.accumulator.stderr += stderrText;
|
|
1410
|
-
if (
|
|
1411
|
-
|
|
1452
|
+
if (binary) record.binaryStdout = binary;
|
|
1453
|
+
if (foreground && !binary) record.outputChunks = [...foreground.outputChunks];
|
|
1454
|
+
else if (binary) {
|
|
1455
|
+
record.outputChunks = [];
|
|
1456
|
+
appendRecordOutput(record, "stdout", stdoutText);
|
|
1457
|
+
appendRecordOutput(record, "stderr", stderrText);
|
|
1458
|
+
} else if (record.outputChunks.length === 0) {
|
|
1412
1459
|
appendRecordOutput(record, "stdout", stdoutText);
|
|
1413
1460
|
appendRecordOutput(record, "stderr", stderrText);
|
|
1414
1461
|
}
|
|
@@ -1427,13 +1474,12 @@ var BashEnvironment = class {
|
|
|
1427
1474
|
record.exitCode = exitCode;
|
|
1428
1475
|
record.audit = [...session.accumulator.audit];
|
|
1429
1476
|
record.commandMetadata = [...session.accumulator.commandMetadata];
|
|
1430
|
-
record.assets = [...session.accumulator.assets];
|
|
1431
1477
|
session.pendingExec = void 0;
|
|
1432
1478
|
if (session.activeCommandId === record.id) session.activeCommandId = void 0;
|
|
1433
|
-
return this.
|
|
1479
|
+
return this.commandStatus(record, input);
|
|
1434
1480
|
}
|
|
1435
1481
|
async collectAborted(session, record, foreground, input = {}) {
|
|
1436
|
-
if (record.status !== "running") return this.
|
|
1482
|
+
if (record.status !== "running") return this.commandStatus(record, input);
|
|
1437
1483
|
foreground.abortController.abort();
|
|
1438
1484
|
foreground.handle.kill("SIGTERM").catch(() => {});
|
|
1439
1485
|
await flushForegroundSinks(session, foreground);
|
|
@@ -1445,10 +1491,10 @@ var BashEnvironment = class {
|
|
|
1445
1491
|
session.foreground = void 0;
|
|
1446
1492
|
session.pendingExec = void 0;
|
|
1447
1493
|
if (session.activeCommandId === record.id) session.activeCommandId = void 0;
|
|
1448
|
-
return this.
|
|
1494
|
+
return this.commandStatus(record, input);
|
|
1449
1495
|
}
|
|
1450
1496
|
collectAbortedWithoutForeground(session, record, input = {}) {
|
|
1451
|
-
if (record.status !== "running") return this.
|
|
1497
|
+
if (record.status !== "running") return this.commandStatus(record, input);
|
|
1452
1498
|
session.abortController?.abort();
|
|
1453
1499
|
session.pendingExec = void 0;
|
|
1454
1500
|
if (session.activeCommandId === record.id) session.activeCommandId = void 0;
|
|
@@ -1460,9 +1506,9 @@ var BashEnvironment = class {
|
|
|
1460
1506
|
}
|
|
1461
1507
|
record.lastOutputAt = Date.now();
|
|
1462
1508
|
record.status = "aborted";
|
|
1463
|
-
return this.
|
|
1509
|
+
return this.commandStatus(record, input);
|
|
1464
1510
|
}
|
|
1465
|
-
|
|
1511
|
+
commandStatus(record, input = {}) {
|
|
1466
1512
|
const foreground = this.shells.get(record.shellId)?.foreground;
|
|
1467
1513
|
if (record.status === "running" && foreground?.commandId === record.id) {
|
|
1468
1514
|
record.stdout = foreground.stdoutBuffer;
|
|
@@ -1471,9 +1517,9 @@ var BashEnvironment = class {
|
|
|
1471
1517
|
record.lastOutputAt = foreground.lastOutputAt;
|
|
1472
1518
|
}
|
|
1473
1519
|
const maxOutputBytes = input.maxOutputBytes ?? this.defaultOutputLimitBytes;
|
|
1474
|
-
const stdout =
|
|
1475
|
-
const stderr =
|
|
1476
|
-
const output =
|
|
1520
|
+
const stdout = streamView(record, "stdout", input.stdoutOffset, maxOutputBytes);
|
|
1521
|
+
const stderr = streamView(record, "stderr", input.stderrOffset, maxOutputBytes);
|
|
1522
|
+
const output = mergedOutputView(record, input.outputOffset, maxOutputBytes);
|
|
1477
1523
|
const base = {
|
|
1478
1524
|
shellId: record.shellId,
|
|
1479
1525
|
commandId: record.id,
|
|
@@ -1492,7 +1538,7 @@ var BashEnvironment = class {
|
|
|
1492
1538
|
audit: record.audit
|
|
1493
1539
|
};
|
|
1494
1540
|
if (record.commandMetadata.length > 0) result.commandMetadata = record.commandMetadata;
|
|
1495
|
-
if (record.
|
|
1541
|
+
if (record.binaryStdout) result.binaryStdout = record.binaryStdout;
|
|
1496
1542
|
return result;
|
|
1497
1543
|
}
|
|
1498
1544
|
if (record.status === "aborted") return {
|
|
@@ -1504,52 +1550,56 @@ var BashEnvironment = class {
|
|
|
1504
1550
|
status: "running"
|
|
1505
1551
|
};
|
|
1506
1552
|
}
|
|
1507
|
-
async lookupVirtualArtifact(
|
|
1553
|
+
async lookupVirtualArtifact(commandStorageId, path) {
|
|
1508
1554
|
const parts = path.split("/").filter(Boolean);
|
|
1509
1555
|
if (parts.length === 1 && parts[0] === "@") return virtualDirectory(["commands"]);
|
|
1510
|
-
if (parts.length === 2 && parts[0] === "@" && parts[1] === "commands") return virtualDirectory(await this.commandArtifactIds(
|
|
1556
|
+
if (parts.length === 2 && parts[0] === "@" && parts[1] === "commands") return virtualDirectory(await this.commandArtifactIds(commandStorageId));
|
|
1511
1557
|
if (parts.length === 3 && parts[0] === "@" && parts[1] === "commands") {
|
|
1512
|
-
|
|
1513
|
-
return
|
|
1558
|
+
const artifact = await this.commandArtifact(commandStorageId, parts[2]);
|
|
1559
|
+
if (!artifact) return null;
|
|
1560
|
+
const entries = [
|
|
1514
1561
|
"meta.json",
|
|
1515
1562
|
"stderr.txt",
|
|
1516
1563
|
"stdout.txt"
|
|
1517
|
-
]
|
|
1564
|
+
];
|
|
1565
|
+
if (artifact.stdoutBinary) entries.push("stdout.bin");
|
|
1566
|
+
return virtualDirectory(entries);
|
|
1518
1567
|
}
|
|
1519
1568
|
if (parts.length !== 4 || parts[0] !== "@" || parts[1] !== "commands") return null;
|
|
1520
|
-
const artifact = await this.commandArtifact(
|
|
1569
|
+
const artifact = await this.commandArtifact(commandStorageId, parts[2]);
|
|
1521
1570
|
if (!artifact) return null;
|
|
1522
1571
|
const fileName = parts[3];
|
|
1523
1572
|
if (fileName === "stdout.txt") return virtualFile(encodeUtf8(artifact.stdout));
|
|
1573
|
+
if (fileName === "stdout.bin" && artifact.stdoutBinary) return virtualFile(base64ToBytes(artifact.stdoutBinary.base64));
|
|
1524
1574
|
if (fileName === "stderr.txt") return virtualFile(encodeUtf8(artifact.stderr));
|
|
1525
1575
|
if (fileName === "meta.json") return virtualFile(encodeUtf8(`${JSON.stringify(commandArtifactMeta(artifact), null, 2)}\n`));
|
|
1526
1576
|
return null;
|
|
1527
1577
|
}
|
|
1528
|
-
async commandArtifactIds(
|
|
1578
|
+
async commandArtifactIds(commandStorageId) {
|
|
1529
1579
|
const ids = /* @__PURE__ */ new Set();
|
|
1530
|
-
for (const record of this.commandsById.values()) if (record.
|
|
1531
|
-
const keys = await this.artifacts.storageFor(
|
|
1580
|
+
for (const record of this.commandsById.values()) if (record.commandStorageId === commandStorageId && !this.artifacts.isReleased(commandStorageId, record.id)) ids.add(record.id);
|
|
1581
|
+
const keys = await this.artifacts.storageFor(commandStorageId).list("commands").catch(() => []);
|
|
1532
1582
|
for (const key of keys) {
|
|
1533
1583
|
const match = /^commands\/([^/]+)\/artifact\.json$/.exec(key);
|
|
1534
|
-
if (match && !this.artifacts.isReleased(
|
|
1584
|
+
if (match && !this.artifacts.isReleased(commandStorageId, match[1])) ids.add(match[1]);
|
|
1535
1585
|
}
|
|
1536
1586
|
return [...ids];
|
|
1537
1587
|
}
|
|
1538
|
-
async commandArtifact(
|
|
1539
|
-
if (this.artifacts.isReleased(
|
|
1588
|
+
async commandArtifact(commandStorageId, commandId) {
|
|
1589
|
+
if (this.artifacts.isReleased(commandStorageId, commandId)) return null;
|
|
1540
1590
|
const record = this.commandsById.get(commandId);
|
|
1541
|
-
if (record?.
|
|
1591
|
+
if (record?.commandStorageId === commandStorageId) {
|
|
1542
1592
|
this.syncRunningRecord(record);
|
|
1543
|
-
return
|
|
1593
|
+
return commandArtifactFromRecord(record);
|
|
1544
1594
|
}
|
|
1545
|
-
const value = await this.artifacts.storageFor(
|
|
1546
|
-
return
|
|
1595
|
+
const value = await this.artifacts.storageFor(commandStorageId).readJson(`commands/${commandId}/artifact.json`).catch(() => null);
|
|
1596
|
+
return isCommandArtifact(value) ? value : null;
|
|
1547
1597
|
}
|
|
1548
1598
|
persistCommandArtifact(record) {
|
|
1549
|
-
const fingerprint = `${record.status}:${record.exitCode ?? ""}:${record.stdout.length}:${record.stderr.length}`;
|
|
1599
|
+
const fingerprint = `${record.status}:${record.exitCode ?? ""}:${record.stdout.length}:${record.stderr.length}:${record.binaryStdout?.totalBytes ?? ""}`;
|
|
1550
1600
|
if (record.persistedFingerprint === fingerprint) return;
|
|
1551
1601
|
record.persistedFingerprint = fingerprint;
|
|
1552
|
-
this.artifacts.persist(record.
|
|
1602
|
+
this.artifacts.persist(record.commandStorageId, record.id, commandArtifactFromRecord(record));
|
|
1553
1603
|
}
|
|
1554
1604
|
syncRunningRecord(record) {
|
|
1555
1605
|
const foreground = this.shells.get(record.shellId)?.foreground;
|
|
@@ -1577,11 +1627,16 @@ function createPortableCommands(session) {
|
|
|
1577
1627
|
}
|
|
1578
1628
|
}));
|
|
1579
1629
|
}
|
|
1630
|
+
/** True when the string contains a char > 0xFF, i.e. already-decoded Unicode text. */
|
|
1631
|
+
function hasWideChar(value) {
|
|
1632
|
+
for (let i = 0; i < value.length; i += 1) if (value.charCodeAt(i) > 255) return true;
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1580
1635
|
function normalizeTimeoutMs(value) {
|
|
1581
1636
|
if (!Number.isFinite(value) || value < 1 || value > 6e5) throw new Error(`timeoutMs must be between 1 and ${MAX_TIMEOUT_MS}`);
|
|
1582
1637
|
return Math.floor(value);
|
|
1583
1638
|
}
|
|
1584
|
-
function
|
|
1639
|
+
function streamView(record, stream, explicitOffset, maxOutputBytes) {
|
|
1585
1640
|
const text = stream === "stdout" ? record.stdout : record.stderr;
|
|
1586
1641
|
const totalBytes = utf8Bytes(text);
|
|
1587
1642
|
const boundedOffset = clampOffset(explicitOffset ?? (stream === "stdout" ? record.stdoutOffset : record.stderrOffset), totalBytes);
|
|
@@ -1601,7 +1656,7 @@ function streamArtifact(record, stream, explicitOffset, maxOutputBytes) {
|
|
|
1601
1656
|
truncated
|
|
1602
1657
|
};
|
|
1603
1658
|
}
|
|
1604
|
-
function
|
|
1659
|
+
function mergedOutputView(record, explicitOffset, maxOutputBytes) {
|
|
1605
1660
|
const totalBytes = record.outputChunks.reduce((total, chunk) => total + chunk.bytes, 0);
|
|
1606
1661
|
const offset = clampOffset(explicitOffset ?? record.outputOffset, totalBytes);
|
|
1607
1662
|
const byteLimit = Math.max(0, Math.floor(maxOutputBytes));
|
|
@@ -1655,7 +1710,7 @@ function ensureRecordOutputCoverage(record) {
|
|
|
1655
1710
|
appendRecordOutput(record, "stdout", record.stdout);
|
|
1656
1711
|
appendRecordOutput(record, "stderr", record.stderr);
|
|
1657
1712
|
}
|
|
1658
|
-
function
|
|
1713
|
+
function commandArtifactFromRecord(record) {
|
|
1659
1714
|
return {
|
|
1660
1715
|
status: record.status,
|
|
1661
1716
|
shellId: record.shellId,
|
|
@@ -1664,7 +1719,12 @@ function persistedArtifactFromRecord(record) {
|
|
|
1664
1719
|
lastOutputAt: record.lastOutputAt,
|
|
1665
1720
|
exitCode: record.exitCode ?? null,
|
|
1666
1721
|
stdout: record.stdout,
|
|
1667
|
-
stderr: record.stderr
|
|
1722
|
+
stderr: record.stderr,
|
|
1723
|
+
...record.binaryStdout ? { stdoutBinary: {
|
|
1724
|
+
base64: bytesToBase64(record.binaryStdout.data),
|
|
1725
|
+
truncated: record.binaryStdout.truncated,
|
|
1726
|
+
totalBytes: record.binaryStdout.totalBytes
|
|
1727
|
+
} } : {}
|
|
1668
1728
|
};
|
|
1669
1729
|
}
|
|
1670
1730
|
function commandArtifactMeta(artifact) {
|
|
@@ -1686,10 +1746,15 @@ function commandArtifactMeta(artifact) {
|
|
|
1686
1746
|
stderr: {
|
|
1687
1747
|
path: stderrPath,
|
|
1688
1748
|
bytes: utf8Bytes(artifact.stderr)
|
|
1689
|
-
}
|
|
1749
|
+
},
|
|
1750
|
+
...artifact.stdoutBinary ? { stdoutBinary: {
|
|
1751
|
+
path: `/@/commands/${artifact.commandId}/stdout.bin`,
|
|
1752
|
+
bytes: artifact.stdoutBinary.totalBytes,
|
|
1753
|
+
truncated: artifact.stdoutBinary.truncated
|
|
1754
|
+
} } : {}
|
|
1690
1755
|
};
|
|
1691
1756
|
}
|
|
1692
|
-
function
|
|
1757
|
+
function isCommandArtifact(value) {
|
|
1693
1758
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1694
1759
|
const record = value;
|
|
1695
1760
|
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 +1786,4 @@ function heredocDelimiter(body) {
|
|
|
1721
1786
|
return delimiter;
|
|
1722
1787
|
}
|
|
1723
1788
|
//#endregion
|
|
1724
|
-
export { AgentSessionCommandStorage, BashEnvironment,
|
|
1789
|
+
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,7 +26,7 @@ interface Command {
|
|
|
27
26
|
positionals?: string[];
|
|
28
27
|
stdinField?: string;
|
|
29
28
|
output?: CommandOutputSpec;
|
|
30
|
-
/** Required when `run` is set
|
|
29
|
+
/** Required when `run` is set; an empty array renders no examples section. */
|
|
31
30
|
examples?: string[];
|
|
32
31
|
}
|
|
33
32
|
interface ParsedCommandInput {
|
|
@@ -36,7 +35,7 @@ interface ParsedCommandInput {
|
|
|
36
35
|
* For help: path of the node help was requested for.
|
|
37
36
|
*/
|
|
38
37
|
path: string[];
|
|
39
|
-
/** True when the invocation
|
|
38
|
+
/** True when the invocation requested `--help`. */
|
|
40
39
|
help: boolean;
|
|
41
40
|
values: Record<string, unknown>;
|
|
42
41
|
json: boolean;
|
|
@@ -49,24 +48,23 @@ interface CommandRunContext {
|
|
|
49
48
|
cwd: string;
|
|
50
49
|
io: CommandIO;
|
|
51
50
|
storage: CommandStorage;
|
|
51
|
+
/** Host of the BashEnvironment executing this command. */
|
|
52
|
+
host: Host;
|
|
52
53
|
}
|
|
53
54
|
interface CommandRunResult {
|
|
54
55
|
exitCode: number;
|
|
55
56
|
metadata?: unknown;
|
|
56
57
|
}
|
|
57
58
|
interface CommandStdin {
|
|
59
|
+
/** Stdin decoded as UTF-8 text (lossy for non-text input). */
|
|
58
60
|
text: string;
|
|
61
|
+
/** Raw stdin bytes, byte-identical to what the pipe delivered. */
|
|
62
|
+
bytes: Uint8Array;
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
type CommandAsset = {
|
|
62
|
-
type: 'image';
|
|
63
|
-
mediaType: string;
|
|
64
|
-
data: string;
|
|
65
|
-
};
|
|
64
|
+
declare function emptyStdin(): CommandStdin;
|
|
66
65
|
interface CommandIO {
|
|
67
66
|
stdout(data: string | Uint8Array): Promise<void> | void;
|
|
68
67
|
stderr(data: string | Uint8Array): Promise<void> | void;
|
|
69
|
-
asset(asset: CommandAsset): Promise<void> | void;
|
|
70
68
|
}
|
|
71
69
|
interface CommandStorage {
|
|
72
70
|
readJson<T>(key: string): Promise<T | null>;
|
|
@@ -81,18 +79,19 @@ interface CommandExecutionContext {
|
|
|
81
79
|
cwd: string;
|
|
82
80
|
io: CommandIO;
|
|
83
81
|
storage: CommandStorage;
|
|
82
|
+
host: Host;
|
|
84
83
|
}
|
|
85
84
|
declare class CommandRegistry {
|
|
86
85
|
private readonly commands;
|
|
87
86
|
register(command: Command): void;
|
|
88
87
|
get(name: string): Command | null;
|
|
89
88
|
list(): Command[];
|
|
90
|
-
|
|
89
|
+
renderHelp(): string;
|
|
91
90
|
}
|
|
92
|
-
declare const
|
|
91
|
+
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
92
|
declare function parseCommandInput(root: Command, argv: string[], stdin?: CommandStdin): ParsedCommandInput;
|
|
94
93
|
declare function runRegisteredCommand(root: Command, ctx: CommandExecutionContext): Promise<CommandRunResult>;
|
|
95
|
-
declare function
|
|
94
|
+
declare function renderCommandHelp(command: Command, parentPath?: string): string;
|
|
96
95
|
//#endregion
|
|
97
96
|
//#region src/storage.d.ts
|
|
98
97
|
declare class AgentSessionCommandStorage implements CommandStorage {
|
|
@@ -106,4 +105,4 @@ declare class AgentSessionCommandStorage implements CommandStorage {
|
|
|
106
105
|
private key;
|
|
107
106
|
}
|
|
108
107
|
//#endregion
|
|
109
|
-
export { runRegisteredCommand as _,
|
|
108
|
+
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-DB9hjZ2G.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.11.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.11.0",
|
|
24
24
|
"zod": "^4.0.0"
|
|
25
25
|
},
|
|
26
26
|
"license": "Apache-2.0",
|