@ours.network/cli 0.1.0 → 0.2.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/README.md CHANGED
@@ -10,13 +10,45 @@ separate package above `@ours.network/sdk`: lifecycle commands call
10
10
  ```bash
11
11
  npm install --global @ours.network/cli
12
12
  ours version
13
+ ours help
13
14
  ours config show --json
14
15
  ours daemon status --json
15
16
  ```
16
17
 
17
- Node 20 or newer is required. `--json` emits one JSON value on stdout; errors
18
- are JSON on stderr. Status exits `0` while running and `3` while stopped. Usage
19
- errors exit `2`; other failures exit `1`.
18
+ Node 20 or newer is required. `--json` emits one JSON value on stdout, except
19
+ `daemon watch`, which streams one JSON event per line (JSONL); errors are JSON
20
+ on stderr. Status exits `0` while running and `3` while stopped. Usage errors
21
+ exit `2`; other failures exit `1`.
22
+
23
+ For a new host, configure safe daemon settings, start the shared daemon, create
24
+ the Human identity, and then add agent roles:
25
+
26
+ ```bash
27
+ ours config setup --port 3070 --state-dir "$HOME/.ours/state"
28
+ ours daemon start
29
+ ours daemon status --json
30
+ ours identity create-root --name Alice@laptop
31
+ ours identity create --name BuildBot --bio "Build coordinator"
32
+ ```
33
+
34
+ ## Help and command discovery
35
+
36
+ The CLI help is the complete operator reference. A bare command group shows its
37
+ subcommands, and both help forms reach the same command-specific page without
38
+ connecting to a daemon or performing the requested action:
39
+
40
+ ```bash
41
+ ours identity
42
+ ours identity create --help
43
+ ours help identity create
44
+ ours help api send-file
45
+ ```
46
+
47
+ Specific help lists every accepted flag, required fields, defaults, enum and
48
+ XOR rules, side effects, safety confirmation, and a credential-free example.
49
+ Parser-compatible flags that an action does not consume are labeled
50
+ `compatibility-only/ignored`; they are not setup knobs. Unknown commands and
51
+ malformed options point to the narrowest relevant help page.
20
52
 
21
53
  ## Daemon lifecycle
22
54
 
@@ -32,6 +64,9 @@ ours daemon restart
32
64
  CLI-owned PID record, the PID reported by the selected daemon, and its expected
33
65
  state directory/port. A daemon started by ours-mcp, a container, systemd, or any
34
66
  other launcher remains fully attachable, but these commands refuse to signal it.
67
+ `start`, `serve`, and `restart` reject `--endpoint` because they must launch a
68
+ locally selected daemon; this rejection happens before `restart` probes or stops
69
+ anything.
35
70
 
36
71
  Selection is coherent and credential-safe. Use a single config file or select
37
72
  both the endpoint and its state directory:
@@ -66,9 +101,11 @@ secret mechanism or the existing SDK configuration, not CLI arguments.
66
101
 
67
102
  ## Operator commands
68
103
 
69
- All commands accept the selection flags `--config`, `--endpoint`, `--port`, and
70
- `--state-dir`; identity-scoped commands also accept `--identity NAME`, which
71
- binds that identity without force for the current process.
104
+ Grouped operation commands accept the selection flags `--config`, `--endpoint`,
105
+ `--port`, and `--state-dir`; identity-scoped commands also accept `--identity
106
+ NAME`, which binds that identity without force for the current process. Run the
107
+ specific command help for lifecycle/config applicability and compatibility-only
108
+ flags.
72
109
 
73
110
  ```text
74
111
  identity create | create-root | create-temporary | close-temporary
@@ -86,11 +123,17 @@ Input fields become kebab-case flags. For example:
86
123
 
87
124
  ```bash
88
125
  ours identity create --name BuildBot --bio "Build coordinator"
126
+ ours profile set-bio --identity BuildBot --bio= # clear the bio
127
+ ours profile set-persona --identity BuildBot --persona= # clear the persona
89
128
  ours message send --identity BuildBot --contact Peer --text "done" --json
90
129
  ours file get --identity BuildBot --wire-ids ID1,ID2 --json
91
130
  ours conversation policy --identity Human --keep-history true
92
131
  ```
93
132
 
133
+ An explicit file retrieval accepts 1–32 unique 64-hex wire IDs. `daemon watch`
134
+ first binds its `--identity` without force, changing the CLI lease; it can fail
135
+ with `BOUND_ELSEWHERE` when another live session holds that identity.
136
+
94
137
  Malformed JSON, unknown input fields, invalid booleans/integers, and incomplete
95
138
  file inputs fail closed before the request. Destructive identity/contact/invite
96
139
  operations require `--yes`.
package/dist/args.d.ts CHANGED
@@ -7,6 +7,6 @@ export interface ParsedFlags {
7
7
  values: Record<string, string>;
8
8
  booleans: Set<string>;
9
9
  }
10
- export declare function parseFlags(argv: string[], valueFlags: ReadonlySet<string>, booleanFlags: ReadonlySet<string>): ParsedFlags;
10
+ export declare function parseFlags(argv: string[], valueFlags: ReadonlySet<string>, booleanFlags: ReadonlySet<string>, emptyValueFlags?: ReadonlySet<string>): ParsedFlags;
11
11
  export declare function parseInteger(value: string, flag: string, min?: number, max?: number): number;
12
12
  export declare function parseBoolean(value: string, flag: string): boolean;
package/dist/args.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  parseBoolean,
4
4
  parseFlags,
5
5
  parseInteger
6
- } from "./chunk-AXXFFER2.js";
6
+ } from "./chunk-6NFATG6P.js";
7
7
  export {
8
8
  CliUsageError,
9
9
  parseBoolean,
@@ -6,7 +6,7 @@ var CliUsageError = class extends Error {
6
6
  this.name = "CliUsageError";
7
7
  }
8
8
  };
9
- function parseFlags(argv, valueFlags, booleanFlags) {
9
+ function parseFlags(argv, valueFlags, booleanFlags, emptyValueFlags = /* @__PURE__ */ new Set()) {
10
10
  const positionals = [];
11
11
  const values = {};
12
12
  const booleans = /* @__PURE__ */ new Set();
@@ -35,7 +35,7 @@ function parseFlags(argv, valueFlags, booleanFlags) {
35
35
  }
36
36
  if (!valueFlags.has(name)) throw new CliUsageError(`unknown option: ${name}`);
37
37
  const value = equal >= 0 ? token.slice(equal + 1) : argv[++i];
38
- if (value === void 0 || value === "") throw new CliUsageError(`${name} requires a value`);
38
+ if (value === void 0 || value === "" && !emptyValueFlags.has(name)) throw new CliUsageError(`${name} requires a value`);
39
39
  if (Object.prototype.hasOwnProperty.call(values, name)) throw new CliUsageError(`${name} may be given only once`);
40
40
  values[name] = value;
41
41
  }
@@ -0,0 +1,399 @@
1
+ import {
2
+ fieldFlag,
3
+ isOperationName,
4
+ operationNames,
5
+ operationSpec
6
+ } from "./chunk-QRNZFPNJ.js";
7
+ import {
8
+ CliUsageError
9
+ } from "./chunk-6NFATG6P.js";
10
+ import {
11
+ COMMAND_GROUPS,
12
+ DESTRUCTIVE_OPERATIONS,
13
+ IDENTITY_PREBIND_EXCEPTIONS,
14
+ commandSpec,
15
+ isCommandGroup
16
+ } from "./chunk-YSHMCQ2B.js";
17
+
18
+ // src/help.ts
19
+ var pad = (value, width = 32) => value.length >= width ? `${value} ` : value.padEnd(width);
20
+ var option = (syntax, description) => ` ${pad(syntax)}${description}`;
21
+ var heading = (name, body) => ["", `${name}:`, ...body];
22
+ function operationSummary(spec) {
23
+ return spec.kind === "operation" ? operationSpec(spec.operation).summary : spec.summary;
24
+ }
25
+ function shellQuote(value) {
26
+ if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) return value;
27
+ return `'${value.replaceAll("'", "'\\''")}'`;
28
+ }
29
+ function displayDefault(value) {
30
+ if (value === "") return "empty string";
31
+ return JSON.stringify(value);
32
+ }
33
+ function fieldType(field, context) {
34
+ if (field.values) return `Accepted values: ${field.values.join(", ")}.`;
35
+ if (field.kind === "boolean") return context === "json" ? "Use a JSON boolean (true or false)." : "Use true or false (1 and 0 are also accepted).";
36
+ if (field.kind === "integer") return field.positive ? "Use an integer of 1 or greater." : context === "json" ? "Use a safe integer." : "Use a non-negative integer.";
37
+ if (field.kind === "integer[]") return "Command flags use comma-separated integers; API JSON uses an integer array.";
38
+ if (field.kind === "string[]") return "Command flags use comma-separated values; API JSON uses a string array.";
39
+ return void 0;
40
+ }
41
+ function fieldDescription(field, name, defaults, context) {
42
+ const details = [field.help];
43
+ if (Object.prototype.hasOwnProperty.call(defaults, name)) details.push(`Default: ${displayDefault(defaults[name])}.`);
44
+ else if (field.required) details.push("Required.");
45
+ else details.push("Optional.");
46
+ const type = fieldType(field, context);
47
+ if (type) details.push(type);
48
+ return details.join(" ");
49
+ }
50
+ function operationFieldOptions(spec, defaults) {
51
+ return Object.entries(spec.fields).map(([name, field]) => option(`${fieldFlag(name)} ${field.valueName}`, fieldDescription(field, name, defaults, "flags")));
52
+ }
53
+ function apiFieldOptions(spec) {
54
+ const entries = Object.entries(spec.fields);
55
+ if (entries.length === 0) return [option("(none)", "Omit --input, or provide an empty JSON object.")];
56
+ return entries.map(([name, field]) => option(name, fieldDescription(field, name, {}, "json")));
57
+ }
58
+ function operationUsage(group, action, command) {
59
+ const spec = operationSpec(command.operation);
60
+ const fields = Object.entries(spec.fields).map(([name, field]) => {
61
+ const flag = `${fieldFlag(name)} ${field.valueName}`;
62
+ return field.required && !Object.prototype.hasOwnProperty.call(command.defaults ?? {}, name) ? flag : `[${flag}]`;
63
+ });
64
+ return [`ours ${group} ${action}`, ...fields, "[selection/output options]"].join(" ");
65
+ }
66
+ function cliExample(group, action, command) {
67
+ const spec = operationSpec(command.operation);
68
+ const args = [`ours ${group} ${action}`];
69
+ for (const [name, value] of Object.entries(spec.exampleInput ?? {})) {
70
+ if (Object.prototype.hasOwnProperty.call(command.defaults ?? {}, name) && command.defaults?.[name] === value) continue;
71
+ const rendered = Array.isArray(value) ? value.join(",") : String(value);
72
+ args.push(fieldFlag(name), shellQuote(rendered));
73
+ }
74
+ if (["profile", "invite", "contact", "message", "file", "conversation"].includes(group) && command.operation !== "list-local-contact-book") {
75
+ args.push("--identity", "BuildBot");
76
+ }
77
+ if (group === "identity" && ["show", "release"].includes(action)) args.push("--identity", "BuildBot");
78
+ if (DESTRUCTIVE_OPERATIONS.has(command.operation)) args.push("--yes");
79
+ args.push("--json");
80
+ return args.join(" ");
81
+ }
82
+ function commonOperationOptions(operation, fieldFlags) {
83
+ const out = [
84
+ option("--endpoint URL", "Connect to this running daemon URL; pair it with the matching --state-dir."),
85
+ option("--port N", "Select daemon port 1\u201365535."),
86
+ option("--state-dir PATH", "Select the daemon state directory used for endpoint identity verification."),
87
+ option("--config PATH", "Read daemon selection and credentials from this config file.")
88
+ ];
89
+ if (!fieldFlags.has("--identity")) {
90
+ out.push(option("--identity NAME", IDENTITY_PREBIND_EXCEPTIONS.has(operation) ? "Compatibility-only/ignored for this operation; use the operation\u2019s --name option instead." : "Bind this identity without force before invoking the operation."));
91
+ }
92
+ out.push(
93
+ option("--json", operation === "watch-notifications" ? "Compatibility-only: watch always streams JSON Lines (one event per line); errors remain on stderr as JSON." : "Write one JSON value to stdout; errors remain on stderr as JSON."),
94
+ option("--yes", DESTRUCTIVE_OPERATIONS.has(operation) ? "Required confirmation for this destructive operation." : "Compatibility-only/ignored for this non-destructive operation."),
95
+ option("--help", "Show this help and exit without connecting to a daemon.")
96
+ );
97
+ return out;
98
+ }
99
+ function nativeDaemonOptions(action) {
100
+ const lifecycle = /* @__PURE__ */ new Set(["serve", "start", "stop", "restart", "status"]);
101
+ const service = /* @__PURE__ */ new Set(["install-service", "uninstall-service"]);
102
+ const selected = lifecycle.has(action);
103
+ const install = action === "install-service";
104
+ const rows = [
105
+ ["--endpoint URL", selected ? action === "serve" || action === "start" || action === "restart" ? "Rejected: this action must start a local selected daemon." : "Select a running endpoint; pair it with --state-dir." : "Compatibility-only/ignored for service management."],
106
+ ["--port N", selected ? "Select daemon port 1\u201365535." : "Compatibility-only/ignored for service management."],
107
+ ["--state-dir PATH", selected ? "Select the daemon state directory." : "Compatibility-only/ignored for service management."],
108
+ ["--config PATH", selected ? "Use this daemon config for lifecycle selection." : action === "install-service" ? "Embed this config file\u2019s absolute path in the installed unit." : "Compatibility-only/ignored by uninstall-service."],
109
+ ["--identity NAME", "Compatibility-only/ignored by daemon lifecycle and service commands."],
110
+ ["--json", action === "serve" ? "Compatibility-only/ignored because serve runs until interrupted." : "Write one JSON result to stdout."],
111
+ ["--yes", service.has(action) ? "Confirm the service-manager change; not needed with --dry-run." : "Compatibility-only/ignored for this action."],
112
+ ["--managed", action === "serve" ? "Write the CLI-owned PID record after the foreground daemon starts." : "Compatibility-only/ignored for this action."],
113
+ ["--dry-run", service.has(action) ? "Return the planned service-manager changes without writing or running systemctl." : "Compatibility-only/ignored for this action."],
114
+ ["--force", install ? "Allow replacement of an existing user unit not marked as CLI-managed." : "Compatibility-only/ignored for this action."],
115
+ ["--help", "Show this help and exit without starting, stopping, probing, or installing anything."]
116
+ ];
117
+ return rows.map(([syntax, description]) => option(syntax, description));
118
+ }
119
+ function nativeConfigOptions(action) {
120
+ const show = action === "show";
121
+ const rows = [
122
+ ["--config PATH", show ? "Read and report this config file." : "Write this config file (default: $OURS_CONFIG or ~/.ours/config.json)."],
123
+ ["--endpoint URL", show ? "Resolve and report this endpoint selection." : "Compatibility-only/ignored by config setup."],
124
+ ["--port N", show ? "Resolve and report port 1\u201365535." : "Set daemon port 1\u201365535."],
125
+ ["--state-dir PATH", show ? "Resolve and report this state directory." : "Set the absolute daemon state directory."],
126
+ ["--identity NAME", "Compatibility-only/ignored by config commands."],
127
+ ["--broker-url URL", show ? "Compatibility-only/ignored by config show." : "Set the broker WebSocket URL."],
128
+ ["--gc-interval-ms N", show ? "Compatibility-only/ignored by config show." : "Set a positive garbage-collection interval in milliseconds."],
129
+ ["--auto-start true|false", show ? "Compatibility-only/ignored by config show." : "Set automatic daemon startup (1/0 are also accepted)."],
130
+ ["--api-visibility MODE", show ? "Compatibility-only/ignored by config show." : "Set owner, shared, or open API visibility."],
131
+ ["--dry-run", show ? "Compatibility-only/ignored by config show." : "Return the merged redacted result without writing the file."],
132
+ ["--json", "Write one JSON value to stdout."],
133
+ ["--help", "Show this help and exit without reading or writing configuration."]
134
+ ];
135
+ return rows.map(([syntax, description]) => option(syntax, description));
136
+ }
137
+ function renderNativeHelp(version, group, action, spec) {
138
+ const options = spec.kind === "daemon" ? nativeDaemonOptions(action) : nativeConfigOptions(action);
139
+ const usage = spec.kind === "daemon" ? `ours daemon ${action} [options]` : action === "setup" ? "ours config setup SETTING... [--dry-run] [--json]" : "ours config show [selection options] [--json]";
140
+ return [
141
+ `ours ${version} \u2014 ${group} ${action}`,
142
+ "",
143
+ spec.summary,
144
+ "",
145
+ `Usage: ${usage}`,
146
+ ...heading("Options", options),
147
+ ...heading("Behavior", [` ${spec.effects}`]),
148
+ ...heading("Example", [` ${spec.example}`]),
149
+ "",
150
+ `More help: ours help ${group}`,
151
+ ""
152
+ ].join("\n");
153
+ }
154
+ function renderOperationHelp(version, group, action, command) {
155
+ const spec = operationSpec(command.operation);
156
+ const defaults = command.defaults ?? {};
157
+ const fields = operationFieldOptions(spec, defaults);
158
+ const fieldFlags = new Set(Object.keys(spec.fields).map(fieldFlag));
159
+ const options = [...fields, ...commonOperationOptions(command.operation, fieldFlags)];
160
+ const behavior = [` ${spec.effects}`];
161
+ if (group === "daemon" && command.operation === "watch-notifications") {
162
+ behavior.push(" Before streaming, --identity attempts a non-force bind that changes this CLI lease; it fails with BOUND_ELSEWHERE when another live session holds the identity.");
163
+ }
164
+ return [
165
+ `ours ${version} \u2014 ${group} ${action}`,
166
+ "",
167
+ spec.summary,
168
+ "",
169
+ `Usage: ${operationUsage(group, action, command)}`,
170
+ "",
171
+ "Positional arguments: none after the command name.",
172
+ ...heading("Options", options),
173
+ ...heading("Behavior", behavior),
174
+ ...heading("Example", [` ${cliExample(group, action, command)}`]),
175
+ "",
176
+ `Expert equivalent: ours help api ${command.operation}`,
177
+ `More help: ours help ${group}`,
178
+ ""
179
+ ].join("\n");
180
+ }
181
+ function renderGroupHelp(version, group) {
182
+ const spec = COMMAND_GROUPS[group];
183
+ const commands = Object.entries(spec.commands).map(([name, command]) => option(name, operationSummary(command)));
184
+ const shared = group === "config" ? [
185
+ "Use `ours config <command> --help` or `ours help config <command>` for exact settings and compatibility-only flags."
186
+ ] : group === "daemon" ? [
187
+ "Use `ours daemon <command> --help` or `ours help daemon <command>` for exact lifecycle, service, selection, and compatibility-only flags."
188
+ ] : [
189
+ "Operation commands accept --endpoint, --port, --state-dir, --config, --identity, --json, --yes, and --help.",
190
+ "--yes is meaningful only where the specific help marks an operation destructive; otherwise it is compatibility-only/ignored."
191
+ ];
192
+ return [
193
+ `ours ${version} \u2014 ${group} commands`,
194
+ "",
195
+ spec.summary,
196
+ "",
197
+ `Usage: ours ${group} <command> [options]`,
198
+ ` ours help ${group} <command>`,
199
+ ...heading("Commands", commands),
200
+ ...heading("Discovery", shared.map((line) => ` ${line}`)),
201
+ ...heading("Example", [` ours help ${group} ${Object.keys(spec.commands)[0]}`]),
202
+ ""
203
+ ].join("\n");
204
+ }
205
+ function apiExample(operation, spec) {
206
+ const args = [`ours api ${operation}`];
207
+ if (Object.keys(spec.fields).length > 0) args.push("--input", shellQuote(JSON.stringify(spec.exampleInput ?? {})));
208
+ const noBindNeeded = /* @__PURE__ */ new Set([
209
+ "create-identity",
210
+ "create-temporary-identity",
211
+ "create-root-identity",
212
+ "define-local-identity-file",
213
+ "choose-identity",
214
+ "list-identities",
215
+ "remove-identity",
216
+ "version",
217
+ "state-dir",
218
+ "identities",
219
+ "unread",
220
+ "watch-notifications",
221
+ "list-local-contact-book"
222
+ ]);
223
+ if (!noBindNeeded.has(operation)) args.push("--identity", "BuildBot");
224
+ if (DESTRUCTIVE_OPERATIONS.has(operation)) args.push("--yes");
225
+ args.push("--json");
226
+ return args.join(" ");
227
+ }
228
+ function renderApiOperationHelp(version, operation) {
229
+ const spec = operationSpec(operation);
230
+ const options = [
231
+ option("--input JSON", "Operation input as one JSON object."),
232
+ option("--input-file PATH", "Read the JSON object from PATH, or use - for stdin. Mutually exclusive with --input."),
233
+ ...commonOperationOptions(operation, /* @__PURE__ */ new Set())
234
+ ];
235
+ return [
236
+ `ours ${version} \u2014 api ${operation}`,
237
+ "",
238
+ spec.summary,
239
+ "",
240
+ `Usage: ours api ${operation} [--input JSON | --input-file PATH] [selection/output options]`,
241
+ "",
242
+ "Positional arguments: exactly one allowlisted operation name after `api`.",
243
+ ...heading("JSON fields", apiFieldOptions(spec)),
244
+ ...heading("Options", options),
245
+ ...heading("Behavior", [` ${spec.effects}`, " Unknown JSON fields and invalid types fail before the operation is invoked."]),
246
+ ...heading("Example", [` ${apiExample(operation, spec)}`]),
247
+ "",
248
+ "List all expert operations: ours api list",
249
+ ""
250
+ ].join("\n");
251
+ }
252
+ function renderApiListHelp(version) {
253
+ return [
254
+ `ours ${version} \u2014 api list`,
255
+ "",
256
+ "List every expert operation accepted by `ours api` in deterministic order.",
257
+ "",
258
+ "Usage: ours api list [--json] [--help]",
259
+ ...heading("Options", [
260
+ option("--json", "Write the operation names as one JSON array instead of text."),
261
+ option("--help", "Show this help without connecting to a daemon.")
262
+ ]),
263
+ ...heading("Behavior", [" Read-only and local: this command does not connect to a daemon."]),
264
+ ...heading("Examples", [" ours api list", " ours api list --json"]),
265
+ ""
266
+ ].join("\n");
267
+ }
268
+ function renderApiHelp(version) {
269
+ const operations = operationNames().map((name) => option(name, operationSpec(name).summary));
270
+ return [
271
+ `ours ${version} \u2014 expert API operations`,
272
+ "",
273
+ "Invoke the CLI\u2019s explicit OursClient allowlist with a typed JSON object. Grouped commands are easier for routine operator work.",
274
+ "",
275
+ "Usage: ours api list [--json]",
276
+ " ours api <operation> [--input JSON | --input-file PATH] [options]",
277
+ " ours help api <operation>",
278
+ ...heading("Operations", operations),
279
+ ...heading("Input", [
280
+ option("--input JSON", "Inline JSON object; quote it so the shell passes one argument."),
281
+ option("--input-file PATH", "Read JSON from a file, or use - for stdin. Mutually exclusive with --input."),
282
+ option("--json", "For `api list`, return the operation-name array as JSON."),
283
+ option("--help", "Show help without connecting to a daemon.")
284
+ ]),
285
+ ...heading("Examples", [
286
+ " ours api list --json",
287
+ " ours help api send-message"
288
+ ]),
289
+ ""
290
+ ].join("\n");
291
+ }
292
+ function renderVersionHelp(version) {
293
+ return [
294
+ `ours ${version} \u2014 CLI version`,
295
+ "",
296
+ "Show the installed @ours.network/cli package version.",
297
+ "",
298
+ "Usage: ours version [--json] [--help]",
299
+ ...heading("Options", [
300
+ option("--json", 'Write {"name":"@ours.network/cli","version":"\u2026"} to stdout.'),
301
+ option("--help", "Show this help.")
302
+ ]),
303
+ ...heading("Compatibility", [" Additional tokens are currently ignored; they are not setup knobs and should not be relied on."]),
304
+ ...heading("Example", [" ours version --json"]),
305
+ ""
306
+ ].join("\n");
307
+ }
308
+ function renderTopHelp(version) {
309
+ const groups = Object.entries(COMMAND_GROUPS).map(([name, spec]) => option(name, spec.summary));
310
+ return [
311
+ `ours ${version} \u2014 operator CLI for the shared ours daemon`,
312
+ "",
313
+ "Create and manage identities, encrypted contacts/messages/files, and daemon lifecycle through the transport-neutral @ours.network/sdk client.",
314
+ "",
315
+ "Usage: ours <command> [options]",
316
+ " ours help [command [subcommand]]",
317
+ " ours <command> [subcommand] --help",
318
+ ...heading("First-time setup", [
319
+ " 1. Install Node.js 20 or newer and `npm install --global @ours.network/cli`.",
320
+ " 2. Inspect redacted defaults with `ours config show --json`.",
321
+ ' 3. Configure safe daemon settings, for example `ours config setup --port 3070 --state-dir "$HOME/.ours/state"`.',
322
+ " 4. Start and verify the shared daemon with `ours daemon start` and `ours daemon status --json`.",
323
+ " 5. Create the Human identity first: `ours identity create-root --name Alice@laptop`.",
324
+ ' 6. Add agent roles with `ours identity create --name BuildBot --bio "Build coordinator"`.'
325
+ ]),
326
+ ...heading("Commands", [
327
+ ...groups,
328
+ option("api", "Invoke the expert operation allowlist with typed JSON input."),
329
+ option("version", "Show the installed CLI version."),
330
+ option("help", "Show top-level, group, command, or API-operation help.")
331
+ ]),
332
+ ...heading("Connection and configuration", [
333
+ " Selection flags are --config PATH, or a coherent --endpoint URL / --port N / --state-dir PATH selection.",
334
+ " The default config is $OURS_CONFIG when set, otherwise ~/.ours/config.json when present.",
335
+ " Pair an explicit endpoint with its matching state directory; the CLI verifies /state-dir before sending a discovered API token.",
336
+ " There is intentionally no token flag. Keep credentials in operator-owned environment/config secret handling."
337
+ ]),
338
+ ...heading("Output and exit status", [
339
+ " Results go to stdout; diagnostics go to stderr. --json writes one JSON value and JSON-formatted errors, except watch streams JSON Lines.",
340
+ " Exit 0 means success, 2 means command/option usage error, and 1 means another failure.",
341
+ " `ours daemon status` uses exit 3 when the selected daemon is stopped.",
342
+ " Help is plain text, exits 0, and performs no daemon, file, or service-manager action."
343
+ ]),
344
+ ...heading("Examples", [
345
+ " ours help identity create",
346
+ ' ours message send --identity BuildBot --contact Peer --text "Build finished." --json',
347
+ " ours file list --identity BuildBot --json",
348
+ " ours api list --json"
349
+ ]),
350
+ "",
351
+ "Run `ours help <command>` to see its commands, then `ours help <command> <subcommand>` for every accepted option and side effect.",
352
+ ""
353
+ ].join("\n");
354
+ }
355
+ function helpRequestPath(argv) {
356
+ if (argv.length === 0) return [];
357
+ if (argv[0] === "help") return argv.slice(1).filter((value) => value !== "--help");
358
+ if (argv.includes("--help")) {
359
+ if (argv[0].startsWith("--")) return [];
360
+ const path = [argv[0]];
361
+ if ((isCommandGroup(argv[0]) || argv[0] === "api") && argv[1] !== void 0 && !argv[1].startsWith("--")) path.push(argv[1]);
362
+ return path;
363
+ }
364
+ if (argv.length === 1 && (isCommandGroup(argv[0]) || argv[0] === "api")) return [argv[0]];
365
+ return null;
366
+ }
367
+ function renderHelp(path, version) {
368
+ if (path.length === 0 || path.length === 1 && path[0] === "help") return renderTopHelp(version);
369
+ if (path.length === 1 && path[0] === "api") return renderApiHelp(version);
370
+ if (path.length === 1 && path[0] === "version") return renderVersionHelp(version);
371
+ if (path[0] === "api") {
372
+ if (path.length === 2 && path[1] === "list") return renderApiListHelp(version);
373
+ if (path.length !== 2 || !isOperationName(path[1])) {
374
+ throw new CliUsageError(`unknown API help topic ${JSON.stringify(path.slice(1).join(" "))}; run \`ours help api\``);
375
+ }
376
+ return renderApiOperationHelp(version, path[1]);
377
+ }
378
+ if (!isCommandGroup(path[0])) throw new CliUsageError(`unknown help topic ${JSON.stringify(path.join(" "))}; run \`ours help\``);
379
+ if (path.length === 1) return renderGroupHelp(version, path[0]);
380
+ if (path.length !== 2) throw new CliUsageError(`too many help topics; run \`ours help ${path[0]}\``);
381
+ const spec = commandSpec(path[0], path[1]);
382
+ if (!spec) throw new CliUsageError(`unknown ${path[0]} help topic ${JSON.stringify(path[1])}; run \`ours help ${path[0]}\``);
383
+ return spec.kind === "operation" ? renderOperationHelp(version, path[0], path[1], spec) : renderNativeHelp(version, path[0], path[1], spec);
384
+ }
385
+ function helpHint(group, action) {
386
+ if (group && isCommandGroup(group)) {
387
+ if (action && commandSpec(group, action)) return `Run \`ours help ${group} ${action}\` for usage.`;
388
+ return `Run \`ours help ${group}\` to list valid commands.`;
389
+ }
390
+ if (group === "api") return action === "list" ? "Run `ours help api list` for usage." : action && isOperationName(action) ? `Run \`ours help api ${action}\` for usage.` : "Run `ours help api` to list valid operations.";
391
+ return "Run `ours help` to list commands.";
392
+ }
393
+
394
+ export {
395
+ renderTopHelp,
396
+ helpRequestPath,
397
+ renderHelp,
398
+ helpHint
399
+ };