omkit 0.8.0 → 0.8.2
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 +49 -0
- package/README.md +2 -2
- package/dist/cli/commands/help.js +2 -1
- package/dist/cli/commands/skill.d.ts +1 -0
- package/dist/cli/commands/skill.js +7 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +14 -8
- package/dist/client/index.js +12 -4
- package/dist/client/types.d.ts +5 -0
- package/dist/client/types.js +5 -1
- package/dist/core/ExecutionTree.d.ts +8 -2
- package/dist/core/ExecutionTree.js +8 -2
- package/dist/foundation/ids.d.ts +12 -0
- package/dist/foundation/ids.js +22 -1
- package/dist/mcp/tools.js +16 -12
- package/dist/output/folder/RunFolder.js +16 -1
- package/dist/skill/index.d.ts +2 -1
- package/dist/skill/index.js +1 -0
- package/dist/skill/model.d.ts +16 -2
- package/dist/skill/model.js +94 -10
- package/dist/skill/render.js +45 -19
- package/dist/skill/root.d.ts +15 -0
- package/dist/skill/root.js +27 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -46,6 +46,47 @@
|
|
|
46
46
|
|
|
47
47
|
### Fixed
|
|
48
48
|
|
|
49
|
+
- **`typescript` moved from `devDependencies` to `dependencies`.** Three shipped modules import
|
|
50
|
+
it at runtime, and one of them — discovery — runs on every command, so an install that did not
|
|
51
|
+
already happen to have TypeScript nearby failed with `ERR_MODULE_NOT_FOUND` on `omkit ls`. It
|
|
52
|
+
went unnoticed because a monorepo hoists it into place.
|
|
53
|
+
- **A node's log file no longer spells its name out as directories.** A node's id is
|
|
54
|
+
`<name>_<shortId>`, ids are joined with `/` into node paths, and those paths become log file
|
|
55
|
+
paths — so a name carrying its own separators was read as tree nesting. A `command` node is
|
|
56
|
+
named after its whole command line, and one watcher's log ended up nine directories deep, with
|
|
57
|
+
`docs`, `packages` and `tskb` as real folders inside the run. Names are now sanitised where the
|
|
58
|
+
id is made, which is the only place the two kinds of separator are still distinguishable, and
|
|
59
|
+
the name's contribution to an id is capped so a long command line cannot push a log path past
|
|
60
|
+
Windows' 260-character limit. Displayed names are untouched: nodes are shown by name, never by
|
|
61
|
+
id.
|
|
62
|
+
|
|
63
|
+
- **One project, one `logs/` tree.** A run's record now lands under the project that owns the
|
|
64
|
+
config, wherever the run was started from. It used to follow the working directory, and the
|
|
65
|
+
frontends chose different ones: `omkit run` set it to the om file's own directory, so a project
|
|
66
|
+
with oms in several folders accumulated a `logs/` beside each of them, while the MCP server used
|
|
67
|
+
the directory it happened to be launched in — with the result that a run started in the terminal
|
|
68
|
+
and a run started by an assistant wrote to two different trees, and neither could read the
|
|
69
|
+
other's record. The child's cwd is unchanged, so relative paths inside an om body still resolve
|
|
70
|
+
where their author reads them. Folder _names_ are unchanged; only their parent moves, so records
|
|
71
|
+
written before this land in the old location and stay readable there.
|
|
72
|
+
- **A generated skill now documents commands that work.** `omkit skill` was given a config path and
|
|
73
|
+
dropped it from everything it wrote, so a project whose om files live in a subfolder got a file
|
|
74
|
+
whose every command failed at the root the file itself is read from. The `--tsconfig` the project
|
|
75
|
+
needs is now carried into each rendered command, including the `claude mcp add` line, and the run
|
|
76
|
+
folder is spelled from the root rather than as a bare `logs/`.
|
|
77
|
+
- **An om name with a space is quoted** in the commands the skill writes. Names are free text, and
|
|
78
|
+
`omkit run DTF Tests` resolves the target as `DTF` and leaves `Tests` as a stray positional.
|
|
79
|
+
- **The shell example is one that can actually be run.** It was whichever om sorted first, which for
|
|
80
|
+
one real project was a long-lived one — an example that hangs, two paragraphs above the heading
|
|
81
|
+
saying it never finishes. A settling om is preferred; when a project has none, the example says so
|
|
82
|
+
on the line itself.
|
|
83
|
+
- **The `OMKIT_ARGS` example is built from the schema** instead of by pattern-matching the rendered
|
|
84
|
+
type sketch. The old reading recognised `{ field: type` and nothing else, so an om whose first
|
|
85
|
+
field was an enum silently produced `OMKIT_ARGS='{}'` — syntactically fine, and wrong about the
|
|
86
|
+
project. Examples now carry every required field (or one optional field when nothing is required),
|
|
87
|
+
take an enum's first member and a declared default where there is one, and are omitted entirely
|
|
88
|
+
when no complete example can be built.
|
|
89
|
+
|
|
49
90
|
- **Two spellings of one path no longer mean two different things** (Windows). A
|
|
50
91
|
case-insensitive filesystem let `d:\repo\om.ts` and `D:\repo\om.ts` name the same file, and
|
|
51
92
|
omkit keyed on the string twice. Node caches ES modules by URL, so a child forked with one
|
|
@@ -59,6 +100,14 @@
|
|
|
59
100
|
|
|
60
101
|
### Changed
|
|
61
102
|
|
|
103
|
+
- **`omkit skill --root` now defaults to the enclosing repository**, not to the tsconfig's own
|
|
104
|
+
directory. The root decides where `.claude/` goes and what the recorded paths are relative to,
|
|
105
|
+
and a skill file is read with the repository as the working directory — so the old default was
|
|
106
|
+
wrong for exactly the layout (`om/` beside the code it drives) that needed it, and had to be
|
|
107
|
+
corrected with a second flag. `--root` remains, for a project deliberately not at the top of its
|
|
108
|
+
checkout.
|
|
109
|
+
- `RunOptions.env` now carries `OMKIT_ROOT` by default — the project owning the client's config.
|
|
110
|
+
A caller may still set its own; the MCP server does, naming the root it serves resources from.
|
|
62
111
|
- `DiscoveredOm` gained `calls`, so `omkit ls --json` carries the outline. Additive, and derived
|
|
63
112
|
statically, so `discover()` is still the pass that never executes user code.
|
|
64
113
|
- The generated `description` in the skill's frontmatter is preserved across regeneration. It is
|
package/README.md
CHANGED
|
@@ -192,7 +192,7 @@ Every action's output, events, asserts, snapshots, and lifecycle land on **one t
|
|
|
192
192
|
|
|
193
193
|
`ctx` also gives each action `assert(cond, msg)` (tallies into the run's verdict), `snapshot(name, value)` (captures JSON state to the run folder), and `artifact(name, file, opts?)` (labels a file the action wrote — name, optional description, MIME inferred from the extension — and returns its absolute path) — so a run is an inspectable artifact, not just an exit code. Write the files you label under `ctx.artifactsFolder`, the run's own output folder.
|
|
194
194
|
|
|
195
|
-
Run folders have a **stable identity**: `logs/<name>-<hash8>/<date>/<time>/`, keyed by the om's name and the file that defines it. An assistant (or a script) can always find "the latest `tskb-dev` run" without parsing scrollback, diff two runs of the same pipeline, or answer questions with evidence instead of inference: _did the server actually pass its healthcheck? which process died first? what config did the build run with?_ It's all in the folder — attributed per action, timestamped, with a verdict.
|
|
195
|
+
Run folders have a **stable identity**: `logs/<name>-<hash8>/<date>/<time>/`, keyed by the om's name and the file that defines it, under the project that owns the config — not under whichever directory the run happened to start in. An assistant (or a script) can always find "the latest `tskb-dev` run" without parsing scrollback, diff two runs of the same pipeline, or answer questions with evidence instead of inference: _did the server actually pass its healthcheck? which process died first? what config did the build run with?_ It's all in the folder — attributed per action, timestamped, with a verdict.
|
|
196
196
|
|
|
197
197
|
### Observing the real world, not assuming it
|
|
198
198
|
|
|
@@ -341,7 +341,7 @@ Point your client at the project:
|
|
|
341
341
|
}
|
|
342
342
|
```
|
|
343
343
|
|
|
344
|
-
The server takes its **project root from the
|
|
344
|
+
The server takes its **project root from the config it was given** — the directory holding your `tsconfig.omkit.json` — and resolves `logs/` under it, so the runs it starts are the runs its resources can serve regardless of which directory your client launched it in. `omkit run` uses the same root, so a run you start in the terminal and a run an assistant starts land in one tree and each can read the other's record. If the om project sits in a subfolder, name the config rather than relying on a `cwd` field your client may not support: `["--no", "--", "omkit", "mcp", "--tsconfig", "om/tsconfig.omkit.json"]`.
|
|
345
345
|
|
|
346
346
|
### Inspecting it
|
|
347
347
|
|
|
@@ -24,7 +24,8 @@ export function helpText() {
|
|
|
24
24
|
" --describe import the oms to show their summaries (ls) — slower, runs no workflow",
|
|
25
25
|
" --check report drift and exit non-zero instead of writing (skill)",
|
|
26
26
|
" --out <path> write somewhere else (skill)",
|
|
27
|
-
" --root <path>
|
|
27
|
+
" --root <path> where the skill is written and what its paths are relative to (skill)",
|
|
28
|
+
" — defaults to the enclosing repository",
|
|
28
29
|
" --inspect[=port] run the om in a forked child with the Node inspector open (default port 9229)",
|
|
29
30
|
" -h, --help show this help",
|
|
30
31
|
"",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { buildSkillModel, readExisting, renderSkill, writeSkill, DEFAULT_DESCRIPTION, SKILL_RELATIVE_PATH, } from "../../skill/index.js";
|
|
3
|
+
export { skillRoot } from "../../skill/index.js";
|
|
3
4
|
const EMPTY_REGISTRY = { oms: [], actions: [], warnings: [] };
|
|
4
5
|
/**
|
|
5
6
|
* The `skill` command: generate `.claude/skills/omkit-runs/SKILL.md` from what this project's
|
|
@@ -11,7 +12,12 @@ const EMPTY_REGISTRY = { oms: [], actions: [], warnings: [] };
|
|
|
11
12
|
export async function skillCommand(client, opts) {
|
|
12
13
|
const file = path.resolve(opts.root, opts.out ?? SKILL_RELATIVE_PATH);
|
|
13
14
|
const registrations = await client.discoverRegistrations();
|
|
14
|
-
|
|
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
|
+
});
|
|
15
21
|
const existing = readExisting(file);
|
|
16
22
|
if (opts.check) {
|
|
17
23
|
reportCheck(file, existing.hash, model.hash);
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface Cli {
|
|
|
13
13
|
check: boolean;
|
|
14
14
|
/** `skill --out <path>`: write somewhere other than `.claude/skills/omkit-runs/SKILL.md`. */
|
|
15
15
|
out?: string;
|
|
16
|
-
/** `skill --root <path>`: the
|
|
16
|
+
/** `skill --root <path>`: override the repository the skill is written for and relative to. */
|
|
17
17
|
root?: string;
|
|
18
18
|
/** The Node inspector, if requested via `--inspect[=port]`. */
|
|
19
19
|
inspect?: InspectOptions;
|
package/dist/cli/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { realpathSync } from "node:fs";
|
|
6
6
|
import { createOmkitClient } from "../client/index.js";
|
|
7
|
+
import { DEFAULT_TSCONFIG } from "../client/types.js";
|
|
7
8
|
/**
|
|
8
9
|
* Pull the Node-style inspector flag out of argv, returning the options plus the remaining args.
|
|
9
10
|
* Matches Node's own syntax: `--inspect`, `--inspect=PORT` (default port 9229); an explicit port
|
|
@@ -31,7 +32,7 @@ export function parseCli(argv) {
|
|
|
31
32
|
args: rest,
|
|
32
33
|
options: {
|
|
33
34
|
json: { type: "boolean", default: false },
|
|
34
|
-
tsconfig: { type: "string", default:
|
|
35
|
+
tsconfig: { type: "string", default: DEFAULT_TSCONFIG },
|
|
35
36
|
help: { type: "boolean", short: "h", default: false },
|
|
36
37
|
describe: { type: "boolean", default: false },
|
|
37
38
|
check: { type: "boolean", default: false },
|
|
@@ -100,12 +101,14 @@ async function initCommand() {
|
|
|
100
101
|
}
|
|
101
102
|
/** The `skill` command: generate the runnable-workflow map for an assistant to read. */
|
|
102
103
|
async function generateSkill(client, cli) {
|
|
103
|
-
const { skillCommand } = await import("./commands/skill.js");
|
|
104
|
+
const { skillCommand, skillRoot } = await import("./commands/skill.js");
|
|
104
105
|
// 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
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
const root = cli.root
|
|
106
|
+
// relative to — so it is one setting, not two. It defaults to the enclosing repository, which
|
|
107
|
+
// is both where `.claude/` belongs and where the file's reader will be standing; `--root`
|
|
108
|
+
// remains for the project that is deliberately not at the top of its checkout.
|
|
109
|
+
const root = cli.root
|
|
110
|
+
? path.resolve(cli.root)
|
|
111
|
+
: skillRoot(path.dirname(path.resolve(cli.tsconfig)));
|
|
109
112
|
await skillCommand(client, { root, check: cli.check, ...(cli.out ? { out: cli.out } : {}) });
|
|
110
113
|
}
|
|
111
114
|
/**
|
|
@@ -116,9 +119,12 @@ const COMMANDS = {
|
|
|
116
119
|
init: () => initCommand(),
|
|
117
120
|
ls: (client, cli) => lsCommand(client, cli),
|
|
118
121
|
check: (client) => checkCommand(client),
|
|
119
|
-
mcp: async (client) => {
|
|
122
|
+
mcp: async (client, cli) => {
|
|
120
123
|
const { mcpCommand } = await import("./commands/mcp.js");
|
|
121
|
-
|
|
124
|
+
// The project owning the config, not the directory the server was launched from: run
|
|
125
|
+
// folders are written under it, so reading them back has to use the same answer. An
|
|
126
|
+
// assistant that starts the server from anywhere still sees the project's own runs.
|
|
127
|
+
await mcpCommand(client, { root: path.dirname(path.resolve(cli.tsconfig)) });
|
|
122
128
|
},
|
|
123
129
|
skill: (client, cli) => generateSkill(client, cli),
|
|
124
130
|
run: async (client, cli) => {
|
package/dist/client/index.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { discover, discoverRegistrations } from "./discovery.js";
|
|
2
3
|
import { runOm, spawnBare } from "./runner.js";
|
|
3
4
|
import { runCheck } from "./check.js";
|
|
4
5
|
export { readRegistrations, discoverRegistrations } from "./discovery.js";
|
|
5
6
|
/** Build the headless engine: discovery + supervised/bare run + typecheck over one config. */
|
|
6
7
|
export function createOmkitClient(config) {
|
|
7
|
-
//
|
|
8
|
-
|
|
8
|
+
// The project every run of this client belongs to: the directory owning the config it was
|
|
9
|
+
// built from. Injected here rather than at each frontend because "which project is this?" has
|
|
10
|
+
// one answer per client, and three call sites remembering it separately is how the terminal
|
|
11
|
+
// and the MCP server ended up writing their run folders to two different trees.
|
|
12
|
+
const root = path.dirname(path.resolve(config.tsconfig));
|
|
13
|
+
// A per-call inspect wins; the client's construction-time inspect is the fallback. Likewise
|
|
14
|
+
// a caller may name its own root — the injected one is a default, not an override.
|
|
15
|
+
const withDefaults = (opts) => ({
|
|
9
16
|
...opts,
|
|
10
17
|
inspect: opts?.inspect ?? config.inspect,
|
|
18
|
+
env: { OMKIT_ROOT: root, ...opts?.env },
|
|
11
19
|
});
|
|
12
20
|
return {
|
|
13
21
|
tsconfig: config.tsconfig,
|
|
14
22
|
discover: async () => discover(config.tsconfig),
|
|
15
23
|
discoverRegistrations: async () => discoverRegistrations(config.tsconfig),
|
|
16
|
-
run: (omFile, opts) => runOm(omFile,
|
|
17
|
-
runBare: (omFile, opts) => spawnBare(omFile,
|
|
24
|
+
run: (omFile, opts) => runOm(omFile, withDefaults(opts)),
|
|
25
|
+
runBare: (omFile, opts) => spawnBare(omFile, withDefaults(opts)),
|
|
18
26
|
check: async () => runCheck(config.tsconfig),
|
|
19
27
|
};
|
|
20
28
|
}
|
package/dist/client/types.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { LogEntry } from "../foundation/LogEntry.ts";
|
|
2
2
|
import type { PromptSpec } from "../core/interaction.ts";
|
|
3
3
|
import type { Registry, RegistrationSet } from "./registry.ts";
|
|
4
|
+
/**
|
|
5
|
+
* The config file omkit looks for when nobody names one — resolved against the working
|
|
6
|
+
* directory, which is why anything generated for a reader elsewhere has to say so explicitly.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_TSCONFIG = "tsconfig.omkit.json";
|
|
4
9
|
/** A run's terminal verdict, resolved when the child settles. */
|
|
5
10
|
export interface Verdict {
|
|
6
11
|
readonly ok: boolean;
|
package/dist/client/types.js
CHANGED
|
@@ -124,8 +124,14 @@ export declare class ExecutionTree {
|
|
|
124
124
|
private curatedArtifacts;
|
|
125
125
|
/**
|
|
126
126
|
* Absolute `.log` path for a node — root is `main.log`, children drop the `main/`
|
|
127
|
-
* prefix.
|
|
128
|
-
*
|
|
127
|
+
* prefix. One directory level per ancestor, mirroring the tree.
|
|
128
|
+
*
|
|
129
|
+
* Every `/` here is a level, and that holds only because ids are sanitised where they are
|
|
130
|
+
* made (see `makeId`): a name that carried its own separators would be split here into
|
|
131
|
+
* levels it never had, and the run folder would grow directories spelling out a command's
|
|
132
|
+
* arguments. `fsSafe` stays as the second line of defence for anything that reaches a
|
|
133
|
+
* segment by another route; it cannot be the first, since by this point a separator from a
|
|
134
|
+
* name and a separator from the tree are the same character.
|
|
129
135
|
*/
|
|
130
136
|
private logFile;
|
|
131
137
|
private rawStreamPath;
|
|
@@ -418,8 +418,14 @@ export class ExecutionTree {
|
|
|
418
418
|
}
|
|
419
419
|
/**
|
|
420
420
|
* Absolute `.log` path for a node — root is `main.log`, children drop the `main/`
|
|
421
|
-
* prefix.
|
|
422
|
-
*
|
|
421
|
+
* prefix. One directory level per ancestor, mirroring the tree.
|
|
422
|
+
*
|
|
423
|
+
* Every `/` here is a level, and that holds only because ids are sanitised where they are
|
|
424
|
+
* made (see `makeId`): a name that carried its own separators would be split here into
|
|
425
|
+
* levels it never had, and the run folder would grow directories spelling out a command's
|
|
426
|
+
* arguments. `fsSafe` stays as the second line of defence for anything that reaches a
|
|
427
|
+
* segment by another route; it cannot be the first, since by this point a separator from a
|
|
428
|
+
* name and a separator from the tree are the same character.
|
|
423
429
|
*/
|
|
424
430
|
logFile(nodePath) {
|
|
425
431
|
const rel = nodePath === "main" ? "main" : nodePath.slice("main/".length);
|
package/dist/foundation/ids.d.ts
CHANGED
|
@@ -5,6 +5,18 @@ export declare const shortId: (uuid: string) => string;
|
|
|
5
5
|
/**
|
|
6
6
|
* A node's id: `${name}_${shortId}` → `chromePage_9f3c`. The root passes its own
|
|
7
7
|
* plain name (`main`) and skips this.
|
|
8
|
+
*
|
|
9
|
+
* The name is sanitised **here**, at the point the id is made, because an id is a path
|
|
10
|
+
* segment everywhere it is used: node paths join ids with `/`, and log file names are those
|
|
11
|
+
* paths mapped onto directories. A name carrying its own separators — `command("npx tsk
|
|
12
|
+
* ./docs/**\/*.tsx …")` is an ordinary one — is then indistinguishable from tree nesting, and
|
|
13
|
+
* one action's log went nine directories deep, spelling out the command's path arguments as
|
|
14
|
+
* real folders. Sanitising downstream cannot fix that: by the time a joined path is split
|
|
15
|
+
* back into segments, the separator that came from a name and the one that came from the tree
|
|
16
|
+
* are the same character.
|
|
17
|
+
*
|
|
18
|
+
* Uniqueness never depended on the name — that is the uuid's job — so truncating is safe and
|
|
19
|
+
* the readable form survives anyway: nodes are displayed by their `name`, not by their id.
|
|
8
20
|
*/
|
|
9
21
|
export declare const makeId: (name: string, uuid: string) => string;
|
|
10
22
|
/**
|
package/dist/foundation/ids.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { fsSafe } from "./fsSafe.js";
|
|
2
3
|
/** A fresh v4 uuid for a node. */
|
|
3
4
|
export const newUuid = () => randomUUID();
|
|
4
5
|
/** The short form used in ids/paths — first 8 hex of the uuid. */
|
|
5
6
|
export const shortId = (uuid) => uuid.slice(0, 8);
|
|
7
|
+
/**
|
|
8
|
+
* How much of a name an id keeps. A node's name is free text and can be long — a `command`
|
|
9
|
+
* node is named after its whole command line — while the id it seeds ends up inside a file
|
|
10
|
+
* path, where Windows still stops at 260 characters for the whole path.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_NAME_IN_ID = 64;
|
|
6
13
|
/**
|
|
7
14
|
* A node's id: `${name}_${shortId}` → `chromePage_9f3c`. The root passes its own
|
|
8
15
|
* plain name (`main`) and skips this.
|
|
16
|
+
*
|
|
17
|
+
* The name is sanitised **here**, at the point the id is made, because an id is a path
|
|
18
|
+
* segment everywhere it is used: node paths join ids with `/`, and log file names are those
|
|
19
|
+
* paths mapped onto directories. A name carrying its own separators — `command("npx tsk
|
|
20
|
+
* ./docs/**\/*.tsx …")` is an ordinary one — is then indistinguishable from tree nesting, and
|
|
21
|
+
* one action's log went nine directories deep, spelling out the command's path arguments as
|
|
22
|
+
* real folders. Sanitising downstream cannot fix that: by the time a joined path is split
|
|
23
|
+
* back into segments, the separator that came from a name and the one that came from the tree
|
|
24
|
+
* are the same character.
|
|
25
|
+
*
|
|
26
|
+
* Uniqueness never depended on the name — that is the uuid's job — so truncating is safe and
|
|
27
|
+
* the readable form survives anyway: nodes are displayed by their `name`, not by their id.
|
|
9
28
|
*/
|
|
10
|
-
export const makeId = (name, uuid) => `${name}_${shortId(uuid)}`;
|
|
29
|
+
export const makeId = (name, uuid) => `${clip(fsSafe(name))}_${shortId(uuid)}`;
|
|
30
|
+
/** Trim to the cap, then drop any separator the cut left dangling. */
|
|
31
|
+
const clip = (name) => name.slice(0, MAX_NAME_IN_ID).replace(/[-_.]+$/, "");
|
|
11
32
|
/**
|
|
12
33
|
* A run's stable identity: first 8 hex of sha256 over the defining file's absolute
|
|
13
34
|
* path + the om name. Same file + name ⇒ same hash on every run; the same name
|
package/dist/mcp/tools.js
CHANGED
|
@@ -158,24 +158,28 @@ export function registerTools(server, ctx) {
|
|
|
158
158
|
/**
|
|
159
159
|
* Launch an entry as a supervised run.
|
|
160
160
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
161
|
+
* `OMKIT_ROOT` is this server's own root, stated rather than inherited: resources here
|
|
162
|
+
* resolve under `<root>/logs`, so a run that writes its folder anywhere else is a run this
|
|
163
|
+
* same server cannot serve, and `get_run`'s resource links would point at nothing. The
|
|
164
|
+
* client would default it to the project owning the tsconfig — the same directory in
|
|
165
|
+
* practice — but "the server serves what it writes" should not rest on the two agreeing.
|
|
165
166
|
*/
|
|
166
167
|
const launch = (entry, args) => {
|
|
167
168
|
// An action cannot run standalone, so an action-backed tool forks omkit's shipped host
|
|
168
169
|
// om instead of the action's own file — which defines no om at all.
|
|
169
170
|
const isAction = entry.kind === "action";
|
|
170
171
|
const file = isAction ? hostFile() : entry.file;
|
|
171
|
-
const env =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
172
|
+
const env = {
|
|
173
|
+
OMKIT_ROOT: ctx.root,
|
|
174
|
+
...(isAction
|
|
175
|
+
? {
|
|
176
|
+
OMKIT_ACTION_FILE: entry.file,
|
|
177
|
+
OMKIT_ACTION_NAME: entry.name,
|
|
178
|
+
OMKIT_ACTION_EXPORT: entry.exportName,
|
|
179
|
+
OMKIT_ACTION_ARGS: JSON.stringify(args),
|
|
180
|
+
}
|
|
181
|
+
: { OMKIT_ARGS: JSON.stringify(args) }),
|
|
182
|
+
};
|
|
179
183
|
const folderName = isAction
|
|
180
184
|
? hostFolderName(entry.name, entry.file)
|
|
181
185
|
: (entry.folderName ?? entry.name);
|
|
@@ -5,6 +5,21 @@ import { fsSafe } from "../../foundation/fsSafe.js";
|
|
|
5
5
|
import { newUuid, shortId } from "../../foundation/ids.js";
|
|
6
6
|
/** Suffixed attempts (`-02` … `-99`) before falling back to a random, certainly-unique one. */
|
|
7
7
|
const MAX_NUMBERED_ATTEMPTS = 99;
|
|
8
|
+
/**
|
|
9
|
+
* Where `logs/` goes: the project root a frontend named, or the working directory when nobody
|
|
10
|
+
* named one.
|
|
11
|
+
*
|
|
12
|
+
* A run's cwd is not a statement about which project it belongs to — `omkit run` sets it to the
|
|
13
|
+
* om file's own directory so relative paths in a body resolve the way their author reads them,
|
|
14
|
+
* which meant one project scattered its records across every folder that happened to contain an
|
|
15
|
+
* om, while the MCP server wrote them wherever the server was started. Two names for one thing.
|
|
16
|
+
* The root is passed explicitly and separately so the record's location is a property of the
|
|
17
|
+
* project, and the cwd stays free to mean what it already meant.
|
|
18
|
+
*/
|
|
19
|
+
function logsRoot() {
|
|
20
|
+
const root = process.env.OMKIT_ROOT;
|
|
21
|
+
return root ? path.resolve(root, "logs") : path.resolve("logs");
|
|
22
|
+
}
|
|
8
23
|
/**
|
|
9
24
|
* Owns this run's single output directory — `logs/<name>-<hash>/<date>/<time>/` —
|
|
10
25
|
* where `raw.jsonl`, the per-action `.log` files, rollups, and `result.json` all
|
|
@@ -61,7 +76,7 @@ export class RunFolder {
|
|
|
61
76
|
*/
|
|
62
77
|
claim() {
|
|
63
78
|
const now = new Date();
|
|
64
|
-
const parent = path.resolve(
|
|
79
|
+
const parent = path.resolve(logsRoot(), this.name(), ymd(now));
|
|
65
80
|
mkdirSync(parent, { recursive: true });
|
|
66
81
|
const base = hms(now, "-");
|
|
67
82
|
for (let attempt = 0; attempt <= MAX_NUMBERED_ATTEMPTS; attempt += 1) {
|
package/dist/skill/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* take data and return data, which is what makes determinism testable without touching a disk.
|
|
6
6
|
*/
|
|
7
7
|
export { buildSkillModel, registryHash } from "./model.ts";
|
|
8
|
-
export type { SkillCall, SkillEntry, SkillModel } from "./model.ts";
|
|
8
|
+
export type { SkillCall, SkillEntry, SkillModel, SkillProject } from "./model.ts";
|
|
9
|
+
export { skillRoot } from "./root.ts";
|
|
9
10
|
export { renderSkill, generatedBy, SKILL_NAME } from "./render.ts";
|
|
10
11
|
export { readExisting, writeSkill, DEFAULT_DESCRIPTION, SKILL_RELATIVE_PATH } from "./file.ts";
|
|
11
12
|
export type { ExistingSkill } from "./file.ts";
|
package/dist/skill/index.js
CHANGED
|
@@ -5,5 +5,6 @@
|
|
|
5
5
|
* take data and return data, which is what makes determinism testable without touching a disk.
|
|
6
6
|
*/
|
|
7
7
|
export { buildSkillModel, registryHash } from "./model.js";
|
|
8
|
+
export { skillRoot } from "./root.js";
|
|
8
9
|
export { renderSkill, generatedBy, SKILL_NAME } from "./render.js";
|
|
9
10
|
export { readExisting, writeSkill, DEFAULT_DESCRIPTION, SKILL_RELATIVE_PATH } from "./file.js";
|
package/dist/skill/model.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface SkillEntry {
|
|
|
16
16
|
readonly argHint?: string;
|
|
17
17
|
/** The raw schema, present only when `shapeHint` could not sketch it (per C7). */
|
|
18
18
|
readonly argSchema?: JsonSchema;
|
|
19
|
+
/** A concrete, complete `OMKIT_ARGS` value. Absent when no honest one can be built. */
|
|
20
|
+
readonly argExample?: Record<string, unknown>;
|
|
19
21
|
/** The static outline of the body. Absent when the walk found nothing recognisable. */
|
|
20
22
|
readonly calls?: readonly SkillCall[];
|
|
21
23
|
/** Why this entry cannot be called — carried through from discovery, rendered as a warning. */
|
|
@@ -25,10 +27,22 @@ export interface SkillEntry {
|
|
|
25
27
|
/** Actions only: true when the chain includes `.ref<…>()`, so it publishes a capability. */
|
|
26
28
|
readonly publishesCapability?: boolean;
|
|
27
29
|
}
|
|
30
|
+
/** The project the skill describes: where its reader stands, and what config omkit was given. */
|
|
31
|
+
export interface SkillProject {
|
|
32
|
+
/** Directory the file is written for — `.claude/` lives here, and paths are relative to it. */
|
|
33
|
+
readonly root: string;
|
|
34
|
+
/** Absolute path of the tsconfig discovery ran against, when the caller knows it. */
|
|
35
|
+
readonly tsconfig?: string;
|
|
36
|
+
}
|
|
28
37
|
/** Everything the renderer needs, plus the hash that detects drift against it. */
|
|
29
38
|
export interface SkillModel {
|
|
30
39
|
readonly oms: readonly SkillEntry[];
|
|
31
40
|
readonly actions: readonly SkillEntry[];
|
|
41
|
+
/**
|
|
42
|
+
* The `--tsconfig` value every rendered command needs, relative to the root. Absent when the
|
|
43
|
+
* CLI's own default already finds it — the flag would then be noise on every line.
|
|
44
|
+
*/
|
|
45
|
+
readonly tsconfig?: string;
|
|
32
46
|
/** First 8 hex of sha256 over the registry data below — never over the rendered markdown. */
|
|
33
47
|
readonly hash: string;
|
|
34
48
|
}
|
|
@@ -42,7 +56,7 @@ export interface SkillModel {
|
|
|
42
56
|
* Only `.mcp()`-marked entries survive — exposure is opt-in, exactly as it is for the MCP
|
|
43
57
|
* server, so generating a skill can never widen what an assistant will attempt.
|
|
44
58
|
*/
|
|
45
|
-
export declare function buildSkillModel(registrations: RegistrationSet, registry: Registry,
|
|
59
|
+
export declare function buildSkillModel(registrations: RegistrationSet, registry: Registry, project: SkillProject): SkillModel;
|
|
46
60
|
/**
|
|
47
61
|
* The staleness hash: first 8 hex of sha256 over the registry data, deliberately **not** over
|
|
48
62
|
* the rendered markdown, so reformatting the file does not read as drift.
|
|
@@ -52,4 +66,4 @@ export declare function buildSkillModel(registrations: RegistrationSet, registry
|
|
|
52
66
|
* drift the hash exists to catch. Keys are sorted before hashing so a schema serialised in a
|
|
53
67
|
* different property order cannot masquerade as a change.
|
|
54
68
|
*/
|
|
55
|
-
export declare function registryHash(entries: readonly SkillEntry[]): string;
|
|
69
|
+
export declare function registryHash(entries: readonly SkillEntry[], tsconfig?: string): string;
|
package/dist/skill/model.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { shapeHint } from "../core/schema-hint.js";
|
|
4
|
+
import { DEFAULT_TSCONFIG } from "../client/types.js";
|
|
4
5
|
/**
|
|
5
6
|
* Fold a project's registrations and its AST scan into the model the skill renders from.
|
|
6
7
|
*
|
|
@@ -11,7 +12,8 @@ import { shapeHint } from "../core/schema-hint.js";
|
|
|
11
12
|
* Only `.mcp()`-marked entries survive — exposure is opt-in, exactly as it is for the MCP
|
|
12
13
|
* server, so generating a skill can never widen what an assistant will attempt.
|
|
13
14
|
*/
|
|
14
|
-
export function buildSkillModel(registrations, registry,
|
|
15
|
+
export function buildSkillModel(registrations, registry, project) {
|
|
16
|
+
const { root } = project;
|
|
15
17
|
const outlines = new Map(registry.oms.map((o) => [key(o.name, o.file), o.calls]));
|
|
16
18
|
const capabilities = new Map(registry.actions.map((a) => [key(a.name, a.file), a.publishesCapability]));
|
|
17
19
|
const oms = registrations.oms
|
|
@@ -22,7 +24,26 @@ export function buildSkillModel(registrations, registry, root) {
|
|
|
22
24
|
.filter(isExposed)
|
|
23
25
|
.map((a) => actionEntry(a, capabilities.get(key(a.name, a.file)), root))
|
|
24
26
|
.sort(byName);
|
|
25
|
-
|
|
27
|
+
const tsconfig = tsconfigFlag(project);
|
|
28
|
+
return {
|
|
29
|
+
oms,
|
|
30
|
+
actions,
|
|
31
|
+
...(tsconfig ? { tsconfig } : {}),
|
|
32
|
+
hash: registryHash([...oms, ...actions], tsconfig),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* What a reader standing at the root must pass as `--tsconfig`, or `undefined` when they need
|
|
37
|
+
* pass nothing. omkit looks for `tsconfig.omkit.json` in the working directory, so a project
|
|
38
|
+
* whose config sits anywhere else — the common case once an om project lives in a subfolder —
|
|
39
|
+
* has commands that only work with the flag. Rendering it is the difference between a file
|
|
40
|
+
* that documents how to run this project and one that documents how to run some other project.
|
|
41
|
+
*/
|
|
42
|
+
function tsconfigFlag({ root, tsconfig }) {
|
|
43
|
+
if (tsconfig === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
const rel = relative(tsconfig, root);
|
|
46
|
+
return rel === DEFAULT_TSCONFIG ? undefined : rel;
|
|
26
47
|
}
|
|
27
48
|
function isExposed(r) {
|
|
28
49
|
return r.mcp !== undefined;
|
|
@@ -60,16 +81,75 @@ function actionEntry(action, publishesCapability, root) {
|
|
|
60
81
|
...(action.unavailable ? { unavailable: action.unavailable } : {}),
|
|
61
82
|
};
|
|
62
83
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Sketch a schema, falling back to the schema itself. `shapeHint` is all-or-nothing by design,
|
|
65
|
-
* so a `undefined` return means the shape would have been misleading — the honest answer then
|
|
66
|
-
* is the raw JSON Schema the caller actually has to satisfy, not a partial sketch.
|
|
67
|
-
*/
|
|
68
84
|
function describeArgs(schema) {
|
|
69
85
|
if (!schema)
|
|
70
86
|
return {};
|
|
71
87
|
const hint = shapeHint(schema);
|
|
72
|
-
|
|
88
|
+
const example = argExample(schema);
|
|
89
|
+
return {
|
|
90
|
+
...(hint === undefined ? { argSchema: schema } : { argHint: hint }),
|
|
91
|
+
...(example ? { argExample: example } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A complete, valid `OMKIT_ARGS` object for this schema, or `undefined` when one cannot be
|
|
96
|
+
* built honestly.
|
|
97
|
+
*
|
|
98
|
+
* "Complete" is the whole point: an example missing a required field does not demonstrate the
|
|
99
|
+
* call, it demonstrates the prompt the reader gets instead. So every required field must be
|
|
100
|
+
* sampleable or there is no example — and when nothing is required, one optional field is
|
|
101
|
+
* enough to show the shape without implying the rest are needed.
|
|
102
|
+
*/
|
|
103
|
+
function argExample(schema) {
|
|
104
|
+
const properties = schema.properties;
|
|
105
|
+
if (typeof properties !== "object" || properties === null)
|
|
106
|
+
return undefined;
|
|
107
|
+
const entries = Object.entries(properties);
|
|
108
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
109
|
+
const mandatory = entries.filter(([key]) => required.has(key));
|
|
110
|
+
if (mandatory.length > 0) {
|
|
111
|
+
const example = {};
|
|
112
|
+
for (const [key, field] of mandatory) {
|
|
113
|
+
const value = sample(field);
|
|
114
|
+
if (value === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
example[key] = value;
|
|
117
|
+
}
|
|
118
|
+
return example;
|
|
119
|
+
}
|
|
120
|
+
for (const [key, field] of entries) {
|
|
121
|
+
const value = sample(field);
|
|
122
|
+
if (value !== undefined)
|
|
123
|
+
return { [key]: value };
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* One plausible value for a field, or `undefined` when the type has no short literal form.
|
|
129
|
+
* A declared default wins over an invented value — it is both valid and the author's own
|
|
130
|
+
* choice, which reads better in an example than a zero or an ellipsis.
|
|
131
|
+
*/
|
|
132
|
+
function sample(field) {
|
|
133
|
+
if (typeof field !== "object" || field === null)
|
|
134
|
+
return undefined;
|
|
135
|
+
const enumValues = field.enum;
|
|
136
|
+
if (Array.isArray(enumValues) && enumValues.length > 0)
|
|
137
|
+
return enumValues[0];
|
|
138
|
+
if (field.default !== undefined)
|
|
139
|
+
return field.default;
|
|
140
|
+
switch (field.type) {
|
|
141
|
+
case "string":
|
|
142
|
+
return "…";
|
|
143
|
+
case "number":
|
|
144
|
+
case "integer":
|
|
145
|
+
return 0;
|
|
146
|
+
case "boolean":
|
|
147
|
+
return false;
|
|
148
|
+
// Objects and arrays have no one-line stand-in: `{}` and `[]` are as likely to be rejected
|
|
149
|
+
// by the schema as accepted by it, and a rejected example is worse than none.
|
|
150
|
+
default:
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
73
153
|
}
|
|
74
154
|
/** Project-relative with forward slashes; absolute only if the file lies outside the root. */
|
|
75
155
|
function relative(file, root) {
|
|
@@ -88,8 +168,8 @@ const posix = (p) => p.split(path.sep).join("/").split("\\").join("/");
|
|
|
88
168
|
* drift the hash exists to catch. Keys are sorted before hashing so a schema serialised in a
|
|
89
169
|
* different property order cannot masquerade as a change.
|
|
90
170
|
*/
|
|
91
|
-
export function registryHash(entries) {
|
|
92
|
-
const
|
|
171
|
+
export function registryHash(entries, tsconfig) {
|
|
172
|
+
const declarations = entries.map((e) => [
|
|
93
173
|
e.kind,
|
|
94
174
|
e.name,
|
|
95
175
|
e.file,
|
|
@@ -101,6 +181,10 @@ export function registryHash(entries) {
|
|
|
101
181
|
e.argSchema ?? e.argHint ?? "",
|
|
102
182
|
(e.calls ?? []).map((c) => [c.name, c.tag ?? ""]),
|
|
103
183
|
]);
|
|
184
|
+
// The invocation is hashed beside the declarations because it is part of what the file
|
|
185
|
+
// claims: move a project's tsconfig and every command in it becomes wrong, while not one
|
|
186
|
+
// om has changed. Drift is "the file no longer describes this project", not "an om changed".
|
|
187
|
+
const material = [declarations, tsconfig ?? ""];
|
|
104
188
|
return createHash("sha256").update(canonical(material)).digest("hex").slice(0, 8);
|
|
105
189
|
}
|
|
106
190
|
/** JSON with object keys sorted at every depth, so key order can never change the hash. */
|
package/dist/skill/render.js
CHANGED
|
@@ -26,21 +26,40 @@ export function renderSkill(model, opts) {
|
|
|
26
26
|
...howToRun(model),
|
|
27
27
|
...workflows(model),
|
|
28
28
|
...actions(model),
|
|
29
|
-
...whereOutputLands(),
|
|
29
|
+
...whereOutputLands(model),
|
|
30
30
|
];
|
|
31
31
|
return `${lines.join("\n")}\n`;
|
|
32
32
|
}
|
|
33
33
|
function howToRun(model) {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// A long-lived om is the wrong thing to demonstrate `run` with: it never returns, and the
|
|
35
|
+
// entry's own heading says so a few lines further down. Prefer one that settles, and fall
|
|
36
|
+
// back to the long-lived one only when the project has nothing else — annotated, so the
|
|
37
|
+
// command still reads as deliberate rather than as an example that hangs.
|
|
38
|
+
const settling = model.oms.filter((o) => o.mode !== "long-lived");
|
|
39
|
+
const pool = settling.length ? settling : model.oms;
|
|
40
|
+
const example = pool[0];
|
|
41
|
+
const withArgs = pool.find((o) => o.argExample !== undefined);
|
|
42
|
+
const name = example?.name ?? "<om>";
|
|
43
|
+
const holds = example?.mode === "long-lived" ? " # long-lived: runs until you stop it" : "";
|
|
44
|
+
// Appended to every command, because omkit resolves its config against the working directory
|
|
45
|
+
// and this file's reader is at the project root, which is not necessarily where the config is.
|
|
46
|
+
const config = model.tsconfig ? ` --tsconfig ${model.tsconfig}` : "";
|
|
36
47
|
return [
|
|
37
48
|
"## How to run",
|
|
38
49
|
"",
|
|
39
|
-
"**Shell — always available
|
|
50
|
+
"**Shell — always available.** Run these from the project root (the directory holding",
|
|
51
|
+
"`.claude/`); every path here is relative to it.",
|
|
40
52
|
"",
|
|
41
53
|
"```bash",
|
|
42
|
-
`npx omkit run ${
|
|
43
|
-
`OMKIT_ARGS='
|
|
54
|
+
`npx omkit run ${shellName(name)}${config}${holds}`,
|
|
55
|
+
// Only when there is a real one to show. An `OMKIT_ARGS='{}'` line teaches the syntax and
|
|
56
|
+
// nothing else, and reads as though this project's runs take no arguments — which is the
|
|
57
|
+
// opposite of what it means when a project's args could not be sampled.
|
|
58
|
+
...(withArgs
|
|
59
|
+
? [
|
|
60
|
+
`OMKIT_ARGS='${JSON.stringify(withArgs.argExample)}' npx omkit run ${shellName(withArgs.name)}${config}`,
|
|
61
|
+
]
|
|
62
|
+
: []),
|
|
44
63
|
"```",
|
|
45
64
|
"",
|
|
46
65
|
"Arguments travel as one JSON object in `OMKIT_ARGS`. Anything an om declares but does not",
|
|
@@ -50,24 +69,25 @@ function howToRun(model) {
|
|
|
50
69
|
"completion and returns its verdict; `start_om` starts a long-lived one and returns a run id",
|
|
51
70
|
"to poll with `get_run`, read with `tail_run`, and end with `cancel_run`.",
|
|
52
71
|
"",
|
|
53
|
-
|
|
72
|
+
`To wire it up: \`claude mcp add omkit -- npx omkit mcp${config}\``,
|
|
54
73
|
"",
|
|
55
74
|
"At runtime `list_oms` is authoritative — this file is orientation, and can drift.",
|
|
56
75
|
"",
|
|
57
76
|
];
|
|
58
77
|
}
|
|
59
78
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
79
|
+
* An om's name as one shell argument.
|
|
80
|
+
*
|
|
81
|
+
* A name is free text — `om("DTF Tests")` is legal, and reads well in a log — but written bare
|
|
82
|
+
* into a command it stops being one argument: `omkit run DTF Tests` resolves the target as
|
|
83
|
+
* "DTF" and leaves "Tests" as a stray positional. So the file carries the quotes rather than
|
|
84
|
+
* leaving the reader to notice they are missing.
|
|
85
|
+
*
|
|
86
|
+
* Double quotes, not single, because cmd.exe and PowerShell understand them too. Names built
|
|
87
|
+
* from shell metacharacters beyond a quote or a backslash (`$`, backticks) are the author's to
|
|
88
|
+
* avoid — there is no quoting form that is literal in every shell at once.
|
|
62
89
|
*/
|
|
63
|
-
|
|
64
|
-
const match = entry?.argHint?.match(/\{ (\w+)\??: (\w+)/);
|
|
65
|
-
if (!match)
|
|
66
|
-
return "{}";
|
|
67
|
-
const [, field, type] = match;
|
|
68
|
-
const value = type === "string" ? '"…"' : type === "boolean" ? "false" : "0";
|
|
69
|
-
return `{"${field}":${value}}`;
|
|
70
|
-
}
|
|
90
|
+
const shellName = (name) => /^[\w:.@/-]+$/.test(name) ? name : `"${name.replace(/([\\"])/g, "\\$1")}"`;
|
|
71
91
|
function workflows(model) {
|
|
72
92
|
if (model.oms.length === 0) {
|
|
73
93
|
return ["## Workflows", "", "_None exposed. Mark an om with `.mcp()` to list it here._", ""];
|
|
@@ -114,12 +134,18 @@ function entry(e, heading) {
|
|
|
114
134
|
lines.push("");
|
|
115
135
|
return lines;
|
|
116
136
|
}
|
|
137
|
+
/** The model's paths are already posix, so the directory half is a plain string operation. */
|
|
138
|
+
const posixDirname = (file) => file.slice(0, Math.max(0, file.lastIndexOf("/")));
|
|
117
139
|
const renderCall = (c) => c.tag ? `\`${c.name}\` [${c.tag}]` : `\`${c.name}\``;
|
|
118
|
-
function whereOutputLands() {
|
|
140
|
+
function whereOutputLands(model) {
|
|
141
|
+
// Spelled from the root the reader stands at, not as a bare `logs/`: the folder belongs to
|
|
142
|
+
// the project that owns the config, which in a subfolder layout is not where they are.
|
|
143
|
+
const dir = model.tsconfig ? posixDirname(model.tsconfig) : "";
|
|
144
|
+
const logs = dir ? `${dir}/logs` : "logs";
|
|
119
145
|
return [
|
|
120
146
|
"## Where output lands",
|
|
121
147
|
"",
|
|
122
|
-
|
|
148
|
+
`\`${logs}/<name>-<hash8>/<date>/<time>/\` — \`main.log\`, \`result.json\`, \`artifacts.log\`,`,
|
|
123
149
|
"`events.log`, `asserts.log`, `raw.jsonl`. A run's identity is its name plus its defining",
|
|
124
150
|
'file, so "the latest run of X" is always resolvable without parsing scrollback.',
|
|
125
151
|
"",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory a generated skill is written for: the nearest enclosing repository, or the
|
|
3
|
+
* project itself when there is none.
|
|
4
|
+
*
|
|
5
|
+
* The root is not really a free choice. A skill file lives at `.claude/skills/<name>/SKILL.md`,
|
|
6
|
+
* an assistant reads it with the repository as its working directory, and so every path and
|
|
7
|
+
* command inside has to be relative to *that* — not to wherever the om project's tsconfig
|
|
8
|
+
* happens to sit. Defaulting to the tsconfig's own directory made the common monorepo layout
|
|
9
|
+
* (an `om/` folder beside the code it drives) wrong by default, and needed a second flag to
|
|
10
|
+
* correct what the first flag had just decided.
|
|
11
|
+
*
|
|
12
|
+
* `.git` is checked as an entry, not as a directory: a worktree or submodule records it as a
|
|
13
|
+
* file containing a `gitdir:` pointer, and that is still the top of the checkout.
|
|
14
|
+
*/
|
|
15
|
+
export declare function skillRoot(projectDir: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* The directory a generated skill is written for: the nearest enclosing repository, or the
|
|
5
|
+
* project itself when there is none.
|
|
6
|
+
*
|
|
7
|
+
* The root is not really a free choice. A skill file lives at `.claude/skills/<name>/SKILL.md`,
|
|
8
|
+
* an assistant reads it with the repository as its working directory, and so every path and
|
|
9
|
+
* command inside has to be relative to *that* — not to wherever the om project's tsconfig
|
|
10
|
+
* happens to sit. Defaulting to the tsconfig's own directory made the common monorepo layout
|
|
11
|
+
* (an `om/` folder beside the code it drives) wrong by default, and needed a second flag to
|
|
12
|
+
* correct what the first flag had just decided.
|
|
13
|
+
*
|
|
14
|
+
* `.git` is checked as an entry, not as a directory: a worktree or submodule records it as a
|
|
15
|
+
* file containing a `gitdir:` pointer, and that is still the top of the checkout.
|
|
16
|
+
*/
|
|
17
|
+
export function skillRoot(projectDir) {
|
|
18
|
+
let dir = path.resolve(projectDir);
|
|
19
|
+
for (;;) {
|
|
20
|
+
if (fs.existsSync(path.join(dir, ".git")))
|
|
21
|
+
return dir;
|
|
22
|
+
const parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir)
|
|
24
|
+
return path.resolve(projectDir);
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"playwright-core": "^1.49.0",
|
|
60
60
|
"react": "^19.2.8",
|
|
61
61
|
"tsx": "4.23.1",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
62
63
|
"zod": "^4.4.3",
|
|
63
64
|
"zx": "^8.8.5"
|
|
64
65
|
},
|
|
@@ -66,7 +67,6 @@
|
|
|
66
67
|
"@types/node": "^24",
|
|
67
68
|
"@types/react": "^19.2.17",
|
|
68
69
|
"ink-testing-library": "^4.0.0",
|
|
69
|
-
"rimraf": "^6.0.1"
|
|
70
|
-
"typescript": "^5.9.3"
|
|
70
|
+
"rimraf": "^6.0.1"
|
|
71
71
|
}
|
|
72
72
|
}
|