@uru-intelligence/cli 0.3.53 → 0.3.54

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 +27 -5
  2. package/dist/uru.mjs +734 -87
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,7 +13,12 @@ bun src/index.ts whoami --json
13
13
  bun src/index.ts switch <workspace-id>
14
14
  ```
15
15
 
16
- The CLI is registry-backed. Dedicated Gem/Library/Dataset commands are thin adapters over the same platform tools used by MCP and the web UI; broader workspace, automation, prompt/persona/context, sharing, web/file, business SQL, secret/env/token operations are exposed through the generated operation registry while dedicated UX commands continue to mature. The CLI does **not** reimplement Gem lifecycle sequencing client-side.
16
+ The CLI is registry-backed. Dedicated Gem/Library/Dataset/Automation commands
17
+ are thin adapters over the same platform tools used by MCP and the web UI;
18
+ broader workspace, prompt/persona/context, sharing, web/file, business SQL,
19
+ secret/env/token operations remain exposed through the generated operation
20
+ registry while dedicated UX commands continue to mature. The CLI does **not**
21
+ reimplement Gem lifecycle sequencing client-side.
17
22
 
18
23
  ## Current commands
19
24
 
@@ -49,11 +54,21 @@ bun src/index.ts gems write 'library/Golden Gems/My Gem.gem' main.html --file ./
49
54
  bun src/index.ts library ls library
50
55
  bun src/index.ts library search revenue
51
56
  bun src/index.ts library mkdir 'library/Golden Gems'
52
- bun src/index.ts datasets query revenue_rows --limit 50 --json
57
+ bun src/index.ts datasets query revenue_rows --limit 50 --response-format concise --json
53
58
  bun src/index.ts datasets get --params-json '{"dataset_id":"revenue_rows"}'
54
59
  bun src/index.ts datasets rows-upsert --params-json '{"dataset_id":"revenue_rows","rows":[{"name":"Acme"}]}'
55
60
  bun src/index.ts datasets manage-create --params-json '{"title":"Revenue Rows","data_schema":{"properties":[]}}'
56
61
 
62
+ # Automation helpers over the platform tool surface.
63
+ bun src/index.ts automations ls --status active --view summary
64
+ bun src/index.ts automations get <automation-id>
65
+ bun src/index.ts automations create "Daily revenue digest" --status draft
66
+ bun src/index.ts automations enable <automation-id>
67
+ bun src/index.ts automations trigger <automation-id> --input-message "Run now"
68
+ bun src/index.ts automations runs ls <automation-id> --scope workspace --limit 20
69
+ bun src/index.ts automations runs tail <run-id> --level warn
70
+ bun src/index.ts automations runs cancel <run-id> --yes
71
+
57
72
  # Raw platform-tool escape hatch for diagnostics and parity.
58
73
  bun src/index.ts tools ls
59
74
  bun src/index.ts tools schema gem_control_plane --json
@@ -81,8 +96,9 @@ Gem version commands create immutable, build-backed versions through
81
96
  `gem_control_plane` `op=version_upload`, then deploy those version ids through
82
97
  `op=version_deploy`. Provider-internal flags such as `--runtime-provider` are
83
98
  not accepted: runtime routing is compiled server-side from the Gem contract.
84
- Gradual rollout percentages below 100 still fail with an explicit error until
85
- split traffic routing is enabled.
99
+ Non-100 rollout percentages are accepted only with `--skip-promote` to stage
100
+ rollout intent; live promote/rollback remains fail-closed until weighted link
101
+ routing is enabled.
86
102
 
87
103
  The scaffold defaults to the production API (`https://api.uruintelligence.com`).
88
104
  Use `--api-url`, `URU_API_BASE`, or `URU_API_URL` to point it at staging or a local backend:
@@ -110,6 +126,8 @@ Headless environments can use device login or explicit token/API-key paths:
110
126
  - linked project state: `.uru/project.json` with `{workspaceId, gemId, libraryPath}`
111
127
  - no credentials are written to `.uru/project.json`
112
128
  - Gem-scoped deploy tokens for CI use `uru tokens create|ls|revoke`
129
+ - `uru tokens create` requires `--name` plus exactly one lifetime flag:
130
+ `--expiry <duration>` or `--expires-at <iso timestamp>`
113
131
  - created tokens default to the server-enforced `gem:deploy` scope
114
132
  - custom `--scope`/`--scopes` values must include `gem:deploy` before the token
115
133
  can operate Gem deploy/query/control routes
@@ -158,4 +176,8 @@ bun --filter=@uru-intelligence/cli run smoke:binaries # dry-run matrix
158
176
  ```
159
177
 
160
178
  The updater command is intentionally conservative: `uru update` prints the exact
161
- `npm install -g @uru-intelligence/cli@latest` command instead of self-mutating the installation.
179
+ `npm install -g @uru-intelligence/cli@latest` command instead of self-mutating
180
+ the installation. Interactive text-mode commands also perform a bounded
181
+ once-per-24h npm version check and write a single stderr notice when a newer CLI
182
+ is available. The notifier is disabled for JSON/streaming output, CI,
183
+ `NO_UPDATE_NOTIFIER`, and `URU_NO_UPDATE_NOTIFIER`.
package/dist/uru.mjs CHANGED
@@ -6,7 +6,7 @@ import { spawn as spawn3 } from "node:child_process";
6
6
 
7
7
  // src/types.ts
8
8
  var DEFAULT_API_URL = "https://api.uruintelligence.com";
9
- var CLI_VERSION = "0.3.53";
9
+ var CLI_VERSION = "0.3.54";
10
10
  var ExitCode = {
11
11
  Ok: 0,
12
12
  Failure: 1,
@@ -202,6 +202,7 @@ function buildOperationGroups(op) {
202
202
  op("gem.fs_read", "gems", "Read a file from a Gem project directory.", "gem_query", "fs_read", "read"),
203
203
  op("gem.fs_write", "gems", "Write a file inside a Gem project directory.", "gem_control_plane", "fs_write", "write"),
204
204
  op("gem.fs_batch_write", "gems", "Write multiple Gem files atomically through the server source layer.", "gem_control_plane", "fs_batch_write", "write"),
205
+ op("gem.fs_sync", "gems", "Synchronize Gem files and deletions atomically through the server source layer.", "gem_control_plane", "fs_sync", "write"),
205
206
  op("gem.fs_patch", "gems", "Apply a patch to Gem source files.", "gem_control_plane", "fs_patch", "write"),
206
207
  op("gem.fs_mkdir", "gems", "Create a directory inside a Gem project.", "gem_control_plane", "fs_mkdir", "write"),
207
208
  op("gem.fs_rm", "gems", "Remove a file or directory from a Gem project.", "gem_control_plane", "fs_rm", "destructive"),
@@ -257,11 +258,12 @@ function buildOperationGroups(op) {
257
258
  const sharingOperations = [
258
259
  op("sharing.share", "sharing", "Share a Library resource with a user, team, or workspace.", "library_collab", "share", "write"),
259
260
  op("sharing.unshare", "sharing", "Remove a Library resource grant.", "library_collab", "unshare", "destructive"),
260
- op("sharing.permissions_list", "sharing", "List permissions for a Library resource.", "library_collab", "list_permissions", "read"),
261
+ op("sharing.permissions_list", "sharing", "List permissions for a Library resource.", "sharing_query", "list_permissions", "read"),
262
+ op("sharing.shares_list", "sharing", "List active share grants for a Library resource.", "sharing_query", "list_shares", "read"),
261
263
  op("sharing.permissions_set", "sharing", "Set permissions for a Library resource.", "library_collab", "set_permissions", "write"),
262
264
  op("sharing.settings_set", "sharing", "Set share settings for a Library resource.", "library_collab", "set_share_settings", "write"),
263
265
  op("sharing.transfer_ownership", "sharing", "Transfer ownership for a Library resource.", "library_collab", "transfer_ownership", "destructive"),
264
- op("sharing.versions_list", "sharing", "List Library resource versions.", "library_collab", "list_versions", "read"),
266
+ op("sharing.versions_list", "sharing", "List Library resource versions.", "sharing_query", "list_versions", "read"),
265
267
  op("sharing.version_restore", "sharing", "Restore a Library resource version.", "library_collab", "restore_version", "destructive"),
266
268
  op("sharing.comment", "sharing", "Comment on a Library resource.", "library_collab", "comment", "write"),
267
269
  op("sharing.comment_resolve", "sharing", "Resolve a Library resource comment.", "library_collab", "resolve_comment", "write"),
@@ -475,6 +477,10 @@ var internalOnlyPlatformToolIds = new Set([
475
477
  "TYLER_SQL",
476
478
  "UPSERT_DATASET_ROWS"
477
479
  ]);
480
+ // ../../packages/platform-operations/src/external_company_mcp_policy.ts
481
+ var externalCompanyMcpDeniedToolIds = new Set([
482
+ "MAGUIRE_WRITE_SQL"
483
+ ]);
478
484
  // ../../packages/platform-operations/src/index.ts
479
485
  var ALL_SURFACES = [
480
486
  "cli",
@@ -621,6 +627,12 @@ class UruApiClient {
621
627
  body: { parameters }
622
628
  });
623
629
  }
630
+ async streamTool(name, parameters, onEvent) {
631
+ return this.requestStream(`/api/platform/tools/${encodeURIComponent(name)}/execute`, {
632
+ method: "POST",
633
+ body: { parameters }
634
+ }, onEvent);
635
+ }
624
636
  async api(path, options) {
625
637
  return this.request(path, options);
626
638
  }
@@ -644,6 +656,30 @@ class UruApiClient {
644
656
  }
645
657
  return payload;
646
658
  }
659
+ async requestStream(path, options, onEvent) {
660
+ const init = {
661
+ method: options.method,
662
+ ...this.signal === undefined ? {} : { signal: this.signal },
663
+ headers: {
664
+ Authorization: `Bearer ${this.token}`,
665
+ Accept: "application/x-ndjson, application/json",
666
+ ...this.workspaceId === undefined ? {} : { "X-Uru-Workspace-Id": this.workspaceId },
667
+ ...options.body === undefined ? {} : { "Content-Type": "application/json" }
668
+ },
669
+ ...options.body === undefined ? {} : { body: JSON.stringify(options.body) }
670
+ };
671
+ const response = await this.fetchImpl(`${this.apiUrl}${path}`, init);
672
+ if (response.ok && response.body !== null && isNdjsonResponse(response.headers.get("content-type"))) {
673
+ await readNdjsonStream(response.body, onEvent);
674
+ return { streamed: true };
675
+ }
676
+ const text = await response.text();
677
+ const payload = parseResponseBody(text);
678
+ if (!response.ok) {
679
+ throw apiError(response.status, payload, options.body);
680
+ }
681
+ return { streamed: false, payload };
682
+ }
647
683
  }
648
684
  function normalizeApiUrl(value) {
649
685
  const trimmed = value.trim();
@@ -659,6 +695,52 @@ function parseResponseBody(text) {
659
695
  }
660
696
  return parsed;
661
697
  }
698
+ function isNdjsonResponse(contentType) {
699
+ const normalized = String(contentType ?? "").toLowerCase();
700
+ return normalized.includes("application/x-ndjson") || normalized.includes("application/ndjson") || normalized.includes("text/x-ndjson");
701
+ }
702
+ async function readNdjsonStream(body, onEvent) {
703
+ const reader = body.getReader();
704
+ const decoder = new TextDecoder;
705
+ let buffered = "";
706
+ try {
707
+ while (true) {
708
+ const { done, value } = await reader.read();
709
+ if (done) {
710
+ break;
711
+ }
712
+ buffered += decoder.decode(value, { stream: true });
713
+ buffered = emitCompleteNdjsonLines(buffered, onEvent);
714
+ }
715
+ buffered += decoder.decode();
716
+ emitNdjsonLine(buffered, onEvent);
717
+ } finally {
718
+ reader.releaseLock();
719
+ }
720
+ }
721
+ function emitCompleteNdjsonLines(buffered, onEvent) {
722
+ let start = 0;
723
+ while (true) {
724
+ const newline = buffered.indexOf(`
725
+ `, start);
726
+ if (newline === -1) {
727
+ return buffered.slice(start);
728
+ }
729
+ emitNdjsonLine(buffered.slice(start, newline), onEvent);
730
+ start = newline + 1;
731
+ }
732
+ }
733
+ function emitNdjsonLine(line, onEvent) {
734
+ const trimmed = line.trim();
735
+ if (trimmed === "") {
736
+ return;
737
+ }
738
+ const parsed = JSON.parse(trimmed);
739
+ if (!isJsonObject(parsed)) {
740
+ throw new CliError("API returned a non-object stream event");
741
+ }
742
+ onEvent(parsed);
743
+ }
662
744
  function apiError(status, payload, requestBody) {
663
745
  const redactions = collectSensitiveValues(requestBody);
664
746
  const summary = errorMessage(payload) ?? `Uru API request failed with HTTP ${status}`;
@@ -1005,6 +1087,15 @@ function writeStreamJson(io, event, value) {
1005
1087
  io.stdout.write(`${JSON.stringify({ event, ...payload })}
1006
1088
  `);
1007
1089
  }
1090
+ function writeStreamJsonEvent(io, value) {
1091
+ const payload = objectPayload(value);
1092
+ if (typeof payload["event"] !== "string" || payload["event"].trim() === "") {
1093
+ writeStreamJson(io, "event", payload);
1094
+ return;
1095
+ }
1096
+ io.stdout.write(`${JSON.stringify(payload)}
1097
+ `);
1098
+ }
1008
1099
  function writeText(io, value) {
1009
1100
  io.stdout.write(value.endsWith(`
1010
1101
  `) ? value : `${value}
@@ -1075,13 +1166,13 @@ GEMS
1075
1166
  uru build [library/name.gem] --dry-run --outdir Build/validate and export build manifest
1076
1167
  uru deploy [library/name.gem] [--prod] [--dry-run] Server-side build/deploy/publish
1077
1168
  uru deploy [library/name.gem] --skip-promote Deploy without promoting the published link
1078
- uru promote <version-id> [library/name.gem] Promote a Gem version to a published link
1169
+ uru promote <version-id> [library/name.gem] Promote a staged Gem version/deployment
1079
1170
  uru status [library/name.gem] Local source status versus source lock
1080
1171
  uru logs [library/name.gem] [--limit 100] [--follow]
1081
1172
  Runtime logs
1082
1173
  uru open [library/name.gem] Print first published URL
1083
- uru rollback <version-id> [library/name.gem] --yes
1084
- Roll back a link to a Gem version
1174
+ uru rollback [<version-id>] [library/name.gem] --yes
1175
+ Roll back a live Gem link
1085
1176
  uru gems inspect|validate|build|status [library/name.gem]
1086
1177
  uru gems versions upload [library/name.gem] Create an immutable Gem version
1087
1178
  uru gems versions deploy <version-id> [library/name.gem]
@@ -1094,19 +1185,22 @@ LIBRARY / DATASETS / TOOLS
1094
1185
  uru library search <query> Search Library items
1095
1186
  uru library mkdir <path> Create a Library folder
1096
1187
  # Prompts, personas, contexts: use typed Library paths such as library/Prompts/Foo.prompt.md
1097
- uru datasets query <dataset-slug> [--limit 50] Query dataset rows
1188
+ uru datasets query <dataset-slug> [--limit 50] Query dataset rows; add --response-format concise for compact output
1189
+ uru automations ls|get|create|enable|disable Manage automations without raw JSON parameters
1190
+ uru automations trigger <id> Start a manual automation run
1191
+ uru automations runs ls|tail <run-id> Inspect automation runs and logs
1098
1192
  uru secrets put <name> [library/name.gem] --value-stdin Store Gem secret without shell history
1099
1193
  uru secrets ls [library/name.gem] List Gem secret metadata only
1100
1194
  uru secrets rm <name> [library/name.gem] --yes Remove a Gem secret value
1101
1195
  uru env pull [library/name.gem] Write a local metadata-only .env template
1102
1196
  uru env run -- <cmd> Run with local env values and redact output
1103
- uru tokens create [library/name.gem] --expiry 30d Create a Gem-scoped deploy token
1197
+ uru tokens create [library/name.gem] --name CI --expiry 30d Create a Gem-scoped deploy token
1104
1198
  uru tokens ls [library/name.gem] List Gem token metadata only
1105
1199
  uru tokens revoke <token-id> --yes Revoke a Gem token immediately
1106
1200
  uru tools ls|schema <name>|run <name> Full platform-tool parity
1107
1201
  uru operations ls [--family gems] Print generated operation registry
1108
1202
  uru operations run <operation-id> --params-json Run any registry-backed operation
1109
- uru workspace|sharing|automations|web <op> Registry families for full-platform parity
1203
+ uru workspace|sharing|web <op> Registry families for full-platform parity
1110
1204
  uru mcp config|install Print/write MCP client config
1111
1205
  uru complete bash|zsh|fish Print shell completions
1112
1206
  uru update Print the self-update command
@@ -1180,8 +1274,6 @@ qualified operation form.
1180
1274
  var REGISTRY_FAMILY_COMMANDS = {
1181
1275
  workspace: "workspace",
1182
1276
  sharing: "sharing",
1183
- automations: "automations",
1184
- automation: "automations",
1185
1277
  web: "web",
1186
1278
  files: "files",
1187
1279
  file: "files",
@@ -1242,13 +1334,16 @@ deployment that can be promoted after checks; --prod cannot be combined with
1242
1334
  --no-publish.
1243
1335
  `,
1244
1336
  promote: `Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
1337
+ uru promote --deployment-id <deployment-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
1245
1338
 
1246
- Deploy an immutable Gem version and promote a published link to it. If --link-id
1247
- is omitted, Uru publishes through the default clean Gem link.
1339
+ Promote an already-staged immutable Gem version or ready deployment. With
1340
+ --link-id, this is a published-link pointer flip after checks pass. If --link-id
1341
+ is omitted, Uru promotes through the default clean Gem link.
1248
1342
  `,
1249
- pull: `Usage: uru pull [library/name.gem]
1343
+ pull: `Usage: uru pull [library/name.gem] [--force]
1250
1344
 
1251
1345
  Download Gem source into the current directory and write .uru/source-lock.json.
1346
+ Refuses to overwrite dirty local files unless --force is supplied.
1252
1347
  `,
1253
1348
  status: `Usage: uru status [library/name.gem] [--json]
1254
1349
 
@@ -1275,11 +1370,12 @@ before the command to emit NDJSON events.
1275
1370
 
1276
1371
  Print the first available hosted/published Gem URL.
1277
1372
  `,
1278
- rollback: `Usage: uru rollback <version-id> [library/name.gem] [--link-id <published-link-id>] --yes
1373
+ rollback: `Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes
1279
1374
 
1280
- Deploy an immutable Gem version and retarget the published link as a rollback.
1281
- Requires --yes because it mutates live traffic. If --link-id is omitted, Uru
1282
- publishes through the default clean Gem link.
1375
+ Rollback retargets a live link to an already-materialized older Gem version or
1376
+ deployment after server-side rollback preflight passes. Requires --yes because it
1377
+ mutates live traffic. If --link-id is omitted, Uru rolls back through the active
1378
+ Primary Gem link.
1283
1379
  `,
1284
1380
  gems: `Usage: uru gems <inspect|validate|build|status|read|write|versions|checks> [...]
1285
1381
 
@@ -1290,8 +1386,9 @@ same platform operations used by MCP and the web UI.
1290
1386
  uru gems versions deploy <version-id> [library/name.gem] [--pct 100]
1291
1387
 
1292
1388
  Create immutable Gem versions and deploy them through the Gem control plane.
1293
- Upload accepts --message, --tag/--tags, and --preview-alias. Gradual percentages
1294
- below 100 fail closed until split traffic routing is enabled.
1389
+ Upload accepts --message, --tag/--tags, and --preview-alias. Non-100
1390
+ percentages are accepted only with --skip-promote to stage rollout intent; live
1391
+ promote/rollback requires 100 until weighted link routing is enabled.
1295
1392
  `,
1296
1393
  "gems checks": `Usage: uru gems checks <version-id> [library/name.gem] [--deployment-id <id>]
1297
1394
  uru gems checks run browser|security [library/name.gem] [--deployment-id <id>] [--blocking]
@@ -1306,9 +1403,24 @@ the check a release gate; security also accepts --deep.
1306
1403
 
1307
1404
  Operate on Library items through platform tools.
1308
1405
  `,
1309
- datasets: `Usage: uru datasets query <dataset-slug> [--limit 50] [--offset 0]
1406
+ datasets: `Usage: uru datasets query <dataset-slug> [--limit 50] [--offset 0] [--response-format detailed|concise]
1407
+
1408
+ Query dataset rows through the platform dataset operation. Use concise for id plus selected/display fields while keeping server totals and aggregates.
1409
+ `,
1410
+ automations: `Usage: uru automations ls [--status draft|active|paused|archived] [--limit 100] [--view summary|detailed]
1411
+ uru automations get <automation-id>
1412
+ uru automations create <name> [--description <text>] [--status draft|active|paused]
1413
+ uru automations enable <automation-id>
1414
+ uru automations disable <automation-id>
1415
+ uru automations trigger <automation-id> [--input-message <text>] [--idempotency-key <key>]
1416
+ uru automations runs ls [automation-id] [--scope personal|workspace] [--limit 100]
1417
+ uru automations runs tail <run-id> [--limit 1000] [--level debug|info|warn|error]
1418
+ uru automations runs debug|timeline <run-id>
1419
+ uru automations runs cancel <run-id> --yes
1310
1420
 
1311
- Query dataset rows through the platform dataset operation.
1421
+ Manage automations through first-class CLI commands backed by the shared
1422
+ operation registry. Reads default to compact summary views; use --view detailed
1423
+ only when you need full graph/config payloads.
1312
1424
  `,
1313
1425
  tokens: `Usage: uru tokens create [library/name.gem] --expiry <duration> --name <name>
1314
1426
  uru tokens ls [library/name.gem]
@@ -1412,6 +1524,19 @@ async function emitTool(ctx, operationId, params, options = {}) {
1412
1524
  operation: options.streamStart["operation"] ?? operationId,
1413
1525
  phase: "requesting"
1414
1526
  });
1527
+ const streamed = await executeOperationStream(ctx, operationId, params, (event) => {
1528
+ writeStreamJsonEvent(ctx.io, event);
1529
+ });
1530
+ if (streamed.streamed) {
1531
+ return;
1532
+ }
1533
+ const unwrapped2 = unwrapToolResult(streamed.payload ?? null);
1534
+ const output2 = options.transform?.(unwrapped2) ?? unwrapped2;
1535
+ writeStreamJson(ctx.io, "result", output2);
1536
+ writeStreamJson(ctx.io, "done", {
1537
+ operation: options.streamStart["operation"] ?? operationId
1538
+ });
1539
+ return;
1415
1540
  }
1416
1541
  const result = await executeOperation(ctx, operationId, params);
1417
1542
  const unwrapped = unwrapToolResult(result);
@@ -1429,6 +1554,17 @@ async function emitTool(ctx, operationId, params, options = {}) {
1429
1554
  writeText(ctx.io, options.textExtractor?.(output) ?? JSON.stringify(output, null, 2));
1430
1555
  }
1431
1556
  }
1557
+ async function executeOperationStream(ctx, operationId, params, onEvent) {
1558
+ const operation = getPlatformOperation(operationId);
1559
+ if (operation === undefined) {
1560
+ throw new CliError(`Unknown operation registry id: ${operationId}`);
1561
+ }
1562
+ const toolParams = { ...params };
1563
+ if (operation.backendOp !== undefined) {
1564
+ toolParams["op"] = operation.backendOp;
1565
+ }
1566
+ return client(ctx).streamTool(operation.backendToolName, toolParams, onEvent);
1567
+ }
1432
1568
  async function executeOperation(ctx, operationId, params) {
1433
1569
  const operation = getPlatformOperation(operationId);
1434
1570
  if (operation === undefined) {
@@ -1474,7 +1610,7 @@ function tokenGemRefParams(flags, linkedProject, options = {}) {
1474
1610
  }
1475
1611
  const explicitGem = flags.values["--gem"];
1476
1612
  if (explicitGem !== undefined) {
1477
- return { gem_id: explicitGem };
1613
+ return explicitGem.startsWith("library/") ? { path: explicitGem } : { gem_id: explicitGem };
1478
1614
  }
1479
1615
  const positional = flags.positionals[0];
1480
1616
  if (positional !== undefined) {
@@ -1624,6 +1760,18 @@ function sanitizeConfig(value) {
1624
1760
  if (value["insecureStorage"] === true) {
1625
1761
  config.insecureStorage = true;
1626
1762
  }
1763
+ if (isRecord(value["update"])) {
1764
+ const update = {};
1765
+ for (const key of ["lastCheckedAt", "latestVersion"]) {
1766
+ const item = value["update"][key];
1767
+ if (typeof item === "string" && item.trim() !== "") {
1768
+ update[key] = item;
1769
+ }
1770
+ }
1771
+ if (Object.keys(update).length > 0) {
1772
+ config.update = update;
1773
+ }
1774
+ }
1627
1775
  return config;
1628
1776
  }
1629
1777
  function isNodeError(error) {
@@ -2008,10 +2156,15 @@ function credentialStoreUnavailableError(backend, message, error) {
2008
2156
  var CLI_DOCS_URL = "https://docs.uruintelligence.com/cli/errors";
2009
2157
  var ERROR_TIPS = {
2010
2158
  auth_required: "Run `uru login`, pass `--token`, or set `URU_TOKEN` before retrying.",
2159
+ interactive_login_required: "Run `uru login` before this command, use `uru login --device` on remote shells, or pass `--token`/`URU_TOKEN` in CI.",
2011
2160
  conflict: "Run `uru status` or `uru pull` to inspect remote changes; use `--force` only when you intentionally want to overwrite.",
2012
2161
  api_request_failed: "Check the request parameters, workspace context, and server response before retrying.",
2013
2162
  keychain_access_failed: "Unlock the OS credential store, or retry with `--insecure-storage` only if plaintext local storage is acceptable.",
2014
2163
  keychain_unavailable: "Install or unlock the OS credential helper, or retry with `--insecure-storage` only if plaintext local storage is acceptable.",
2164
+ oauth_state_mismatch: "Restart `uru login`; this usually means the browser callback came from an old or mismatched login tab.",
2165
+ oauth_authorization_failed: "Restart `uru login` and approve access in the browser, or use `uru login --device` if the browser cannot reach the callback.",
2166
+ oauth_missing_code: "Restart `uru login`; if the browser callback keeps failing, retry with `uru login --device`.",
2167
+ oauth_request_failed: "Check the API URL and network access, then retry `uru login`; use `uru login --device` from SSH/headless environments.",
2015
2168
  oauth_device_failed: "Restart `uru login --device` and complete authorization in the browser before the code expires.",
2016
2169
  oauth_device_timeout: "Run `uru login --device` again and complete browser authorization before the timeout.",
2017
2170
  cancelled: "The command was cancelled; rerun it only if you still want the operation to continue."
@@ -2257,6 +2410,120 @@ function terminalWidth(stream, env) {
2257
2410
  return 80;
2258
2411
  }
2259
2412
 
2413
+ // src/update-notifier.ts
2414
+ var PACKAGE_NAME = "@uru-intelligence/cli";
2415
+ var REGISTRY_LATEST_URL = "https://registry.npmjs.org/@uru-intelligence%2Fcli/latest";
2416
+ var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
2417
+ var UPDATE_CHECK_TIMEOUT_MS = 750;
2418
+ async function maybeNotifyCliUpdate(ctx, options = {}) {
2419
+ if (!shouldCheckForUpdate(ctx, options.env ?? process.env)) {
2420
+ return;
2421
+ }
2422
+ const now = options.now?.() ?? Date.now();
2423
+ if (!isUpdateCheckDue(ctx.config, now)) {
2424
+ return;
2425
+ }
2426
+ const checkedConfig = withUpdateCheckTimestamp(ctx.config, now);
2427
+ await writeConfigBestEffort(ctx, checkedConfig);
2428
+ const latestVersion = await fetchLatestVersion({
2429
+ fetchImpl: ctx.fetch ?? fetch,
2430
+ parentSignal: ctx.abortSignal
2431
+ });
2432
+ if (latestVersion === undefined) {
2433
+ return;
2434
+ }
2435
+ await writeConfigBestEffort(ctx, {
2436
+ ...checkedConfig,
2437
+ update: {
2438
+ ...checkedConfig.update,
2439
+ latestVersion
2440
+ }
2441
+ });
2442
+ if (compareVersions(latestVersion, CLI_VERSION) <= 0) {
2443
+ return;
2444
+ }
2445
+ ctx.io.stderr.write(`A newer Uru CLI is available: ${CLI_VERSION} → ${latestVersion}. ` + `Run npm install -g ${PACKAGE_NAME}@latest
2446
+ `);
2447
+ }
2448
+ function shouldCheckForUpdate(ctx, env) {
2449
+ return ctx.outputFormat === "text" && ctx.terminal.stderrIsTty && !ctx.terminal.ci && env["NO_UPDATE_NOTIFIER"] === undefined && env["URU_NO_UPDATE_NOTIFIER"] === undefined;
2450
+ }
2451
+ function isUpdateCheckDue(config, now) {
2452
+ const raw = config.update?.lastCheckedAt;
2453
+ if (raw === undefined) {
2454
+ return true;
2455
+ }
2456
+ const lastCheckedAt = Date.parse(raw);
2457
+ return !Number.isFinite(lastCheckedAt) || now - lastCheckedAt >= UPDATE_CHECK_INTERVAL_MS;
2458
+ }
2459
+ function withUpdateCheckTimestamp(config, now) {
2460
+ return {
2461
+ ...config,
2462
+ update: {
2463
+ ...config.update,
2464
+ lastCheckedAt: new Date(now).toISOString()
2465
+ }
2466
+ };
2467
+ }
2468
+ async function fetchLatestVersion(args) {
2469
+ const controller = new AbortController;
2470
+ const timeout = setTimeout(() => {
2471
+ controller.abort();
2472
+ }, UPDATE_CHECK_TIMEOUT_MS);
2473
+ timeout.unref?.();
2474
+ const abortFromParent = () => {
2475
+ controller.abort(args.parentSignal.reason);
2476
+ };
2477
+ if (args.parentSignal.aborted) {
2478
+ abortFromParent();
2479
+ } else {
2480
+ args.parentSignal.addEventListener("abort", abortFromParent, { once: true });
2481
+ }
2482
+ try {
2483
+ const response = await args.fetchImpl(REGISTRY_LATEST_URL, {
2484
+ headers: { Accept: "application/json" },
2485
+ signal: controller.signal
2486
+ });
2487
+ if (!response.ok) {
2488
+ return;
2489
+ }
2490
+ const payload = await response.json();
2491
+ if (!isRecord2(payload)) {
2492
+ return;
2493
+ }
2494
+ const version = payload["version"];
2495
+ return typeof version === "string" && version.trim() !== "" ? version.trim() : undefined;
2496
+ } catch {
2497
+ return;
2498
+ } finally {
2499
+ clearTimeout(timeout);
2500
+ args.parentSignal.removeEventListener("abort", abortFromParent);
2501
+ }
2502
+ }
2503
+ async function writeConfigBestEffort(ctx, config) {
2504
+ try {
2505
+ await ctx.store.write(config);
2506
+ } catch {}
2507
+ }
2508
+ function compareVersions(left, right) {
2509
+ const leftParts = versionParts(left);
2510
+ const rightParts = versionParts(right);
2511
+ const width = Math.max(leftParts.length, rightParts.length);
2512
+ for (let index = 0;index < width; index += 1) {
2513
+ const delta = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
2514
+ if (delta !== 0) {
2515
+ return delta;
2516
+ }
2517
+ }
2518
+ return 0;
2519
+ }
2520
+ function versionParts(version) {
2521
+ return version.replace(/^v/, "").split(/[.-]/).map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
2522
+ }
2523
+ function isRecord2(value) {
2524
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2525
+ }
2526
+
2260
2527
  // src/auth-commands.ts
2261
2528
  import { readFile as readFile3 } from "node:fs/promises";
2262
2529
  import { createServer } from "node:http";
@@ -2633,14 +2900,19 @@ async function resolveFlagToken(flags, stdin) {
2633
2900
  return;
2634
2901
  }
2635
2902
  async function login(ctx, options = {}) {
2636
- const token = ctx.token === undefined || ctx.token.trim() === "" ? await obtainLoginSessionToken(ctx) : ctx.token;
2637
- const loginClient = new UruApiClient({
2638
- apiUrl: ctx.apiUrl,
2639
- token,
2640
- ...ctx.workspace !== undefined ? { workspaceId: ctx.workspace } : {},
2641
- ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
2642
- });
2643
- const result = await loginClient.validateToken();
2903
+ const existingToken = ctx.token === undefined || ctx.token.trim() === "" ? undefined : ctx.token;
2904
+ let token = existingToken ?? await obtainLoginSessionToken(ctx);
2905
+ let result;
2906
+ try {
2907
+ result = await validateSessionToken(ctx, token);
2908
+ } catch (error) {
2909
+ const storedTokenIsStale = existingToken !== undefined && !ctx.tokenIsExplicit && isAuthRequiredError(error);
2910
+ if (!storedTokenIsStale) {
2911
+ throw error;
2912
+ }
2913
+ token = await obtainLoginSessionToken(ctx);
2914
+ result = await validateSessionToken(ctx, token);
2915
+ }
2644
2916
  const selectedWorkspace = selectWorkspace(result, ctx.workspace);
2645
2917
  await persistLoginToken(ctx, token, selectedWorkspace);
2646
2918
  if (options.silentSuccess === true) {
@@ -2661,6 +2933,18 @@ async function login(ctx, options = {}) {
2661
2933
  writeText(ctx.io, ctx.insecureStorage ? "Logged in (token stored in local config)" : "Logged in");
2662
2934
  }
2663
2935
  }
2936
+ async function validateSessionToken(ctx, token) {
2937
+ const loginClient = new UruApiClient({
2938
+ apiUrl: ctx.apiUrl,
2939
+ token,
2940
+ ...ctx.workspace !== undefined ? { workspaceId: ctx.workspace } : {},
2941
+ ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
2942
+ });
2943
+ return loginClient.validateToken();
2944
+ }
2945
+ function isAuthRequiredError(error) {
2946
+ return error instanceof CliError && error.exitCode === ExitCode.AuthRequired;
2947
+ }
2664
2948
  async function readAllStdin(input) {
2665
2949
  const chunks = [];
2666
2950
  for await (const chunk of input) {
@@ -2905,8 +3189,6 @@ import { dirname as dirname3 } from "node:path";
2905
3189
  var GENERIC_FAMILY_COMMANDS = {
2906
3190
  workspace: "workspace",
2907
3191
  sharing: "sharing",
2908
- automations: "automations",
2909
- automation: "automations",
2910
3192
  web: "web",
2911
3193
  files: "files",
2912
3194
  file: "files",
@@ -2942,6 +3224,7 @@ var TOP_LEVEL_COMMANDS = [
2942
3224
  "open",
2943
3225
  "rollback",
2944
3226
  "gems",
3227
+ "automations",
2945
3228
  "library",
2946
3229
  "datasets",
2947
3230
  "tokens",
@@ -3139,13 +3422,154 @@ async function dispatchDatasets(ctx, sub, args) {
3139
3422
  return;
3140
3423
  }
3141
3424
  const flags = parseLocalFlags(args);
3142
- const datasetId = requireCommandValue(flags.positionals[0], "Usage: uru datasets query <dataset-slug> [--limit 50]");
3143
- await emitTool(ctx, "dataset.query", {
3425
+ const datasetId = requireCommandValue(flags.positionals[0], "Usage: uru datasets query <dataset-slug> [--limit 50] [--response-format detailed|concise]");
3426
+ const params = {
3144
3427
  dataset_id: datasetId,
3145
3428
  datasetId,
3146
3429
  limit: integerFlag(flags, "--limit", 50),
3147
3430
  offset: integerFlag(flags, "--offset", 0)
3148
- });
3431
+ };
3432
+ const responseFormat = flags.values["--response-format"];
3433
+ if (responseFormat !== undefined) {
3434
+ if (responseFormat !== "detailed" && responseFormat !== "concise") {
3435
+ throw new CliError('--response-format must be "detailed" or "concise"');
3436
+ }
3437
+ params["response_format"] = responseFormat;
3438
+ }
3439
+ await emitTool(ctx, "dataset.query", params);
3440
+ }
3441
+ async function dispatchAutomations(ctx, sub, args) {
3442
+ if (sub === "ls" || sub === "list") {
3443
+ const flags = parseLocalFlags(args);
3444
+ const params = {
3445
+ limit: integerFlag(flags, "--limit", 100)
3446
+ };
3447
+ copyOptionalFlag(flags, params, "--status", "status_filter");
3448
+ copyOptionalFlag(flags, params, "--parent-folder-id", "parent_folder_id");
3449
+ copyOptionalFlag(flags, params, "--folder-id", "parent_folder_id");
3450
+ copyAutomationView(flags, params);
3451
+ await emitTool(ctx, "automation.list", params);
3452
+ return;
3453
+ }
3454
+ if (sub === "get" || sub === "inspect") {
3455
+ const flags = parseLocalFlags(args);
3456
+ await emitTool(ctx, "automation.get", {
3457
+ automation_id: requireCommandValue(flags.positionals[0], "Usage: uru automations get <automation-id>")
3458
+ });
3459
+ return;
3460
+ }
3461
+ if (sub === "create") {
3462
+ const flags = parseLocalFlags(args);
3463
+ const params = {
3464
+ name: requireCommandValue(flags.positionals[0], "Usage: uru automations create <name> [--description <text>] [--status draft|active|paused] [--graph-json {...}]")
3465
+ };
3466
+ copyOptionalFlag(flags, params, "--description", "description");
3467
+ copyOptionalFlag(flags, params, "--status", "status");
3468
+ copyOptionalFlag(flags, params, "--parent-folder-id", "parent_folder_id");
3469
+ copyOptionalFlag(flags, params, "--folder-id", "parent_folder_id");
3470
+ const graphJson = flags.values["--graph-json"];
3471
+ if (graphJson !== undefined) {
3472
+ params["graph_json"] = parseParamsJson(graphJson, "--graph-json");
3473
+ }
3474
+ await emitTool(ctx, "automation.create", params);
3475
+ return;
3476
+ }
3477
+ if (sub === "enable" || sub === "disable") {
3478
+ const flags = parseLocalFlags(args);
3479
+ await emitTool(ctx, "automation.update", {
3480
+ automation_id: requireCommandValue(flags.positionals[0], `Usage: uru automations ${sub} <automation-id>`),
3481
+ status: sub === "enable" ? "active" : "paused"
3482
+ });
3483
+ return;
3484
+ }
3485
+ if (sub === "trigger" || sub === "run" || sub === "start") {
3486
+ const flags = parseLocalFlags(args);
3487
+ const params = {
3488
+ automation_id: requireCommandValue(flags.values["--automation-id"] ?? flags.positionals[0], "Usage: uru automations trigger <automation-id> [--input-message <text>] [--idempotency-key <key>]")
3489
+ };
3490
+ copyOptionalFlag(flags, params, "--input-message", "input_message");
3491
+ copyOptionalFlag(flags, params, "--idempotency-key", "idempotency_key");
3492
+ await emitTool(ctx, "automation.run_start", params);
3493
+ return;
3494
+ }
3495
+ if (sub === "runs") {
3496
+ await dispatchAutomationRuns(ctx, args[0], args.slice(1));
3497
+ return;
3498
+ }
3499
+ throw new CliError("Usage: uru automations ls|get|create|enable|disable|trigger|runs");
3500
+ }
3501
+ async function dispatchAutomationRuns(ctx, sub, args) {
3502
+ if (sub === "ls" || sub === "list") {
3503
+ const flags = parseLocalFlags(args);
3504
+ const params = {
3505
+ limit: integerFlag(flags, "--limit", 100)
3506
+ };
3507
+ copyOptionalFlag(flags, params, "--automation-id", "automation_id");
3508
+ copyOptionalFlag(flags, params, "--scope", "scope");
3509
+ copyOptionalFlag(flags, params, "--cursor", "cursor");
3510
+ copyOptionalFlag(flags, params, "--include", "include");
3511
+ if (params["automation_id"] === undefined && flags.positionals[0] !== undefined) {
3512
+ params["automation_id"] = flags.positionals[0];
3513
+ }
3514
+ await emitTool(ctx, "automation.runs_list", params);
3515
+ return;
3516
+ }
3517
+ if (sub === "tail" || sub === "logs") {
3518
+ const flags = parseLocalFlags(args);
3519
+ const params = {
3520
+ run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs tail <run-id> [--limit 1000] [--level debug|info|warn|error]"),
3521
+ limit: integerFlag(flags, "--limit", 1000)
3522
+ };
3523
+ copyOptionalFlag(flags, params, "--level", "level");
3524
+ await emitTool(ctx, "automation.run_logs", params);
3525
+ return;
3526
+ }
3527
+ if (sub === "debug") {
3528
+ const flags = parseLocalFlags(args);
3529
+ const params = {
3530
+ run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs debug <run-id> [--log-limit 100] [--transcript-limit 50]"),
3531
+ log_limit: integerFlag(flags, "--log-limit", 100),
3532
+ transcript_limit: integerFlag(flags, "--transcript-limit", 50)
3533
+ };
3534
+ copyOptionalFlag(flags, params, "--log-level", "log_level");
3535
+ await emitTool(ctx, "automation.run_debug", params);
3536
+ return;
3537
+ }
3538
+ if (sub === "timeline") {
3539
+ const flags = parseLocalFlags(args);
3540
+ await emitTool(ctx, "automation.run_timeline", {
3541
+ run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs timeline <run-id> [--limit 5000]"),
3542
+ limit: integerFlag(flags, "--limit", 5000)
3543
+ });
3544
+ return;
3545
+ }
3546
+ if (sub === "cancel") {
3547
+ if (!ctx.yes) {
3548
+ throw new CliError("Cancelling an automation run requires --yes because it mutates execution state.");
3549
+ }
3550
+ const flags = parseLocalFlags(args);
3551
+ await emitTool(ctx, "automation.run_cancel", {
3552
+ run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs cancel <run-id> --yes")
3553
+ });
3554
+ return;
3555
+ }
3556
+ throw new CliError("Usage: uru automations runs ls|tail|debug|timeline|cancel");
3557
+ }
3558
+ function copyOptionalFlag(flags, target, flagName, paramName) {
3559
+ const value = flags.values[flagName];
3560
+ if (value !== undefined) {
3561
+ target[paramName] = value;
3562
+ }
3563
+ }
3564
+ function copyAutomationView(flags, target) {
3565
+ const value = flags.values["--view"];
3566
+ if (value === undefined) {
3567
+ return;
3568
+ }
3569
+ if (value !== "summary" && value !== "detailed") {
3570
+ throw new CliError('--view must be "summary" or "detailed"');
3571
+ }
3572
+ target["view"] = value;
3149
3573
  }
3150
3574
  function mcpServerConfig(args) {
3151
3575
  const apiKey = requireCommandValue(args.apiKey, "MCP config requires --api-key, --token, URU_TOKEN, or a logged-in CLI token.");
@@ -3353,10 +3777,10 @@ async function gemBuild(ctx, args, linkedProject) {
3353
3777
  }
3354
3778
  async function gemPromote(ctx, args, linkedProject) {
3355
3779
  const flags = parseLocalFlags(args);
3356
- assertAllowedLocalFlags(flags, ["--gem", "--link-id", "--no-wait", "--path", "--pct"], "uru promote");
3780
+ assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--link-id", "--no-wait", "--path", "--pct"], "uru promote");
3357
3781
  await deployVersionToPublishedLink(ctx, flags, linkedProject, {
3358
3782
  operation: "promote",
3359
- usage: "Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>]"
3783
+ usage: "Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] | uru promote --deployment-id <deployment-id> [library/name.gem]"
3360
3784
  });
3361
3785
  }
3362
3786
  async function gemChecks(ctx, args, linkedProject) {
@@ -3366,10 +3790,11 @@ async function gemChecks(ctx, args, linkedProject) {
3366
3790
  return;
3367
3791
  }
3368
3792
  assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--path"], "uru gems checks");
3793
+ const explicitDeploymentId = flags.values["--deployment-id"];
3794
+ const gemPathArgs = explicitDeploymentId === undefined ? flags.positionals.slice(1) : flags.positionals;
3369
3795
  const params = {
3370
- path: gemPathFromArgs(flags.positionals.slice(1), linkedProject)
3796
+ path: gemPathFromArgs(gemPathArgs, linkedProject)
3371
3797
  };
3372
- const explicitDeploymentId = flags.values["--deployment-id"];
3373
3798
  if (explicitDeploymentId !== undefined) {
3374
3799
  params["deployment_id"] = explicitDeploymentId;
3375
3800
  } else if (flags.positionals[0] !== undefined) {
@@ -3576,13 +4001,21 @@ async function gemOpen(ctx, args, linkedProject) {
3576
4001
  }
3577
4002
  async function gemRollback(ctx, args, linkedProject) {
3578
4003
  const flags = parseLocalFlags(args);
3579
- assertAllowedLocalFlags(flags, ["--gem", "--link-id", "--no-wait", "--path", "--pct", "--yes"], "uru rollback");
4004
+ assertAllowedLocalFlags(flags, [
4005
+ "--deployment-id",
4006
+ "--gem",
4007
+ "--link-id",
4008
+ "--no-wait",
4009
+ "--path",
4010
+ "--pct",
4011
+ "--yes"
4012
+ ], "uru rollback");
3580
4013
  if (!ctx.yes && !flags.booleans.has("--yes")) {
3581
4014
  throw new CliError("Rollback requires --yes to confirm the link pointer change.");
3582
4015
  }
3583
4016
  await deployVersionToPublishedLink(ctx, flags, linkedProject, {
3584
4017
  operation: "rollback",
3585
- usage: "Usage: uru rollback <version-id> [library/name.gem] [--link-id <published-link-id>] --yes"
4018
+ usage: "Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes"
3586
4019
  });
3587
4020
  }
3588
4021
  async function gemWrite(ctx, args) {
@@ -3619,12 +4052,15 @@ async function dispatchGemVersions(ctx, args, linkedProject) {
3619
4052
  const gemPath = gemPathFromArgs(flags.positionals.slice(1), linkedProject);
3620
4053
  const pct = flags.values["--pct"] ?? trafficPercent;
3621
4054
  const trafficPercentValue = pct === undefined || pct === "" ? 100 : parseTrafficPercent(pct);
3622
- assertCurrentBackendTrafficSupport(trafficPercentValue);
4055
+ const publish = !flags.booleans.has("--skip-promote");
4056
+ if (publish && trafficPercentValue !== 100) {
4057
+ throw new CliError("Gradual Gem traffic shifts are not enabled yet. Use --skip-promote to create the deployment without changing live traffic, or deploy with --pct 100.");
4058
+ }
3623
4059
  const params = {
3624
4060
  path: gemPath,
3625
4061
  version_id: requireCommandValue(versionId, "Usage: uru gems versions deploy <version-id> [library/name.gem] [--pct 100] [--skip-promote]"),
3626
4062
  traffic_percent: trafficPercentValue,
3627
- publish: !flags.booleans.has("--skip-promote")
4063
+ publish
3628
4064
  };
3629
4065
  copyStringFlag(flags, params, "--link-id", "link_id");
3630
4066
  if (flags.booleans.has("--no-wait")) {
@@ -3659,13 +4095,17 @@ async function gemVersionUpload(ctx, args, linkedProject) {
3659
4095
  }
3660
4096
  copyStringFlag(flags, params, "--source-version-id", "source_version_id");
3661
4097
  copyStringFlag(flags, params, "--message", "message");
3662
- copyStringFlag(flags, params, "--preview-alias", "preview_alias");
4098
+ const previewAlias = flags.values["--preview-alias"];
4099
+ if (previewAlias !== undefined) {
4100
+ params["preview_alias"] = validatePreviewAlias(previewAlias);
4101
+ }
3663
4102
  const tags = tagsFromFlags(flags);
3664
4103
  if (tags.length > 0) {
3665
4104
  params["tags"] = tags;
3666
4105
  }
3667
4106
  await emitGemTool(ctx, "gem.version_upload", params, {
3668
- streamStart: { operation: "gem.version_upload", path: gemPath }
4107
+ streamStart: { operation: "gem.version_upload", path: gemPath },
4108
+ textExtractor: extractVersionUploadSummary
3669
4109
  });
3670
4110
  }
3671
4111
  function parseTrafficPercent(raw) {
@@ -3675,29 +4115,33 @@ function parseTrafficPercent(raw) {
3675
4115
  }
3676
4116
  return value;
3677
4117
  }
3678
- function assertCurrentBackendTrafficSupport(value) {
3679
- if (value !== 100) {
3680
- throw new CliError("Gradual percentage Gem deploys require backend traffic-splitting support; omit @pct/--pct or use 100 until split traffic is enabled.");
3681
- }
3682
- }
3683
4118
  async function deployVersionToPublishedLink(ctx, flags, linkedProject, options) {
3684
- const versionId = requireCommandValue(flags.positionals[0], options.usage);
3685
- const gemPath = gemPathFromArgs(flags.positionals.slice(1), linkedProject);
4119
+ const deploymentId = flags.values["--deployment-id"];
4120
+ const firstPositional = flags.positionals[0];
4121
+ const versionId = deploymentId === undefined && firstPositional !== undefined ? firstPositional.startsWith("library/") ? undefined : firstPositional : undefined;
4122
+ if (deploymentId === undefined && versionId === undefined) {
4123
+ throw new CliError(options.usage);
4124
+ }
4125
+ const gemPath = gemPathFromArgs(versionId === undefined ? flags.positionals : flags.positionals.slice(1), linkedProject);
3686
4126
  const trafficPercent = parseTrafficPercent(flags.values["--pct"] ?? "100");
3687
- assertCurrentBackendTrafficSupport(trafficPercent);
4127
+ if (trafficPercent !== 100) {
4128
+ throw new CliError(`${options.operation} does not support partial traffic shifts yet. Deploy the version with --skip-promote for validation, or ${options.operation} with --pct 100.`);
4129
+ }
3688
4130
  const params = {
3689
- path: gemPath,
3690
- version_id: versionId,
3691
- traffic_percent: trafficPercent,
3692
- publish: true,
3693
- operation: options.operation
4131
+ path: gemPath
3694
4132
  };
4133
+ if (deploymentId === undefined) {
4134
+ params["version_id"] = requireCommandValue(versionId, options.usage);
4135
+ } else {
4136
+ params["app_deployment_id"] = deploymentId;
4137
+ }
3695
4138
  copyStringFlag(flags, params, "--link-id", "link_id");
3696
4139
  if (flags.booleans.has("--no-wait")) {
3697
4140
  params["wait"] = false;
3698
4141
  }
3699
- await emitGemTool(ctx, "gem.version_deploy", params, {
3700
- streamStart: { operation: "gem.version_deploy", path: gemPath },
4142
+ const operation = `gem.${options.operation}`;
4143
+ await emitGemTool(ctx, operation, params, {
4144
+ streamStart: { operation, path: gemPath },
3701
4145
  textExtractor: extractDeploymentUrl
3702
4146
  });
3703
4147
  }
@@ -3708,6 +4152,13 @@ function tagsFromFlags(flags) {
3708
4152
  }
3709
4153
  return raw.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
3710
4154
  }
4155
+ function validatePreviewAlias(rawAlias) {
4156
+ const alias = rawAlias.trim();
4157
+ if (alias.length === 0 || alias.length > 63 || !/^[a-z][a-z0-9-]*[a-z0-9]$/u.test(alias)) {
4158
+ throw new CliError("--preview-alias must be a DNS-safe label: 1-63 characters, start with a lowercase letter, contain only lowercase letters, numbers, and dashes, and not end with a dash.");
4159
+ }
4160
+ return alias;
4161
+ }
3711
4162
  function assertAllowedLocalFlags(flags, allowedFlags, commandName) {
3712
4163
  const allowed = new Set(allowedFlags);
3713
4164
  const unexpected = localFlagNames(flags).filter((flag) => !allowed.has(flag));
@@ -3727,9 +4178,12 @@ function localFlagNames(flags) {
3727
4178
  }
3728
4179
  async function writeBuildManifest(ctx, outdir, buildResult) {
3729
4180
  const manifest = gemBuildManifestFromBuildResult(buildResult);
4181
+ const providerDecision = gemProviderDecisionFromBuildResult(buildResult, manifest);
3730
4182
  const absoluteOutdir = resolve2(ctx.cwd, outdir);
3731
4183
  await mkdir4(absoluteOutdir, { recursive: true });
3732
4184
  await writeFile4(join3(absoluteOutdir, "gem-build-manifest.json"), `${JSON.stringify(manifest, null, 2)}
4185
+ `, { mode: 384 });
4186
+ await writeFile4(join3(absoluteOutdir, "gem-provider-decision.json"), `${JSON.stringify(providerDecision, null, 2)}
3733
4187
  `, { mode: 384 });
3734
4188
  }
3735
4189
  function gemBuildManifestFromBuildResult(buildResult) {
@@ -3782,6 +4236,80 @@ function normalizeGemBuildManifest(value) {
3782
4236
  manifest_kind: "gem_build_manifest"
3783
4237
  };
3784
4238
  }
4239
+ function gemProviderDecisionFromBuildResult(buildResult, manifest) {
4240
+ const root = jsonObjectOrNull(buildResult);
4241
+ const build = jsonObjectOrNull(root?.["build"]);
4242
+ const buildResultDetails = jsonObjectOrNull(build?.["build_result"]);
4243
+ const buildMetadata = jsonObjectOrNull(buildResultDetails?.["metadata"]);
4244
+ const runtimeProfile = firstJsonObject([
4245
+ root?.["runtime_profile"],
4246
+ root?.["runtimeProfile"],
4247
+ build?.["runtime_profile"],
4248
+ build?.["runtimeProfile"],
4249
+ buildResultDetails?.["runtime_profile"],
4250
+ buildResultDetails?.["runtimeProfile"],
4251
+ buildMetadata?.["runtime_profile"],
4252
+ buildMetadata?.["runtimeProfile"]
4253
+ ]) ?? {};
4254
+ const providerDecision = firstJsonObject([
4255
+ root?.["provider_decision"],
4256
+ root?.["providerDecision"],
4257
+ build?.["provider_decision"],
4258
+ build?.["providerDecision"],
4259
+ buildResultDetails?.["provider_decision"],
4260
+ buildResultDetails?.["providerDecision"],
4261
+ buildMetadata?.["provider_decision"],
4262
+ buildMetadata?.["providerDecision"]
4263
+ ]) ?? {};
4264
+ const runtimeContract = firstJsonObject([
4265
+ root?.["runtime_contract"],
4266
+ root?.["runtimeContract"],
4267
+ build?.["runtime_contract"],
4268
+ build?.["runtimeContract"],
4269
+ buildResultDetails?.["runtime_contract"],
4270
+ buildResultDetails?.["runtimeContract"],
4271
+ buildMetadata?.["runtime_contract"],
4272
+ buildMetadata?.["runtimeContract"],
4273
+ manifest["runtime_contract"],
4274
+ manifest["runtimeContract"]
4275
+ ]) ?? {};
4276
+ return {
4277
+ schema: "uru.gem.provider_decision.v1",
4278
+ schema_version: 1,
4279
+ manifest_kind: "gem_provider_decision",
4280
+ provider: stringFieldFromJsonObjects([providerDecision, runtimeProfile, manifest], ["provider", "runtime_provider", "runtimeProvider", "selected_provider"]),
4281
+ runtime_class: stringFieldFromJsonObjects([providerDecision, runtimeProfile, manifest], ["runtime_class", "runtimeClass", "class", "serving_mode"]),
4282
+ runtime_profile: runtimeProfile,
4283
+ provider_decision: providerDecision,
4284
+ runtime_contract: runtimeContract,
4285
+ manifest_summary: {
4286
+ serving_mode: manifest["serving_mode"] ?? null,
4287
+ worker_compatibility: manifest["worker_compatibility"] ?? null,
4288
+ serverful_compatibility: manifest["serverful_compatibility"] ?? null,
4289
+ runtime_requirements: manifest["runtime_requirements"] ?? null
4290
+ }
4291
+ };
4292
+ }
4293
+ function firstJsonObject(values) {
4294
+ for (const value of values) {
4295
+ const object = jsonObjectOrNull(value);
4296
+ if (object !== null) {
4297
+ return object;
4298
+ }
4299
+ }
4300
+ return null;
4301
+ }
4302
+ function stringFieldFromJsonObjects(objects, keys) {
4303
+ for (const object of objects) {
4304
+ for (const key of keys) {
4305
+ const value = object[key];
4306
+ if (typeof value === "string" && value.trim() !== "") {
4307
+ return value.trim();
4308
+ }
4309
+ }
4310
+ }
4311
+ return null;
4312
+ }
3785
4313
  function jsonObjectOrNull(value) {
3786
4314
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
3787
4315
  return null;
@@ -3791,6 +4319,48 @@ function jsonObjectOrNull(value) {
3791
4319
  function extractDeploymentUrl(value) {
3792
4320
  return firstUrl(value);
3793
4321
  }
4322
+ function extractVersionUploadSummary(value) {
4323
+ const objects = collectJsonObjects(value);
4324
+ const versionId = stringFieldFromJsonObjects(objects, [
4325
+ "version_id",
4326
+ "versionId",
4327
+ "id"
4328
+ ]);
4329
+ const previewUrl = stringFieldFromJsonObjects(objects, [
4330
+ "preview_url",
4331
+ "previewUrl",
4332
+ "preview_alias_url",
4333
+ "previewAliasUrl",
4334
+ "url"
4335
+ ]) ?? firstUrl(value);
4336
+ const lines = [
4337
+ versionId === null ? null : `Version: ${versionId}`,
4338
+ previewUrl === undefined || previewUrl === null ? null : `Preview: ${previewUrl}`
4339
+ ].filter((line) => line !== null);
4340
+ return lines.length === 0 ? undefined : lines.join(`
4341
+ `);
4342
+ }
4343
+ function collectJsonObjects(value) {
4344
+ const objects = [];
4345
+ collectJsonObjectsInto(value, objects);
4346
+ return objects;
4347
+ }
4348
+ function collectJsonObjectsInto(value, objects) {
4349
+ if (Array.isArray(value)) {
4350
+ for (const item of value) {
4351
+ collectJsonObjectsInto(item, objects);
4352
+ }
4353
+ return;
4354
+ }
4355
+ const object = jsonObjectOrNull(value);
4356
+ if (object === null) {
4357
+ return;
4358
+ }
4359
+ objects.push(object);
4360
+ for (const child of Object.values(object)) {
4361
+ collectJsonObjectsInto(child, objects);
4362
+ }
4363
+ }
3794
4364
  function trimTrailingSlashes(value) {
3795
4365
  let end = value.length;
3796
4366
  while (end > 0 && value.charCodeAt(end - 1) === 47) {
@@ -3807,10 +4377,20 @@ function trimLeadingSlashes(value) {
3807
4377
  }
3808
4378
 
3809
4379
  // src/gem-source-commands.ts
3810
- import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
4380
+ import { mkdir as mkdir5, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
3811
4381
  import { dirname as dirname4, join as join4, resolve as resolve3 } from "node:path";
3812
4382
  async function gemSourcePull(ctx, args, linkedProject) {
3813
- const gemPath = gemPathFromArgs(args, linkedProject);
4383
+ const flags = parseLocalFlags(args);
4384
+ const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
4385
+ const force = flags.booleans.has("--force");
4386
+ const previousLock = await readSourceLock(ctx.cwd);
4387
+ const localFilesBeforePull = await collectLocalSourceFiles(ctx.cwd);
4388
+ const localStatusBeforePull = compareSourceLock(previousLock, localFilesBeforePull);
4389
+ if (!force && !localStatusBeforePull.clean) {
4390
+ throw new CliError(dirtyPullErrorMessage(localStatusBeforePull), {
4391
+ code: ExitCode.Conflict
4392
+ });
4393
+ }
3814
4394
  const tree = unwrapToolResult(await executeOperation(ctx, "gem.fs_tree", {
3815
4395
  path: gemPath,
3816
4396
  recursive: true
@@ -3830,6 +4410,7 @@ async function gemSourcePull(ctx, args, linkedProject) {
3830
4410
  await writePulledSourceFile(ctx.cwd, filePath, content);
3831
4411
  files[filePath] = sha256Hex(content);
3832
4412
  }
4413
+ const pruned = await pruneRemoteDeletedSourceFiles(ctx.cwd, previousLock, localFilesBeforePull, new Set(filePaths));
3833
4414
  const lock = {
3834
4415
  ...sourceVersionId === undefined ? {} : { sourceVersionId },
3835
4416
  files,
@@ -3840,8 +4421,10 @@ async function gemSourcePull(ctx, args, linkedProject) {
3840
4421
  ok: true,
3841
4422
  path: gemPath,
3842
4423
  files: filePaths,
4424
+ prunedFiles: pruned.deleted,
4425
+ preservedFiles: pruned.preserved,
3843
4426
  ...sourceVersionId === undefined ? {} : { sourceVersionId }
3844
- }, `Pulled ${filePaths.length} file${filePaths.length === 1 ? "" : "s"} from ${gemPath}`);
4427
+ }, formatPullSummary(gemPath, filePaths.length, pruned));
3845
4428
  }
3846
4429
  async function gemSourceStatus(ctx, args, linkedProject) {
3847
4430
  const flags = parseLocalFlags(args);
@@ -3901,24 +4484,16 @@ async function gemSourcePush(ctx, args, linkedProject) {
3901
4484
  ...lock.sourceVersionId === undefined ? {} : { sourceVersionId: lock.sourceVersionId }
3902
4485
  };
3903
4486
  try {
3904
- for (const deletedPath of localStatus.deleted) {
3905
- const deleteResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_rm", {
3906
- path: gemFilePath(gemPath, deletedPath),
3907
- recursive: false,
3908
- ...sourceMutationCasParams(sourceCas, force),
3909
- change_summary: force ? "Force-push local Gem source deletion via Uru CLI." : "Push local Gem source deletion via Uru CLI."
3910
- }));
3911
- sourceCas = nextSourceMutationCas(sourceCas, deleteResult);
3912
- }
3913
- if (sortedFiles.length > 0) {
3914
- const writeResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_batch_write", {
3915
- path: gemPath,
3916
- files: sortedFiles.map(localFileBatchWritePayload),
3917
- ...sourceMutationCasParams(sourceCas, force),
3918
- change_summary: force ? "Force-push local Gem source via Uru CLI." : "Push local Gem source via Uru CLI."
3919
- }));
3920
- sourceCas = nextSourceMutationCas(sourceCas, writeResult);
3921
- }
4487
+ const syncResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_sync", {
4488
+ path: gemPath,
4489
+ files: sortedFiles.map(localFileBatchWritePayload),
4490
+ delete_paths: localStatus.deleted.map((deletedPath) => ({
4491
+ path: deletedPath
4492
+ })),
4493
+ ...sourceMutationCasParams(sourceCas, force),
4494
+ change_summary: force ? "Force-push local Gem source via Uru CLI." : "Push local Gem source via Uru CLI."
4495
+ }));
4496
+ sourceCas = nextSourceMutationCas(sourceCas, syncResult);
3922
4497
  } catch (error) {
3923
4498
  if (isConflictLikeError(error)) {
3924
4499
  throw new CliError(error instanceof Error ? error.message : "Gem source conflict", { code: ExitCode.Conflict, cause: error });
@@ -3945,6 +4520,44 @@ function localFileBatchWritePayload(file) {
3945
4520
  content_base64: file.contentBase64
3946
4521
  };
3947
4522
  }
4523
+ async function pruneRemoteDeletedSourceFiles(cwd, previousLock, localFilesBeforePull, remotePaths) {
4524
+ const deleted = [];
4525
+ const preserved = [];
4526
+ const lockedFiles = Object.entries(previousLock.files ?? {}).sort(([left], [right]) => left.localeCompare(right));
4527
+ for (const [projectPath2, lockedHash] of lockedFiles) {
4528
+ if (remotePaths.has(projectPath2)) {
4529
+ continue;
4530
+ }
4531
+ const localFile = localFilesBeforePull[projectPath2];
4532
+ if (localFile === undefined) {
4533
+ continue;
4534
+ }
4535
+ if (localFile.sha256 !== lockedHash) {
4536
+ preserved.push(projectPath2);
4537
+ continue;
4538
+ }
4539
+ const safePath = safeProjectPath(projectPath2);
4540
+ if (safePath === null) {
4541
+ preserved.push(projectPath2);
4542
+ continue;
4543
+ }
4544
+ await rm3(join4(resolve3(cwd), ...safePath.split("/")), { force: true });
4545
+ deleted.push(projectPath2);
4546
+ }
4547
+ return { deleted, preserved };
4548
+ }
4549
+ function formatPullSummary(gemPath, writtenCount, pruned) {
4550
+ const parts = [
4551
+ `Pulled ${writtenCount} file${writtenCount === 1 ? "" : "s"} from ${gemPath}`
4552
+ ];
4553
+ if (pruned.deleted.length > 0) {
4554
+ parts.push(`pruned ${pruned.deleted.length} remote-deleted file${pruned.deleted.length === 1 ? "" : "s"}`);
4555
+ }
4556
+ if (pruned.preserved.length > 0) {
4557
+ parts.push(`preserved ${pruned.preserved.length} locally changed remote-deleted file${pruned.preserved.length === 1 ? "" : "s"}`);
4558
+ }
4559
+ return parts.join("; ");
4560
+ }
3948
4561
  function sourceMutationCasParams(cas, force) {
3949
4562
  if (force || cas.sourceVersionId === undefined) {
3950
4563
  return { force: true };
@@ -4036,6 +4649,17 @@ function formatLocalStatusEntries(status) {
4036
4649
  ...status.deleted.map((path) => `D ${path}`)
4037
4650
  ];
4038
4651
  }
4652
+ function dirtyPullErrorMessage(status) {
4653
+ const entries = formatLocalStatusEntries(status);
4654
+ const preview = entries.slice(0, 10).map((entry) => ` ${entry}`).join(`
4655
+ `);
4656
+ const moreCount = Math.max(0, entries.length - 10);
4657
+ const moreLine = moreCount > 0 ? `
4658
+ ...and ${moreCount} more local change(s)` : "";
4659
+ return `Refusing to pull because local Gem source has uncommitted changes that could be overwritten.
4660
+ Run \`uru status\` to inspect them, save or push your changes, or re-run \`uru pull --force\` to replace local files with server source.
4661
+ ${preview}${moreLine}`;
4662
+ }
4039
4663
  async function tryRemoteSourceOperation(ctx, operationId, params) {
4040
4664
  try {
4041
4665
  return {
@@ -4198,15 +4822,29 @@ import { dirname as dirname5, resolve as resolve4 } from "node:path";
4198
4822
  async function dispatchTokens(ctx, sub, args, linkedProject) {
4199
4823
  if (sub === "create") {
4200
4824
  const flags = parseLocalFlags(args);
4825
+ const usage = "Usage: uru tokens create [library/name.gem] [--gem <id>] --name <name> (--expiry 30d|--expires-at <iso>)";
4826
+ const name = requireCommandValue(flags.values["--name"], usage);
4827
+ const expiry = flags.values["--expiry"];
4828
+ const expiresAt = flags.values["--expires-at"];
4829
+ if (expiry === undefined && expiresAt === undefined) {
4830
+ throw new CliError("Gem deploy token creation requires --expiry <duration> or --expires-at <iso timestamp>.");
4831
+ }
4832
+ if (expiry !== undefined && expiresAt !== undefined) {
4833
+ throw new CliError("Pass only one token lifetime flag: --expiry or --expires-at.");
4834
+ }
4201
4835
  const params = {
4202
4836
  ...tokenGemRefParams(flags, linkedProject, {
4203
4837
  required: true,
4204
- usage: "Usage: uru tokens create [library/name.gem] [--gem <id>] [--name <name>] [--expiry 30d]"
4205
- })
4838
+ usage
4839
+ }),
4840
+ name
4206
4841
  };
4207
- copyStringFlag(flags, params, "--name", "name");
4208
- copyStringFlag(flags, params, "--expiry", "expiry");
4209
- copyStringFlag(flags, params, "--expires-at", "expires_at");
4842
+ if (expiry !== undefined) {
4843
+ params["expiry"] = expiry;
4844
+ }
4845
+ if (expiresAt !== undefined) {
4846
+ params["expires_at"] = expiresAt;
4847
+ }
4210
4848
  const scopes = tokenScopes(flags);
4211
4849
  if (scopes.length > 0) {
4212
4850
  params["scopes"] = scopes;
@@ -4640,6 +5278,7 @@ async function runCli(options) {
4640
5278
  credentialStore,
4641
5279
  apiUrl,
4642
5280
  ...token !== undefined ? { token } : {},
5281
+ tokenIsExplicit: flagToken !== undefined || envToken !== undefined,
4643
5282
  ...workspace !== undefined ? { workspace } : {},
4644
5283
  cwd,
4645
5284
  json: outputFormat === "json",
@@ -4661,6 +5300,9 @@ async function runCli(options) {
4661
5300
  };
4662
5301
  throwIfCancelled(cancellation.signal);
4663
5302
  await dispatchWithImplicitLogin(ctx, parsed.command, linkedProject);
5303
+ await maybeNotifyCliUpdate(ctx, {
5304
+ ...options.env === undefined ? {} : { env: options.env }
5305
+ });
4664
5306
  return ExitCode.Ok;
4665
5307
  } catch (error) {
4666
5308
  const cliError = normalizeError(error);
@@ -4894,6 +5536,11 @@ async function dispatch(ctx, command, linkedProject) {
4894
5536
  await dispatchDatasets(ctx, sub, command.slice(2));
4895
5537
  return;
4896
5538
  }
5539
+ if (head === "automations") {
5540
+ await withWorkspace(ctx);
5541
+ await dispatchAutomations(ctx, sub, command.slice(2));
5542
+ return;
5543
+ }
4897
5544
  if (head === "tokens" || head === "token") {
4898
5545
  await withWorkspace(ctx);
4899
5546
  await dispatchTokens(ctx, sub, command.slice(2), linkedProject);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uru-intelligence/cli",
3
- "version": "0.3.53",
3
+ "version": "0.3.54",
4
4
  "private": false,
5
5
  "description": "Uru full-platform command line interface",
6
6
  "type": "module",