omkit 0.4.8 → 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 +168 -12
- 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 +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
|
@@ -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[];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* Every identifier a file binds through an import — the whole allow-list for the outline.
|
|
4
|
+
* An action always arrives by import (`omkit/actions`, or a local actions module), so
|
|
5
|
+
* "was imported" is a cheap stand-in for "is a step" that never needs the type checker.
|
|
6
|
+
*/
|
|
7
|
+
export function importedNames(sf) {
|
|
8
|
+
const names = new Set();
|
|
9
|
+
for (const stmt of sf.statements) {
|
|
10
|
+
if (!ts.isImportDeclaration(stmt) || !stmt.importClause)
|
|
11
|
+
continue;
|
|
12
|
+
const clause = stmt.importClause;
|
|
13
|
+
if (clause.name)
|
|
14
|
+
names.add(clause.name.text);
|
|
15
|
+
const bindings = clause.namedBindings;
|
|
16
|
+
if (!bindings)
|
|
17
|
+
continue;
|
|
18
|
+
if (ts.isNamespaceImport(bindings))
|
|
19
|
+
names.add(bindings.name.text);
|
|
20
|
+
else
|
|
21
|
+
for (const el of bindings.elements)
|
|
22
|
+
names.add(el.name.text);
|
|
23
|
+
}
|
|
24
|
+
return names;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Walk a fluent chain down from any of its calls to the identifier call at its root.
|
|
28
|
+
*
|
|
29
|
+
* Descends through `.expression` rather than climbing through `.parent`: a `SourceFile` from
|
|
30
|
+
* `ts.createProgram` only has parent pointers once its file has been bound, so climbing would
|
|
31
|
+
* work or not depending on whether the caller happened to ask for diagnostics first.
|
|
32
|
+
*/
|
|
33
|
+
export function chainRoot(node) {
|
|
34
|
+
const calls = [];
|
|
35
|
+
let current = node;
|
|
36
|
+
while (ts.isCallExpression(current)) {
|
|
37
|
+
calls.push(current);
|
|
38
|
+
const callee = current.expression;
|
|
39
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
40
|
+
current = callee.expression;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ts.isIdentifier(callee))
|
|
44
|
+
return { name: callee.text, call: current, calls };
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A static sketch of what an om body calls: imported identifiers invoked directly inside
|
|
51
|
+
* `.run(body)`, in source order, each paired with the `.tag("…")` from its own fluent chain.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately an approximation, and labelled as one wherever it is rendered. A conditional
|
|
54
|
+
* call appears unconditionally, a loop appears once, an action picked dynamically does not
|
|
55
|
+
* appear at all, and a call made inside a module-level helper is invisible — the walk does
|
|
56
|
+
* not follow indirection. It answers "roughly what happens in here" and nothing stronger.
|
|
57
|
+
*/
|
|
58
|
+
export function outlineBody(body, imported) {
|
|
59
|
+
const calls = [];
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const visit = (node) => {
|
|
62
|
+
const chain = ts.isCallExpression(node) ? chainRoot(node) : undefined;
|
|
63
|
+
if (chain && imported.has(chain.name)) {
|
|
64
|
+
const tag = tagOf(chain);
|
|
65
|
+
// Name and tag together are the identity: three `command` calls with distinct tags are
|
|
66
|
+
// three steps, while the same call written twice is one.
|
|
67
|
+
const key = `${chain.name} ${tag ?? ""}`;
|
|
68
|
+
if (!seen.has(key)) {
|
|
69
|
+
seen.add(key);
|
|
70
|
+
calls.push(tag === undefined ? { name: chain.name } : { name: chain.name, tag });
|
|
71
|
+
}
|
|
72
|
+
// The chain's own spine is now accounted for; only its arguments can hold further steps.
|
|
73
|
+
// Walking it again would re-enter each inner call and record the same step repeatedly.
|
|
74
|
+
for (const call of chain.calls)
|
|
75
|
+
for (const arg of call.arguments)
|
|
76
|
+
visit(arg);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
ts.forEachChild(node, visit);
|
|
80
|
+
};
|
|
81
|
+
visit(body);
|
|
82
|
+
return calls;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The `.tag("…")` on a chain, taken nearest its root. `calls` is outermost-first, so the last
|
|
86
|
+
* match is the innermost — the tag attached directly to the action rather than to something
|
|
87
|
+
* further along the chain.
|
|
88
|
+
*/
|
|
89
|
+
function tagOf(chain) {
|
|
90
|
+
let tag;
|
|
91
|
+
for (const call of chain.calls) {
|
|
92
|
+
const callee = call.expression;
|
|
93
|
+
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "tag")
|
|
94
|
+
continue;
|
|
95
|
+
const arg = call.arguments[0];
|
|
96
|
+
if (arg && ts.isStringLiteralLike(arg))
|
|
97
|
+
tag = arg.text;
|
|
98
|
+
}
|
|
99
|
+
return tag;
|
|
100
|
+
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import type { JsonSchema } from "../core/schema-json.ts";
|
|
2
|
+
/** One step in an om's static outline: an imported call, with the tag authored on its chain. */
|
|
3
|
+
export interface OmCall {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** The `.tag("…")` literal from the same fluent chain, when the author wrote one. */
|
|
6
|
+
readonly tag?: string;
|
|
7
|
+
}
|
|
1
8
|
/** A runnable om discovered from a top-level `om("name", …)` call. */
|
|
2
9
|
export interface DiscoveredOm {
|
|
3
10
|
/** The om's name — its first string argument. */
|
|
@@ -6,6 +13,12 @@ export interface DiscoveredOm {
|
|
|
6
13
|
readonly file: string;
|
|
7
14
|
/** 1-based line of the `om(...)` call (informational). */
|
|
8
15
|
readonly line: number;
|
|
16
|
+
/**
|
|
17
|
+
* A static approximation of what the body calls, in source order. Absent when the walk
|
|
18
|
+
* found nothing recognisable. Never a contract — see {@link import("./outline.ts").outlineBody}
|
|
19
|
+
* for what it deliberately cannot see.
|
|
20
|
+
*/
|
|
21
|
+
readonly calls?: readonly OmCall[];
|
|
9
22
|
}
|
|
10
23
|
/** An inspectable action discovered from an exported `action("name")…` builder. */
|
|
11
24
|
export interface DiscoveredAction {
|
|
@@ -27,3 +40,49 @@ export interface Registry {
|
|
|
27
40
|
/** Non-fatal diagnostics (type errors, unresolved config) — discovery never throws for these. */
|
|
28
41
|
readonly warnings: string[];
|
|
29
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* What importing a file in discovery mode reveals — the metadata the AST scan cannot see,
|
|
45
|
+
* because `.describe()`, `.args()` and `.mcp()` only have values once the module has been
|
|
46
|
+
* evaluated. Produced by `discoverRegistrations`, never by `discover`.
|
|
47
|
+
*/
|
|
48
|
+
export interface Registration {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
/** Absolute path of the defining file. */
|
|
51
|
+
readonly file: string;
|
|
52
|
+
/** The `.describe({ summary })` line, when the author wrote one. */
|
|
53
|
+
readonly summary?: string;
|
|
54
|
+
/** Present only when the author called `.mcp()`. Absent ⇒ not exposed over MCP. */
|
|
55
|
+
readonly mcp?: {
|
|
56
|
+
readonly mode: "settling" | "long-lived";
|
|
57
|
+
};
|
|
58
|
+
/** The declared args as JSON Schema. Absent when none were declared. */
|
|
59
|
+
readonly inputSchema?: JsonSchema;
|
|
60
|
+
/** Why this entry cannot be called — currently only "its schema would not convert". */
|
|
61
|
+
readonly unavailable?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface OmRegistration extends Registration {
|
|
64
|
+
/**
|
|
65
|
+
* `<name>-<hash8>` — the run's log folder, derived from the name and the defining file
|
|
66
|
+
* by the same rule the runtime uses, so it is known before the om has ever run.
|
|
67
|
+
*/
|
|
68
|
+
readonly folderName: string;
|
|
69
|
+
}
|
|
70
|
+
export interface ActionRegistration extends Registration {
|
|
71
|
+
/** The exported binding (`export const <exportName> = action(...)`). */
|
|
72
|
+
readonly exportName: string;
|
|
73
|
+
}
|
|
74
|
+
/** The result of importing a project's candidate files in discovery mode. */
|
|
75
|
+
export interface RegistrationSet {
|
|
76
|
+
readonly oms: OmRegistration[];
|
|
77
|
+
readonly actions: ActionRegistration[];
|
|
78
|
+
/** Import failures and AST warnings — registration discovery never throws for these. */
|
|
79
|
+
readonly warnings: string[];
|
|
80
|
+
/**
|
|
81
|
+
* The AST scan that chose the candidate files, carried through so a caller wanting both
|
|
82
|
+
* halves — the outline and capability flags from the AST, the summaries and schemas from
|
|
83
|
+
* the fork — does not pay to build a second TypeScript program.
|
|
84
|
+
*
|
|
85
|
+
* Absent when the fork was driven with an explicit file list, since there was no scan.
|
|
86
|
+
*/
|
|
87
|
+
readonly registry?: Registry;
|
|
88
|
+
}
|
package/dist/client/runner.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { RunSession, RunOptions, InspectOptions } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The `tsx` loader, resolved to an absolute URL from omkit's own location. Resolving it here
|
|
4
|
+
* (not as the bare specifier `--import tsx`) keeps the child's `tsx` independent of its cwd —
|
|
5
|
+
* a run in any directory still finds the loader shipped with omkit.
|
|
6
|
+
*/
|
|
7
|
+
export declare const tsxLoader: string;
|
|
2
8
|
/**
|
|
3
9
|
* The inspector flag to prepend to a child's `execArgv` — empty when no inspector is requested.
|
|
4
10
|
* Plain `--inspect` opens the port without stopping, so the child still installs its teardown
|
|
@@ -10,6 +16,12 @@ export declare function inspectArgs(inspect: InspectOptions | undefined): string
|
|
|
10
16
|
* `OMKIT_SUPERVISED=1` flips the child into channel mode (see core/interaction.ts), and its
|
|
11
17
|
* IPC channel is wrapped into a {@link RunSession}. stdout/stderr are piped (not inherited)
|
|
12
18
|
* so the child never writes to the supervisor's terminal.
|
|
19
|
+
*
|
|
20
|
+
* The path is canonicalised first, and that is load-bearing rather than tidy — see
|
|
21
|
+
* {@link canonicalPath}. The child resolves `"omkit"` through `node_modules`, which is a
|
|
22
|
+
* symlink Node realpaths to the package's true on-disk name; forking with any other spelling
|
|
23
|
+
* of the same file gives the child a *second* copy of omkit, with its own module state. The
|
|
24
|
+
* run then exists in one copy while the om's actions look for it in the other.
|
|
13
25
|
*/
|
|
14
26
|
export declare function runOm(omFile: string, opts?: RunOptions): RunSession;
|
|
15
27
|
/**
|
package/dist/client/runner.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { fork } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { canonicalPath } from "../foundation/canonicalPath.js";
|
|
3
4
|
import { createChannel } from "./channel.js";
|
|
4
5
|
/**
|
|
5
6
|
* The `tsx` loader, resolved to an absolute URL from omkit's own location. Resolving it here
|
|
6
7
|
* (not as the bare specifier `--import tsx`) keeps the child's `tsx` independent of its cwd —
|
|
7
8
|
* a run in any directory still finds the loader shipped with omkit.
|
|
8
9
|
*/
|
|
9
|
-
const tsxLoader = import.meta.resolve("tsx");
|
|
10
|
+
export const tsxLoader = import.meta.resolve("tsx");
|
|
10
11
|
/**
|
|
11
12
|
* The inspector flag to prepend to a child's `execArgv` — empty when no inspector is requested.
|
|
12
13
|
* Plain `--inspect` opens the port without stopping, so the child still installs its teardown
|
|
@@ -22,12 +23,19 @@ export function inspectArgs(inspect) {
|
|
|
22
23
|
* `OMKIT_SUPERVISED=1` flips the child into channel mode (see core/interaction.ts), and its
|
|
23
24
|
* IPC channel is wrapped into a {@link RunSession}. stdout/stderr are piped (not inherited)
|
|
24
25
|
* so the child never writes to the supervisor's terminal.
|
|
26
|
+
*
|
|
27
|
+
* The path is canonicalised first, and that is load-bearing rather than tidy — see
|
|
28
|
+
* {@link canonicalPath}. The child resolves `"omkit"` through `node_modules`, which is a
|
|
29
|
+
* symlink Node realpaths to the package's true on-disk name; forking with any other spelling
|
|
30
|
+
* of the same file gives the child a *second* copy of omkit, with its own module state. The
|
|
31
|
+
* run then exists in one copy while the om's actions look for it in the other.
|
|
25
32
|
*/
|
|
26
33
|
export function runOm(omFile, opts = {}) {
|
|
27
|
-
const
|
|
34
|
+
const file = canonicalPath(omFile);
|
|
35
|
+
const child = fork(file, [], {
|
|
28
36
|
execArgv: [...inspectArgs(opts.inspect), "--import", tsxLoader],
|
|
29
|
-
cwd: opts.cwd ?? path.dirname(
|
|
30
|
-
env: { ...process.env, OMKIT_SUPERVISED: "1" },
|
|
37
|
+
cwd: opts.cwd ?? path.dirname(file),
|
|
38
|
+
env: { ...process.env, OMKIT_SUPERVISED: "1", ...opts.env },
|
|
31
39
|
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
32
40
|
});
|
|
33
41
|
const transport = {
|
|
@@ -57,11 +65,12 @@ export function runOm(omFile, opts = {}) {
|
|
|
57
65
|
export function spawnBare(omFile, opts = {}) {
|
|
58
66
|
// A bare run is unsupervised even if the parent process happens to carry the flag (e.g. a
|
|
59
67
|
// test runner spawned from inside a supervised run) — clear it so the child stays bare.
|
|
60
|
-
const env = { ...process.env };
|
|
68
|
+
const env = { ...process.env, ...opts.env };
|
|
61
69
|
delete env.OMKIT_SUPERVISED;
|
|
62
|
-
const
|
|
70
|
+
const file = canonicalPath(omFile);
|
|
71
|
+
const child = fork(file, [], {
|
|
63
72
|
execArgv: [...inspectArgs(opts.inspect), "--import", tsxLoader],
|
|
64
|
-
cwd: opts.cwd ?? path.dirname(
|
|
73
|
+
cwd: opts.cwd ?? path.dirname(file),
|
|
65
74
|
stdio: "inherit",
|
|
66
75
|
env,
|
|
67
76
|
});
|
package/dist/client/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LogEntry } from "../foundation/LogEntry.ts";
|
|
2
2
|
import type { PromptSpec } from "../core/interaction.ts";
|
|
3
|
-
import type { Registry } from "./registry.ts";
|
|
3
|
+
import type { Registry, RegistrationSet } from "./registry.ts";
|
|
4
4
|
/** A run's terminal verdict, resolved when the child settles. */
|
|
5
5
|
export interface Verdict {
|
|
6
6
|
readonly ok: boolean;
|
|
@@ -27,6 +27,8 @@ export interface RunOptions {
|
|
|
27
27
|
readonly cwd?: string;
|
|
28
28
|
/** When set, fork the child with the Node inspector open so a debugger can attach. */
|
|
29
29
|
readonly inspect?: InspectOptions;
|
|
30
|
+
/** Extra environment for the child, merged over the parent's — how `OMKIT_ARGS` travels. */
|
|
31
|
+
readonly env?: Record<string, string>;
|
|
30
32
|
}
|
|
31
33
|
/** A prompt the running om is waiting on — surfaced to the frontend, answered via `answer`. */
|
|
32
34
|
export interface PromptRequest {
|
|
@@ -57,6 +59,12 @@ export interface OmkitClient {
|
|
|
57
59
|
readonly tsconfig: string;
|
|
58
60
|
/** Statically scan the project's `tsconfig.omkit.json` for oms and actions. */
|
|
59
61
|
discover(): Promise<Registry>;
|
|
62
|
+
/**
|
|
63
|
+
* Read what the project's oms and actions declare, by importing them in a throwaway
|
|
64
|
+
* child. Unlike {@link OmkitClient.discover} this executes user code, so it is never on
|
|
65
|
+
* the `omkit ls` path — only a caller that needs real schemas should ask for it.
|
|
66
|
+
*/
|
|
67
|
+
discoverRegistrations(): Promise<RegistrationSet>;
|
|
60
68
|
/** Spawn an om file as a supervised child and return its live session (the Ink UI drives this). */
|
|
61
69
|
run(omFile: string, opts?: RunOptions): RunSession;
|
|
62
70
|
/** Spawn an om file bare — inherited stdio, no supervision — and resolve its exit code. */
|