@naturali/cli 0.132.0 → 0.133.1

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.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/dist/index.mjs +209 -15
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -54,9 +54,42 @@ the spec, so you never have to know.
54
54
  | Number / boolean | `--temperature 0.7`, `--force` (a bare flag is `true`) |
55
55
  | Object | `--tool-choice '{"type":"tool","name":"get_weather"}'` |
56
56
  | Array | repeat the flag (`--capabilities a --capabilities b`) or pass a JSON array (`--tool-bindings '[{"tool_id":"tool_a"}]'`) |
57
+ | From a file | `--content @playbook.md` (`@@` sends a literal leading `@`) |
57
58
 
58
59
  An array-typed field is always sent as a list, even with one value.
59
60
 
61
+ ### File values
62
+
63
+ `@path` reads the value from disk. A `.yaml`, `.yml` or `.json` file is parsed,
64
+ and a `{ "file": "..." }` inside it is replaced by that file's contents,
65
+ resolved relative to the document holding the reference:
66
+
67
+ ```yaml
68
+ # naturali/squad.formation.yaml
69
+ resources:
70
+ Playbook:
71
+ type: document
72
+ properties:
73
+ path: /squad/playbook.md
74
+ content:
75
+ file: ../intelligence/playbook.md
76
+ ```
77
+
78
+ ```bash
79
+ naturali validate-formation --project-id proj_V1StGXR8Z5jdHi6B \
80
+ --template @naturali/squad.formation.yaml
81
+ ```
82
+
83
+ That is what lets a formation template declare documents whose text is versioned
84
+ beside it, instead of carrying a pasted copy of each. Any other extension is read
85
+ as text and never parsed, so a markdown file opening with `{` stays a string.
86
+
87
+ Only a lone `{ "file": ... }` is a reference — an object with other keys beside
88
+ it is left alone, so a property genuinely named `file` is not hijacked.
89
+
90
+ A template can read any file you can. Read one you did not write before
91
+ deploying it.
92
+
60
93
  Every path parameter a command needs must be passed explicitly; a command that
61
94
  leaves one out fails and names it rather than guessing.
62
95
 
package/dist/index.mjs CHANGED
@@ -1,6 +1,9 @@
1
1
  import * as nodePath from "node:path";
2
+ import { dirname, resolve } from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { Command } from "commander";
5
+ import { readFileSync } from "node:fs";
6
+ import { parse } from "yaml";
4
7
  //#region \0rolldown/runtime.js
5
8
  var __defProp = Object.defineProperty;
6
9
  var __exportAll = (all, no_symbols) => {
@@ -14,7 +17,7 @@ var __exportAll = (all, no_symbols) => {
14
17
  };
15
18
  //#endregion
16
19
  //#region package.json
17
- var version = "0.132.0";
20
+ var version = "0.133.1";
18
21
  //#endregion
19
22
  //#region ../sdk/src/generated/core/bodySerializer.gen.ts
20
23
  const serializeFormDataPair = (data, key, value) => {
@@ -5216,13 +5219,13 @@ const parseStringFlagValue = (value) => {
5216
5219
  *
5217
5220
  * An array *of objects* — `tool_bindings` — has only the literal form:
5218
5221
  * `--tool-bindings '[{"tool_id":"tool_a"}]'`.
5222
+ *
5223
+ * Values arrive already coerced, because an item may have come from a `@file`
5224
+ * reference and must keep the type its file declared.
5219
5225
  */
5220
- const buildArrayFlagValue = (rawValues) => {
5221
- const parsed = rawValues.map((value) => {
5222
- return parseFlagValue(value);
5223
- });
5224
- if (parsed.length === 1 && Array.isArray(parsed[0])) return parsed[0];
5225
- return parsed;
5226
+ const buildArrayFlagValue = (values) => {
5227
+ if (values.length === 1 && Array.isArray(values[0])) return values[0];
5228
+ return values;
5226
5229
  };
5227
5230
  //#endregion
5228
5231
  //#region src/help.ts
@@ -5279,16 +5282,174 @@ const renderCommandHelp = (args) => {
5279
5282
  return lines.join("\n");
5280
5283
  };
5281
5284
  //#endregion
5285
+ //#region src/fileValues.ts
5286
+ /**
5287
+ * File references in flag values.
5288
+ *
5289
+ * A flag value written `@path` is read from disk instead of the command line.
5290
+ * That covers the two shapes a shell makes awkward: a long text field whose
5291
+ * content is already a file (`--content @playbook.md`), and a structured
5292
+ * document that itself names other files.
5293
+ *
5294
+ * A structured reference — `.yaml`, `.yml` or `.json` — is parsed, and any
5295
+ * `{ "file": "..." }` inside it is replaced by that file's contents, resolved
5296
+ * relative to the document holding the reference. That is what lets one
5297
+ * formation template declare documents whose text is versioned beside it,
5298
+ * rather than carrying a pasted copy of each.
5299
+ *
5300
+ * A template can read any file the caller can, as `curl -d @file` can — which
5301
+ * is the reason to read a template you did not write before deploying it.
5302
+ */
5303
+ /** How deep `{ "file": ... }` references may nest before we call it a cycle. */
5304
+ const MAX_DEPTH = 10;
5305
+ const STRUCTURED = /\.(ya?ml|json)$/i;
5306
+ /** `@@` escapes a leading `@`, for a value that really starts with one. */
5307
+ const isFileReference = (raw) => {
5308
+ return raw.startsWith("@") && !raw.startsWith("@@");
5309
+ };
5310
+ /** Strips the escape from a literal value that begins with `@@`. */
5311
+ const unescapeLeadingAt = (raw) => {
5312
+ return raw.startsWith("@@") ? raw.slice(1) : raw;
5313
+ };
5314
+ var FileValueError = class extends Error {};
5315
+ const readText = (args) => {
5316
+ const { path } = args;
5317
+ try {
5318
+ return readFileSync(path, "utf8");
5319
+ } catch (error) {
5320
+ throw new FileValueError(`cannot read ${path}: ${error instanceof Error && "code" in error && error.code === "ENOENT" ? "no such file" : error instanceof Error ? error.message : String(error)}`);
5321
+ }
5322
+ };
5323
+ const parseStructured = (args) => {
5324
+ const { path, text } = args;
5325
+ try {
5326
+ return parse(text);
5327
+ } catch (error) {
5328
+ throw new FileValueError(`cannot parse ${path}: ${error instanceof Error ? error.message : String(error)}`);
5329
+ }
5330
+ };
5331
+ /**
5332
+ * A lone `{ file: "..." }` is a reference; an object with other keys beside it
5333
+ * is not, so a property genuinely named `file` keeps its value.
5334
+ */
5335
+ const referenceTarget = (node) => {
5336
+ if (typeof node !== "object" || node === null || Array.isArray(node)) return;
5337
+ const keys = Object.keys(node);
5338
+ if (keys.length !== 1 || keys[0] !== "file") return;
5339
+ const target = node.file;
5340
+ return typeof target === "string" ? target : void 0;
5341
+ };
5342
+ /**
5343
+ * Walks a parsed document, replacing each reference with what it names. One
5344
+ * function rather than two: reading a structured file resumes the same walk on
5345
+ * its contents, so splitting it would only make the recursion mutual.
5346
+ */
5347
+ const expand = (args) => {
5348
+ const { node, baseDir, depth } = args;
5349
+ const target = referenceTarget(node);
5350
+ if (target !== void 0) {
5351
+ if (depth >= MAX_DEPTH) throw new FileValueError(`file references nested more than ${MAX_DEPTH} deep — a reference cycle?`);
5352
+ const path = resolve(baseDir, target);
5353
+ const text = readText({ path });
5354
+ if (!STRUCTURED.test(path)) return text;
5355
+ return expand({
5356
+ node: parseStructured({
5357
+ path,
5358
+ text
5359
+ }),
5360
+ baseDir: dirname(path),
5361
+ depth: depth + 1
5362
+ });
5363
+ }
5364
+ if (Array.isArray(node)) return node.map((item) => {
5365
+ return expand({
5366
+ node: item,
5367
+ baseDir,
5368
+ depth
5369
+ });
5370
+ });
5371
+ if (typeof node === "object" && node !== null) return Object.fromEntries(Object.entries(node).map(([key, value]) => {
5372
+ return [key, expand({
5373
+ node: value,
5374
+ baseDir,
5375
+ depth
5376
+ })];
5377
+ }));
5378
+ return node;
5379
+ };
5380
+ /**
5381
+ * Resolves one raw flag value.
5382
+ *
5383
+ * `fromFile` tells the caller the value already has the type its file
5384
+ * declared, so it must skip the string coercions: those exist to guess a JSON
5385
+ * value out of a shell word, and a file has nothing to guess about.
5386
+ */
5387
+ const resolveFlagValue = (args) => {
5388
+ const { raw } = args;
5389
+ if (!isFileReference(raw)) return {
5390
+ ok: true,
5391
+ value: unescapeLeadingAt(raw),
5392
+ fromFile: false
5393
+ };
5394
+ const reference = raw.slice(1);
5395
+ if (reference === "") return {
5396
+ ok: false,
5397
+ message: "\"@\" needs a path after it."
5398
+ };
5399
+ try {
5400
+ return {
5401
+ ok: true,
5402
+ value: expand({
5403
+ node: { file: reference },
5404
+ baseDir: process.cwd(),
5405
+ depth: 0
5406
+ }),
5407
+ fromFile: true
5408
+ };
5409
+ } catch (error) {
5410
+ if (error instanceof FileValueError) return {
5411
+ ok: false,
5412
+ message: error.message
5413
+ };
5414
+ throw error;
5415
+ }
5416
+ };
5417
+ //#endregion
5282
5418
  //#region src/routeCall.ts
5283
5419
  const placeFlags = (args) => {
5284
5420
  const { route, parsedFlags, call } = args;
5421
+ const fileErrors = [];
5422
+ /**
5423
+ * A value read from a file is used as the file declared it. Only a value
5424
+ * still in its shell spelling goes through `coerce`, which guesses a JSON
5425
+ * type out of a word.
5426
+ */
5427
+ const readValue = (readArgs) => {
5428
+ const { flagKey, raw, coerce } = readArgs;
5429
+ const resolved = resolveFlagValue({ raw });
5430
+ if (!resolved.ok) {
5431
+ fileErrors.push(`--${flagKey}: ${resolved.message}`);
5432
+ return;
5433
+ }
5434
+ return resolved.fromFile ? resolved.value : coerce(resolved.value);
5435
+ };
5285
5436
  const typeByCanonical = new Map(route.flags.map((flag) => {
5286
5437
  return [toCanonical(flag.name), flag.type];
5287
5438
  }));
5288
5439
  for (const [flagKey, rawValue] of Object.entries(parsedFlags.single)) {
5289
5440
  const canonical = toCanonical(flagKey);
5290
5441
  const declaredType = typeByCanonical.get(canonical);
5291
- const value = declaredType === "array" ? buildArrayFlagValue(parsedFlags.repeated[flagKey] ?? [rawValue]) : declaredType === "string" ? parseStringFlagValue(rawValue) : parseFlagValue(rawValue);
5442
+ const value = declaredType === "array" ? buildArrayFlagValue((parsedFlags.repeated[flagKey] ?? [rawValue]).map((item) => {
5443
+ return readValue({
5444
+ flagKey,
5445
+ raw: item,
5446
+ coerce: parseFlagValue
5447
+ });
5448
+ })) : readValue({
5449
+ flagKey,
5450
+ raw: rawValue,
5451
+ coerce: declaredType === "string" ? parseStringFlagValue : parseFlagValue
5452
+ });
5292
5453
  const pathParam = route.pathParams.find((param) => {
5293
5454
  return toCanonical(param) === canonical;
5294
5455
  });
@@ -5309,6 +5470,7 @@ const placeFlags = (args) => {
5309
5470
  }
5310
5471
  call.body[kebabToSnake(flagKey)] = value;
5311
5472
  }
5473
+ return fileErrors;
5312
5474
  };
5313
5475
  const PROJECT_ID_PARAM = "project_id";
5314
5476
  /**
@@ -5336,11 +5498,16 @@ const buildRouteCall = (args) => {
5336
5498
  query: {},
5337
5499
  body: {}
5338
5500
  };
5339
- placeFlags({
5501
+ const fileErrors = placeFlags({
5340
5502
  route,
5341
5503
  parsedFlags,
5342
5504
  call
5343
5505
  });
5506
+ if (fileErrors.length > 0) return {
5507
+ ok: false,
5508
+ kind: "file_errors",
5509
+ fileErrors
5510
+ };
5344
5511
  applyProjectIdEnvDefault({
5345
5512
  route,
5346
5513
  call
@@ -5350,6 +5517,7 @@ const buildRouteCall = (args) => {
5350
5517
  });
5351
5518
  if (missingPathParams.length > 0) return {
5352
5519
  ok: false,
5520
+ kind: "missing_path_params",
5353
5521
  missingPathParams
5354
5522
  };
5355
5523
  return {
@@ -5494,6 +5662,10 @@ const dispatchCommand = async (args) => {
5494
5662
  route,
5495
5663
  parsedFlags
5496
5664
  });
5665
+ if (!built.ok && built.kind === "file_errors") {
5666
+ console.error(built.fileErrors.join("\n"));
5667
+ process.exit(1);
5668
+ }
5497
5669
  if (!built.ok) {
5498
5670
  const flagsHint = built.missingPathParams.map((param) => {
5499
5671
  return `--${param.replace(/_/g, "-")} <value>`;
@@ -5828,7 +6000,7 @@ const routes = {
5828
6000
  },
5829
6001
  {
5830
6002
  "name": "tags",
5831
- "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search.",
6003
+ "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search.\n\nKeys beginning `system.` are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag.",
5832
6004
  "required": false,
5833
6005
  "type": "object",
5834
6006
  "in": "body"
@@ -9320,6 +9492,13 @@ const routes = {
9320
9492
  "required": false,
9321
9493
  "type": "string",
9322
9494
  "in": "body"
9495
+ },
9496
+ {
9497
+ "name": "retrieval",
9498
+ "description": "Whether this conversation's turns are embedded for vector retrieval. Turns are stored and chunked either way, so `none` leaves them readable and reachable by full-text search without paying for an embedding. `null` (the default) inherits the project's `default_conversation_retrieval`.",
9499
+ "required": false,
9500
+ "type": "string",
9501
+ "in": "body"
9323
9502
  }
9324
9503
  ]
9325
9504
  },
@@ -9385,6 +9564,13 @@ const routes = {
9385
9564
  "required": false,
9386
9565
  "type": "string",
9387
9566
  "in": "body"
9567
+ },
9568
+ {
9569
+ "name": "retrieval",
9570
+ "description": "Whether this conversation's turns are embedded for vector retrieval. Turns are stored and chunked either way, so `none` leaves them readable and reachable by full-text search without paying for an embedding. `null` (the default) inherits the project's `default_conversation_retrieval`. Switching it on embeds the turns already in the conversation, so the whole conversation becomes retrievable rather than only what is said next.",
9571
+ "required": false,
9572
+ "type": "string",
9573
+ "in": "body"
9388
9574
  }
9389
9575
  ]
9390
9576
  },
@@ -9767,7 +9953,7 @@ const routes = {
9767
9953
  },
9768
9954
  {
9769
9955
  "name": "path",
9770
- "description": "Logical path within the project (e.g. /reports/q1.txt). Defaults to /filename if omitted.",
9956
+ "description": "Logical path within the project (e.g. /reports/q1.txt). Defaults to `/<filename>`, or to `/<document_id>.txt` when neither is given — a document with no path is reachable only by its id, since a prefix filter never matches null.",
9771
9957
  "required": false,
9772
9958
  "type": "string",
9773
9959
  "in": "body"
@@ -9795,7 +9981,7 @@ const routes = {
9795
9981
  },
9796
9982
  {
9797
9983
  "name": "tags",
9798
- "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search.",
9984
+ "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search.\n\nKeys beginning `system.` are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag.",
9799
9985
  "required": false,
9800
9986
  "type": "object",
9801
9987
  "in": "body"
@@ -9864,7 +10050,7 @@ const routes = {
9864
10050
  },
9865
10051
  {
9866
10052
  "name": "tags",
9867
- "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search.",
10053
+ "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search.\n\nKeys beginning `system.` are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag.",
9868
10054
  "required": false,
9869
10055
  "type": "object",
9870
10056
  "in": "body"
@@ -9971,7 +10157,7 @@ const routes = {
9971
10157
  },
9972
10158
  {
9973
10159
  "name": "tags",
9974
- "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search.",
10160
+ "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search.\n\nKeys beginning `system.` are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag.",
9975
10161
  "required": false,
9976
10162
  "type": "object",
9977
10163
  "in": "body"
@@ -11159,6 +11345,7 @@ const routes = {
11159
11345
  httpMethod: "get",
11160
11346
  pathParams: ["project_id"],
11161
11347
  queryParams: [
11348
+ "path_prefix",
11162
11349
  "tags",
11163
11350
  "limit",
11164
11351
  "offset"
@@ -11173,6 +11360,13 @@ const routes = {
11173
11360
  "type": "string",
11174
11361
  "in": "path"
11175
11362
  },
11363
+ {
11364
+ "name": "path_prefix",
11365
+ "description": "Only files under this directory. The prefix is a path boundary, not a substring: `/reports` returns `/reports/q1.txt` and never `/reports-archive/q1.txt`, and `/` selects the whole project. A leading slash is optional and a trailing one is ignored, so `reports`, `/reports` and `/reports/` are the same filter. `%` and `_` are literal characters, not wildcards. Naming a directory under `/.system/` is what includes platform-written files, which a list without this parameter leaves out.",
11366
+ "required": false,
11367
+ "type": "string",
11368
+ "in": "query"
11369
+ },
11176
11370
  {
11177
11371
  "name": "tags",
11178
11372
  "description": "Filter by tag pairs, written `key:value` (split on the first colon, so a value may contain colons). Repeat the parameter for several pairs; **all** must be present with exactly that value.\n",
@@ -16290,7 +16484,7 @@ const routes = {
16290
16484
  },
16291
16485
  {
16292
16486
  "name": "tags",
16293
- "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search.",
16487
+ "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search.\n\nKeys beginning `system.` are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag.",
16294
16488
  "required": false,
16295
16489
  "type": "object",
16296
16490
  "in": "body"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturali/cli",
3
- "version": "0.132.0",
3
+ "version": "0.133.1",
4
4
  "description": "Command-line interface for the naturali.ai API, generated from its OpenAPI specs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,10 +21,11 @@
21
21
  "access": "public"
22
22
  },
23
23
  "dependencies": {
24
- "commander": "^15.0.0"
24
+ "commander": "^15.0.0",
25
+ "yaml": "^2.9.1"
25
26
  },
26
27
  "devDependencies": {
27
- "@naturali/sdk": "0.132.0",
28
+ "@naturali/sdk": "0.133.1",
28
29
  "@ttoss/openapi-codegen": "^0.3.1",
29
30
  "@types/node": "^26.5.1",
30
31
  "tsdown": "^0.23.0",