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/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,17 @@
|
|
|
1
|
+
import type { OmkitClient } from "../../client/index.ts";
|
|
2
|
+
export interface SkillOptions {
|
|
3
|
+
/** Project root the skill is written relative to — the directory owning the tsconfig. */
|
|
4
|
+
readonly root: string;
|
|
5
|
+
/** Override the output path (still resolved against `root` when relative). */
|
|
6
|
+
readonly out?: string;
|
|
7
|
+
/** Compare hashes and exit non-zero on drift instead of writing. */
|
|
8
|
+
readonly check?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The `skill` command: generate `.claude/skills/omkit-runs/SKILL.md` from what this project's
|
|
12
|
+
* oms and actions declare, so an assistant knows what is runnable before its first tool call.
|
|
13
|
+
*
|
|
14
|
+
* Generation runs no workflow. It uses the same `OMKIT_DISCOVER=1` fork the MCP server's
|
|
15
|
+
* `list_oms` uses, which returns from `launch()` before an execution tree exists.
|
|
16
|
+
*/
|
|
17
|
+
export declare function skillCommand(client: OmkitClient, opts: SkillOptions): Promise<void>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { buildSkillModel, readExisting, renderSkill, writeSkill, DEFAULT_DESCRIPTION, SKILL_RELATIVE_PATH, } from "../../skill/index.js";
|
|
3
|
+
const EMPTY_REGISTRY = { oms: [], actions: [], warnings: [] };
|
|
4
|
+
/**
|
|
5
|
+
* The `skill` command: generate `.claude/skills/omkit-runs/SKILL.md` from what this project's
|
|
6
|
+
* oms and actions declare, so an assistant knows what is runnable before its first tool call.
|
|
7
|
+
*
|
|
8
|
+
* Generation runs no workflow. It uses the same `OMKIT_DISCOVER=1` fork the MCP server's
|
|
9
|
+
* `list_oms` uses, which returns from `launch()` before an execution tree exists.
|
|
10
|
+
*/
|
|
11
|
+
export async function skillCommand(client, opts) {
|
|
12
|
+
const file = path.resolve(opts.root, opts.out ?? SKILL_RELATIVE_PATH);
|
|
13
|
+
const registrations = await client.discoverRegistrations();
|
|
14
|
+
const model = buildSkillModel(registrations, registrations.registry ?? EMPTY_REGISTRY, opts.root);
|
|
15
|
+
const existing = readExisting(file);
|
|
16
|
+
if (opts.check) {
|
|
17
|
+
reportCheck(file, existing.hash, model.hash);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// The description is the skill's loading trigger and is authored, so an existing one always
|
|
21
|
+
// wins; the fallback is only ever written into a file that does not exist yet.
|
|
22
|
+
const content = renderSkill(model, { description: existing.description ?? DEFAULT_DESCRIPTION });
|
|
23
|
+
writeSkill(file, content);
|
|
24
|
+
const total = model.oms.length + model.actions.length;
|
|
25
|
+
console.log(`wrote ${path.relative(opts.root, file) || file}`);
|
|
26
|
+
console.log(` ${model.oms.length} om${model.oms.length === 1 ? "" : "s"}, ` +
|
|
27
|
+
`${model.actions.length} action${model.actions.length === 1 ? "" : "s"} ` +
|
|
28
|
+
`[registry-hash: ${model.hash}]`);
|
|
29
|
+
if (total === 0) {
|
|
30
|
+
console.log(" nothing is exposed yet — mark an om or action with .mcp() to list it");
|
|
31
|
+
}
|
|
32
|
+
reportWarnings(registrations.warnings);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `--check` is for a hook or a CI step, so it says what changed and sets the exit code rather
|
|
36
|
+
* than writing. A file with no marker at all counts as drift: it was hand-written or predates
|
|
37
|
+
* the generator, and either way regenerating is the fix.
|
|
38
|
+
*/
|
|
39
|
+
function reportCheck(file, existing, fresh) {
|
|
40
|
+
if (existing === fresh) {
|
|
41
|
+
console.log(`up to date [registry-hash: ${fresh}]`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const detail = existing === undefined ? "no registry-hash found" : `has ${existing}`;
|
|
45
|
+
console.error(`${file} is stale — ${detail}, project is ${fresh}`);
|
|
46
|
+
console.error("run `omkit skill` to regenerate");
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
/** Discovery degrades to warnings rather than failing, so they have to reach the terminal. */
|
|
50
|
+
function reportWarnings(warnings) {
|
|
51
|
+
if (warnings.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
console.error(`\nwarnings (${warnings.length})`);
|
|
54
|
+
for (const w of warnings)
|
|
55
|
+
console.error(` ${w}`);
|
|
56
|
+
}
|
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>`: the project the skill describes, when the tsconfig sits below it. */
|
|
17
|
+
root?: string;
|
|
10
18
|
/** The Node inspector, if requested via `--inspect[=port]`. */
|
|
11
19
|
inspect?: InspectOptions;
|
|
12
20
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -33,6 +33,10 @@ export function parseCli(argv) {
|
|
|
33
33
|
json: { type: "boolean", default: false },
|
|
34
34
|
tsconfig: { type: "string", default: "tsconfig.omkit.json" },
|
|
35
35
|
help: { type: "boolean", short: "h", default: false },
|
|
36
|
+
describe: { type: "boolean", default: false },
|
|
37
|
+
check: { type: "boolean", default: false },
|
|
38
|
+
out: { type: "string" },
|
|
39
|
+
root: { type: "string" },
|
|
36
40
|
},
|
|
37
41
|
allowPositionals: true,
|
|
38
42
|
});
|
|
@@ -42,6 +46,10 @@ export function parseCli(argv) {
|
|
|
42
46
|
json: Boolean(values.json),
|
|
43
47
|
tsconfig: values.tsconfig,
|
|
44
48
|
help: Boolean(values.help),
|
|
49
|
+
describe: Boolean(values.describe),
|
|
50
|
+
check: Boolean(values.check),
|
|
51
|
+
...(values.out === undefined ? {} : { out: values.out }),
|
|
52
|
+
...(values.root === undefined ? {} : { root: values.root }),
|
|
45
53
|
inspect,
|
|
46
54
|
};
|
|
47
55
|
}
|
|
@@ -62,6 +70,62 @@ async function checkCommand(client) {
|
|
|
62
70
|
await renderDiagnostics({ kind: n === 0 ? "ok" : "error", title, items }, n === 0 ? process.stdout : process.stderr);
|
|
63
71
|
process.exitCode = n === 0 ? 0 : 1;
|
|
64
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The `ls` command: print the discovered oms and actions.
|
|
75
|
+
*
|
|
76
|
+
* `--describe` is the only path here that imports user code. Without it `ls` is the AST scan
|
|
77
|
+
* and nothing else — the property that keeps it instant and safe to run in any repo, including
|
|
78
|
+
* one whose oms would start servers if they were evaluated.
|
|
79
|
+
*/
|
|
80
|
+
async function lsCommand(client, cli) {
|
|
81
|
+
const { formatRegistry } = await import("./commands/ls.js");
|
|
82
|
+
const { withSpinner } = await import("./ui/Report.js");
|
|
83
|
+
if (!cli.describe) {
|
|
84
|
+
const registry = await withSpinner("discovering…", () => client.discover());
|
|
85
|
+
console.log(formatRegistry(registry, { json: cli.json }));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const registrations = await withSpinner("reading declarations…", () => client.discoverRegistrations());
|
|
89
|
+
const registry = registrations.registry ?? (await client.discover());
|
|
90
|
+
console.log(formatRegistry(registry, { json: cli.json, registrations }));
|
|
91
|
+
}
|
|
92
|
+
/** The `init` command: scaffold a starter project in the current directory. */
|
|
93
|
+
async function initCommand() {
|
|
94
|
+
const { scaffold } = await import("./commands/init.js");
|
|
95
|
+
const { created, skipped } = scaffold(process.cwd());
|
|
96
|
+
for (const f of created)
|
|
97
|
+
console.log(`created ${f}`);
|
|
98
|
+
for (const f of skipped)
|
|
99
|
+
console.log(`exists, skipped ${f}`);
|
|
100
|
+
}
|
|
101
|
+
/** The `skill` command: generate the runnable-workflow map for an assistant to read. */
|
|
102
|
+
async function generateSkill(client, cli) {
|
|
103
|
+
const { skillCommand } = await import("./commands/skill.js");
|
|
104
|
+
// The root decides two things at once — where `.claude/` goes, and what the recorded paths are
|
|
105
|
+
// relative to — so it is one setting, not two. It defaults to the tsconfig's directory (the
|
|
106
|
+
// common layout, where they are the same place); `--root` covers the repo that keeps its om
|
|
107
|
+
// project in a subfolder, where the reader's cwd is the repo and so the paths must be too.
|
|
108
|
+
const root = cli.root ? path.resolve(cli.root) : path.dirname(path.resolve(cli.tsconfig));
|
|
109
|
+
await skillCommand(client, { root, check: cli.check, ...(cli.out ? { out: cli.out } : {}) });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Every command, by name. A table rather than a chain of `if`s: each entry is one lazy import
|
|
113
|
+
* and one call, so adding a command cannot quietly grow the entry point's branching.
|
|
114
|
+
*/
|
|
115
|
+
const COMMANDS = {
|
|
116
|
+
init: () => initCommand(),
|
|
117
|
+
ls: (client, cli) => lsCommand(client, cli),
|
|
118
|
+
check: (client) => checkCommand(client),
|
|
119
|
+
mcp: async (client) => {
|
|
120
|
+
const { mcpCommand } = await import("./commands/mcp.js");
|
|
121
|
+
await mcpCommand(client, { root: process.cwd() });
|
|
122
|
+
},
|
|
123
|
+
skill: (client, cli) => generateSkill(client, cli),
|
|
124
|
+
run: async (client, cli) => {
|
|
125
|
+
const { runCommand } = await import("./commands/run.js");
|
|
126
|
+
await runCommand(client, { target: cli.target, inspect: cli.inspect });
|
|
127
|
+
},
|
|
128
|
+
};
|
|
65
129
|
/** The bin entry: route to a lazily-imported command; owns stdout and the exit code. */
|
|
66
130
|
async function main() {
|
|
67
131
|
const cli = parseCli(process.argv.slice(2));
|
|
@@ -71,37 +135,15 @@ async function main() {
|
|
|
71
135
|
console.log(helpText());
|
|
72
136
|
return;
|
|
73
137
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.log(`created ${f}`);
|
|
81
|
-
for (const f of skipped)
|
|
82
|
-
console.log(`exists, skipped ${f}`);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
if (cli.command === "ls") {
|
|
86
|
-
const { formatRegistry } = await import("./commands/ls.js");
|
|
87
|
-
const { withSpinner } = await import("./ui/Report.js");
|
|
88
|
-
const registry = await withSpinner("discovering…", () => client.discover());
|
|
89
|
-
console.log(formatRegistry(registry, { json: cli.json }));
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (cli.command === "check") {
|
|
93
|
-
await checkCommand(client);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (cli.command === "run") {
|
|
97
|
-
const { runCommand } = await import("./commands/run.js");
|
|
98
|
-
await runCommand(client, { target: cli.target, inspect: cli.inspect });
|
|
138
|
+
const handler = COMMANDS[cli.command];
|
|
139
|
+
if (!handler) {
|
|
140
|
+
const { helpText } = await import("./commands/help.js");
|
|
141
|
+
console.error(`unknown command "${cli.command}"\n`);
|
|
142
|
+
console.error(helpText());
|
|
143
|
+
process.exitCode = 1;
|
|
99
144
|
return;
|
|
100
145
|
}
|
|
101
|
-
|
|
102
|
-
console.error(`unknown command "${cli.command}"\n`);
|
|
103
|
-
console.error(helpText());
|
|
104
|
-
process.exitCode = 1;
|
|
146
|
+
await handler(createOmkitClient({ tsconfig: cli.tsconfig, inspect: cli.inspect }), cli);
|
|
105
147
|
}
|
|
106
148
|
/**
|
|
107
149
|
* True when this module is the process entry point — not when a test imports `parseCli`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ActionRegistration } from "./registry.ts";
|
|
2
|
+
/** The parent's opening message: sent over IPC, so no argv length limit applies. */
|
|
3
|
+
export interface ScanMessage {
|
|
4
|
+
readonly kind: "scan";
|
|
5
|
+
readonly files: string[];
|
|
6
|
+
}
|
|
7
|
+
/** The child's closing message. Om registrations arrive separately, as each om is reached. */
|
|
8
|
+
export interface ActionsMessage {
|
|
9
|
+
readonly kind: "actions";
|
|
10
|
+
readonly actions: ActionRegistration[];
|
|
11
|
+
readonly warnings: string[];
|
|
12
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
function asAction(value) {
|
|
11
|
+
if (typeof value !== "function")
|
|
12
|
+
return undefined;
|
|
13
|
+
const candidate = value;
|
|
14
|
+
if (typeof candidate.actionName !== "string")
|
|
15
|
+
return undefined;
|
|
16
|
+
if (typeof candidate.describeArgs !== "function")
|
|
17
|
+
return undefined;
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The discovery child. Forked with `OMKIT_DISCOVER=1`, so importing a file makes every
|
|
22
|
+
* `om(...).run(...)` in it post a registration up this same channel instead of launching
|
|
23
|
+
* (see core/discovery-mode.ts). Actions post nothing — they are plain values — so their
|
|
24
|
+
* metadata is read off each module's exports here, afterwards.
|
|
25
|
+
*/
|
|
26
|
+
async function scan(files) {
|
|
27
|
+
const actions = [];
|
|
28
|
+
const warnings = [];
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
let mod;
|
|
31
|
+
try {
|
|
32
|
+
mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href)));
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
warnings.push(`${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
for (const [exportName, value] of Object.entries(mod)) {
|
|
39
|
+
const found = asAction(value);
|
|
40
|
+
if (!found)
|
|
41
|
+
continue;
|
|
42
|
+
const { inputSchema, schemaError } = found.describeArgs();
|
|
43
|
+
actions.push({
|
|
44
|
+
name: found.actionName,
|
|
45
|
+
exportName,
|
|
46
|
+
file,
|
|
47
|
+
...(found.description ? { summary: found.description.summary } : {}),
|
|
48
|
+
...(found.mcp ? { mcp: found.mcp } : {}),
|
|
49
|
+
...(inputSchema ? { inputSchema } : {}),
|
|
50
|
+
...(schemaError ? { unavailable: schemaError } : {}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Exit only once the message has actually flushed. `process.send` is asynchronous —
|
|
55
|
+
// exiting on the next line can drop the payload before it reaches the parent, and the
|
|
56
|
+
// parent would see a `close` with no `actions` message and report an empty project.
|
|
57
|
+
// A user module may also have started a server or a timer at import; nothing here is
|
|
58
|
+
// supervised and nothing will tear it down, so leave deliberately rather than linger.
|
|
59
|
+
const done = () => process.exit(0);
|
|
60
|
+
if (process.send) {
|
|
61
|
+
process.send({ kind: "actions", actions, warnings }, done);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
done();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
process.on("message", (message) => {
|
|
68
|
+
if (message?.kind === "scan") {
|
|
69
|
+
void scan(message.files);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Registry } from "./registry.ts";
|
|
1
|
+
import type { Registry, RegistrationSet } from "./registry.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Statically scan the project described by `tsconfigPath` for runnable oms and inspectable
|
|
4
4
|
* actions. Type errors in the user's oms/actions degrade to `warnings` (an editor-in-progress
|
|
@@ -6,3 +6,23 @@ import type { Registry } from "./registry.ts";
|
|
|
6
6
|
* tsconfig — throws, so the CLI crashes visibly instead of silently returning an empty project.
|
|
7
7
|
*/
|
|
8
8
|
export declare function discover(tsconfigPath: string): Registry;
|
|
9
|
+
/**
|
|
10
|
+
* Import `files` in a throwaway child with `OMKIT_DISCOVER=1` and collect what they
|
|
11
|
+
* declare. Exposed separately from {@link discoverRegistrations} so the fork can be driven
|
|
12
|
+
* with an explicit file list — by tests, and by anything that already knows its candidates.
|
|
13
|
+
*
|
|
14
|
+
* Never rejects for user-code problems: an import failure, a crashed child, or a child
|
|
15
|
+
* that outstays its timeout all come back as warnings on an otherwise usable set.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readRegistrations(files: string[], opts?: {
|
|
18
|
+
cwd?: string;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
}): Promise<RegistrationSet>;
|
|
21
|
+
/**
|
|
22
|
+
* The full pass: the AST scan picks the candidate files — it stays the only cheap,
|
|
23
|
+
* never-executing phase — then {@link readRegistrations} imports just those. AST warnings
|
|
24
|
+
* are carried through so nothing is lost between the two phases.
|
|
25
|
+
*/
|
|
26
|
+
export declare function discoverRegistrations(tsconfigPath: string, opts?: {
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}): Promise<RegistrationSet>;
|
package/dist/client/discovery.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { fork } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { omHash } from "../foundation/ids.js";
|
|
6
|
+
import { fsSafe } from "../foundation/fsSafe.js";
|
|
7
|
+
import { canonicalPath } from "../foundation/canonicalPath.js";
|
|
8
|
+
import { tsxLoader } from "./runner.js";
|
|
9
|
+
import { chainRoot, importedNames, outlineBody } from "./outline.js";
|
|
3
10
|
/**
|
|
4
11
|
* Statically scan the project described by `tsconfigPath` for runnable oms and inspectable
|
|
5
12
|
* actions. Type errors in the user's oms/actions degrade to `warnings` (an editor-in-progress
|
|
@@ -20,16 +27,28 @@ export function discover(tsconfigPath) {
|
|
|
20
27
|
for (const sf of program.getSourceFiles()) {
|
|
21
28
|
if (!fileSet.has(canonical(sf.fileName)))
|
|
22
29
|
continue;
|
|
30
|
+
// TypeScript hands back its own spelling of the path — on Windows it lowercases the drive
|
|
31
|
+
// letter — and every consumer downstream keys on this string: the discovery fork imports
|
|
32
|
+
// it, `omHash` hashes it, and Node caches modules by it. Canonicalise once here so the
|
|
33
|
+
// whole chain agrees with the filesystem, and with the paths a real run reports.
|
|
34
|
+
const file = canonicalPath(sf.fileName);
|
|
23
35
|
for (const d of program.getSemanticDiagnostics(sf))
|
|
24
36
|
warnings.push(formatDiagnostic(d));
|
|
25
37
|
const names = omkitImports(sf);
|
|
38
|
+
const outlines = outlinesIn(sf, names.om, importedNames(sf));
|
|
26
39
|
const visit = (node) => {
|
|
27
40
|
if (ts.isCallExpression(node) &&
|
|
28
41
|
ts.isIdentifier(node.expression) &&
|
|
29
42
|
(node.expression.text === names.om || node.expression.text === names.step)) {
|
|
30
43
|
const arg = node.arguments[0];
|
|
31
44
|
if (arg && ts.isStringLiteralLike(arg)) {
|
|
32
|
-
|
|
45
|
+
const calls = outlines.get(arg.text);
|
|
46
|
+
oms.push({
|
|
47
|
+
name: arg.text,
|
|
48
|
+
file,
|
|
49
|
+
line: lineOf(sf, node),
|
|
50
|
+
...(calls?.length ? { calls } : {}),
|
|
51
|
+
});
|
|
33
52
|
}
|
|
34
53
|
}
|
|
35
54
|
ts.forEachChild(node, visit);
|
|
@@ -45,7 +64,7 @@ export function discover(tsconfigPath) {
|
|
|
45
64
|
if (info) {
|
|
46
65
|
actions.push({
|
|
47
66
|
name: info.name,
|
|
48
|
-
file
|
|
67
|
+
file,
|
|
49
68
|
exportName: decl.name.text,
|
|
50
69
|
publishesCapability: info.ref,
|
|
51
70
|
events: info.emits,
|
|
@@ -56,6 +75,124 @@ export function discover(tsconfigPath) {
|
|
|
56
75
|
}
|
|
57
76
|
return { oms, actions, warnings };
|
|
58
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Outline every `om("name")….run(body)` chain in a file, keyed by om name.
|
|
80
|
+
*
|
|
81
|
+
* Collected in its own pass rather than alongside the `om(...)` match, because a body is only
|
|
82
|
+
* reachable from the `.run` end of the chain and the om name only from the other — and the
|
|
83
|
+
* chain is walked downward (see {@link chainRoot}), so one pass cannot have both.
|
|
84
|
+
*/
|
|
85
|
+
function outlinesIn(sf, omName, imported) {
|
|
86
|
+
const outlines = new Map();
|
|
87
|
+
if (omName === undefined)
|
|
88
|
+
return outlines;
|
|
89
|
+
const visit = (node) => {
|
|
90
|
+
if (ts.isCallExpression(node) &&
|
|
91
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
92
|
+
node.expression.name.text === "run") {
|
|
93
|
+
const root = chainRoot(node);
|
|
94
|
+
const body = node.arguments[0];
|
|
95
|
+
const name = root?.call.arguments[0];
|
|
96
|
+
if (root?.name === omName && name && ts.isStringLiteralLike(name) && body) {
|
|
97
|
+
if (ts.isFunctionLike(body))
|
|
98
|
+
outlines.set(name.text, outlineBody(body, imported));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
ts.forEachChild(node, visit);
|
|
102
|
+
};
|
|
103
|
+
visit(sf);
|
|
104
|
+
return outlines;
|
|
105
|
+
}
|
|
106
|
+
/** How long the child gets to import every file before it is killed and reported. */
|
|
107
|
+
const DISCOVER_TIMEOUT_MS = 30_000;
|
|
108
|
+
/**
|
|
109
|
+
* Absolute path of a sibling module, carrying the extension this build actually uses —
|
|
110
|
+
* `.ts` under tsx and vitest, `.js` once compiled. `rewriteRelativeImportExtensions`
|
|
111
|
+
* rewrites import specifiers; this path is data (a fork target), so it does not.
|
|
112
|
+
*/
|
|
113
|
+
function siblingScript(name) {
|
|
114
|
+
const here = fileURLToPath(import.meta.url);
|
|
115
|
+
return path.join(path.dirname(here), `${name}${path.extname(here)}`);
|
|
116
|
+
}
|
|
117
|
+
/** Project an om's registration message onto the client-facing shape, folder name and all. */
|
|
118
|
+
function toOmRegistration(m) {
|
|
119
|
+
return {
|
|
120
|
+
name: m.name,
|
|
121
|
+
file: m.file ?? "",
|
|
122
|
+
folderName: `${fsSafe(m.name)}-${omHash(m.name, m.file)}`,
|
|
123
|
+
...(m.description ? { summary: m.description.summary } : {}),
|
|
124
|
+
...(m.mcp ? { mcp: m.mcp } : {}),
|
|
125
|
+
...(m.inputSchema ? { inputSchema: m.inputSchema } : {}),
|
|
126
|
+
...(m.schemaError ? { unavailable: m.schemaError } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Import `files` in a throwaway child with `OMKIT_DISCOVER=1` and collect what they
|
|
131
|
+
* declare. Exposed separately from {@link discoverRegistrations} so the fork can be driven
|
|
132
|
+
* with an explicit file list — by tests, and by anything that already knows its candidates.
|
|
133
|
+
*
|
|
134
|
+
* Never rejects for user-code problems: an import failure, a crashed child, or a child
|
|
135
|
+
* that outstays its timeout all come back as warnings on an otherwise usable set.
|
|
136
|
+
*/
|
|
137
|
+
export function readRegistrations(files, opts = {}) {
|
|
138
|
+
if (files.length === 0)
|
|
139
|
+
return Promise.resolve({ oms: [], actions: [], warnings: [] });
|
|
140
|
+
const timeoutMs = opts.timeoutMs ?? DISCOVER_TIMEOUT_MS;
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
const oms = [];
|
|
143
|
+
const warnings = [];
|
|
144
|
+
const child = fork(siblingScript("discover-child"), [], {
|
|
145
|
+
execArgv: ["--import", tsxLoader],
|
|
146
|
+
cwd: opts.cwd ?? path.dirname(files[0]),
|
|
147
|
+
env: { ...process.env, OMKIT_DISCOVER: "1" },
|
|
148
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
149
|
+
});
|
|
150
|
+
let settled = false;
|
|
151
|
+
const finish = (actions) => {
|
|
152
|
+
if (settled)
|
|
153
|
+
return;
|
|
154
|
+
settled = true;
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
157
|
+
child.kill();
|
|
158
|
+
resolve({ oms, actions, warnings });
|
|
159
|
+
};
|
|
160
|
+
const timer = setTimeout(() => {
|
|
161
|
+
warnings.push(`discovery timed out after ${timeoutMs}ms`);
|
|
162
|
+
finish([]);
|
|
163
|
+
}, timeoutMs);
|
|
164
|
+
child.on("message", (message) => {
|
|
165
|
+
const m = message;
|
|
166
|
+
if (m.kind === "om-registration")
|
|
167
|
+
oms.push(toOmRegistration(m));
|
|
168
|
+
else if (m.kind === "actions") {
|
|
169
|
+
warnings.push(...m.warnings);
|
|
170
|
+
finish(m.actions);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
// A child that dies before reporting still yields whatever streamed in before it did.
|
|
174
|
+
child.on("close", () => finish([]));
|
|
175
|
+
child.on("error", (e) => {
|
|
176
|
+
warnings.push(`discovery child failed: ${e.message}`);
|
|
177
|
+
finish([]);
|
|
178
|
+
});
|
|
179
|
+
child.send({ kind: "scan", files });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The full pass: the AST scan picks the candidate files — it stays the only cheap,
|
|
184
|
+
* never-executing phase — then {@link readRegistrations} imports just those. AST warnings
|
|
185
|
+
* are carried through so nothing is lost between the two phases.
|
|
186
|
+
*/
|
|
187
|
+
export async function discoverRegistrations(tsconfigPath, opts = {}) {
|
|
188
|
+
const ast = discover(tsconfigPath);
|
|
189
|
+
const files = [...new Set([...ast.oms.map((o) => o.file), ...ast.actions.map((a) => a.file)])];
|
|
190
|
+
const set = await readRegistrations(files, {
|
|
191
|
+
cwd: path.dirname(path.resolve(tsconfigPath)),
|
|
192
|
+
...opts,
|
|
193
|
+
});
|
|
194
|
+
return { ...set, warnings: [...ast.warnings, ...set.warnings], registry: ast };
|
|
195
|
+
}
|
|
59
196
|
/**
|
|
60
197
|
* Load and validate the tsconfig, throwing on a fatal config problem — a missing/unparseable file,
|
|
61
198
|
* or a config-level error (bad compilerOptions, an unresolvable `extends`). The benign "no inputs
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OmkitClient } from "./types.ts";
|
|
2
|
+
export { readRegistrations, discoverRegistrations } from "./discovery.ts";
|
|
2
3
|
export type { OmkitClient, RunSession, RunOptions, InspectOptions, Verdict, Diagnostic, PromptRequest, RunEvents, } from "./types.ts";
|
|
3
|
-
export type { Registry, DiscoveredOm, DiscoveredAction } from "./registry.ts";
|
|
4
|
+
export type { Registry, DiscoveredOm, DiscoveredAction, Registration, OmRegistration, ActionRegistration, RegistrationSet, } from "./registry.ts";
|
|
4
5
|
import type { InspectOptions } from "./types.ts";
|
|
5
6
|
/** Configuration for a client — the project's `tsconfig.omkit.json` and optional debugging. */
|
|
6
7
|
export interface OmkitConfig {
|
package/dist/client/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { discover } from "./discovery.js";
|
|
1
|
+
import { discover, discoverRegistrations } from "./discovery.js";
|
|
2
2
|
import { runOm, spawnBare } from "./runner.js";
|
|
3
3
|
import { runCheck } from "./check.js";
|
|
4
|
+
export { readRegistrations, discoverRegistrations } from "./discovery.js";
|
|
4
5
|
/** Build the headless engine: discovery + supervised/bare run + typecheck over one config. */
|
|
5
6
|
export function createOmkitClient(config) {
|
|
6
7
|
// A per-call inspect wins; the client's construction-time inspect is the fallback.
|
|
@@ -11,6 +12,7 @@ export function createOmkitClient(config) {
|
|
|
11
12
|
return {
|
|
12
13
|
tsconfig: config.tsconfig,
|
|
13
14
|
discover: async () => discover(config.tsconfig),
|
|
15
|
+
discoverRegistrations: async () => discoverRegistrations(config.tsconfig),
|
|
14
16
|
run: (omFile, opts) => runOm(omFile, withInspect(opts)),
|
|
15
17
|
runBare: (omFile, opts) => spawnBare(omFile, withInspect(opts)),
|
|
16
18
|
check: async () => runCheck(config.tsconfig),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { OmCall } from "./registry.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Every identifier a file binds through an import — the whole allow-list for the outline.
|
|
5
|
+
* An action always arrives by import (`omkit/actions`, or a local actions module), so
|
|
6
|
+
* "was imported" is a cheap stand-in for "is a step" that never needs the type checker.
|
|
7
|
+
*/
|
|
8
|
+
export declare function importedNames(sf: ts.SourceFile): ReadonlySet<string>;
|
|
9
|
+
/** A fluent chain resolved down to the call that starts it: `a(…).b().c()` → the `a(…)`. */
|
|
10
|
+
export interface ChainRoot {
|
|
11
|
+
/** The identifier the chain is rooted in. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/** The base call itself, so a caller can read its arguments. */
|
|
14
|
+
readonly call: ts.CallExpression;
|
|
15
|
+
/** Every call in the chain, outermost first. */
|
|
16
|
+
readonly calls: readonly ts.CallExpression[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Walk a fluent chain down from any of its calls to the identifier call at its root.
|
|
20
|
+
*
|
|
21
|
+
* Descends through `.expression` rather than climbing through `.parent`: a `SourceFile` from
|
|
22
|
+
* `ts.createProgram` only has parent pointers once its file has been bound, so climbing would
|
|
23
|
+
* work or not depending on whether the caller happened to ask for diagnostics first.
|
|
24
|
+
*/
|
|
25
|
+
export declare function chainRoot(node: ts.CallExpression): ChainRoot | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* A static sketch of what an om body calls: imported identifiers invoked directly inside
|
|
28
|
+
* `.run(body)`, in source order, each paired with the `.tag("…")` from its own fluent chain.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately an approximation, and labelled as one wherever it is rendered. A conditional
|
|
31
|
+
* call appears unconditionally, a loop appears once, an action picked dynamically does not
|
|
32
|
+
* appear at all, and a call made inside a module-level helper is invisible — the walk does
|
|
33
|
+
* not follow indirection. It answers "roughly what happens in here" and nothing stronger.
|
|
34
|
+
*/
|
|
35
|
+
export declare function outlineBody(body: ts.Node, imported: ReadonlySet<string>): OmCall[];
|