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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ChromePageSource, type PageMatch } from "./chrome-page.ts";
|
|
2
|
+
export interface DrivePageOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Where the page is. Either a **CDP endpoint** (`host:port` or a URL) for a browser
|
|
5
|
+
* running in another process, or a **live handle** — a `Page`, `BrowserContext`, or
|
|
6
|
+
* `Browser` — published by an earlier action in this same run.
|
|
7
|
+
*
|
|
8
|
+
* Both matter, and for different reasons. The address form is plain data, so it can be
|
|
9
|
+
* described by a schema and called from outside the process; the handle form skips the
|
|
10
|
+
* network entirely when the browser is already yours, which is what an om driving its own
|
|
11
|
+
* stack wants. Defaults to the endpoint `browser` opens when given a debugging port.
|
|
12
|
+
*/
|
|
13
|
+
target?: ChromePageSource;
|
|
14
|
+
/** Which open tab to drive. Omit to take the first page the browser has open. */
|
|
15
|
+
match?: PageMatch;
|
|
16
|
+
/** Navigate the chosen tab here first. Omit to drive it wherever it already is. */
|
|
17
|
+
url?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The JS to run in the page, as an **expression** — `document.title`, not `return …`.
|
|
20
|
+
* Wrap several statements in an IIFE: `(() => { …; return x })()`. The result travels
|
|
21
|
+
* back through the run's log, so it has to be JSON-serialisable.
|
|
22
|
+
*/
|
|
23
|
+
js: string;
|
|
24
|
+
/** Save a PNG of the page afterwards and register it as a run artifact. Default true. */
|
|
25
|
+
screenshot?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** What {@link drivePage} reports back. */
|
|
28
|
+
export interface DrivePageResult {
|
|
29
|
+
/** Whatever the expression evaluated to. */
|
|
30
|
+
readonly value: unknown;
|
|
31
|
+
readonly title: string;
|
|
32
|
+
readonly url: string;
|
|
33
|
+
/** Absolute path of the screenshot, when one was taken. */
|
|
34
|
+
readonly screenshot?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Evaluate an expression in a live page and report what came back.
|
|
38
|
+
*
|
|
39
|
+
* Reaches its page either way round, and that is the point. Given a **handle** it drives a
|
|
40
|
+
* page an earlier action in the same run already published — the cheap path, and the one an
|
|
41
|
+
* om driving its own stack should take. Given a **CDP address** it attaches to a browser in
|
|
42
|
+
* another process entirely, which is what lets this action be described by a schema and
|
|
43
|
+
* called by name from outside: `browser` and {@link chromePage} publish capability handles,
|
|
44
|
+
* and a handle is not something a caller across a process boundary can pass.
|
|
45
|
+
*
|
|
46
|
+
* Nothing here is owned. `chromePage` closes only a CDP connection it opened, and closing a
|
|
47
|
+
* CDP connection disconnects the client rather than stopping the browser — so this finishing,
|
|
48
|
+
* failing, or being cancelled never tears down the stack it attached to, in either mode.
|
|
49
|
+
*/
|
|
50
|
+
export declare const drivePage: import("../core/types.ts").Action<[opts: DrivePageOptions], DrivePageResult, import("../core/types.ts").NoEvents, void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { action } from "../core/action.js";
|
|
3
|
+
import { chromePage } from "./chrome-page.js";
|
|
4
|
+
/**
|
|
5
|
+
* Evaluate an expression in a live page and report what came back.
|
|
6
|
+
*
|
|
7
|
+
* Reaches its page either way round, and that is the point. Given a **handle** it drives a
|
|
8
|
+
* page an earlier action in the same run already published — the cheap path, and the one an
|
|
9
|
+
* om driving its own stack should take. Given a **CDP address** it attaches to a browser in
|
|
10
|
+
* another process entirely, which is what lets this action be described by a schema and
|
|
11
|
+
* called by name from outside: `browser` and {@link chromePage} publish capability handles,
|
|
12
|
+
* and a handle is not something a caller across a process boundary can pass.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here is owned. `chromePage` closes only a CDP connection it opened, and closing a
|
|
15
|
+
* CDP connection disconnects the client rather than stopping the browser — so this finishing,
|
|
16
|
+
* failing, or being cancelled never tears down the stack it attached to, in either mode.
|
|
17
|
+
*/
|
|
18
|
+
export const drivePage = action("drivePage")
|
|
19
|
+
.describe({
|
|
20
|
+
summary: "Evaluate JS in a live page — a handle from this run, or a tab of a browser reached over CDP — and report the result.",
|
|
21
|
+
})
|
|
22
|
+
.run(async ({ snapshot, artifact, artifactsFolder }, opts) => {
|
|
23
|
+
const { target = "localhost:9222", match, url } = opts;
|
|
24
|
+
// `chromePage` is daemon-shaped: it stays alive until something stops it, so a run that
|
|
25
|
+
// only awaits its `.ref` never settles — the tree finishes while the process is still
|
|
26
|
+
// held open, and the caller waits out a timeout for a run that is already done. Keep the
|
|
27
|
+
// handle and release it, the same shape `tskb:build` uses for its watcher.
|
|
28
|
+
const attached = chromePage("target", target, {
|
|
29
|
+
...(url === undefined ? {} : { url }),
|
|
30
|
+
...(match === undefined ? {} : { match }),
|
|
31
|
+
}).tag("page:attach");
|
|
32
|
+
try {
|
|
33
|
+
const page = await attached.ref;
|
|
34
|
+
return await evaluateIn(page, opts, { snapshot, artifact, artifactsFolder });
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
// Releases the daemon whether the expression threw or not. Cancelling closes the CDP
|
|
38
|
+
// connection, which disconnects this client and leaves the browser to its owner.
|
|
39
|
+
attached.cancel();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
/** The work itself, split out so the daemon handle above has one obvious release point. */
|
|
43
|
+
async function evaluateIn(page, opts, { snapshot, artifact, artifactsFolder }) {
|
|
44
|
+
const { js, screenshot = true } = opts;
|
|
45
|
+
const title = await page.title();
|
|
46
|
+
void snapshot("page", { title, url: page.url() });
|
|
47
|
+
// Evaluated as an expression in page context. A string is sent verbatim, which is what
|
|
48
|
+
// keeps it clear of the esbuild `__name` rewriting that catches serialized *functions*
|
|
49
|
+
// under tsx — see the shim in chrome-page.ts.
|
|
50
|
+
let value;
|
|
51
|
+
try {
|
|
52
|
+
value = await page.evaluate(js);
|
|
53
|
+
}
|
|
54
|
+
catch (cause) {
|
|
55
|
+
// A page-side stack means little to a caller who only sent a string, so lead with the
|
|
56
|
+
// expression that failed and keep the original as the cause.
|
|
57
|
+
throw new Error(`the expression threw in the page: ${js}`, { cause });
|
|
58
|
+
}
|
|
59
|
+
void snapshot("result", { js, value });
|
|
60
|
+
console.log(`result: ${JSON.stringify(value) ?? "undefined"}`);
|
|
61
|
+
const result = { value, title, url: page.url() };
|
|
62
|
+
if (!screenshot)
|
|
63
|
+
return result;
|
|
64
|
+
const file = path.join(artifactsFolder, "page.png");
|
|
65
|
+
await page.screenshot({ path: file });
|
|
66
|
+
artifact("page.png", file, { description: `The page after evaluating: ${js}` });
|
|
67
|
+
return { ...result, screenshot: file };
|
|
68
|
+
}
|
package/dist/actions/index.d.ts
CHANGED
|
@@ -17,8 +17,10 @@ export type { WatchDirOptions, WatchDirEvents } from "./watch-dir.ts";
|
|
|
17
17
|
export { tailLog } from "./tail-log.ts";
|
|
18
18
|
export type { TailLogOptions } from "./tail-log.ts";
|
|
19
19
|
export { prompt } from "./prompt.ts";
|
|
20
|
-
export type { PromptOptions, InputPromptOptions, ChoicePromptOptions, PromptChoice, PromptVia, PromptEvents, } from "./prompt.ts";
|
|
21
|
-
export { chromePage } from "./chrome-page.ts";
|
|
22
|
-
export type { ChromePageOptions, ChromePageEvents, ChromePageSource, Page, Browser, BrowserContext, } from "./chrome-page.ts";
|
|
20
|
+
export type { PromptOptions, InputPromptOptions, ChoicePromptOptions, MultilinePromptOptions, MultilineUntil, PromptChoice, PromptVia, PromptEvents, } from "./prompt.ts";
|
|
21
|
+
export { chromePage, selectPage, pagesOf } from "./chrome-page.ts";
|
|
22
|
+
export type { ChromePageOptions, ChromePageEvents, ChromePageSource, PageMatch, Page, Browser, BrowserContext, } from "./chrome-page.ts";
|
|
23
23
|
export { browser } from "./browser.ts";
|
|
24
24
|
export type { BrowserOptions } from "./browser.ts";
|
|
25
|
+
export { drivePage } from "./drive-page.ts";
|
|
26
|
+
export type { DrivePageOptions, DrivePageResult } from "./drive-page.ts";
|
package/dist/actions/index.js
CHANGED
|
@@ -10,5 +10,6 @@ export { watch } from "./watch.js";
|
|
|
10
10
|
export { watchDir } from "./watch-dir.js";
|
|
11
11
|
export { tailLog } from "./tail-log.js";
|
|
12
12
|
export { prompt } from "./prompt.js";
|
|
13
|
-
export { chromePage } from "./chrome-page.js";
|
|
13
|
+
export { chromePage, selectPage, pagesOf } from "./chrome-page.js";
|
|
14
14
|
export { browser } from "./browser.js";
|
|
15
|
+
export { drivePage } from "./drive-page.js";
|
package/dist/actions/prompt.d.ts
CHANGED
|
@@ -20,7 +20,33 @@ export interface ChoicePromptOptions extends PromptCommon {
|
|
|
20
20
|
/** Default selection — a choice value or a 1-based index. Default: the first choice. */
|
|
21
21
|
default?: string | number;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
/** How a multiline prompt decides the user is done. */
|
|
24
|
+
export type MultilineUntil = "json" | string | ((text: string) => boolean);
|
|
25
|
+
export interface MultilinePromptOptions extends PromptCommon {
|
|
26
|
+
kind: "multiline";
|
|
27
|
+
/** A one-line type sketch printed under the message. */
|
|
28
|
+
hint?: string;
|
|
29
|
+
/** Default `"json"` — stop once the text parses as JSON. */
|
|
30
|
+
until?: MultilineUntil;
|
|
31
|
+
/** Value used on EOF with no input, or on timeout. Default "". */
|
|
32
|
+
default?: string;
|
|
33
|
+
}
|
|
34
|
+
export type PromptOptions = InputPromptOptions | ChoicePromptOptions | MultilinePromptOptions;
|
|
35
|
+
/** Reads one line, or `undefined` at EOF. */
|
|
36
|
+
export type LineReader = () => Promise<string | undefined>;
|
|
37
|
+
/**
|
|
38
|
+
* Accumulate lines until `until` says stop, or EOF. Exported for testing: keeping the
|
|
39
|
+
* termination rule separate from the terminal means it can be driven by a scripted
|
|
40
|
+
* reader instead of a TTY.
|
|
41
|
+
*
|
|
42
|
+
* `"json"` is self-terminating — pasting a pretty-printed blob just works, with no
|
|
43
|
+
* sentinel to explain. A sentinel line is excluded from the result.
|
|
44
|
+
*
|
|
45
|
+
* Note there is deliberately no Ctrl-D/EOF-by-keystroke terminator: readline treats it as
|
|
46
|
+
* a close, and `prompt` builds and tears down an interface per call, so it risks leaving
|
|
47
|
+
* stdin unusable for later prompts in the same run.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readUntil(read: LineReader, until: MultilineUntil): Promise<string>;
|
|
24
50
|
/** How a prompt's answer was decided. */
|
|
25
51
|
export type PromptVia = "input" | "default" | "timeout";
|
|
26
52
|
export interface PromptEvents {
|
|
@@ -31,10 +57,11 @@ export interface PromptEvents {
|
|
|
31
57
|
};
|
|
32
58
|
}
|
|
33
59
|
/**
|
|
34
|
-
* Asks the terminal a question — free-text **input** or a **
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* unattended run never blocks; teardown
|
|
60
|
+
* Asks the terminal a question — free-text **input**, a **choice**, or a **multiline**
|
|
61
|
+
* block (see {@link readUntil} for how it ends) — and resolves with the answer, which it
|
|
62
|
+
* also attaches as the handle (`prompt(...).ref` feeds another action). A timeout
|
|
63
|
+
* (default 30s) falls back to the default so an unattended run never blocks; teardown
|
|
64
|
+
* also unblocks a pending prompt.
|
|
38
65
|
*/
|
|
39
66
|
export declare const prompt: import("../index.ts").Action<[opts?: PromptOptions | undefined], string, PromptEvents, string>;
|
|
40
67
|
export {};
|
package/dist/actions/prompt.js
CHANGED
|
@@ -1,15 +1,56 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { action } from "../core/action.js";
|
|
3
3
|
import { activeSupervisor } from "../core/interaction.js";
|
|
4
|
+
/**
|
|
5
|
+
* Accumulate lines until `until` says stop, or EOF. Exported for testing: keeping the
|
|
6
|
+
* termination rule separate from the terminal means it can be driven by a scripted
|
|
7
|
+
* reader instead of a TTY.
|
|
8
|
+
*
|
|
9
|
+
* `"json"` is self-terminating — pasting a pretty-printed blob just works, with no
|
|
10
|
+
* sentinel to explain. A sentinel line is excluded from the result.
|
|
11
|
+
*
|
|
12
|
+
* Note there is deliberately no Ctrl-D/EOF-by-keystroke terminator: readline treats it as
|
|
13
|
+
* a close, and `prompt` builds and tears down an interface per call, so it risks leaving
|
|
14
|
+
* stdin unusable for later prompts in the same run.
|
|
15
|
+
*/
|
|
16
|
+
export async function readUntil(read, until) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
for (;;) {
|
|
19
|
+
const line = await read();
|
|
20
|
+
if (line === undefined)
|
|
21
|
+
break; // EOF
|
|
22
|
+
if (typeof until === "string" && until !== "json" && line === until)
|
|
23
|
+
break;
|
|
24
|
+
lines.push(line);
|
|
25
|
+
const text = lines.join("\n");
|
|
26
|
+
if (until === "json" && parses(text))
|
|
27
|
+
break;
|
|
28
|
+
if (typeof until === "function" && until(text))
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
function parses(text) {
|
|
34
|
+
if (text.trim() === "")
|
|
35
|
+
return false;
|
|
36
|
+
try {
|
|
37
|
+
JSON.parse(text);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
4
44
|
const asChoices = (choices) => choices.map((c) => (typeof c === "string" ? { label: c, value: c } : c));
|
|
5
45
|
/** Route the request over the supervisor channel, resolving the raw answer against the choices. */
|
|
6
46
|
async function askSupervisor(supervisor, ctx, waitSignal) {
|
|
7
47
|
const { opts, message, defaultValue, resolveRaw } = ctx;
|
|
8
48
|
const spec = {
|
|
9
|
-
kind: opts.kind === "choice" ? "choice" : "input",
|
|
49
|
+
kind: opts.kind === "choice" ? "choice" : opts.kind === "multiline" ? "multiline" : "input",
|
|
10
50
|
message,
|
|
11
51
|
default: defaultValue,
|
|
12
52
|
...(opts.kind === "choice" ? { choices: asChoices(opts.choices) } : {}),
|
|
53
|
+
...(opts.kind === "multiline" && opts.hint ? { hint: opts.hint } : {}),
|
|
13
54
|
};
|
|
14
55
|
const answer = await supervisor.request(spec, waitSignal);
|
|
15
56
|
const resolved = resolveRaw(answer.value);
|
|
@@ -18,9 +59,44 @@ async function askSupervisor(supervisor, ctx, waitSignal) {
|
|
|
18
59
|
return { value: resolved, via: answer.via ?? "input" };
|
|
19
60
|
}
|
|
20
61
|
/** Read the answer straight from the terminal (the bare, unsupervised path). */
|
|
21
|
-
async function askTerminal(query, resolveRaw, defaultValue, waitSignal) {
|
|
62
|
+
async function askTerminal(query, resolveRaw, defaultValue, waitSignal, multiline) {
|
|
22
63
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
23
64
|
try {
|
|
65
|
+
if (multiline) {
|
|
66
|
+
process.stdout.write(query);
|
|
67
|
+
// Iterate the interface instead of calling `rl.question` per line. A paste arrives as a
|
|
68
|
+
// single chunk and readline emits every line in it synchronously, so a one-shot `question`
|
|
69
|
+
// takes the first line and the rest are emitted with no listener and lost — which would
|
|
70
|
+
// break the one case this kind exists for. The async iterator queues lines and applies
|
|
71
|
+
// backpressure, so a pasted blob and a typed one read the same.
|
|
72
|
+
const it = rl[Symbol.asyncIterator]();
|
|
73
|
+
// Nothing can abort a pending `it.next()`, so an abort (prompt timeout or run teardown)
|
|
74
|
+
// closes the interface, which ends the iteration. `waitSignal` then tells that apart from
|
|
75
|
+
// a genuinely closed stream below.
|
|
76
|
+
const onAbort = () => void rl.close();
|
|
77
|
+
if (waitSignal.aborted)
|
|
78
|
+
onAbort();
|
|
79
|
+
else
|
|
80
|
+
waitSignal.addEventListener("abort", onAbort, { once: true });
|
|
81
|
+
try {
|
|
82
|
+
const read = async () => {
|
|
83
|
+
const { value, done } = await it.next();
|
|
84
|
+
return done ? undefined : value; // done → end of input
|
|
85
|
+
};
|
|
86
|
+
const text = await readUntil(read, multiline.until);
|
|
87
|
+
// Giving up is not end-of-input. Propagate so the action's outer catch labels the answer
|
|
88
|
+
// `timeout` and falls back to the default, the same as the `input` and `choice` kinds —
|
|
89
|
+
// half a JSON blob is not a useful answer, so the partial text is dropped here.
|
|
90
|
+
if (waitSignal.aborted)
|
|
91
|
+
throw new Error("prompt aborted");
|
|
92
|
+
if (text.trim() === "")
|
|
93
|
+
return { value: defaultValue, via: "default" };
|
|
94
|
+
return { value: text, via: "input" };
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
waitSignal.removeEventListener("abort", onAbort);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
24
100
|
const raw = (await rl.question(query, { signal: waitSignal })).trim();
|
|
25
101
|
if (raw === "")
|
|
26
102
|
return { value: defaultValue, via: "default" };
|
|
@@ -35,10 +111,11 @@ async function askTerminal(query, resolveRaw, defaultValue, waitSignal) {
|
|
|
35
111
|
}
|
|
36
112
|
}
|
|
37
113
|
/**
|
|
38
|
-
* Asks the terminal a question — free-text **input** or a **
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* unattended run never blocks; teardown
|
|
114
|
+
* Asks the terminal a question — free-text **input**, a **choice**, or a **multiline**
|
|
115
|
+
* block (see {@link readUntil} for how it ends) — and resolves with the answer, which it
|
|
116
|
+
* also attaches as the handle (`prompt(...).ref` feeds another action). A timeout
|
|
117
|
+
* (default 30s) falls back to the default so an unattended run never blocks; teardown
|
|
118
|
+
* also unblocks a pending prompt.
|
|
42
119
|
*/
|
|
43
120
|
export const prompt = action("prompt")
|
|
44
121
|
.emits()
|
|
@@ -50,7 +127,13 @@ export const prompt = action("prompt")
|
|
|
50
127
|
let query;
|
|
51
128
|
let defaultValue;
|
|
52
129
|
let resolveRaw;
|
|
53
|
-
if (opts.kind === "
|
|
130
|
+
if (opts.kind === "multiline") {
|
|
131
|
+
defaultValue = opts.default ?? "";
|
|
132
|
+
message = opts.message ?? "Input:";
|
|
133
|
+
query = opts.hint ? `${message}\n ${opts.hint}\n> ` : `${message}\n> `;
|
|
134
|
+
resolveRaw = (raw) => raw;
|
|
135
|
+
}
|
|
136
|
+
else if (opts.kind === "choice") {
|
|
54
137
|
const choices = asChoices(opts.choices);
|
|
55
138
|
const { default: def } = opts;
|
|
56
139
|
const picked = typeof def === "number"
|
|
@@ -83,6 +166,7 @@ export const prompt = action("prompt")
|
|
|
83
166
|
const waitSignal = AbortSignal.any([signal, onTimeout.signal]);
|
|
84
167
|
let value = defaultValue;
|
|
85
168
|
let via = "default";
|
|
169
|
+
const multiline = opts.kind === "multiline" ? { until: opts.until ?? "json" } : undefined;
|
|
86
170
|
const supervisor = activeSupervisor();
|
|
87
171
|
try {
|
|
88
172
|
// Supervised: hand off to whoever owns the terminal (Ink app / MCP client). Bare: read
|
|
@@ -90,7 +174,7 @@ export const prompt = action("prompt")
|
|
|
90
174
|
// silent supervisor (or nobody) can't wedge the run.
|
|
91
175
|
const outcome = supervisor
|
|
92
176
|
? await askSupervisor(supervisor, { opts, message, defaultValue, resolveRaw }, waitSignal)
|
|
93
|
-
: await askTerminal(query, resolveRaw, defaultValue, waitSignal);
|
|
177
|
+
: await askTerminal(query, resolveRaw, defaultValue, waitSignal, multiline);
|
|
94
178
|
value = outcome.value;
|
|
95
179
|
via = outcome.via;
|
|
96
180
|
}
|
|
@@ -13,12 +13,19 @@ export function helpText() {
|
|
|
13
13
|
" run [om] run an om — interactive picker if no om is given, else by name or file path",
|
|
14
14
|
" ls list the discovered oms and actions",
|
|
15
15
|
" check typecheck the project (tsc --noEmit)",
|
|
16
|
+
" skill generate .claude/skills/omkit-runs/SKILL.md from what this project exposes",
|
|
17
|
+
" mcp serve this project over MCP on stdio (for Claude and other assistants)",
|
|
16
18
|
" init scaffold a starter project (tsconfig.omkit.json + sample oms/ and actions/)",
|
|
17
19
|
" help show this help",
|
|
18
20
|
"",
|
|
19
21
|
"Flags:",
|
|
20
22
|
" --tsconfig <path> project config to scan (default: tsconfig.omkit.json)",
|
|
21
23
|
" --json machine-readable output (ls)",
|
|
24
|
+
" --describe import the oms to show their summaries (ls) — slower, runs no workflow",
|
|
25
|
+
" --check report drift and exit non-zero instead of writing (skill)",
|
|
26
|
+
" --out <path> write somewhere else (skill)",
|
|
27
|
+
" --root <path> where the skill is written and what its paths are relative to (skill)",
|
|
28
|
+
" — defaults to the enclosing repository",
|
|
22
29
|
" --inspect[=port] run the om in a forked child with the Node inspector open (default port 9229)",
|
|
23
30
|
" -h, --help show this help",
|
|
24
31
|
"",
|
|
@@ -16,7 +16,7 @@ const TSCONFIG = `{
|
|
|
16
16
|
const DEV_OM = `import { om } from "omkit";
|
|
17
17
|
import { command, healthcheck } from "omkit/actions";
|
|
18
18
|
|
|
19
|
-
om("dev"
|
|
19
|
+
om("dev").run(async () => {
|
|
20
20
|
command("npm run dev", { cwd: "api" }).tag("api");
|
|
21
21
|
const up = await healthcheck({ url: "http://localhost:3000" }).result;
|
|
22
22
|
if (!up.ok) return;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import type { Registry } from "../../client/registry.ts";
|
|
1
|
+
import type { RegistrationSet, Registry } from "../../client/registry.ts";
|
|
2
|
+
export interface FormatOptions {
|
|
3
|
+
readonly json?: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* What the oms and actions declare, from the discovery fork. Present only under
|
|
6
|
+
* `--describe`: reading it costs an import of every candidate file, which is why plain
|
|
7
|
+
* `ls` stays AST-only and prints exactly what it always did.
|
|
8
|
+
*/
|
|
9
|
+
readonly registrations?: RegistrationSet;
|
|
10
|
+
}
|
|
2
11
|
/** Render a {@link Registry} for the terminal (plain text) or as JSON. */
|
|
3
|
-
export declare function formatRegistry(registry: Registry, opts?:
|
|
4
|
-
json?: boolean;
|
|
5
|
-
}): string;
|
|
12
|
+
export declare function formatRegistry(registry: Registry, opts?: FormatOptions): string;
|
package/dist/cli/commands/ls.js
CHANGED
|
@@ -2,24 +2,44 @@ import path from "node:path";
|
|
|
2
2
|
/** Render a {@link Registry} for the terminal (plain text) or as JSON. */
|
|
3
3
|
export function formatRegistry(registry, opts = {}) {
|
|
4
4
|
if (opts.json)
|
|
5
|
-
return
|
|
6
|
-
const
|
|
7
|
-
lines
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const tags = [a.publishesCapability ? "ref" : "", a.events ? "events" : ""].filter(Boolean);
|
|
15
|
-
const suffix = tags.length ? ` [${tags.join(", ")}]` : "";
|
|
16
|
-
lines.push(` ${a.name} ${path.basename(a.file)}${suffix}`);
|
|
17
|
-
}
|
|
5
|
+
return renderJson(registry, opts.registrations);
|
|
6
|
+
const declared = index(opts.registrations);
|
|
7
|
+
const lines = [
|
|
8
|
+
`oms (${registry.oms.length})`,
|
|
9
|
+
...registry.oms.flatMap((om) => row(`${om.name} ${path.basename(om.file)}:${om.line}`, [], declared?.get(key(om.name, om.file)))),
|
|
10
|
+
"",
|
|
11
|
+
`actions (${registry.actions.length})`,
|
|
12
|
+
...registry.actions.flatMap((a) => row(`${a.name} ${path.basename(a.file)}`, [a.publishesCapability ? "ref" : "", a.events ? "events" : ""], declared?.get(key(a.name, a.file)))),
|
|
13
|
+
];
|
|
18
14
|
if (registry.warnings.length) {
|
|
19
|
-
lines.push("");
|
|
20
|
-
lines.push(`warnings (${registry.warnings.length})`);
|
|
15
|
+
lines.push("", `warnings (${registry.warnings.length})`);
|
|
21
16
|
for (const w of registry.warnings)
|
|
22
17
|
lines.push(` ${w}`);
|
|
23
18
|
}
|
|
24
19
|
return lines.join("\n");
|
|
25
20
|
}
|
|
21
|
+
/** One entry: its head line with any markers, plus an indented summary when one was declared. */
|
|
22
|
+
function row(head, tags, meta) {
|
|
23
|
+
const all = [...tags, ...mcpTags(meta)].filter(Boolean);
|
|
24
|
+
const suffix = all.length ? ` [${all.join(", ")}]` : "";
|
|
25
|
+
return meta?.summary ? [` ${head}${suffix}`, ` ${meta.summary}`] : [` ${head}${suffix}`];
|
|
26
|
+
}
|
|
27
|
+
function renderJson(registry, registrations) {
|
|
28
|
+
return JSON.stringify(registrations ? { ...registry, ...registrations } : registry, null, 2);
|
|
29
|
+
}
|
|
30
|
+
/** Declarations keyed by name-in-file, so a name reused across files cannot cross over. */
|
|
31
|
+
function index(set) {
|
|
32
|
+
if (!set)
|
|
33
|
+
return undefined;
|
|
34
|
+
const byKey = new Map();
|
|
35
|
+
for (const r of [...set.oms, ...set.actions])
|
|
36
|
+
byKey.set(key(r.name, r.file), r);
|
|
37
|
+
return byKey;
|
|
38
|
+
}
|
|
39
|
+
const key = (name, file) => `${name}\0${path.resolve(file)}`;
|
|
40
|
+
/** `[mcp]` / `[mcp, long-lived]` — only meaningful once the fork has told us what is exposed. */
|
|
41
|
+
function mcpTags(meta) {
|
|
42
|
+
if (!meta?.mcp)
|
|
43
|
+
return [];
|
|
44
|
+
return meta.mcp.mode === "long-lived" ? ["mcp", "long-lived"] : ["mcp"];
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OmkitClient } from "../../client/index.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The `mcp` command: serve this project over MCP on stdio, until the client hangs up.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately silent. Every other command prints; this one must not, because its stdout
|
|
6
|
+
* is the protocol stream — a banner here reaches the client as a JSON parse error.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mcpCommand(client: OmkitClient, opts: {
|
|
9
|
+
root: string;
|
|
10
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { startMcpServer } from "../../mcp/server.js";
|
|
2
|
+
/**
|
|
3
|
+
* The `mcp` command: serve this project over MCP on stdio, until the client hangs up.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately silent. Every other command prints; this one must not, because its stdout
|
|
6
|
+
* is the protocol stream — a banner here reaches the client as a JSON parse error.
|
|
7
|
+
*/
|
|
8
|
+
export async function mcpCommand(client, opts) {
|
|
9
|
+
await startMcpServer({ client, root: opts.root });
|
|
10
|
+
// `connect` resolves once the transport is bound; the transport keeps the process alive
|
|
11
|
+
// from here, so hold the command open rather than falling out of `main` and exiting.
|
|
12
|
+
await new Promise(() => { });
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { OmkitClient } from "../../client/index.ts";
|
|
2
|
+
export { skillRoot } from "../../skill/index.ts";
|
|
3
|
+
export interface SkillOptions {
|
|
4
|
+
/** Project root the skill is written relative to — the directory owning the tsconfig. */
|
|
5
|
+
readonly root: string;
|
|
6
|
+
/** Override the output path (still resolved against `root` when relative). */
|
|
7
|
+
readonly out?: string;
|
|
8
|
+
/** Compare hashes and exit non-zero on drift instead of writing. */
|
|
9
|
+
readonly check?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The `skill` command: generate `.claude/skills/omkit-runs/SKILL.md` from what this project's
|
|
13
|
+
* oms and actions declare, so an assistant knows what is runnable before its first tool call.
|
|
14
|
+
*
|
|
15
|
+
* Generation runs no workflow. It uses the same `OMKIT_DISCOVER=1` fork the MCP server's
|
|
16
|
+
* `list_oms` uses, which returns from `launch()` before an execution tree exists.
|
|
17
|
+
*/
|
|
18
|
+
export declare function skillCommand(client: OmkitClient, opts: SkillOptions): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { buildSkillModel, readExisting, renderSkill, writeSkill, DEFAULT_DESCRIPTION, SKILL_RELATIVE_PATH, } from "../../skill/index.js";
|
|
3
|
+
export { skillRoot } from "../../skill/index.js";
|
|
4
|
+
const EMPTY_REGISTRY = { oms: [], actions: [], warnings: [] };
|
|
5
|
+
/**
|
|
6
|
+
* The `skill` command: generate `.claude/skills/omkit-runs/SKILL.md` from what this project's
|
|
7
|
+
* oms and actions declare, so an assistant knows what is runnable before its first tool call.
|
|
8
|
+
*
|
|
9
|
+
* Generation runs no workflow. It uses the same `OMKIT_DISCOVER=1` fork the MCP server's
|
|
10
|
+
* `list_oms` uses, which returns from `launch()` before an execution tree exists.
|
|
11
|
+
*/
|
|
12
|
+
export async function skillCommand(client, opts) {
|
|
13
|
+
const file = path.resolve(opts.root, opts.out ?? SKILL_RELATIVE_PATH);
|
|
14
|
+
const registrations = await client.discoverRegistrations();
|
|
15
|
+
// The client's tsconfig is the one discovery actually ran against, so the commands the file
|
|
16
|
+
// renders are the commands that produced it — not a guess at what the reader should type.
|
|
17
|
+
const model = buildSkillModel(registrations, registrations.registry ?? EMPTY_REGISTRY, {
|
|
18
|
+
root: opts.root,
|
|
19
|
+
tsconfig: path.resolve(client.tsconfig),
|
|
20
|
+
});
|
|
21
|
+
const existing = readExisting(file);
|
|
22
|
+
if (opts.check) {
|
|
23
|
+
reportCheck(file, existing.hash, model.hash);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// The description is the skill's loading trigger and is authored, so an existing one always
|
|
27
|
+
// wins; the fallback is only ever written into a file that does not exist yet.
|
|
28
|
+
const content = renderSkill(model, { description: existing.description ?? DEFAULT_DESCRIPTION });
|
|
29
|
+
writeSkill(file, content);
|
|
30
|
+
const total = model.oms.length + model.actions.length;
|
|
31
|
+
console.log(`wrote ${path.relative(opts.root, file) || file}`);
|
|
32
|
+
console.log(` ${model.oms.length} om${model.oms.length === 1 ? "" : "s"}, ` +
|
|
33
|
+
`${model.actions.length} action${model.actions.length === 1 ? "" : "s"} ` +
|
|
34
|
+
`[registry-hash: ${model.hash}]`);
|
|
35
|
+
if (total === 0) {
|
|
36
|
+
console.log(" nothing is exposed yet — mark an om or action with .mcp() to list it");
|
|
37
|
+
}
|
|
38
|
+
reportWarnings(registrations.warnings);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `--check` is for a hook or a CI step, so it says what changed and sets the exit code rather
|
|
42
|
+
* than writing. A file with no marker at all counts as drift: it was hand-written or predates
|
|
43
|
+
* the generator, and either way regenerating is the fix.
|
|
44
|
+
*/
|
|
45
|
+
function reportCheck(file, existing, fresh) {
|
|
46
|
+
if (existing === fresh) {
|
|
47
|
+
console.log(`up to date [registry-hash: ${fresh}]`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const detail = existing === undefined ? "no registry-hash found" : `has ${existing}`;
|
|
51
|
+
console.error(`${file} is stale — ${detail}, project is ${fresh}`);
|
|
52
|
+
console.error("run `omkit skill` to regenerate");
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
}
|
|
55
|
+
/** Discovery degrades to warnings rather than failing, so they have to reach the terminal. */
|
|
56
|
+
function reportWarnings(warnings) {
|
|
57
|
+
if (warnings.length === 0)
|
|
58
|
+
return;
|
|
59
|
+
console.error(`\nwarnings (${warnings.length})`);
|
|
60
|
+
for (const w of warnings)
|
|
61
|
+
console.error(` ${w}`);
|
|
62
|
+
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ export interface Cli {
|
|
|
7
7
|
tsconfig: string;
|
|
8
8
|
/** Set by `--help`/`-h` (the `help` command is carried on `command` instead). */
|
|
9
9
|
help: boolean;
|
|
10
|
+
/** `ls --describe`: import the oms to read their summaries, instead of staying AST-only. */
|
|
11
|
+
describe: boolean;
|
|
12
|
+
/** `skill --check`: report drift and exit non-zero instead of writing. */
|
|
13
|
+
check: boolean;
|
|
14
|
+
/** `skill --out <path>`: write somewhere other than `.claude/skills/omkit-runs/SKILL.md`. */
|
|
15
|
+
out?: string;
|
|
16
|
+
/** `skill --root <path>`: override the repository the skill is written for and relative to. */
|
|
17
|
+
root?: string;
|
|
10
18
|
/** The Node inspector, if requested via `--inspect[=port]`. */
|
|
11
19
|
inspect?: InspectOptions;
|
|
12
20
|
}
|