omkit 0.4.8 → 0.8.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/CHANGELOG.md +188 -0
- package/README.md +169 -13
- package/dist/actions/chrome-page.d.ts +41 -0
- package/dist/actions/chrome-page.js +77 -29
- package/dist/actions/drive-page.d.ts +50 -0
- package/dist/actions/drive-page.js +68 -0
- package/dist/actions/index.d.ts +5 -3
- package/dist/actions/index.js +2 -1
- package/dist/actions/prompt.d.ts +32 -5
- package/dist/actions/prompt.js +92 -8
- package/dist/cli/commands/help.js +7 -0
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/ls.d.ts +11 -4
- package/dist/cli/commands/ls.js +35 -15
- package/dist/cli/commands/mcp.d.ts +10 -0
- package/dist/cli/commands/mcp.js +13 -0
- package/dist/cli/commands/skill.d.ts +18 -0
- package/dist/cli/commands/skill.js +62 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +78 -30
- package/dist/client/discover-child.d.ts +12 -0
- package/dist/client/discover-child.js +71 -0
- package/dist/client/discovery.d.ts +21 -1
- package/dist/client/discovery.js +139 -2
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +15 -5
- package/dist/client/outline.d.ts +35 -0
- package/dist/client/outline.js +100 -0
- package/dist/client/registry.d.ts +59 -0
- package/dist/client/runner.d.ts +12 -0
- package/dist/client/runner.js +16 -7
- package/dist/client/types.d.ts +14 -1
- package/dist/client/types.js +5 -1
- package/dist/core/ActionRun.d.ts +5 -0
- package/dist/core/ActionRun.js +9 -0
- package/dist/core/ExecutionTree.d.ts +40 -2
- package/dist/core/ExecutionTree.js +66 -2
- package/dist/core/action.d.ts +2 -1
- package/dist/core/action.js +93 -9
- package/dist/core/args.d.ts +50 -0
- package/dist/core/args.js +166 -0
- package/dist/core/discovery-mode.d.ts +37 -0
- package/dist/core/discovery-mode.js +54 -0
- package/dist/core/interaction.d.ts +3 -1
- package/dist/core/om.d.ts +11 -9
- package/dist/core/om.js +182 -13
- package/dist/core/schema-hint.d.ts +20 -0
- package/dist/core/schema-hint.js +73 -0
- package/dist/core/schema-json.d.ts +12 -0
- package/dist/core/schema-json.js +12 -0
- package/dist/core/types.d.ts +113 -1
- package/dist/foundation/LogEntry.d.ts +1 -1
- package/dist/foundation/canonicalPath.d.ts +26 -0
- package/dist/foundation/canonicalPath.js +34 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/mcp/action-host.d.ts +1 -0
- package/dist/mcp/action-host.js +65 -0
- package/dist/mcp/action-identity.d.ts +23 -0
- package/dist/mcp/action-identity.js +35 -0
- package/dist/mcp/progress.d.ts +61 -0
- package/dist/mcp/progress.js +92 -0
- package/dist/mcp/resources.d.ts +55 -0
- package/dist/mcp/resources.js +208 -0
- package/dist/mcp/runs.d.ts +37 -0
- package/dist/mcp/runs.js +60 -0
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +38 -0
- package/dist/mcp/tail.d.ts +37 -0
- package/dist/mcp/tail.js +64 -0
- package/dist/mcp/tools.d.ts +54 -0
- package/dist/mcp/tools.js +378 -0
- package/dist/mcp/validate.d.ts +14 -0
- package/dist/mcp/validate.js +54 -0
- package/dist/mcp/verdict.d.ts +31 -0
- package/dist/mcp/verdict.js +47 -0
- package/dist/output/artifact/ArtifactStore.d.ts +24 -0
- package/dist/output/artifact/ArtifactStore.js +49 -0
- package/dist/output/folder/RunFolder.d.ts +23 -1
- package/dist/output/folder/RunFolder.js +70 -11
- package/dist/output/writers/views.d.ts +10 -0
- package/dist/skill/file.d.ts +26 -0
- package/dist/skill/file.js +58 -0
- package/dist/skill/index.d.ts +12 -0
- package/dist/skill/index.js +10 -0
- package/dist/skill/model.d.ts +69 -0
- package/dist/skill/model.js +200 -0
- package/dist/skill/render.d.ts +18 -0
- package/dist/skill/render.js +159 -0
- package/dist/skill/root.d.ts +15 -0
- package/dist/skill/root.js +27 -0
- package/package.json +5 -2
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import type { JsonSchema } from "../core/schema-json.ts";
|
|
2
|
+
/** One step in an om's static outline: an imported call, with the tag authored on its chain. */
|
|
3
|
+
export interface OmCall {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** The `.tag("…")` literal from the same fluent chain, when the author wrote one. */
|
|
6
|
+
readonly tag?: string;
|
|
7
|
+
}
|
|
1
8
|
/** A runnable om discovered from a top-level `om("name", …)` call. */
|
|
2
9
|
export interface DiscoveredOm {
|
|
3
10
|
/** The om's name — its first string argument. */
|
|
@@ -6,6 +13,12 @@ export interface DiscoveredOm {
|
|
|
6
13
|
readonly file: string;
|
|
7
14
|
/** 1-based line of the `om(...)` call (informational). */
|
|
8
15
|
readonly line: number;
|
|
16
|
+
/**
|
|
17
|
+
* A static approximation of what the body calls, in source order. Absent when the walk
|
|
18
|
+
* found nothing recognisable. Never a contract — see {@link import("./outline.ts").outlineBody}
|
|
19
|
+
* for what it deliberately cannot see.
|
|
20
|
+
*/
|
|
21
|
+
readonly calls?: readonly OmCall[];
|
|
9
22
|
}
|
|
10
23
|
/** An inspectable action discovered from an exported `action("name")…` builder. */
|
|
11
24
|
export interface DiscoveredAction {
|
|
@@ -27,3 +40,49 @@ export interface Registry {
|
|
|
27
40
|
/** Non-fatal diagnostics (type errors, unresolved config) — discovery never throws for these. */
|
|
28
41
|
readonly warnings: string[];
|
|
29
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* What importing a file in discovery mode reveals — the metadata the AST scan cannot see,
|
|
45
|
+
* because `.describe()`, `.args()` and `.mcp()` only have values once the module has been
|
|
46
|
+
* evaluated. Produced by `discoverRegistrations`, never by `discover`.
|
|
47
|
+
*/
|
|
48
|
+
export interface Registration {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
/** Absolute path of the defining file. */
|
|
51
|
+
readonly file: string;
|
|
52
|
+
/** The `.describe({ summary })` line, when the author wrote one. */
|
|
53
|
+
readonly summary?: string;
|
|
54
|
+
/** Present only when the author called `.mcp()`. Absent ⇒ not exposed over MCP. */
|
|
55
|
+
readonly mcp?: {
|
|
56
|
+
readonly mode: "settling" | "long-lived";
|
|
57
|
+
};
|
|
58
|
+
/** The declared args as JSON Schema. Absent when none were declared. */
|
|
59
|
+
readonly inputSchema?: JsonSchema;
|
|
60
|
+
/** Why this entry cannot be called — currently only "its schema would not convert". */
|
|
61
|
+
readonly unavailable?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface OmRegistration extends Registration {
|
|
64
|
+
/**
|
|
65
|
+
* `<name>-<hash8>` — the run's log folder, derived from the name and the defining file
|
|
66
|
+
* by the same rule the runtime uses, so it is known before the om has ever run.
|
|
67
|
+
*/
|
|
68
|
+
readonly folderName: string;
|
|
69
|
+
}
|
|
70
|
+
export interface ActionRegistration extends Registration {
|
|
71
|
+
/** The exported binding (`export const <exportName> = action(...)`). */
|
|
72
|
+
readonly exportName: string;
|
|
73
|
+
}
|
|
74
|
+
/** The result of importing a project's candidate files in discovery mode. */
|
|
75
|
+
export interface RegistrationSet {
|
|
76
|
+
readonly oms: OmRegistration[];
|
|
77
|
+
readonly actions: ActionRegistration[];
|
|
78
|
+
/** Import failures and AST warnings — registration discovery never throws for these. */
|
|
79
|
+
readonly warnings: string[];
|
|
80
|
+
/**
|
|
81
|
+
* The AST scan that chose the candidate files, carried through so a caller wanting both
|
|
82
|
+
* halves — the outline and capability flags from the AST, the summaries and schemas from
|
|
83
|
+
* the fork — does not pay to build a second TypeScript program.
|
|
84
|
+
*
|
|
85
|
+
* Absent when the fork was driven with an explicit file list, since there was no scan.
|
|
86
|
+
*/
|
|
87
|
+
readonly registry?: Registry;
|
|
88
|
+
}
|
package/dist/client/runner.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { RunSession, RunOptions, InspectOptions } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The `tsx` loader, resolved to an absolute URL from omkit's own location. Resolving it here
|
|
4
|
+
* (not as the bare specifier `--import tsx`) keeps the child's `tsx` independent of its cwd —
|
|
5
|
+
* a run in any directory still finds the loader shipped with omkit.
|
|
6
|
+
*/
|
|
7
|
+
export declare const tsxLoader: string;
|
|
2
8
|
/**
|
|
3
9
|
* The inspector flag to prepend to a child's `execArgv` — empty when no inspector is requested.
|
|
4
10
|
* Plain `--inspect` opens the port without stopping, so the child still installs its teardown
|
|
@@ -10,6 +16,12 @@ export declare function inspectArgs(inspect: InspectOptions | undefined): string
|
|
|
10
16
|
* `OMKIT_SUPERVISED=1` flips the child into channel mode (see core/interaction.ts), and its
|
|
11
17
|
* IPC channel is wrapped into a {@link RunSession}. stdout/stderr are piped (not inherited)
|
|
12
18
|
* so the child never writes to the supervisor's terminal.
|
|
19
|
+
*
|
|
20
|
+
* The path is canonicalised first, and that is load-bearing rather than tidy — see
|
|
21
|
+
* {@link canonicalPath}. The child resolves `"omkit"` through `node_modules`, which is a
|
|
22
|
+
* symlink Node realpaths to the package's true on-disk name; forking with any other spelling
|
|
23
|
+
* of the same file gives the child a *second* copy of omkit, with its own module state. The
|
|
24
|
+
* run then exists in one copy while the om's actions look for it in the other.
|
|
13
25
|
*/
|
|
14
26
|
export declare function runOm(omFile: string, opts?: RunOptions): RunSession;
|
|
15
27
|
/**
|
package/dist/client/runner.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { fork } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { canonicalPath } from "../foundation/canonicalPath.js";
|
|
3
4
|
import { createChannel } from "./channel.js";
|
|
4
5
|
/**
|
|
5
6
|
* The `tsx` loader, resolved to an absolute URL from omkit's own location. Resolving it here
|
|
6
7
|
* (not as the bare specifier `--import tsx`) keeps the child's `tsx` independent of its cwd —
|
|
7
8
|
* a run in any directory still finds the loader shipped with omkit.
|
|
8
9
|
*/
|
|
9
|
-
const tsxLoader = import.meta.resolve("tsx");
|
|
10
|
+
export const tsxLoader = import.meta.resolve("tsx");
|
|
10
11
|
/**
|
|
11
12
|
* The inspector flag to prepend to a child's `execArgv` — empty when no inspector is requested.
|
|
12
13
|
* Plain `--inspect` opens the port without stopping, so the child still installs its teardown
|
|
@@ -22,12 +23,19 @@ export function inspectArgs(inspect) {
|
|
|
22
23
|
* `OMKIT_SUPERVISED=1` flips the child into channel mode (see core/interaction.ts), and its
|
|
23
24
|
* IPC channel is wrapped into a {@link RunSession}. stdout/stderr are piped (not inherited)
|
|
24
25
|
* so the child never writes to the supervisor's terminal.
|
|
26
|
+
*
|
|
27
|
+
* The path is canonicalised first, and that is load-bearing rather than tidy — see
|
|
28
|
+
* {@link canonicalPath}. The child resolves `"omkit"` through `node_modules`, which is a
|
|
29
|
+
* symlink Node realpaths to the package's true on-disk name; forking with any other spelling
|
|
30
|
+
* of the same file gives the child a *second* copy of omkit, with its own module state. The
|
|
31
|
+
* run then exists in one copy while the om's actions look for it in the other.
|
|
25
32
|
*/
|
|
26
33
|
export function runOm(omFile, opts = {}) {
|
|
27
|
-
const
|
|
34
|
+
const file = canonicalPath(omFile);
|
|
35
|
+
const child = fork(file, [], {
|
|
28
36
|
execArgv: [...inspectArgs(opts.inspect), "--import", tsxLoader],
|
|
29
|
-
cwd: opts.cwd ?? path.dirname(
|
|
30
|
-
env: { ...process.env, OMKIT_SUPERVISED: "1" },
|
|
37
|
+
cwd: opts.cwd ?? path.dirname(file),
|
|
38
|
+
env: { ...process.env, OMKIT_SUPERVISED: "1", ...opts.env },
|
|
31
39
|
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
32
40
|
});
|
|
33
41
|
const transport = {
|
|
@@ -57,11 +65,12 @@ export function runOm(omFile, opts = {}) {
|
|
|
57
65
|
export function spawnBare(omFile, opts = {}) {
|
|
58
66
|
// A bare run is unsupervised even if the parent process happens to carry the flag (e.g. a
|
|
59
67
|
// test runner spawned from inside a supervised run) — clear it so the child stays bare.
|
|
60
|
-
const env = { ...process.env };
|
|
68
|
+
const env = { ...process.env, ...opts.env };
|
|
61
69
|
delete env.OMKIT_SUPERVISED;
|
|
62
|
-
const
|
|
70
|
+
const file = canonicalPath(omFile);
|
|
71
|
+
const child = fork(file, [], {
|
|
63
72
|
execArgv: [...inspectArgs(opts.inspect), "--import", tsxLoader],
|
|
64
|
-
cwd: opts.cwd ?? path.dirname(
|
|
73
|
+
cwd: opts.cwd ?? path.dirname(file),
|
|
65
74
|
stdio: "inherit",
|
|
66
75
|
env,
|
|
67
76
|
});
|
package/dist/client/types.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { LogEntry } from "../foundation/LogEntry.ts";
|
|
2
2
|
import type { PromptSpec } from "../core/interaction.ts";
|
|
3
|
-
import type { Registry } from "./registry.ts";
|
|
3
|
+
import type { Registry, RegistrationSet } from "./registry.ts";
|
|
4
|
+
/**
|
|
5
|
+
* The config file omkit looks for when nobody names one — resolved against the working
|
|
6
|
+
* directory, which is why anything generated for a reader elsewhere has to say so explicitly.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_TSCONFIG = "tsconfig.omkit.json";
|
|
4
9
|
/** A run's terminal verdict, resolved when the child settles. */
|
|
5
10
|
export interface Verdict {
|
|
6
11
|
readonly ok: boolean;
|
|
@@ -27,6 +32,8 @@ export interface RunOptions {
|
|
|
27
32
|
readonly cwd?: string;
|
|
28
33
|
/** When set, fork the child with the Node inspector open so a debugger can attach. */
|
|
29
34
|
readonly inspect?: InspectOptions;
|
|
35
|
+
/** Extra environment for the child, merged over the parent's — how `OMKIT_ARGS` travels. */
|
|
36
|
+
readonly env?: Record<string, string>;
|
|
30
37
|
}
|
|
31
38
|
/** A prompt the running om is waiting on — surfaced to the frontend, answered via `answer`. */
|
|
32
39
|
export interface PromptRequest {
|
|
@@ -57,6 +64,12 @@ export interface OmkitClient {
|
|
|
57
64
|
readonly tsconfig: string;
|
|
58
65
|
/** Statically scan the project's `tsconfig.omkit.json` for oms and actions. */
|
|
59
66
|
discover(): Promise<Registry>;
|
|
67
|
+
/**
|
|
68
|
+
* Read what the project's oms and actions declare, by importing them in a throwaway
|
|
69
|
+
* child. Unlike {@link OmkitClient.discover} this executes user code, so it is never on
|
|
70
|
+
* the `omkit ls` path — only a caller that needs real schemas should ask for it.
|
|
71
|
+
*/
|
|
72
|
+
discoverRegistrations(): Promise<RegistrationSet>;
|
|
60
73
|
/** Spawn an om file as a supervised child and return its live session (the Ink UI drives this). */
|
|
61
74
|
run(omFile: string, opts?: RunOptions): RunSession;
|
|
62
75
|
/** Spawn an om file bare — inherited stdio, no supervision — and resolve its exit code. */
|
package/dist/client/types.js
CHANGED
package/dist/core/ActionRun.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type EventHandler } from "../foundation/events.ts";
|
|
|
2
2
|
import type { ActionRef } from "../foundation/ActionRef.ts";
|
|
3
3
|
import type { LogStore } from "../output/log/LogStore.ts";
|
|
4
4
|
import type { SnapshotStore } from "../output/snapshot/SnapshotStore.ts";
|
|
5
|
+
import type { ArtifactStore } from "../output/artifact/ArtifactStore.ts";
|
|
5
6
|
import type { Activity, Exec, InstanceEvents, NodeStatus } from "./types.ts";
|
|
6
7
|
/** Everything a node needs to exist, minus its behavior. */
|
|
7
8
|
export interface NodeInit {
|
|
@@ -19,6 +20,8 @@ export interface NodeInit {
|
|
|
19
20
|
readonly parentSignal: AbortSignal | null;
|
|
20
21
|
/** The run's snapshot store — backs `ctx.snapshot`. */
|
|
21
22
|
readonly snapshots: SnapshotStore;
|
|
23
|
+
/** The run's artifact registry — backs `ctx.artifact`. */
|
|
24
|
+
readonly artifacts: ArtifactStore;
|
|
22
25
|
/** Absolute path to the run's output folder — backs `ctx.artifactsFolder`. */
|
|
23
26
|
readonly artifactsFolder: string;
|
|
24
27
|
/** Report an assertion outcome to the run verdict. */
|
|
@@ -54,6 +57,7 @@ export declare class ActionRun<Result = unknown, Events extends object = object,
|
|
|
54
57
|
duration: number;
|
|
55
58
|
private readonly store;
|
|
56
59
|
private readonly snapshots;
|
|
60
|
+
private readonly artifacts;
|
|
57
61
|
private readonly artifactsFolder;
|
|
58
62
|
private readonly onAssert;
|
|
59
63
|
private readonly onUnhandledFailure;
|
|
@@ -101,6 +105,7 @@ export declare class ActionRun<Result = unknown, Events extends object = object,
|
|
|
101
105
|
private context;
|
|
102
106
|
private assertInvariant;
|
|
103
107
|
private captureSnapshot;
|
|
108
|
+
private registerArtifact;
|
|
104
109
|
private attach;
|
|
105
110
|
private succeed;
|
|
106
111
|
private fail;
|
package/dist/core/ActionRun.js
CHANGED
|
@@ -30,6 +30,7 @@ export class ActionRun {
|
|
|
30
30
|
duration = 0;
|
|
31
31
|
store;
|
|
32
32
|
snapshots;
|
|
33
|
+
artifacts;
|
|
33
34
|
artifactsFolder;
|
|
34
35
|
onAssert;
|
|
35
36
|
onUnhandledFailure;
|
|
@@ -59,6 +60,7 @@ export class ActionRun {
|
|
|
59
60
|
this.parentId = init.parentId;
|
|
60
61
|
this.store = init.store;
|
|
61
62
|
this.snapshots = init.snapshots;
|
|
63
|
+
this.artifacts = init.artifacts;
|
|
62
64
|
this.artifactsFolder = init.artifactsFolder;
|
|
63
65
|
this.onAssert = init.onAssert;
|
|
64
66
|
this.onUnhandledFailure = init.onUnhandledFailure;
|
|
@@ -228,6 +230,7 @@ export class ActionRun {
|
|
|
228
230
|
tag: (name) => void this.tag(name),
|
|
229
231
|
assert: (condition, message) => this.assertInvariant(condition, message),
|
|
230
232
|
snapshot: (name, value) => this.captureSnapshot(name, value),
|
|
233
|
+
artifact: (name, file, opts) => this.registerArtifact(name, file, opts),
|
|
231
234
|
artifactsFolder: this.artifactsFolder,
|
|
232
235
|
proc: createProc((source, level, message) => this.store.append({ nodeId: this.id, path: this.path, level, source, message }), this.signal),
|
|
233
236
|
};
|
|
@@ -244,6 +247,12 @@ export class ActionRun {
|
|
|
244
247
|
this.bubble(`📸 ${name} → ${file}`);
|
|
245
248
|
return file;
|
|
246
249
|
}
|
|
250
|
+
registerArtifact(name, file, opts) {
|
|
251
|
+
const record = this.artifacts.register(name, file, this.id, opts);
|
|
252
|
+
this.log("artifact", "artifact", `${name} → ${record.file}`);
|
|
253
|
+
this.bubble(`📎 ${name} → ${record.file}`);
|
|
254
|
+
return record.file;
|
|
255
|
+
}
|
|
247
256
|
attach(handle) {
|
|
248
257
|
if (this.attached)
|
|
249
258
|
return;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { LogStore } from "../output/log/LogStore.ts";
|
|
2
2
|
import { RunFolder } from "../output/folder/RunFolder.ts";
|
|
3
|
+
import { type ArtifactRecord } from "../output/artifact/ArtifactStore.ts";
|
|
3
4
|
import type { RunView } from "../output/writers/views.ts";
|
|
4
5
|
import { ActionRun } from "./ActionRun.ts";
|
|
5
|
-
import type { Exec, LaunchSpec } from "./types.ts";
|
|
6
|
+
import type { Exec, LaunchSpec, OmDescription, ResolvedMcpExposure } from "./types.ts";
|
|
7
|
+
import type { LogEntry } from "../foundation/LogEntry.ts";
|
|
6
8
|
/**
|
|
7
9
|
* The run: a module singleton that owns the log store, the run folder, the node
|
|
8
10
|
* registry, and the process-wide teardown. `launch` creates a child node under the
|
|
@@ -20,9 +22,22 @@ export declare class ExecutionTree {
|
|
|
20
22
|
readonly store: LogStore;
|
|
21
23
|
readonly folder: RunFolder;
|
|
22
24
|
readonly root: ActionRun;
|
|
25
|
+
/**
|
|
26
|
+
* The om's `.describe(…)` summary, carried here from the builder so it survives the
|
|
27
|
+
* terminal `.run(...)` call. Stored so it is retrievable; nothing reads it yet.
|
|
28
|
+
*/
|
|
29
|
+
readonly description: OmDescription | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* The om's `.mcp(…)` exposure, carried here from the builder for the same reason
|
|
32
|
+
* `description` is: `.run(...)` is the terminal call, and an om has no definition object
|
|
33
|
+
* to hang it on. The MCP server reads its own copy from the discovery fork, not from
|
|
34
|
+
* here — this is what makes the carry observable, and therefore testable, at all.
|
|
35
|
+
*/
|
|
36
|
+
readonly mcp: ResolvedMcpExposure | undefined;
|
|
23
37
|
private readonly rawStream;
|
|
24
38
|
private readonly consoleCapture;
|
|
25
39
|
private readonly snapshotStore;
|
|
40
|
+
private readonly artifactStore;
|
|
26
41
|
private readonly registry;
|
|
27
42
|
private readonly unsettled;
|
|
28
43
|
private readonly originalLog;
|
|
@@ -31,6 +46,7 @@ export declare class ExecutionTree {
|
|
|
31
46
|
private forwarding;
|
|
32
47
|
private assertionsPassed;
|
|
33
48
|
private assertionsFailed;
|
|
49
|
+
private rootArgs;
|
|
34
50
|
private readonly faults;
|
|
35
51
|
static graceMs: number;
|
|
36
52
|
private phase;
|
|
@@ -40,7 +56,9 @@ export declare class ExecutionTree {
|
|
|
40
56
|
private readonly onSigint;
|
|
41
57
|
private readonly onUncaught;
|
|
42
58
|
private readonly onUnhandled;
|
|
43
|
-
constructor(name: string, definedAt?: string);
|
|
59
|
+
constructor(name: string, definedAt?: string, description?: OmDescription, mcp?: ResolvedMcpExposure);
|
|
60
|
+
/** Record the run's resolved args, so the root's log header reports them. */
|
|
61
|
+
setRootArgs(args: unknown): void;
|
|
44
62
|
/** Launch an instance under the ambient current node (or the root). */
|
|
45
63
|
launch(spec: LaunchSpec): ActionRun;
|
|
46
64
|
private append;
|
|
@@ -83,7 +101,27 @@ export declare class ExecutionTree {
|
|
|
83
101
|
private nodeView;
|
|
84
102
|
/** Test seam: the projected run view (same object written to result.json). */
|
|
85
103
|
runViewForTest(): RunView;
|
|
104
|
+
/** Test seam: the run's curated artifacts. */
|
|
105
|
+
artifactsForTest(): readonly ArtifactRecord[];
|
|
106
|
+
/** Test seam: every log entry recorded for this run. */
|
|
107
|
+
entriesForTest(): readonly LogEntry[];
|
|
86
108
|
private runView;
|
|
109
|
+
/**
|
|
110
|
+
* The run's curated artifacts, keyed by `(nodeId, name)`: one action re-registering
|
|
111
|
+
* a name (e.g. re-labelling the same file after regenerating it) updates that entry
|
|
112
|
+
* in place rather than appending a second one, so `result.json` always reflects the
|
|
113
|
+
* latest registration per name. Keying on the pair — not `name` alone — matters
|
|
114
|
+
* because two *different* actions can independently choose the same name (two
|
|
115
|
+
* screenshot steps both calling `ctx.artifact("screenshot", …)`); those describe two
|
|
116
|
+
* distinct files and both must survive into `result.json`, not collapse into one.
|
|
117
|
+
* Position follows first registration; `artifacts.log` (a chronological rollup, like
|
|
118
|
+
* `events.log`/`asserts.log`) is unaffected either way and still lists every call.
|
|
119
|
+
*
|
|
120
|
+
* The pair is joined with `\0` — the same separator `omHash` uses, and for the same
|
|
121
|
+
* reason: a step name may legitimately contain colons (`TSKB:root:watch:docs`) and an
|
|
122
|
+
* artifact name is free-form, so any printable separator can be forged inside a key.
|
|
123
|
+
*/
|
|
124
|
+
private curatedArtifacts;
|
|
87
125
|
/**
|
|
88
126
|
* Absolute `.log` path for a node — root is `main.log`, children drop the `main/`
|
|
89
127
|
* prefix. Each id segment is sanitized so names with filesystem-illegal characters
|
|
@@ -7,6 +7,7 @@ import { LogStore } from "../output/log/LogStore.js";
|
|
|
7
7
|
import { RunFolder } from "../output/folder/RunFolder.js";
|
|
8
8
|
import { RawStream } from "../output/log/RawStream.js";
|
|
9
9
|
import { SnapshotStore } from "../output/snapshot/SnapshotStore.js";
|
|
10
|
+
import { ArtifactStore } from "../output/artifact/ArtifactStore.js";
|
|
10
11
|
import { ConsoleCapture } from "../output/console/ConsoleCapture.js";
|
|
11
12
|
import { LiveRenderer } from "../output/LiveRenderer.js";
|
|
12
13
|
import { writeNodeLogs } from "../output/writers/NodeLogWriter.js";
|
|
@@ -40,9 +41,22 @@ export class ExecutionTree {
|
|
|
40
41
|
store = new LogStore();
|
|
41
42
|
folder;
|
|
42
43
|
root;
|
|
44
|
+
/**
|
|
45
|
+
* The om's `.describe(…)` summary, carried here from the builder so it survives the
|
|
46
|
+
* terminal `.run(...)` call. Stored so it is retrievable; nothing reads it yet.
|
|
47
|
+
*/
|
|
48
|
+
description;
|
|
49
|
+
/**
|
|
50
|
+
* The om's `.mcp(…)` exposure, carried here from the builder for the same reason
|
|
51
|
+
* `description` is: `.run(...)` is the terminal call, and an om has no definition object
|
|
52
|
+
* to hang it on. The MCP server reads its own copy from the discovery fork, not from
|
|
53
|
+
* here — this is what makes the carry observable, and therefore testable, at all.
|
|
54
|
+
*/
|
|
55
|
+
mcp;
|
|
43
56
|
rawStream;
|
|
44
57
|
consoleCapture;
|
|
45
58
|
snapshotStore;
|
|
59
|
+
artifactStore = new ArtifactStore();
|
|
46
60
|
registry = [];
|
|
47
61
|
unsettled = new Set();
|
|
48
62
|
originalLog = console.log.bind(console);
|
|
@@ -51,6 +65,10 @@ export class ExecutionTree {
|
|
|
51
65
|
forwarding;
|
|
52
66
|
assertionsPassed = 0;
|
|
53
67
|
assertionsFailed = 0;
|
|
68
|
+
// The root's args are resolved *inside* its body (prompting is async), so unlike a
|
|
69
|
+
// child node's they cannot be handed to `run()` up front — they are recorded here
|
|
70
|
+
// instead and projected onto the root by `nodeView`.
|
|
71
|
+
rootArgs = [];
|
|
54
72
|
faults = [];
|
|
55
73
|
static graceMs = 5000; // teardown waits this long for nodes to settle, then finalizes anyway
|
|
56
74
|
phase = "open";
|
|
@@ -65,7 +83,9 @@ export class ExecutionTree {
|
|
|
65
83
|
// Process hooks — installed at run start, removed at finalize (they must not outlive the run).
|
|
66
84
|
onUncaught = (e) => this.onFatal("uncaughtException", e);
|
|
67
85
|
onUnhandled = (e) => this.onFatal("unhandledRejection", e);
|
|
68
|
-
constructor(name, definedAt) {
|
|
86
|
+
constructor(name, definedAt, description, mcp) {
|
|
87
|
+
this.description = description;
|
|
88
|
+
this.mcp = mcp;
|
|
69
89
|
// The run's identity: its name + where om() is written (not how it was launched),
|
|
70
90
|
// so same-named oms in different files get distinct log folders.
|
|
71
91
|
this.folder = new RunFolder(name, omHash(name, siteFile(definedAt)));
|
|
@@ -87,6 +107,10 @@ export class ExecutionTree {
|
|
|
87
107
|
this.consoleCapture = new ConsoleCapture(this.store, () => (currentNode.getStore() ?? this.root).toRef());
|
|
88
108
|
ExecutionTree.last = this;
|
|
89
109
|
}
|
|
110
|
+
/** Record the run's resolved args, so the root's log header reports them. */
|
|
111
|
+
setRootArgs(args) {
|
|
112
|
+
this.rootArgs = [args];
|
|
113
|
+
}
|
|
90
114
|
/** Launch an instance under the ambient current node (or the root). */
|
|
91
115
|
launch(spec) {
|
|
92
116
|
if (this.phase !== "open") {
|
|
@@ -137,6 +161,7 @@ export class ExecutionTree {
|
|
|
137
161
|
return {
|
|
138
162
|
store: this.store,
|
|
139
163
|
snapshots: this.snapshotStore,
|
|
164
|
+
artifacts: this.artifactStore,
|
|
140
165
|
artifactsFolder: this.folder.path(),
|
|
141
166
|
onAssert: (pass, actionPath, message) => this.recordAssert(pass, actionPath, message),
|
|
142
167
|
onUnhandledFailure: (actionPath, error) => {
|
|
@@ -276,6 +301,7 @@ export class ExecutionTree {
|
|
|
276
301
|
await writeRollup(at("events.log"), flat, entries, (e) => e.level === "event");
|
|
277
302
|
await writeRollup(at("asserts.log"), flat, entries, (e) => e.level === "assert", assertSummary);
|
|
278
303
|
await writeRollup(at("snapshots.log"), flat, entries, (e) => e.level === "snapshot");
|
|
304
|
+
await writeRollup(at("artifacts.log"), flat, entries, (e) => e.level === "artifact");
|
|
279
305
|
await writeResult(at("result.json"), this.runView());
|
|
280
306
|
// Only now let go of the process hooks — a Ctrl+C during the writes must still be
|
|
281
307
|
// caught (as a no-op) so it can't fall through to Node's kill-without-log default.
|
|
@@ -306,6 +332,7 @@ export class ExecutionTree {
|
|
|
306
332
|
` events → ${at("events.log")}`,
|
|
307
333
|
` asserts → ${at("asserts.log")} ${assertSummary}`,
|
|
308
334
|
` snapshots → ${at("snapshots.log")}`,
|
|
335
|
+
` artifacts → ${at("artifacts.log")}`,
|
|
309
336
|
];
|
|
310
337
|
}
|
|
311
338
|
/** The recap to the terminal (files are already written). */
|
|
@@ -327,7 +354,8 @@ export class ExecutionTree {
|
|
|
327
354
|
path: node.path,
|
|
328
355
|
parentId: node.parentId,
|
|
329
356
|
tags: [...node.tags],
|
|
330
|
-
args
|
|
357
|
+
// The root never has args passed to `run()`; its own are resolved mid-body.
|
|
358
|
+
args: [...(node.parentId === null ? this.rootArgs : node.args)],
|
|
331
359
|
status: node.status,
|
|
332
360
|
startedAt: node.startedAt,
|
|
333
361
|
endedAt: node.endedAt,
|
|
@@ -340,6 +368,14 @@ export class ExecutionTree {
|
|
|
340
368
|
runViewForTest() {
|
|
341
369
|
return this.runView();
|
|
342
370
|
}
|
|
371
|
+
/** Test seam: the run's curated artifacts. */
|
|
372
|
+
artifactsForTest() {
|
|
373
|
+
return this.artifactStore.all();
|
|
374
|
+
}
|
|
375
|
+
/** Test seam: every log entry recorded for this run. */
|
|
376
|
+
entriesForTest() {
|
|
377
|
+
return this.store.entries();
|
|
378
|
+
}
|
|
343
379
|
runView() {
|
|
344
380
|
return {
|
|
345
381
|
ok: this.faults.length === 0,
|
|
@@ -350,8 +386,36 @@ export class ExecutionTree {
|
|
|
350
386
|
duration: this.root.duration,
|
|
351
387
|
rawStream: this.rawStreamPath(),
|
|
352
388
|
root: this.nodeView(this.root),
|
|
389
|
+
artifacts: this.curatedArtifacts(),
|
|
353
390
|
};
|
|
354
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* The run's curated artifacts, keyed by `(nodeId, name)`: one action re-registering
|
|
394
|
+
* a name (e.g. re-labelling the same file after regenerating it) updates that entry
|
|
395
|
+
* in place rather than appending a second one, so `result.json` always reflects the
|
|
396
|
+
* latest registration per name. Keying on the pair — not `name` alone — matters
|
|
397
|
+
* because two *different* actions can independently choose the same name (two
|
|
398
|
+
* screenshot steps both calling `ctx.artifact("screenshot", …)`); those describe two
|
|
399
|
+
* distinct files and both must survive into `result.json`, not collapse into one.
|
|
400
|
+
* Position follows first registration; `artifacts.log` (a chronological rollup, like
|
|
401
|
+
* `events.log`/`asserts.log`) is unaffected either way and still lists every call.
|
|
402
|
+
*
|
|
403
|
+
* The pair is joined with `\0` — the same separator `omHash` uses, and for the same
|
|
404
|
+
* reason: a step name may legitimately contain colons (`TSKB:root:watch:docs`) and an
|
|
405
|
+
* artifact name is free-form, so any printable separator can be forged inside a key.
|
|
406
|
+
*/
|
|
407
|
+
curatedArtifacts() {
|
|
408
|
+
const byKey = new Map();
|
|
409
|
+
for (const { name, file, description, mime, nodeId } of this.artifactStore.all()) {
|
|
410
|
+
byKey.set(`${nodeId}\0${name}`, {
|
|
411
|
+
name,
|
|
412
|
+
file,
|
|
413
|
+
mime,
|
|
414
|
+
...(description === undefined ? {} : { description }),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return [...byKey.values()];
|
|
418
|
+
}
|
|
355
419
|
/**
|
|
356
420
|
* Absolute `.log` path for a node — root is `main.log`, children drop the `main/`
|
|
357
421
|
* prefix. Each id segment is sanitized so names with filesystem-illegal characters
|
package/dist/core/action.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ActionBuilderEvents, NoEvents } from "./types.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Define an action. Provide the impl via `.run(…)`, optionally after declaring
|
|
4
|
-
*
|
|
4
|
+
* metadata (`.describe(…)`), events (`.emits<…>()`), an imperative handle
|
|
5
|
+
* (`.ref<…>()`), and/or pinning the args type (`.args(schema)`). Call the result
|
|
5
6
|
* to launch it.
|
|
6
7
|
*/
|
|
7
8
|
export declare function action(name: string): ActionBuilderEvents<NoEvents, void>;
|
package/dist/core/action.js
CHANGED
|
@@ -1,36 +1,120 @@
|
|
|
1
1
|
import { ExecutionTree } from "./ExecutionTree.js";
|
|
2
2
|
import { callerSite } from "../foundation/callsite.js";
|
|
3
|
+
import { describeSchema } from "./discovery-mode.js";
|
|
3
4
|
/**
|
|
4
|
-
* The builder returned by `action(name)`: declare
|
|
5
|
-
* imperative handle (`.ref<…>()`),
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The builder returned by `action(name)`: declare metadata (`.describe(…)`), events
|
|
6
|
+
* (`.emits<…>()`), an imperative handle (`.ref<…>()`), and/or pin the args type
|
|
7
|
+
* (`.args(schema)`), then bind the impl with `.run(…)`. `.run` returns a callable
|
|
8
|
+
* {@link Action} — **invoking it launches** the action under the ambient current node
|
|
9
|
+
* and returns the live {@link Activity}, on which `withCache`/`tag`/… chain.
|
|
8
10
|
*/
|
|
9
11
|
class Builder {
|
|
10
12
|
name;
|
|
11
|
-
|
|
13
|
+
description;
|
|
14
|
+
exposure;
|
|
15
|
+
constructor(name,
|
|
16
|
+
/** Carried across `.emits()`/`.ref()`, which otherwise rebuild the builder. */
|
|
17
|
+
description = undefined,
|
|
18
|
+
/** Carried the same way, for the same reason. */
|
|
19
|
+
exposure = undefined) {
|
|
12
20
|
this.name = name;
|
|
21
|
+
this.description = description;
|
|
22
|
+
this.exposure = exposure;
|
|
23
|
+
}
|
|
24
|
+
describe(description) {
|
|
25
|
+
this.description = description;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
mcp(exposure = {}) {
|
|
29
|
+
// The default is filled here, not at the reader, so every consumer sees one shape.
|
|
30
|
+
this.exposure = { mode: exposure.mode ?? "settling" };
|
|
31
|
+
return this;
|
|
13
32
|
}
|
|
14
33
|
emits() {
|
|
15
|
-
return new Builder(this.name);
|
|
34
|
+
return new Builder(this.name, this.description, this.exposure);
|
|
16
35
|
}
|
|
17
36
|
ref() {
|
|
18
|
-
return new Builder(this.name);
|
|
37
|
+
return new Builder(this.name, this.description, this.exposure);
|
|
38
|
+
}
|
|
39
|
+
args(schema) {
|
|
40
|
+
return new ArgsBuilder(this.name, schema, this.description, this.exposure);
|
|
19
41
|
}
|
|
20
42
|
run(body) {
|
|
21
43
|
const name = this.name;
|
|
44
|
+
// Carried onto the definition for `omkit ls` and the MCP server; not read by the runtime.
|
|
45
|
+
const description = this.description;
|
|
46
|
+
const mcp = this.exposure;
|
|
22
47
|
const definedAt = callerSite(); // the `.run(...)` call site — where this action lives
|
|
48
|
+
// No `.args()` on this path, so there is nothing to describe.
|
|
49
|
+
const describeArgs = () => describeSchema(undefined);
|
|
50
|
+
const exec = body;
|
|
51
|
+
const create = (...args) => {
|
|
52
|
+
const spec = { name, args, tags: [], body: exec, definedAt };
|
|
53
|
+
return ExecutionTree.require().launch(spec);
|
|
54
|
+
};
|
|
55
|
+
return Object.assign(create, {
|
|
56
|
+
actionName: name,
|
|
57
|
+
definedAt,
|
|
58
|
+
description,
|
|
59
|
+
mcp,
|
|
60
|
+
describeArgs,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The builder after `.args(schema)`. The schema pins `.run`'s parameter type; it is not
|
|
66
|
+
* validated here — an action invoked from an om body is passed its args directly in code.
|
|
67
|
+
* (Spec B's MCP server validates before it ever reaches this point.)
|
|
68
|
+
*/
|
|
69
|
+
class ArgsBuilder {
|
|
70
|
+
name;
|
|
71
|
+
schema;
|
|
72
|
+
description;
|
|
73
|
+
exposure;
|
|
74
|
+
constructor(name,
|
|
75
|
+
/** Type-level only: `S` pins `.run`'s parameter. Nothing reads the value at runtime. */
|
|
76
|
+
schema, description, exposure) {
|
|
77
|
+
this.name = name;
|
|
78
|
+
this.schema = schema;
|
|
79
|
+
this.description = description;
|
|
80
|
+
this.exposure = exposure;
|
|
81
|
+
}
|
|
82
|
+
describe(description) {
|
|
83
|
+
this.description = description;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
mcp(exposure = {}) {
|
|
87
|
+
this.exposure = { mode: exposure.mode ?? "settling" };
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
run(body) {
|
|
91
|
+
const name = this.name;
|
|
92
|
+
// Carried onto the definition for `omkit ls` and the MCP server; not read by the runtime.
|
|
93
|
+
const description = this.description;
|
|
94
|
+
const mcp = this.exposure;
|
|
95
|
+
const definedAt = callerSite();
|
|
96
|
+
// The schema is captured so the conversion happens in *this* copy of omkit — the one
|
|
97
|
+
// that built it — rather than in whichever copy the discovery child is running.
|
|
98
|
+
const schema = this.schema;
|
|
99
|
+
const describeArgs = () => describeSchema(schema);
|
|
23
100
|
const exec = body;
|
|
24
101
|
const create = (...args) => {
|
|
25
102
|
const spec = { name, args, tags: [], body: exec, definedAt };
|
|
26
103
|
return ExecutionTree.require().launch(spec);
|
|
27
104
|
};
|
|
28
|
-
return Object.assign(create, {
|
|
105
|
+
return Object.assign(create, {
|
|
106
|
+
actionName: name,
|
|
107
|
+
definedAt,
|
|
108
|
+
description,
|
|
109
|
+
mcp,
|
|
110
|
+
describeArgs,
|
|
111
|
+
});
|
|
29
112
|
}
|
|
30
113
|
}
|
|
31
114
|
/**
|
|
32
115
|
* Define an action. Provide the impl via `.run(…)`, optionally after declaring
|
|
33
|
-
*
|
|
116
|
+
* metadata (`.describe(…)`), events (`.emits<…>()`), an imperative handle
|
|
117
|
+
* (`.ref<…>()`), and/or pinning the args type (`.args(schema)`). Call the result
|
|
34
118
|
* to launch it.
|
|
35
119
|
*/
|
|
36
120
|
export function action(name) {
|