@nseng-ai/kernel 0.1.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/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # @ns/kernel
2
+
3
+ `@ns/kernel` owns the `ns` CLI host and the `@ns/kernel/sdk` author API for command-contributing extensions. It owns command discovery, precedence, selected-command loading, argument/schema parsing, rendering glue, completion plumbing, shell integration, and execution-context construction. It does not own concrete workflow policy or capability command surfaces; those belong to the extension or capability package that contributes them.
4
+
5
+ ## Command catalog
6
+
7
+ The kernel builds a command catalog from lightweight metadata before it imports selected command code. Catalog entries can come from four source levels, in increasing precedence:
8
+
9
+ ```text
10
+ built-in command table < preinstalled extension metadata < $XDG_DATA_HOME/ns/extensions < <cwd>/.ns/extensions
11
+ ```
12
+
13
+ Built-in commands are kernel-owned host commands. Preinstalled extension metadata is injected by the installed CLI distribution so reusable first-party extensions can be available without every repository checking in matching manifests. Global and project roots provide user- or repository-local entries. Higher-precedence entries override lower-precedence entries with the same command key; overrides are recorded as diagnostics rather than compatibility aliases.
14
+
15
+ ## Extension roots
16
+
17
+ Global and project extension roots support these one-level entry shapes:
18
+
19
+ ```text
20
+ .ns/extensions/greet.ts
21
+ .ns/extensions/greet.js
22
+ .ns/extensions/greet/index.ts
23
+ .ns/extensions/greet/index.js
24
+ .ns/extensions/package-name/package.json
25
+ ```
26
+
27
+ Direct files and directory indexes infer one command-entry name from the file or directory name. Package manifests provide command metadata without importing command modules:
28
+
29
+ ```json
30
+ {
31
+ "ns": {
32
+ "commands": [
33
+ {
34
+ "name": "greet",
35
+ "description": "Say hello.",
36
+ "fullDescription": "Say hello with custom project policy.",
37
+ "entry": "./src/greet.ts"
38
+ }
39
+ ]
40
+ }
41
+ }
42
+ ```
43
+
44
+ Manifest command entries require `name`, `description`, and a relative POSIX-style `entry` path to a `.ts` or `.js` file. `fullDescription` is optional and defaults to `description`. A manifest can declare a `group` and command `path` segments for nested command surfaces. Command path segments must match `[a-z][a-z0-9-]*`; slashes, colons, spaces, and uppercase names are not supported.
45
+
46
+ Duplicate command names within one extension root are errors. Across roots, higher-precedence sources override lower-precedence sources. The legacy `.ns/commands/<command>.ts` path has been removed and is not a compatibility fallback.
47
+
48
+ ## Preinstalled extension metadata
49
+
50
+ The kernel accepts an injected preinstalled command catalog. Each preinstalled catalog entry carries the same command metadata as a filesystem manifest plus a module specifier for the owning package's command module. The kernel treats these entries as ordinary catalog candidates at the preinstalled precedence level:
51
+
52
+ - top-level help and completion can list them from metadata;
53
+ - global and project entries can override them;
54
+ - selected-command help, JSON schema, and execution import only the selected module;
55
+ - the owning package remains responsible for domain behavior, prompts, gateways, and presentation policy.
56
+
57
+ This keeps the kernel generic while allowing an installed CLI to bundle a reusable catalog.
58
+
59
+ ## Loading behavior
60
+
61
+ Discovery is side-effect-light. `ns --version`, `ns --runtime`, unselected command lookup, and completion use built-in definitions, injected preinstalled metadata, filesystem entries, and JSON manifests without importing unrelated external extension modules.
62
+
63
+ Top-level help (`ns`, `ns --help`, and `ns -h`) imports non-package direct-file and directory-index extension modules only so the command list can show their explicit summaries. Package manifest commands and preinstalled commands are listed from metadata. Malformed discovery entries and help-time import failures that do not affect the selected command are printed as stderr warnings while the invocation continues and stdout remains reserved for primary output.
64
+
65
+ When a command is selected, the kernel imports and validates exactly that command contribution, including selected-command help and `--json-schema`. Diagnostics that affect the selected command are fatal, including higher-precedence broken overrides that would otherwise fall back to a lower-precedence command.
66
+
67
+ ## Shell completion
68
+
69
+ `ns` ships first-party shell completion built on the Clinkr completion engine. Completion is a Clinkr primitive: Clinkr owns the static command/option/value planner over its own surface metadata, and the kernel wires that planner to the dynamic command catalog.
70
+
71
+ Supported shells are bash, zsh, and fish:
72
+
73
+ ```bash
74
+ # bash (~/.bashrc)
75
+ eval "$(ns completion bash)"
76
+
77
+ # zsh (~/.zshrc)
78
+ eval "$(ns completion zsh)"
79
+
80
+ # fish (~/.config/fish/config.fish)
81
+ ns completion fish | source
82
+ ```
83
+
84
+ Each `ns completion <shell>` command prints a setup script that registers a completion hook for `ns`. The script does not embed a snapshot of the command tree; it calls back into `ns completion exec resolve <words...>` so suggestions reflect the current built-in, preinstalled, global, and project-local catalog.
85
+
86
+ Resolution preserves lazy extension loading. Top-level completion is built from side-effect-light catalog metadata. Selected-command option and value completion imports only the selected command, matching selected-command help and JSON schema behavior. Extension commands can provide runtime value candidates through the optional `completionProvider` hook; provider failures are captured so static candidates remain available and resolver stdout stays candidate-only.
87
+
88
+ Limitations:
89
+
90
+ - bash, zsh, and fish only;
91
+ - no Carapace spec export backend;
92
+ - no rich file/directory completion helper API;
93
+ - candidate descriptions are omitted from resolver stdout.
94
+
95
+ ## Extension authoring API
96
+
97
+ Extension modules default-export an extension object created with `defineExtension()` from `@ns/kernel/sdk`. Command contributions live in the extension's optional `commands` array:
98
+
99
+ ```ts
100
+ import { defineExtension, ok, z } from "@ns/kernel/sdk";
101
+
102
+ export default defineExtension({
103
+ commands: [
104
+ {
105
+ name: "greet",
106
+ summary: "Say hello.",
107
+ description: "Say hello with custom project policy.",
108
+ schema: z.object({ name: z.string().default("world") }),
109
+ run(_ctx, request) {
110
+ return ok(`hello ${request.name}`);
111
+ },
112
+ },
113
+ ],
114
+ });
115
+ ```
116
+
117
+ `@ns/kernel/sdk` is the public author API for extensions. `@ns/kernel` is the host/kernel container that loads extensions. The authoritative SDK export reference lives in [`docs/sdk-reference.md`](./docs/sdk-reference.md). Extension authors should import SDK vocabulary from `@ns/kernel/sdk` rather than kernel implementation modules or lower packages unless another package explicitly documents a public API for that dependency.
118
+
119
+ Single-file extension modules are leaf authoring surfaces, not shared libraries. Workspace packages must not import from `.ns/extensions/*.ts` files. If reusable behavior proves out inside one extension, move or copy the contract into an owning package and expose it deliberately through `@ns/kernel/sdk` or another documented package export.
120
+
121
+ ## Boundary checklist
122
+
123
+ Use these cut lines when deciding where code belongs:
124
+
125
+ - **Kernel service:** discovery, loading, precedence, command presentation, completion, execution/context primitives, shell integration, and small author helpers with proven reuse or explicit necessity.
126
+ - **Extension or capability package:** workflow policy, prompts, external command choreography, gateway construction, domain behavior, command-specific rendering, and command names that should travel with the owning package rather than every kernel installation.
127
+ - **Preinstalled catalog entry:** distribution metadata for a reusable extension command, not proof that the kernel owns the command's behavior.
128
+ - **Internal workspace export:** package-to-package sharing for kernel-owned implementation seams, not an author API and not a reason for extension files to import kernel internals.
129
+
130
+ Future command slices should add tests at the layer that owns the behavior: kernel tests for generic discovery/loading/precedence/SDK contracts, owning-package tests for capability command behavior, and host tests for any explicit host mirror.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@nseng-ai/kernel",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "files": [
6
+ "src",
7
+ "README.md"
8
+ ],
9
+ "engines": {
10
+ "node": ">=24.12.0",
11
+ "pnpm": ">=11.8.0"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "bin": {
17
+ "ns": "src/cli/index.ts"
18
+ },
19
+ "exports": {
20
+ "./cli": "./src/cli/index.ts",
21
+ "./command-io": "./src/runtime/command-io.ts",
22
+ "./context": "./src/cli/context.ts",
23
+ "./pi-text-generation": "./src/runtime/pi-text-generation.ts",
24
+ "./progress-phase-state": "./src/sdk/progress-phase-state.ts",
25
+ "./project-config/points": "./src/project-config/points.ts",
26
+ "./sdk": "./src/sdk/index.ts"
27
+ },
28
+ "dependencies": {
29
+ "@earendil-works/pi-ai": "0.79.1",
30
+ "@earendil-works/pi-coding-agent": "0.79.1",
31
+ "@nseng-ai/clinkr": "0.1.2",
32
+ "@nseng-ai/foundation": "0.1.2",
33
+ "jiti": "2.7.0",
34
+ "smol-toml": "^1.6.1",
35
+ "zod": "^4.4.3"
36
+ }
37
+ }
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+
3
+ import {
4
+ renderClinkrCompletionScript,
5
+ type ClinkrCompletionShell,
6
+ } from "@nseng-ai/clinkr/completion";
7
+
8
+ export const nsCompletionShells = ["bash", "zsh", "fish"] as const;
9
+
10
+ export const nsCompletionScriptResultSchema = z.object({
11
+ shell: z.enum(nsCompletionShells),
12
+ script: z.string(),
13
+ });
14
+
15
+ export type NsCompletionScriptResult = z.infer<typeof nsCompletionScriptResultSchema>;
16
+
17
+ export function renderNsCompletionScriptResult(result: NsCompletionScriptResult): string {
18
+ return result.script;
19
+ }
20
+
21
+ export function buildNsCompletionScript(shell: ClinkrCompletionShell): NsCompletionScriptResult {
22
+ return {
23
+ shell,
24
+ script: renderClinkrCompletionScript({
25
+ commandName: "ns",
26
+ shell,
27
+ resolverCommand: ["completion", "exec", "resolve"],
28
+ }),
29
+ };
30
+ }
@@ -0,0 +1,110 @@
1
+ import process from "node:process";
2
+
3
+ import {
4
+ createClinkrInteraction,
5
+ renderCapabilitiesForTerminal,
6
+ resolveProcessCaps,
7
+ type ClinkrInteraction,
8
+ } from "@nseng-ai/clinkr";
9
+ import { readStdinLine } from "@nseng-ai/foundation/cli-runtime";
10
+ import { runCommand } from "@nseng-ai/foundation/exec";
11
+ import { optionalEntry, resolveHomeDir } from "@nseng-ai/foundation/primitives";
12
+
13
+ import { createCliCommandIo, noopNsProgress } from "../runtime/command-io.ts";
14
+ import { PiTextGenerator } from "../runtime/pi-text-generation.ts";
15
+ import type { NsConfirmPrompt, NsExtensionApi } from "../sdk/index.ts";
16
+ import type { TextGenerator } from "../sdk/index.ts";
17
+
18
+ export interface NsCliContext {
19
+ context: NsExtensionApi;
20
+ cwd: string;
21
+ env: Record<string, string | undefined>;
22
+ interaction: ClinkrInteraction;
23
+ stdout: (text: string) => void;
24
+ stderr: (text: string) => void;
25
+ }
26
+
27
+ export interface RealNsCommandContextOptions {
28
+ cwd?: string;
29
+ env?: Record<string, string | undefined>;
30
+ homeDir?: string;
31
+ }
32
+
33
+ export function createTextGenerator(): TextGenerator {
34
+ return new PiTextGenerator();
35
+ }
36
+
37
+ export function createRealNsCommandContext(
38
+ options: RealNsCommandContextOptions = {},
39
+ ): NsExtensionApi {
40
+ const cwd = options.cwd ?? process.cwd();
41
+ const env = options.env ?? process.env;
42
+ const homeDir = resolveHomeDir(options.homeDir, env);
43
+ const textGenerator = createTextGenerator();
44
+ const confirm = createTerminalConfirmPrompt();
45
+ const stdout = (text: string) => process.stdout.write(text);
46
+ const stderr = (text: string) => process.stderr.write(text);
47
+ const commandIo = createCliCommandIo({ stdout, stderr });
48
+ return {
49
+ cwd,
50
+ env,
51
+ ...optionalEntry("homeDir", homeDir),
52
+ textGenerator,
53
+ commandIo,
54
+ progress: noopNsProgress,
55
+ renderCapabilities: renderCapabilitiesForTerminal(resolveProcessCaps()),
56
+ outputFormat: "human",
57
+ stdout,
58
+ stderr,
59
+ exec: async (command, args, execOptions = {}) => {
60
+ const result = await runCommand(command, args, {
61
+ cwd,
62
+ env,
63
+ ...(execOptions.timeoutMs === undefined ? {} : { timeout: execOptions.timeoutMs }),
64
+ ...(execOptions.stdin === undefined ? {} : { stdin: execOptions.stdin }),
65
+ ...(execOptions.onStdout === undefined ? {} : { onStdout: execOptions.onStdout }),
66
+ ...(execOptions.onStderr === undefined ? {} : { onStderr: execOptions.onStderr }),
67
+ });
68
+ return {
69
+ code: result.code,
70
+ stdout: result.stdout,
71
+ stderr: result.stderr,
72
+ killed: result.killed,
73
+ };
74
+ },
75
+ ...(confirm === undefined ? {} : { confirm }),
76
+ };
77
+ }
78
+
79
+ export function createNsCliInteraction(options: {
80
+ stderr: (text: string) => void;
81
+ }): ClinkrInteraction {
82
+ return createClinkrInteraction(createBaseCliInteractionOptions(options.stderr));
83
+ }
84
+
85
+ export function createTerminalConfirmPrompt(): NsConfirmPrompt | undefined {
86
+ if (process.stdin.isTTY !== true || process.stderr.isTTY !== true) return undefined;
87
+ return async (title, message, options) => {
88
+ const interaction = createClinkrInteraction({
89
+ ...createBaseCliInteractionOptions((text) => {
90
+ process.stderr.write(text);
91
+ }),
92
+ isInteractive: () => process.stdin.isTTY === true && process.stderr.isTTY === true,
93
+ formatPrompt: (_request, suffix) => `${title}\n\n${message}\n\nProceed? ${suffix} `,
94
+ });
95
+ const result = await interaction.confirm({
96
+ message,
97
+ defaultAnswer: options?.defaultAnswer ?? "no",
98
+ });
99
+ return result.type === "confirmed";
100
+ };
101
+ }
102
+
103
+ function createBaseCliInteractionOptions(stderr: (text: string) => void) {
104
+ return {
105
+ stdin: readStdinLine,
106
+ stderr,
107
+ };
108
+ }
109
+
110
+ export type { NsExtensionApi } from "../sdk/index.ts";