apcore-cli 0.8.1 → 0.9.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 CHANGED
@@ -5,6 +5,58 @@ All notable changes to apcore-cli (TypeScript SDK) will be documented in this fi
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.9.0] - 2026-05-13
9
+
10
+ ### Fixed (2026-05-13 — cross-SDK audit D10/D11/D1)
11
+
12
+ - **`ConfigEncryptor` LOGNAME key-derivation chain** (D10-001) — PBKDF2 username fallback was `USER → USERNAME → "unknown"` (3-tier); now `USER → LOGNAME → USERNAME → "unknown"` (4-tier) matching the spec and Rust. `src/security/config-encryptor.ts:183, 219`.
13
+ - **Sandbox stdin write lacks `'error'` listener** (D11-008) — `child.stdin.on('error', () => {})` added before `write()` so an EPIPE event from a child that exits early no longer surfaces as an uncaught exception. `src/security/sandbox.ts:153`.
14
+ - **`buildSandboxEnv` drops explicitly-empty env values** (D11-009) — `if (process.env[key])` changed to `if (process.env[key] !== undefined)` so `PATH=""` is forwarded uniformly with Python and Rust. `src/security/sandbox.ts:264`.
15
+ - **`exec --trace` flag ignored when used without `--strategy`** (D11-011) — condition `if (opts.strategy && executor.callWithTrace)` changed to `if ((opts.trace || opts.strategy) && executor.callWithTrace)`. `--trace` alone now routes through `callWithTrace` matching Python. `src/discovery.ts:345`.
16
+ - **CLI brand string in auth error messages** (D11-006) — remediation strings now say `apcli config set auth.api_key` (canonical FE-13 name). `src/security/auth.ts:46`.
17
+ - **`requestApproval` missing `requires_approval=false` short-circuit** (D11-014) — returns `approved/not_required` when the request explicitly carries `requires_approval: false`, matching Rust. `src/approval.ts:63`.
18
+ - **`AuthProvider` missing `config.encryptor` peer-attribute fallback** (D11-005) — `getEncryptor()` now walks explicit constructor arg → `config.encryptor` peer attribute → fresh instance, matching Python's three-tier chain. `src/security/auth.ts:22`.
19
+ - **`APCLI_SUBCOMMAND_NAMES` and `DEFAULT_BUILTIN_GROUP_NAME` not re-exported** (D1 re-audit) — both constants added to `src/index.ts:27` export block. Python and Rust already re-exported both.
20
+ - **Standalone bin entrypoint used deprecated `verbose:` field internally** (D9 re-audit) — `src/main.ts:848` `createCli` call now passes canonical `allOptions: verboseHelp`.
21
+ - **Stale `cli.ts` placeholder-type TODO** (D9-W2) — TODO comment updated to document the actual `apcore-js` Registry/ModuleDescriptor shape gap (method names diverge: `listModules`/`getModule` vs `list`/`getDefinition`/`moduleId`), replacing the generic "until available" wording.
22
+
23
+ ### Added
24
+
25
+ - **`CreateCliOptions.allOptions` field** (D1-W5) — canonical successor to the deprecated `verbose` field. Embedders should migrate `createCli({ verbose: true })` → `createCli({ allOptions: true })`. `verbose` remains for backward compat through v0.9 and will be removed in v0.10. `src/main.ts:251`.
26
+ - **`setLogLevel` / `getLogLevel` documented as intentionally TS-only** (D1-W4) — `src/index.ts:96-102` now carries a cross-SDK parity note explaining that Python and Rust delegate to their native logging channels.
27
+ - **`getAuditLogger` documented as intentionally TS-only** (D1 re-audit) — `src/index.ts:106` now carries a note. Only the setter (`setAuditLogger`) is the canonical cross-SDK API.
28
+
29
+ ### Fixed
30
+
31
+ - **CSV `--format csv` heterogeneous-keys data loss** — `formatExecResult` previously derived CSV headers from `Object.keys(rows[0])` only, silently dropping fields that first appeared in later rows. Surfaced via aisee-cli's `summarizeAction()` which emits optional `description` / `solution` fields. The header is now the **union of keys across all rows** in insertion-order. `src/output.ts:340-357`.
32
+ - **CSV line terminator** — now `\r\n` per RFC 4180 (was `\n`). Existing Excel + downstream-parser compatibility improves significantly.
33
+ - **CSV nested-value serialization** — now goes through the toolkit's canonical JSON encoder (compact, insertion-order, unicode-preserved). Behavior was already correct via `JSON.stringify`, but the contract is now enforced at the toolkit layer.
34
+
35
+ ### Changed
36
+
37
+ - **User-visible help/man/completion/error text no longer leaks the `apcore` / `apcore-js` framework name** to end users of downstream CLIs built on apcore-cli. Affected strings: footer hint (`Use --verbose to show all options (including built-in apcore options)` → `… (including built-in options)`, `src/main.ts:947`), `init` group description (`Scaffold new apcore modules` → `Scaffold new modules`, `src/init-cmd.ts:82`), top-level CLI description (`… execute apcore modules from the command line` → `… execute modules from the command line`, `src/main.ts:835`), standalone unwired-registry error message (`Error: no apcore-js registry wired.` → `Error: no module registry wired.`, `src/main.ts:619`), and man-page `ENVIRONMENT` text (`Path to the apcore extensions directory.` → `Path to the extensions directory.`, `src/shell.ts:302`). README's `--verbose` row updated to match. Two `tests/main.test.ts` assertions (`:891`, `:916`) updated to the new error string. Logger names, source comments, type comments, and environment-variable identifiers (`APCORE_*`) are unchanged — only descriptive copy that appears in `--help`, shell completion, `man` output, or user-facing error messages. Cross-SDK parity with Python 0.8.1 and Rust 0.8.1.
38
+
39
+ ### Changed (breaking CLI surface)
40
+
41
+ - **Global `--verbose` flag renamed to `--all-options`** — The help-display flag is now `--all-options`; use `apcore-cli module --help --all-options` to reveal hidden built-in options. `verbose` is removed from the reserved schema property names set — module schemas may now freely define `verbose: boolean` for runtime output control. Tracked in [apcore-cli#21](https://github.com/aiperceivable/apcore-cli/issues/21).
42
+
43
+ ### Changed (breaking peer-dep semantics)
44
+
45
+ - **`apcore-toolkit` promoted from optional to REQUIRED peer dependency** (`>=0.7.0`). All `--format` operations now go through the toolkit's reference implementation for csv/jsonl/markdown/skill (was only markdown/skill). Consumers that did not install the optional peer must add it. `package.json` peer-dependency-meta `optional: true` removed.
46
+
47
+ ### Removed
48
+
49
+ - `csvCellString` and `escapeCsvField` private helpers — replaced by `apcore_toolkit.formatCsv()` and the toolkit's RFC 4180 internals.
50
+
51
+ ### Why
52
+
53
+ Per-SDK CSV reimplementations had accumulated divergence: Python emitted Python repr `{'k': 'v'}`, TS dropped heterogeneous keys, Rust used `\n` not CRLF. The spec MUST language couldn't enforce conformance on downstream consumers (e.g. aisee-cli) that reimplemented. See ADR-09 in `apcore-cli/docs/tech-design.md` for the byte-equivalent vs SDK-native tier split.
54
+
55
+ ### Migration
56
+
57
+ Downstream consumers using only `json` / `table` formats are unaffected at runtime but need `apcore-toolkit@^0.7` installed alongside `apcore-cli@^0.9` (previously optional). aisee-cli and similar adapters get the CSV bug fix automatically on upgrade.
58
+
59
+
8
60
  ## [0.8.1] - 2026-05-09
9
61
 
10
62
  ### Fixed
package/README.md CHANGED
@@ -8,7 +8,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
8
8
 
9
9
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
10
10
  [![Node](https://img.shields.io/badge/node-18%2B-blue.svg)](https://nodejs.org)
11
- [![Tests](https://img.shields.io/badge/tests-275%2B%20passed-brightgreen.svg)]()
11
+ [![Tests](https://img.shields.io/badge/tests-passing-brightgreen.svg)]()
12
12
 
13
13
  | | |
14
14
  |---|---|
@@ -46,16 +46,15 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
46
46
  ## Installation
47
47
 
48
48
  ```bash
49
- pnpm add apcore-cli apcore-js
49
+ pnpm add apcore-cli apcore-js apcore-toolkit
50
50
  ```
51
51
 
52
- Requires Node.js 18+ and `apcore-js >= 0.21.0`.
52
+ Requires Node.js 18+, `apcore-js >= 0.21.0`, and `apcore-toolkit >= 0.7.0` (required peer dep as of v0.9.0).
53
53
 
54
- **Optional:** install `apcore-toolkit` (>=0.6.0) to enable display overlay and registry writer integration via `applyToolkitIntegration`, `DisplayResolver`, and `RegistryWriter`.
54
+ **v0.9.0 breaking change:** `apcore-toolkit` is now a **required** peer dependency (previously optional). All `--format` operations route through the toolkit's byte-equivalent reference implementations for csv / jsonl / markdown / skill. See [tech-design ADR-09](https://github.com/aiperceivable/apcore-cli/blob/main/docs/tech-design.md) for the rationale and migration notes.
55
55
 
56
56
  ```bash
57
- pnpm add apcore-cli apcore-js
58
- pnpm add -D apcore-toolkit # optional, for display overlay / registry writer
57
+ pnpm add apcore-cli apcore-js apcore-toolkit
59
58
  ```
60
59
 
61
60
  ## Quick Start
@@ -151,7 +150,7 @@ your-project/
151
150
  No changes to your project. Just install and run:
152
151
 
153
152
  ```bash
154
- pnpm add apcore-cli apcore-js
153
+ pnpm add apcore-cli apcore-js apcore-toolkit
155
154
  apcore-cli --extensions-dir ./extensions list
156
155
  apcore-cli --extensions-dir ./extensions math.add --a 5 --b 10
157
156
  ```
@@ -185,7 +184,7 @@ apcore-cli [OPTIONS] COMMAND [ARGS]
185
184
  | `--log-level` | `WARNING` | Logging: `DEBUG`, `INFO`, `WARNING`, `ERROR` |
186
185
  | `--version` | | Show version and exit |
187
186
  | `--help` | | Show help and exit |
188
- | `--verbose` | | Show all options in help (including built-in apcore options) |
187
+ | `--all-options` | | Show all options in help (including built-in options) |
189
188
  | `--man` | | Output man page in roff format (use with `--help`) |
190
189
 
191
190
  ### Built-in Commands (the `apcli` group)
@@ -235,12 +234,17 @@ The canonical 13 `apcli` subcommands:
235
234
  | `apcli init module <id>` | Scaffold a new module (TS/JS/YAML binding) into the extensions or commands directory (see `registerInitCommand` in `src/init-cmd.ts`) |
236
235
  | `apcli validate` | Validate modules and configuration against JSON Schema (see `registerValidateCommand` in `src/discovery.ts`) |
237
236
 
238
- **Shell integration**
237
+ **Shell integration** (under `apcli` group)
239
238
 
240
239
  | Command | Description |
241
240
  |---------|-------------|
242
241
  | `apcli completion <shell>` | Generate shell completion script for bash / zsh / fish (see `registerCompletionCommand` in `src/shell.ts`) |
243
- | `man [command]` (root) | Generate a man page in roff format for a single command or the whole program (see `configureManHelp` in `src/shell.ts`). Stays at the root (meta-command). |
242
+
243
+ **Root meta-commands** (NOT under `apcli` — invoked directly on the host CLI)
244
+
245
+ | Command | Description |
246
+ |---------|-------------|
247
+ | `<cli> --help --man [command]` | Generate a man page in roff format for a single command or the whole program (see `configureManHelp` in `src/shell.ts`). This is a root-level option, not an `apcli` subcommand. |
244
248
 
245
249
  #### Standalone vs. embedded surfaces
246
250
 
@@ -251,14 +255,14 @@ The canonical 13 `apcli` subcommands:
251
255
 
252
256
  ### Module Execution Options
253
257
 
254
- When executing a module (e.g. `apcore-cli math.add`), these built-in options are available (hidden by default; use `--verbose` to show in `--help`):
258
+ When executing a module (e.g. `apcore-cli math.add`), these built-in options are available (hidden by default; use `--all-options` to show in `--help`):
255
259
 
256
260
  | Option | Description |
257
261
  |--------|-------------|
258
262
  | `--input -` | Read JSON input from STDIN |
259
263
  | `--yes` / `-y` | Bypass approval prompts |
260
264
  | `--large-input` | Allow STDIN input larger than 10MB |
261
- | `--format <fmt>` | Output format: `json`, `table`, `csv`, `yaml`, or `jsonl` |
265
+ | `--format <fmt>` | Output format: `json`, `table`, `csv`, `yaml`, `jsonl`, `markdown`, `skill`. **v0.9.0:** `csv` / `jsonl` are byte-identical across SDKs via `apcore-toolkit.formatCsv` / `formatJsonl`. Fixes the prior heterogeneous-keys data-loss bug (header was derived from first row only). |
262
266
  | `--sandbox` | Run module in a subprocess sandbox (re-exec with stripped env; 64MiB stdout/stderr cap; 300s default timeout). Hidden by default — set `APCORE_CLI_SANDBOX=1` to enable globally. |
263
267
  | `--dry-run` | Run preflight checks (schema, ACL, approval) without executing (FE-11) |
264
268
  | `--trace` | Emit execution pipeline trace (strategy, hooks, middleware timings) |
@@ -35,6 +35,12 @@ function exitCodeForError(error) {
35
35
  if (error instanceof SchemaValidationError) {
36
36
  return EXIT_CODES.SCHEMA_VALIDATION_ERROR;
37
37
  }
38
+ if (error instanceof MaxDepthExceededError || error instanceof CircularRefError) {
39
+ return EXIT_CODES.SCHEMA_CIRCULAR_REF;
40
+ }
41
+ if (error instanceof UnresolvableRefError) {
42
+ return EXIT_CODES.SCHEMA_VALIDATION_ERROR;
43
+ }
38
44
  if (error instanceof ModuleNotFoundError) {
39
45
  return EXIT_CODES.MODULE_NOT_FOUND;
40
46
  }
@@ -74,7 +80,7 @@ function exitCodeForError(error) {
74
80
  }
75
81
  return EXIT_CODES.MODULE_EXECUTE_ERROR;
76
82
  }
77
- var ApprovalTimeoutError, AuthenticationError, ConfigDecryptionError, ModuleExecutionError, ApprovalDeniedError, SchemaValidationError, ModuleNotFoundError, EXIT_CODES;
83
+ var ApprovalTimeoutError, AuthenticationError, ConfigDecryptionError, ModuleExecutionError, ApprovalDeniedError, SchemaValidationError, MaxDepthExceededError, CircularRefError, UnresolvableRefError, ModuleNotFoundError, EXIT_CODES;
78
84
  var init_errors = __esm({
79
85
  "src/errors.ts"() {
80
86
  "use strict";
@@ -115,6 +121,24 @@ var init_errors = __esm({
115
121
  this.name = "SchemaValidationError";
116
122
  }
117
123
  };
124
+ MaxDepthExceededError = class extends Error {
125
+ constructor(message = "Schema $ref resolution depth exceeded") {
126
+ super(message);
127
+ this.name = "MaxDepthExceededError";
128
+ }
129
+ };
130
+ CircularRefError = class extends Error {
131
+ constructor(message = "Circular $ref detected in schema") {
132
+ super(message);
133
+ this.name = "CircularRefError";
134
+ }
135
+ };
136
+ UnresolvableRefError = class extends Error {
137
+ constructor(message = "Unresolvable $ref in schema") {
138
+ super(message);
139
+ this.name = "UnresolvableRefError";
140
+ }
141
+ };
118
142
  ModuleNotFoundError = class extends Error {
119
143
  constructor(message = "Module not found") {
120
144
  super(message);
@@ -155,6 +179,7 @@ var init_errors = __esm({
155
179
  var sandbox_exports = {};
156
180
  __export(sandbox_exports, {
157
181
  Sandbox: () => Sandbox,
182
+ _buildSandboxEnvForTesting: () => _buildSandboxEnvForTesting,
158
183
  runSandboxRunner: () => runSandboxRunner
159
184
  });
160
185
  import { spawn } from "child_process";
@@ -195,10 +220,13 @@ async function runSandboxRunner(moduleId) {
195
220
  process.exit(1);
196
221
  }
197
222
  }
223
+ function _buildSandboxEnvForTesting(tmpDir) {
224
+ return buildSandboxEnv(tmpDir);
225
+ }
198
226
  function buildSandboxEnv(tmpDir) {
199
227
  const env = {};
200
228
  for (const key of SANDBOX_ALLOW_KEYS) {
201
- if (process.env[key]) env[key] = process.env[key];
229
+ if (process.env[key] !== void 0) env[key] = process.env[key];
202
230
  }
203
231
  for (const [key, val] of Object.entries(process.env)) {
204
232
  if (key.startsWith(SANDBOX_ALLOW_PREFIX) && !key.startsWith(SANDBOX_DENY_PREFIX) && !SANDBOX_DENY_KEYS.includes(key)) {
@@ -303,11 +331,20 @@ var init_sandbox = __esm({
303
331
  }
304
332
  stderr += chunk.toString();
305
333
  });
334
+ child.stdin.on("error", () => {
335
+ });
306
336
  child.stdin.write(JSON.stringify(inputData));
307
337
  child.stdin.end();
308
338
  return new Promise((resolve2, reject) => {
339
+ const cleanup = () => {
340
+ try {
341
+ rmSync(tmpDir, { recursive: true, force: true });
342
+ } catch {
343
+ }
344
+ };
309
345
  const timer = setTimeout(() => {
310
346
  child.kill("SIGKILL");
347
+ cleanup();
311
348
  reject(
312
349
  new ModuleExecutionError(
313
350
  `Sandbox module '${moduleId}' timed out after ${this.timeoutSeconds}s.`
@@ -316,10 +353,7 @@ var init_sandbox = __esm({
316
353
  }, this.timeoutSeconds * 1e3);
317
354
  child.on("close", (code) => {
318
355
  clearTimeout(timer);
319
- try {
320
- rmSync(tmpDir, { recursive: true, force: true });
321
- } catch {
322
- }
356
+ cleanup();
323
357
  if (sizeExceeded) {
324
358
  const limitMiB = Math.floor(outputCap / (1024 * 1024));
325
359
  reject(new ModuleExecutionError(
@@ -343,6 +377,7 @@ var init_sandbox = __esm({
343
377
  });
344
378
  child.on("error", (err) => {
345
379
  clearTimeout(timer);
380
+ cleanup();
346
381
  reject(new ModuleExecutionError(`Failed to spawn sandbox process: ${err.message}`));
347
382
  });
348
383
  });
@@ -375,27 +410,21 @@ function resolveNode(node, defs, visited, depth, maxDepth, moduleId) {
375
410
  if ("$ref" in obj) {
376
411
  const refPath = obj.$ref;
377
412
  if (depth >= maxDepth) {
378
- process.stderr.write(
379
- `Error: $ref resolution depth exceeded maximum of ${maxDepth} for module '${moduleId}'.
380
- `
413
+ throw new MaxDepthExceededError(
414
+ `$ref resolution depth exceeded maximum of ${maxDepth} for module '${moduleId}'.`
381
415
  );
382
- process.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF);
383
416
  }
384
417
  if (visited.has(refPath)) {
385
- process.stderr.write(
386
- `Error: Circular $ref detected in schema for module '${moduleId}' at path '${refPath}'.
387
- `
418
+ throw new CircularRefError(
419
+ `Circular $ref detected in schema for module '${moduleId}' at path '${refPath}'.`
388
420
  );
389
- process.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF);
390
421
  }
391
422
  const parts = refPath.split("/");
392
423
  const key = parts[parts.length - 1];
393
424
  if (!(key in defs)) {
394
- process.stderr.write(
395
- `Error: Unresolvable $ref '${refPath}' in schema for module '${moduleId}'.
396
- `
425
+ throw new UnresolvableRefError(
426
+ `Unresolvable $ref '${refPath}' in schema for module '${moduleId}'.`
397
427
  );
398
- process.exit(EXIT_CODES.SCHEMA_VALIDATION_ERROR);
399
428
  }
400
429
  const newVisited = new Set(visited);
401
430
  newVisited.add(refPath);
@@ -496,17 +525,12 @@ function resolveNode(node, defs, visited, depth, maxDepth, moduleId) {
496
525
  return merged;
497
526
  }
498
527
  }
499
- if ("properties" in obj && typeof obj.properties === "object" && obj.properties !== null) {
500
- const props = obj.properties;
501
- for (const [propName, propSchema] of Object.entries(props)) {
502
- props[propName] = resolveNode(
503
- propSchema,
504
- defs,
505
- visited,
506
- depth,
507
- maxDepth,
508
- moduleId
509
- );
528
+ for (const [k, v] of Object.entries(obj)) {
529
+ if (k === "allOf" || k === "anyOf" || k === "oneOf" || k === "$ref") {
530
+ continue;
531
+ }
532
+ if (typeof v === "object" && v !== null && !Array.isArray(v)) {
533
+ obj[k] = resolveNode(v, defs, visited, depth, maxDepth, moduleId);
510
534
  }
511
535
  }
512
536
  return obj;
@@ -703,7 +727,7 @@ var init_schema_parser = __esm({
703
727
  "format",
704
728
  "fields",
705
729
  "sandbox",
706
- "verbose",
730
+ "all_options",
707
731
  "dry_run",
708
732
  "trace",
709
733
  "stream",
@@ -816,6 +840,14 @@ var init_approval = __esm({
816
840
  }
817
841
  async requestApproval(request) {
818
842
  const moduleId = request.module_id ?? "unknown";
843
+ if (request.requires_approval === false) {
844
+ return { status: "approved", approved_by: "not_required" };
845
+ }
846
+ const moduleDef = request.module_def;
847
+ const annotationsForCheck = moduleDef?.annotations;
848
+ if (annotationsForCheck && annotationsForCheck.requires_approval === false) {
849
+ return { status: "approved", approved_by: "not_required" };
850
+ }
819
851
  if (this.autoApprove) {
820
852
  return { status: "approved", approved_by: "auto_approve" };
821
853
  }
@@ -852,6 +884,7 @@ var init_approval = __esm({
852
884
 
853
885
  // src/output.ts
854
886
  import yaml from "js-yaml";
887
+ import { formatCsv, formatJsonl } from "apcore-toolkit";
855
888
  function descriptorToScanned(m) {
856
889
  const metadata = m.metadata ?? {};
857
890
  const display = metadata["display"] ?? null;
@@ -872,11 +905,6 @@ function descriptorToScanned(m) {
872
905
  warnings: []
873
906
  };
874
907
  }
875
- function csvCellString(value) {
876
- if (value === null || value === void 0) return "";
877
- if (typeof value === "object") return JSON.stringify(value);
878
- return String(value);
879
- }
880
908
  function resolveFormat(explicitFormat) {
881
909
  if (explicitFormat !== void 0) {
882
910
  return explicitFormat;
@@ -1079,30 +1107,18 @@ function formatExecResult(result, format, fields) {
1079
1107
  }
1080
1108
  const effective = resolveFormat(format);
1081
1109
  if (effective === "csv") {
1082
- if (typeof effective_result === "object" && !Array.isArray(effective_result) && effective_result !== null) {
1083
- const obj = effective_result;
1084
- const keys = Object.keys(obj);
1085
- const header = keys.map(escapeCsvField).join(",");
1086
- const row = keys.map((k) => escapeCsvField(csvCellString(obj[k]))).join(",");
1087
- process.stdout.write(header + "\n" + row + "\n");
1088
- } else if (Array.isArray(effective_result) && effective_result.length > 0 && typeof effective_result[0] === "object") {
1089
- const keys = Object.keys(effective_result[0]);
1090
- const header = keys.map(escapeCsvField).join(",");
1091
- const rows = effective_result.map((item) => {
1092
- const obj = item;
1093
- return keys.map((k) => escapeCsvField(csvCellString(obj[k]))).join(",");
1094
- });
1095
- process.stdout.write(header + "\n" + rows.join("\n") + "\n");
1110
+ const rows = toRowsForTabular(effective_result);
1111
+ if (rows !== null) {
1112
+ process.stdout.write(formatCsv(rows));
1096
1113
  } else {
1097
1114
  process.stdout.write(JSON.stringify(effective_result) + "\n");
1098
1115
  }
1099
1116
  } else if (effective === "yaml") {
1100
1117
  process.stdout.write(yaml.dump(effective_result, { lineWidth: -1 }));
1101
1118
  } else if (effective === "jsonl") {
1102
- if (Array.isArray(effective_result)) {
1103
- for (const item of effective_result) {
1104
- process.stdout.write(JSON.stringify(item) + "\n");
1105
- }
1119
+ const rows = toRowsForTabular(effective_result);
1120
+ if (rows !== null) {
1121
+ process.stdout.write(formatJsonl(rows));
1106
1122
  } else {
1107
1123
  process.stdout.write(JSON.stringify(effective_result) + "\n");
1108
1124
  }
@@ -1119,11 +1135,19 @@ function formatExecResult(result, format, fields) {
1119
1135
  process.stdout.write(String(effective_result) + "\n");
1120
1136
  }
1121
1137
  }
1122
- function escapeCsvField(value) {
1123
- if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
1124
- return '"' + value.replace(/"/g, '""') + '"';
1138
+ function toRowsForTabular(value) {
1139
+ if (value === null || value === void 0) return null;
1140
+ if (Array.isArray(value)) {
1141
+ if (value.length === 0) return null;
1142
+ if (!value.every((item) => typeof item === "object" && item !== null && !Array.isArray(item))) {
1143
+ return null;
1144
+ }
1145
+ return value;
1146
+ }
1147
+ if (typeof value === "object") {
1148
+ return [value];
1125
1149
  }
1126
- return value;
1150
+ return null;
1127
1151
  }
1128
1152
  function formatPreflightResult(result, format) {
1129
1153
  const resolved = resolveFormat(format);
@@ -1204,7 +1228,7 @@ var init_output = __esm({
1204
1228
  "use strict";
1205
1229
  init_esm_shims();
1206
1230
  init_errors();
1207
- TOOLKIT_MISSING_HINT = "The 'markdown' and 'skill' output formats require the apcore-toolkit peer dependency. Install with: npm install apcore-toolkit@^0.6";
1231
+ TOOLKIT_MISSING_HINT = "The 'markdown' and 'skill' output formats require the apcore-toolkit peer dependency. Install with: npm install apcore-toolkit@^0.7";
1208
1232
  }
1209
1233
  });
1210
1234
 
@@ -1235,7 +1259,7 @@ function renderTemplate(template, context) {
1235
1259
  return result;
1236
1260
  }
1237
1261
  function registerInitCommand(cli) {
1238
- const initGroup = cli.command("init").description("Scaffold new apcore modules.");
1262
+ const initGroup = cli.command("init").description("Scaffold new modules.");
1239
1263
  initGroup.command("module <module-id>").description("Create a new module from a template.\n\nMODULE_ID is the module identifier (e.g., ops.deploy, user.create).").option(
1240
1264
  "--style <style>",
1241
1265
  "Module style: decorator (@module), convention (plain function), or binding (YAML).",
@@ -1811,7 +1835,7 @@ function buildProgramManPage(program, progName, version, description, docsUrl2)
1811
1835
  s.push(".SH ENVIRONMENT");
1812
1836
  s.push(".TP");
1813
1837
  s.push("\\fBAPCORE_EXTENSIONS_ROOT\\fR");
1814
- s.push("Path to the apcore extensions directory.");
1838
+ s.push("Path to the extensions directory.");
1815
1839
  s.push(".TP");
1816
1840
  s.push("\\fBAPCORE_CLI_AUTO_APPROVE\\fR");
1817
1841
  s.push("Set to \\fB1\\fR to bypass approval prompts.");
@@ -1836,7 +1860,7 @@ function buildProgramManPage(program, progName, version, description, docsUrl2)
1836
1860
  ${meaning}`);
1837
1861
  }
1838
1862
  s.push(".SH SEE ALSO");
1839
- s.push(`\\fB${progName} \\-\\-help \\-\\-verbose\\fR for full option list.`);
1863
+ s.push(`\\fB${progName} \\-\\-help \\-\\-all\\-options\\fR for full option list.`);
1840
1864
  if (docsUrl2) {
1841
1865
  s.push(`.PP
1842
1866
  Full documentation at \\fI${roffEscape(docsUrl2)}\\fR`);
@@ -2243,7 +2267,7 @@ var init_config_encryptor = __esm({
2243
2267
  _ConfigEncryptor.weakFallbackWarned = true;
2244
2268
  }
2245
2269
  const hostname2 = os3.hostname();
2246
- const username = process.env.USER ?? process.env.USERNAME ?? "unknown";
2270
+ const username = process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2247
2271
  const material = `${hostname2}:${username}`;
2248
2272
  return crypto2.pbkdf2Sync(material, salt, PBKDF2_ITERATIONS, 32, "sha256");
2249
2273
  }
@@ -2275,7 +2299,7 @@ var init_config_encryptor = __esm({
2275
2299
  const tag = data.subarray(12, 28);
2276
2300
  const ct = data.subarray(28);
2277
2301
  const hostname2 = os3.hostname();
2278
- const username = process.env.USER ?? process.env.USERNAME ?? "unknown";
2302
+ const username = process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2279
2303
  const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
2280
2304
  const materials = passphrase ? [passphrase, `${hostname2}:${username}`] : [`${hostname2}:${username}`];
2281
2305
  for (const material of materials) {
@@ -2305,10 +2329,26 @@ var init_auth = __esm({
2305
2329
  init_config_encryptor();
2306
2330
  AuthProvider = class {
2307
2331
  config;
2308
- encryptor;
2332
+ _encryptor;
2309
2333
  constructor(config, encryptor) {
2310
2334
  this.config = config;
2311
- this.encryptor = encryptor ?? new ConfigEncryptor();
2335
+ this._encryptor = encryptor;
2336
+ }
2337
+ /**
2338
+ * Resolve the active ConfigEncryptor instance.
2339
+ *
2340
+ * D11-005 (2026-05-12): three-tier fallback chain matching Python's
2341
+ * `_get_encryptor` (auth.py:33): explicit constructor arg > peer attribute
2342
+ * `config.encryptor` (set by embedders injecting forced-AES test fixtures
2343
+ * or shared instances) > fresh `new ConfigEncryptor()`. Previously TS
2344
+ * skipped the peer-attribute tier, silently giving embedders a different
2345
+ * encryptor than the one they wired on the config.
2346
+ */
2347
+ getEncryptor() {
2348
+ if (this._encryptor) return this._encryptor;
2349
+ const fromConfig = this.config.encryptor;
2350
+ if (fromConfig) return fromConfig;
2351
+ return new ConfigEncryptor();
2312
2352
  }
2313
2353
  /**
2314
2354
  * Retrieve the API key from the configured sources.
@@ -2326,11 +2366,11 @@ var init_auth = __esm({
2326
2366
  const strResult = String(result);
2327
2367
  if (strResult.startsWith("keyring:") || strResult.startsWith("enc:")) {
2328
2368
  try {
2329
- return await this.encryptor.retrieve(strResult, "auth.api_key");
2369
+ return await this.getEncryptor().retrieve(strResult, "auth.api_key");
2330
2370
  } catch (err) {
2331
2371
  if (err instanceof ConfigDecryptionError) {
2332
2372
  throw new AuthenticationError(
2333
- "Failed to decrypt stored API key. Re-configure with 'apcore-cli config set auth.api_key'."
2373
+ "Failed to decrypt stored API key. Re-store with 'apcli config set auth.api_key'."
2334
2374
  );
2335
2375
  }
2336
2376
  throw err;
@@ -2359,7 +2399,7 @@ var init_auth = __esm({
2359
2399
  }
2360
2400
  if (/[\r\n]/.test(key)) {
2361
2401
  throw new AuthenticationError(
2362
- "Malformed API key: contains invalid characters (CR/LF). Re-configure with 'apcore-cli config set auth.api_key'."
2402
+ "Malformed API key: contains invalid characters (CR/LF). Re-store with 'apcli config set auth.api_key'."
2363
2403
  );
2364
2404
  }
2365
2405
  headers.Authorization = `Bearer ${key.trim()}`;
@@ -2581,7 +2621,7 @@ function registerExecCommand(apcliGroup, registry, executor) {
2581
2621
  return;
2582
2622
  }
2583
2623
  let result;
2584
- if (opts.strategy && executor.callWithTrace) {
2624
+ if ((opts.trace || opts.strategy) && executor.callWithTrace) {
2585
2625
  const [res] = await executor.callWithTrace(moduleId, merged, { strategy: opts.strategy });
2586
2626
  result = res;
2587
2627
  } else {
@@ -3697,6 +3737,7 @@ __export(main_exports, {
3697
3737
  reconvertEnumValues: () => reconvertEnumValues,
3698
3738
  resolveIntOption: () => resolveIntOption,
3699
3739
  resolveStringOption: () => resolveStringOption,
3740
+ setAllOptionsHelp: () => setAllOptionsHelp,
3700
3741
  setDocsUrl: () => setDocsUrl,
3701
3742
  setVerboseHelp: () => setVerboseHelp,
3702
3743
  validateModuleId: () => validateModuleId
@@ -3705,14 +3746,17 @@ import { readFileSync as readFileSync3 } from "fs";
3705
3746
  import { fileURLToPath as fileURLToPath2 } from "url";
3706
3747
  import * as path5 from "path";
3707
3748
  import { Command as Command5, CommanderError, Option as Option4 } from "commander";
3749
+ function setAllOptionsHelp(allOptions) {
3750
+ verboseHelp = allOptions;
3751
+ }
3708
3752
  function setVerboseHelp(verbose) {
3709
- verboseHelp = verbose;
3753
+ setAllOptionsHelp(verbose);
3710
3754
  }
3711
3755
  function setDocsUrl(url) {
3712
3756
  docsUrl = url;
3713
3757
  }
3714
3758
  function hasVerboseFlag() {
3715
- return process.argv.includes("--verbose");
3759
+ return process.argv.includes("--all-options");
3716
3760
  }
3717
3761
  function resolveIntOption(cliValue, envValue, defaultValue) {
3718
3762
  if (cliValue !== void 0 && Number.isFinite(cliValue) && cliValue > 0) {
@@ -3787,7 +3831,7 @@ function emitErrorTty(e, exitCode) {
3787
3831
  Exit code: ${exitCode}
3788
3832
  `);
3789
3833
  }
3790
- function createCli(extensionsDirOrOpts, progName, verbose = false) {
3834
+ function createCli(extensionsDirOrOpts, progName, allOptions = false) {
3791
3835
  let extensionsDir;
3792
3836
  let registry;
3793
3837
  let executor;
@@ -3802,7 +3846,7 @@ function createCli(extensionsDirOrOpts, progName, verbose = false) {
3802
3846
  if (typeof extensionsDirOrOpts === "object" && extensionsDirOrOpts !== null) {
3803
3847
  extensionsDir = extensionsDirOrOpts.extensionsDir;
3804
3848
  progName = extensionsDirOrOpts.progName ?? progName;
3805
- verbose = extensionsDirOrOpts.verbose ?? verbose;
3849
+ allOptions = extensionsDirOrOpts.allOptions ?? extensionsDirOrOpts.verbose ?? allOptions;
3806
3850
  app = extensionsDirOrOpts.app;
3807
3851
  registry = extensionsDirOrOpts.registry;
3808
3852
  executor = extensionsDirOrOpts.executor;
@@ -3816,7 +3860,7 @@ function createCli(extensionsDirOrOpts, progName, verbose = false) {
3816
3860
  } else {
3817
3861
  extensionsDir = extensionsDirOrOpts;
3818
3862
  }
3819
- verboseHelp = verbose;
3863
+ verboseHelp = allOptions;
3820
3864
  registerConfigNamespace();
3821
3865
  try {
3822
3866
  const auditLogger = new AuditLogger();
@@ -3849,7 +3893,7 @@ function createCli(extensionsDirOrOpts, progName, verbose = false) {
3849
3893
  }
3850
3894
  }
3851
3895
  const registryInjected = registry !== void 0;
3852
- const program = new Command5(resolvedProgName).exitOverride().helpOption("-h, --help", "Print help").addHelpCommand("help [command]", "Print this message or the help of the given subcommand(s)").description(appDescription ?? `${resolvedProgName} CLI`).option("--log-level <level>", "Logging level (DEBUG|INFO|WARNING|ERROR)", "WARNING").option("--verbose", "Show all options in help output (including built-in options)");
3896
+ const program = new Command5(resolvedProgName).exitOverride().helpOption("-h, --help", "Print help").addHelpCommand("help [command]", "Print this message or the help of the given subcommand(s)").description(appDescription ?? `${resolvedProgName} CLI`).option("--log-level <level>", "Logging level (DEBUG|INFO|WARNING|ERROR)", "WARNING").option("--all-options", "Show all options in help output (including built-in options)");
3853
3897
  if (appVersion) {
3854
3898
  program.version(appVersion, "-V, --version", "Print version");
3855
3899
  }
@@ -3920,7 +3964,7 @@ function createCli(extensionsDirOrOpts, progName, verbose = false) {
3920
3964
  _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exposureFilter);
3921
3965
  program.addHelpText("after", [
3922
3966
  "",
3923
- "Use --help --verbose to show all options (including built-in options).",
3967
+ "Use --help --all-options to show all options (including built-in options).",
3924
3968
  "Use --help --man to display a formatted man page."
3925
3969
  ].join("\n"));
3926
3970
  configureManHelp(program, resolvedProgName, appVersion ?? VERSION);
@@ -3957,7 +4001,7 @@ function createCli(extensionsDirOrOpts, progName, verbose = false) {
3957
4001
  function _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exposureFilter) {
3958
4002
  const emitUnwiredError = () => {
3959
4003
  process.stderr.write(
3960
- "Error: no apcore-js registry wired. In standalone mode, pass --extensions-dir <path> to enable module discovery.\n"
4004
+ "Error: no module registry wired. In standalone mode, pass --extensions-dir <path> to enable module discovery.\n"
3961
4005
  );
3962
4006
  process.exit(EXIT_CODES.CONFIG_INVALID);
3963
4007
  };
@@ -4071,9 +4115,9 @@ function main(progName) {
4071
4115
  verboseHelp = hasVerboseFlag();
4072
4116
  const program = createCli({
4073
4117
  progName,
4074
- verbose: verboseHelp,
4118
+ allOptions: verboseHelp,
4075
4119
  version: VERSION,
4076
- description: `${progName ?? "apcore-cli"} \u2014 execute apcore modules from the command line`
4120
+ description: `${progName ?? "apcore-cli"} \u2014 execute modules from the command line`
4077
4121
  });
4078
4122
  try {
4079
4123
  program.parse(process.argv);
@@ -4101,7 +4145,17 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
4101
4145
  if (inputSchema && typeof inputSchema === "object" && inputSchema.properties) {
4102
4146
  try {
4103
4147
  resolvedSchema = resolveRefs(inputSchema, 32, moduleId);
4104
- } catch {
4148
+ } catch (err) {
4149
+ if (err instanceof MaxDepthExceededError || err instanceof CircularRefError) {
4150
+ process.stderr.write(`Error: ${err.message}
4151
+ `);
4152
+ process.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF);
4153
+ }
4154
+ if (err instanceof UnresolvableRefError) {
4155
+ process.stderr.write(`Error: ${err.message}
4156
+ `);
4157
+ process.exit(EXIT_CODES.SCHEMA_VALIDATION_ERROR);
4158
+ }
4105
4159
  resolvedSchema = inputSchema;
4106
4160
  }
4107
4161
  schemaOptions = schemaToCliOptions(resolvedSchema, helpTextMaxLength);
@@ -4146,7 +4200,7 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
4146
4200
  cmd.addOption(approvalTokenOpt);
4147
4201
  const footerParts = [];
4148
4202
  if (!verbose) {
4149
- footerParts.push("Use --verbose to show all options (including built-in apcore options).");
4203
+ footerParts.push("Use --all-options to show all options (including built-in options).");
4150
4204
  }
4151
4205
  if (docsUrl) {
4152
4206
  footerParts.push(`Docs: ${docsUrl}/commands/${effectiveCmdName}`);