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.
Files changed (92) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/README.md +169 -13
  3. package/dist/actions/chrome-page.d.ts +41 -0
  4. package/dist/actions/chrome-page.js +77 -29
  5. package/dist/actions/drive-page.d.ts +50 -0
  6. package/dist/actions/drive-page.js +68 -0
  7. package/dist/actions/index.d.ts +5 -3
  8. package/dist/actions/index.js +2 -1
  9. package/dist/actions/prompt.d.ts +32 -5
  10. package/dist/actions/prompt.js +92 -8
  11. package/dist/cli/commands/help.js +7 -0
  12. package/dist/cli/commands/init.js +1 -1
  13. package/dist/cli/commands/ls.d.ts +11 -4
  14. package/dist/cli/commands/ls.js +35 -15
  15. package/dist/cli/commands/mcp.d.ts +10 -0
  16. package/dist/cli/commands/mcp.js +13 -0
  17. package/dist/cli/commands/skill.d.ts +18 -0
  18. package/dist/cli/commands/skill.js +62 -0
  19. package/dist/cli/index.d.ts +8 -0
  20. package/dist/cli/index.js +78 -30
  21. package/dist/client/discover-child.d.ts +12 -0
  22. package/dist/client/discover-child.js +71 -0
  23. package/dist/client/discovery.d.ts +21 -1
  24. package/dist/client/discovery.js +139 -2
  25. package/dist/client/index.d.ts +2 -1
  26. package/dist/client/index.js +15 -5
  27. package/dist/client/outline.d.ts +35 -0
  28. package/dist/client/outline.js +100 -0
  29. package/dist/client/registry.d.ts +59 -0
  30. package/dist/client/runner.d.ts +12 -0
  31. package/dist/client/runner.js +16 -7
  32. package/dist/client/types.d.ts +14 -1
  33. package/dist/client/types.js +5 -1
  34. package/dist/core/ActionRun.d.ts +5 -0
  35. package/dist/core/ActionRun.js +9 -0
  36. package/dist/core/ExecutionTree.d.ts +40 -2
  37. package/dist/core/ExecutionTree.js +66 -2
  38. package/dist/core/action.d.ts +2 -1
  39. package/dist/core/action.js +93 -9
  40. package/dist/core/args.d.ts +50 -0
  41. package/dist/core/args.js +166 -0
  42. package/dist/core/discovery-mode.d.ts +37 -0
  43. package/dist/core/discovery-mode.js +54 -0
  44. package/dist/core/interaction.d.ts +3 -1
  45. package/dist/core/om.d.ts +11 -9
  46. package/dist/core/om.js +182 -13
  47. package/dist/core/schema-hint.d.ts +20 -0
  48. package/dist/core/schema-hint.js +73 -0
  49. package/dist/core/schema-json.d.ts +12 -0
  50. package/dist/core/schema-json.js +12 -0
  51. package/dist/core/types.d.ts +113 -1
  52. package/dist/foundation/LogEntry.d.ts +1 -1
  53. package/dist/foundation/canonicalPath.d.ts +26 -0
  54. package/dist/foundation/canonicalPath.js +34 -0
  55. package/dist/index.d.ts +2 -2
  56. package/dist/index.js +1 -1
  57. package/dist/mcp/action-host.d.ts +1 -0
  58. package/dist/mcp/action-host.js +65 -0
  59. package/dist/mcp/action-identity.d.ts +23 -0
  60. package/dist/mcp/action-identity.js +35 -0
  61. package/dist/mcp/progress.d.ts +61 -0
  62. package/dist/mcp/progress.js +92 -0
  63. package/dist/mcp/resources.d.ts +55 -0
  64. package/dist/mcp/resources.js +208 -0
  65. package/dist/mcp/runs.d.ts +37 -0
  66. package/dist/mcp/runs.js +60 -0
  67. package/dist/mcp/server.d.ts +19 -0
  68. package/dist/mcp/server.js +38 -0
  69. package/dist/mcp/tail.d.ts +37 -0
  70. package/dist/mcp/tail.js +64 -0
  71. package/dist/mcp/tools.d.ts +54 -0
  72. package/dist/mcp/tools.js +378 -0
  73. package/dist/mcp/validate.d.ts +14 -0
  74. package/dist/mcp/validate.js +54 -0
  75. package/dist/mcp/verdict.d.ts +31 -0
  76. package/dist/mcp/verdict.js +47 -0
  77. package/dist/output/artifact/ArtifactStore.d.ts +24 -0
  78. package/dist/output/artifact/ArtifactStore.js +49 -0
  79. package/dist/output/folder/RunFolder.d.ts +23 -1
  80. package/dist/output/folder/RunFolder.js +70 -11
  81. package/dist/output/writers/views.d.ts +10 -0
  82. package/dist/skill/file.d.ts +26 -0
  83. package/dist/skill/file.js +58 -0
  84. package/dist/skill/index.d.ts +12 -0
  85. package/dist/skill/index.js +10 -0
  86. package/dist/skill/model.d.ts +69 -0
  87. package/dist/skill/model.js +200 -0
  88. package/dist/skill/render.d.ts +18 -0
  89. package/dist/skill/render.js +159 -0
  90. package/dist/skill/root.d.ts +15 -0
  91. package/dist/skill/root.js +27 -0
  92. package/package.json +5 -2
@@ -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
+ }
@@ -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 provide the impl. */
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, body)` runs a linear orchestration as the root of
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, body)` runs a linear orchestration as the root of
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
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The name the host om runs under: `<action>@<hash8 of the action's defining file>`.
3
+ *
4
+ * Shared by the host (which passes it to `om(...)`) and the server (which has to predict
5
+ * the resulting folder before the run exists). One function so the two cannot drift —
6
+ * they are the same rule applied from opposite sides of a fork.
7
+ */
8
+ export declare function hostOmName(action: string, actionFile: string): string;
9
+ /**
10
+ * Absolute path of the shipped action host, with the extension this build actually uses —
11
+ * `.ts` under tsx and vitest, `.js` once compiled. `rewriteRelativeImportExtensions`
12
+ * rewrites import specifiers; this path is data (a fork target), so it does not.
13
+ */
14
+ export declare function hostFile(): string;
15
+ /**
16
+ * Where a hosted action's runs land: `<fsSafe(hostName)>-<hash8>`.
17
+ *
18
+ * Derived rather than observed, because `start_om` has to answer before the child has
19
+ * created anything. It reproduces `RunFolder.name()` exactly — including `fsSafe`, which
20
+ * collapses the `@` to a `-`, so the folder reads `seed-a1b2c3d4-<hash8>`. A test pins
21
+ * this against a real run: if the derivation ever drifts from the runtime, it fails.
22
+ */
23
+ export declare function hostFolderName(action: string, actionFile: string): string;
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { omHash } from "../foundation/ids.js";
4
+ import { fsSafe } from "../foundation/fsSafe.js";
5
+ /**
6
+ * The name the host om runs under: `<action>@<hash8 of the action's defining file>`.
7
+ *
8
+ * Shared by the host (which passes it to `om(...)`) and the server (which has to predict
9
+ * the resulting folder before the run exists). One function so the two cannot drift —
10
+ * they are the same rule applied from opposite sides of a fork.
11
+ */
12
+ export function hostOmName(action, actionFile) {
13
+ return `${action}@${omHash(action, actionFile)}`;
14
+ }
15
+ /**
16
+ * Absolute path of the shipped action host, with the extension this build actually uses —
17
+ * `.ts` under tsx and vitest, `.js` once compiled. `rewriteRelativeImportExtensions`
18
+ * rewrites import specifiers; this path is data (a fork target), so it does not.
19
+ */
20
+ export function hostFile() {
21
+ const here = fileURLToPath(import.meta.url);
22
+ return path.join(path.dirname(here), `action-host${path.extname(here)}`);
23
+ }
24
+ /**
25
+ * Where a hosted action's runs land: `<fsSafe(hostName)>-<hash8>`.
26
+ *
27
+ * Derived rather than observed, because `start_om` has to answer before the child has
28
+ * created anything. It reproduces `RunFolder.name()` exactly — including `fsSafe`, which
29
+ * collapses the `@` to a `-`, so the folder reads `seed-a1b2c3d4-<hash8>`. A test pins
30
+ * this against a real run: if the derivation ever drifts from the runtime, it fails.
31
+ */
32
+ export function hostFolderName(action, actionFile) {
33
+ const hostName = hostOmName(action, actionFile);
34
+ return `${fsSafe(hostName)}-${omHash(hostName, hostFile())}`;
35
+ }
@@ -0,0 +1,61 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { LogEntry } from "../foundation/LogEntry.ts";
3
+ import type { PromptSpec } from "../core/interaction.ts";
4
+ import type { RunSession } from "../client/types.ts";
5
+ export declare function isMilestone(entry: LogEntry): boolean;
6
+ /** The slice of the SDK's request context this module needs — narrow, so tests can fake it. */
7
+ export interface ProgressExtra {
8
+ readonly signal: AbortSignal;
9
+ readonly _meta?: {
10
+ progressToken?: string | number;
11
+ };
12
+ readonly sendNotification: (n: {
13
+ method: "notifications/progress";
14
+ params: {
15
+ progressToken: string | number;
16
+ progress: number;
17
+ message?: string;
18
+ };
19
+ }) => Promise<void>;
20
+ }
21
+ /**
22
+ * Wire a live run to the request that started it: milestones become progress
23
+ * notifications, and the client cancelling the request tears the run down.
24
+ *
25
+ * `progress` is the log store's run-global sequence, which is monotonic by construction —
26
+ * exactly what the spec requires, with no counter to keep. `total` is omitted, which the
27
+ * spec permits, because a run does not know how many steps it has left.
28
+ *
29
+ * Progress is a courtesy, not a deadline extension: whether a client resets its timeout on
30
+ * progress is implementation-dependent, so a long run still belongs in `start_om`.
31
+ */
32
+ export declare function attachProgress(session: RunSession, extra: ProgressExtra): void;
33
+ /** MCP's elicitation schema: top-level primitives only, no nesting. */
34
+ export interface RequestedSchema {
35
+ type: "object";
36
+ properties: Record<string, {
37
+ type: "string";
38
+ enum?: string[];
39
+ description: string;
40
+ }>;
41
+ required: string[];
42
+ }
43
+ /**
44
+ * Turn one of omkit's prompt specs into an elicitation request. All three kinds become a
45
+ * single `value` string: a choice narrows it with `enum` so the client cannot answer
46
+ * invalidly, and a multiline prompt stays a string because MCP's restricted schema has
47
+ * nowhere to say "several lines".
48
+ */
49
+ export declare function toRequestedSchema(spec: PromptSpec): RequestedSchema;
50
+ /**
51
+ * Forward the run's prompts to the client as elicitations, and its answers back.
52
+ *
53
+ * prompt.ts already describes its supervisor as "whoever owns the terminal (Ink app / MCP
54
+ * client)" — this is that. A declined or cancelled elicitation answers with the prompt's
55
+ * own default rather than leaving the run to time out three times over: the run has to
56
+ * make progress either way, and the default is what a timeout would have produced.
57
+ *
58
+ * `promptDone` means the child gave up first; the in-flight elicitation is aborted so the
59
+ * client's dialog does not linger with nothing behind it.
60
+ */
61
+ export declare function attachElicitation(server: McpServer, session: RunSession): void;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Which log levels count as "something happened". Forwarding every entry would turn a
3
+ * chatty run into a notification flood; these are the levels that mark a step, an event,
4
+ * an assertion, a failure, or a file — the same shape the terminal's live renderer treats
5
+ * as worth showing.
6
+ */
7
+ const MILESTONE_LEVELS = new Set(["run", "event", "assert", "error", "artifact", "child"]);
8
+ export function isMilestone(entry) {
9
+ return MILESTONE_LEVELS.has(entry.level);
10
+ }
11
+ /**
12
+ * Wire a live run to the request that started it: milestones become progress
13
+ * notifications, and the client cancelling the request tears the run down.
14
+ *
15
+ * `progress` is the log store's run-global sequence, which is monotonic by construction —
16
+ * exactly what the spec requires, with no counter to keep. `total` is omitted, which the
17
+ * spec permits, because a run does not know how many steps it has left.
18
+ *
19
+ * Progress is a courtesy, not a deadline extension: whether a client resets its timeout on
20
+ * progress is implementation-dependent, so a long run still belongs in `start_om`.
21
+ */
22
+ export function attachProgress(session, extra) {
23
+ const token = extra._meta?.progressToken;
24
+ if (token !== undefined) {
25
+ session.on("log", (entry) => {
26
+ if (!isMilestone(entry))
27
+ return;
28
+ void extra
29
+ .sendNotification({
30
+ method: "notifications/progress",
31
+ params: { progressToken: token, progress: entry.sequence, message: entry.message },
32
+ })
33
+ .catch(() => {
34
+ // The client went away mid-run. It is cancelled through `signal` anyway; a
35
+ // rejected notification must not fault the run itself.
36
+ });
37
+ });
38
+ }
39
+ extra.signal.addEventListener("abort", () => session.cancel(), { once: true });
40
+ }
41
+ /**
42
+ * Turn one of omkit's prompt specs into an elicitation request. All three kinds become a
43
+ * single `value` string: a choice narrows it with `enum` so the client cannot answer
44
+ * invalidly, and a multiline prompt stays a string because MCP's restricted schema has
45
+ * nowhere to say "several lines".
46
+ */
47
+ export function toRequestedSchema(spec) {
48
+ const description = spec.hint ? `${spec.message} — ${spec.hint}` : spec.message;
49
+ const value = spec.kind === "choice" && spec.choices
50
+ ? { type: "string", enum: spec.choices.map((c) => c.value), description }
51
+ : { type: "string", description };
52
+ return { type: "object", properties: { value }, required: ["value"] };
53
+ }
54
+ /**
55
+ * Forward the run's prompts to the client as elicitations, and its answers back.
56
+ *
57
+ * prompt.ts already describes its supervisor as "whoever owns the terminal (Ink app / MCP
58
+ * client)" — this is that. A declined or cancelled elicitation answers with the prompt's
59
+ * own default rather than leaving the run to time out three times over: the run has to
60
+ * make progress either way, and the default is what a timeout would have produced.
61
+ *
62
+ * `promptDone` means the child gave up first; the in-flight elicitation is aborted so the
63
+ * client's dialog does not linger with nothing behind it.
64
+ */
65
+ export function attachElicitation(server, session) {
66
+ const pending = new Map();
67
+ session.on("promptDone", (id) => {
68
+ pending.get(id)?.abort();
69
+ pending.delete(id);
70
+ });
71
+ session.on("prompt", (request) => {
72
+ const controller = new AbortController();
73
+ pending.set(request.id, controller);
74
+ void (async () => {
75
+ try {
76
+ const result = await server.server.elicitInput({ message: request.spec.message, requestedSchema: toRequestedSchema(request.spec) }, { signal: controller.signal });
77
+ const answer = result.content?.value;
78
+ const value = result.action === "accept" && typeof answer === "string" ? answer : request.spec.default;
79
+ session.answer(request.id, value, result.action === "accept" ? "elicitation" : "default");
80
+ }
81
+ catch {
82
+ // Aborted, or a client with no elicitation capability. Either way the run must not
83
+ // hang on us: hand it the default and let it carry on.
84
+ if (!controller.signal.aborted)
85
+ session.answer(request.id, request.spec.default, "default");
86
+ }
87
+ finally {
88
+ pending.delete(request.id);
89
+ }
90
+ })();
91
+ });
92
+ }
@@ -0,0 +1,55 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function mimeOf(file: string): string;
3
+ /**
4
+ * Resolve a path and refuse anything that is not strictly inside `<root>/logs`.
5
+ *
6
+ * URIs are untrusted input — a model can propose any string — so this runs on every read,
7
+ * not just at the template. Two checks, because `..` and a symlink escape by different
8
+ * routes: the lexical one catches `logs/../secret`, and the realpath one catches
9
+ * `logs/link → /etc`. `logs/` itself is refused: it is the root, not a run.
10
+ */
11
+ export declare function resolveUnderLogs(root: string, target: string): string | undefined;
12
+ /** A run folder the caller named directly (a path from `get_run`, or one a client kept). */
13
+ export declare function resolveRunFolder(root: string, candidate: string): string | undefined;
14
+ /**
15
+ * The newest run of one om. Folder names are `<date>/<time>` with zero-padded, fixed-width
16
+ * components (`2026-08-04/11-30-00`), so lexical sort is chronological sort — no parsing,
17
+ * and no dependence on filesystem mtime.
18
+ *
19
+ * This is what the run-folder-identity constraint is *for*: "a script or an assistant can
20
+ * always find the latest run of X without parsing scrollback".
21
+ */
22
+ export declare function latestRun(root: string, folderName: string): string | undefined;
23
+ export interface RunFileRead {
24
+ readonly path: string;
25
+ readonly mimeType: string;
26
+ readonly text?: string;
27
+ readonly blob?: string;
28
+ }
29
+ /** Read one file from a run folder, confined, size-capped, and typed by extension. */
30
+ export declare function readRunFile(root: string, segments: string[]): RunFileRead | undefined;
31
+ export interface ResourceLink {
32
+ readonly type: "resource_link";
33
+ readonly uri: string;
34
+ readonly name: string;
35
+ readonly description?: string;
36
+ readonly mimeType: string;
37
+ }
38
+ /**
39
+ * The run's files as `resource_link` blocks rather than inlined content, so a model reads
40
+ * only what it needs — `raw.jsonl` on a long run is megabytes. Curated artifacts come
41
+ * first, carrying the name and description the om gave them; the rest of the folder
42
+ * follows, unlabelled. Curation raises signal; it does not gate access.
43
+ */
44
+ export declare function runFileLinks(root: string, folder: string): ResourceLink[];
45
+ /**
46
+ * Publish the two templates. Read-only by construction: there is no write path here, and
47
+ * no way to delete a run folder through MCP.
48
+ *
49
+ * `list` is `undefined` deliberately — enumerating every file of every historical run
50
+ * would be a large, mostly-useless listing. `get_run` returns resource links for the run a
51
+ * client actually asked about, which is the path that matters.
52
+ */
53
+ export declare function registerResources(server: McpServer, opts: {
54
+ root: string;
55
+ }): void;