@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 +95 -0
- package/README.md +27 -10
- package/dist/command-registry.d.ts +19 -7
- package/dist/command-registry.js +8 -7
- package/dist/daemon-command.d.ts +22 -7
- package/dist/daemon-command.js +34 -40
- package/dist/daemon-url.d.ts +16 -6
- package/dist/daemon-url.js +9 -24
- package/dist/errors.d.ts +16 -6
- package/dist/errors.js +17 -16
- package/dist/flags.d.ts +11 -5
- package/dist/flags.js +5 -5
- package/dist/http.d.ts +25 -9
- package/dist/http.js +27 -24
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/introspection.d.ts +75 -0
- package/dist/introspection.js +63 -0
- package/dist/local-daemon-discovery.d.ts +7 -15
- package/dist/local-daemon-discovery.js +7 -7
- package/dist/main.d.ts +12 -4
- package/dist/main.js +32 -34
- package/dist/prompt.d.ts +17 -7
- package/dist/prompt.js +13 -13
- package/dist/redact.d.ts +5 -20
- package/dist/redact.js +5 -97
- package/dist/run-command.d.ts +38 -10
- package/dist/run-command.js +68 -84
- package/dist/tokens.js +1 -1
- package/dist/usage.d.ts +4 -2
- package/dist/usage.js +8 -8
- package/dist/version-command.d.ts +18 -6
- package/dist/version-command.js +13 -14
- package/package.json +19 -3
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* @module local-daemon-discovery
|
|
3
3
|
*
|
|
4
4
|
* The real `discover` callback `resolveDaemonUrl` (`daemon-url.ts`) has always accepted but this
|
|
5
|
-
* package never implemented — see `
|
|
5
|
+
* package never implemented — see `archived provenance ledger`'s "Deferred" item 1 and its own 2026-07-21
|
|
6
6
|
* investigation, which found `@jini-ai/server`'s `createLocalNodeDaemon` wrote nothing anywhere a
|
|
7
7
|
* separate CLI process could read. That daemon side is now built (`@jini-ai/server`'s
|
|
8
8
|
* `discoveryFile` config, backed by `@jini-ai/sidecar`'s `daemon-registry.ts`); this module is the
|
|
9
9
|
* matching CLI-side reader: `createLocalDaemonDiscovery(...)` builds a probe over that same
|
|
10
10
|
* on-disk record, verified live (not just present — see `readLiveDaemonRegistryRecord`'s own
|
|
11
|
-
* doc), that plugs directly into `resolveDaemonUrl({ discover })`.
|
|
11
|
+
* doc), that plugs directly into `resolveDaemonUrl({}, { discover })`.
|
|
12
12
|
*
|
|
13
13
|
* Deliberately not wired to a hardcoded default `dataDir`: exactly like `resolveDaemonUrl` itself
|
|
14
14
|
* has no baked-in env var name or default URL, this package has no opinion on where a given
|
|
@@ -23,14 +23,14 @@ import { readLiveDaemonRegistryRecord, resolveDaemonRegistryPath } from '@jini-a
|
|
|
23
23
|
* `registryPath`/`dataDir` was supplied.
|
|
24
24
|
*/
|
|
25
25
|
function resolveRegistryPathFromOptions(options) {
|
|
26
|
-
if (
|
|
26
|
+
if ("registryPath" in options)
|
|
27
27
|
return options.registryPath;
|
|
28
|
-
if (
|
|
29
|
-
return resolveDaemonRegistryPath(options.dataDir);
|
|
28
|
+
if ("dataDir" in options)
|
|
29
|
+
return resolveDaemonRegistryPath({ dataDir: options.dataDir });
|
|
30
30
|
throw new Error('createLocalDaemonDiscovery requires either dataDir or registryPath');
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* Build a `resolveDaemonUrl({ discover })`-compatible probe backed by a local, on-disk daemon
|
|
33
|
+
* Build a `resolveDaemonUrl({}, { discover })`-compatible probe backed by a local, on-disk daemon
|
|
34
34
|
* registry record: reads the record at the resolved registry path, confirms the recording
|
|
35
35
|
* process's pid is still alive (a stale record left behind by a daemon that crashed rather than
|
|
36
36
|
* shut down cleanly is never trusted — see `readLiveDaemonRegistryRecord`), and returns its
|
|
@@ -46,7 +46,7 @@ function resolveRegistryPathFromOptions(options) {
|
|
|
46
46
|
export function createLocalDaemonDiscovery(options) {
|
|
47
47
|
const registryPath = resolveRegistryPathFromOptions(options);
|
|
48
48
|
return async () => {
|
|
49
|
-
const record = await readLiveDaemonRegistryRecord(registryPath);
|
|
49
|
+
const record = await readLiveDaemonRegistryRecord({ registryPath });
|
|
50
50
|
return record?.url ?? null;
|
|
51
51
|
};
|
|
52
52
|
}
|
package/dist/main.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
export interface MainDeps {
|
|
3
3
|
/** Defaults to `process.stdout.write`; inject for tests. */
|
|
4
|
-
write?: (
|
|
4
|
+
write?: (requiredArgs: {
|
|
5
|
+
text: string;
|
|
6
|
+
}) => void;
|
|
5
7
|
/** Defaults to `process.stderr.write`; inject for tests. */
|
|
6
|
-
writeErr?: (
|
|
8
|
+
writeErr?: (requiredArgs: {
|
|
9
|
+
text: string;
|
|
10
|
+
}) => void;
|
|
7
11
|
/** Defaults to `process.exit`; inject for tests (must not return). */
|
|
8
|
-
exit?: (
|
|
12
|
+
exit?: (requiredArgs: {
|
|
13
|
+
code: number;
|
|
14
|
+
}) => never;
|
|
9
15
|
/** Defaults to the global `fetch`; inject for tests. */
|
|
10
16
|
fetchImpl?: typeof fetch;
|
|
11
17
|
/** Defaults to `process.env`; inject for tests. */
|
|
@@ -17,5 +23,7 @@ export interface MainDeps {
|
|
|
17
23
|
* caller under normal use — usage errors and daemon failures both exit through this package's
|
|
18
24
|
* structured-error contract (`errors.ts`), via the injected (or default, real) `exit`.
|
|
19
25
|
*/
|
|
20
|
-
export declare function main(argv
|
|
26
|
+
export declare function main({ argv }: {
|
|
27
|
+
argv: readonly string[];
|
|
28
|
+
}, deps?: MainDeps): Promise<void>;
|
|
21
29
|
//# sourceMappingURL=main.d.ts.map
|
package/dist/main.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* other module in this package is a library building block — `run-command.ts`,
|
|
7
7
|
* `daemon-command.ts`, and `version-command.ts` each export a `register*Commands(registry,
|
|
8
8
|
* deps)` function ready for a pack to call, but until this file, nothing actually parsed
|
|
9
|
-
* `process.argv` and called them: `packages/cli/
|
|
9
|
+
* `process.argv` and called them: `packages/cli/archived provenance ledger`'s "Not built" note on
|
|
10
10
|
* `run-command.ts`'s original addition said as much — "no wiring of this file's commands into
|
|
11
11
|
* an actual bootable CLI entrypoint." This closes that gap.
|
|
12
12
|
*
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* **Daemon-URL resolution.** `resolveDaemonUrl` (`daemon-url.ts`) has no baked-in default —
|
|
24
24
|
* this package has never had a locked default daemon port to fall back to (see
|
|
25
|
-
* `
|
|
25
|
+
* `archived provenance ledger`: `@jini-ai/server`'s `createLocalNodeDaemon` binds an ephemeral port, not a
|
|
26
26
|
* fixed one). This binary wires the three real, already-built pieces together instead of
|
|
27
27
|
* inventing a new one: an explicit `--daemon-url <url>` flag (highest precedence), a
|
|
28
28
|
* `JINI_DAEMON_URL` env var, and — when `--data-dir <path>` or `--registry-path <path>` is
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
* dispatch, so `run`/`daemon`/`version` always land as a clean first token no matter where on
|
|
49
49
|
* the command line `--daemon-url`/`--data-dir`/`--registry-path` were typed.
|
|
50
50
|
*
|
|
51
|
-
* **`--version`/`-v`.** This package
|
|
52
|
-
*
|
|
51
|
+
* **`--version`/`-v`.** This package has its own published semver, but this transport command reports
|
|
52
|
+
* the daemon it is controlling so operators can identify the running service. Accordingly,
|
|
53
53
|
* `jini --version`/`jini -v` (recognized only as the very first token — see {@link main}) is a
|
|
54
54
|
* plain alias for `jini version` (`version-command.ts`): it prints the *running daemon's* real
|
|
55
55
|
* version. That is arguably more useful to a user of this transport-only CLI than this
|
|
56
|
-
* package's own
|
|
56
|
+
* package's own transport version would be, and it means `--version` has exactly one
|
|
57
57
|
* implementation to keep correct, not two.
|
|
58
58
|
*
|
|
59
59
|
* **Error boundary.** `parseFlags` (`flags.ts`) throws a plain `Error` for an unrecognized
|
|
@@ -83,25 +83,21 @@ import { createLocalDaemonDiscovery } from './local-daemon-discovery.js';
|
|
|
83
83
|
import { registerRunCommands } from './run-command.js';
|
|
84
84
|
import { renderUsage } from './usage.js';
|
|
85
85
|
import { registerVersionCommand } from './version-command.js';
|
|
86
|
-
function defaultWrite(text) {
|
|
86
|
+
function defaultWrite({ text }) {
|
|
87
87
|
process.stdout.write(text);
|
|
88
88
|
}
|
|
89
|
-
function defaultWriteErr(text) {
|
|
89
|
+
function defaultWriteErr({ text }) {
|
|
90
90
|
process.stderr.write(text);
|
|
91
91
|
}
|
|
92
92
|
/** Name of the environment variable {@link resolveDaemonUrl} checks when `--daemon-url` is absent. */
|
|
93
93
|
const DAEMON_URL_ENV_VAR = 'JINI_DAEMON_URL';
|
|
94
|
-
const ROOT_USAGE = renderUsage({
|
|
95
|
-
usage: ['jini <command> [...args]'],
|
|
96
|
-
description: 'CLI transport for a Jini daemon over HTTP. Commands: run, daemon, version.',
|
|
97
|
-
options: [
|
|
94
|
+
const ROOT_USAGE = renderUsage({ usage: ['jini <command> [...args]'] }, { description: 'CLI transport for a Jini daemon over HTTP. Commands: run, daemon, version.', options: [
|
|
98
95
|
{ flag: '--daemon-url <url>', description: 'Explicit daemon base URL. Takes precedence over JINI_DAEMON_URL and local discovery.' },
|
|
99
96
|
{ flag: '--data-dir <path>', description: "Resolve a locally running daemon's URL from this data directory's on-disk registry (see @jini-ai/server's createLocalNodeDaemon)." },
|
|
100
97
|
{ flag: '--registry-path <path>', description: "Exact registry file path, overriding --data-dir's derived default." },
|
|
101
98
|
{ flag: '--help, -h', description: 'Show this help.' },
|
|
102
99
|
{ flag: '--version, -v', description: 'Print the running daemon\'s version (alias for "jini version"; recognized only as the first argument).' },
|
|
103
|
-
]
|
|
104
|
-
});
|
|
100
|
+
] });
|
|
105
101
|
const GLOBAL_FLAG_NAMES = ['daemon-url', 'data-dir', 'registry-path'];
|
|
106
102
|
/**
|
|
107
103
|
* Single-pass extraction of this binary's own global `--daemon-url`/`--data-dir`/
|
|
@@ -154,7 +150,7 @@ function buildResolveBaseUrl(globals, env, warn) {
|
|
|
154
150
|
: dataDir !== undefined
|
|
155
151
|
? createLocalDaemonDiscovery({ dataDir })
|
|
156
152
|
: undefined;
|
|
157
|
-
return () => resolveDaemonUrl({
|
|
153
|
+
return () => resolveDaemonUrl({}, {
|
|
158
154
|
flagUrl: daemonUrl ?? null,
|
|
159
155
|
env,
|
|
160
156
|
envVarName: DAEMON_URL_ENV_VAR,
|
|
@@ -168,13 +164,13 @@ function buildResolveBaseUrl(globals, env, warn) {
|
|
|
168
164
|
* caller under normal use — usage errors and daemon failures both exit through this package's
|
|
169
165
|
* structured-error contract (`errors.ts`), via the injected (or default, real) `exit`.
|
|
170
166
|
*/
|
|
171
|
-
export async function main(argv, deps = {}) {
|
|
167
|
+
export async function main({ argv }, deps = {}) {
|
|
172
168
|
const write = deps.write ?? defaultWrite;
|
|
173
169
|
const writeErr = deps.writeErr ?? defaultWriteErr;
|
|
174
|
-
const rawExit = deps.exit ?? ((code) => process.exit(code));
|
|
170
|
+
const rawExit = deps.exit ?? (({ code }) => process.exit(code));
|
|
175
171
|
const env = deps.env ?? process.env;
|
|
176
172
|
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
177
|
-
write(`${ROOT_USAGE}\n`);
|
|
173
|
+
write({ text: `${ROOT_USAGE}\n` });
|
|
178
174
|
return;
|
|
179
175
|
}
|
|
180
176
|
// `--version`/`-v` as the very first token is a plain alias for `jini version` — see this
|
|
@@ -184,26 +180,28 @@ export async function main(argv, deps = {}) {
|
|
|
184
180
|
// already exited cleanly" (re-throw untouched) apart from "a genuinely raw, unhandled throw"
|
|
185
181
|
// (reformat into a structured error) — see this file's own module doc, "Error boundary."
|
|
186
182
|
let exitCalled = false;
|
|
187
|
-
const exit = (code) => {
|
|
183
|
+
const exit = ({ code }) => {
|
|
188
184
|
exitCalled = true;
|
|
189
|
-
return rawExit(code);
|
|
185
|
+
return rawExit({ code });
|
|
190
186
|
};
|
|
191
187
|
let result;
|
|
192
188
|
try {
|
|
193
189
|
const { globals, rest } = partitionGlobalArgv(effectiveArgv);
|
|
194
|
-
const resolveBaseUrl = buildResolveBaseUrl(globals, env, writeErr);
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
write,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
190
|
+
const resolveBaseUrl = buildResolveBaseUrl(globals, env, ({ message }) => writeErr({ text: message }));
|
|
191
|
+
const registry = new CommandRegistry({});
|
|
192
|
+
registerRunCommands({ registry, resolveBaseUrl }, {
|
|
193
|
+
write, writeErr, exit,
|
|
194
|
+
...(deps.fetchImpl === undefined ? {} : { fetchImpl: deps.fetchImpl }),
|
|
195
|
+
});
|
|
196
|
+
registerDaemonCommands({ registry, resolveBaseUrl }, {
|
|
197
|
+
write, writeErr, exit,
|
|
198
|
+
...(deps.fetchImpl === undefined ? {} : { fetchImpl: deps.fetchImpl }),
|
|
199
|
+
});
|
|
200
|
+
registerVersionCommand({ registry, resolveBaseUrl }, {
|
|
201
|
+
write, writeErr, exit,
|
|
202
|
+
...(deps.fetchImpl === undefined ? {} : { fetchImpl: deps.fetchImpl }),
|
|
203
|
+
});
|
|
204
|
+
result = await registry.dispatch({ argv: rest });
|
|
207
205
|
}
|
|
208
206
|
catch (error) {
|
|
209
207
|
if (exitCalled)
|
|
@@ -213,7 +211,7 @@ export async function main(argv, deps = {}) {
|
|
|
213
211
|
return;
|
|
214
212
|
}
|
|
215
213
|
if (result.kind === 'empty') {
|
|
216
|
-
write(`${ROOT_USAGE}\n`);
|
|
214
|
+
write({ text: `${ROOT_USAGE}\n` });
|
|
217
215
|
return;
|
|
218
216
|
}
|
|
219
217
|
if (result.kind === 'not-found') {
|
|
@@ -226,6 +224,6 @@ export async function main(argv, deps = {}) {
|
|
|
226
224
|
// `require.main === module` idiom for ESM; see this file's own module doc.
|
|
227
225
|
const isMainModule = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1];
|
|
228
226
|
if (isMainModule) {
|
|
229
|
-
await main(process.argv.slice(2));
|
|
227
|
+
await main({ argv: process.argv.slice(2) });
|
|
230
228
|
}
|
|
231
229
|
//# sourceMappingURL=main.js.map
|
package/dist/prompt.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* `readPromptFromFlags` is the `--prompt <text>` / `--prompt-file <path>` /
|
|
9
9
|
* `--prompt-file -` convention, ported from OD's `apps/daemon/src/cli.ts`
|
|
10
|
-
* `readPromptFromFlags` (see `
|
|
10
|
+
* `readPromptFromFlags` (see `archived provenance ledger`). This exact pattern recurred
|
|
11
11
|
* verbatim across many of OD's product commands (`brand create`, `run
|
|
12
12
|
* start`, `files version-create`, `automation source ingest`, …) with zero
|
|
13
13
|
* product nouns in the reading logic itself, which is what makes it a
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* `readBodyFromFlags` is the sibling `--body`/`--body-file` convention,
|
|
18
18
|
* ported from the `cli-capability-barrels` branch's `cli/core/io.ts`
|
|
19
|
-
* `readMemoryBodyFromFlags` (see `
|
|
19
|
+
* `readMemoryBodyFromFlags` (see `archived provenance ledger`'s "Barrel branch
|
|
20
20
|
* reconciliation" section). Despite the origin name, it has no memory-domain
|
|
21
21
|
* coupling — that module's own docblock confirms it is reused verbatim by
|
|
22
22
|
* both the memory and automation domains — so it is ported here under a
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
*/
|
|
35
35
|
/** Thrown by the default readers when a file or stdin stream exceeds its configured byte cap. */
|
|
36
36
|
export declare class PayloadTooLargeError extends Error {
|
|
37
|
-
constructor(message:
|
|
37
|
+
constructor({ message }: {
|
|
38
|
+
message: string;
|
|
39
|
+
});
|
|
38
40
|
}
|
|
39
41
|
export interface PromptFlags {
|
|
40
42
|
prompt?: string;
|
|
@@ -42,7 +44,9 @@ export interface PromptFlags {
|
|
|
42
44
|
}
|
|
43
45
|
export interface ReadPromptFromFlagsOptions {
|
|
44
46
|
/** Defaults to a bounded, cancellable `node:fs` stream read; inject for tests. */
|
|
45
|
-
readFile?: (
|
|
47
|
+
readFile?: (requiredArgs: {
|
|
48
|
+
path: string;
|
|
49
|
+
}) => Promise<string>;
|
|
46
50
|
/** Defaults to a bounded, cancellable read of `process.stdin` to EOF as utf8; inject for tests. */
|
|
47
51
|
readStdin?: () => Promise<string>;
|
|
48
52
|
/** Maximum bytes the default readFile/readStdin will read before rejecting. Defaults to 10 MiB. No effect on an injected reader. */
|
|
@@ -55,14 +59,18 @@ export interface ReadPromptFromFlagsOptions {
|
|
|
55
59
|
* `--prompt-file -` (stdin). Returns `null` when neither flag is set so the
|
|
56
60
|
* caller can fall back to its own default.
|
|
57
61
|
*/
|
|
58
|
-
export declare function readPromptFromFlags(flags
|
|
62
|
+
export declare function readPromptFromFlags({ flags }: {
|
|
63
|
+
flags: PromptFlags;
|
|
64
|
+
}, options?: ReadPromptFromFlagsOptions): Promise<string | null>;
|
|
59
65
|
export interface BodyFlags {
|
|
60
66
|
body?: string;
|
|
61
67
|
'body-file'?: string;
|
|
62
68
|
}
|
|
63
69
|
export interface ReadBodyFromFlagsOptions {
|
|
64
70
|
/** Defaults to a bounded, cancellable `node:fs` stream read; inject for tests. */
|
|
65
|
-
readFile?: (
|
|
71
|
+
readFile?: (requiredArgs: {
|
|
72
|
+
path: string;
|
|
73
|
+
}) => Promise<string>;
|
|
66
74
|
/** Defaults to a bounded, cancellable drain of `process.stdin`'s async iterator as utf8; inject for tests. */
|
|
67
75
|
readStdin?: () => Promise<string>;
|
|
68
76
|
/** Maximum bytes the default readFile/readStdin will read before rejecting. Defaults to 10 MiB. No effect on an injected reader. */
|
|
@@ -79,5 +87,7 @@ export interface ReadBodyFromFlagsOptions {
|
|
|
79
87
|
* primitive's origin caller (`od memory tree edit`) needs to allow clearing
|
|
80
88
|
* a body to an empty string.
|
|
81
89
|
*/
|
|
82
|
-
export declare function readBodyFromFlags(flags
|
|
90
|
+
export declare function readBodyFromFlags({ flags }: {
|
|
91
|
+
flags: BodyFlags;
|
|
92
|
+
}, options?: ReadBodyFromFlagsOptions): Promise<string | undefined>;
|
|
83
93
|
//# sourceMappingURL=prompt.d.ts.map
|
package/dist/prompt.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* `readPromptFromFlags` is the `--prompt <text>` / `--prompt-file <path>` /
|
|
9
9
|
* `--prompt-file -` convention, ported from OD's `apps/daemon/src/cli.ts`
|
|
10
|
-
* `readPromptFromFlags` (see `
|
|
10
|
+
* `readPromptFromFlags` (see `archived provenance ledger`). This exact pattern recurred
|
|
11
11
|
* verbatim across many of OD's product commands (`brand create`, `run
|
|
12
12
|
* start`, `files version-create`, `automation source ingest`, …) with zero
|
|
13
13
|
* product nouns in the reading logic itself, which is what makes it a
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* `readBodyFromFlags` is the sibling `--body`/`--body-file` convention,
|
|
18
18
|
* ported from the `cli-capability-barrels` branch's `cli/core/io.ts`
|
|
19
|
-
* `readMemoryBodyFromFlags` (see `
|
|
19
|
+
* `readMemoryBodyFromFlags` (see `archived provenance ledger`'s "Barrel branch
|
|
20
20
|
* reconciliation" section). Despite the origin name, it has no memory-domain
|
|
21
21
|
* coupling — that module's own docblock confirms it is reused verbatim by
|
|
22
22
|
* both the memory and automation domains — so it is ported here under a
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
/** Thrown by the default readers when a file or stdin stream exceeds its configured byte cap. */
|
|
36
36
|
export class PayloadTooLargeError extends Error {
|
|
37
|
-
constructor(message) {
|
|
37
|
+
constructor({ message }) {
|
|
38
38
|
super(message);
|
|
39
39
|
this.name = 'PayloadTooLargeError';
|
|
40
40
|
}
|
|
@@ -46,7 +46,7 @@ const DEFAULT_MAX_READ_BYTES = 10 * 1024 * 1024;
|
|
|
46
46
|
* `--prompt-file -` (stdin). Returns `null` when neither flag is set so the
|
|
47
47
|
* caller can fall back to its own default.
|
|
48
48
|
*/
|
|
49
|
-
export async function readPromptFromFlags(flags, options = {}) {
|
|
49
|
+
export async function readPromptFromFlags({ flags }, options = {}) {
|
|
50
50
|
if (typeof flags.prompt === 'string' && flags.prompt.length > 0) {
|
|
51
51
|
return flags.prompt;
|
|
52
52
|
}
|
|
@@ -59,8 +59,8 @@ export async function readPromptFromFlags(flags, options = {}) {
|
|
|
59
59
|
const readStdin = options.readStdin ?? (() => defaultReadStdin(readLimits));
|
|
60
60
|
return await readStdin();
|
|
61
61
|
}
|
|
62
|
-
const readFile = options.readFile ?? ((path) => defaultReadFile(path, readLimits));
|
|
63
|
-
return await readFile(promptFile);
|
|
62
|
+
const readFile = options.readFile ?? (({ path }) => defaultReadFile(path, readLimits));
|
|
63
|
+
return await readFile({ path: promptFile });
|
|
64
64
|
}
|
|
65
65
|
async function defaultReadFile(path, limits) {
|
|
66
66
|
const { createReadStream } = await import('node:fs');
|
|
@@ -76,7 +76,7 @@ async function defaultReadFile(path, limits) {
|
|
|
76
76
|
// suppress any further 'error' from the destroy path itself). Kept as
|
|
77
77
|
// real protection against a future/third-party stream implementation
|
|
78
78
|
// bug, not something this module's own real `createReadStream` usage
|
|
79
|
-
// can trigger — see `
|
|
79
|
+
// can trigger — see `archived provenance ledger`'s dated entry for the full
|
|
80
80
|
// re-verification record (a single terminal event is all this file's
|
|
81
81
|
// own usage can ever produce).
|
|
82
82
|
const finish = (fn) => {
|
|
@@ -98,7 +98,7 @@ async function defaultReadFile(path, limits) {
|
|
|
98
98
|
total += chunk.length;
|
|
99
99
|
if (total > limits.maxBytes) {
|
|
100
100
|
stream.destroy();
|
|
101
|
-
finish(() => reject(new PayloadTooLargeError(`file exceeded the ${limits.maxBytes}-byte limit: ${path}`)));
|
|
101
|
+
finish(() => reject(new PayloadTooLargeError({ message: `file exceeded the ${limits.maxBytes}-byte limit: ${path}` })));
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
chunks.push(chunk);
|
|
@@ -130,7 +130,7 @@ async function defaultReadStdin(limits) {
|
|
|
130
130
|
if (total > limits.maxBytes) {
|
|
131
131
|
cleanup();
|
|
132
132
|
pauseStdin();
|
|
133
|
-
reject(new PayloadTooLargeError(`stdin exceeded the ${limits.maxBytes}-byte limit`));
|
|
133
|
+
reject(new PayloadTooLargeError({ message: `stdin exceeded the ${limits.maxBytes}-byte limit` }));
|
|
134
134
|
return;
|
|
135
135
|
}
|
|
136
136
|
buffer += chunk;
|
|
@@ -157,7 +157,7 @@ async function defaultReadStdin(limits) {
|
|
|
157
157
|
* primitive's origin caller (`od memory tree edit`) needs to allow clearing
|
|
158
158
|
* a body to an empty string.
|
|
159
159
|
*/
|
|
160
|
-
export async function readBodyFromFlags(flags, options = {}) {
|
|
160
|
+
export async function readBodyFromFlags({ flags }, options = {}) {
|
|
161
161
|
if (typeof flags.body === 'string')
|
|
162
162
|
return flags.body;
|
|
163
163
|
const bodyFile = flags['body-file'];
|
|
@@ -168,8 +168,8 @@ export async function readBodyFromFlags(flags, options = {}) {
|
|
|
168
168
|
const readStdin = options.readStdin ?? (() => defaultReadBodyStdin(readLimits));
|
|
169
169
|
return await readStdin();
|
|
170
170
|
}
|
|
171
|
-
const readFile = options.readFile ?? ((path) => defaultReadFile(path, readLimits));
|
|
172
|
-
return await readFile(bodyFile);
|
|
171
|
+
const readFile = options.readFile ?? (({ path }) => defaultReadFile(path, readLimits));
|
|
172
|
+
return await readFile({ path: bodyFile });
|
|
173
173
|
}
|
|
174
174
|
async function defaultReadBodyStdin(limits) {
|
|
175
175
|
limits.signal?.throwIfAborted();
|
|
@@ -185,7 +185,7 @@ async function defaultReadBodyStdin(limits) {
|
|
|
185
185
|
total += Buffer.byteLength(text, 'utf8');
|
|
186
186
|
if (total > limits.maxBytes) {
|
|
187
187
|
stdin.destroy?.();
|
|
188
|
-
throw new PayloadTooLargeError(`stdin exceeded the ${limits.maxBytes}-byte limit`);
|
|
188
|
+
throw new PayloadTooLargeError({ message: `stdin exceeded the ${limits.maxBytes}-byte limit` });
|
|
189
189
|
}
|
|
190
190
|
body += text;
|
|
191
191
|
}
|
package/dist/redact.d.ts
CHANGED
|
@@ -17,25 +17,6 @@
|
|
|
17
17
|
* arbitrary daemon JSON, or complete non-JSON response bodies straight to
|
|
18
18
|
* the terminal.
|
|
19
19
|
*/
|
|
20
|
-
/**
|
|
21
|
-
* Strip ANSI/terminal escape sequences (CSI `ESC [ ... final-byte`, OSC `ESC ] ... BEL-or-ST`,
|
|
22
|
-
* and bare ESC-prefixed bytes) plus other C0/C1 control characters from `text`. Written as an
|
|
23
|
-
* explicit char-code scanner rather than a regex literal with hex escapes, so the pattern is
|
|
24
|
-
* plain ASCII source with no embedded control bytes.
|
|
25
|
-
*/
|
|
26
|
-
export declare function stripControlSequences(text: string): string;
|
|
27
|
-
/** Redact labeled credentials and long opaque-token-looking substrings from `text`. */
|
|
28
|
-
export declare function redactSecretLike(text: string): string;
|
|
29
|
-
export interface SanitizeTextOptions {
|
|
30
|
-
/** Maximum output length; longer input is truncated with a trailing marker. Defaults to 500. */
|
|
31
|
-
maxLength?: number;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Strip terminal control sequences, redact anything that looks like a secret, and cap the
|
|
35
|
-
* length of `text` — the one function every boundary that surfaces daemon/network-derived text
|
|
36
|
-
* to stderr/stdout should route through first.
|
|
37
|
-
*/
|
|
38
|
-
export declare function sanitizeUntrustedText(text: string, options?: SanitizeTextOptions): string;
|
|
39
20
|
/**
|
|
40
21
|
* Recursively sanitize an arbitrary parsed-JSON value (the shape a daemon error envelope's
|
|
41
22
|
* `data`/`details` field can legitimately be): every string leaf is passed through
|
|
@@ -44,5 +25,9 @@ export declare function sanitizeUntrustedText(text: string, options?: SanitizeTe
|
|
|
44
25
|
* the cap is replaced with a placeholder rather than silently dropped, so truncation is visible
|
|
45
26
|
* instead of looking like an empty/missing value.
|
|
46
27
|
*/
|
|
47
|
-
export declare function sanitizeUnknownDeep(value
|
|
28
|
+
export declare function sanitizeUnknownDeep({ value }: {
|
|
29
|
+
value: unknown;
|
|
30
|
+
}, { depth }?: {
|
|
31
|
+
depth?: number;
|
|
32
|
+
}): unknown;
|
|
48
33
|
//# sourceMappingURL=redact.d.ts.map
|
package/dist/redact.js
CHANGED
|
@@ -17,102 +17,10 @@
|
|
|
17
17
|
* arbitrary daemon JSON, or complete non-JSON response bodies straight to
|
|
18
18
|
* the terminal.
|
|
19
19
|
*/
|
|
20
|
-
|
|
21
|
-
const DEFAULT_MAX_EXCERPT_LENGTH = 500;
|
|
20
|
+
import { sanitizeUntrustedText } from '@jini-ai/core/text';
|
|
22
21
|
/** Recursion/branching caps for {@link sanitizeUnknownDeep} — bounds the work done on an arbitrary parsed-JSON tree. */
|
|
23
22
|
const MAX_SANITIZE_DEPTH = 4;
|
|
24
23
|
const MAX_SANITIZE_ENTRIES = 50;
|
|
25
|
-
const ESC = 0x1b;
|
|
26
|
-
const BEL = 0x07;
|
|
27
|
-
const BACKSLASH = 0x5c;
|
|
28
|
-
const OSC_OPEN = 0x5d; // ']'
|
|
29
|
-
const CSI_OPEN = 0x5b; // '['
|
|
30
|
-
function isCsiFinalByte(code) {
|
|
31
|
-
return code >= 0x40 && code <= 0x7e; // '@'..'~'
|
|
32
|
-
}
|
|
33
|
-
function isOtherControlCode(code) {
|
|
34
|
-
if (code === 0x09 || code === 0x0a || code === 0x0d)
|
|
35
|
-
return false; // tab / LF / CR pass through
|
|
36
|
-
if (code <= 0x1f)
|
|
37
|
-
return true; // remaining C0 controls (ESC itself is handled separately)
|
|
38
|
-
if (code === 0x7f)
|
|
39
|
-
return true; // DEL
|
|
40
|
-
if (code >= 0x80 && code <= 0x9f)
|
|
41
|
-
return true; // C1 controls
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Strip ANSI/terminal escape sequences (CSI `ESC [ ... final-byte`, OSC `ESC ] ... BEL-or-ST`,
|
|
46
|
-
* and bare ESC-prefixed bytes) plus other C0/C1 control characters from `text`. Written as an
|
|
47
|
-
* explicit char-code scanner rather than a regex literal with hex escapes, so the pattern is
|
|
48
|
-
* plain ASCII source with no embedded control bytes.
|
|
49
|
-
*/
|
|
50
|
-
export function stripControlSequences(text) {
|
|
51
|
-
let out = '';
|
|
52
|
-
for (let i = 0; i < text.length; i++) {
|
|
53
|
-
const code = text.charCodeAt(i);
|
|
54
|
-
if (code === ESC) {
|
|
55
|
-
const next = text.charCodeAt(i + 1);
|
|
56
|
-
if (next === CSI_OPEN) {
|
|
57
|
-
let j = i + 2;
|
|
58
|
-
while (j < text.length && !isCsiFinalByte(text.charCodeAt(j)))
|
|
59
|
-
j++;
|
|
60
|
-
i = j; // land on the final byte; the outer loop's i++ steps past it
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (next === OSC_OPEN) {
|
|
64
|
-
let j = i + 2;
|
|
65
|
-
while (j < text.length &&
|
|
66
|
-
text.charCodeAt(j) !== BEL &&
|
|
67
|
-
!(text.charCodeAt(j) === ESC && text.charCodeAt(j + 1) === BACKSLASH)) {
|
|
68
|
-
j++;
|
|
69
|
-
}
|
|
70
|
-
if (j < text.length && text.charCodeAt(j) === BEL)
|
|
71
|
-
i = j;
|
|
72
|
-
else
|
|
73
|
-
i = Math.min(j + 1, text.length - 1);
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
continue; // bare ESC (or an unrecognized ESC-prefixed byte): drop just the ESC byte
|
|
77
|
-
}
|
|
78
|
-
if (isOtherControlCode(code))
|
|
79
|
-
continue;
|
|
80
|
-
out += text[i];
|
|
81
|
-
}
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
// A labeled credential (`Authorization: ...`, `api-key=...`) has everything after the label
|
|
85
|
-
// redacted. A bare run of 20+ base64url/hex-alphabet characters is treated as a possible
|
|
86
|
-
// opaque token/secret and redacted outright, even with no label.
|
|
87
|
-
const LABELED_SECRET_RE = /\b(authorization|bearer|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|password|secret|cookie)\b\s*[:=]\s*\S+/gi;
|
|
88
|
-
const OPAQUE_TOKEN_RE = /[A-Za-z0-9_-]{20,}/g;
|
|
89
|
-
/**
|
|
90
|
-
* Whole opaque-token runs that are known not to be secrets, kept verbatim. Matched against the
|
|
91
|
-
* ENTIRE run, so a secret glued onto one of these is still one longer run and still redacted:
|
|
92
|
-
* - a host's tool-failure correlation id (`ERR-XXXX-XXXX-XXXX-XXXX`, upper-case hex — Tovu's
|
|
93
|
-
* `mintToolErrorId`), which exists to be shown to the model and quoted back to an operator;
|
|
94
|
-
* - `delegated-tool-calls`, the daemon route segment every delegated tool failure names in its URL.
|
|
95
|
-
*/
|
|
96
|
-
const NON_SECRET_TOKEN_RE = /^(?:ERR-[0-9A-F]{4}(?:-[0-9A-F]{4}){3}|delegated-tool-calls)$/;
|
|
97
|
-
/** Redact labeled credentials and long opaque-token-looking substrings from `text`. */
|
|
98
|
-
export function redactSecretLike(text) {
|
|
99
|
-
return text
|
|
100
|
-
.replace(LABELED_SECRET_RE, (match) => match.replace(/\S+$/, '[redacted]'))
|
|
101
|
-
.replace(OPAQUE_TOKEN_RE, (match) => (NON_SECRET_TOKEN_RE.test(match) ? match : '[redacted]'));
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Strip terminal control sequences, redact anything that looks like a secret, and cap the
|
|
105
|
-
* length of `text` — the one function every boundary that surfaces daemon/network-derived text
|
|
106
|
-
* to stderr/stdout should route through first.
|
|
107
|
-
*/
|
|
108
|
-
export function sanitizeUntrustedText(text, options = {}) {
|
|
109
|
-
const maxLength = options.maxLength ?? DEFAULT_MAX_EXCERPT_LENGTH;
|
|
110
|
-
const sanitized = redactSecretLike(stripControlSequences(text));
|
|
111
|
-
if (sanitized.length <= maxLength)
|
|
112
|
-
return sanitized;
|
|
113
|
-
const omitted = sanitized.length - maxLength;
|
|
114
|
-
return `${sanitized.slice(0, maxLength)}… [truncated ${omitted} more characters]`;
|
|
115
|
-
}
|
|
116
24
|
/**
|
|
117
25
|
* Recursively sanitize an arbitrary parsed-JSON value (the shape a daemon error envelope's
|
|
118
26
|
* `data`/`details` field can legitimately be): every string leaf is passed through
|
|
@@ -121,18 +29,18 @@ export function sanitizeUntrustedText(text, options = {}) {
|
|
|
121
29
|
* the cap is replaced with a placeholder rather than silently dropped, so truncation is visible
|
|
122
30
|
* instead of looking like an empty/missing value.
|
|
123
31
|
*/
|
|
124
|
-
export function sanitizeUnknownDeep(value, depth = 0) {
|
|
32
|
+
export function sanitizeUnknownDeep({ value }, { depth = 0 } = {}) {
|
|
125
33
|
if (depth > MAX_SANITIZE_DEPTH)
|
|
126
34
|
return '[omitted: nested too deeply]';
|
|
127
35
|
if (typeof value === 'string')
|
|
128
|
-
return sanitizeUntrustedText(value);
|
|
36
|
+
return sanitizeUntrustedText({ text: value });
|
|
129
37
|
if (Array.isArray(value)) {
|
|
130
|
-
return value.slice(0, MAX_SANITIZE_ENTRIES).map((item) => sanitizeUnknownDeep(item, depth + 1));
|
|
38
|
+
return value.slice(0, MAX_SANITIZE_ENTRIES).map((item) => sanitizeUnknownDeep({ value: item }, { depth: depth + 1 }));
|
|
131
39
|
}
|
|
132
40
|
if (value !== null && typeof value === 'object') {
|
|
133
41
|
const out = {};
|
|
134
42
|
for (const [key, val] of Object.entries(value).slice(0, MAX_SANITIZE_ENTRIES)) {
|
|
135
|
-
out[sanitizeUntrustedText(key, { maxLength: 100 })] = sanitizeUnknownDeep(val, depth + 1);
|
|
43
|
+
out[sanitizeUntrustedText({ text: key }, { maxLength: 100 })] = sanitizeUnknownDeep({ value: val }, { depth: depth + 1 });
|
|
136
44
|
}
|
|
137
45
|
return out;
|
|
138
46
|
}
|
package/dist/run-command.d.ts
CHANGED
|
@@ -4,24 +4,42 @@ export interface RunCommandDeps {
|
|
|
4
4
|
/** Resolves the daemon HTTP base URL once per command invocation (e.g. wraps `resolveDaemonUrl`). */
|
|
5
5
|
resolveBaseUrl: () => Promise<string> | string;
|
|
6
6
|
/** Defaults to `process.stdout.write`; inject for tests. Used for successful command output. */
|
|
7
|
-
write?: (
|
|
7
|
+
write?: (requiredArgs: {
|
|
8
|
+
text: string;
|
|
9
|
+
}) => void;
|
|
8
10
|
/** Defaults to `process.stderr.write`; inject for tests. Used for usage/validation errors. */
|
|
9
|
-
writeErr?: (
|
|
11
|
+
writeErr?: (requiredArgs: {
|
|
12
|
+
text: string;
|
|
13
|
+
}) => void;
|
|
10
14
|
/** Defaults to the global `fetch`; inject for tests. */
|
|
11
15
|
fetchImpl?: typeof fetch;
|
|
12
16
|
/** Defaults to `process.exit`; inject for tests (must not return). */
|
|
13
|
-
exit?: (
|
|
17
|
+
exit?: (requiredArgs: {
|
|
18
|
+
code: number;
|
|
19
|
+
}) => never;
|
|
14
20
|
/** Extra/overriding `code -> exitCode` entries layered on the package default table. */
|
|
15
21
|
exitCodes?: ExitCodeTable;
|
|
16
22
|
}
|
|
17
23
|
/** `run start --context-ref <ref> [--agent-id <id>] [--idempotency-key <key>]` */
|
|
18
|
-
export declare function runStartCommand(args
|
|
24
|
+
export declare function runStartCommand({ args, resolveBaseUrl: resolveBaseUrlPort }: {
|
|
25
|
+
args: readonly string[];
|
|
26
|
+
resolveBaseUrl: RunCommandDeps["resolveBaseUrl"];
|
|
27
|
+
}, optionalArgs?: Omit<RunCommandDeps, "resolveBaseUrl">): Promise<void>;
|
|
19
28
|
/** `run list [--context-ref <ref>]` */
|
|
20
|
-
export declare function runListCommand(args
|
|
29
|
+
export declare function runListCommand({ args, resolveBaseUrl: resolveBaseUrlPort }: {
|
|
30
|
+
args: readonly string[];
|
|
31
|
+
resolveBaseUrl: RunCommandDeps["resolveBaseUrl"];
|
|
32
|
+
}, optionalArgs?: Omit<RunCommandDeps, "resolveBaseUrl">): Promise<void>;
|
|
21
33
|
/** `run get <runId>` */
|
|
22
|
-
export declare function runGetCommand(args
|
|
34
|
+
export declare function runGetCommand({ args, resolveBaseUrl: resolveBaseUrlPort }: {
|
|
35
|
+
args: readonly string[];
|
|
36
|
+
resolveBaseUrl: RunCommandDeps["resolveBaseUrl"];
|
|
37
|
+
}, optionalArgs?: Omit<RunCommandDeps, "resolveBaseUrl">): Promise<void>;
|
|
23
38
|
/** `run cancel <runId> [--reason <text>]` */
|
|
24
|
-
export declare function runCancelCommand(args
|
|
39
|
+
export declare function runCancelCommand({ args, resolveBaseUrl: resolveBaseUrlPort }: {
|
|
40
|
+
args: readonly string[];
|
|
41
|
+
resolveBaseUrl: RunCommandDeps["resolveBaseUrl"];
|
|
42
|
+
}, optionalArgs?: Omit<RunCommandDeps, "resolveBaseUrl">): Promise<void>;
|
|
25
43
|
/**
|
|
26
44
|
* Streams `GET /api/runs/:runId/events` and writes one NDJSON line per event to `deps.write`,
|
|
27
45
|
* stopping once a terminal `'end'` event arrives (or the stream closes on its own). Each frame's
|
|
@@ -31,9 +49,19 @@ export declare function runCancelCommand(args: readonly string[], deps: RunComma
|
|
|
31
49
|
* the way a diagnostic error message is, since it is the run's actual event payload, not incidental
|
|
32
50
|
* error detail.
|
|
33
51
|
*/
|
|
34
|
-
export declare function watchRunEvents(baseUrl
|
|
52
|
+
export declare function watchRunEvents({ baseUrl, runId, args }: {
|
|
53
|
+
baseUrl: string;
|
|
54
|
+
runId: string;
|
|
55
|
+
args: readonly string[];
|
|
56
|
+
}, deps?: Omit<RunCommandDeps, "resolveBaseUrl">): Promise<void>;
|
|
35
57
|
/** `run watch <runId> [--after-cursor <cursor>]` */
|
|
36
|
-
export declare function runWatchCommand(args
|
|
58
|
+
export declare function runWatchCommand({ args, resolveBaseUrl: resolveBaseUrlPort }: {
|
|
59
|
+
args: readonly string[];
|
|
60
|
+
resolveBaseUrl: RunCommandDeps["resolveBaseUrl"];
|
|
61
|
+
}, optionalArgs?: Omit<RunCommandDeps, "resolveBaseUrl">): Promise<void>;
|
|
37
62
|
/** Registers `run` (dispatching `start`/`list`/`cancel`/`watch` on the first remaining token) against `registry`. */
|
|
38
|
-
export declare function registerRunCommands(registry
|
|
63
|
+
export declare function registerRunCommands({ registry, resolveBaseUrl: resolveBaseUrlPort }: {
|
|
64
|
+
registry: CommandRegistry;
|
|
65
|
+
resolveBaseUrl: RunCommandDeps["resolveBaseUrl"];
|
|
66
|
+
}, optionalArgs?: Omit<RunCommandDeps, "resolveBaseUrl">): void;
|
|
39
67
|
//# sourceMappingURL=run-command.d.ts.map
|