@vimhead.dev/norn-cli 0.1.0-tip.36148588553.1 → 0.1.0-tip.36679386408.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/README.md +57 -5
- package/assets/docs/cli.md +3 -0
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/core/src/host-introduction.mjs +20 -9
- package/assets/packages/core/src/runtime-resolution.mjs +137 -0
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/package.json +3 -3
package/assets/README.md
CHANGED
|
@@ -113,6 +113,17 @@ npm install -g @vimhead.dev/norn-cli@tip
|
|
|
113
113
|
norn version
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
Alternatively, install into the package that owns your workflows, without a global
|
|
117
|
+
Norn installation:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install --save-dev --save-exact @vimhead.dev/norn-cli@tip
|
|
121
|
+
./node_modules/.bin/norn version
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Use that local executable in place of `norn` in the commands below. Connect harnesses
|
|
125
|
+
with [project-local runtime selection](#project-local-runtime-selection).
|
|
126
|
+
|
|
116
127
|
Releases are prereleases, not stable `latest` releases. For a standalone binary
|
|
117
128
|
without Node, use the matching GitHub `tip` release:
|
|
118
129
|
|
|
@@ -154,10 +165,51 @@ The shipped Pi, Cursor, and Claude Code adapters deliver Norn documentation cont
|
|
|
154
165
|
harness at session start. Installing Norn alone does not register an adapter. Codex
|
|
155
166
|
and other harnesses can [invoke the CLI directly](docs/cli.md#javascript-client-and-other-harnesses).
|
|
156
167
|
|
|
168
|
+
#### Project-local runtime selection
|
|
169
|
+
|
|
170
|
+
To use a CLI dependency from a harness launched elsewhere in your workspace, put
|
|
171
|
+
`.nornrc.json` in the harness's working directory or an ancestor:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"runtime": {
|
|
176
|
+
"packageRoot": "./packages/workflows"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`packageRoot` is relative to this configuration file (absolute paths also work).
|
|
182
|
+
It names a package whose `package.json` declares `@vimhead.dev/norn-cli` in
|
|
183
|
+
`dependencies`, `devDependencies`, or `optionalDependencies`. Install that package's
|
|
184
|
+
dependencies first; its manifest and lockfile own the version. npm hoisting and
|
|
185
|
+
pnpm's linked `node_modules` installations are supported. No download occurs at
|
|
186
|
+
harness startup. Node `>=22.19.0` is required for the npm runtime.
|
|
187
|
+
|
|
188
|
+
The Pi, Cursor, and Claude Code adapters select an explicit executable override
|
|
189
|
+
first, then the nearest `.nornrc.json`, then `norn` on `PATH` only when no configuration
|
|
190
|
+
exists. Configuration files do not merge. Invalid configuration or a missing/broken
|
|
191
|
+
selected installation fails without falling back. Pi ignores repository runtime
|
|
192
|
+
configuration in untrusted projects; Cursor and Claude Code use their host's hook
|
|
193
|
+
trust/approval boundary. Only enable adapters and runtime selection in workspaces
|
|
194
|
+
you trust: the selected dependency executes code.
|
|
195
|
+
|
|
196
|
+
Both introductions come from the selected runtime, and its documentation introduction
|
|
197
|
+
supplies concrete argv for subsequent agent calls. The session's working directory
|
|
198
|
+
is preserved; runtime selection does not select a workflow project or workspace.
|
|
199
|
+
Restart the session (or `/reload` in Pi) after changing selection or dependencies.
|
|
200
|
+
|
|
201
|
+
This is adapter configuration, **not CLI delegation**: globally installed Norn and
|
|
202
|
+
direct CLI invocations do not read `.nornrc.json` or proxy commands. For terminal use,
|
|
203
|
+
invoke the dependency directly, preserving your working directory:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
./packages/workflows/node_modules/.bin/norn version
|
|
207
|
+
```
|
|
208
|
+
|
|
157
209
|
#### Pi
|
|
158
210
|
|
|
159
|
-
With Pi already installed and
|
|
160
|
-
and start a new session:
|
|
211
|
+
With Pi already installed and a runtime selected as above or available on `PATH`,
|
|
212
|
+
install the adapter and start a new session:
|
|
161
213
|
|
|
162
214
|
```bash
|
|
163
215
|
pi install npm:@vimhead.dev/pi-norn@tip
|
|
@@ -177,8 +229,8 @@ pi --norn-executable /absolute/path/to/norn
|
|
|
177
229
|
3. Install the **norn** plugin, choosing user or project scope.
|
|
178
230
|
4. Start a new agent conversation.
|
|
179
231
|
|
|
180
|
-
|
|
181
|
-
|
|
232
|
+
Use [project-local runtime selection](#project-local-runtime-selection), or override
|
|
233
|
+
it by starting Cursor with an executable path:
|
|
182
234
|
|
|
183
235
|
```bash
|
|
184
236
|
NORN_EXECUTABLE=/absolute/path/to/norn cursor .
|
|
@@ -186,7 +238,7 @@ NORN_EXECUTABLE=/absolute/path/to/norn cursor .
|
|
|
186
238
|
|
|
187
239
|
#### Claude Code
|
|
188
240
|
|
|
189
|
-
With Node and
|
|
241
|
+
With Node and a [selected runtime](#project-local-runtime-selection), run in your terminal:
|
|
190
242
|
|
|
191
243
|
```bash
|
|
192
244
|
claude plugin marketplace add vimhead/norn
|
package/assets/docs/cli.md
CHANGED
|
@@ -10,6 +10,9 @@ norn version
|
|
|
10
10
|
norn help
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
For harnesses using an npm dependency without global Norn, see
|
|
14
|
+
[adapter runtime selection](../README.md#project-local-runtime-selection).
|
|
15
|
+
|
|
13
16
|
For a source checkout, run `pnpm install --frozen-lockfile` and `pnpm build` first. A shell function keeps all examples bound to that checkout rather than another `PATH` installation:
|
|
14
17
|
|
|
15
18
|
```bash
|
package/assets/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.1.0-tip.
|
|
1
|
+
{"version":"0.1.0-tip.36679386408.1"}
|
|
@@ -2,8 +2,8 @@ import type { NornBuildInfo } from "./build-info.ts";
|
|
|
2
2
|
|
|
3
3
|
export const NORN_GENERATED_BUILD_INFO = {
|
|
4
4
|
"kind": "npm-registry",
|
|
5
|
-
"version": "0.1.0-tip.
|
|
6
|
-
"commit": "
|
|
5
|
+
"version": "0.1.0-tip.36679386408.1",
|
|
6
|
+
"commit": "3cb3189ec70f3b4407456061207294d685297b69",
|
|
7
7
|
"packageSpec": "@vimhead.dev/norn-cli@tip",
|
|
8
8
|
"upgrade": {
|
|
9
9
|
"supported": false,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
|
+
import { resolveNornRuntime } from "./runtime-resolution.mjs";
|
|
4
5
|
|
|
5
6
|
const runExecutable = promisify(execFile);
|
|
6
7
|
const MAX_INTRO_BYTES = 16_384;
|
|
@@ -18,19 +19,29 @@ function readIntroResponse({ stdout, group }) {
|
|
|
18
19
|
return response.intro;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
async function loadIntroduction({
|
|
22
|
-
const { stdout } = await runExecutable(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
async function loadIntroduction({ invocation, cwd, group }) {
|
|
23
|
+
const { stdout } = await runExecutable(
|
|
24
|
+
invocation.executable,
|
|
25
|
+
[...invocation.args, group, "intro"],
|
|
26
|
+
{
|
|
27
|
+
cwd,
|
|
28
|
+
timeout: 10_000,
|
|
29
|
+
maxBuffer: MAX_INTRO_BYTES * 2,
|
|
30
|
+
},
|
|
31
|
+
);
|
|
27
32
|
return readIntroResponse({ stdout, group });
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
export async function loadHostIntroduction({
|
|
35
|
+
export async function loadHostIntroduction({ executableOverride, cwd }) {
|
|
36
|
+
const invocation = await resolveNornRuntime({
|
|
37
|
+
cwd,
|
|
38
|
+
executableOverride,
|
|
39
|
+
isProjectTrusted: true,
|
|
40
|
+
nodeExecutable: process.execPath,
|
|
41
|
+
});
|
|
31
42
|
const [intro, workflowsIntro] = await Promise.all([
|
|
32
|
-
loadIntroduction({
|
|
33
|
-
loadIntroduction({
|
|
43
|
+
loadIntroduction({ invocation, cwd, group: "docs" }),
|
|
44
|
+
loadIntroduction({ invocation, cwd, group: "workflows" }),
|
|
34
45
|
]);
|
|
35
46
|
return `<norn-docs-intro>\n${intro}\n</norn-docs-intro>${workflowsIntro ? `\n\n${workflowsIntro}` : ""}`;
|
|
36
47
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
const CLI_PACKAGE_NAME = "@vimhead.dev/norn-cli";
|
|
5
|
+
|
|
6
|
+
export class RuntimeResolutionError extends Error {}
|
|
7
|
+
|
|
8
|
+
export async function resolveNornRuntime({
|
|
9
|
+
cwd,
|
|
10
|
+
executableOverride,
|
|
11
|
+
isProjectTrusted,
|
|
12
|
+
nodeExecutable,
|
|
13
|
+
}) {
|
|
14
|
+
if (executableOverride !== null) {
|
|
15
|
+
if (typeof executableOverride !== "string" || !executableOverride.trim())
|
|
16
|
+
throw new RuntimeResolutionError(
|
|
17
|
+
"Norn executable override must be a nonempty executable path or name.",
|
|
18
|
+
);
|
|
19
|
+
return { executable: executableOverride, args: [] };
|
|
20
|
+
}
|
|
21
|
+
if (!isProjectTrusted) return { executable: "norn", args: [] };
|
|
22
|
+
const configPath = await findAncestorEntry({
|
|
23
|
+
cwd,
|
|
24
|
+
relativePath: ".nornrc.json",
|
|
25
|
+
});
|
|
26
|
+
if (configPath === null) return { executable: "norn", args: [] };
|
|
27
|
+
const packageRoot = await readRuntimePackageRoot(configPath);
|
|
28
|
+
const scriptPath = await resolvePackageEntrypoint(packageRoot);
|
|
29
|
+
return { executable: nodeExecutable, args: [scriptPath] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readRuntimePackageRoot(configPath) {
|
|
33
|
+
const configuration = await readManifest(configPath);
|
|
34
|
+
if (
|
|
35
|
+
!isRecord(configuration) ||
|
|
36
|
+
Object.keys(configuration).some((key) => key !== "runtime") ||
|
|
37
|
+
!isRecord(configuration.runtime) ||
|
|
38
|
+
Object.keys(configuration.runtime).some((key) => key !== "packageRoot") ||
|
|
39
|
+
typeof configuration.runtime.packageRoot !== "string" ||
|
|
40
|
+
!configuration.runtime.packageRoot.trim()
|
|
41
|
+
)
|
|
42
|
+
throw new RuntimeResolutionError(
|
|
43
|
+
`Invalid ${JSON.stringify(configPath)}: expected { "runtime": { "packageRoot": "./path/to/package" } }.`,
|
|
44
|
+
);
|
|
45
|
+
return resolve(dirname(configPath), configuration.runtime.packageRoot);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function resolvePackageEntrypoint(packageRoot) {
|
|
49
|
+
const owner = await readManifest(join(packageRoot, "package.json"));
|
|
50
|
+
if (
|
|
51
|
+
!isRecord(owner) ||
|
|
52
|
+
![
|
|
53
|
+
owner.dependencies,
|
|
54
|
+
owner.devDependencies,
|
|
55
|
+
owner.optionalDependencies,
|
|
56
|
+
].some(
|
|
57
|
+
(dependencies) =>
|
|
58
|
+
isRecord(dependencies) &&
|
|
59
|
+
typeof dependencies[CLI_PACKAGE_NAME] === "string",
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
throw new RuntimeResolutionError(
|
|
63
|
+
`The package at ${JSON.stringify(packageRoot)} must declare ${CLI_PACKAGE_NAME} as a dependency or devDependency.`,
|
|
64
|
+
);
|
|
65
|
+
const packagePath = await findAncestorEntry({
|
|
66
|
+
cwd: packageRoot,
|
|
67
|
+
relativePath: `node_modules/${CLI_PACKAGE_NAME}`,
|
|
68
|
+
});
|
|
69
|
+
if (packagePath === null)
|
|
70
|
+
throw new RuntimeResolutionError(
|
|
71
|
+
`Cannot find ${CLI_PACKAGE_NAME} from ${JSON.stringify(packageRoot)}. Install that package's dependencies and retry.`,
|
|
72
|
+
);
|
|
73
|
+
const manifestPath = join(packagePath, "package.json");
|
|
74
|
+
const installedManifestPath = await realpath(manifestPath).catch(() => {
|
|
75
|
+
throw new RuntimeResolutionError(
|
|
76
|
+
`Cannot resolve the installed CLI at ${JSON.stringify(manifestPath)}. Reinstall the selected package's dependencies.`,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
const installed = await readManifest(installedManifestPath);
|
|
80
|
+
const entrypoint = isRecord(installed?.bin) ? installed.bin.norn : undefined;
|
|
81
|
+
if (
|
|
82
|
+
installed?.name !== CLI_PACKAGE_NAME ||
|
|
83
|
+
typeof entrypoint !== "string" ||
|
|
84
|
+
!entrypoint.trim() ||
|
|
85
|
+
isAbsolute(entrypoint)
|
|
86
|
+
)
|
|
87
|
+
throw new RuntimeResolutionError(
|
|
88
|
+
`Invalid Norn CLI manifest at ${JSON.stringify(installedManifestPath)}: expected a relative bin.norn entrypoint.`,
|
|
89
|
+
);
|
|
90
|
+
const installationRoot = dirname(installedManifestPath);
|
|
91
|
+
const scriptPath = resolve(installationRoot, entrypoint);
|
|
92
|
+
if (!scriptPath.startsWith(`${installationRoot}${sep}`))
|
|
93
|
+
throw new RuntimeResolutionError(
|
|
94
|
+
`Norn CLI entrypoint must be inside ${JSON.stringify(installationRoot)}.`,
|
|
95
|
+
);
|
|
96
|
+
try {
|
|
97
|
+
if (!(await stat(scriptPath)).isFile()) throw new Error();
|
|
98
|
+
} catch {
|
|
99
|
+
throw new RuntimeResolutionError(
|
|
100
|
+
`Missing Norn CLI entrypoint at ${JSON.stringify(scriptPath)}. Reinstall the selected package's dependencies.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return scriptPath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isRecord(value) {
|
|
107
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readManifest(path) {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
113
|
+
} catch {
|
|
114
|
+
throw new RuntimeResolutionError(
|
|
115
|
+
`Cannot read valid JSON from ${JSON.stringify(path)}. Check the selected package and configuration.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function findAncestorEntry({ cwd, relativePath }) {
|
|
121
|
+
let directory = resolve(cwd);
|
|
122
|
+
while (true) {
|
|
123
|
+
const candidate = join(directory, relativePath);
|
|
124
|
+
try {
|
|
125
|
+
await lstat(candidate);
|
|
126
|
+
return candidate;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code !== "ENOENT")
|
|
129
|
+
throw new RuntimeResolutionError(
|
|
130
|
+
`Cannot inspect ${JSON.stringify(candidate)}.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const parent = dirname(directory);
|
|
134
|
+
if (parent === directory) return null;
|
|
135
|
+
directory = parent;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const NORN_GENERATED_BUILD_INFO: {
|
|
2
2
|
readonly kind: "npm-registry";
|
|
3
|
-
readonly version: "0.1.0-tip.
|
|
4
|
-
readonly commit: "
|
|
3
|
+
readonly version: "0.1.0-tip.36679386408.1";
|
|
4
|
+
readonly commit: "3cb3189ec70f3b4407456061207294d685297b69";
|
|
5
5
|
readonly packageSpec: "@vimhead.dev/norn-cli@tip";
|
|
6
6
|
readonly upgrade: {
|
|
7
7
|
readonly supported: false;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/generated-build-info.ts
|
|
2
2
|
var NORN_GENERATED_BUILD_INFO = {
|
|
3
3
|
"kind": "npm-registry",
|
|
4
|
-
"version": "0.1.0-tip.
|
|
5
|
-
"commit": "
|
|
4
|
+
"version": "0.1.0-tip.36679386408.1",
|
|
5
|
+
"commit": "3cb3189ec70f3b4407456061207294d685297b69",
|
|
6
6
|
"packageSpec": "@vimhead.dev/norn-cli@tip",
|
|
7
7
|
"upgrade": {
|
|
8
8
|
"supported": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vimhead.dev/norn-cli",
|
|
3
|
-
"version": "0.1.0-tip.
|
|
3
|
+
"version": "0.1.0-tip.36679386408.1",
|
|
4
4
|
"description": "Harness-agnostic runtime for agent-driven and code-driven workflows, built primarily for agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"@vimhead.dev/norn-core": "0.0.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
43
|
+
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
44
44
|
"jiti": "^2.7.0",
|
|
45
45
|
"typebox": "^1.3.34",
|
|
46
|
-
"@vimhead.dev/norn": "0.1.0-tip.
|
|
46
|
+
"@vimhead.dev/norn": "0.1.0-tip.36679386408.1"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "node ../../scripts/build-package.ts"
|