@vimhead.dev/norn-cli 0.1.0-tip.35976246970.1 → 0.1.0-tip.36533963240.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
 
@@ -149,15 +160,56 @@ methods, custom providers, and Norn's configuration directory, see
149
160
 
150
161
  ### 3. Optionally connect your harness
151
162
 
152
- The shipped Pi and Cursor adapters deliver Norn documentation context and
163
+ The shipped Pi, Cursor, and Claude Code adapters deliver Norn documentation context and
153
164
  [available workflow introductions](docs/cli.md#workflow-introduction) to your
154
- harness at session start. Installing Norn alone does not register an adapter. Claude Code, Codex,
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,13 +229,39 @@ 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 .
185
237
  ```
186
238
 
239
+ #### Claude Code
240
+
241
+ With Node and a [selected runtime](#project-local-runtime-selection), run in your terminal:
242
+
243
+ ```bash
244
+ claude plugin marketplace add vimhead/norn
245
+ claude plugin install norn@norn-adapters
246
+ claude
247
+ ```
248
+
249
+ To select a different runtime, launch Claude Code with
250
+ `NORN_EXECUTABLE=/absolute/path/to/norn claude`. The plugin uses that runtime's
251
+ documentation and the workflows in the session's working directory; it does not
252
+ install Norn or configure authentication for Norn agents.
253
+
254
+ Context is added on startup, `/clear`, and compaction, not again on resume or
255
+ fork. Named-agent sessions (`--agent`) and subagent hook calls are excluded.
256
+ Existing instructions and prompts are preserved. Introduction failures appear as
257
+ hook errors without falling back to another runtime.
258
+
259
+ To test a checkout without installing the marketplace:
260
+
261
+ ```bash
262
+ claude --plugin-dir /absolute/path/to/norn
263
+ ```
264
+
187
265
  Need an adapter for another harness? [Open an issue](https://github.com/vimhead/norn/issues/new)
188
266
  with the harness name.
189
267
 
@@ -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.35976246970.1"}
1
+ {"version":"0.1.0-tip.36533963240.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.35976246970.1",
6
- "commit": "85fe72374183623e66290c6315b3ba51bee7a79c",
5
+ "version": "0.1.0-tip.36533963240.1",
6
+ "commit": "73cebf815a2158d1f7e7e5614225e36a52b73924",
7
7
  "packageSpec": "@vimhead.dev/norn-cli@tip",
8
8
  "upgrade": {
9
9
  "supported": false,
@@ -0,0 +1,47 @@
1
+ import { execFile } from "node:child_process";
2
+ import { Buffer } from "node:buffer";
3
+ import { promisify } from "node:util";
4
+ import { resolveNornRuntime } from "./runtime-resolution.mjs";
5
+
6
+ const runExecutable = promisify(execFile);
7
+ const MAX_INTRO_BYTES = 16_384;
8
+
9
+ function readIntroResponse({ stdout, group }) {
10
+ const response = JSON.parse(stdout);
11
+ if (
12
+ !response ||
13
+ typeof response !== "object" ||
14
+ typeof response.intro !== "string" ||
15
+ (group === "docs" && response.intro.trim().length === 0) ||
16
+ Buffer.byteLength(response.intro, "utf8") > MAX_INTRO_BYTES
17
+ )
18
+ throw new Error("Invalid Norn introduction response");
19
+ return response.intro;
20
+ }
21
+
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
+ );
32
+ return readIntroResponse({ stdout, group });
33
+ }
34
+
35
+ export async function loadHostIntroduction({ executableOverride, cwd }) {
36
+ const invocation = await resolveNornRuntime({
37
+ cwd,
38
+ executableOverride,
39
+ isProjectTrusted: true,
40
+ nodeExecutable: process.execPath,
41
+ });
42
+ const [intro, workflowsIntro] = await Promise.all([
43
+ loadIntroduction({ invocation, cwd, group: "docs" }),
44
+ loadIntroduction({ invocation, cwd, group: "workflows" }),
45
+ ]);
46
+ return `<norn-docs-intro>\n${intro}\n</norn-docs-intro>${workflowsIntro ? `\n\n${workflowsIntro}` : ""}`;
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.35976246970.1";
4
- readonly commit: "85fe72374183623e66290c6315b3ba51bee7a79c";
3
+ readonly version: "0.1.0-tip.36533963240.1";
4
+ readonly commit: "73cebf815a2158d1f7e7e5614225e36a52b73924";
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.35976246970.1",
5
- "commit": "85fe72374183623e66290c6315b3ba51bee7a79c",
4
+ "version": "0.1.0-tip.36533963240.1",
5
+ "commit": "73cebf815a2158d1f7e7e5614225e36a52b73924",
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.35976246970.1",
3
+ "version": "0.1.0-tip.36533963240.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",
@@ -43,7 +43,7 @@
43
43
  "@earendil-works/pi-coding-agent": "0.85.1",
44
44
  "jiti": "^2.7.0",
45
45
  "typebox": "^1.3.34",
46
- "@vimhead.dev/norn": "0.1.0-tip.35976246970.1"
46
+ "@vimhead.dev/norn": "0.1.0-tip.36533963240.1"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "node ../../scripts/build-package.ts"