@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 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 `norn` available on `PATH`, install the adapter
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
- By default the adapter runs `norn` from `PATH`. To select a CLI executable
181
- outside `PATH`, start Cursor with an executable path:
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 `norn` available on `PATH`, run in your terminal:
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
@@ -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
@@ -1 +1 @@
1
- {"version":"0.1.0-tip.36148588553.1"}
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.36148588553.1",
6
- "commit": "bef15bd5452b3202f93f33f0b33bdf07a48f7ffc",
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({ executable, cwd, group }) {
22
- const { stdout } = await runExecutable(executable, [group, "intro"], {
23
- cwd,
24
- timeout: 10_000,
25
- maxBuffer: MAX_INTRO_BYTES * 2,
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({ executable, cwd }) {
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({ executable, cwd, group: "docs" }),
33
- loadIntroduction({ executable, cwd, group: "workflows" }),
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.36148588553.1";
4
- readonly commit: "bef15bd5452b3202f93f33f0b33bdf07a48f7ffc";
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.36148588553.1",
5
- "commit": "bef15bd5452b3202f93f33f0b33bdf07a48f7ffc",
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.36148588553.1",
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.85.1",
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.36148588553.1"
46
+ "@vimhead.dev/norn": "0.1.0-tip.36679386408.1"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "node ../../scripts/build-package.ts"