@jini-ai/cli 0.3.1 → 0.4.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/dist/errors.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Structured error / exit-code handling, generalized from OD's
5
5
  * `apps/daemon/src/cli.ts` `exitWithStructuredError`/`structuredHttpFailure`
6
- * (see `source-map.md`). The mechanism — map a stable error `code` to a
6
+ * (see `archived provenance ledger`). The mechanism — map a stable error `code` to a
7
7
  * process exit code, write a `{ error: { code, message, data } }` envelope
8
8
  * to stderr — is generic CLI transport plumbing; OD's own exit-code table
9
9
  * (`plugin-not-found`, `snapshot-stale`, …) is product-specific and was not
@@ -23,15 +23,19 @@ export interface StructuredErrorEnvelope {
23
23
  export interface ExitWithStructuredErrorInput {
24
24
  code: string;
25
25
  message: string;
26
- data?: Record<string, unknown>;
27
26
  }
28
27
  export interface StructuredErrorOptions {
28
+ data?: Record<string, unknown>;
29
29
  /** Extra/overriding `code -> exitCode` entries layered on {@link DEFAULT_CLI_EXIT_CODES}. */
30
30
  exitCodes?: ExitCodeTable;
31
31
  /** Defaults to `process.stderr.write`; inject for tests. */
32
- write?: (text: string) => void;
32
+ write?: (requiredArgs: {
33
+ text: string;
34
+ }) => void;
33
35
  /** Defaults to `process.exit`; inject for tests (must not return). */
34
- exit?: (code: number) => never;
36
+ exit?: (requiredArgs: {
37
+ code: number;
38
+ }) => never;
35
39
  }
36
40
  /**
37
41
  * Write a `{ error }` envelope to stderr and exit with the code mapped from
@@ -60,7 +64,9 @@ export interface HttpFailureLike {
60
64
  * control-sequence-stripped, and secret-redacted before it can reach a
61
65
  * printed envelope (CR-004/SEC-RB-009).
62
66
  */
63
- export declare function structuredErrorData(error: DaemonErrorBody | undefined): Record<string, unknown> | undefined;
67
+ export declare function structuredErrorData({ error }: {
68
+ error: DaemonErrorBody | undefined;
69
+ }): Record<string, unknown> | undefined;
64
70
  /**
65
71
  * Map a daemon HTTP error response into the structured exit-code envelope.
66
72
  * Daemon error bodies come in two shapes in practice: `{ error: { code,
@@ -77,6 +83,10 @@ export declare function structuredErrorData(error: DaemonErrorBody | undefined):
77
83
  * structured `message` (when present) is still sanitized — it originates
78
84
  * from the same untrusted response.
79
85
  */
80
- export declare function structuredHttpFailure(resp: HttpFailureLike, fallbackCode?: string, options?: StructuredErrorOptions): Promise<never>;
86
+ export declare function structuredHttpFailure({ resp }: {
87
+ resp: HttpFailureLike;
88
+ }, { fallbackCode, ...options }?: StructuredErrorOptions & {
89
+ fallbackCode?: string;
90
+ }): Promise<never>;
81
91
  export {};
82
92
  //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js CHANGED
@@ -3,14 +3,15 @@
3
3
  *
4
4
  * Structured error / exit-code handling, generalized from OD's
5
5
  * `apps/daemon/src/cli.ts` `exitWithStructuredError`/`structuredHttpFailure`
6
- * (see `source-map.md`). The mechanism — map a stable error `code` to a
6
+ * (see `archived provenance ledger`). The mechanism — map a stable error `code` to a
7
7
  * process exit code, write a `{ error: { code, message, data } }` envelope
8
8
  * to stderr — is generic CLI transport plumbing; OD's own exit-code table
9
9
  * (`plugin-not-found`, `snapshot-stale`, …) is product-specific and was not
10
10
  * ported. Callers extend {@link DEFAULT_CLI_EXIT_CODES} with their own pack's
11
11
  * codes rather than this package hosting one global mutable map.
12
12
  */
13
- import { sanitizeUnknownDeep, sanitizeUntrustedText, stripControlSequences } from './redact.js';
13
+ import { sanitizeUnknownDeep } from './redact.js';
14
+ import { sanitizeUntrustedText, stripControlSequences } from '@jini-ai/core/text';
14
15
  /** A minimal, product-neutral starting table. Packs extend this with their own codes. */
15
16
  export const DEFAULT_CLI_EXIT_CODES = {
16
17
  'daemon-not-running': 64,
@@ -25,12 +26,12 @@ export function exitWithStructuredError(input, options = {}) {
25
26
  const exitCodes = { ...DEFAULT_CLI_EXIT_CODES, ...options.exitCodes };
26
27
  const exitCode = exitCodes[input.code] ?? 1;
27
28
  const envelope = {
28
- error: { code: input.code, message: input.message, data: input.data ?? {} },
29
+ error: { code: input.code, message: input.message, data: options.data ?? {} },
29
30
  };
30
- const write = options.write ?? ((text) => { process.stderr.write(text); });
31
- const exit = options.exit ?? ((code) => process.exit(code));
32
- write(`${JSON.stringify(envelope)}\n`);
33
- return exit(exitCode);
31
+ const write = options.write ?? (({ text }) => { process.stderr.write(text); });
32
+ const exit = options.exit ?? (({ code }) => process.exit(code));
33
+ write({ text: `${JSON.stringify(envelope)}\n` });
34
+ return exit({ code: exitCode });
34
35
  }
35
36
  /**
36
37
  * Extract `{ data }` worth surfacing to the structured envelope from a
@@ -42,15 +43,15 @@ export function exitWithStructuredError(input, options = {}) {
42
43
  * control-sequence-stripped, and secret-redacted before it can reach a
43
44
  * printed envelope (CR-004/SEC-RB-009).
44
45
  */
45
- export function structuredErrorData(error) {
46
+ export function structuredErrorData({ error }) {
46
47
  if (error === undefined)
47
48
  return undefined;
48
49
  const data = {};
49
50
  if (error.data !== undefined && typeof error.data === 'object' && error.data !== null) {
50
- Object.assign(data, sanitizeUnknownDeep(error.data));
51
+ Object.assign(data, sanitizeUnknownDeep({ value: error.data }));
51
52
  }
52
53
  if (error.details !== undefined)
53
- data.details = sanitizeUnknownDeep(error.details);
54
+ data.details = sanitizeUnknownDeep({ value: error.details });
54
55
  if (typeof error.retryable === 'boolean')
55
56
  data.retryable = error.retryable;
56
57
  return Object.keys(data).length > 0 ? data : undefined;
@@ -81,7 +82,7 @@ function extractErrorObject(parsed) {
81
82
  * structured `message` (when present) is still sanitized — it originates
82
83
  * from the same untrusted response.
83
84
  */
84
- export async function structuredHttpFailure(resp, fallbackCode = 'daemon-not-running', options = {}) {
85
+ export async function structuredHttpFailure({ resp }, { fallbackCode = 'daemon-not-running', ...options } = {}) {
85
86
  let raw = '';
86
87
  let parsed = {};
87
88
  try {
@@ -92,12 +93,12 @@ export async function structuredHttpFailure(resp, fallbackCode = 'daemon-not-run
92
93
  parsed = {};
93
94
  }
94
95
  const errorObj = extractErrorObject(parsed);
95
- const data = structuredErrorData(errorObj);
96
- const code = typeof errorObj?.code === 'string' ? stripControlSequences(errorObj.code) : fallbackCode;
97
- const structuredMessage = typeof errorObj?.message === 'string' ? sanitizeUntrustedText(errorObj.message) : undefined;
96
+ const data = structuredErrorData({ error: errorObj });
97
+ const code = typeof errorObj?.code === 'string' ? stripControlSequences({ text: errorObj.code }) : fallbackCode;
98
+ const structuredMessage = typeof errorObj?.message === 'string' ? sanitizeUntrustedText({ text: errorObj.message }) : undefined;
98
99
  const message = structuredMessage ?? `HTTP ${resp.status}`;
99
- const rawExcerpt = structuredMessage === undefined && raw.length > 0 ? sanitizeUntrustedText(raw) : undefined;
100
+ const rawExcerpt = structuredMessage === undefined && raw.length > 0 ? sanitizeUntrustedText({ text: raw }) : undefined;
100
101
  const mergedData = rawExcerpt !== undefined ? { ...(data ?? {}), rawExcerpt } : data;
101
- return exitWithStructuredError({ code, message, ...(mergedData !== undefined ? { data: mergedData } : {}) }, options);
102
+ return exitWithStructuredError({ code, message }, { ...(mergedData !== undefined ? { data: mergedData } : {}), ...options });
102
103
  }
103
104
  //# sourceMappingURL=errors.js.map
package/dist/flags.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * @module flags
3
3
  *
4
4
  * Generic `argv` → flags parsing, ported from OD's `apps/daemon/src/cli.ts`
5
- * `parseFlags`/`positionalArgs`/`collectCliPositionals` (see `source-map.md`).
5
+ * `parseFlags`/`positionalArgs`/`collectCliPositionals` (see `archived provenance ledger`).
6
6
  * Zero product nouns: callers declare which `--flag` names take a string
7
7
  * value vs. are booleans, and get back a flat map plus the leftover
8
8
  * positional tokens.
@@ -28,7 +28,9 @@ export interface ParseFlagsOptions {
28
28
  * `--flag` is accepted and a heuristic decides string-vs-boolean: if the
29
29
  * next token doesn't itself look like a flag, it's consumed as the value.
30
30
  */
31
- export declare function parseFlags(argv: readonly string[], opts?: ParseFlagsOptions): ParsedFlags;
31
+ export declare function parseFlags({ argv }: {
32
+ argv: readonly string[];
33
+ }, opts?: ParseFlagsOptions): ParsedFlags;
32
34
  export interface PositionalArgsOptions {
33
35
  /** String-valued flag names whose value token must be skipped, not collected as a positional. */
34
36
  string?: ReadonlySet<string>;
@@ -45,17 +47,21 @@ export interface PositionalArgsOptions {
45
47
  * follows a known string flag (so `od run start --project p1 foo` yields
46
48
  * `['foo']`, not `['p1', 'foo']`).
47
49
  */
48
- export declare function positionalArgs(argv: readonly string[], opts?: PositionalArgsOptions): string[];
50
+ export declare function positionalArgs({ argv }: {
51
+ argv: readonly string[];
52
+ }, opts?: PositionalArgsOptions): string[];
49
53
  /** The native type {@link coerceCliValue} can produce from a raw string flag value. */
50
54
  export type CoercedCliValue = boolean | number | string;
51
55
  /**
52
56
  * Coerce a raw `--flag <value>` string into its likely native type:
53
57
  * `'true'`/`'false'` become booleans, a numeric-looking string becomes a
54
58
  * `number`, and anything else passes through unchanged. Ported from OD's
55
- * `cli.ts` `coerceCliValue` (see `source-map.md`) — used wherever a CLI
59
+ * `cli.ts` `coerceCliValue` (see `archived provenance ledger`) — used wherever a CLI
56
60
  * accepts an untyped string flag value that application code should receive
57
61
  * as its natural type (e.g. a config `set` command, or a plugin's dynamic
58
62
  * `--input` flags forwarded as a JSON-typed payload).
59
63
  */
60
- export declare function coerceCliValue(raw: string): CoercedCliValue;
64
+ export declare function coerceCliValue({ raw }: {
65
+ raw: string;
66
+ }): CoercedCliValue;
61
67
  //# sourceMappingURL=flags.d.ts.map
package/dist/flags.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * @module flags
3
3
  *
4
4
  * Generic `argv` → flags parsing, ported from OD's `apps/daemon/src/cli.ts`
5
- * `parseFlags`/`positionalArgs`/`collectCliPositionals` (see `source-map.md`).
5
+ * `parseFlags`/`positionalArgs`/`collectCliPositionals` (see `archived provenance ledger`).
6
6
  * Zero product nouns: callers declare which `--flag` names take a string
7
7
  * value vs. are booleans, and get back a flat map plus the leftover
8
8
  * positional tokens.
@@ -18,7 +18,7 @@
18
18
  * `--flag` is accepted and a heuristic decides string-vs-boolean: if the
19
19
  * next token doesn't itself look like a flag, it's consumed as the value.
20
20
  */
21
- export function parseFlags(argv, opts = {}) {
21
+ export function parseFlags({ argv }, opts = {}) {
22
22
  const stringFlags = opts.string ?? new Set();
23
23
  const booleanFlags = opts.boolean ?? new Set();
24
24
  const knownFlags = new Set([...stringFlags, ...booleanFlags]);
@@ -67,7 +67,7 @@ export function parseFlags(argv, opts = {}) {
67
67
  * follows a known string flag (so `od run start --project p1 foo` yields
68
68
  * `['foo']`, not `['p1', 'foo']`).
69
69
  */
70
- export function positionalArgs(argv, opts = {}) {
70
+ export function positionalArgs({ argv }, opts = {}) {
71
71
  const stringFlags = opts.string ?? new Set();
72
72
  const out = [];
73
73
  for (let i = 0; i < argv.length; i++) {
@@ -94,12 +94,12 @@ const NUMERIC_LITERAL_RE = /^-?\d+(\.\d+)?$/;
94
94
  * Coerce a raw `--flag <value>` string into its likely native type:
95
95
  * `'true'`/`'false'` become booleans, a numeric-looking string becomes a
96
96
  * `number`, and anything else passes through unchanged. Ported from OD's
97
- * `cli.ts` `coerceCliValue` (see `source-map.md`) — used wherever a CLI
97
+ * `cli.ts` `coerceCliValue` (see `archived provenance ledger`) — used wherever a CLI
98
98
  * accepts an untyped string flag value that application code should receive
99
99
  * as its natural type (e.g. a config `set` command, or a plugin's dynamic
100
100
  * `--input` flags forwarded as a JSON-typed payload).
101
101
  */
102
- export function coerceCliValue(raw) {
102
+ export function coerceCliValue({ raw }) {
103
103
  if (raw === 'true')
104
104
  return true;
105
105
  if (raw === 'false')
package/dist/http.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * HTTP-client-mode transport plumbing, ported from OD's
5
5
  * `apps/daemon/src/cli.ts` `surfaceFetchError`/`postJsonToDaemon` (see
6
- * `source-map.md`). This is the "HTTP-client mode default" half of
6
+ * `archived provenance ledger`). This is the "HTTP-client mode default" half of
7
7
  * extraction-plan §3's `@jini-ai/cli` spec: fetch a daemon route, translate a
8
8
  * network failure or a daemon error envelope into the structured exit-code
9
9
  * contract from `errors.ts`.
@@ -11,7 +11,9 @@
11
11
  import { type ExitCodeTable } from './errors.js';
12
12
  export interface SurfaceFetchErrorOptions {
13
13
  /** Defaults to `process.stderr.write`; inject for tests. */
14
- write?: (text: string) => void;
14
+ write?: (requiredArgs: {
15
+ text: string;
16
+ }) => void;
15
17
  }
16
18
  /**
17
19
  * Format a `fetch()` rejection (network-unreachable, sandbox-denied) into a
@@ -22,16 +24,23 @@ export interface SurfaceFetchErrorOptions {
22
24
  * embeds terminal control sequences or a leaked credential, must not be
23
25
  * printed verbatim.
24
26
  */
25
- export declare function surfaceFetchError(err: unknown, daemonUrl: string, options?: SurfaceFetchErrorOptions): void;
27
+ export declare function surfaceFetchError({ err, daemonUrl }: {
28
+ err: unknown;
29
+ daemonUrl: string;
30
+ }, options?: SurfaceFetchErrorOptions): void;
26
31
  export interface PostJsonToDaemonOptions {
27
32
  headers?: Record<string, string>;
28
33
  exitCodes?: ExitCodeTable;
29
34
  /** Defaults to the global `fetch`; inject for tests. */
30
35
  fetchImpl?: typeof fetch;
31
- /** Defaults to `process.stderr.write`; inject for tests. Used only for the unrecognized-error-code fallback path. */
32
- write?: (text: string) => void;
33
- /** Defaults to `process.exit`; inject for tests (must not return). Used only for the unrecognized-error-code fallback path. */
34
- exit?: (code: number) => never;
36
+ /** Defaults to `process.stderr.write`; inject for every structured failure path. */
37
+ write?: (requiredArgs: {
38
+ text: string;
39
+ }) => void;
40
+ /** Defaults to `process.exit`; inject for every structured failure path (must not return). */
41
+ exit?: (requiredArgs: {
42
+ code: number;
43
+ }) => never;
35
44
  /** Caller-supplied cancellation signal; combined with the internal timeout signal, so either one aborts the request. */
36
45
  signal?: AbortSignal;
37
46
  /** Request deadline in ms. Defaults to {@link DEFAULT_TIMEOUT_MS}. */
@@ -52,12 +61,19 @@ export type GetJsonFromDaemonOptions = PostJsonToDaemonOptions;
52
61
  * daemon-supplied error detail before it reaches stderr instead of dumping
53
62
  * it verbatim.
54
63
  */
55
- export declare function postJsonToDaemon(base: string, route: string, body: unknown, options?: PostJsonToDaemonOptions): Promise<unknown>;
64
+ export declare function postJsonToDaemon({ base, route, body }: {
65
+ base: string;
66
+ route: string;
67
+ body: unknown;
68
+ }, options?: PostJsonToDaemonOptions): Promise<unknown>;
56
69
  /**
57
70
  * `GET` `<base><route>` and return the parsed JSON response. Same transport contract as
58
71
  * {@link postJsonToDaemon} (timeout, response-size cap, structured-error mapping, redaction) with
59
72
  * no request body — for read-only daemon routes such as `@jini-ai/http-kit`'s `GET /api/runs` and
60
73
  * `GET /api/runs/:runId`.
61
74
  */
62
- export declare function getJsonFromDaemon(base: string, route: string, options?: GetJsonFromDaemonOptions): Promise<unknown>;
75
+ export declare function getJsonFromDaemon({ base, route }: {
76
+ base: string;
77
+ route: string;
78
+ }, options?: GetJsonFromDaemonOptions): Promise<unknown>;
63
79
  //# sourceMappingURL=http.d.ts.map
package/dist/http.js CHANGED
@@ -3,14 +3,14 @@
3
3
  *
4
4
  * HTTP-client-mode transport plumbing, ported from OD's
5
5
  * `apps/daemon/src/cli.ts` `surfaceFetchError`/`postJsonToDaemon` (see
6
- * `source-map.md`). This is the "HTTP-client mode default" half of
6
+ * `archived provenance ledger`). This is the "HTTP-client mode default" half of
7
7
  * extraction-plan §3's `@jini-ai/cli` spec: fetch a daemon route, translate a
8
8
  * network failure or a daemon error envelope into the structured exit-code
9
9
  * contract from `errors.ts`.
10
10
  */
11
11
  import { DEFAULT_CLI_EXIT_CODES, exitWithStructuredError, structuredErrorData } from './errors.js';
12
12
  import { sanitizeDaemonUrlForDisplay } from './daemon-url.js';
13
- import { sanitizeUntrustedText } from './redact.js';
13
+ import { sanitizeUntrustedText } from '@jini-ai/core/text';
14
14
  /**
15
15
  * Format a `fetch()` rejection (network-unreachable, sandbox-denied) into a
16
16
  * human-readable stderr line, with a hint when the underlying cause looks
@@ -20,8 +20,8 @@ import { sanitizeUntrustedText } from './redact.js';
20
20
  * embeds terminal control sequences or a leaked credential, must not be
21
21
  * printed verbatim.
22
22
  */
23
- export function surfaceFetchError(err, daemonUrl, options = {}) {
24
- const write = options.write ?? ((text) => { process.stderr.write(text); });
23
+ export function surfaceFetchError({ err, daemonUrl }, options = {}) {
24
+ const write = options.write ?? (({ text }) => { process.stderr.write(text); });
25
25
  const cause = err !== null && typeof err === 'object' ? err.cause : null;
26
26
  const code = cause !== null && typeof cause === 'object' && typeof cause.code === 'string'
27
27
  ? cause.code
@@ -34,11 +34,12 @@ export function surfaceFetchError(err, daemonUrl, options = {}) {
34
34
  detail = `${code}${causeMessage.length > 0 ? ` — ${causeMessage}` : ''}`;
35
35
  else if (causeMessage.length > 0)
36
36
  detail = causeMessage;
37
- write(`failed to reach daemon at ${sanitizeDaemonUrlForDisplay(daemonUrl)}: ${sanitizeUntrustedText(detail)}\n`);
37
+ write({ text: `failed to reach daemon at ${sanitizeDaemonUrlForDisplay({ url: daemonUrl })}: ${sanitizeUntrustedText({ text: detail })}\n` });
38
38
  if (code === 'EPERM' || code === 'ENETUNREACH') {
39
- write('hint: outbound connect was denied, likely by a sandbox policy. ' +
40
- 'If this command ran inside an agent sandbox, check its network policy — ' +
41
- 'the daemon itself is unaffected and can be reached from a regular shell.\n');
39
+ write({ text: 'hint: outbound connect was denied, likely by a sandbox policy. ' +
40
+ 'If this command ran inside an agent sandbox, check its network policy — ' +
41
+ 'the daemon itself is unaffected and can be reached from a regular shell.\n',
42
+ });
42
43
  }
43
44
  }
44
45
  /** Default request deadline. Generous enough for a slow agent run kickoff, short enough that a stalled daemon doesn't hang a CLI invocation forever. */
@@ -123,8 +124,8 @@ async function readJsonWithLimit(resp, maxBytes) {
123
124
  */
124
125
  async function requestJsonFromDaemon(base, route, init, options = {}) {
125
126
  const fetchImpl = options.fetchImpl ?? fetch;
126
- const write = options.write ?? ((text) => { process.stderr.write(text); });
127
- const exit = options.exit ?? ((exitCode) => process.exit(exitCode));
127
+ const write = options.write ?? (({ text }) => { process.stderr.write(text); });
128
+ const exit = options.exit ?? (({ code }) => process.exit(code));
128
129
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
129
130
  const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
130
131
  const structuredOptions = {
@@ -154,36 +155,38 @@ async function requestJsonFromDaemon(base, route, init, options = {}) {
154
155
  });
155
156
  }
156
157
  catch (err) {
157
- surfaceFetchError(err, base, { write });
158
- return exitWithStructuredError({ code: 'daemon-not-running', message: `Cannot reach daemon at ${sanitizeDaemonUrlForDisplay(base)}` }, structuredOptions);
158
+ const diagnostics = [];
159
+ surfaceFetchError({ err, daemonUrl: base }, { write: ({ text }) => { diagnostics.push(text.trimEnd()); } });
160
+ return exitWithStructuredError({ code: 'daemon-not-running', message: `Cannot reach daemon at ${sanitizeDaemonUrlForDisplay({ url: base })}` }, { data: { diagnostics }, ...structuredOptions });
159
161
  }
160
162
  try {
161
163
  data = await readJsonWithLimit(resp, maxResponseBytes);
162
164
  }
163
165
  catch (err) {
164
166
  if (err instanceof ResponseTooLargeError) {
165
- write(`response from ${sanitizeDaemonUrlForDisplay(base)}${route} exceeded the ${err.limitBytes}-byte limit; aborting.\n`);
166
- return exit(1);
167
+ timeoutController.abort(err);
168
+ return exitWithStructuredError({ code: 'response-too-large', message: `response from ${sanitizeDaemonUrlForDisplay({ url: base })}${route} exceeded the ${err.limitBytes}-byte limit; aborting.` }, structuredOptions);
167
169
  }
168
- throw err;
170
+ const detail = err instanceof Error ? err.message : String(err);
171
+ return exitWithStructuredError({ code: 'request-failed', message: sanitizeUntrustedText({ text: detail }) }, structuredOptions);
169
172
  }
170
173
  }
171
174
  finally {
172
175
  clearTimeout(timeoutHandle);
173
176
  }
174
177
  if (!resp.ok) {
175
- const errorObj = data.error;
178
+ const errorObj = data?.error;
176
179
  const exitCodes = { ...DEFAULT_CLI_EXIT_CODES, ...options.exitCodes };
177
180
  const code = typeof errorObj?.code === 'string' && errorObj.code in exitCodes ? errorObj.code : null;
178
181
  if (code !== null) {
179
182
  const rawMessage = typeof errorObj?.message === 'string' ? errorObj.message : `HTTP ${resp.status}`;
180
- const message = sanitizeUntrustedText(rawMessage);
181
- const errorData = structuredErrorData(errorObj);
182
- return exitWithStructuredError({ code, message, ...(errorData !== undefined ? { data: errorData } : {}) }, structuredOptions);
183
+ const message = sanitizeUntrustedText({ text: rawMessage });
184
+ const errorData = structuredErrorData({ error: errorObj });
185
+ return exitWithStructuredError({ code, message }, { ...(errorData !== undefined ? { data: errorData } : {}), ...structuredOptions });
183
186
  }
184
187
  const rawExcerpt = describeUnrecognizedDaemonPayload(data);
185
- write(`${init.method} ${route} failed: ${resp.status}${rawExcerpt}\n`);
186
- return exit(1);
188
+ const fallbackCode = typeof errorObj?.code === 'string' ? sanitizeUntrustedText({ text: errorObj.code }) : 'http-error';
189
+ return exitWithStructuredError({ code: fallbackCode, message: `${init.method} ${route} failed: ${resp.status}${rawExcerpt}` }, structuredOptions);
187
190
  }
188
191
  return data;
189
192
  }
@@ -198,7 +201,7 @@ async function requestJsonFromDaemon(base, route, init, options = {}) {
198
201
  * daemon-supplied error detail before it reaches stderr instead of dumping
199
202
  * it verbatim.
200
203
  */
201
- export async function postJsonToDaemon(base, route, body, options = {}) {
204
+ export async function postJsonToDaemon({ base, route, body }, options = {}) {
202
205
  return requestJsonFromDaemon(base, route, { method: 'POST', body }, options);
203
206
  }
204
207
  /**
@@ -207,7 +210,7 @@ export async function postJsonToDaemon(base, route, body, options = {}) {
207
210
  * no request body — for read-only daemon routes such as `@jini-ai/http-kit`'s `GET /api/runs` and
208
211
  * `GET /api/runs/:runId`.
209
212
  */
210
- export async function getJsonFromDaemon(base, route, options = {}) {
213
+ export async function getJsonFromDaemon({ base, route }, options = {}) {
211
214
  return requestJsonFromDaemon(base, route, { method: 'GET' }, options);
212
215
  }
213
216
  /** A bounded, redacted excerpt of an unrecognized daemon error payload — never the full raw body. */
@@ -221,6 +224,6 @@ function describeUnrecognizedDaemonPayload(data) {
221
224
  }
222
225
  if (raw.length === 0)
223
226
  return '';
224
- return ` (daemon response, redacted/truncated): ${sanitizeUntrustedText(raw)}`;
227
+ return ` (daemon response, redacted/truncated): ${sanitizeUntrustedText({ text: raw })}`;
225
228
  }
226
229
  //# sourceMappingURL=http.js.map
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * CLI transport-shell: HTTP-client-mode generic transport/infra ported from
5
5
  * OD's `apps/daemon/src/cli.ts` (extraction-plan §3 / §8 task 9). See
6
- * `source-map.md` for the full classification of what was and wasn't
6
+ * `archived provenance ledger` for the full classification of what was and wasn't
7
7
  * ported. This is a first generic slice, not the full `@jini-ai/cli` package.
8
8
  * This barrel itself registers nothing against `CommandRegistry` — see the
9
9
  * NOTE below for `main.ts`, this package's own bootable entrypoint, which does.
@@ -21,4 +21,5 @@ export * from './daemon-command.js';
21
21
  export * from './version-command.js';
22
22
  export * from './local-daemon-discovery.js';
23
23
  export * from './tokens.js';
24
+ export * from './introspection.js';
24
25
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * CLI transport-shell: HTTP-client-mode generic transport/infra ported from
5
5
  * OD's `apps/daemon/src/cli.ts` (extraction-plan §3 / §8 task 9). See
6
- * `source-map.md` for the full classification of what was and wasn't
6
+ * `archived provenance ledger` for the full classification of what was and wasn't
7
7
  * ported. This is a first generic slice, not the full `@jini-ai/cli` package.
8
8
  * This barrel itself registers nothing against `CommandRegistry` — see the
9
9
  * NOTE below for `main.ts`, this package's own bootable entrypoint, which does.
@@ -27,4 +27,5 @@ export * from './tokens.js';
27
27
  // imported for real (guarded so that merely importing its named `main` export does not trigger
28
28
  // that side effect — see `main.ts`'s own module doc) — bundling it into this barrel would mean
29
29
  // `import '@jini-ai/cli'` could start behaving like a CLI invocation.
30
+ export * from './introspection.js';
30
31
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,75 @@
1
+ /** Structural Commander port: accepts a real Command tree without a runtime dependency. */
2
+ export interface IntrospectionCommand {
3
+ name(): string;
4
+ description(): string;
5
+ readonly commands: readonly IntrospectionCommand[];
6
+ readonly registeredArguments: readonly {
7
+ name(): string;
8
+ required: boolean;
9
+ description?: string;
10
+ }[];
11
+ readonly options: readonly {
12
+ flags: string;
13
+ attributeName(): string;
14
+ description?: string;
15
+ mandatory: boolean;
16
+ defaultValue?: unknown;
17
+ }[];
18
+ }
19
+ export interface IntrospectedArgument {
20
+ name: string;
21
+ required: boolean;
22
+ description: string;
23
+ }
24
+ export interface IntrospectedOption {
25
+ flags: string;
26
+ attributeName: string;
27
+ description: string;
28
+ required: boolean;
29
+ defaultValue?: unknown;
30
+ takesValue: boolean;
31
+ }
32
+ export interface IntrospectedCommand {
33
+ name: string;
34
+ description: string;
35
+ arguments: IntrospectedArgument[];
36
+ options: IntrospectedOption[];
37
+ }
38
+ export interface CliManifest {
39
+ name: string;
40
+ description: string;
41
+ commands: IntrospectedCommand[];
42
+ }
43
+ export interface McpToolDefinition {
44
+ name: string;
45
+ description: string;
46
+ inputSchema: {
47
+ type: "object";
48
+ properties: Record<string, {
49
+ type: "string";
50
+ description: string;
51
+ }>;
52
+ required: string[];
53
+ };
54
+ }
55
+ /**
56
+ * Read invocable leaves from a live command tree, using full space-separated paths.
57
+ * @param required Program port and the host's explicitly excluded command names.
58
+ * @returns A plain manifest preserving command/argument/option order and defaults.
59
+ * @complexity O(c + f), where c is the number of commands and f the total fields.
60
+ */
61
+ export declare function introspectProgram({ program, excludedCommands }: {
62
+ program: IntrospectionCommand;
63
+ excludedCommands: readonly string[];
64
+ }): CliManifest;
65
+ /**
66
+ * Project a CLI manifest into string-valued MCP input schemas; boolean switches are omitted.
67
+ * @param required Manifest and caller-owned prefix (including any desired separator).
68
+ * @returns One tool per leaf command, with spaces in invocation paths replaced by underscores.
69
+ * @complexity O(c + f), for commands and argument/option fields.
70
+ */
71
+ export declare function toMcpTools({ manifest, toolNamePrefix }: {
72
+ manifest: CliManifest;
73
+ toolNamePrefix: string;
74
+ }): McpToolDefinition[];
75
+ //# sourceMappingURL=introspection.d.ts.map
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Read invocable leaves from a live command tree, using full space-separated paths.
3
+ * @param required Program port and the host's explicitly excluded command names.
4
+ * @returns A plain manifest preserving command/argument/option order and defaults.
5
+ * @complexity O(c + f), where c is the number of commands and f the total fields.
6
+ */
7
+ export function introspectProgram({ program, excludedCommands }) {
8
+ const excluded = new Set(excludedCommands);
9
+ const commands = [];
10
+ function visit(command, path) {
11
+ // Root meta commands describe the CLI itself; nested namesakes remain invocable capabilities.
12
+ if (path.length === 0 && excluded.has(command.name()))
13
+ return;
14
+ const fullPath = [...path, command.name()];
15
+ // Group commands only print usage in this command model; expose their invocable leaves.
16
+ // Full paths still fit the flat manifest consumed by callers written before nesting existed.
17
+ if (command.commands.length > 0) {
18
+ for (const child of command.commands)
19
+ visit(child, fullPath);
20
+ return;
21
+ }
22
+ commands.push({
23
+ name: fullPath.join(" "), description: command.description() ?? "",
24
+ arguments: command.registeredArguments.map((arg) => ({ name: arg.name(), required: arg.required, description: arg.description ?? "" })),
25
+ options: command.options.map((opt) => ({
26
+ flags: opt.flags, attributeName: opt.attributeName(), description: opt.description ?? "",
27
+ required: opt.mandatory, defaultValue: opt.defaultValue,
28
+ takesValue: opt.flags.includes("<") || opt.flags.includes("["),
29
+ })),
30
+ });
31
+ }
32
+ for (const command of program.commands)
33
+ visit(command, []);
34
+ return { name: program.name(), description: program.description() ?? "", commands };
35
+ }
36
+ /**
37
+ * Project a CLI manifest into string-valued MCP input schemas; boolean switches are omitted.
38
+ * @param required Manifest and caller-owned prefix (including any desired separator).
39
+ * @returns One tool per leaf command, with spaces in invocation paths replaced by underscores.
40
+ * @complexity O(c + f), for commands and argument/option fields.
41
+ */
42
+ export function toMcpTools({ manifest, toolNamePrefix }) {
43
+ // argv supplies strings; numeric/enum validation belongs to command handlers, not this projection.
44
+ // Invocation paths remain space-separated in the manifest, but tool names must be single tokens.
45
+ return manifest.commands.map((command) => {
46
+ const properties = {};
47
+ const required = [];
48
+ for (const arg of command.arguments) {
49
+ Object.defineProperty(properties, arg.name, { value: { type: "string", description: arg.description }, enumerable: true, configurable: true, writable: true });
50
+ if (arg.required)
51
+ required.push(arg.name);
52
+ }
53
+ for (const option of command.options) {
54
+ if (!option.takesValue)
55
+ continue;
56
+ Object.defineProperty(properties, option.attributeName, { value: { type: "string", description: option.description }, enumerable: true, configurable: true, writable: true });
57
+ if (option.required)
58
+ required.push(option.attributeName);
59
+ }
60
+ return { name: `${toolNamePrefix}${command.name.replace(/\s+/g, "_")}`, description: command.description, inputSchema: { type: "object", properties, required } };
61
+ });
62
+ }
63
+ //# sourceMappingURL=introspection.js.map
@@ -1,20 +1,12 @@
1
1
  import type { ResolveDaemonUrlOptions } from './daemon-url.js';
2
- export interface LocalDaemonDiscoveryOptions {
3
- /**
4
- * The same `dataDir` the target `createLocalNodeDaemon({ dataDir, ... })` call was given. The
5
- * registry path is derived from it via `@jini-ai/sidecar`'s `resolveDaemonRegistryPath` — the
6
- * identical derivation `@jini-ai/server` uses for its own `discoveryFile` default. Ignored when
7
- * `registryPath` is also given.
8
- */
9
- dataDir?: string;
10
- /**
11
- * The exact registry file path, for when the host overrode `createLocalNodeDaemon`'s
12
- * `discoveryFile` away from its `dataDir`-derived default. Takes precedence over `dataDir`.
13
- */
14
- registryPath?: string;
15
- }
2
+ /** Explicit registry path or the daemon data directory; neither is defaulted. */
3
+ export type LocalDaemonDiscoveryOptions = {
4
+ registryPath: string;
5
+ } | {
6
+ dataDir: string;
7
+ };
16
8
  /**
17
- * Build a `resolveDaemonUrl({ discover })`-compatible probe backed by a local, on-disk daemon
9
+ * Build a `resolveDaemonUrl({}, { discover })`-compatible probe backed by a local, on-disk daemon
18
10
  * registry record: reads the record at the resolved registry path, confirms the recording
19
11
  * process's pid is still alive (a stale record left behind by a daemon that crashed rather than
20
12
  * shut down cleanly is never trusted — see `readLiveDaemonRegistryRecord`), and returns its