@better-fs-tools/shell 0.1.0 → 0.2.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/README.md CHANGED
@@ -85,7 +85,7 @@ if (result.status === "error") {
85
85
  }
86
86
  ```
87
87
 
88
- [docs/result-schema.md](docs/result-schema.md) lists every field, every error code with its phase, and every note. Note codes are kebab case, as in read and write: `clamped`, `output-incomplete`, `spill-failed`, `unconfirmed-stop`, `extension-failed`, and the error note of an error result, whose code is the error code in kebab case (`denied`, `aborted`, `output-cap`, and so on).
88
+ [docs/result-schema.md](docs/result-schema.md) lists every field, every error code with its phase, and every note. Note codes are kebab case, as in read and write: `clamped`, `output-incomplete`, `unconfirmed-stop`, and the error note of an error result, whose code is the error code in kebab case (`denied`, `aborted`, `output-cap`, and so on).
89
89
 
90
90
  The model text starts with the status line:
91
91
 
@@ -232,7 +232,7 @@ export const bash = createNodeBashTool({ spill: fileSpill });
232
232
 
233
233
  A hook that throws, or returns a malformed value, gives `EXTENSION_FAILED` with its phase. After the run, the result keeps the run and the output.
234
234
 
235
- A formatter that throws, or returns neither a string nor an array, does not change the status. The core adds an `extension-failed` warning and formats with `defaultShellFormatter()`, as the read and write tools do. The format context has `digest`, `limits`, `messages`, `mode`, and `call`.
235
+ A formatter that throws, or returns neither a string nor an array, does not change the status. The core formats with `defaultShellFormatter()` and adds no note, as the read and write tools do. The format context has `digest`, `limits`, `messages`, `mode`, and `call`.
236
236
 
237
237
  ## Runners
238
238
 
@@ -14,9 +14,9 @@ export interface ShellFormatContext<THost = undefined> {
14
14
  export interface ShellFormatter<THost = undefined> {
15
15
  readonly id: string;
16
16
  /**
17
- * Sync and pure. A string becomes one text part. A throw, or a return that
18
- * is neither a string nor an array, adds an extension-failed warning, and the
19
- * default formatter formats the report. The status stays.
17
+ * Sync and pure. A string becomes one text part. After a throw, or a return
18
+ * that is neither a string nor an array, the default formatter formats the
19
+ * report with no added note. The status stays.
20
20
  */
21
21
  format(report: ShellReport, ctx: ShellFormatContext<THost>): string | readonly ContentPart[];
22
22
  }
@@ -50,9 +50,6 @@ export interface ShellMessageCatalog extends ToolMessages {
50
50
  }): string;
51
51
  unconfirmedStop(): string;
52
52
  abortedBeforeStart(): string;
53
- spillFailed(c: {
54
- sink: string;
55
- }): string;
56
53
  /**
57
54
  * The output stream failed (`detail`), gave chunks that were not output
58
55
  * (`skippedChunks`), or had not ended `drainMs` after the exit.
@@ -6,8 +6,7 @@ import { StageStop, errorNote, messageOf, warning } from "./stop.js";
6
6
  const DRAIN_AFTER_EXIT_MS = 1_000;
7
7
  const EXIT_WAIT_AFTER_KILL_GRACE_MS = 1_000;
8
8
  export async function execute(scope, runner, planned, env) {
9
- const { deps } = scope;
10
- const { limits, messages } = deps;
9
+ const { limits, messages } = scope.deps;
11
10
  const spill = await openSpill(scope, planned);
12
11
  const controller = new AbortController();
13
12
  let stoppedBy = null;
@@ -96,9 +95,6 @@ export async function execute(scope, runner, planned, env) {
96
95
  throw startError(scope, exit.error.reason, exit.error.detail ?? null, planned.cwd);
97
96
  }
98
97
  const spillRef = await (spill?.close() ?? Promise.resolve(null));
99
- if (spill?.failed === true) {
100
- scope.notes.push(warning("spill-failed", messages.spillFailed({ sink: deps.spill?.id ?? "unknown" })));
101
- }
102
98
  const reason = stoppedBy;
103
99
  if (exit === null)
104
100
  scope.notes.push(warning("unconfirmed-stop", messages.unconfirmedStop()));
@@ -235,9 +231,6 @@ async function openSpill(scope, planned) {
235
231
  let chain = Promise.resolve();
236
232
  let failed = false;
237
233
  return {
238
- get failed() {
239
- return failed;
240
- },
241
234
  write(bytes) {
242
235
  if (failed)
243
236
  return;
@@ -45,7 +45,6 @@ function catalogFor(param, duration) {
45
45
  outputCap: ({ limit }) => `The command wrote more than ${formatBytes(limit)} of output, so it was stopped. Send less output, for example with a filter, a count, or a redirect to a file.`,
46
46
  unconfirmedStop: () => "The runner did not confirm that the process tree stopped. Processes may still be running.",
47
47
  abortedBeforeStart: () => "The call was aborted before the command started.",
48
- spillFailed: ({ sink }) => `The ${sink} spill sink failed, so the full output was not saved. The command was not affected.`,
49
48
  outputIncomplete: ({ detail, skippedChunks, drainMs }) => {
50
49
  const causes = [
51
50
  ...(detail === null ? [] : [`the output stream failed (${detail})`]),
@@ -3,7 +3,7 @@ import { AbortStop } from "./abort.js";
3
3
  import { execute } from "./execute.js";
4
4
  import { clampedTimeout, isRecord, parseBashInput } from "./input.js";
5
5
  import { authorize, baseCwd, beforeRun, environment, extensionFailure, resolveCwd, runnerFor, } from "./stages.js";
6
- import { StageStop, errorNote, extensionId, info, isNoteList, messageOf, warning } from "./stop.js";
6
+ import { StageStop, errorNote, info, isNoteList, messageOf } from "./stop.js";
7
7
  export async function runBash(deps, input, call) {
8
8
  const notes = [];
9
9
  const { messages, limits } = deps;
@@ -165,15 +165,9 @@ function finish(deps, call, done) {
165
165
  catch { }
166
166
  if (content !== null)
167
167
  return Object.freeze({ ...done, content });
168
- const id = extensionId(deps.formatter);
169
- const note = warning("extension-failed", deps.messages.extensionFailed({ extension: id, phase: "format" }), { extension: "formatter", id });
170
- const kept = Object.freeze({
171
- ...done,
172
- notes: Object.freeze([...done.notes, note]),
173
- });
174
168
  return Object.freeze({
175
- ...kept,
176
- content: toContent(defaultShellFormatter().format(kept, context)) ?? [],
169
+ ...done,
170
+ content: toContent(defaultShellFormatter().format(done, context)) ?? [],
177
171
  });
178
172
  }
179
173
  function toContent(value) {
@@ -1,13 +1,16 @@
1
1
  import type { Note } from "@better-fs-tools/read";
2
2
  import type { ShellFormatter } from "../contract/format.ts";
3
3
  export interface ShellFormatterOptions {
4
+ /** Filter or rewrite a note for the model text. null hides it. Default: show every note. */
5
+ readonly notes?: (note: Note) => Note | null;
4
6
  /** Default: `[bash:${note.code}] ${note.message}`. */
5
7
  readonly noteLine?: (note: Note) => string;
6
8
  }
7
9
  /**
8
10
  * The status line, then the output view, then a blank line and the note
9
11
  * lines. A call that did not run prints the note lines only. "view" mode
10
- * leaves out the notes.
12
+ * leaves out the notes. The `notes` option hides or rewrites a note, as the
13
+ * read tool's formatters do.
11
14
  *
12
15
  * ```text
13
16
  * Exit code 1 · 0.4 s
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The status line, then the output view, then a blank line and the note
3
3
  * lines. A call that did not run prints the note lines only. "view" mode
4
- * leaves out the notes.
4
+ * leaves out the notes. The `notes` option hides or rewrites a note, as the
5
+ * read tool's formatters do.
5
6
  *
6
7
  * ```text
7
8
  * Exit code 1 · 0.4 s
@@ -12,7 +13,9 @@ export function defaultShellFormatter(options = {}) {
12
13
  if (options === null || typeof options !== "object" || Array.isArray(options)) {
13
14
  throw new TypeError("defaultShellFormatter options must be an object");
14
15
  }
15
- const { noteLine = defaultNoteLine } = options;
16
+ const { notes: filter = showNote, noteLine = defaultNoteLine } = options;
17
+ if (typeof filter !== "function")
18
+ throw new TypeError("notes must be a function");
16
19
  if (typeof noteLine !== "function")
17
20
  throw new TypeError("noteLine must be a function");
18
21
  return Object.freeze({
@@ -21,7 +24,7 @@ export function defaultShellFormatter(options = {}) {
21
24
  const main = body(report, ctx.messages);
22
25
  if (ctx.mode === "view")
23
26
  return main.join("\n");
24
- const notes = report.notes.map(noteLine);
27
+ const notes = noteLines(report.notes, filter, noteLine);
25
28
  if (notes.length === 0)
26
29
  return main.join("\n");
27
30
  if (main.length === 0)
@@ -30,6 +33,18 @@ export function defaultShellFormatter(options = {}) {
30
33
  },
31
34
  });
32
35
  }
36
+ function noteLines(notes, filter, line) {
37
+ const lines = [];
38
+ for (const note of notes) {
39
+ const shown = filter(note);
40
+ if (shown !== null)
41
+ lines.push(line(shown));
42
+ }
43
+ return lines;
44
+ }
45
+ function showNote(note) {
46
+ return note;
47
+ }
33
48
  function defaultNoteLine(note) {
34
49
  return `[bash:${note.code}] ${note.message}`;
35
50
  }
@@ -63,7 +63,7 @@ A failing output stream, chunks that are not output, or a stream that has not en
63
63
 
64
64
  ## Host code
65
65
 
66
- A throw or a malformed return from host code gives `EXTENSION_FAILED` with the phase of the stage. An `afterRun` failure keeps the run and the output. A formatter failure is the exception: the status stays, the core adds an `extension-failed` warning, and `defaultShellFormatter()` makes the content, as in the read and write tools.
66
+ A throw or a malformed return from host code gives `EXTENSION_FAILED` with the phase of the stage. An `afterRun` failure keeps the run and the output. A formatter failure is the exception: the status stays, the core adds no note, and `defaultShellFormatter()` makes the content, as in the read and write tools.
67
67
 
68
68
  Every host function gets `ctx`: the tool name, the request, the limits, the messages, `digest`, `clock`, and the call context. The bash tool hashes nothing itself. `digest` and `clock` come from the dependencies, so a bundle can give bash the same digest and clock as its file tools.
69
69
 
@@ -91,7 +91,7 @@ The validated canonical input. `timeoutMs` is concrete: the input value clamped
91
91
  omittedLines: number;
92
92
  stdoutBytes: number;
93
93
  stderrBytes: number;
94
- spill: string | null; // the spill sink's reference, or null without a sink
94
+ spill: string | null; // the spill sink's reference, or null without a sink or when the sink failed
95
95
  }
96
96
  ```
97
97
 
@@ -111,7 +111,7 @@ stdout and stderr are merged in arrival order. The view is `head`, a gap line, t
111
111
  | `OUTPUT_CAP` | `run` | The command wrote more than `maxCaptureBytes`, so the core stopped it | set |
112
112
  | `EXTENSION_FAILED` | `resolve`, `beforeRun`, `authorize`, `env`, `spill`, `run`, `afterRun` | Host code threw or returned a malformed value. `run` is a runner factory, `resolve` a cwd factory or resolver | set for `afterRun` |
113
113
 
114
- `ShellPhase` also has `format`. A formatter failure is not an error: it gives an `extension-failed` warning, and the status stays.
114
+ `ShellPhase` also has `format`. A formatter failure is not an error: the default formatter runs, no note is added, and the status stays.
115
115
 
116
116
  An `afterRun` hook that fails turns the result into `EXTENSION_FAILED`, and keeps `run`, `output`, and the notes so far. An abort during the run, and `OUTPUT_CAP`, keep them too. The formatter then shows the exit as the status line, and the reason in the error note.
117
117
 
@@ -131,14 +131,12 @@ Notes from the core:
131
131
  | ----------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
132
132
  | `clamped` | info | The input timeout was over `maxTimeoutMs` | `{ param: "timeoutMs", requested, max }` |
133
133
  | `output-incomplete` | warning | The output stream failed, the runner gave chunks that were not output, or the stream had not ended `drainMs` after the exit. Not added after a stop the tool started (timeout, abort, output cap) | `{ skippedChunks, detail?, drainMs? }` |
134
- | `spill-failed` | warning | The spill sink failed. The command was not affected | none |
135
134
  | `unconfirmed-stop` | warning | The runner did not settle its exit in the grace time after a stop | none |
136
- | `extension-failed` | warning | The formatter failed, and the default formatter ran. The status stays | `{ extension: "formatter", id }` |
137
135
  | `invalid-input`, `cwd-not-found`, `cwd-not-a-directory`, `refused`, `denied`, `aborted`, `spawn-failed`, `output-cap`, `extension-failed` | warning | The error note of each error code | none |
138
136
 
139
137
  A resolver can add its own note, for example `path-repaired` from `unicodeRepair()`. An allow decision from the authorizer and a `beforeRun` hook can add notes. They keep their codes.
140
138
 
141
- Order: the error note comes first, then `clamped`, then the resolver note, then the `beforeRun` and authorizer notes, then the run notes (`spill-failed`, `unconfirmed-stop`, `output-incomplete`). An `afterRun` hook can replace the notes. A formatter failure adds its `extension-failed` note at the very end.
139
+ Order: the error note comes first, then `clamped`, then the resolver note, then the `beforeRun` and authorizer notes, then the run notes (`unconfirmed-stop`, `output-incomplete`). An `afterRun` hook can replace the notes.
142
140
 
143
141
  ## Content
144
142
 
@@ -149,3 +147,5 @@ The formatter makes `content` once, with `mode: "model"`. A formatter that retur
149
147
  3. A blank line, then the note lines.
150
148
 
151
149
  A call that never started prints the note lines only. In `mode: "view"` the notes are left out.
150
+
151
+ `defaultShellFormatter({ notes })` takes a function that rewrites a note, or returns `null` to hide it from the model text. `noteLine(note)` changes the text of one note line. The read and write formatters take the same `notes` option. The report keeps every note.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-fs-tools/shell",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A bash tool for TypeScript agents: a small run core with hooks and defaults.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,7 +38,7 @@
38
38
  "access": "public"
39
39
  },
40
40
  "dependencies": {
41
- "@better-fs-tools/fs": "^0.1.0",
42
- "@better-fs-tools/read": "^0.1.0"
41
+ "@better-fs-tools/fs": "^0.2.0",
42
+ "@better-fs-tools/read": "^0.2.0"
43
43
  }
44
44
  }