@crediolabs/policy-builder-cli 0.1.6 → 0.1.8

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,16 @@
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>]
17
+ // [--recipient <C...|G...>]...
12
18
  // [--json] [--quiet] [--out <path>]
13
19
  //
14
20
  // The router is hand-rolled (no commander / yargs dep). It splits argv into
@@ -59,21 +65,55 @@ Usage:
59
65
  policy-builder record --network <mainnet|testnet> --hash <tx> | --xdr <b64>
60
66
  [--json] [--quiet] [--out <path>]
61
67
  policy-builder synthesize --mandate <path.json>
68
+ [--oz-config <path.json>] [--confidence <0..1>]
62
69
  [--json] [--quiet] [--out <path>]
63
70
  policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
64
71
  [--responses <path.json>]
72
+ [--oz-config <path.json>] [--confidence <0..1>]
65
73
  [--smart-account <C...>] [--install-nonce <n>]
74
+ [--oracle-max-staleness <s>] [--oracle-max-deviation <bps>]
75
+ [--window-seconds <n>] [--valid-until <ledger>]
76
+ [--limit-amount <i128str>] [--invocation-limit <n>]
77
+ [--recipient <C...|G...>]...
66
78
  [--json] [--quiet] [--out <path>]
67
79
 
68
80
  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)
81
+ --json emit machine-readable JSON on stdout
82
+ --quiet suppress progress / non-error output
83
+ --out write artefact to file (JSON)
84
+ --oz-config path to a JSON OzAdapterConfig (custom OZ instance
85
+ addresses for the network); applies to both the
86
+ --mandate and --recorded-tx synthesize paths
87
+ --confidence recorder confidence GATE threshold (0..1, inclusive);
88
+ rejects when parseConfidence.overall < threshold; does
89
+ not change the recording's parseConfidence.thresholdUsed
90
+ in the output
91
+ --smart-account C... account to opt into the interpreter adapter, so
92
+ non-OZ constraints (per-method scoping, invocation-count
93
+ windows, oracle bounds, exact hop paths) lower to a real
94
+ predicate document instead of warnings
95
+ --install-nonce per-rule install nonce for the interpreter policy
96
+ (default 1); requires --smart-account
97
+ --oracle-max-staleness interpreter oracle max staleness in seconds
98
+ (positive integer <= 600, tighten-only);
99
+ requires --smart-account
100
+ --oracle-max-deviation interpreter oracle max deviation in basis points
101
+ (positive integer <= 200, tighten-only);
102
+ requires --smart-account
103
+ --window-seconds per-field override of userResponses.windowSeconds;
104
+ overrides the same field from --responses (CLI flag
105
+ wins)
106
+ --valid-until per-field override of userResponses.validUntilLedger;
107
+ overrides the same field from --responses
108
+ --limit-amount per-field override of userResponses.limitAmount
109
+ (i128 decimal string); overrides the same field
110
+ from --responses
111
+ --invocation-limit per-field override of userResponses.invocationLimit;
112
+ overrides the same field from --responses
113
+ --recipient swap-recipient allowlist entry (C... contract or
114
+ G... wallet) for a SoroSwap swap; REPEATABLE. Absent,
115
+ the recorded recipient is pinned by default; supplying
116
+ --recipient REPLACES that pin with the given set
77
117
  `);
78
118
  }
79
119
  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);
@@ -1,3 +1,3 @@
1
- import type { ProposedPolicy } from '@crediolabs/policy-synth';
1
+ import { type ProposedPolicy } from '@crediolabs/policy-synth';
2
2
  import { type CliFlags } from '../output.ts';
3
3
  export declare function runSynthesizeCommand(argv: ReadonlyArray<string>, flags: CliFlags): Promise<ProposedPolicy>;
@@ -7,8 +7,20 @@
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 { isStellarAddress } from '@crediolabs/policy-synth';
18
+ import { runSynthesizePolicy } from '@crediolabs/policy-synth/run';
11
19
  import { CliError, formatToolResponse, parsePairs, readJsonFile } from "../output.js";
20
+ // Positive-int flags and i128 amount strings share the same wire shape: a
21
+ // base-10 unsigned decimal, no sign. The i128 stays a string at the boundary
22
+ // because it is wider than Number.MAX_SAFE_INTEGER.
23
+ const POSITIVE_INT_RE = /^[0-9]+$/;
12
24
  export async function runSynthesizeCommand(argv, flags) {
13
25
  const pairs = parsePairs(argv);
14
26
  const hasMandate = Boolean(pairs.mandate);
@@ -23,7 +35,14 @@ export async function runSynthesizeCommand(argv, flags) {
23
35
  }
24
36
  if (hasMandate) {
25
37
  const mandate = readJsonFile(pairs.mandate);
26
- const res = await runSynthesizePolicy({ source: 'mandate', mandate });
38
+ const args = { source: 'mandate', mandate };
39
+ if (pairs['oz-config'] !== undefined) {
40
+ args.ozConfig = readOzConfigFile(pairs['oz-config']);
41
+ }
42
+ if (pairs.confidence !== undefined) {
43
+ args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
44
+ }
45
+ const res = await runSynthesizePolicy(args);
27
46
  return formatToolResponse(res, flags, 'synthesize(mandate)');
28
47
  }
29
48
  // hasRecorded
@@ -44,20 +63,232 @@ export async function runSynthesizeCommand(argv, flags) {
44
63
  });
45
64
  }
46
65
  const args = { source: 'recording', recordedTx, network };
66
+ // userResponses precedence: --responses file is the base; per-field flags
67
+ // override the same field. Only the override'd fields are merged in.
68
+ const userResponses = {};
47
69
  if (pairs.responses) {
48
- args.userResponses = readJsonFile(pairs.responses);
70
+ const file = readJsonFile(pairs.responses);
71
+ if (file !== null && typeof file === 'object' && !Array.isArray(file)) {
72
+ Object.assign(userResponses, file);
73
+ }
74
+ else {
75
+ throw new CliError({
76
+ code: 'CLI_INVALID_JSON',
77
+ message: `synthesize: --responses ${pairs.responses} must be a JSON object`,
78
+ severity: 'error',
79
+ retryable: false,
80
+ });
81
+ }
82
+ }
83
+ if (pairs['window-seconds'] !== undefined) {
84
+ userResponses.windowSeconds = parsePositiveInt(pairs['window-seconds'], '--window-seconds');
85
+ }
86
+ if (pairs['valid-until'] !== undefined) {
87
+ userResponses.validUntilLedger = parsePositiveInt(pairs['valid-until'], '--valid-until');
88
+ }
89
+ if (pairs['limit-amount'] !== undefined) {
90
+ userResponses.limitAmount = parseI128String(pairs['limit-amount'], '--limit-amount');
91
+ }
92
+ if (pairs['invocation-limit'] !== undefined) {
93
+ userResponses.invocationLimit = parsePositiveInt(pairs['invocation-limit'], '--invocation-limit');
94
+ }
95
+ // --recipient <C...|G...> is REPEATABLE (parsePairs collapses duplicate keys,
96
+ // so it is collected straight from argv). Each value builds the swap-recipient
97
+ // allowlist; supplying it REPLACES the default pin to the recorded recipient.
98
+ const recipients = collectRepeated(argv, 'recipient');
99
+ if (recipients.length > 0) {
100
+ for (const r of recipients) {
101
+ // Same validator the run-layer schema applies (SDK StrKey underneath) - a
102
+ // swap recipient may be a G... wallet or a C... contract. Shared rather
103
+ // than re-inlined so the CLI and the schema cannot drift apart.
104
+ if (!isStellarAddress(r)) {
105
+ throw new CliError({
106
+ code: 'CLI_MISSING_ARG',
107
+ message: `synthesize: --recipient "${r}" is not a valid Stellar address (expected a G... wallet or C... contract)`,
108
+ severity: 'error',
109
+ retryable: false,
110
+ });
111
+ }
112
+ }
113
+ userResponses.swapRecipientAllowlist = recipients;
114
+ }
115
+ if (Object.keys(userResponses).length > 0) {
116
+ args.userResponses = userResponses;
117
+ }
118
+ if (pairs['oz-config'] !== undefined) {
119
+ args.ozConfig = readOzConfigFile(pairs['oz-config']);
120
+ }
121
+ if (pairs.confidence !== undefined) {
122
+ args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
49
123
  }
50
124
  // --smart-account <C...> opts into the interpreter adapter, so constraints OZ
51
125
  // cannot express (per-method scoping, invocation-count windows, oracle bounds,
52
126
  // exact hop paths) lower to a real predicate document instead of just warnings.
53
127
  // 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']);
128
+ //
129
+ // Use `!== undefined` (not truthy) so `--smart-account ""` and `--install-nonce`
130
+ // without `--smart-account` are rejected up front instead of being silently
131
+ // dropped. The foot-gun: an empty value previously produced an "ok" envelope
132
+ // with 0 policyDocuments, so callers thought the constraint had been enforced
133
+ // when in fact it had been silently skipped.
134
+ const smartAccountRaw = pairs['smart-account'];
135
+ const installNonceRaw = pairs['install-nonce'];
136
+ const oracleStalenessRaw = pairs['oracle-max-staleness'];
137
+ const oracleDeviationRaw = pairs['oracle-max-deviation'];
138
+ if (installNonceRaw !== undefined && smartAccountRaw === undefined) {
139
+ throw new CliError({
140
+ code: 'CLI_MISSING_ARG',
141
+ message: 'synthesize: --install-nonce requires --smart-account <C...> (interpreter opt-in)',
142
+ severity: 'error',
143
+ retryable: false,
144
+ });
145
+ }
146
+ // Oracle params are an interpreter-only knob; reject up front so they cannot
147
+ // be silently dropped when --smart-account is absent.
148
+ if ((oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) &&
149
+ smartAccountRaw === undefined) {
150
+ throw new CliError({
151
+ code: 'CLI_MISSING_ARG',
152
+ message: 'synthesize: --oracle-max-staleness / --oracle-max-deviation require --smart-account <C...> (interpreter opt-in)',
153
+ severity: 'error',
154
+ retryable: false,
155
+ });
156
+ }
157
+ if (smartAccountRaw !== undefined) {
158
+ const smartAccount = smartAccountRaw.trim();
159
+ if (smartAccount.length === 0) {
160
+ throw new CliError({
161
+ code: 'CLI_MISSING_ARG',
162
+ message: 'synthesize: --smart-account <C...> was passed empty; provide a 56-character contract strkey or omit the flag',
163
+ severity: 'error',
164
+ retryable: false,
165
+ });
166
+ }
167
+ if (!/^C[2-7A-Z]{55}$/.test(smartAccount)) {
168
+ throw new CliError({
169
+ code: 'CLI_MISSING_ARG',
170
+ message: `synthesize: --smart-account "${smartAccount}" is not a valid C... contract strkey (expected 56 chars starting with C)`,
171
+ severity: 'error',
172
+ retryable: false,
173
+ });
174
+ }
175
+ const interpreter = { smartAccountAddress: smartAccount };
176
+ if (installNonceRaw !== undefined) {
177
+ const nonce = Number(installNonceRaw);
178
+ if (!Number.isInteger(nonce) || nonce < 0) {
179
+ throw new CliError({
180
+ code: 'CLI_MISSING_ARG',
181
+ message: `synthesize: --install-nonce "${installNonceRaw}" is not a non-negative integer`,
182
+ severity: 'error',
183
+ retryable: false,
184
+ });
185
+ }
186
+ interpreter.installNonce = nonce;
187
+ }
188
+ // Oracle params only attach when at least one bound was provided. The
189
+ // core validates tighten-only (maxStalenessSeconds <= 600,
190
+ // maxDeviationBps <= 200) - a too-loose value surfaces as SYNTHESIS_ERROR.
191
+ if (oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) {
192
+ const oracleParams = {};
193
+ if (oracleStalenessRaw !== undefined) {
194
+ oracleParams.maxStalenessSeconds = parsePositiveInt(oracleStalenessRaw, '--oracle-max-staleness');
195
+ }
196
+ if (oracleDeviationRaw !== undefined) {
197
+ oracleParams.maxDeviationBps = parsePositiveInt(oracleDeviationRaw, '--oracle-max-deviation');
198
+ }
199
+ interpreter.oracleParams = oracleParams;
58
200
  }
59
201
  args.interpreter = interpreter;
60
202
  }
61
203
  const res = await runSynthesizePolicy(args);
62
204
  return formatToolResponse(res, flags, 'synthesize(recording)');
63
205
  }
206
+ /** Read and validate an OzAdapterConfig JSON file. Throws CLI_FILE_NOT_FOUND /
207
+ * CLI_INVALID_JSON for filesystem / parse failures; the core's strict schema
208
+ * on `ozConfig` catches shape mismatches downstream. */
209
+ function readOzConfigFile(path) {
210
+ const value = readJsonFile(path);
211
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
212
+ throw new CliError({
213
+ code: 'CLI_INVALID_JSON',
214
+ message: `synthesize: --oz-config ${path} must be a JSON object`,
215
+ severity: 'error',
216
+ retryable: false,
217
+ });
218
+ }
219
+ return value;
220
+ }
221
+ /** Parse and validate `--confidence <n>` as a finite number in [0, 1]. A
222
+ * threshold above 1 would disable the recorder gate; reject it up front. */
223
+ function parseConfidence(raw) {
224
+ const n = Number(raw);
225
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
226
+ throw new CliError({
227
+ code: 'CLI_MISSING_ARG',
228
+ message: `synthesize: --confidence "${raw}" must be a finite number within [0, 1]`,
229
+ severity: 'error',
230
+ retryable: false,
231
+ });
232
+ }
233
+ return n;
234
+ }
235
+ /** Parse a strictly positive integer (windowSeconds, validUntilLedger,
236
+ * invocationLimit, oracleParams bounds). The core re-validates these with
237
+ * field-specific caps; the CLI just enforces "looks like an integer > 0". */
238
+ function parsePositiveInt(raw, flagName) {
239
+ if (!POSITIVE_INT_RE.test(raw)) {
240
+ throw new CliError({
241
+ code: 'CLI_MISSING_ARG',
242
+ message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
243
+ severity: 'error',
244
+ retryable: false,
245
+ });
246
+ }
247
+ const n = Number(raw);
248
+ if (!Number.isInteger(n) || n <= 0) {
249
+ throw new CliError({
250
+ code: 'CLI_MISSING_ARG',
251
+ message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
252
+ severity: 'error',
253
+ retryable: false,
254
+ });
255
+ }
256
+ return n;
257
+ }
258
+ /** Collect ALL values for a repeatable `--<name> <value>` / `--<name>=<value>`
259
+ * flag from argv, in order. Unlike `parsePairs` (which keeps only the last
260
+ * occurrence of a key), this preserves every occurrence so a flag like
261
+ * `--recipient` can be supplied multiple times to build an allowlist. A
262
+ * trailing `--<name>` with no value (or another flag next) is skipped. */
263
+ function collectRepeated(argv, name) {
264
+ const out = [];
265
+ const eqPrefix = `--${name}=`;
266
+ for (let i = 0; i < argv.length; i++) {
267
+ const a = argv[i];
268
+ if (a === `--${name}`) {
269
+ const next = argv[i + 1];
270
+ if (next !== undefined && !next.startsWith('--')) {
271
+ out.push(next);
272
+ i++;
273
+ }
274
+ }
275
+ else if (a?.startsWith(eqPrefix)) {
276
+ out.push(a.slice(eqPrefix.length));
277
+ }
278
+ }
279
+ return out;
280
+ }
281
+ /** Parse an i128 decimal string (positive, base 10). The synth gate, not the
282
+ * CLI, decides what to do with negatives - real recordings carry positive
283
+ * amounts on the wire for `limitAmount`. */
284
+ function parseI128String(raw, flagName) {
285
+ if (!POSITIVE_INT_RE.test(raw)) {
286
+ throw new CliError({
287
+ code: 'CLI_MISSING_ARG',
288
+ message: `synthesize: ${flagName} "${raw}" must be a positive decimal integer string (base-10 i128)`,
289
+ severity: 'error',
290
+ retryable: false,
291
+ });
292
+ }
293
+ return raw;
294
+ }
@@ -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>;