@jini-ai/cli 0.3.1 → 0.4.0

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 ADDED
@@ -0,0 +1,95 @@
1
+ # @jini-ai/cli
2
+
3
+ ## 0.4.0 — 2026-10-02
4
+
5
+ ### BREAKING
6
+
7
+ - Shared sanitization now comes from core/text and loopback policy from platform/net. Public helpers use required/optional argument objects.
8
+ - Distribution includes runtime output, release documentation and required assets only. Process records and per-job neutrality checks are no longer part of the package surface.
9
+
10
+ ## Unreleased
11
+
12
+ - Fix command and discovery test calls to match required/optional argument objects, and create the command-registry token with core's `{ id }` argument.
13
+
14
+ - **BREAKING:** Remove CLI sanitizeUntrustedText/SanitizeTextOptions/stripControlSequences exports;
15
+ callers import core/text. Remove redactSecretLike in favor of core redactSecrets with explicit
16
+ aggressive policy for former direct opaque-token users. Its category markers and PII masking
17
+ are covered by migration vectors; core's default remains conservative. Terminal transport and
18
+ recursive JSON sanitization keep their frozen lowercase output.
19
+ - Depend on platform/net for daemon URL loopback checks; recognize full IPv4 loopback range and
20
+ dotted localhost. Mapped IPv6 retains remote-warning behavior. Tests are written, not run.
21
+
22
+ - Emit structured JSON HTTP failures through injected ports for unknown codes, response-size and
23
+ body-read failures. Include the truncation marker within diagnostic maxLength and correct the
24
+ binary version rationale. Regression tests are written, not run (owner directive).
25
+
26
+ - **BREAKING:** Public helpers, command handlers/registrations and injected I/O ports now take required and optional argument objects. Output, exit, warning and file-reader ports receive `{ text }`, `{ code }`, `{ message }` and `{ path }`. See `API.md` and the archived migration inventory.
27
+ - Expose introspection from the library barrel and declare its universal runtime without importing the binary entrypoint.
28
+
29
+ - Add an introspection subpath for structural Commander trees and MCP schemas with required host exclusions and tool-name prefix.
30
+
31
+ ## 0.1.2
32
+
33
+ ### Patch Changes
34
+
35
+ - Add top-level `main`/`types` fields alongside the existing `exports` map. A consumer on
36
+ TypeScript's classic `moduleResolution: "node"` (node10) — which ignores `package.json#exports`
37
+ entirely — could not resolve this package's types at all (`TS2307: Cannot find module`) even
38
+ after the previous exports-map fix restored `require()` at runtime; type resolution and runtime
39
+ resolution are separate algorithms. Verified against a real external consumer (whose
40
+ tsconfig uses this legacy resolution mode): adding these two fields, with its tsconfig completely
41
+ unchanged, made the error disappear. Also fixes absolute-path `require()` (distinct from a bare
42
+ specifier, which already worked) for the same reason — `main` was previously absent.
43
+
44
+ Purely additive: every modern resolver (Node's own runtime `exports` resolution, TypeScript's
45
+ `bundler`/`node16`/`nodenext`) prefers `exports` over `main`/`types` when both are present, so
46
+ this changes nothing for a consumer already on a modern resolver.
47
+
48
+ - Updated dependencies
49
+ - @jini-ai/core@0.1.2
50
+ - @jini-ai/sidecar@0.1.2
51
+
52
+ ## 0.1.1
53
+
54
+ ### Patch Changes
55
+
56
+ - Add a `"default"` export condition to every published package's `exports` map — every one of
57
+ them lacked it, which meant `require()` failed with `ERR_PACKAGE_PATH_NOT_EXPORTED` for any
58
+ CommonJS consumer (found via a real external integration attempt; Node needs `require(esm)`
59
+ support, i.e. Node >=22.12, for this to resolve).
60
+
61
+ `@jini-ai/agent-runtime`:
62
+
63
+ - **New**: `RuntimeBuildOptions.permissionMode` (`'bypass' | 'restricted'`) lets a caller opt a
64
+ run OUT of the auto-approve-every-permission-prompt flag every def with one
65
+ (`bypassPermissions` / `--yolo` / `--dangerously-skip-permissions`) previously pushed
66
+ unconditionally, with no way to turn it off. Omitting it keeps today's default (bypass)
67
+ behavior unchanged.
68
+ - **New**: `ClaudeStreamEvent`, `CopilotStreamEvent`, and `QoderEvent` are now real exported
69
+ discriminated unions instead of `Record<string, unknown>` — a real external consumer guessed a
70
+ nonexistent field name (`event.text` instead of the actual `event.delta`) against the old
71
+ untyped sink and silently lost every streamed token with no compile or runtime error.
72
+ - Fixed a doc/implementation mismatch in `claude-stream.ts`: the module doc claimed `tool_result`
73
+ events carry `{ tool_use_id, content, is_error }`; the actual emitted shape is
74
+ `{ toolUseId, content, isError }`.
75
+
76
+ `@jini-ai/daemon`: `AgentExecutorRunInput.permissionMode` forwards the new
77
+ `RuntimeBuildOptions.permissionMode` through to `buildArgs`, so a host can actually reach the new
78
+ opt-out from the daemon's real run-input surface, not just from `@jini-ai/agent-runtime` in
79
+ isolation.
80
+
81
+ `@jini-ai/agentic`: `setAtPointer` no longer throws on a malformed (e.g. missing leading `/`)
82
+ `updateDataModel` path — degrades to a no-op like its sibling `getAtPointer`, matching this
83
+ package's own "a bad binding must not crash the renderer" contract. That path is agent-authored
84
+ wire data with no error boundary above it in any host, so the uncaught throw could unmount an
85
+ entire chat UI from ~40 bytes of malformed input.
86
+
87
+ `@jini-ai/chat-react`: a local (client-resolved) A2UI button action is no longer a silent no-op —
88
+ `A2uiSurfaceCard` now surfaces the resolved value. New `ExtEventErrorBoundary` confines a
89
+ `kind: 'ext'` event group's renderer to its own card instead of letting a render/effect-phase
90
+ throw from agent-controlled content unmount the whole chat root (there was no error boundary
91
+ anywhere in this package or its hosts before this).
92
+
93
+ - Updated dependencies
94
+ - @jini-ai/core@0.1.1
95
+ - @jini-ai/sidecar@0.1.1
package/README.md CHANGED
@@ -5,7 +5,7 @@ local daemon discovery, and a pluggable command registry. This is deliberately *
5
5
  slice, not the full `@jini-ai/cli` package** — it was extracted from Open Design's ~9,900-line
6
6
  `cli.ts` by keeping only the transport/plumbing primitives that have nothing to do with any
7
7
  product noun (project, plugin, brand, conversation, …); everything OD-domain-specific was left
8
- behind (see `source-map.md`'s classification table). The library barrel (`index.ts`) intentionally
8
+ behind (see the archived provenance ledger's classification table). The library barrel (`index.ts`) intentionally
9
9
  registers nothing — it ships the building blocks a host pack uses to register real commands, plus
10
10
  a small set of concrete commands (`run`, `daemon`, `version`) that wrap `@jini-ai/http`'s actual
11
11
  routes, and a bootable `jini` binary (`src/main.ts`, wired via `package.json`'s `bin` field) that
@@ -33,27 +33,27 @@ No peer dependencies. Depends on `@jini-ai/core` (for the `token()`-based transp
33
33
  - **Structured errors** — `exitWithStructuredError`, `DEFAULT_CLI_EXIT_CODES`,
34
34
  `structuredErrorData` — maps an error code to a stable exit code and a `{ error: { code,
35
35
  message, data } }` stderr envelope.
36
- - **Command registry** — `CommandRegistry` (`.add(name, handler, { usage?, override? })`,
37
- `.dispatch(argv, { valueFlags? })`), `CommandDispatchResult`, `CommandHandler` — the generic
36
+ - **Command registry** — `CommandRegistry` (`.add({ name, handler }, { usage?, override? })`,
37
+ `.dispatch({ argv }, { valueFlags? })`), `CommandDispatchResult`, `CommandHandler` — the generic
38
38
  "first-non-flag-token dispatch" shape a pack's `Pack['cli']` hook registers against.
39
39
  - **Built-in commands** — `registerRunCommands` (`run start|list|get|cancel|watch`),
40
40
  `registerDaemonCommands` (`daemon status|stop`), `registerVersionCommand` (`version`) — each a
41
41
  thin transport over `@jini-ai/http`'s run/daemon-status routes.
42
42
  - **Prompt/body input helpers** — `readPromptFromFlags`/`readBodyFromFlags` (`--prompt`/
43
- `--prompt-file`/stdin conventions) and `redact.ts`'s `stripControlSequences`.
43
+ `--prompt-file`/stdin conventions) and `@jini-ai/core/text`'s `stripControlSequences`.
44
44
 
45
45
  ## Usage
46
46
 
47
47
  ```ts
48
48
  import { CommandRegistry, resolveDaemonUrl, registerRunCommands, exitWithStructuredError } from '@jini-ai/cli';
49
49
 
50
- const registry = new CommandRegistry();
50
+ const registry = new CommandRegistry({});
51
51
 
52
- registerRunCommands(registry, {
53
- resolveBaseUrl: () => resolveDaemonUrl({ envVarName: 'JINI_DAEMON_URL', defaultUrl: 'http://127.0.0.1:4317' }),
52
+ registerRunCommands({ registry,
53
+ resolveBaseUrl: () => resolveDaemonUrl({}, { envVarName: 'JINI_DAEMON_URL', defaultUrl: 'http://127.0.0.1:4317' }),
54
54
  });
55
55
 
56
- const result = await registry.dispatch(process.argv.slice(2));
56
+ const result = await registry.dispatch({ argv: process.argv.slice(2) });
57
57
  if (result.kind === 'not-found') {
58
58
  exitWithStructuredError({ code: 'invalid-flag', message: `unknown command "${result.name}"` });
59
59
  }
@@ -64,9 +64,11 @@ and run its `jini` binary directly (`npx jini run list --daemon-url http://127.0
64
64
 
65
65
  ## What's swappable
66
66
 
67
+ Command handlers and registrars receive `resolveBaseUrl` in the required argument object; optional I/O ports belong in the second object. Call a discovery probe with `discover({ env }, { timeoutMs })`.
68
+
67
69
  `resolveDaemonUrl`'s `discover` callback is the intended injection point for daemon-discovery
68
70
  strategy — `createLocalDaemonDiscovery` is the one concrete implementation shipped, but any
69
- `(env, timeoutMs) => Promise<string | null>` works. `CommandRegistry` itself is the seam a pack
71
+ `({ env }, { timeoutMs }) => Promise<string | null>` works. `CommandRegistry` itself is the seam a pack
70
72
  uses to add or `override` commands. The exit-code table (`DEFAULT_CLI_EXIT_CODES`) is a small
71
73
  default a caller extends with its own `code → exitCode` entries rather than a fixed, closed set.
72
74
 
@@ -77,4 +79,19 @@ ESM only — ships `"type": "module"` with no CommonJS `require` build.
77
79
 
78
80
  ## Provenance
79
81
 
80
- See [source-map.md](./source-map.md) for per-file provenance and scope decisions. Apache-2.0.
82
+ See the archived provenance ledger for per-file provenance and scope decisions. Apache-2.0.
83
+
84
+ ## Introspection and I/O ports
85
+
86
+ The universal `./introspection` entry reads structural Commander trees and builds MCP schemas with caller-owned exclusions and tool-name prefixes. It is also exported from the Node library barrel. The binary stays separate. See [API.md](https://github.com/AINSEP/Jini/blob/main/packages/cli/API.md) for the object argument convention, including injected I/O ports.
87
+
88
+ ## Design decisions
89
+
90
+ - [Introspection derives from the live command tree](docs/decisions/DR-001-live-command-introspection.md).
91
+
92
+
93
+ Text sanitization and terminal control stripping now live at `@jini-ai/core/text`, with their
94
+ bounded lowercase-marker output preserved. Former direct `redactSecretLike` callers use core
95
+ `redactSecrets({ input }, { policy: 'aggressive' })`; this explicit policy emits category markers
96
+ and also masks recognized PII. Core's default stays conservative for diagnostics. The CLI keeps
97
+ `sanitizeUnknownDeep` for bounded recursive JSON. Loopback warnings use `@jini-ai/platform/net`.
@@ -2,14 +2,16 @@
2
2
  * @module command-registry
3
3
  *
4
4
  * The generic dispatcher shape behind OD's `SUBCOMMAND_MAP` (see
5
- * `source-map.md`): look at the first non-flag `argv` token, dispatch to a
5
+ * `archived provenance ledger`): look at the first non-flag `argv` token, dispatch to a
6
6
  * registered handler, or report that nothing matched so the caller can fall
7
7
  * back to root help. OD's actual map (33 product command names) was not
8
8
  * ported — only the registrar pattern, which is what `@jini-ai/core`'s
9
9
  * `Pack['cli']` hook (`cli?: (reg: unknown, services: Services) => void`)
10
10
  * needs a concrete `reg` type for.
11
11
  */
12
- export type CommandHandler = (args: readonly string[]) => void | Promise<void>;
12
+ export type CommandHandler = (requiredArgs: {
13
+ args: readonly string[];
14
+ }) => void | Promise<void>;
13
15
  export type CommandDispatchResult = {
14
16
  kind: 'handled';
15
17
  } | {
@@ -53,17 +55,25 @@ export interface CommandDispatchOptions {
53
55
  * to skip over a value-flag's value while scanning for the command name.
54
56
  */
55
57
  export declare class CommandRegistry {
58
+ constructor(_requiredArgs?: Record<string, never>);
56
59
  private readonly commands;
57
60
  /**
58
61
  * Register a command. Throws if `name` is already registered — pass
59
62
  * `{ override: true }` to intentionally replace an existing registration.
60
63
  */
61
- add(name: string, handler: CommandHandler, opts?: AddCommandOptions): this;
62
- has(name: string): boolean;
64
+ add({ name, handler }: {
65
+ name: string;
66
+ handler: CommandHandler;
67
+ }, opts?: AddCommandOptions): this;
68
+ has({ name }: {
69
+ name: string;
70
+ }): boolean;
63
71
  /** The usage text registered alongside `name`, if any. */
64
- usageFor(name: string): string | undefined;
72
+ usageFor({ name }: {
73
+ name: string;
74
+ }): string | undefined;
65
75
  /** Every registered command name, insertion order. */
66
- names(): string[];
76
+ names(_requiredArgs?: Record<string, never>): string[];
67
77
  /**
68
78
  * Find the first non-`-`-prefixed token in `argv`, look up its handler,
69
79
  * and invoke it with every other token (flags before *and* after the
@@ -77,6 +87,8 @@ export declare class CommandRegistry {
77
87
  * `options.valueFlags` — the set of global `--flag` names that consume the
78
88
  * next token as a value — so the scan skips over it instead.
79
89
  */
80
- dispatch(argv: readonly string[], options?: CommandDispatchOptions): Promise<CommandDispatchResult>;
90
+ dispatch({ argv }: {
91
+ argv: readonly string[];
92
+ }, options?: CommandDispatchOptions): Promise<CommandDispatchResult>;
81
93
  }
82
94
  //# sourceMappingURL=command-registry.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * @module command-registry
3
3
  *
4
4
  * The generic dispatcher shape behind OD's `SUBCOMMAND_MAP` (see
5
- * `source-map.md`): look at the first non-flag `argv` token, dispatch to a
5
+ * `archived provenance ledger`): look at the first non-flag `argv` token, dispatch to a
6
6
  * registered handler, or report that nothing matched so the caller can fall
7
7
  * back to root help. OD's actual map (33 product command names) was not
8
8
  * ported — only the registrar pattern, which is what `@jini-ai/core`'s
@@ -18,27 +18,28 @@
18
18
  * to skip over a value-flag's value while scanning for the command name.
19
19
  */
20
20
  export class CommandRegistry {
21
+ constructor(_requiredArgs = {}) { }
21
22
  commands = new Map();
22
23
  /**
23
24
  * Register a command. Throws if `name` is already registered — pass
24
25
  * `{ override: true }` to intentionally replace an existing registration.
25
26
  */
26
- add(name, handler, opts = {}) {
27
+ add({ name, handler }, opts = {}) {
27
28
  if (this.commands.has(name) && opts.override !== true) {
28
29
  throw new Error(`command "${name}" is already registered; pass { override: true } to replace it intentionally`);
29
30
  }
30
31
  this.commands.set(name, { name, handler, ...(opts.usage !== undefined ? { usage: opts.usage } : {}) });
31
32
  return this;
32
33
  }
33
- has(name) {
34
+ has({ name }) {
34
35
  return this.commands.has(name);
35
36
  }
36
37
  /** The usage text registered alongside `name`, if any. */
37
- usageFor(name) {
38
+ usageFor({ name }) {
38
39
  return this.commands.get(name)?.usage;
39
40
  }
40
41
  /** Every registered command name, insertion order. */
41
- names() {
42
+ names(_requiredArgs = {}) {
42
43
  return [...this.commands.keys()];
43
44
  }
44
45
  /**
@@ -54,7 +55,7 @@ export class CommandRegistry {
54
55
  * `options.valueFlags` — the set of global `--flag` names that consume the
55
56
  * next token as a value — so the scan skips over it instead.
56
57
  */
57
- async dispatch(argv, options = {}) {
58
+ async dispatch({ argv }, options = {}) {
58
59
  const valueFlags = options.valueFlags ?? new Set();
59
60
  let index = -1;
60
61
  for (let i = 0; i < argv.length; i++) {
@@ -81,7 +82,7 @@ export class CommandRegistry {
81
82
  if (command === undefined)
82
83
  return { kind: 'not-found', name };
83
84
  const rest = [...argv.slice(0, index), ...argv.slice(index + 1)];
84
- await command.handler(rest);
85
+ await command.handler({ args: rest });
85
86
  return { kind: 'handled' };
86
87
  }
87
88
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `daemon status` / `daemon stop` — concrete commands over `@jini-ai/http-kit`'s
5
5
  * `daemonStatusRoute`/`daemonShutdownRoute` (`packages/http/src/daemon-status.ts`).
6
- * `packages/cli/source-map.md`'s original UNCLEAR verdict on these two
6
+ * `packages/cli/archived provenance ledger`'s original UNCLEAR verdict on these two
7
7
  * blocked on `@jini-ai/http-kit` being a stub with no `/status`/`/shutdown` route to
8
8
  * call; that blocker no longer holds (both routes exist and are tested), and
9
9
  * `run-command.ts` is now a real, established "first concrete command"
@@ -23,20 +23,35 @@ export interface DaemonCommandDeps {
23
23
  /** Resolves the daemon HTTP base URL once per command invocation (e.g. wraps `resolveDaemonUrl`). */
24
24
  resolveBaseUrl: () => Promise<string> | string;
25
25
  /** Defaults to `process.stdout.write`; inject for tests. Used for successful command output. */
26
- write?: (text: string) => void;
26
+ write?: (requiredArgs: {
27
+ text: string;
28
+ }) => void;
27
29
  /** Defaults to `process.stderr.write`; inject for tests. Used for usage/validation errors. */
28
- writeErr?: (text: string) => void;
30
+ writeErr?: (requiredArgs: {
31
+ text: string;
32
+ }) => void;
29
33
  /** Defaults to the global `fetch`; inject for tests. */
30
34
  fetchImpl?: typeof fetch;
31
35
  /** Defaults to `process.exit`; inject for tests (must not return). */
32
- exit?: (code: number) => never;
36
+ exit?: (requiredArgs: {
37
+ code: number;
38
+ }) => never;
33
39
  /** Extra/overriding `code -> exitCode` entries layered on the package default table. */
34
40
  exitCodes?: ExitCodeTable;
35
41
  }
36
42
  /** `daemon status` */
37
- export declare function daemonStatusCommand(args: readonly string[], deps: DaemonCommandDeps): Promise<void>;
43
+ export declare function daemonStatusCommand({ args, resolveBaseUrl }: {
44
+ args: readonly string[];
45
+ resolveBaseUrl: DaemonCommandDeps["resolveBaseUrl"];
46
+ }, optionalArgs?: Omit<DaemonCommandDeps, "resolveBaseUrl">): Promise<void>;
38
47
  /** `daemon stop` */
39
- export declare function daemonStopCommand(args: readonly string[], deps: DaemonCommandDeps): Promise<void>;
48
+ export declare function daemonStopCommand({ args, resolveBaseUrl }: {
49
+ args: readonly string[];
50
+ resolveBaseUrl: DaemonCommandDeps["resolveBaseUrl"];
51
+ }, optionalArgs?: Omit<DaemonCommandDeps, "resolveBaseUrl">): Promise<void>;
40
52
  /** Registers `daemon` (dispatching `status`/`stop` on the first remaining token) against `registry`. */
41
- export declare function registerDaemonCommands(registry: CommandRegistry, deps: DaemonCommandDeps): void;
53
+ export declare function registerDaemonCommands({ registry, resolveBaseUrl }: {
54
+ registry: CommandRegistry;
55
+ resolveBaseUrl: DaemonCommandDeps["resolveBaseUrl"];
56
+ }, optionalArgs?: Omit<DaemonCommandDeps, "resolveBaseUrl">): void;
42
57
  //# sourceMappingURL=daemon-command.d.ts.map
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `daemon status` / `daemon stop` — concrete commands over `@jini-ai/http-kit`'s
5
5
  * `daemonStatusRoute`/`daemonShutdownRoute` (`packages/http/src/daemon-status.ts`).
6
- * `packages/cli/source-map.md`'s original UNCLEAR verdict on these two
6
+ * `packages/cli/archived provenance ledger`'s original UNCLEAR verdict on these two
7
7
  * blocked on `@jini-ai/http-kit` being a stub with no `/status`/`/shutdown` route to
8
8
  * call; that blocker no longer holds (both routes exist and are tested), and
9
9
  * `run-command.ts` is now a real, established "first concrete command"
@@ -20,29 +20,20 @@
20
20
  import { exitWithStructuredError } from './errors.js';
21
21
  import { getJsonFromDaemon, postJsonToDaemon } from './http.js';
22
22
  import { renderUsage } from './usage.js';
23
- function defaultWrite(text) {
23
+ function defaultWrite({ text }) {
24
24
  process.stdout.write(text);
25
25
  }
26
- function defaultWriteErr(text) {
26
+ function defaultWriteErr({ text }) {
27
27
  process.stderr.write(text);
28
28
  }
29
- const DAEMON_STATUS_USAGE = renderUsage({
30
- usage: ['daemon status'],
31
- description: 'Reports the running daemon\'s version/host/port/dataDir/shutdown state.',
32
- });
33
- const DAEMON_STOP_USAGE = renderUsage({
34
- usage: ['daemon stop'],
35
- description: 'Schedules a graceful daemon shutdown. Responds once the shutdown is scheduled, not once the process has actually exited.',
36
- });
37
- const DAEMON_USAGE = renderUsage({
38
- usage: ['daemon <status|stop> ...'],
39
- description: 'Inspect or control a running Jini daemon over HTTP.',
40
- });
29
+ const DAEMON_STATUS_USAGE = renderUsage({ usage: ['daemon status'] }, { description: 'Reports the running daemon\'s version/host/port/dataDir/shutdown state.' });
30
+ const DAEMON_STOP_USAGE = renderUsage({ usage: ['daemon stop'] }, { description: 'Schedules a graceful daemon shutdown. Responds once the shutdown is scheduled, not once the process has actually exited.' });
31
+ const DAEMON_USAGE = renderUsage({ usage: ['daemon <status|stop> ...'] }, { description: 'Inspect or control a running Jini daemon over HTTP.' });
41
32
  /** Builds `{write, exit, exitCodes?}` for `errors.ts`'s structured-error helpers, never assigning `exitCodes` when unset (required under `exactOptionalPropertyTypes`). */
42
33
  function errorOptions(deps) {
43
34
  return {
44
35
  write: deps.writeErr ?? defaultWriteErr,
45
- exit: deps.exit ?? ((code) => process.exit(code)),
36
+ exit: deps.exit ?? (({ code }) => process.exit(code)),
46
37
  ...(deps.exitCodes !== undefined ? { exitCodes: deps.exitCodes } : {}),
47
38
  };
48
39
  }
@@ -59,45 +50,48 @@ function invalidFlag(deps, message) {
59
50
  return exitWithStructuredError({ code: 'invalid-flag', message }, errorOptions(deps));
60
51
  }
61
52
  function printJsonResult(deps, value) {
62
- (deps.write ?? defaultWrite)(`${JSON.stringify(value)}\n`);
53
+ (deps.write ?? defaultWrite)({ text: `${JSON.stringify(value)}\n` });
63
54
  }
64
55
  /** `daemon status` */
65
- export async function daemonStatusCommand(args, deps) {
56
+ export async function daemonStatusCommand({ args, resolveBaseUrl }, optionalArgs = {}) {
57
+ const deps = { resolveBaseUrl, ...optionalArgs };
66
58
  if (args.includes('--help') || args.includes('-h')) {
67
- (deps.write ?? defaultWrite)(`${DAEMON_STATUS_USAGE}\n`);
59
+ (deps.write ?? defaultWrite)({ text: `${DAEMON_STATUS_USAGE}\n` });
68
60
  return;
69
61
  }
70
62
  const baseUrl = await deps.resolveBaseUrl();
71
- const result = await getJsonFromDaemon(baseUrl, '/api/daemon/status', transportOptions(deps));
63
+ const result = await getJsonFromDaemon({ base: baseUrl, route: '/api/daemon/status' }, transportOptions(deps));
72
64
  printJsonResult(deps, result);
73
65
  }
74
66
  /** `daemon stop` */
75
- export async function daemonStopCommand(args, deps) {
67
+ export async function daemonStopCommand({ args, resolveBaseUrl }, optionalArgs = {}) {
68
+ const deps = { resolveBaseUrl, ...optionalArgs };
76
69
  if (args.includes('--help') || args.includes('-h')) {
77
- (deps.write ?? defaultWrite)(`${DAEMON_STOP_USAGE}\n`);
70
+ (deps.write ?? defaultWrite)({ text: `${DAEMON_STOP_USAGE}\n` });
78
71
  return;
79
72
  }
80
73
  const baseUrl = await deps.resolveBaseUrl();
81
- const result = await postJsonToDaemon(baseUrl, '/api/daemon/shutdown', {}, transportOptions(deps));
74
+ const result = await postJsonToDaemon({ base: baseUrl, route: '/api/daemon/shutdown', body: {} }, transportOptions(deps));
82
75
  printJsonResult(deps, result);
83
76
  }
84
77
  /** Registers `daemon` (dispatching `status`/`stop` on the first remaining token) against `registry`. */
85
- export function registerDaemonCommands(registry, deps) {
86
- registry.add('daemon', async (args) => {
87
- const [sub, ...rest] = args;
88
- switch (sub) {
89
- case 'status':
90
- return daemonStatusCommand(rest, deps);
91
- case 'stop':
92
- return daemonStopCommand(rest, deps);
93
- case undefined:
94
- case '--help':
95
- case '-h':
96
- (deps.write ?? defaultWrite)(`${DAEMON_USAGE}\n`);
97
- return;
98
- default:
99
- invalidFlag(deps, `unknown "daemon" subcommand: ${sub}`);
100
- }
101
- }, { usage: DAEMON_USAGE });
78
+ export function registerDaemonCommands({ registry, resolveBaseUrl }, optionalArgs = {}) {
79
+ const deps = { resolveBaseUrl, ...optionalArgs };
80
+ registry.add({ name: 'daemon', handler: async ({ args }) => {
81
+ const [sub, ...rest] = args;
82
+ switch (sub) {
83
+ case 'status':
84
+ return daemonStatusCommand({ args: rest, resolveBaseUrl: resolveBaseUrl }, optionalArgs);
85
+ case 'stop':
86
+ return daemonStopCommand({ args: rest, resolveBaseUrl: resolveBaseUrl }, optionalArgs);
87
+ case undefined:
88
+ case '--help':
89
+ case '-h':
90
+ (deps.write ?? defaultWrite)({ text: `${DAEMON_USAGE}\n` });
91
+ return;
92
+ default:
93
+ invalidFlag(deps, `unknown "daemon" subcommand: ${sub}`);
94
+ }
95
+ } }, { usage: DAEMON_USAGE });
102
96
  }
103
97
  //# sourceMappingURL=daemon-command.js.map
@@ -2,7 +2,7 @@
2
2
  * @module daemon-url
3
3
  *
4
4
  * Generic daemon-URL resolution, generalized from OD's
5
- * `apps/daemon/src/daemon-url.ts` (see `source-map.md`). The *order* —
5
+ * `apps/daemon/src/daemon-url.ts` (see `archived provenance ledger`). The *order* —
6
6
  * explicit flag, then an env var, then an injected discovery probe, then a
7
7
  * caller-supplied default — is the reusable part. OD's own implementation
8
8
  * hardcodes its sidecar IPC protocol and a `pnpm exec tools-dev status`
@@ -26,7 +26,11 @@ export interface ResolveDaemonUrlOptions {
26
26
  * `null` when nothing was discovered so resolution falls through to
27
27
  * `defaultUrl`.
28
28
  */
29
- discover?: (env: NodeJS.ProcessEnv, timeoutMs: number) => Promise<string | null>;
29
+ discover?: (requiredArgs: {
30
+ env: NodeJS.ProcessEnv;
31
+ }, optionalArgs: {
32
+ timeoutMs: number;
33
+ }) => Promise<string | null>;
30
34
  /** Discovery timeout in ms. Defaults to 800ms so an absent daemon doesn't stall CLI startup. */
31
35
  timeoutMs?: number;
32
36
  /** Returned when the flag, env, and discovery steps all come up empty. */
@@ -36,7 +40,9 @@ export interface ResolveDaemonUrlOptions {
36
40
  * succeeds. Defaults to a no-op. A user's own daemon may legitimately run
37
41
  * on a remote, non-HTTPS host, so this only warns — it never rejects.
38
42
  */
39
- warn?: (message: string) => void;
43
+ warn?: (requiredArgs: {
44
+ message: string;
45
+ }) => void;
40
46
  }
41
47
  /**
42
48
  * Resolve a daemon HTTP base URL for a CLI client command.
@@ -47,7 +53,7 @@ export interface ResolveDaemonUrlOptions {
47
53
  * `http://127.0.0.1:0`-style fallback would be worse than a clear error,
48
54
  * and this package has no locked default port to fall back to on its own.
49
55
  */
50
- export declare function resolveDaemonUrl(options?: ResolveDaemonUrlOptions): Promise<string>;
56
+ export declare function resolveDaemonUrl(_requiredArgs: Record<string, never>, options?: ResolveDaemonUrlOptions): Promise<string>;
51
57
  /**
52
58
  * Return `url` with any embedded userinfo (`user:pass@host`) stripped, safe to include in a
53
59
  * printed error message, log line, or structured-error envelope. Per SEC-RB-009
@@ -56,7 +62,9 @@ export declare function resolveDaemonUrl(options?: ResolveDaemonUrlOptions): Pro
56
62
  * reach stderr/stdout verbatim. Returns `url` unchanged when it has no userinfo, so normal URLs
57
63
  * are never reformatted/normalized as a side effect of display-sanitizing them.
58
64
  */
59
- export declare function sanitizeDaemonUrlForDisplay(url: string): string;
65
+ export declare function sanitizeDaemonUrlForDisplay({ url }: {
66
+ url: string;
67
+ }): string;
60
68
  /**
61
69
  * Non-fatal policy check for a resolved daemon URL: warn (never throw or reject) when it is
62
70
  * neither loopback nor HTTPS, since that combination sends CLI traffic — including whatever
@@ -64,5 +72,7 @@ export declare function sanitizeDaemonUrlForDisplay(url: string): string;
64
72
  * `null` when the URL looks safe or doesn't parse (a parse failure is reported elsewhere, by
65
73
  * whatever code actually tries to use the URL).
66
74
  */
67
- export declare function daemonUrlPolicyWarning(url: string): string | null;
75
+ export declare function daemonUrlPolicyWarning({ url }: {
76
+ url: string;
77
+ }): string | null;
68
78
  //# sourceMappingURL=daemon-url.d.ts.map
@@ -1,15 +1,4 @@
1
- /**
2
- * @module daemon-url
3
- *
4
- * Generic daemon-URL resolution, generalized from OD's
5
- * `apps/daemon/src/daemon-url.ts` (see `source-map.md`). The *order* —
6
- * explicit flag, then an env var, then an injected discovery probe, then a
7
- * caller-supplied default — is the reusable part. OD's own implementation
8
- * hardcodes its sidecar IPC protocol and a `pnpm exec tools-dev status`
9
- * fallback; neither belongs in a product-neutral package, so both are
10
- * replaced with an injected `discover` callback the caller (e.g. a future
11
- * `@jini-ai/sidecar`-backed adapter) supplies.
12
- */
1
+ import { isLoopbackHostname } from '@jini-ai/platform/net';
13
2
  /**
14
3
  * Resolve a daemon HTTP base URL for a CLI client command.
15
4
  *
@@ -19,13 +8,13 @@
19
8
  * `http://127.0.0.1:0`-style fallback would be worse than a clear error,
20
9
  * and this package has no locked default port to fall back to on its own.
21
10
  */
22
- export async function resolveDaemonUrl(options = {}) {
11
+ export async function resolveDaemonUrl(_requiredArgs, options = {}) {
23
12
  const env = options.env ?? process.env;
24
13
  const warn = options.warn ?? (() => { });
25
14
  const report = (url) => {
26
- const warning = daemonUrlPolicyWarning(url);
15
+ const warning = daemonUrlPolicyWarning({ url });
27
16
  if (warning !== null)
28
- warn(warning);
17
+ warn({ message: warning });
29
18
  return url;
30
19
  };
31
20
  const flagUrl = options.flagUrl ?? null;
@@ -37,7 +26,7 @@ export async function resolveDaemonUrl(options = {}) {
37
26
  return report(envUrl);
38
27
  }
39
28
  if (options.discover !== undefined) {
40
- const discovered = await options.discover(env, options.timeoutMs ?? 800);
29
+ const discovered = await options.discover({ env }, { timeoutMs: options.timeoutMs ?? 800 });
41
30
  if (discovered !== null && discovered.length > 0)
42
31
  return report(discovered);
43
32
  }
@@ -58,7 +47,7 @@ const USERINFO_PREFIX_RE = /^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/@]*@/;
58
47
  * reach stderr/stdout verbatim. Returns `url` unchanged when it has no userinfo, so normal URLs
59
48
  * are never reformatted/normalized as a side effect of display-sanitizing them.
60
49
  */
61
- export function sanitizeDaemonUrlForDisplay(url) {
50
+ export function sanitizeDaemonUrlForDisplay({ url }) {
62
51
  let parsed;
63
52
  try {
64
53
  parsed = new URL(url);
@@ -72,10 +61,6 @@ export function sanitizeDaemonUrlForDisplay(url) {
72
61
  parsed.password = '';
73
62
  return parsed.toString();
74
63
  }
75
- const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
76
- function isLoopbackHostname(hostname) {
77
- return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());
78
- }
79
64
  /**
80
65
  * Non-fatal policy check for a resolved daemon URL: warn (never throw or reject) when it is
81
66
  * neither loopback nor HTTPS, since that combination sends CLI traffic — including whatever
@@ -83,7 +68,7 @@ function isLoopbackHostname(hostname) {
83
68
  * `null` when the URL looks safe or doesn't parse (a parse failure is reported elsewhere, by
84
69
  * whatever code actually tries to use the URL).
85
70
  */
86
- export function daemonUrlPolicyWarning(url) {
71
+ export function daemonUrlPolicyWarning({ url }) {
87
72
  let parsed;
88
73
  try {
89
74
  parsed = new URL(url);
@@ -91,9 +76,9 @@ export function daemonUrlPolicyWarning(url) {
91
76
  catch {
92
77
  return null;
93
78
  }
94
- if (parsed.protocol === 'https:' || isLoopbackHostname(parsed.hostname))
79
+ if (parsed.protocol === 'https:' || isLoopbackHostname({ hostname: parsed.hostname }))
95
80
  return null;
96
- return (`daemon URL ${sanitizeDaemonUrlForDisplay(url)} is neither loopback nor HTTPS; ` +
81
+ return (`daemon URL ${sanitizeDaemonUrlForDisplay({ url: url })} is neither loopback nor HTTPS; ` +
97
82
  'traffic to a remote, non-HTTPS daemon is not encrypted.');
98
83
  }
99
84
  //# sourceMappingURL=daemon-url.js.map