@alisio/core 0.1.0-alpha.9 → 0.1.1
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 +52 -7
- package/dist/agents/active.d.ts +99 -0
- package/dist/agents/active.js +218 -0
- package/dist/agents/capabilities.d.ts +26 -0
- package/dist/agents/capabilities.js +120 -0
- package/dist/agents/create-agent-skill.d.ts +11 -0
- package/dist/agents/create-agent-skill.js +60 -0
- package/dist/agents/definitions.d.ts +88 -0
- package/dist/agents/definitions.js +404 -0
- package/dist/agents/draft.d.ts +41 -0
- package/dist/agents/draft.js +133 -0
- package/dist/agents/service.d.ts +57 -0
- package/dist/agents/service.js +86 -0
- package/dist/agents/templates.d.ts +8 -0
- package/dist/agents/templates.js +102 -0
- package/dist/analysis/capabilities.d.ts +48 -0
- package/dist/analysis/capabilities.js +90 -0
- package/dist/analysis/data/csv.d.ts +46 -0
- package/dist/analysis/data/csv.js +240 -0
- package/dist/analysis/data/datasets.d.ts +132 -0
- package/dist/analysis/data/datasets.js +441 -0
- package/dist/analysis/data/describe.d.ts +27 -0
- package/dist/analysis/data/describe.js +103 -0
- package/dist/analysis/data/engine-client.d.ts +58 -0
- package/dist/analysis/data/engine-client.js +304 -0
- package/dist/analysis/data/engine-entry.d.ts +1 -0
- package/dist/analysis/data/engine-entry.js +59 -0
- package/dist/analysis/data/engine-source.d.ts +4 -0
- package/dist/analysis/data/engine-source.js +6 -0
- package/dist/analysis/data/engine-types.d.ts +86 -0
- package/dist/analysis/data/engine-types.js +7 -0
- package/dist/analysis/data/infer.d.ts +42 -0
- package/dist/analysis/data/infer.js +118 -0
- package/dist/analysis/data/ingest-worker.d.ts +17 -0
- package/dist/analysis/data/ingest-worker.js +365 -0
- package/dist/analysis/data/json.d.ts +19 -0
- package/dist/analysis/data/json.js +44 -0
- package/dist/analysis/data/query-worker.d.ts +12 -0
- package/dist/analysis/data/query-worker.js +75 -0
- package/dist/analysis/data/rows.d.ts +39 -0
- package/dist/analysis/data/rows.js +120 -0
- package/dist/analysis/data/sql-guard.d.ts +20 -0
- package/dist/analysis/data/sql-guard.js +129 -0
- package/dist/analysis/data/stats.d.ts +18 -0
- package/dist/analysis/data/stats.js +77 -0
- package/dist/analysis/install-hints.d.ts +33 -0
- package/dist/analysis/install-hints.js +144 -0
- package/dist/analysis/janitor.d.ts +60 -0
- package/dist/analysis/janitor.js +264 -0
- package/dist/analysis/jobs.d.ts +94 -0
- package/dist/analysis/jobs.js +165 -0
- package/dist/analysis/oci.d.ts +129 -0
- package/dist/analysis/oci.js +330 -0
- package/dist/analysis/python/sources.d.ts +2 -0
- package/dist/analysis/python/sources.js +10 -0
- package/dist/analysis/requirements.d.ts +32 -0
- package/dist/analysis/requirements.js +73 -0
- package/dist/analysis/rerun.d.ts +40 -0
- package/dist/analysis/rerun.js +136 -0
- package/dist/analysis/runtime-manager.d.ts +146 -0
- package/dist/analysis/runtime-manager.js +472 -0
- package/dist/analysis/status.d.ts +21 -0
- package/dist/analysis/status.js +52 -0
- package/dist/application.d.ts +105 -3
- package/dist/application.js +446 -18
- package/dist/artifacts/kinds.d.ts +35 -0
- package/dist/artifacts/kinds.js +177 -0
- package/dist/artifacts/publisher.d.ts +7 -0
- package/dist/artifacts/publisher.js +29 -0
- package/dist/artifacts/store.d.ts +119 -0
- package/dist/artifacts/store.js +581 -0
- package/dist/background/janitor.d.ts +22 -0
- package/dist/background/janitor.js +62 -0
- package/dist/background/mirror.d.ts +14 -0
- package/dist/background/mirror.js +41 -0
- package/dist/background/notify.d.ts +58 -0
- package/dist/background/notify.js +172 -0
- package/dist/background/output.d.ts +46 -0
- package/dist/background/output.js +179 -0
- package/dist/background/service.d.ts +137 -0
- package/dist/background/service.js +410 -0
- package/dist/background/store.d.ts +103 -0
- package/dist/background/store.js +195 -0
- package/dist/changelog/data.d.ts +6 -0
- package/dist/changelog/data.js +306 -0
- package/dist/changelog/index.d.ts +5 -0
- package/dist/changelog/index.js +7 -0
- package/dist/changelog/parse.d.ts +53 -0
- package/dist/changelog/parse.js +160 -0
- package/dist/commands/builtins.d.ts +10 -0
- package/dist/commands/builtins.js +161 -0
- package/dist/commands/catalog.d.ts +86 -0
- package/dist/commands/catalog.js +304 -0
- package/dist/config.d.ts +206 -6
- package/dist/config.js +426 -20
- package/dist/core/compaction.js +13 -2
- package/dist/core/contracts.d.ts +180 -3
- package/dist/core/opt-in.d.ts +12 -0
- package/dist/core/opt-in.js +12 -0
- package/dist/core/output-limit.d.ts +34 -0
- package/dist/core/output-limit.js +39 -0
- package/dist/core/run-stats.d.ts +16 -0
- package/dist/core/run-stats.js +8 -0
- package/dist/core/runner.d.ts +134 -4
- package/dist/core/runner.js +780 -138
- package/dist/core/timeout.d.ts +15 -0
- package/dist/core/timeout.js +61 -0
- package/dist/core/truncation.d.ts +30 -0
- package/dist/core/truncation.js +83 -0
- package/dist/goal/blockers.d.ts +45 -0
- package/dist/goal/blockers.js +51 -0
- package/dist/goal/budget.d.ts +24 -0
- package/dist/goal/budget.js +53 -0
- package/dist/goal/command.d.ts +36 -0
- package/dist/goal/command.js +98 -0
- package/dist/goal/format.d.ts +19 -0
- package/dist/goal/format.js +102 -0
- package/dist/goal/host.d.ts +29 -0
- package/dist/goal/host.js +42 -0
- package/dist/goal/index.d.ts +9 -0
- package/dist/goal/index.js +9 -0
- package/dist/goal/machine.d.ts +186 -0
- package/dist/goal/machine.js +344 -0
- package/dist/goal/prompts.d.ts +10 -0
- package/dist/goal/prompts.js +75 -0
- package/dist/goal/service.d.ts +113 -0
- package/dist/goal/service.js +273 -0
- package/dist/goal/store.d.ts +59 -0
- package/dist/goal/store.js +184 -0
- package/dist/index.d.ts +50 -5
- package/dist/index.js +49 -4
- package/dist/mcp/connector.js +38 -6
- package/dist/mcp/rich.d.ts +41 -0
- package/dist/mcp/rich.js +362 -0
- package/dist/permissions/modes.d.ts +72 -0
- package/dist/permissions/modes.js +98 -0
- package/dist/plan/exit-plan.d.ts +25 -0
- package/dist/plan/exit-plan.js +130 -0
- package/dist/plan/state.d.ts +102 -0
- package/dist/plan/state.js +165 -0
- package/dist/plugins/host.d.ts +34 -1
- package/dist/plugins/host.js +143 -9
- package/dist/providers/settings.d.ts +15 -0
- package/dist/providers/settings.js +38 -0
- package/dist/reload.d.ts +75 -0
- package/dist/reload.js +204 -0
- package/dist/resources/context.js +6 -1
- package/dist/runtime/access.d.ts +62 -0
- package/dist/runtime/access.js +115 -0
- package/dist/runtime/blobs.d.ts +51 -0
- package/dist/runtime/blobs.js +177 -0
- package/dist/runtime/diff.d.ts +23 -0
- package/dist/runtime/diff.js +154 -0
- package/dist/runtime/ids.d.ts +8 -0
- package/dist/runtime/ids.js +32 -0
- package/dist/runtime/paths.d.ts +11 -0
- package/dist/runtime/paths.js +17 -1
- package/dist/runtime/process.d.ts +22 -0
- package/dist/runtime/process.js +100 -15
- package/dist/runtime/sqlite.d.ts +2 -0
- package/dist/runtime/sqlite.js +18 -0
- package/dist/runtime/store.d.ts +91 -3
- package/dist/runtime/store.js +465 -11
- package/dist/runtime/zip.d.ts +24 -0
- package/dist/runtime/zip.js +126 -0
- package/dist/sessions/children.js +17 -1
- package/dist/sessions/side-questions.d.ts +57 -0
- package/dist/sessions/side-questions.js +161 -0
- package/dist/tools/analysis.d.ts +37 -0
- package/dist/tools/analysis.js +573 -0
- package/dist/tools/artifacts.d.ts +14 -0
- package/dist/tools/artifacts.js +143 -0
- package/dist/tools/background.d.ts +7 -0
- package/dist/tools/background.js +165 -0
- package/dist/tools/data.d.ts +7 -0
- package/dist/tools/data.js +160 -0
- package/dist/tools/execute.d.ts +5 -0
- package/dist/tools/execute.js +15 -3
- package/dist/tools/goal.d.ts +19 -0
- package/dist/tools/goal.js +126 -0
- package/dist/tools/search.d.ts +17 -3
- package/dist/tools/search.js +153 -0
- package/dist/tools/standard.d.ts +4 -1
- package/dist/tools/standard.js +99 -18
- package/dist/version.d.ts +5 -3
- package/dist/version.js +35 -6
- package/package.json +2 -2
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mediated path access: the workspace plus explicitly declared extra directories.
|
|
3
|
+
*
|
|
4
|
+
* A tool path is resolved against the workspace first. Paths that fall outside every allowed root
|
|
5
|
+
* are NOT rejected outright anymore: they trigger an interactive approval scoped to the containing
|
|
6
|
+
* directory (mirroring OpenCode's `external_directory` model), and the approved directory is
|
|
7
|
+
* remembered for the session. In non-interactive runs the same check denies with an actionable
|
|
8
|
+
* error that names the resolved path and the exact remedy instead of hanging.
|
|
9
|
+
*
|
|
10
|
+
* This reuses `safePath`'s symlink hardening for the final resolution, so the symlink and
|
|
11
|
+
* per-segment `lstat` guarantees are preserved. Like the rest of Alisio, this is mediation, not an
|
|
12
|
+
* OS sandbox and not race-proof against hostile concurrent processes.
|
|
13
|
+
*/
|
|
14
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
15
|
+
import { dirname, resolve } from "node:path";
|
|
16
|
+
import { inside, outsideRootsMessage, safePath } from "./paths.js";
|
|
17
|
+
/** Nearest existing ancestor directory of `target`, canonicalized; used as the approval boundary. */
|
|
18
|
+
async function nearestExistingDirectory(target) {
|
|
19
|
+
let current = target;
|
|
20
|
+
for (;;) {
|
|
21
|
+
try {
|
|
22
|
+
if ((await lstat(current)).isDirectory())
|
|
23
|
+
return await realpath(current);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
const code = e.code;
|
|
27
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
const parent = dirname(current);
|
|
31
|
+
if (parent === current)
|
|
32
|
+
return current;
|
|
33
|
+
current = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class PathAccess {
|
|
37
|
+
workspace;
|
|
38
|
+
roots;
|
|
39
|
+
approve;
|
|
40
|
+
readOnly;
|
|
41
|
+
/** Session-approved directory subtrees (allow for the session). */
|
|
42
|
+
approved = new Set();
|
|
43
|
+
/** Canonicalized roots, memoized; a nonexistent declared root never matches. */
|
|
44
|
+
canonical = new Map();
|
|
45
|
+
constructor(options) {
|
|
46
|
+
this.workspace = options.workspace;
|
|
47
|
+
this.roots = options.readOnly ? [] : [...(options.extraRoots ?? [])];
|
|
48
|
+
this.readOnly = !!options.readOnly;
|
|
49
|
+
if (options.approve && !options.readOnly)
|
|
50
|
+
this.approve = options.approve;
|
|
51
|
+
}
|
|
52
|
+
/** Extra roots actually in force (always empty under `--read-only`). */
|
|
53
|
+
get extraRoots() {
|
|
54
|
+
return this.roots;
|
|
55
|
+
}
|
|
56
|
+
/** Directory subtrees approved for the session (diagnostics and tests). */
|
|
57
|
+
approvedDirectories() {
|
|
58
|
+
return [...this.approved];
|
|
59
|
+
}
|
|
60
|
+
canonicalize(path) {
|
|
61
|
+
let cached = this.canonical.get(path);
|
|
62
|
+
if (!cached) {
|
|
63
|
+
cached = realpath(path).catch(() => undefined);
|
|
64
|
+
this.canonical.set(path, cached);
|
|
65
|
+
}
|
|
66
|
+
return cached;
|
|
67
|
+
}
|
|
68
|
+
/** The allowed root that contains `target`, if any. */
|
|
69
|
+
async rootFor(target, workspace) {
|
|
70
|
+
const canonicalWorkspace = (await this.canonicalize(workspace)) ?? workspace;
|
|
71
|
+
if (inside(canonicalWorkspace, target))
|
|
72
|
+
return canonicalWorkspace;
|
|
73
|
+
for (const root of this.roots) {
|
|
74
|
+
const canonical = await this.canonicalize(root);
|
|
75
|
+
if (canonical && inside(canonical, target))
|
|
76
|
+
return canonical;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolves one tool path under the mediated policy. Inside the workspace or a declared extra
|
|
82
|
+
* root it behaves exactly like `safePath`; outside every root it asks for directory approval (or
|
|
83
|
+
* denies with the actionable message when no approval is possible).
|
|
84
|
+
*/
|
|
85
|
+
async resolve(path, options = {}) {
|
|
86
|
+
const workspace = options.workspace ?? this.workspace;
|
|
87
|
+
const target = resolve(workspace, path);
|
|
88
|
+
const root = await this.rootFor(target, workspace);
|
|
89
|
+
if (root)
|
|
90
|
+
return safePath(root, target);
|
|
91
|
+
if (this.readOnly)
|
|
92
|
+
throw new Error(outsideRootsMessage(target, "--read-only disables access outside the workspace; run without it to allow it"));
|
|
93
|
+
const boundary = await nearestExistingDirectory(target);
|
|
94
|
+
for (const approved of this.approved)
|
|
95
|
+
if (inside(approved, target))
|
|
96
|
+
return safePath(approved, target);
|
|
97
|
+
const approve = this.approve;
|
|
98
|
+
if (options.interactive === false || !approve)
|
|
99
|
+
throw new Error(outsideRootsMessage(target, approve
|
|
100
|
+
? "this call cannot request a new interactive approval"
|
|
101
|
+
: "no interactive approval is available in this run"));
|
|
102
|
+
const decision = await approve({
|
|
103
|
+
directory: boundary,
|
|
104
|
+
path: target,
|
|
105
|
+
...(options.session ? { session: options.session } : {}),
|
|
106
|
+
...(options.label ? { label: options.label } : {}),
|
|
107
|
+
signal: options.signal ?? new AbortController().signal,
|
|
108
|
+
});
|
|
109
|
+
if (decision === "session")
|
|
110
|
+
this.approved.add(boundary);
|
|
111
|
+
if (decision === "session" || decision === "once")
|
|
112
|
+
return safePath(boundary, target);
|
|
113
|
+
throw new Error(outsideRootsMessage(target, `access to directory "${boundary}" was denied by the user`));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Attachment, BlobRef, SqlDatabase } from "@alisio/sdk";
|
|
2
|
+
/** A streamed blob exceeded its size limit. */
|
|
3
|
+
export declare class BlobTooLarge extends Error {
|
|
4
|
+
readonly limit: number;
|
|
5
|
+
constructor(limit: number);
|
|
6
|
+
}
|
|
7
|
+
export interface BlobStoreOptions {
|
|
8
|
+
/** Directory that holds `sha256/…` (by default `<state home>/blobs`). */
|
|
9
|
+
root: string;
|
|
10
|
+
/** Session database with the v4 `blobs` table (e.g. `SQLiteStore.db`). */
|
|
11
|
+
db: SqlDatabase;
|
|
12
|
+
}
|
|
13
|
+
export declare class BlobStore {
|
|
14
|
+
private readonly options;
|
|
15
|
+
constructor(options: BlobStoreOptions);
|
|
16
|
+
/** Absolute file path of a blob. Throws for anything that is not a lowercase sha256 hex. */
|
|
17
|
+
path(hash: string): string;
|
|
18
|
+
has(hash: string): boolean;
|
|
19
|
+
private row;
|
|
20
|
+
/** Stores bytes once per content hash and returns their reference. */
|
|
21
|
+
put(bytes: Uint8Array, mimeType: string, dims?: {
|
|
22
|
+
width?: number;
|
|
23
|
+
height?: number;
|
|
24
|
+
}): BlobRef;
|
|
25
|
+
/**
|
|
26
|
+
* Stores a byte stream (an upload too large to hold in memory, e.g. a dataset) once per content
|
|
27
|
+
* hash, hashing while it writes. Fails with `BlobTooLarge` as soon as `maxBytes` is exceeded and
|
|
28
|
+
* leaves nothing behind. The stored type is the declared one; images stay the only blobs that
|
|
29
|
+
* `attachment()` and the blob route serve.
|
|
30
|
+
*/
|
|
31
|
+
putStream(stream: AsyncIterable<Uint8Array>, mimeType: string, options: {
|
|
32
|
+
maxBytes: number;
|
|
33
|
+
}): Promise<BlobRef>;
|
|
34
|
+
/**
|
|
35
|
+
* Retention: deletes the bytes and the row of a blob that is not an image (an uploaded dataset
|
|
36
|
+
* original). Images are referenced by messages and are never removed here. Returns the freed
|
|
37
|
+
* bytes (0 when the blob is unknown, an image or already gone).
|
|
38
|
+
*/
|
|
39
|
+
removeUpload(hash: string): number;
|
|
40
|
+
/** Hashes of non-image blobs created before `cutoff` (epoch ms): uploaded dataset originals. */
|
|
41
|
+
uploadsBefore(cutoff: number): string[];
|
|
42
|
+
get(hash: string): {
|
|
43
|
+
bytes: Buffer;
|
|
44
|
+
mimeType: string;
|
|
45
|
+
} | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Resolves a reference into a provider-ready image `Attachment` (base64 `data`), verifying
|
|
48
|
+
* that the stored bytes still match the hash.
|
|
49
|
+
*/
|
|
50
|
+
attachment(ref: BlobRef): Attachment;
|
|
51
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-addressed attachment bytes (v4). Files live at `<root>/sha256/<hash[0..2]>/<hash>`
|
|
3
|
+
* (directories 0700, files 0600, atomic temp-file + rename); metadata lives in the `blobs` table
|
|
4
|
+
* of the session database. Portable `node:fs`/`node:crypto` only (Node and Bun).
|
|
5
|
+
*
|
|
6
|
+
* `Attachment.data` stays required in the SDK, so blobs never reach the runner by reference: the
|
|
7
|
+
* host resolves a `BlobRef` into a base64 `Attachment` with `attachment()` before `runner.run`.
|
|
8
|
+
* Persisted messages therefore still carry base64 (as legacy inline attachments do).
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { pipeline } from "node:stream/promises";
|
|
14
|
+
const HASH = /^[0-9a-f]{64}$/;
|
|
15
|
+
/** A streamed blob exceeded its size limit. */
|
|
16
|
+
export class BlobTooLarge extends Error {
|
|
17
|
+
limit;
|
|
18
|
+
constructor(limit) {
|
|
19
|
+
super(`The upload exceeds ${limit} bytes`);
|
|
20
|
+
this.limit = limit;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class BlobStore {
|
|
24
|
+
options;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
}
|
|
28
|
+
/** Absolute file path of a blob. Throws for anything that is not a lowercase sha256 hex. */
|
|
29
|
+
path(hash) {
|
|
30
|
+
if (!HASH.test(hash))
|
|
31
|
+
throw new Error(`Invalid blob hash: ${hash.slice(0, 80)}`);
|
|
32
|
+
return join(this.options.root, "sha256", hash.slice(0, 2), hash);
|
|
33
|
+
}
|
|
34
|
+
has(hash) {
|
|
35
|
+
return HASH.test(hash) && existsSync(this.path(hash)) && !!this.row(hash);
|
|
36
|
+
}
|
|
37
|
+
row(hash) {
|
|
38
|
+
return this.options.db.prepare("SELECT * FROM blobs WHERE hash=?").get(hash);
|
|
39
|
+
}
|
|
40
|
+
/** Stores bytes once per content hash and returns their reference. */
|
|
41
|
+
put(bytes, mimeType, dims = {}) {
|
|
42
|
+
const mime = mimeType.trim();
|
|
43
|
+
if (!mime || mime.length > 255)
|
|
44
|
+
throw new Error("A blob needs a MIME type");
|
|
45
|
+
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
46
|
+
const file = this.path(hash);
|
|
47
|
+
if (!existsSync(file)) {
|
|
48
|
+
const dir = join(this.options.root, "sha256", hash.slice(0, 2));
|
|
49
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
50
|
+
chmodSync(dir, 0o700);
|
|
51
|
+
const temp = join(dir, `.tmp-${randomUUID()}`);
|
|
52
|
+
try {
|
|
53
|
+
writeFileSync(temp, bytes, { mode: 0o600, flag: "wx" });
|
|
54
|
+
renameSync(temp, file);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
rmSync(temp, { force: true });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.options.db
|
|
62
|
+
.prepare("INSERT OR IGNORE INTO blobs(hash,mime,size,width,height,created_at) VALUES(?,?,?,?,?,?)")
|
|
63
|
+
.run(hash, mime, bytes.byteLength, dims.width ?? null, dims.height ?? null, Date.now());
|
|
64
|
+
const row = this.row(hash);
|
|
65
|
+
return {
|
|
66
|
+
hash,
|
|
67
|
+
mimeType: row?.mime ?? mime,
|
|
68
|
+
bytes: bytes.byteLength,
|
|
69
|
+
...(row?.width != null ? { width: Number(row.width) } : {}),
|
|
70
|
+
...(row?.height != null ? { height: Number(row.height) } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Stores a byte stream (an upload too large to hold in memory, e.g. a dataset) once per content
|
|
75
|
+
* hash, hashing while it writes. Fails with `BlobTooLarge` as soon as `maxBytes` is exceeded and
|
|
76
|
+
* leaves nothing behind. The stored type is the declared one; images stay the only blobs that
|
|
77
|
+
* `attachment()` and the blob route serve.
|
|
78
|
+
*/
|
|
79
|
+
async putStream(stream, mimeType, options) {
|
|
80
|
+
const mime = mimeType.trim();
|
|
81
|
+
if (!mime || mime.length > 255)
|
|
82
|
+
throw new Error("A blob needs a MIME type");
|
|
83
|
+
const dir = join(this.options.root, "sha256");
|
|
84
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
85
|
+
const temp = join(dir, `.tmp-${randomUUID()}`);
|
|
86
|
+
const hash = createHash("sha256");
|
|
87
|
+
let size = 0;
|
|
88
|
+
try {
|
|
89
|
+
await pipeline(stream, async function* (source) {
|
|
90
|
+
for await (const chunk of source) {
|
|
91
|
+
size += chunk.byteLength;
|
|
92
|
+
if (size > options.maxBytes)
|
|
93
|
+
throw new BlobTooLarge(options.maxBytes);
|
|
94
|
+
hash.update(chunk);
|
|
95
|
+
yield chunk;
|
|
96
|
+
}
|
|
97
|
+
}, createWriteStream(temp, { mode: 0o600, flags: "wx" }));
|
|
98
|
+
const digest = hash.digest("hex");
|
|
99
|
+
const file = this.path(digest);
|
|
100
|
+
const fileDir = join(dir, digest.slice(0, 2));
|
|
101
|
+
mkdirSync(fileDir, { recursive: true, mode: 0o700 });
|
|
102
|
+
chmodSync(fileDir, 0o700);
|
|
103
|
+
if (existsSync(file))
|
|
104
|
+
rmSync(temp, { force: true });
|
|
105
|
+
else
|
|
106
|
+
renameSync(temp, file);
|
|
107
|
+
this.options.db
|
|
108
|
+
.prepare("INSERT OR IGNORE INTO blobs(hash,mime,size,width,height,created_at) VALUES(?,?,?,?,?,?)")
|
|
109
|
+
.run(digest, mime, statSync(file).size, null, null, Date.now());
|
|
110
|
+
const row = this.row(digest);
|
|
111
|
+
return { hash: digest, mimeType: row?.mime ?? mime, bytes: statSync(file).size };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
rmSync(temp, { force: true });
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Retention: deletes the bytes and the row of a blob that is not an image (an uploaded dataset
|
|
120
|
+
* original). Images are referenced by messages and are never removed here. Returns the freed
|
|
121
|
+
* bytes (0 when the blob is unknown, an image or already gone).
|
|
122
|
+
*/
|
|
123
|
+
removeUpload(hash) {
|
|
124
|
+
if (!HASH.test(hash))
|
|
125
|
+
return 0;
|
|
126
|
+
const row = this.row(hash);
|
|
127
|
+
if (row?.mime.startsWith("image/"))
|
|
128
|
+
return 0;
|
|
129
|
+
const file = this.path(hash);
|
|
130
|
+
let size = 0;
|
|
131
|
+
try {
|
|
132
|
+
size = statSync(file).size;
|
|
133
|
+
rmSync(file, { force: true });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* Already gone. */
|
|
137
|
+
}
|
|
138
|
+
this.options.db.prepare("DELETE FROM blobs WHERE hash=?").run(hash);
|
|
139
|
+
return size;
|
|
140
|
+
}
|
|
141
|
+
/** Hashes of non-image blobs created before `cutoff` (epoch ms): uploaded dataset originals. */
|
|
142
|
+
uploadsBefore(cutoff) {
|
|
143
|
+
return this.options.db
|
|
144
|
+
.prepare("SELECT hash FROM blobs WHERE mime NOT LIKE 'image/%' AND created_at<?")
|
|
145
|
+
.all(cutoff).map((r) => r.hash);
|
|
146
|
+
}
|
|
147
|
+
get(hash) {
|
|
148
|
+
if (!HASH.test(hash))
|
|
149
|
+
return undefined;
|
|
150
|
+
const row = this.row(hash);
|
|
151
|
+
const file = this.path(hash);
|
|
152
|
+
if (!row || !existsSync(file))
|
|
153
|
+
return undefined;
|
|
154
|
+
return { bytes: readFileSync(file), mimeType: row.mime };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolves a reference into a provider-ready image `Attachment` (base64 `data`), verifying
|
|
158
|
+
* that the stored bytes still match the hash.
|
|
159
|
+
*/
|
|
160
|
+
attachment(ref) {
|
|
161
|
+
const found = this.get(ref.hash);
|
|
162
|
+
if (!found)
|
|
163
|
+
throw new Error(`Blob not found: ${ref.hash}`);
|
|
164
|
+
if (createHash("sha256").update(found.bytes).digest("hex") !== ref.hash)
|
|
165
|
+
throw new Error(`Blob content does not match its hash: ${ref.hash}`);
|
|
166
|
+
if (!found.mimeType.startsWith("image/"))
|
|
167
|
+
throw new Error(`Only image blobs can be attached (got ${found.mimeType})`);
|
|
168
|
+
return {
|
|
169
|
+
kind: "image",
|
|
170
|
+
mimeType: found.mimeType,
|
|
171
|
+
data: found.bytes.toString("base64"),
|
|
172
|
+
bytes: found.bytes.byteLength,
|
|
173
|
+
...(ref.width !== undefined ? { width: ref.width } : {}),
|
|
174
|
+
...(ref.height !== undefined ? { height: ref.height } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified line diffs for the `diff` UiBlock that `write_file`/`edit_file` attach for rich
|
|
3
|
+
* clients (spec §8.6, RF-13). Myers O((N+M)·D) on lines after trimming the common prefix and
|
|
4
|
+
* suffix; when the edit distance exceeds `maxD` the middle becomes a full replacement. Pure and
|
|
5
|
+
* portable (no dependency). The web UI ships its own copy of the algorithm for third-party blocks
|
|
6
|
+
* that only carry `before`/`after` (packages cannot share code with the browser bundle).
|
|
7
|
+
*/
|
|
8
|
+
type Op = 0 | 1 | 2;
|
|
9
|
+
/** Line edit script (0 equal, 1 delete, 2 insert). */
|
|
10
|
+
export declare function lineEdits(a: string[], b: string[], maxD?: number): Op[];
|
|
11
|
+
/**
|
|
12
|
+
* A unified patch (`--- a/path`, `+++ b/path`, hunks with `context` lines) from `before` to
|
|
13
|
+
* `after`; `undefined` means the file did not exist. Identical texts give `""`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function unifiedPatch(path: string, before: string | undefined, after: string | undefined, options?: {
|
|
16
|
+
context?: number;
|
|
17
|
+
}): string;
|
|
18
|
+
/** Cuts `text` at a line boundary so it fits in `maxBytes` (UTF-8). */
|
|
19
|
+
export declare function clipLines(text: string, maxBytes: number): {
|
|
20
|
+
text: string;
|
|
21
|
+
clipped: boolean;
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified line diffs for the `diff` UiBlock that `write_file`/`edit_file` attach for rich
|
|
3
|
+
* clients (spec §8.6, RF-13). Myers O((N+M)·D) on lines after trimming the common prefix and
|
|
4
|
+
* suffix; when the edit distance exceeds `maxD` the middle becomes a full replacement. Pure and
|
|
5
|
+
* portable (no dependency). The web UI ships its own copy of the algorithm for third-party blocks
|
|
6
|
+
* that only carry `before`/`after` (packages cannot share code with the browser bundle).
|
|
7
|
+
*/
|
|
8
|
+
function myers(a, b, maxD) {
|
|
9
|
+
const n = a.length;
|
|
10
|
+
const m = b.length;
|
|
11
|
+
if (!n)
|
|
12
|
+
return b.map(() => 2);
|
|
13
|
+
if (!m)
|
|
14
|
+
return a.map(() => 1);
|
|
15
|
+
const max = n + m;
|
|
16
|
+
const offset = max + 1;
|
|
17
|
+
const v = new Int32Array(2 * max + 3);
|
|
18
|
+
const trace = [];
|
|
19
|
+
for (let d = 0; d <= Math.min(max, maxD); d++) {
|
|
20
|
+
trace.push(v.slice(offset - d - 1, offset + d + 2));
|
|
21
|
+
for (let k = -d; k <= d; k += 2) {
|
|
22
|
+
let x = k === -d || (k !== d && v[offset + k - 1] < v[offset + k + 1])
|
|
23
|
+
? v[offset + k + 1]
|
|
24
|
+
: v[offset + k - 1] + 1;
|
|
25
|
+
let y = x - k;
|
|
26
|
+
while (x < n && y < m && a[x] === b[y]) {
|
|
27
|
+
x++;
|
|
28
|
+
y++;
|
|
29
|
+
}
|
|
30
|
+
v[offset + k] = x;
|
|
31
|
+
if (x >= n && y >= m)
|
|
32
|
+
return backtrack(trace, n, m);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
function backtrack(trace, n, m) {
|
|
38
|
+
const ops = [];
|
|
39
|
+
let x = n;
|
|
40
|
+
let y = m;
|
|
41
|
+
for (let d = trace.length - 1; d > 0; d--) {
|
|
42
|
+
const snap = trace[d];
|
|
43
|
+
const at = (k) => snap[k + d + 1];
|
|
44
|
+
const k = x - y;
|
|
45
|
+
const prevK = k === -d || (k !== d && at(k - 1) < at(k + 1)) ? k + 1 : k - 1;
|
|
46
|
+
const prevX = at(prevK);
|
|
47
|
+
const prevY = prevX - prevK;
|
|
48
|
+
while (x > prevX && y > prevY) {
|
|
49
|
+
ops.push(0);
|
|
50
|
+
x--;
|
|
51
|
+
y--;
|
|
52
|
+
}
|
|
53
|
+
ops.push(x === prevX ? 2 : 1);
|
|
54
|
+
x = prevX;
|
|
55
|
+
y = prevY;
|
|
56
|
+
}
|
|
57
|
+
while (x > 0 && y > 0) {
|
|
58
|
+
ops.push(0);
|
|
59
|
+
x--;
|
|
60
|
+
y--;
|
|
61
|
+
}
|
|
62
|
+
return ops.reverse();
|
|
63
|
+
}
|
|
64
|
+
/** Line edit script (0 equal, 1 delete, 2 insert). */
|
|
65
|
+
export function lineEdits(a, b, maxD = 2000) {
|
|
66
|
+
let start = 0;
|
|
67
|
+
while (start < a.length && start < b.length && a[start] === b[start])
|
|
68
|
+
start++;
|
|
69
|
+
let endA = a.length;
|
|
70
|
+
let endB = b.length;
|
|
71
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
72
|
+
endA--;
|
|
73
|
+
endB--;
|
|
74
|
+
}
|
|
75
|
+
const midA = a.slice(start, endA);
|
|
76
|
+
const midB = b.slice(start, endB);
|
|
77
|
+
const middle = myers(midA, midB, maxD) ?? [...midA.map(() => 1), ...midB.map(() => 2)];
|
|
78
|
+
return [...new Array(start).fill(0), ...middle, ...new Array(a.length - endA).fill(0)];
|
|
79
|
+
}
|
|
80
|
+
const NO_EOL = "\u0000no-eol";
|
|
81
|
+
/** Lines of a text; a last line without `\n` carries a marker so a newline change is a change. */
|
|
82
|
+
function linesOf(text) {
|
|
83
|
+
if (!text)
|
|
84
|
+
return [];
|
|
85
|
+
const lines = text.split("\n");
|
|
86
|
+
if (lines.at(-1) === "")
|
|
87
|
+
lines.pop();
|
|
88
|
+
else
|
|
89
|
+
lines[lines.length - 1] = `${lines.at(-1)}${NO_EOL}`;
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A unified patch (`--- a/path`, `+++ b/path`, hunks with `context` lines) from `before` to
|
|
94
|
+
* `after`; `undefined` means the file did not exist. Identical texts give `""`.
|
|
95
|
+
*/
|
|
96
|
+
export function unifiedPatch(path, before, after, options = {}) {
|
|
97
|
+
const context = options.context ?? 3;
|
|
98
|
+
const a = linesOf(before);
|
|
99
|
+
const b = linesOf(after);
|
|
100
|
+
const rows = [];
|
|
101
|
+
let i = 0;
|
|
102
|
+
let j = 0;
|
|
103
|
+
for (const op of lineEdits(a, b)) {
|
|
104
|
+
if (op === 0)
|
|
105
|
+
rows.push({ op, text: a[i], old: i++, new: j++ });
|
|
106
|
+
else if (op === 1)
|
|
107
|
+
rows.push({ op, text: a[i], old: i++, new: j });
|
|
108
|
+
else
|
|
109
|
+
rows.push({ op, text: b[j], old: i, new: j++ });
|
|
110
|
+
}
|
|
111
|
+
const hunks = [];
|
|
112
|
+
for (let k = 0; k < rows.length; k++) {
|
|
113
|
+
if (rows[k].op === 0)
|
|
114
|
+
continue;
|
|
115
|
+
const from = Math.max(0, k - context);
|
|
116
|
+
const last = hunks.at(-1);
|
|
117
|
+
if (last && from <= last[1] + 1)
|
|
118
|
+
last[1] = Math.min(rows.length - 1, k + context);
|
|
119
|
+
else
|
|
120
|
+
hunks.push([from, Math.min(rows.length - 1, k + context)]);
|
|
121
|
+
}
|
|
122
|
+
if (!hunks.length)
|
|
123
|
+
return "";
|
|
124
|
+
const out = [
|
|
125
|
+
`--- ${before === undefined ? "/dev/null" : `a/${path}`}`,
|
|
126
|
+
`+++ ${after === undefined ? "/dev/null" : `b/${path}`}`,
|
|
127
|
+
];
|
|
128
|
+
const range = (start, count) => count === 1 ? `${start + 1}` : `${count === 0 ? start : start + 1},${count}`;
|
|
129
|
+
for (const [from, to] of hunks) {
|
|
130
|
+
const slice = rows.slice(from, to + 1);
|
|
131
|
+
const first = slice[0];
|
|
132
|
+
const oldCount = slice.filter((r) => r.op !== 2).length;
|
|
133
|
+
const newCount = slice.filter((r) => r.op !== 1).length;
|
|
134
|
+
out.push(`@@ -${range(first.old, oldCount)} +${range(first.new, newCount)} @@`);
|
|
135
|
+
for (const row of slice) {
|
|
136
|
+
const noEol = row.text.endsWith(NO_EOL);
|
|
137
|
+
const text = noEol ? row.text.slice(0, -NO_EOL.length) : row.text;
|
|
138
|
+
out.push(`${row.op === 0 ? " " : row.op === 1 ? "-" : "+"}${text}`);
|
|
139
|
+
if (noEol)
|
|
140
|
+
out.push("\");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out.join("\n");
|
|
144
|
+
}
|
|
145
|
+
/** Cuts `text` at a line boundary so it fits in `maxBytes` (UTF-8). */
|
|
146
|
+
export function clipLines(text, maxBytes) {
|
|
147
|
+
if (Buffer.byteLength(text) <= maxBytes)
|
|
148
|
+
return { text, clipped: false };
|
|
149
|
+
let cut = Buffer.from(text).subarray(0, maxBytes).toString("utf8");
|
|
150
|
+
// A split multi-byte character decodes to U+FFFD; drop the partial last line either way.
|
|
151
|
+
const newline = cut.lastIndexOf("\n");
|
|
152
|
+
cut = newline > 0 ? cut.slice(0, newline) : cut.replace(/�$/, "");
|
|
153
|
+
return { text: cut, clipped: true };
|
|
154
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ULID (26 characters, lexicographically sortable by creation time): 48 bits of milliseconds
|
|
3
|
+
* and 80 random bits. No dependency and no monotonic counter; two ids created in the same
|
|
4
|
+
* millisecond sort arbitrarily, which is fine for row ids and folder names.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ulid(now?: number): string;
|
|
7
|
+
/** A prefixed id such as `art_01J…` or `exec_01J…`. */
|
|
8
|
+
export declare const newId: (prefix: string, now?: number) => string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
/** Crockford base32 (no I, L, O, U), the ULID alphabet. */
|
|
3
|
+
const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
4
|
+
/**
|
|
5
|
+
* A ULID (26 characters, lexicographically sortable by creation time): 48 bits of milliseconds
|
|
6
|
+
* and 80 random bits. No dependency and no monotonic counter; two ids created in the same
|
|
7
|
+
* millisecond sort arbitrarily, which is fine for row ids and folder names.
|
|
8
|
+
*/
|
|
9
|
+
export function ulid(now = Date.now()) {
|
|
10
|
+
let time = "";
|
|
11
|
+
let t = Math.max(0, Math.floor(now));
|
|
12
|
+
for (let i = 0; i < 10; i++) {
|
|
13
|
+
time = ALPHABET[t % 32] + time;
|
|
14
|
+
t = Math.floor(t / 32);
|
|
15
|
+
}
|
|
16
|
+
const bytes = randomBytes(10);
|
|
17
|
+
let random = "";
|
|
18
|
+
// 80 bits → 16 characters of 5 bits each.
|
|
19
|
+
let buffer = 0, bits = 0;
|
|
20
|
+
for (const byte of bytes) {
|
|
21
|
+
buffer = (buffer << 8) | byte;
|
|
22
|
+
bits += 8;
|
|
23
|
+
while (bits >= 5) {
|
|
24
|
+
bits -= 5;
|
|
25
|
+
random += ALPHABET[(buffer >> bits) & 31];
|
|
26
|
+
}
|
|
27
|
+
buffer &= (1 << bits) - 1;
|
|
28
|
+
}
|
|
29
|
+
return time + random;
|
|
30
|
+
}
|
|
31
|
+
/** A prefixed id such as `art_01J…` or `exec_01J…`. */
|
|
32
|
+
export const newId = (prefix, now) => `${prefix}_${ulid(now)}`;
|
package/dist/runtime/paths.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
export declare function inside(root: string, path: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Actionable message for a path that no allowed root covers. It always names the resolved target
|
|
4
|
+
* and both remedies (the one-run flag and the persistent configuration key) instead of the old
|
|
5
|
+
* opaque "Path outside workspace".
|
|
6
|
+
*/
|
|
7
|
+
export declare function outsideRootsMessage(target: string, reason?: string): string;
|
|
2
8
|
/** Mediated path policy; not an OS sandbox and not race-proof against hostile concurrent processes. */
|
|
3
9
|
export declare function safePath(root: string, path: string): Promise<string>;
|
|
4
10
|
export declare function findWorkspace(cwd: string): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Opaque, stable key of a workspace path: the first 16 hex characters of its sha256. Core names
|
|
13
|
+
* per-workspace folders with it and the server exposes it as the web `workspaceId`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const workspaceKey: (path: string) => string;
|
package/dist/runtime/paths.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { lstat, realpath } from "node:fs/promises";
|
|
2
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
4
|
export function inside(root, path) {
|
|
4
5
|
const r = relative(root, path);
|
|
5
6
|
return r === "" || (!r.startsWith(`..${sep}`) && r !== ".." && !isAbsolute(r));
|
|
6
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Actionable message for a path that no allowed root covers. It always names the resolved target
|
|
10
|
+
* and both remedies (the one-run flag and the persistent configuration key) instead of the old
|
|
11
|
+
* opaque "Path outside workspace".
|
|
12
|
+
*/
|
|
13
|
+
export function outsideRootsMessage(target, reason) {
|
|
14
|
+
return (`Path outside the workspace: ${target}${reason ? ` (${reason})` : ""}. ` +
|
|
15
|
+
"Allow its directory for one run with `--add-dir <directory>`, add it to the " +
|
|
16
|
+
"`additionalDirectories` configuration key, or approve it interactively in the TUI.");
|
|
17
|
+
}
|
|
7
18
|
/** Mediated path policy; not an OS sandbox and not race-proof against hostile concurrent processes. */
|
|
8
19
|
export async function safePath(root, path) {
|
|
9
20
|
const canonical = await realpath(root), target = resolve(canonical, path);
|
|
10
21
|
if (!inside(canonical, target))
|
|
11
|
-
throw new Error(
|
|
22
|
+
throw new Error(outsideRootsMessage(target));
|
|
12
23
|
let cursor = canonical;
|
|
13
24
|
for (const part of relative(canonical, target).split(sep).filter(Boolean)) {
|
|
14
25
|
cursor = resolve(cursor, part);
|
|
@@ -40,3 +51,8 @@ export async function findWorkspace(cwd) {
|
|
|
40
51
|
path = dirname(path);
|
|
41
52
|
}
|
|
42
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Opaque, stable key of a workspace path: the first 16 hex characters of its sha256. Core names
|
|
56
|
+
* per-workspace folders with it and the server exposes it as the web `workspaceId`.
|
|
57
|
+
*/
|
|
58
|
+
export const workspaceKey = (path) => createHash("sha256").update(path).digest("hex").slice(0, 16);
|
|
@@ -13,6 +13,14 @@ export declare function isMissingCommand(error: unknown): boolean;
|
|
|
13
13
|
export declare const RIPGREP_INSTALL_HINT: string;
|
|
14
14
|
/** Human-readable detection status for `alisio doctor`. */
|
|
15
15
|
export declare function ripgrepDiagnostic(found: boolean): string | undefined;
|
|
16
|
+
/** Whether a process id belongs to a live process (EPERM still means alive). */
|
|
17
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Kills a spawned process and everything it started: the process group on POSIX (children are
|
|
20
|
+
* spawned with `detached`, so the pid leads its own group) and `taskkill /T /F` on Windows.
|
|
21
|
+
* Synchronous, so it is also safe inside a `process.on("exit")` handler. Never throws.
|
|
22
|
+
*/
|
|
23
|
+
export declare function killProcessTree(pid: number, fallback?: () => void): void;
|
|
16
24
|
export declare function runProcess(command: string, args: string[], options: {
|
|
17
25
|
cwd: string;
|
|
18
26
|
signal: AbortSignal;
|
|
@@ -22,4 +30,18 @@ export declare function runProcess(command: string, args: string[], options: {
|
|
|
22
30
|
onData?: (chunk: string) => void;
|
|
23
31
|
/** On cancellation: SIGTERM the process group, then SIGKILL after this grace (default 5s). */
|
|
24
32
|
killGraceMs?: number;
|
|
33
|
+
/**
|
|
34
|
+
* What happens when a stream exceeds `maxBytes`: `kill` (default) stops the process; `truncate`
|
|
35
|
+
* lets it run and keeps only the LAST `maxBytes` of each stream (the result is `truncated`).
|
|
36
|
+
*/
|
|
37
|
+
onOverflow?: "kill" | "truncate";
|
|
38
|
+
/** Full stdout/stderr copies (up to `maxLogBytes` each), independent of `maxBytes`. */
|
|
39
|
+
logFiles?: {
|
|
40
|
+
stdout: string;
|
|
41
|
+
stderr: string;
|
|
42
|
+
};
|
|
43
|
+
/** Cap of each log file (default 10 MiB); a marker line notes the cut. */
|
|
44
|
+
maxLogBytes?: number;
|
|
45
|
+
/** Called once with the pid of the spawned process (it leads its own group on POSIX). */
|
|
46
|
+
onSpawn?: (pid: number) => void;
|
|
25
47
|
}): Promise<ProcessResult>;
|