@crediolabs/policy-builder-cli 0.1.5 → 0.1.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Untangled Finance Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -19,10 +19,16 @@ policy-builder record --network <mainnet|testnet> --hash <tx> | --xdr <b64>
19
19
  [--json] [--quiet] [--out <path>]
20
20
 
21
21
  policy-builder synthesize --mandate <path.json>
22
+ [--oz-config <path.json>] [--confidence <0..1>]
22
23
  [--json] [--quiet] [--out <path>]
23
24
 
24
25
  policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
25
26
  [--responses <path.json>]
27
+ [--oz-config <path.json>] [--confidence <0..1>]
28
+ [--smart-account <C...>] [--install-nonce <n>]
29
+ [--oracle-max-staleness <s>] [--oracle-max-deviation <bps>]
30
+ [--window-seconds <n>] [--valid-until <ledger>]
31
+ [--limit-amount <i128str>] [--invocation-limit <n>]
26
32
  [--json] [--quiet] [--out <path>]
27
33
  ```
28
34
 
@@ -31,8 +37,47 @@ Global flags:
31
37
  - `--json` — emit a machine-readable JSON envelope (`{ ok, data }` / `{ ok, error }`) on stdout.
32
38
  - `--quiet` — suppress progress / non-error output.
33
39
  - `--out <path>` — write the artefact to a file (JSON).
34
-
35
- A `ToolError` exits non-zero (`1`), so CI scripts can gate on `$?`.
40
+ - `--oz-config <path.json>` — JSON `OzAdapterConfig` (custom OZ instance
41
+ addresses for the network). Defaults to the core's mainnet placeholder.
42
+ Applies to both the `--mandate` and `--recorded-tx` synthesize paths.
43
+ - `--confidence <0..1>` — recorder confidence gate threshold. The synth
44
+ rejects when `parseConfidence.overall < threshold`; it does not change the
45
+ recording's `parseConfidence.thresholdUsed` in the output. Must be a finite
46
+ number in `[0, 1]` (inclusive); out-of-range values are rejected with
47
+ `CLI_MISSING_ARG`.
48
+ - `--smart-account <C...>` — opt into the interpreter adapter, so
49
+ constraints OZ built-ins cannot express (per-method scoping, invocation-count
50
+ windows, oracle bounds, exact hop paths) lower to a real predicate document
51
+ instead of warnings. Must be a Stellar contract strkey (56 chars, base32
52
+ alphabet `[2-7A-Z]`, matching `^C[2-7A-Z]{55}$`).
53
+ - `--install-nonce <n>` — per-rule install nonce for the interpreter policy.
54
+ Requires `--smart-account`; the CLI rejects `--install-nonce` without
55
+ `--smart-account` so callers cannot silently drop the nonce.
56
+ - `--oracle-max-staleness <s>` — interpreter oracle max staleness in seconds
57
+ (positive integer `<= 600`, tighten-only vs the wasm default). Requires
58
+ `--smart-account`; widen values are rejected by the core.
59
+ - `--oracle-max-deviation <bps>` — interpreter oracle max deviation in basis
60
+ points (positive integer `<= 200`, tighten-only). Requires
61
+ `--smart-account`; widen values are rejected by the core.
62
+ - `--window-seconds <n>` — per-field override of `userResponses.windowSeconds`.
63
+ Overrides the same field from `--responses` (CLI flag wins).
64
+ - `--valid-until <ledger>` — per-field override of `userResponses.validUntilLedger`.
65
+ Overrides the same field from `--responses` (CLI flag wins).
66
+ - `--limit-amount <i128str>` — per-field override of `userResponses.limitAmount`
67
+ (positive base-10 i128 string). Overrides the same field from `--responses`.
68
+ - `--invocation-limit <n>` — per-field override of `userResponses.invocationLimit`.
69
+ Overrides the same field from `--responses` (CLI flag wins).
70
+
71
+ **Precedence for per-field response flags:** `--responses <file>` provides the
72
+ base `userResponses` object; any of the per-field flags above override the
73
+ same-named field from the file (the CLI flag is the explicit caller intent,
74
+ the file is the default bag). The two never merge - either-or on the same
75
+ field.
76
+
77
+ A `ToolError` exits non-zero (`1`), so CI scripts can gate on `$?`. Empty
78
+ `--smart-account`, out-of-range `--confidence`, non-numeric `--window-seconds`,
79
+ non-integer `--oracle-*`, and `--oracle-*` without `--smart-account` are all
80
+ rejected with `CLI_MISSING_ARG` before the call reaches the core.
36
81
 
37
82
  ## Examples
38
83
 
@@ -43,6 +88,19 @@ policy-builder record --network mainnet --hash <tx-hash> --out recorded-tx.json
43
88
  policy-builder synthesize --recorded-tx recorded-tx.json --network mainnet --json
44
89
  ```
45
90
 
91
+ Synthesise with the interpreter opt-in (use when the policy needs constraints
92
+ OZ built-ins cannot express — e.g. per-claim frequency caps on Blend):
93
+
94
+ ```sh
95
+ policy-builder synthesize \
96
+ --recorded-tx recorded-tx.json \
97
+ --network mainnet \
98
+ --responses responses.json \
99
+ --smart-account CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIT4C \
100
+ --install-nonce 1 \
101
+ --json
102
+ ```
103
+
46
104
  Synthesise deterministically from a mandate (`mandate.json`):
47
105
 
48
106
  ```json
@@ -5,10 +5,15 @@
5
5
  // policy-builder record --network <mainnet|testnet> --hash <tx>
6
6
  // [--xdr <b64>] [--json] [--quiet] [--out <path>]
7
7
  // policy-builder synthesize --mandate <path.json>
8
+ // [--oz-config <path.json>] [--confidence <0..1>]
8
9
  // [--json] [--quiet] [--out <path>]
9
10
  // policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
10
11
  // [--responses <path.json>]
12
+ // [--oz-config <path.json>] [--confidence <0..1>]
11
13
  // [--smart-account <C...>] [--install-nonce <n>]
14
+ // [--oracle-max-staleness <s>] [--oracle-max-deviation <bps>]
15
+ // [--window-seconds <n>] [--valid-until <ledger>]
16
+ // [--limit-amount <i128str>] [--invocation-limit <n>]
12
17
  // [--json] [--quiet] [--out <path>]
13
18
  //
14
19
  // The router is hand-rolled (no commander / yargs dep). It splits argv into
@@ -59,21 +64,50 @@ Usage:
59
64
  policy-builder record --network <mainnet|testnet> --hash <tx> | --xdr <b64>
60
65
  [--json] [--quiet] [--out <path>]
61
66
  policy-builder synthesize --mandate <path.json>
67
+ [--oz-config <path.json>] [--confidence <0..1>]
62
68
  [--json] [--quiet] [--out <path>]
63
69
  policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
64
70
  [--responses <path.json>]
71
+ [--oz-config <path.json>] [--confidence <0..1>]
65
72
  [--smart-account <C...>] [--install-nonce <n>]
73
+ [--oracle-max-staleness <s>] [--oracle-max-deviation <bps>]
74
+ [--window-seconds <n>] [--valid-until <ledger>]
75
+ [--limit-amount <i128str>] [--invocation-limit <n>]
66
76
  [--json] [--quiet] [--out <path>]
67
77
 
68
78
  Flags:
69
- --json emit machine-readable JSON on stdout
70
- --quiet suppress progress / non-error output
71
- --out write artefact to file (JSON)
72
- --smart-account C... account to opt into the interpreter adapter, so
73
- non-OZ constraints (per-method scoping, invocation-count
74
- windows, oracle bounds, exact hop paths) lower to a real
75
- predicate document instead of warnings
76
- --install-nonce per-rule install nonce for the interpreter policy (default 1)
79
+ --json emit machine-readable JSON on stdout
80
+ --quiet suppress progress / non-error output
81
+ --out write artefact to file (JSON)
82
+ --oz-config path to a JSON OzAdapterConfig (custom OZ instance
83
+ addresses for the network); applies to both the
84
+ --mandate and --recorded-tx synthesize paths
85
+ --confidence recorder confidence GATE threshold (0..1, inclusive);
86
+ rejects when parseConfidence.overall < threshold; does
87
+ not change the recording's parseConfidence.thresholdUsed
88
+ in the output
89
+ --smart-account C... account to opt into the interpreter adapter, so
90
+ non-OZ constraints (per-method scoping, invocation-count
91
+ windows, oracle bounds, exact hop paths) lower to a real
92
+ predicate document instead of warnings
93
+ --install-nonce per-rule install nonce for the interpreter policy
94
+ (default 1); requires --smart-account
95
+ --oracle-max-staleness interpreter oracle max staleness in seconds
96
+ (positive integer <= 600, tighten-only);
97
+ requires --smart-account
98
+ --oracle-max-deviation interpreter oracle max deviation in basis points
99
+ (positive integer <= 200, tighten-only);
100
+ requires --smart-account
101
+ --window-seconds per-field override of userResponses.windowSeconds;
102
+ overrides the same field from --responses (CLI flag
103
+ wins)
104
+ --valid-until per-field override of userResponses.validUntilLedger;
105
+ overrides the same field from --responses
106
+ --limit-amount per-field override of userResponses.limitAmount
107
+ (i128 decimal string); overrides the same field
108
+ from --responses
109
+ --invocation-limit per-field override of userResponses.invocationLimit;
110
+ overrides the same field from --responses
77
111
  `);
78
112
  }
79
113
  await main();
@@ -3,7 +3,7 @@
3
3
  // `policy-builder record` subcommand. Thin wrapper around the core
4
4
  // `recordTransaction` - no business logic, just argv -> RecordInput + the
5
5
  // canonical CLI envelope.
6
- import { runRecordTransaction } from '@crediolabs/policy-builder-mcp';
6
+ import { runRecordTransaction } from '@crediolabs/policy-synth/run';
7
7
  import { CliError, formatToolResponse, parsePairs } from "../output.js";
8
8
  export async function runRecordCommand(argv, flags) {
9
9
  const pairs = parsePairs(argv);
@@ -7,8 +7,19 @@
7
7
  //
8
8
  // The CLI mirrors the MCP tool's discriminated union: one subcommand, two
9
9
  // front-ends, mutually exclusive.
10
- import { runSynthesizePolicy } from '@crediolabs/policy-builder-mcp';
10
+ //
11
+ // Per-field response flags (--window-seconds, --valid-until, --limit-amount,
12
+ // --invocation-limit) merge into `userResponses`. A flag overrides the same
13
+ // field from --responses (CLI flags are explicit; the file is a default bag).
14
+ // Oracle params (--oracle-max-staleness, --oracle-max-deviation) are part of
15
+ // the interpreter opt-in and are rejected without --smart-account; tighten-only
16
+ // bounds are validated by the core.
17
+ import { runSynthesizePolicy } from '@crediolabs/policy-synth/run';
11
18
  import { CliError, formatToolResponse, parsePairs, readJsonFile } from "../output.js";
19
+ // Positive-int flags and i128 amount strings share the same wire shape: a
20
+ // base-10 unsigned decimal, no sign. The i128 stays a string at the boundary
21
+ // because it is wider than Number.MAX_SAFE_INTEGER.
22
+ const POSITIVE_INT_RE = /^[0-9]+$/;
12
23
  export async function runSynthesizeCommand(argv, flags) {
13
24
  const pairs = parsePairs(argv);
14
25
  const hasMandate = Boolean(pairs.mandate);
@@ -23,7 +34,14 @@ export async function runSynthesizeCommand(argv, flags) {
23
34
  }
24
35
  if (hasMandate) {
25
36
  const mandate = readJsonFile(pairs.mandate);
26
- const res = await runSynthesizePolicy({ source: 'mandate', mandate });
37
+ const args = { source: 'mandate', mandate };
38
+ if (pairs['oz-config'] !== undefined) {
39
+ args.ozConfig = readOzConfigFile(pairs['oz-config']);
40
+ }
41
+ if (pairs.confidence !== undefined) {
42
+ args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
43
+ }
44
+ const res = await runSynthesizePolicy(args);
27
45
  return formatToolResponse(res, flags, 'synthesize(mandate)');
28
46
  }
29
47
  // hasRecorded
@@ -44,20 +62,189 @@ export async function runSynthesizeCommand(argv, flags) {
44
62
  });
45
63
  }
46
64
  const args = { source: 'recording', recordedTx, network };
65
+ // userResponses precedence: --responses file is the base; per-field flags
66
+ // override the same field. Only the override'd fields are merged in.
67
+ const userResponses = {};
47
68
  if (pairs.responses) {
48
- args.userResponses = readJsonFile(pairs.responses);
69
+ const file = readJsonFile(pairs.responses);
70
+ if (file !== null && typeof file === 'object' && !Array.isArray(file)) {
71
+ Object.assign(userResponses, file);
72
+ }
73
+ else {
74
+ throw new CliError({
75
+ code: 'CLI_INVALID_JSON',
76
+ message: `synthesize: --responses ${pairs.responses} must be a JSON object`,
77
+ severity: 'error',
78
+ retryable: false,
79
+ });
80
+ }
81
+ }
82
+ if (pairs['window-seconds'] !== undefined) {
83
+ userResponses.windowSeconds = parsePositiveInt(pairs['window-seconds'], '--window-seconds');
84
+ }
85
+ if (pairs['valid-until'] !== undefined) {
86
+ userResponses.validUntilLedger = parsePositiveInt(pairs['valid-until'], '--valid-until');
87
+ }
88
+ if (pairs['limit-amount'] !== undefined) {
89
+ userResponses.limitAmount = parseI128String(pairs['limit-amount'], '--limit-amount');
90
+ }
91
+ if (pairs['invocation-limit'] !== undefined) {
92
+ userResponses.invocationLimit = parsePositiveInt(pairs['invocation-limit'], '--invocation-limit');
93
+ }
94
+ if (Object.keys(userResponses).length > 0) {
95
+ args.userResponses = userResponses;
96
+ }
97
+ if (pairs['oz-config'] !== undefined) {
98
+ args.ozConfig = readOzConfigFile(pairs['oz-config']);
99
+ }
100
+ if (pairs.confidence !== undefined) {
101
+ args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
49
102
  }
50
103
  // --smart-account <C...> opts into the interpreter adapter, so constraints OZ
51
104
  // cannot express (per-method scoping, invocation-count windows, oracle bounds,
52
105
  // exact hop paths) lower to a real predicate document instead of just warnings.
53
106
  // The core validates the address and installNonce; a bad value surfaces there.
54
- if (pairs['smart-account']) {
55
- const interpreter = { smartAccountAddress: pairs['smart-account'] };
56
- if (pairs['install-nonce']) {
57
- interpreter.installNonce = Number(pairs['install-nonce']);
107
+ //
108
+ // Use `!== undefined` (not truthy) so `--smart-account ""` and `--install-nonce`
109
+ // without `--smart-account` are rejected up front instead of being silently
110
+ // dropped. The foot-gun: an empty value previously produced an "ok" envelope
111
+ // with 0 policyDocuments, so callers thought the constraint had been enforced
112
+ // when in fact it had been silently skipped.
113
+ const smartAccountRaw = pairs['smart-account'];
114
+ const installNonceRaw = pairs['install-nonce'];
115
+ const oracleStalenessRaw = pairs['oracle-max-staleness'];
116
+ const oracleDeviationRaw = pairs['oracle-max-deviation'];
117
+ if (installNonceRaw !== undefined && smartAccountRaw === undefined) {
118
+ throw new CliError({
119
+ code: 'CLI_MISSING_ARG',
120
+ message: 'synthesize: --install-nonce requires --smart-account <C...> (interpreter opt-in)',
121
+ severity: 'error',
122
+ retryable: false,
123
+ });
124
+ }
125
+ // Oracle params are an interpreter-only knob; reject up front so they cannot
126
+ // be silently dropped when --smart-account is absent.
127
+ if ((oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) &&
128
+ smartAccountRaw === undefined) {
129
+ throw new CliError({
130
+ code: 'CLI_MISSING_ARG',
131
+ message: 'synthesize: --oracle-max-staleness / --oracle-max-deviation require --smart-account <C...> (interpreter opt-in)',
132
+ severity: 'error',
133
+ retryable: false,
134
+ });
135
+ }
136
+ if (smartAccountRaw !== undefined) {
137
+ const smartAccount = smartAccountRaw.trim();
138
+ if (smartAccount.length === 0) {
139
+ throw new CliError({
140
+ code: 'CLI_MISSING_ARG',
141
+ message: 'synthesize: --smart-account <C...> was passed empty; provide a 56-character contract strkey or omit the flag',
142
+ severity: 'error',
143
+ retryable: false,
144
+ });
145
+ }
146
+ if (!/^C[2-7A-Z]{55}$/.test(smartAccount)) {
147
+ throw new CliError({
148
+ code: 'CLI_MISSING_ARG',
149
+ message: `synthesize: --smart-account "${smartAccount}" is not a valid C... contract strkey (expected 56 chars starting with C)`,
150
+ severity: 'error',
151
+ retryable: false,
152
+ });
153
+ }
154
+ const interpreter = { smartAccountAddress: smartAccount };
155
+ if (installNonceRaw !== undefined) {
156
+ const nonce = Number(installNonceRaw);
157
+ if (!Number.isInteger(nonce) || nonce < 0) {
158
+ throw new CliError({
159
+ code: 'CLI_MISSING_ARG',
160
+ message: `synthesize: --install-nonce "${installNonceRaw}" is not a non-negative integer`,
161
+ severity: 'error',
162
+ retryable: false,
163
+ });
164
+ }
165
+ interpreter.installNonce = nonce;
166
+ }
167
+ // Oracle params only attach when at least one bound was provided. The
168
+ // core validates tighten-only (maxStalenessSeconds <= 600,
169
+ // maxDeviationBps <= 200) - a too-loose value surfaces as SYNTHESIS_ERROR.
170
+ if (oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) {
171
+ const oracleParams = {};
172
+ if (oracleStalenessRaw !== undefined) {
173
+ oracleParams.maxStalenessSeconds = parsePositiveInt(oracleStalenessRaw, '--oracle-max-staleness');
174
+ }
175
+ if (oracleDeviationRaw !== undefined) {
176
+ oracleParams.maxDeviationBps = parsePositiveInt(oracleDeviationRaw, '--oracle-max-deviation');
177
+ }
178
+ interpreter.oracleParams = oracleParams;
58
179
  }
59
180
  args.interpreter = interpreter;
60
181
  }
61
182
  const res = await runSynthesizePolicy(args);
62
183
  return formatToolResponse(res, flags, 'synthesize(recording)');
63
184
  }
185
+ /** Read and validate an OzAdapterConfig JSON file. Throws CLI_FILE_NOT_FOUND /
186
+ * CLI_INVALID_JSON for filesystem / parse failures; the core's strict schema
187
+ * on `ozConfig` catches shape mismatches downstream. */
188
+ function readOzConfigFile(path) {
189
+ const value = readJsonFile(path);
190
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
191
+ throw new CliError({
192
+ code: 'CLI_INVALID_JSON',
193
+ message: `synthesize: --oz-config ${path} must be a JSON object`,
194
+ severity: 'error',
195
+ retryable: false,
196
+ });
197
+ }
198
+ return value;
199
+ }
200
+ /** Parse and validate `--confidence <n>` as a finite number in [0, 1]. A
201
+ * threshold above 1 would disable the recorder gate; reject it up front. */
202
+ function parseConfidence(raw) {
203
+ const n = Number(raw);
204
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
205
+ throw new CliError({
206
+ code: 'CLI_MISSING_ARG',
207
+ message: `synthesize: --confidence "${raw}" must be a finite number within [0, 1]`,
208
+ severity: 'error',
209
+ retryable: false,
210
+ });
211
+ }
212
+ return n;
213
+ }
214
+ /** Parse a strictly positive integer (windowSeconds, validUntilLedger,
215
+ * invocationLimit, oracleParams bounds). The core re-validates these with
216
+ * field-specific caps; the CLI just enforces "looks like an integer > 0". */
217
+ function parsePositiveInt(raw, flagName) {
218
+ if (!POSITIVE_INT_RE.test(raw)) {
219
+ throw new CliError({
220
+ code: 'CLI_MISSING_ARG',
221
+ message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
222
+ severity: 'error',
223
+ retryable: false,
224
+ });
225
+ }
226
+ const n = Number(raw);
227
+ if (!Number.isInteger(n) || n <= 0) {
228
+ throw new CliError({
229
+ code: 'CLI_MISSING_ARG',
230
+ message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
231
+ severity: 'error',
232
+ retryable: false,
233
+ });
234
+ }
235
+ return n;
236
+ }
237
+ /** Parse an i128 decimal string (positive, base 10). The synth gate, not the
238
+ * CLI, decides what to do with negatives - real recordings carry positive
239
+ * amounts on the wire for `limitAmount`. */
240
+ function parseI128String(raw, flagName) {
241
+ if (!POSITIVE_INT_RE.test(raw)) {
242
+ throw new CliError({
243
+ code: 'CLI_MISSING_ARG',
244
+ message: `synthesize: ${flagName} "${raw}" must be a positive decimal integer string (base-10 i128)`,
245
+ severity: 'error',
246
+ retryable: false,
247
+ });
248
+ }
249
+ return raw;
250
+ }
@@ -10,7 +10,14 @@ export interface CliFlags {
10
10
  export declare function parseFlags(argv: ReadonlyArray<string>): CliFlags;
11
11
  /** Resolve `--value <v>` style pairs after the subcommand name. Returns
12
12
  * an object keyed by the option name (without `--`). Throws on missing
13
- * value or duplicate keys. */
13
+ * value or duplicate keys.
14
+ *
15
+ * Note: an empty value (`--smart-account ""`) IS captured as an empty
16
+ * string so the caller can distinguish "flag omitted" from "flag passed
17
+ * empty" - a foot-gun: silently dropping empty values caused callers to
18
+ * believe the interpreter adapter was engaged when it was not. The next
19
+ * token is treated as a value iff it is present and does not start with
20
+ * `--`; tokens starting with `--` are never consumed as values. */
14
21
  export declare function parsePairs(argv: ReadonlyArray<string>): Record<string, string>;
15
22
  /** Read a JSON file, parse it, and return the value. Throws with a CLI-
16
23
  * friendly error if the file is missing or malformed. */
@@ -29,7 +29,14 @@ export function parseFlags(argv) {
29
29
  }
30
30
  /** Resolve `--value <v>` style pairs after the subcommand name. Returns
31
31
  * an object keyed by the option name (without `--`). Throws on missing
32
- * value or duplicate keys. */
32
+ * value or duplicate keys.
33
+ *
34
+ * Note: an empty value (`--smart-account ""`) IS captured as an empty
35
+ * string so the caller can distinguish "flag omitted" from "flag passed
36
+ * empty" - a foot-gun: silently dropping empty values caused callers to
37
+ * believe the interpreter adapter was engaged when it was not. The next
38
+ * token is treated as a value iff it is present and does not start with
39
+ * `--`; tokens starting with `--` are never consumed as values. */
33
40
  export function parsePairs(argv) {
34
41
  const out = {};
35
42
  for (let i = 0; i < argv.length; i++) {
@@ -41,11 +48,23 @@ export function parsePairs(argv) {
41
48
  if (key && val !== undefined)
42
49
  out[key] = val;
43
50
  }
44
- else if (a?.startsWith('--') && argv[i + 1] && !argv[i + 1]?.startsWith('--')) {
51
+ else if (a?.startsWith('--')) {
45
52
  const key = a.slice(2);
46
- const val = argv[i + 1];
47
- out[key] = val;
48
- i++;
53
+ if (!key)
54
+ continue;
55
+ const next = argv[i + 1];
56
+ // Only consume the next token if it is present AND does not look like
57
+ // another flag. Empty strings DO count as values so callers can
58
+ // distinguish "omitted" from "passed empty".
59
+ if (next !== undefined && !next.startsWith('--')) {
60
+ out[key] = next;
61
+ i++;
62
+ }
63
+ else {
64
+ // Standalone flag (no value) - record as empty string so `!== undefined`
65
+ // checks upstream can detect presence.
66
+ out[key] = '';
67
+ }
49
68
  }
50
69
  }
51
70
  return out;
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,3 @@
1
+ import type { RecordedTransaction } from '@crediolabs/policy-synth';
2
+ import { type CliFlags } from '../output.ts';
3
+ export declare function runRecordCommand(argv: ReadonlyArray<string>, flags: CliFlags): Promise<RecordedTransaction>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ // apps/policy-builder-cli/src/commands/record.ts
3
+ //
4
+ // `policy-builder record` subcommand. Thin wrapper around the core
5
+ // `recordTransaction` - no business logic, just argv -> RecordInput + the
6
+ // canonical CLI envelope.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.runRecordCommand = runRecordCommand;
9
+ const run_1 = require("@crediolabs/policy-synth/run");
10
+ const output_ts_1 = require("../output.js");
11
+ async function runRecordCommand(argv, flags) {
12
+ const pairs = (0, output_ts_1.parsePairs)(argv);
13
+ const network = pairs.network;
14
+ if (!network) {
15
+ throw new output_ts_1.CliError({
16
+ code: 'CLI_MISSING_ARG',
17
+ message: 'record: --network <mainnet|testnet> is required',
18
+ severity: 'error',
19
+ retryable: false,
20
+ });
21
+ }
22
+ if (!pairs.hash && !pairs.xdr) {
23
+ throw new output_ts_1.CliError({
24
+ code: 'CLI_MISSING_ARG',
25
+ message: 'record: exactly one of --hash <tx> or --xdr <b64> is required',
26
+ severity: 'error',
27
+ retryable: false,
28
+ });
29
+ }
30
+ if (pairs.hash && pairs.xdr) {
31
+ throw new output_ts_1.CliError({
32
+ code: 'CLI_MISSING_ARG',
33
+ message: 'record: provide exactly one of --hash or --xdr, not both',
34
+ severity: 'error',
35
+ retryable: false,
36
+ });
37
+ }
38
+ const args = { network };
39
+ if (pairs.hash)
40
+ args.hash = pairs.hash;
41
+ if (pairs.xdr)
42
+ args.xdr = pairs.xdr;
43
+ const res = await (0, run_1.runRecordTransaction)(args);
44
+ return (0, output_ts_1.formatToolResponse)(res, flags, 'record');
45
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProposedPolicy } from '@crediolabs/policy-synth';
2
+ import { type CliFlags } from '../output.ts';
3
+ export declare function runSynthesizeCommand(argv: ReadonlyArray<string>, flags: CliFlags): Promise<ProposedPolicy>;