@vxil/cli 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,7 +52,7 @@ Verbs: `init templates quickstart try login link keys account logout invites mem
52
52
  - `vxil doctor` — preflight: secrets referenced-vs-stored (advisory, names only) plus a served / published / local hash verdict per function. Under `--json` stdout is one JSON document; notices go to stderr.
53
53
  - `vxil link <slug>` — reuses the key already stored for the slug on this machine; `--mint` for a fresh one, `--key` to hand one in.
54
54
  - `vxil keys mint --name <name> --scopes <a,b,c> [--env live|test] [--store]` — mint a **narrow** key for a server (scopes validated locally; `admin` refused); the key prints **once** to stdout (`--json` → `{ api_key, key_id, scopes }`) and is never stored unless `--store`. `vxil keys list` shows names, ids, scopes, created and last-used (never secrets); `vxil keys revoke <key_id> [--yes]` asks you to type the id, and refuses the key the CLI itself is using unless `--yes`. Needs a dashboard session (`vxil login`, or it prompts at a terminal; a piped / `--json` run fails with that remedy instead — pass `--email`/`--password` to script it, which never stores the session).
55
- - `vxil cms drop <collection> [--yes]` — remove one collection definition through the same guarded ack `push --allow-destructive` uses (rows are retained).
55
+ - `vxil cms drop <collection> [--purge-rows] [--yes]` — remove one collection definition through the same guarded ack `push --allow-destructive` uses (rows are retained, and a re-create brings them back; `--purge-rows` hard-deletes them too, and on a collection dropped earlier deletes its retained rows).
56
56
  - `vxil members list|invite|add|rm|uninvite` and `vxil invites accept <token|url>` — the bound project's team from the terminal (your `vxil login` session): invite by e-mail with a role, add an existing account (also how you change a role), remove (typed e-mail, refuses the last owner), revoke a pending invitation, accept one.
57
57
  - `vxil projects list|create|rm` and `vxil billing status|upgrade --tier <t>` — your projects and the bound project's plan. `projects rm` deletes any project you own after you type its slug; `billing upgrade` prints the hosted checkout URL (never opens a browser) and also downgrades (`--tier free`).
58
58
  - `vxil files put <path> --user <id>`, `vxil webhooks sources add|list|rm|test`, `vxil webhooks events replay|runs`, `vxil cms bulk-rm <collection> --filter '<json>' [--dry-run]`, `vxil search sync run|reset <key> [--purge]`, `vxil users import <file>` — the operator actions where the CLI adds something a raw call cannot (bytes from disk, a print-once receiver URL, a dry run before a typed confirm, a chunked bulk). Everything else the dashboard does per feature is one `vxil api` line: `vxil api --help`.
@@ -105,6 +105,7 @@ export declare const NotificationsConfigSchema: import("@sinclair/typebox").TObj
105
105
  html: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
106
106
  text: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
107
107
  }>>>>;
108
+ retainSentRender: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
108
109
  }>>;
109
110
  /** in-app inbox channel (send with channel: 'inbox' | 'both') */
110
111
  inboxEnabled: import("@sinclair/typebox").TBoolean;
@@ -616,6 +617,13 @@ export declare const DeclaredSearchCollectionSchema: import("@sinclair/typebox")
616
617
  orgScoped: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
617
618
  }>;
618
619
  export type DeclaredSearchCollection = Static<typeof DeclaredSearchCollectionSchema>;
620
+ /** VECTOR-HYBRID-LANE-WEIGHTS: the two RRF lane weights (weighted RRF —
621
+ * each lane's 1/(k+rank) is scaled by its weight). 0..100 each; a request
622
+ * with both 0 is refused at query time. */
623
+ export declare const HybridLaneWeights: import("@sinclair/typebox").TObject<{
624
+ vector: import("@sinclair/typebox").TNumber;
625
+ keyword: import("@sinclair/typebox").TNumber;
626
+ }>;
619
627
  export declare const VectorSearchConfigSchema: import("@sinclair/typebox").TObject<{
620
628
  enabled: import("@sinclair/typebox").TBoolean;
621
629
  backend: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"auto">, import("@sinclair/typebox").TLiteral<"lakebase">, import("@sinclair/typebox").TLiteral<"pgvector">]>;
@@ -629,10 +637,14 @@ export declare const VectorSearchConfigSchema: import("@sinclair/typebox").TObje
629
637
  maxTokens: import("@sinclair/typebox").TInteger;
630
638
  overlap: import("@sinclair/typebox").TInteger;
631
639
  }>;
632
- hybrid: import("@sinclair/typebox").TObject<{
640
+ hybrid: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
633
641
  defaultMode: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"hybrid">, import("@sinclair/typebox").TLiteral<"vector">, import("@sinclair/typebox").TLiteral<"keyword">]>;
634
642
  rrfK: import("@sinclair/typebox").TInteger;
635
- }>;
643
+ weights: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
644
+ vector: import("@sinclair/typebox").TNumber;
645
+ keyword: import("@sinclair/typebox").TNumber;
646
+ }>>;
647
+ }>>;
636
648
  limits: import("@sinclair/typebox").TObject<{
637
649
  maxDocsPerCollection: import("@sinclair/typebox").TInteger;
638
650
  maxChunksPerDoc: import("@sinclair/typebox").TInteger;
@@ -673,6 +685,11 @@ export declare const DeclaredAiTemplateSchema: import("@sinclair/typebox").TObje
673
685
  user: import("@sinclair/typebox").TString;
674
686
  system: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
675
687
  schema: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
688
+ /** MX-1023-MEDIA-RESOLUTION: this template's Gemini media resolution (sent
689
+ * to ai-v1 as `media_resolution`). A request's media_resolution wins; absent
690
+ * falls back to defaults.mediaResolution. Part of the content hash only
691
+ * when set, so an existing declared template is not re-versioned. */
692
+ mediaResolution: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"low">, import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"high">]>>;
676
693
  }>;
677
694
  export type DeclaredAiTemplate = Static<typeof DeclaredAiTemplateSchema>;
678
695
  export declare const AiConfigSchema: import("@sinclair/typebox").TObject<{
@@ -692,6 +709,7 @@ export declare const AiConfigSchema: import("@sinclair/typebox").TObject<{
692
709
  maxTokens: import("@sinclair/typebox").TInteger;
693
710
  temperature: import("@sinclair/typebox").TNumber;
694
711
  thinkingLevel: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"minimal" | "low" | "medium" | "high">[]>>;
712
+ mediaResolution: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"low">, import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"high">]>>;
695
713
  }>>;
696
714
  cache: import("@sinclair/typebox").TObject<{
697
715
  ttlSeconds: import("@sinclair/typebox").TInteger;
@@ -710,6 +728,11 @@ export declare const AiConfigSchema: import("@sinclair/typebox").TObject<{
710
728
  user: import("@sinclair/typebox").TString;
711
729
  system: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
712
730
  schema: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
731
+ /** MX-1023-MEDIA-RESOLUTION: this template's Gemini media resolution (sent
732
+ * to ai-v1 as `media_resolution`). A request's media_resolution wins; absent
733
+ * falls back to defaults.mediaResolution. Part of the content hash only
734
+ * when set, so an existing declared template is not re-versioned. */
735
+ mediaResolution: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"low">, import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"high">]>>;
713
736
  }>>>;
714
737
  testMarkers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
715
738
  }>;
@@ -1025,7 +1048,14 @@ export declare function isDeclaredRetainDays(v: unknown): v is number | null | u
1025
1048
  export declare function retainDaysShortens(declared: number | null, remote: number | null): boolean;
1026
1049
  /** = cms-v1 core.ts DELETED_RETAIN_DAYS_MAX (a parity test pins the two). */
1027
1050
  export declare const CMS_DELETED_RETAIN_DAYS_MAX = 29;
1051
+ /** = cms-v1 core.ts DELETED_RETAIN_DAYS_DEFAULT: the platform window. Writing
1052
+ * it explicitly is accepted and stored as null (MX-1023-SMALL — the
1053
+ * documented default must never be a 422). */
1054
+ export declare const CMS_DELETED_RETAIN_DAYS_DEFAULT = 30;
1028
1055
  export declare function isDeclaredDeletedRetainDays(v: unknown): v is number | null | undefined;
1056
+ /** The stored form of a declared window: the explicit default (30) is null,
1057
+ * so a push declaring 30 against a null remote plans nothing. */
1058
+ export declare function normalizeDeletedRetainDays<T extends number | null | undefined>(v: T): T | null;
1029
1059
  export declare function isDeclaredOnOwnerErase(v: unknown): v is 'delete' | 'keep' | undefined;
1030
1060
  /** Does moving from `remote` to `declared` make an erase delete MORE (turning
1031
1061
  * 'delete' on)? On an existing collection that applies only under
package/dist/config.d.ts CHANGED
@@ -143,7 +143,7 @@ export interface CollectionDef {
143
143
  /** Recovery window for DELETED items, in days (guide ch. 4): a soft-deleted
144
144
  * item stays restorable this long before the nightly cleanup purges it for
145
145
  * good. An integer 0..29 (0 = purged at the next nightly run, ≤ 24 h), or
146
- * `null` for the platform default of 30 days. ABSENT = not managed by
146
+ * `null` (or `30`, stored as null) for the platform default of 30 days. ABSENT = not managed by
147
147
  * config. Shortening it on an existing collection needs
148
148
  * `vxil push --allow-destructive`. Carried by BOTH push paths. */
149
149
  deletedRetainDays?: number | null;
package/dist/vxil-main.js CHANGED
@@ -3699,7 +3699,14 @@ var NotificationsConfigSchema = Type.Object({
3699
3699
  html: Type.Optional(Type.String({ maxLength: NOTIF_OVERRIDE_BODY_MAX })),
3700
3700
  text: Type.Optional(Type.String({ maxLength: NOTIF_OVERRIDE_BODY_MAX }))
3701
3701
  }))
3702
- ))
3702
+ )),
3703
+ // CVS-1022-1 (2026-10-23): keep the render of a REAL send on its delivery
3704
+ // row for 7 days (GET /v1/notifications/deliveries/{id}, rendered_from
3705
+ // send | send_redacted). Opt-in, no default (absent = off, manifests stay
3706
+ // byte-identical); auth-class mail is kept WITHOUT its data; a render
3707
+ // over 64 KiB is dropped (row kept). Inside the ONE optional templates
3708
+ // leaf — the schema is at the 15-leaf cap.
3709
+ retainSentRender: Type.Optional(Type.Boolean())
3703
3710
  },
3704
3711
  { default: {} }
3705
3712
  )),
@@ -4598,6 +4605,10 @@ var DeclaredSearchCollectionSchema = Type.Object({
4598
4605
  // filtered to the active org; immutable after create (inside the array leaf).
4599
4606
  orgScoped: Type.Optional(Type.Boolean())
4600
4607
  });
4608
+ var HybridLaneWeights = Type.Object({
4609
+ vector: Type.Number({ minimum: 0, maximum: 100 }),
4610
+ keyword: Type.Number({ minimum: 0, maximum: 100 })
4611
+ }, { additionalProperties: false });
4601
4612
  var VectorSearchConfigSchema = Type.Object({
4602
4613
  enabled: Type.Boolean({ default: true }),
4603
4614
  // 'auto' resolves to the default managed vector backend for the tier.
@@ -4633,16 +4644,22 @@ var VectorSearchConfigSchema = Type.Object({
4633
4644
  },
4634
4645
  { default: {} }
4635
4646
  ),
4636
- hybrid: Type.Object(
4647
+ // The hybrid-fusion knobs — ONE optional bag (VECTOR-HYBRID-LANE-WEIGHTS
4648
+ // folded it from 2 leaves to 1, so `weights` rides it at no leaf cost);
4649
+ // `{ default: {} }` keeps defaultMode / rrfK filled exactly as before.
4650
+ hybrid: Type.Optional(Type.Object(
4637
4651
  {
4638
4652
  defaultMode: Type.Union(
4639
4653
  [Type.Literal("hybrid"), Type.Literal("vector"), Type.Literal("keyword")],
4640
4654
  { default: "hybrid" }
4641
4655
  ),
4642
- rrfK: Type.Integer({ default: 60, minimum: 1, maximum: 1e3 })
4656
+ rrfK: Type.Integer({ default: 60, minimum: 1, maximum: 1e3 }),
4657
+ // Per-lane RRF weights for hybrid queries (absent = 1 / 1, today's
4658
+ // fusion); a request's `weights` overrides. At least one must be > 0.
4659
+ weights: Type.Optional(HybridLaneWeights)
4643
4660
  },
4644
4661
  { default: {} }
4645
- ),
4662
+ )),
4646
4663
  limits: Type.Object(
4647
4664
  {
4648
4665
  maxDocsPerCollection: Type.Integer({ default: 1e5, minimum: 1 }),
@@ -4700,7 +4717,12 @@ var DeclaredAiTemplateSchema = Type.Object({
4700
4717
  template: Type.String({ minLength: 1, maxLength: 128, pattern: AI_TEMPLATE_NAME_PATTERN }),
4701
4718
  user: Type.String({ minLength: 1, maxLength: 64e3 }),
4702
4719
  system: Type.Optional(Type.String({ maxLength: 32e3 })),
4703
- schema: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
4720
+ schema: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
4721
+ /** MX-1023-MEDIA-RESOLUTION: this template's Gemini media resolution (sent
4722
+ * to ai-v1 as `media_resolution`). A request's media_resolution wins; absent
4723
+ * falls back to defaults.mediaResolution. Part of the content hash only
4724
+ * when set, so an existing declared template is not re-versioned. */
4725
+ mediaResolution: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")]))
4704
4726
  });
4705
4727
  var AiConfigSchema = Type.Object({
4706
4728
  enabled: Type.Boolean({ default: true }),
@@ -4750,7 +4772,11 @@ var AiConfigSchema = Type.Object({
4750
4772
  model: Type.String({ default: "mock-1" }),
4751
4773
  maxTokens: Type.Integer({ default: 1024, minimum: 1, maximum: 2e5 }),
4752
4774
  temperature: Type.Number({ default: 0.7, minimum: 0, maximum: 2 }),
4753
- thinkingLevel: Type.Optional(Type.Union(AI_THINKING_LEVELS.map((l) => Type.Literal(l))))
4775
+ thinkingLevel: Type.Optional(Type.Union(AI_THINKING_LEVELS.map((l) => Type.Literal(l)))),
4776
+ // MX-1023-MEDIA-RESOLUTION (2026-10-23): Gemini's per-image / per-page
4777
+ // token budget, sent to Gemini only (ignored elsewhere). No default, so an
4778
+ // existing manifest stays byte-identical; inside the bag, so no new leaf.
4779
+ mediaResolution: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")]))
4754
4780
  }, { default: {} })),
4755
4781
  cache: Type.Object({ ttlSeconds: Type.Integer({ default: 0, minimum: 0 }) }, { default: {} }),
4756
4782
  // 0 = off
@@ -4781,8 +4807,9 @@ var AiConfigSchema = Type.Object({
4781
4807
  // aiTemplateContentSha256) against the `content_sha256` GET /v1/ai/templates
4782
4808
  // stamps on the latest version, and POSTs a new version ONLY when the content
4783
4809
  // differs — so a push is idempotent and versions stay monotonic per name.
4784
- // Item shape mirrors ai-v1 core.ts TemplateBody exactly (`template` is the
4785
- // name). Stored templates the config does not declare are reported and left
4810
+ // Item shape mirrors ai-v1 core.ts TemplateBody (`template` is the name;
4811
+ // `mediaResolution` is sent as the body's `media_resolution`).
4812
+ // Stored templates the config does not declare are reported and left
4786
4813
  // in place — RETIRED (soft: hidden from list + render, history kept) only
4787
4814
  // under --allow-destructive (2026-10-01). Bounded to 50 entries: the
4788
4815
  // manifest rides the 1 MiB config body cap. An Optional ARRAY is ONE leaf.
@@ -5412,8 +5439,12 @@ function retainDaysShortens(declared, remote) {
5412
5439
  return remote === null || declared < remote;
5413
5440
  }
5414
5441
  var CMS_DELETED_RETAIN_DAYS_MAX = 29;
5442
+ var CMS_DELETED_RETAIN_DAYS_DEFAULT = 30;
5415
5443
  function isDeclaredDeletedRetainDays(v) {
5416
- return v === void 0 || v === null || typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= CMS_DELETED_RETAIN_DAYS_MAX;
5444
+ return v === void 0 || v === null || typeof v === "number" && Number.isInteger(v) && v >= 0 && (v <= CMS_DELETED_RETAIN_DAYS_MAX || v === CMS_DELETED_RETAIN_DAYS_DEFAULT);
5445
+ }
5446
+ function normalizeDeletedRetainDays(v) {
5447
+ return v === CMS_DELETED_RETAIN_DAYS_DEFAULT ? null : v;
5417
5448
  }
5418
5449
  function isDeclaredOnOwnerErase(v) {
5419
5450
  return v === void 0 || v === "delete" || v === "keep";
@@ -5641,9 +5672,9 @@ function lowerAllTriggerBindings(def, name) {
5641
5672
  return bindings;
5642
5673
  }
5643
5674
  var CONFIG_FILENAMES = ["vxil.config.ts", "vxil.config.mjs", "vxil.config.js"];
5644
- var VXIL_CONFIG_PKG_VERSION = "0.21.0";
5645
- var VXIL_SDK_PKG_VERSION = "0.28.0";
5646
- var VXIL_CLI_PKG_VERSION = "0.29.0";
5675
+ var VXIL_CONFIG_PKG_VERSION = "0.23.0";
5676
+ var VXIL_SDK_PKG_VERSION = "0.30.0";
5677
+ var VXIL_CLI_PKG_VERSION = "0.31.0";
5647
5678
  function ensureScaffoldPackageJson(cwd, opts = {}) {
5648
5679
  const file = resolve(cwd, "package.json");
5649
5680
  const wanted = {
@@ -8544,6 +8575,7 @@ async function aiTemplateContentSha256(t) {
8544
8575
  return Object.fromEntries(Object.keys(o).sort().map((k) => [k, canon(o[k], depth + 1)]));
8545
8576
  };
8546
8577
  return sha256Hex(JSON.stringify({
8578
+ ...t.mediaResolution ? { media_resolution: t.mediaResolution } : {},
8547
8579
  schema: canon(t.schema ?? null, 1),
8548
8580
  system: t.system ?? null,
8549
8581
  user: t.user
@@ -8989,7 +9021,7 @@ async function readOnlyPlan(api, block, allowDestructive) {
8989
9021
  } else {
8990
9022
  const hashed = await Promise.all(manifest.templates.map(async (t) => ({
8991
9023
  template: t.template,
8992
- content_sha256: await aiTemplateContentSha256({ system: t.system ?? null, user: t.user, schema: t.schema ?? null })
9024
+ content_sha256: await aiTemplateContentSha256({ system: t.system ?? null, user: t.user, schema: t.schema ?? null, mediaResolution: t.mediaResolution ?? null })
8993
9025
  })));
8994
9026
  const plan = planAiTemplates(hashed, listOf(data2.templates));
8995
9027
  s.created.push(...plan.create);
@@ -9846,7 +9878,7 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
9846
9878
  }
9847
9879
  const drd = def.deletedRetainDays;
9848
9880
  if (!isDeclaredDeletedRetainDays(drd)) {
9849
- throw new Error(`cms.collections.${name}.deletedRetainDays must be an integer 0..29 (days) or null (got ${JSON.stringify(drd)})`);
9881
+ throw new Error(`cms.collections.${name}.deletedRetainDays must be an integer 0..29 (days), or 30 / null for the default (got ${JSON.stringify(drd)})`);
9850
9882
  }
9851
9883
  const ooe = def.onOwnerErase;
9852
9884
  if (!isDeclaredOnOwnerErase(ooe)) {
@@ -10089,7 +10121,7 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
10089
10121
  });
10090
10122
  }
10091
10123
  }
10092
- const declaredWindow = def.deletedRetainDays;
10124
+ const declaredWindow = normalizeDeletedRetainDays(def.deletedRetainDays);
10093
10125
  if (declaredWindow !== void 0 && declaredWindow !== rc.deletedRetainDays) {
10094
10126
  const fmtWin = (d) => d === null ? "30 days (default)" : `${d} days`;
10095
10127
  const shortens = retainDaysShortens(declaredWindow, rc.deletedRetainDays);
@@ -10195,7 +10227,8 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
10195
10227
  const orgRoles = writeRolesOf(def.orgReadRoles);
10196
10228
  if (orgRoles.length > 0) body.org_read_roles = orgRoles;
10197
10229
  if (typeof def.retainDays === "number") body.retain_days = def.retainDays;
10198
- if (typeof def.deletedRetainDays === "number") body.deleted_retain_days = def.deletedRetainDays;
10230
+ const createWindow = normalizeDeletedRetainDays(def.deletedRetainDays);
10231
+ if (typeof createWindow === "number") body.deleted_retain_days = createWindow;
10199
10232
  if (def.onOwnerErase === "delete") body.on_owner_erase = "delete";
10200
10233
  if (def.actions && def.actions.length > 0) body.actions = normalizeActions(def.actions);
10201
10234
  const res = await api("POST", "/v1/cms/collections", body);
@@ -10280,7 +10313,7 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
10280
10313
  applied++;
10281
10314
  } else if (c.kind === "set-deleted-retain-days" || c.kind === "set-on-owner-erase") {
10282
10315
  const def = collections[c.collection];
10283
- const body = c.kind === "set-deleted-retain-days" ? { deleted_retain_days: def.deletedRetainDays ?? null } : { on_owner_erase: def.onOwnerErase ?? "keep" };
10316
+ const body = c.kind === "set-deleted-retain-days" ? { deleted_retain_days: normalizeDeletedRetainDays(def.deletedRetainDays) ?? null } : { on_owner_erase: def.onOwnerErase ?? "keep" };
10284
10317
  const res = await api("POST", `/v1/cms/collections/${encodeURIComponent(c.collection)}/fields`, body);
10285
10318
  if (res.status !== 200 && res.status !== 201) {
10286
10319
  const e = res.body.error ?? {};
@@ -10370,14 +10403,43 @@ async function dropCollection(api, collection) {
10370
10403
  throw new Error(`cms: drop collection ${collection} failed: ${e.code ?? res.status} ${e.message ?? ""}`);
10371
10404
  }
10372
10405
  }
10373
- function formatCollectionDrop(collection, plan, declaredLocally) {
10406
+ async function dropCollectionPurgingRows(api, collection) {
10407
+ let purgedRows = 0;
10408
+ for (let round = 0; ; round++) {
10409
+ if (round > 0) {
10410
+ const read = await readRemote2(api);
10411
+ if (read.unavailable) {
10412
+ throw new Error(`cms: purge of ${collection} stopped after ${purgedRows} purged row(s): cannot read ${read.unavailable.route}`);
10413
+ }
10414
+ if (read.map.has(collection)) {
10415
+ throw new Error(`cms: purge of ${collection} stopped after ${purgedRows} purged row(s): the collection was re-created meanwhile \u2014 its rows are kept`);
10416
+ }
10417
+ }
10418
+ const res = await api("POST", "/v1/cms/collections", { drop_collection: collection, allow_destructive: true, purge_rows: true });
10419
+ if (res.body.error?.code === "not_found") return { purgedRows };
10420
+ if (res.status < 200 || res.status >= 300) {
10421
+ const e = res.body.error ?? {};
10422
+ throw new Error(`cms: drop collection ${collection} failed after ${purgedRows} purged row(s): ${e.code ?? res.status} ${e.message ?? ""}`);
10423
+ }
10424
+ const d = res.body.data ?? {};
10425
+ const n = typeof d.purged_rows === "number" ? d.purged_rows : 0;
10426
+ purgedRows += n;
10427
+ if (d.purge_complete === true) return { purgedRows };
10428
+ if (n === 0) throw new Error(`cms: purge of ${collection} made no progress after ${purgedRows} purged row(s) \u2014 re-run \`vxil cms drop ${collection} --purge-rows\``);
10429
+ }
10430
+ }
10431
+ function formatCollectionDrop(collection, plan, declaredLocally, purgeRows = false) {
10374
10432
  const rows = plan.rows === null ? "an unknown number of live rows" : `${plan.rows} live row(s)`;
10375
- const lines = [
10376
- ` ! drop collection ${collection} (${plan.fields} field(s), ${rows}) \u2014 the definition is removed; stored item rows are NOT deleted (they become unreachable through the API until a collection of that name exists again)`
10433
+ const lines = !plan.exists && purgeRows ? [` ! collection ${collection} has no definition \u2014 its retained rows from an earlier drop (live and deleted) are HARD-DELETED; they cannot be restored`] : [
10434
+ ` ! drop collection ${collection} (${plan.fields} field(s), ${rows}) \u2014 the definition is removed; ` + (purgeRows ? "its stored item rows (live and deleted) are HARD-DELETED with it; they cannot be restored" : "stored item rows are NOT deleted (they become unreachable through the API until a collection of that name exists again)")
10377
10435
  ];
10378
10436
  if (declaredLocally) {
10379
- const back = plan.rows === null ? "the retained rows" : `the ${plan.rows} retained row(s)`;
10380
- lines.push(` \u26A0 '${collection}' is still declared in vxil.config.ts \u2014 the next \`vxil push\` re-creates it and ${back} come back with it; remove it from the config too`);
10437
+ if (purgeRows) {
10438
+ lines.push(` \u26A0 '${collection}' is still declared in vxil.config.ts \u2014 the next \`vxil push\` re-creates it empty; remove it from the config too`);
10439
+ } else {
10440
+ const back = plan.rows === null ? "the retained rows" : `the ${plan.rows} retained row(s)`;
10441
+ lines.push(` \u26A0 '${collection}' is still declared in vxil.config.ts \u2014 the next \`vxil push\` re-creates it and ${back} come back with it; remove it from the config too`);
10442
+ }
10381
10443
  }
10382
10444
  return lines.join("\n");
10383
10445
  }
@@ -14852,6 +14914,13 @@ var TOOLS = [
14852
14914
  mode: { type: "string", enum: ["hybrid", "vector", "keyword"], default: "hybrid" },
14853
14915
  filter: { type: "object", additionalProperties: true, description: 'Metadata filter, keys ANDed: equality {"user_id":"u_42"} or any-of {"folder":{"$in":["a","b"]}} (1\u201350 values, \u22644 $in keys). null / arrays / other operators are refused 422.' },
14854
14916
  rerank: { type: "boolean", description: "Apply the configured BYO reranker (config rerank block) to the fused results before top-k. Fail-open: a provider fault returns the RRF order with reranked:false." },
14917
+ weights: {
14918
+ type: "object",
14919
+ additionalProperties: false,
14920
+ required: ["vector", "keyword"],
14921
+ properties: { vector: { type: "number", minimum: 0, maximum: 100 }, keyword: { type: "number", minimum: 0, maximum: 100 } },
14922
+ description: `Hybrid mode only: per-lane RRF weights, e.g. {"vector":0.8,"keyword":0.2} to favour meaning over exact words. Not both 0. Default: the config's hybrid.weights, else equal.`
14923
+ },
14855
14924
  org_id: { type: "string", description: "Workspace-scoped (org_scoped) collections only: the org to search. Required with a server key (422 org_id_required without); an end-user session always searches its active workspace. Refused 422 on a collection that is not org-scoped." }
14856
14925
  },
14857
14926
  required: ["collection", "query"]
@@ -14862,7 +14931,7 @@ var TOOLS = [
14862
14931
  {
14863
14932
  name: "ai_generate",
14864
14933
  feature: "ai",
14865
- description: "Generate a completion from a prompt or a named tenant template. Pass either prompt (raw text) or template + input (vars for a stored template). Defaults to the tenant-configured provider/model; override with provider/model. response_schema makes the output schema-guaranteed JSON (parsed into data.output). Returns the synchronous completion (set stream only via the REST API).",
14934
+ description: "Generate a completion from a prompt or a named tenant template. Pass either prompt (raw text) or template + input (vars for a stored template). Defaults to the tenant-configured provider/model; override with provider/model. response_schema makes the output schema-guaranteed JSON (parsed into data.output). Returns the synchronous completion by default (set stream only via the REST API); mode:'job' instead returns a 202 handle (generation_id, run_id) \u2014 read the answer with ai_generation_get and wait:25.",
14866
14935
  inputSchema: {
14867
14936
  type: "object",
14868
14937
  properties: {
@@ -14878,6 +14947,22 @@ var TOOLS = [
14878
14947
  enum: ["minimal", "low", "medium", "high"],
14879
14948
  description: "Gemini thinking depth (ignored by other providers). Omitted: the tenant's defaults.thinkingLevel, then the model default. 'minimal' on a model that rejects it is a 422 thinking_level_unsupported. Thinking tokens bill as output."
14880
14949
  },
14950
+ media_resolution: {
14951
+ type: "string",
14952
+ enum: ["low", "medium", "high"],
14953
+ description: "Gemini token budget per image or PDF page (ignored by other providers): lower is faster and bills fewer input tokens. Omitted: the stored template's own level (when template is set), then the tenant's defaults.mediaResolution, then the model default."
14954
+ },
14955
+ mode: {
14956
+ type: "string",
14957
+ enum: ["sync", "job"],
14958
+ description: "'sync' (default) answers with the completion. 'job' routes the generation through a jobs run and answers 202 with generation_id + run_id; read it with ai_generation_get (wait:25)."
14959
+ },
14960
+ deadline_ms: {
14961
+ type: "integer",
14962
+ minimum: 1e3,
14963
+ maximum: 17e3,
14964
+ description: "mode:'job' only (422 otherwise): bound the run's first provider call at this many ms so it can start without the queue hop. A first call that passes it is retried on the queue with the normal job-lane bound."
14965
+ },
14881
14966
  json_mode: { type: "boolean", description: "Constrain the output to JSON (no schema \u2014 use response_schema for a guaranteed shape)." },
14882
14967
  response_schema: {
14883
14968
  type: "object",
@@ -15168,14 +15253,15 @@ var TOOLS = [
15168
15253
  properties: {
15169
15254
  since: { type: "string", description: "ISO-8601 lower bound on created_at (inclusive; no zone = UTC). Tail with an overlap (last stored created_at minus a minute) and dedupe on ledger_entry_id." },
15170
15255
  cursor: { type: "string", description: "Opaque keyset cursor from a prior page (next_cursor)." },
15171
- user_id: { type: "string", maxLength: 512, description: "Only this end user's rows." },
15256
+ user_id: { type: "string", maxLength: 512, description: "Only this end user's rows. Give this OR org_id." },
15257
+ org_id: { type: "string", maxLength: 64, description: "WORKSPACE BILLING: only this workspace's rows (an orgs id; the same as user_id 'org:<id>'). Give this OR user_id." },
15172
15258
  credit_type: { type: "string", maxLength: 200, description: "Only rows of this credit type, e.g. 'scans'." },
15173
15259
  limit: { type: "number", default: 100, description: "Page size, clamped 1..500." }
15174
15260
  }
15175
15261
  },
15176
15262
  method: "GET",
15177
15263
  path: "/v1/payments/credits/ledger",
15178
- queryArgs: ["since", "cursor", "user_id", "credit_type", "limit"]
15264
+ queryArgs: ["since", "cursor", "user_id", "org_id", "credit_type", "limit"]
15179
15265
  },
15180
15266
  {
15181
15267
  name: "payments_list_refunds",
@@ -16882,7 +16968,7 @@ var VERB_USAGE = {
16882
16968
  import: "usage: vxil import <feature> --file <bundle.json|export.ndjson> [--mode merge|replace] [--allow-split-links]",
16883
16969
  migrate: "usage: vxil migrate plan|schema|data|verify|payments \u2026 (run `vxil migrate --help`)",
16884
16970
  functions: "usage: vxil functions new <name> [--force]|deploy [<name>] [--force]|list|delete|enable|disable [--yes]|invoke ['<json>' | --data '<json>'] [--async]|logs [--tail]|dev <name> [--port <n>] [--trigger <lane>] [--event <file.json>]",
16885
- cms: "usage: vxil cms status|pull|reindex <collection> [--field <field>]|drop <collection> [--yes]|bulk-rm <collection> --filter '<json>' [--limit <1..100>] [--dry-run] [--yes]",
16971
+ cms: "usage: vxil cms status|pull|reindex <collection> [--field <field>]|drop <collection> [--purge-rows] [--yes]|bulk-rm <collection> --filter '<json>' [--limit <1..100>] [--dry-run] [--yes]",
16886
16972
  payments: "usage: vxil payments simulate --scenario <refund-pair|cross-platform-unlock|renewal|expiry|past-due-grace|transfer> --user <id> [--tier <t>] [--dev] \u2014 mock/dev projects only",
16887
16973
  dev: "usage: vxil dev up [--ttl <h>] [--new] [--name <n>] [--reuse-slug <slug>] [--email <e> --password <p>] [--invite <code>] | down [--yes] | seed | reset | branch [<name>] [--ttl <h=24>] [--no-env] [--print] | --list | --rm <name> [--yes] | --watch [--target <name>] \u2014 dev up signs in with --email/--password (or VXIL_DEV_EMAIL/VXIL_DEV_PASSWORD, + VXIL_DEV_INVITE), else your `vxil login` session, else a prompt",
16888
16974
  config: "usage: vxil config get <feature> \xB7 vxil config draft show|save [--file <f>]|publish|discard <feature> \xB7 vxil config draft duplicate <feature> --from <version>",
@@ -17002,6 +17088,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
17002
17088
  "check",
17003
17089
  "all-projects",
17004
17090
  "allow-destructive",
17091
+ "purge-rows",
17005
17092
  "exit-code",
17006
17093
  "show-runs",
17007
17094
  "skip-functions",
@@ -26084,25 +26171,35 @@ export default defineConfig(${JSON.stringify({ features: featBlocks, cms: { coll
26084
26171
  const [sub] = positional(0);
26085
26172
  if (sub === "drop") {
26086
26173
  const [collection] = positional(1);
26087
- if (!collection) fail("usage: vxil cms drop <collection> [--yes]");
26174
+ if (!collection) fail("usage: vxil cms drop <collection> [--purge-rows] [--yes]");
26175
+ const purgeRows = hasFlag("purge-rows");
26088
26176
  const { api: api2, target } = requireApi(selector(), { write: `cms drop ${collection}` });
26089
26177
  const plan = await planCollectionDrop(api2, collection);
26090
26178
  if (plan.remoteUnavailable) {
26091
26179
  fail(`cannot read ${plan.remoteUnavailable.route} (${plan.remoteUnavailable.code ?? plan.remoteUnavailable.status}) \u2014 refusing to drop against an unread target`);
26092
26180
  }
26093
- if (!plan.exists) fail(`collection '${collection}' does not exist on tenant '${target.tenantSlug ?? "(env key)"}' \u2014 nothing to drop`);
26181
+ if (!plan.exists && !purgeRows) fail(`collection '${collection}' does not exist on tenant '${target.tenantSlug ?? "(env key)"}' \u2014 nothing to drop (\`--purge-rows\` deletes the rows an earlier drop retained)`);
26094
26182
  let declaredLocally = false;
26095
26183
  try {
26096
26184
  const cfg = await loadVxilConfig();
26097
26185
  declaredLocally = Object.prototype.hasOwnProperty.call(cfg.cms?.collections ?? {}, collection);
26098
26186
  } catch {
26099
26187
  }
26100
- console.error(formatCollectionDrop(collection, plan, declaredLocally));
26188
+ console.error(formatCollectionDrop(collection, plan, declaredLocally, purgeRows));
26101
26189
  if (!hasFlag("yes")) {
26102
26190
  if (!process.stdin.isTTY) fail("cms drop is destructive \u2014 pass --yes to confirm in a non-interactive run");
26103
- const ans = await prompt(`drop collection '${collection}'? type the collection name to confirm: `);
26191
+ const ans = await prompt(`${purgeRows ? "drop collection and hard-delete the rows of" : "drop collection"} '${collection}'? type the collection name to confirm: `);
26104
26192
  if (ans !== collection) fail("confirmation did not match \u2014 aborted.");
26105
26193
  }
26194
+ if (purgeRows) {
26195
+ const { purgedRows } = await dropCollectionPurgingRows(api2, collection);
26196
+ if (jsonOut) {
26197
+ console.log(JSON.stringify({ ok: true, collection, dropped: plan.exists, fields: plan.fields, purged_rows: purgedRows, declared_locally: declaredLocally }));
26198
+ } else {
26199
+ console.log(`\u2713 ${plan.exists ? `dropped collection ${collection} (${plan.fields} field(s)) and ` : `${collection}: `}hard-deleted ${purgedRows} stored row(s)`);
26200
+ }
26201
+ break;
26202
+ }
26106
26203
  await dropCollection(api2, collection);
26107
26204
  if (jsonOut) {
26108
26205
  console.log(JSON.stringify({ ok: true, collection, fields: plan.fields, rows: plan.rows, declared_locally: declaredLocally }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/cli",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "private": false,
5
5
  "description": "The vxil CLI — init, quickstart, push, gen, secrets, keys, migrate, doctor, diff, functions, payments; installs the `vxil` command (npm i -g @vxil/cli).",
6
6
  "license": "MIT",
@@ -45,8 +45,8 @@
45
45
  }
46
46
  },
47
47
  "devDependencies": {
48
- "@vxil/config": "0.21.0",
49
- "@vxil/feature-configs": "0.19.0",
48
+ "@vxil/config": "0.23.0",
49
+ "@vxil/feature-configs": "0.21.0",
50
50
  "@vxil/runtime": "0.0.1",
51
51
  "@vxil/types": "0.0.1",
52
52
  "miniflare": "^4.20260730.0"