omkit 0.4.7 → 0.8.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/CHANGELOG.md +153 -0
- package/README.md +170 -13
- package/dist/actions/browser.d.ts +1 -1
- package/dist/actions/browser.js +1 -1
- package/dist/actions/chrome-page.d.ts +46 -4
- package/dist/actions/chrome-page.js +81 -33
- package/dist/actions/drive-page.d.ts +50 -0
- package/dist/actions/drive-page.js +68 -0
- package/dist/actions/index.d.ts +7 -3
- package/dist/actions/index.js +3 -1
- package/dist/actions/port-free.d.ts +29 -0
- package/dist/actions/port-free.js +78 -0
- package/dist/actions/prompt.d.ts +32 -5
- package/dist/actions/prompt.js +92 -8
- package/dist/cli/commands/help.js +6 -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 +17 -0
- package/dist/cli/commands/skill.js +56 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +71 -29
- 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 +3 -1
- 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 +9 -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 +374 -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 +55 -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 +11 -0
- package/dist/skill/index.js +9 -0
- package/dist/skill/model.d.ts +55 -0
- package/dist/skill/model.js +116 -0
- package/dist/skill/render.d.ts +18 -0
- package/dist/skill/render.js +133 -0
- package/package.json +5 -2
package/dist/core/om.js
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
import { ExecutionTree } from "./ExecutionTree.js";
|
|
2
|
-
import { callerSite } from "../foundation/callsite.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* ambient scope, so any action call / `step(...)` / `console.*` it reaches
|
|
9
|
-
* attributes correctly. Resolves once the run has torn down and produced its
|
|
10
|
-
* artifacts; never rejects (failures are recorded in the tree and set the exit code).
|
|
11
|
-
*/
|
|
12
|
-
export function om(name, body) {
|
|
2
|
+
import { callerSite, siteFile } from "../foundation/callsite.js";
|
|
3
|
+
import { describeSchema, isDiscovering, reportOm } from "./discovery-mode.js";
|
|
4
|
+
import { activeSupervisor } from "./interaction.js";
|
|
5
|
+
import { jsonAnswerComplete, resolveArgs } from "./args.js";
|
|
6
|
+
import { OPAQUE } from "./schema-hint.js";
|
|
7
|
+
function assertName(name) {
|
|
13
8
|
if (typeof name !== "string" || name.trim() === "") {
|
|
14
|
-
throw new Error("om(name
|
|
9
|
+
throw new Error("om(name) requires a non-empty name — it identifies the run in logs/");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Start the run: build the tree with the captured site and drive the body as the root. */
|
|
13
|
+
function launch(name, site, description, exposure, body, schema) {
|
|
14
|
+
// Discovery: report what this om declares and start nothing.
|
|
15
|
+
//
|
|
16
|
+
// The guard belongs *here*, at the single choke point both `run()` methods funnel
|
|
17
|
+
// through, for two reasons. `OmBuilderArgs.run` resolves args inside the run body, so a
|
|
18
|
+
// guard one level up would reach `resolveArgs` first and prompt into a child nobody can
|
|
19
|
+
// answer (or throw MissingArgsError) — neither is a discovery outcome. And
|
|
20
|
+
// `new ExecutionTree(...)` creates the run's log folder in its constructor, so the
|
|
21
|
+
// guard has to precede it or discovery litters `logs/` with empty runs.
|
|
22
|
+
if (isDiscovering()) {
|
|
23
|
+
const { inputSchema, schemaError } = describeSchema(schema);
|
|
24
|
+
reportOm({
|
|
25
|
+
kind: "om-registration",
|
|
26
|
+
name,
|
|
27
|
+
file: siteFile(site),
|
|
28
|
+
description,
|
|
29
|
+
mcp: exposure,
|
|
30
|
+
inputSchema,
|
|
31
|
+
schemaError,
|
|
32
|
+
});
|
|
33
|
+
return Promise.resolve();
|
|
15
34
|
}
|
|
16
35
|
if (ExecutionTree.current) {
|
|
17
36
|
throw new Error("an om() run is already active in this process");
|
|
18
37
|
}
|
|
19
|
-
const tree = new ExecutionTree(name,
|
|
38
|
+
const tree = new ExecutionTree(name, site, description, exposure);
|
|
20
39
|
ExecutionTree.current = tree;
|
|
21
40
|
const rootBody = (ctx) => body({
|
|
22
41
|
signal: ctx.signal,
|
|
@@ -25,7 +44,157 @@ export function om(name, body) {
|
|
|
25
44
|
cancel: () => tree.cancel(),
|
|
26
45
|
assert: ctx.assert,
|
|
27
46
|
snapshot: ctx.snapshot,
|
|
47
|
+
artifact: ctx.artifact,
|
|
28
48
|
artifactsFolder: ctx.artifactsFolder,
|
|
29
49
|
});
|
|
30
50
|
return tree.runRoot(rootBody);
|
|
31
51
|
}
|
|
52
|
+
class Builder {
|
|
53
|
+
name;
|
|
54
|
+
site;
|
|
55
|
+
/** Carried into `ArgsBuilder` by `.args()`, which otherwise rebuilds the builder. */
|
|
56
|
+
description;
|
|
57
|
+
/** Carried the same way, for the same reason — `.args()` returns a new object. */
|
|
58
|
+
exposure;
|
|
59
|
+
constructor(name,
|
|
60
|
+
/** Captured in `om()`, not here — the defining file must be the caller's, not om.ts. */
|
|
61
|
+
site) {
|
|
62
|
+
this.name = name;
|
|
63
|
+
this.site = site;
|
|
64
|
+
}
|
|
65
|
+
describe(description) {
|
|
66
|
+
this.description = description;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
mcp(exposure = {}) {
|
|
70
|
+
// The default is filled here, not at the reader, so every consumer sees one shape.
|
|
71
|
+
this.exposure = { mode: exposure.mode ?? "settling" };
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
args(schema) {
|
|
75
|
+
return new ArgsBuilder(this.name, this.site, schema, this.description, this.exposure);
|
|
76
|
+
}
|
|
77
|
+
run(body) {
|
|
78
|
+
// Carried onto the run for `omkit ls` and the MCP server; not read by the runtime yet.
|
|
79
|
+
return launch(this.name, this.site, this.description, this.exposure, body);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The builder after `.args(schema)`. Unlike an action's, the schema here is live: `.run`
|
|
84
|
+
* resolves it before calling the body, so an om declares what it needs and omkit fills it
|
|
85
|
+
* in. The site is the one `om()` captured — threaded through, never re-taken.
|
|
86
|
+
*/
|
|
87
|
+
class ArgsBuilder {
|
|
88
|
+
name;
|
|
89
|
+
site;
|
|
90
|
+
schema;
|
|
91
|
+
description;
|
|
92
|
+
exposure;
|
|
93
|
+
constructor(name, site, schema, description, exposure) {
|
|
94
|
+
this.name = name;
|
|
95
|
+
this.site = site;
|
|
96
|
+
this.schema = schema;
|
|
97
|
+
this.description = description;
|
|
98
|
+
this.exposure = exposure;
|
|
99
|
+
}
|
|
100
|
+
describe(description) {
|
|
101
|
+
this.description = description;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
mcp(exposure = {}) {
|
|
105
|
+
this.exposure = { mode: exposure.mode ?? "settling" };
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
run(body) {
|
|
109
|
+
// Resolution happens inside the run, not before it: prompting is async, and the run
|
|
110
|
+
// must already exist for the prompt and its answer to land on the timeline. A failure
|
|
111
|
+
// to resolve is therefore an ordinary failure of the root node — logged, and the run
|
|
112
|
+
// is marked failed — rather than a throw out of `om(...)`.
|
|
113
|
+
return launch(this.name, this.site, this.description, this.exposure, async (ctx) => {
|
|
114
|
+
const args = (await resolveArgs(this.schema, {
|
|
115
|
+
supplied: parseSuppliedArgs(),
|
|
116
|
+
interactive: isInteractive(),
|
|
117
|
+
ask: askForArg,
|
|
118
|
+
}));
|
|
119
|
+
ExecutionTree.current?.setRootArgs(args);
|
|
120
|
+
await body(ctx, args);
|
|
121
|
+
},
|
|
122
|
+
// Handed to `launch` for discovery only: in a real run the schema is resolved above,
|
|
123
|
+
// inside the body. Discovery never reaches that body, so it converts the schema here.
|
|
124
|
+
this.schema);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** `OMKIT_ARGS` carries JSON — the only channel readable synchronously at `.run()`. */
|
|
128
|
+
function parseSuppliedArgs() {
|
|
129
|
+
const raw = process.env.OMKIT_ARGS;
|
|
130
|
+
// Read it once, then clear it from the environment so any subprocess this run spawns
|
|
131
|
+
// (`command("npx omkit run other-om")`, a nested npm script, …) does not inherit this
|
|
132
|
+
// run's args and resolve them against a completely different schema. Whoever launches an
|
|
133
|
+
// omkit child sets that child's args explicitly.
|
|
134
|
+
delete process.env.OMKIT_ARGS;
|
|
135
|
+
if (!raw)
|
|
136
|
+
return {};
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(raw);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
throw new Error("OMKIT_ARGS is not valid JSON");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Supervised, or a real terminal. Otherwise nobody can answer a prompt and resolution must
|
|
146
|
+
* fail fast instead of blocking on a question no one will see.
|
|
147
|
+
*/
|
|
148
|
+
function isInteractive() {
|
|
149
|
+
return activeSupervisor() !== null || process.stdin.isTTY === true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Ask the user for one arg, via the `prompt` battery.
|
|
153
|
+
*
|
|
154
|
+
* The import is dynamic so that `import { om }` does not pull readline in, and because
|
|
155
|
+
* `core` is otherwise not allowed to import `actions`. Be clear about what that costs: this
|
|
156
|
+
* is a **deliberate, unenforced** runtime edge, not a permitted one. ESLint's
|
|
157
|
+
* `no-restricted-imports` has no `ImportExpression` visitor, so a dynamic import is
|
|
158
|
+
* *invisible* to the rule rather than allowed by it — the linter will not catch the next one
|
|
159
|
+
* either, and nothing but this comment stands between here and a real dependency cycle.
|
|
160
|
+
*/
|
|
161
|
+
async function askForArg(spec) {
|
|
162
|
+
const { prompt } = await import("../actions/prompt.js");
|
|
163
|
+
if (spec.kind === "json") {
|
|
164
|
+
return prompt({
|
|
165
|
+
kind: "multiline",
|
|
166
|
+
message: `${spec.path} — JSON`,
|
|
167
|
+
// No sketch means `shapeHint` could not express the shape; a placeholder would tell
|
|
168
|
+
// the user nothing about what to type, so the schema itself goes in its place.
|
|
169
|
+
hint: spec.hint ?? JSON.stringify(spec.jsonSchema),
|
|
170
|
+
// Not `"json"`: a JSON-kind arg may also be answered with the path to a JSON file, and
|
|
171
|
+
// a path is not JSON, so a JSON-only terminator would never end the read. At a bare
|
|
172
|
+
// terminal there is no EOF to fall back on, so the user would wait out the timeout —
|
|
173
|
+
// three times. This is the same rule `coerce` reads the answer back with.
|
|
174
|
+
until: jsonAnswerComplete,
|
|
175
|
+
timeoutMs: 120_000,
|
|
176
|
+
}).result;
|
|
177
|
+
}
|
|
178
|
+
// One line has no room for a raw schema, so an unsketchable scalar shows the placeholder.
|
|
179
|
+
return prompt({ message: `${spec.path} (${spec.hint ?? OPAQUE})`, timeoutMs: 120_000 }).result;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Define a run. `om(name)` returns a builder — chain `.describe(…)` and/or `.args(schema)`
|
|
183
|
+
* and finish with `.run(body)`, which launches it as the root of a fresh
|
|
184
|
+
* {@link ExecutionTree}. The name plus the absolute path of the calling file identify the
|
|
185
|
+
* run; its log folder is `logs/<name>-<hash8>/…`, so same-named oms in different files
|
|
186
|
+
* never share a folder.
|
|
187
|
+
*
|
|
188
|
+
* The call site is captured **here**, not in `.run()` (nor in `.args()`): `om(name)` is
|
|
189
|
+
* where a run is defined, so a builder handed to (and run from) another file still hashes
|
|
190
|
+
* to the file that defined it — see the run-folder-identity constraint doc.
|
|
191
|
+
*/
|
|
192
|
+
export function om(name) {
|
|
193
|
+
assertName(name);
|
|
194
|
+
// The removed two-argument form: JS callers would otherwise silently lose their body
|
|
195
|
+
// to a builder nobody runs. TypeScript callers get a compile error before this.
|
|
196
|
+
if (arguments.length > 1) {
|
|
197
|
+
throw new Error("om(name, body) was removed — use om(name).run(body). See the omkit CHANGELOG.");
|
|
198
|
+
}
|
|
199
|
+
return new Builder(name, callerSite()); // the om() call site — the run's defining script
|
|
200
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JsonSchema } from "./schema-json.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Placeholder text for a schema that cannot be sketched, for the one-line prompts where
|
|
4
|
+
* printing the raw JSON Schema instead would not fit. Callers do **not** detect degradation
|
|
5
|
+
* by comparing against it — {@link shapeHint} returns `undefined` for that.
|
|
6
|
+
*/
|
|
7
|
+
export declare const OPAQUE = "see schema";
|
|
8
|
+
/**
|
|
9
|
+
* Render a JSON Schema as a one-line type sketch for a prompt — `{ host: string,
|
|
10
|
+
* port?: number }` — or `undefined` when it cannot express the shape (deep unions,
|
|
11
|
+
* tuples, recursion).
|
|
12
|
+
*
|
|
13
|
+
* Deliberately partial, and **all-or-nothing**: an unsketchable type anywhere in the tree
|
|
14
|
+
* degrades the whole sketch rather than leaving a placeholder inside an otherwise-valid
|
|
15
|
+
* one. `{ mode: see schema }` reads as a field whose type is the string "see schema", which
|
|
16
|
+
* is exactly the misleading output this is supposed to refuse to print. Degradation is
|
|
17
|
+
* signalled by the return type so a caller cannot miss it by string-matching the wrong
|
|
18
|
+
* shape of sentinel.
|
|
19
|
+
*/
|
|
20
|
+
export declare function shapeHint(schema: JsonSchema): string | undefined;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder text for a schema that cannot be sketched, for the one-line prompts where
|
|
3
|
+
* printing the raw JSON Schema instead would not fit. Callers do **not** detect degradation
|
|
4
|
+
* by comparing against it — {@link shapeHint} returns `undefined` for that.
|
|
5
|
+
*/
|
|
6
|
+
export const OPAQUE = "see schema";
|
|
7
|
+
/**
|
|
8
|
+
* Render a JSON Schema as a one-line type sketch for a prompt — `{ host: string,
|
|
9
|
+
* port?: number }` — or `undefined` when it cannot express the shape (deep unions,
|
|
10
|
+
* tuples, recursion).
|
|
11
|
+
*
|
|
12
|
+
* Deliberately partial, and **all-or-nothing**: an unsketchable type anywhere in the tree
|
|
13
|
+
* degrades the whole sketch rather than leaving a placeholder inside an otherwise-valid
|
|
14
|
+
* one. `{ mode: see schema }` reads as a field whose type is the string "see schema", which
|
|
15
|
+
* is exactly the misleading output this is supposed to refuse to print. Degradation is
|
|
16
|
+
* signalled by the return type so a caller cannot miss it by string-matching the wrong
|
|
17
|
+
* shape of sentinel.
|
|
18
|
+
*/
|
|
19
|
+
export function shapeHint(schema) {
|
|
20
|
+
return render(schema);
|
|
21
|
+
}
|
|
22
|
+
function render(node) {
|
|
23
|
+
if (typeof node !== "object" || node === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
const s = node;
|
|
26
|
+
const enumValues = s.enum;
|
|
27
|
+
if (Array.isArray(enumValues) && enumValues.length > 0) {
|
|
28
|
+
return enumValues.map((v) => JSON.stringify(v)).join(" | ");
|
|
29
|
+
}
|
|
30
|
+
switch (s.type) {
|
|
31
|
+
case "string":
|
|
32
|
+
case "number":
|
|
33
|
+
case "integer":
|
|
34
|
+
case "boolean":
|
|
35
|
+
case "null":
|
|
36
|
+
return String(s.type);
|
|
37
|
+
case "array":
|
|
38
|
+
return renderArray(s);
|
|
39
|
+
case "object":
|
|
40
|
+
return renderObject(s);
|
|
41
|
+
default:
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A tuple's `items` is absent (it uses `prefixItems`), and an opaque element type renders
|
|
47
|
+
* to nothing — either way there is no element sketch, so there is no array sketch.
|
|
48
|
+
* Propagating `undefined` here is what stops `see schema[]` being printed.
|
|
49
|
+
*/
|
|
50
|
+
function renderArray(s) {
|
|
51
|
+
const items = render(s.items);
|
|
52
|
+
return items === undefined ? undefined : `${items}[]`;
|
|
53
|
+
}
|
|
54
|
+
function renderObject(s) {
|
|
55
|
+
const properties = s.properties;
|
|
56
|
+
if (typeof properties !== "object" || properties === null)
|
|
57
|
+
return undefined;
|
|
58
|
+
const required = new Set(Array.isArray(s.required) ? s.required : []);
|
|
59
|
+
const entries = Object.entries(properties);
|
|
60
|
+
if (entries.length === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
const fields = [];
|
|
63
|
+
for (const [key, value] of entries) {
|
|
64
|
+
const rendered = render(value);
|
|
65
|
+
// One field we cannot sketch makes the whole line untrustworthy: the reader has no way
|
|
66
|
+
// to tell the interpolated placeholder from a real type. Degrade the object instead, so
|
|
67
|
+
// the caller falls back to printing the schema the user actually has to satisfy.
|
|
68
|
+
if (rendered === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
fields.push(`${key}${required.has(key) ? "" : "?"}: ${rendered}`);
|
|
71
|
+
}
|
|
72
|
+
return `{ ${fields.join(", ")} }`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** A JSON Schema document, kept deliberately loose — consumers read known keys. */
|
|
3
|
+
export type JsonSchema = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Convert a zod schema to JSON Schema. The single conversion point in omkit: prompt
|
|
6
|
+
* hints (`schema-hint.ts`) and any external consumer read the result, so the choice of
|
|
7
|
+
* converter stays here rather than spreading across callers.
|
|
8
|
+
*
|
|
9
|
+
* Uses `io: "input"` to describe the input type where defaulted fields are optional,
|
|
10
|
+
* correctly excluding them from `required` arrays at all nesting levels.
|
|
11
|
+
*/
|
|
12
|
+
export declare function toJsonSchema(schema: z.ZodType): JsonSchema;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Convert a zod schema to JSON Schema. The single conversion point in omkit: prompt
|
|
4
|
+
* hints (`schema-hint.ts`) and any external consumer read the result, so the choice of
|
|
5
|
+
* converter stays here rather than spreading across callers.
|
|
6
|
+
*
|
|
7
|
+
* Uses `io: "input"` to describe the input type where defaulted fields are optional,
|
|
8
|
+
* correctly excluding them from `required` arrays at all nesting levels.
|
|
9
|
+
*/
|
|
10
|
+
export function toJsonSchema(schema) {
|
|
11
|
+
return z.toJSONSchema(schema, { io: "input" });
|
|
12
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { JsonSchema } from "./schema-json.ts";
|
|
1
3
|
import type { Emitter, EventHandler } from "../foundation/events.ts";
|
|
2
4
|
import type { ReadableLog } from "../foundation/LogEntry.ts";
|
|
3
5
|
import type { Proc } from "../system/proc.ts";
|
|
6
|
+
/** Any zod schema. Aliased so the rest of the file reads without zod's generics. */
|
|
7
|
+
export type ZodTypeLike = z.ZodType;
|
|
8
|
+
/** The TypeScript type a schema validates to. */
|
|
9
|
+
export type InferSchema<S extends ZodTypeLike> = z.infer<S>;
|
|
4
10
|
/** A value an action's exec may return: a result, or a promise of one. */
|
|
5
11
|
export type Awaitable<T> = T | Promise<T>;
|
|
6
12
|
/** An action that declares no events. */
|
|
@@ -34,11 +40,81 @@ export interface ActionContext<Events extends object = NoEvents, Handle = void>
|
|
|
34
40
|
readonly assert: (condition: boolean, message: string) => void;
|
|
35
41
|
/** Capture a JSON snapshot; drops a timeline line and resolves the file's absolute path. */
|
|
36
42
|
readonly snapshot: (name: string, value: unknown) => Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Label a file this action wrote — name, optional description, MIME inferred from the
|
|
45
|
+
* extension. Drops a timeline line and returns the file's path. Purely descriptive:
|
|
46
|
+
* the file must already exist (or be written next); this records what it is.
|
|
47
|
+
*/
|
|
48
|
+
readonly artifact: (name: string, file: string, opts?: {
|
|
49
|
+
description?: string;
|
|
50
|
+
mime?: string;
|
|
51
|
+
}) => string;
|
|
37
52
|
/** Absolute path to this run's output folder — write artifacts (screenshots, dumps, …) here. */
|
|
38
53
|
readonly artifactsFolder: string;
|
|
39
54
|
/** Spawn child processes bound to this action (output → this node's log, killed on teardown). */
|
|
40
55
|
readonly proc: Proc;
|
|
41
56
|
}
|
|
57
|
+
/** Human-readable metadata about an om or action. */
|
|
58
|
+
export interface OmDescription {
|
|
59
|
+
/** One line saying what this does. */
|
|
60
|
+
summary: string;
|
|
61
|
+
}
|
|
62
|
+
/** How an om or action behaves when a client runs it as an MCP tool. */
|
|
63
|
+
export type McpMode = "settling" | "long-lived";
|
|
64
|
+
/**
|
|
65
|
+
* Marks an om or action as exposed over MCP. Absent ⇒ invisible to the server:
|
|
66
|
+
* `.describe()` and `.args()` alone expose nothing.
|
|
67
|
+
*/
|
|
68
|
+
export interface McpExposure {
|
|
69
|
+
/**
|
|
70
|
+
* Whether the run settles on its own. `run_om` accepts only a `"settling"` entry; a
|
|
71
|
+
* `"long-lived"` one must be started with `start_om`. Omitted ⇒ `"settling"`, so being
|
|
72
|
+
* wrong surfaces as a timeout rather than a silently orphaned process.
|
|
73
|
+
*/
|
|
74
|
+
mode?: McpMode;
|
|
75
|
+
}
|
|
76
|
+
/** An {@link McpExposure} after the builder has filled its default. */
|
|
77
|
+
export interface ResolvedMcpExposure {
|
|
78
|
+
readonly mode: McpMode;
|
|
79
|
+
}
|
|
80
|
+
/** The result of converting a declared schema — one of the two fields is always undefined. */
|
|
81
|
+
export interface DescribedArgs {
|
|
82
|
+
readonly inputSchema: JsonSchema | undefined;
|
|
83
|
+
readonly schemaError: string | undefined;
|
|
84
|
+
}
|
|
85
|
+
/** The builder returned by `om(name)`. `.run(body)` launches the run. */
|
|
86
|
+
export interface OmBuilder {
|
|
87
|
+
/** Attach a human-readable summary — carried for `omkit ls` and future tooling. */
|
|
88
|
+
describe(description: OmDescription): OmBuilder;
|
|
89
|
+
/** Expose this om over MCP. Without it the MCP server does not list or run it. */
|
|
90
|
+
mcp(exposure?: McpExposure): OmBuilder;
|
|
91
|
+
/**
|
|
92
|
+
* Declare the run's input shape. Unlike an action's `.args()`, this is resolved at
|
|
93
|
+
* runtime: the values come from what was supplied, then the schema's defaults, then by
|
|
94
|
+
* prompting — and the run fails if that is not enough.
|
|
95
|
+
*/
|
|
96
|
+
args<S extends ZodTypeLike>(schema: S): OmBuilderArgs<S>;
|
|
97
|
+
/**
|
|
98
|
+
* Launch the run with `body` as its root. The body runs inside the root node's
|
|
99
|
+
* ambient scope, so any action call / `step(...)` / `console.*` it reaches attributes
|
|
100
|
+
* correctly. Resolves once the run has torn down and produced its artifacts; never
|
|
101
|
+
* rejects (failures are recorded in the tree and set the exit code).
|
|
102
|
+
*/
|
|
103
|
+
run(body: (ctx: OmContext) => Awaitable<void>): Promise<void>;
|
|
104
|
+
}
|
|
105
|
+
/** After `.args(schema)`: `.run`'s body receives the resolved, typed args. */
|
|
106
|
+
export interface OmBuilderArgs<S extends ZodTypeLike> {
|
|
107
|
+
describe(description: OmDescription): OmBuilderArgs<S>;
|
|
108
|
+
/** Expose this om over MCP. Without it the MCP server does not list or run it. */
|
|
109
|
+
mcp(exposure?: McpExposure): OmBuilderArgs<S>;
|
|
110
|
+
/**
|
|
111
|
+
* Launch the run, resolving the declared args first — supplied values, then defaults,
|
|
112
|
+
* then prompting — and pass them to `body` as its second parameter. Resolution happens
|
|
113
|
+
* *inside* the run, so a prompt and its answer land on the run's own timeline; an
|
|
114
|
+
* unresolvable arg fails the run like any other error in the body.
|
|
115
|
+
*/
|
|
116
|
+
run(body: (ctx: OmContext, args: InferSchema<S>) => Awaitable<void>): Promise<void>;
|
|
117
|
+
}
|
|
42
118
|
/** What the `om` body receives — the run-level counterpart to {@link ActionContext}. */
|
|
43
119
|
export interface OmContext {
|
|
44
120
|
/** The run's (root) abort signal. */
|
|
@@ -53,6 +129,15 @@ export interface OmContext {
|
|
|
53
129
|
readonly assert: (condition: boolean, message: string) => void;
|
|
54
130
|
/** Capture a run-level JSON snapshot; resolves the file's absolute path. */
|
|
55
131
|
readonly snapshot: (name: string, value: unknown) => Promise<string>;
|
|
132
|
+
/**
|
|
133
|
+
* Label a file this action wrote — name, optional description, MIME inferred from the
|
|
134
|
+
* extension. Drops a timeline line and returns the file's path. Purely descriptive:
|
|
135
|
+
* the file must already exist (or be written next); this records what it is.
|
|
136
|
+
*/
|
|
137
|
+
readonly artifact: (name: string, file: string, opts?: {
|
|
138
|
+
description?: string;
|
|
139
|
+
mime?: string;
|
|
140
|
+
}) => string;
|
|
56
141
|
/** Absolute path to this run's output folder — write run-level artifacts here. */
|
|
57
142
|
readonly artifactsFolder: string;
|
|
58
143
|
}
|
|
@@ -101,13 +186,40 @@ export interface Action<Args extends unknown[], Result, Events extends object =
|
|
|
101
186
|
readonly actionName: string;
|
|
102
187
|
/** `file:line` where this action was defined (the `.run(...)` site), for the log header. */
|
|
103
188
|
readonly definedAt: string | undefined;
|
|
189
|
+
/**
|
|
190
|
+
* The summary from `.describe(…)`, carried across every builder link onto the definition.
|
|
191
|
+
* Stored so it is retrievable; nothing in the runtime reads it yet.
|
|
192
|
+
*/
|
|
193
|
+
readonly description: OmDescription | undefined;
|
|
194
|
+
/** The `.mcp(…)` exposure, carried across every builder link. Undefined ⇒ not exposed. */
|
|
195
|
+
readonly mcp: ResolvedMcpExposure | undefined;
|
|
196
|
+
/**
|
|
197
|
+
* The declared args as JSON Schema, computed on demand. Called by the discovery child
|
|
198
|
+
* rather than converting the schema itself: the user's file resolves `"omkit"` to the
|
|
199
|
+
* installed package while the child runs omkit's own module graph, so the two can hold
|
|
200
|
+
* different zod instances. Converting inside the defining copy sidesteps that. Never
|
|
201
|
+
* throws — an unconvertible schema comes back as `schemaError`.
|
|
202
|
+
*/
|
|
203
|
+
readonly describeArgs: () => DescribedArgs;
|
|
104
204
|
}
|
|
105
|
-
/** Intermediate step from `action(name)`: declare events/handle, then
|
|
205
|
+
/** Intermediate step from `action(name)`: declare metadata/events/handle, then the impl. */
|
|
106
206
|
export interface ActionBuilderEvents<Events extends object, Handle = void> {
|
|
207
|
+
describe(description: OmDescription): ActionBuilderEvents<Events, Handle>;
|
|
208
|
+
/** Expose this action over MCP. Without it the MCP server does not list or run it. */
|
|
209
|
+
mcp(exposure?: McpExposure): ActionBuilderEvents<Events, Handle>;
|
|
107
210
|
emits<E extends object>(): ActionBuilderEvents<E, Handle>;
|
|
108
211
|
ref<H>(): ActionBuilderEvents<Events, H>;
|
|
212
|
+
/** Pin the first parameter to the schema's inferred type. */
|
|
213
|
+
args<S extends ZodTypeLike>(schema: S): ActionBuilderArgs<S, Events, Handle>;
|
|
109
214
|
run<Args extends unknown[], Result>(body: (ctx: ActionContext<Events, Handle>, ...args: Args) => Awaitable<Result>): Action<Args, Result, Events, Handle>;
|
|
110
215
|
}
|
|
216
|
+
/** After `.args(schema)`: `.run` takes exactly one typed argument. */
|
|
217
|
+
export interface ActionBuilderArgs<S extends ZodTypeLike, Events extends object, Handle = void> {
|
|
218
|
+
describe(description: OmDescription): ActionBuilderArgs<S, Events, Handle>;
|
|
219
|
+
/** Expose this action over MCP. Without it the MCP server does not list or run it. */
|
|
220
|
+
mcp(exposure?: McpExposure): ActionBuilderArgs<S, Events, Handle>;
|
|
221
|
+
run<Result>(body: (ctx: ActionContext<Events, Handle>, args: InferSchema<S>) => Awaitable<Result>): Action<[InferSchema<S>], Result, Events, Handle>;
|
|
222
|
+
}
|
|
111
223
|
/** The exec signature bound by `.run(...)` (arg types erased at the boundary). */
|
|
112
224
|
export type Exec<Events extends object, Handle, Result> = (ctx: ActionContext<Events, Handle>, ...args: never[]) => Awaitable<Result>;
|
|
113
225
|
/** What the {@link ExecutionTree} needs to launch an instance — the shape an instance satisfies. */
|
|
@@ -13,7 +13,7 @@ export interface LogEntry {
|
|
|
13
13
|
readonly nodeId: string;
|
|
14
14
|
/** The owning node's execution path (`main/chromePage_9f3c`). */
|
|
15
15
|
readonly path: string;
|
|
16
|
-
/** `info` | `error` | `event` | `assert` | `snapshot` | `tag` | `child` | `run`. */
|
|
16
|
+
/** `info` | `error` | `event` | `assert` | `snapshot` | `artifact` | `tag` | `child` | `run`. */
|
|
17
17
|
readonly level: string;
|
|
18
18
|
/** Finer origin within the node (proc name, `event`, `run`, …). */
|
|
19
19
|
readonly source: string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operating system's own name for a file — the spelling every other spelling of it
|
|
3
|
+
* resolves to.
|
|
4
|
+
*
|
|
5
|
+
* Windows filesystems are case-insensitive but case-*preserving*, so `d:\repo\om.ts` and
|
|
6
|
+
* `D:\repo\om.ts` are one file with two names. That is harmless right up until something
|
|
7
|
+
* keys on the string, and omkit keys on it twice:
|
|
8
|
+
*
|
|
9
|
+
* - **Module identity.** Node's ESM loader caches by URL, so `file:///d:/…` and
|
|
10
|
+
* `file:///D:/…` load the *same file twice* into two module instances with separate
|
|
11
|
+
* module-level state. An action whose file resolved `"omkit"` one way cannot see the run
|
|
12
|
+
* started by a host that resolved it the other — the symptom is a bare "no active om() run"
|
|
13
|
+
* from a run that is demonstrably in progress.
|
|
14
|
+
* - **Run identity.** {@link import("./ids.ts").omHash} hashes the defining file's path, so
|
|
15
|
+
* the same om launched by two differently-spelled paths writes to two different run
|
|
16
|
+
* folders — and `logs/<name>-<hash>/` stops meaning "every run of this om".
|
|
17
|
+
*
|
|
18
|
+
* `realpathSync.native` is the OS's answer rather than ours: it fixes drive-letter case, 8.3
|
|
19
|
+
* short names, and symlinked directories in one step. Resolving symlinks is deliberate, not
|
|
20
|
+
* incidental — Node realpaths a module before caching it, so canonicalising the same way is
|
|
21
|
+
* what keeps our identity and Node's agreeing.
|
|
22
|
+
*
|
|
23
|
+
* A path that does not exist yet is returned unchanged: there is no canonical name for a file
|
|
24
|
+
* the filesystem has never seen, and guessing one would be worse than passing it through.
|
|
25
|
+
*/
|
|
26
|
+
export declare function canonicalPath(file: string): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* The operating system's own name for a file — the spelling every other spelling of it
|
|
4
|
+
* resolves to.
|
|
5
|
+
*
|
|
6
|
+
* Windows filesystems are case-insensitive but case-*preserving*, so `d:\repo\om.ts` and
|
|
7
|
+
* `D:\repo\om.ts` are one file with two names. That is harmless right up until something
|
|
8
|
+
* keys on the string, and omkit keys on it twice:
|
|
9
|
+
*
|
|
10
|
+
* - **Module identity.** Node's ESM loader caches by URL, so `file:///d:/…` and
|
|
11
|
+
* `file:///D:/…` load the *same file twice* into two module instances with separate
|
|
12
|
+
* module-level state. An action whose file resolved `"omkit"` one way cannot see the run
|
|
13
|
+
* started by a host that resolved it the other — the symptom is a bare "no active om() run"
|
|
14
|
+
* from a run that is demonstrably in progress.
|
|
15
|
+
* - **Run identity.** {@link import("./ids.ts").omHash} hashes the defining file's path, so
|
|
16
|
+
* the same om launched by two differently-spelled paths writes to two different run
|
|
17
|
+
* folders — and `logs/<name>-<hash>/` stops meaning "every run of this om".
|
|
18
|
+
*
|
|
19
|
+
* `realpathSync.native` is the OS's answer rather than ours: it fixes drive-letter case, 8.3
|
|
20
|
+
* short names, and symlinked directories in one step. Resolving symlinks is deliberate, not
|
|
21
|
+
* incidental — Node realpaths a module before caching it, so canonicalising the same way is
|
|
22
|
+
* what keeps our identity and Node's agreeing.
|
|
23
|
+
*
|
|
24
|
+
* A path that does not exist yet is returned unchanged: there is no canonical name for a file
|
|
25
|
+
* the filesystem has never seen, and guessing one would be worse than passing it through.
|
|
26
|
+
*/
|
|
27
|
+
export function canonicalPath(file) {
|
|
28
|
+
try {
|
|
29
|
+
return fs.realpathSync.native(file);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return file;
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* omkit (refactored core) — the ExecutionTree model.
|
|
3
3
|
*
|
|
4
|
-
* A curated public surface. `om(name
|
|
4
|
+
* A curated public surface. `om(name).run(body)` runs a linear orchestration as the root of
|
|
5
5
|
* one run; `action(...)` defines typed units you launch by **calling** them;
|
|
6
6
|
* `step(name, fn)` is the inline anonymous action. The run's introspectable output
|
|
7
7
|
* is the on-disk `result.json` / `raw.jsonl` / `.log` files — the ExecutionTree
|
|
@@ -11,4 +11,4 @@ export { om } from "./core/om.ts";
|
|
|
11
11
|
export { action } from "./core/action.ts";
|
|
12
12
|
export { step } from "./core/step.ts";
|
|
13
13
|
export { CancelledError, isCancelled } from "./foundation/CancelledError.ts";
|
|
14
|
-
export type { Action, Activity, ActionContext, OmContext } from "./core/types.ts";
|
|
14
|
+
export type { Action, Activity, ActionContext, OmBuilder, OmBuilderArgs, OmContext, OmDescription, } from "./core/types.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* omkit (refactored core) — the ExecutionTree model.
|
|
3
3
|
*
|
|
4
|
-
* A curated public surface. `om(name
|
|
4
|
+
* A curated public surface. `om(name).run(body)` runs a linear orchestration as the root of
|
|
5
5
|
* one run; `action(...)` defines typed units you launch by **calling** them;
|
|
6
6
|
* `step(name, fn)` is the inline anonymous action. The run's introspectable output
|
|
7
7
|
* is the on-disk `result.json` / `raw.jsonl` / `.log` files — the ExecutionTree
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { om } from "../index.js";
|
|
11
|
+
import { hostOmName } from "./action-identity.js";
|
|
12
|
+
/**
|
|
13
|
+
* The host om for an action-backed tool. Forked as the om file, with the action to run
|
|
14
|
+
* named on the environment.
|
|
15
|
+
*
|
|
16
|
+
* An action cannot run standalone — `ExecutionTree.require()` throws outside a run — so
|
|
17
|
+
* something has to host it. Nothing here is forged: `callerSite()` captures this file,
|
|
18
|
+
* which genuinely is where the run is defined. The disambiguator lives in the **name**:
|
|
19
|
+
* `seed@a1b2c3d4` is stable across runs, and two same-named actions in different files get
|
|
20
|
+
* different suffixes and therefore different folders. That satisfies the
|
|
21
|
+
* run-folder-identity contract through the name rather than around it, and avoids adding
|
|
22
|
+
* an escape hatch that would let any caller claim another run's identity.
|
|
23
|
+
*
|
|
24
|
+
* Known sharp edge: an action declared with `.ref<H>()` publishes a capability for
|
|
25
|
+
* downstream actions. Standalone it does its work and hands that capability to nobody —
|
|
26
|
+
* `chromePage` would attach to Chrome and return nothing useful. Such actions make poor
|
|
27
|
+
* tools. Documented, not prevented.
|
|
28
|
+
*/
|
|
29
|
+
function required(name) {
|
|
30
|
+
const value = process.env[name];
|
|
31
|
+
if (!value) {
|
|
32
|
+
throw new Error(`${name} must be set — the MCP server sets it when forking this host`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
const file = required("OMKIT_ACTION_FILE");
|
|
37
|
+
const name = required("OMKIT_ACTION_NAME");
|
|
38
|
+
const exportName = required("OMKIT_ACTION_EXPORT");
|
|
39
|
+
// Deliberately not OMKIT_ARGS: this om declares no `.args()`, so nothing here would read
|
|
40
|
+
// that variable, and a distinct name keeps a host run from colliding with an om's args.
|
|
41
|
+
const args = JSON.parse(process.env.OMKIT_ACTION_ARGS ?? "{}");
|
|
42
|
+
om(hostOmName(name, file))
|
|
43
|
+
.describe({ summary: `Standalone host for the ${name} action.` })
|
|
44
|
+
.run(async () => {
|
|
45
|
+
const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href)));
|
|
46
|
+
const launch = mod[exportName];
|
|
47
|
+
if (typeof launch !== "function") {
|
|
48
|
+
throw new Error(`${file} has no exported action "${exportName}"`);
|
|
49
|
+
}
|
|
50
|
+
// The action reaches `ExecutionTree.require()` through the copy of omkit *its* file
|
|
51
|
+
// imported, and `current` is a static — so this only works when both resolve to the
|
|
52
|
+
// same installed omkit. True for a normal install and for the fixtures; false under
|
|
53
|
+
// `npm link`, a non-hoisted pnpm layout, or two omkit versions in one tree. The bare
|
|
54
|
+
// error is undiagnosable from a client, so name the cause here.
|
|
55
|
+
try {
|
|
56
|
+
await launch(args).result;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e instanceof Error && /no active om\(\) run/.test(e.message)) {
|
|
60
|
+
throw new Error(`${name} could not see this run. Its file resolved a different copy of omkit ` +
|
|
61
|
+
`than the host did — check for a linked, duplicated, or non-hoisted omkit install.`);
|
|
62
|
+
}
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
});
|