@uru-intelligence/cli 0.3.53 → 0.3.55

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 +35 -5
  2. package/dist/uru.mjs +881 -114
  3. package/package.json +1 -1
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.55";
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"),
@@ -240,8 +241,12 @@ function buildOperationGroups(op) {
240
241
  const libraryOperations = [
241
242
  op("library.ls", "library", "List visible Library folders and resources.", "library_query", "ls", "read"),
242
243
  op("library.stat", "library", "Read metadata for a visible Library path.", "library_query", "stat", "read"),
244
+ op("library.read", "library", "Read a visible text-backed Library resource by path.", "library_query", "read", "read"),
243
245
  op("library.search", "library", "Search visible Library resources server-side.", "library_query", "search", "read"),
244
246
  op("library.mkdir", "library", "Create a Library folder.", "library_fs", "mkdir", "write"),
247
+ op("library.write", "library", "Create or replace a text-backed Library resource by path.", "library_fs", "write", "write"),
248
+ op("library.patch", "library", "Replace text content for a text-backed Library resource by path.", "library_fs", "patch", "write"),
249
+ op("library.cp", "library", "Copy a supported Library item when the backing resource supports copy semantics.", "library_fs", "cp", "write"),
245
250
  op("library.mv", "library", "Move or rename a Library item.", "library_fs", "mv", "write"),
246
251
  op("library.move_workspace", "library", "Move a Library item to another workspace.", "library_fs", "move_workspace", "destructive"),
247
252
  op("library.rm", "library", "Trash a Library item.", "library_fs", "rm", "destructive"),
@@ -249,19 +254,17 @@ function buildOperationGroups(op) {
249
254
  op("library.empty_trash", "library", "Permanently empty Library trash.", "library_fs", "empty_trash", "destructive"),
250
255
  op("library.upload_preflight", "library", "Preflight a Library upload.", "library_upload", "preflight", "read"),
251
256
  op("library.upload_create_session", "library", "Create a direct Library upload session.", "library_upload", "create_session", "write"),
252
- op("library.upload_complete_session", "library", "Complete a direct Library upload session.", "library_upload", "complete_session", "write"),
253
- op("context.manage", "library", "Manage typed Context Library metadata such as workspace-default, teams, category, and favorite state.", "context_manage", undefined, "write"),
254
- op("prompt.render", "library", "Render a typed Prompt Library resource with supplied variables.", "prompt_render", undefined, "read"),
255
- op("persona.query", "library", "Query typed Persona Library resources and linked prompt/context resources.", "persona_query", undefined, "read")
257
+ op("library.upload_complete_session", "library", "Complete a direct Library upload session.", "library_upload", "complete_session", "write")
256
258
  ];
257
259
  const sharingOperations = [
258
260
  op("sharing.share", "sharing", "Share a Library resource with a user, team, or workspace.", "library_collab", "share", "write"),
259
261
  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"),
262
+ op("sharing.permissions_list", "sharing", "List permissions for a Library resource.", "sharing_query", "list_permissions", "read"),
263
+ op("sharing.shares_list", "sharing", "List active share grants for a Library resource.", "sharing_query", "list_shares", "read"),
261
264
  op("sharing.permissions_set", "sharing", "Set permissions for a Library resource.", "library_collab", "set_permissions", "write"),
262
265
  op("sharing.settings_set", "sharing", "Set share settings for a Library resource.", "library_collab", "set_share_settings", "write"),
263
266
  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"),
267
+ op("sharing.versions_list", "sharing", "List Library resource versions.", "sharing_query", "list_versions", "read"),
265
268
  op("sharing.version_restore", "sharing", "Restore a Library resource version.", "library_collab", "restore_version", "destructive"),
266
269
  op("sharing.comment", "sharing", "Comment on a Library resource.", "library_collab", "comment", "write"),
267
270
  op("sharing.comment_resolve", "sharing", "Resolve a Library resource comment.", "library_collab", "resolve_comment", "write"),
@@ -414,7 +417,7 @@ function buildOperationGroups(op) {
414
417
  op("file.download_for_user", "files", "Prepare a file download for the user.", "DOWNLOAD_FILE_FOR_USER", undefined, "read")
415
418
  ];
416
419
  const businessSqlOperations = [
417
- op("business_sql.transcript", "business_sql", "Query transcript SQL.", "TRANSCRIPT_SQL", undefined, "read"),
420
+ op("business_sql.transcript", "business_sql", "Query transcript SQL.", "transcript_sql", undefined, "read"),
418
421
  op("business_sql.maguire", "business_sql", "Query Maguire SQL.", "MAGUIRE_SQL", undefined, "read"),
419
422
  op("business_sql.tyler", "business_sql", "Query Tyler SQL.", "TYLER_SQL", undefined, "read"),
420
423
  op("business_sql.cfo", "business_sql", "Query CFO SQL.", "CFO_SQL", undefined, "read"),
@@ -471,10 +474,13 @@ var internalOnlyPlatformToolIds = new Set([
471
474
  "MAGUIRE_WRITE_SQL",
472
475
  "PEREGRINE_SQL",
473
476
  "RESTORE_DATASET_ROWS",
474
- "TRANSCRIPT_SQL",
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]
@@ -1093,20 +1184,29 @@ LIBRARY / DATASETS / TOOLS
1093
1184
  uru library ls [path] List Library items
1094
1185
  uru library search <query> Search Library items
1095
1186
  uru library mkdir <path> Create a Library folder
1187
+ uru library read <path> Print a text-backed Library resource
1188
+ uru library write <path> --file ./note.md --create Create/replace text-backed Library content
1189
+ uru library patch <path> --content "..." Replace text-backed Library content
1190
+ uru library cp|mv <path> <target-path> Copy or move visible Library items
1191
+ uru library rm <path> [--permanent --yes] Trash or permanently delete Library items
1192
+ uru library restore <path> Restore a trashed Library item
1096
1193
  # 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
1194
+ uru datasets query <dataset-slug> [--limit 50] Query dataset rows; add --response-format concise for compact output
1195
+ uru automations ls|get|create|enable|disable Manage automations without raw JSON parameters
1196
+ uru automations trigger <id> Start a manual automation run
1197
+ uru automations runs ls|tail <run-id> Inspect automation runs and logs
1098
1198
  uru secrets put <name> [library/name.gem] --value-stdin Store Gem secret without shell history
1099
1199
  uru secrets ls [library/name.gem] List Gem secret metadata only
1100
1200
  uru secrets rm <name> [library/name.gem] --yes Remove a Gem secret value
1101
1201
  uru env pull [library/name.gem] Write a local metadata-only .env template
1102
1202
  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
1203
+ uru tokens create [library/name.gem] --name CI --expiry 30d Create a Gem-scoped deploy token
1104
1204
  uru tokens ls [library/name.gem] List Gem token metadata only
1105
1205
  uru tokens revoke <token-id> --yes Revoke a Gem token immediately
1106
1206
  uru tools ls|schema <name>|run <name> Full platform-tool parity
1107
1207
  uru operations ls [--family gems] Print generated operation registry
1108
1208
  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
1209
+ uru workspace|sharing|web <op> Registry families for full-platform parity
1110
1210
  uru mcp config|install Print/write MCP client config
1111
1211
  uru complete bash|zsh|fish Print shell completions
1112
1212
  uru update Print the self-update command
@@ -1180,8 +1280,6 @@ qualified operation form.
1180
1280
  var REGISTRY_FAMILY_COMMANDS = {
1181
1281
  workspace: "workspace",
1182
1282
  sharing: "sharing",
1183
- automations: "automations",
1184
- automation: "automations",
1185
1283
  web: "web",
1186
1284
  files: "files",
1187
1285
  file: "files",
@@ -1242,13 +1340,16 @@ deployment that can be promoted after checks; --prod cannot be combined with
1242
1340
  --no-publish.
1243
1341
  `,
1244
1342
  promote: `Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
1343
+ uru promote --deployment-id <deployment-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
1245
1344
 
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.
1345
+ Promote an already-staged immutable Gem version or ready deployment. With
1346
+ --link-id, this is a published-link pointer flip after checks pass. If --link-id
1347
+ is omitted, Uru promotes through the default clean Gem link.
1248
1348
  `,
1249
- pull: `Usage: uru pull [library/name.gem]
1349
+ pull: `Usage: uru pull [library/name.gem] [--force]
1250
1350
 
1251
1351
  Download Gem source into the current directory and write .uru/source-lock.json.
1352
+ Refuses to overwrite dirty local files unless --force is supplied.
1252
1353
  `,
1253
1354
  status: `Usage: uru status [library/name.gem] [--json]
1254
1355
 
@@ -1275,11 +1376,12 @@ before the command to emit NDJSON events.
1275
1376
 
1276
1377
  Print the first available hosted/published Gem URL.
1277
1378
  `,
1278
- rollback: `Usage: uru rollback <version-id> [library/name.gem] [--link-id <published-link-id>] --yes
1379
+ rollback: `Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes
1279
1380
 
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.
1381
+ Rollback retargets a live link to an already-materialized older Gem version or
1382
+ deployment after server-side rollback preflight passes. Requires --yes because it
1383
+ mutates live traffic. If --link-id is omitted, Uru rolls back through the active
1384
+ Primary Gem link.
1283
1385
  `,
1284
1386
  gems: `Usage: uru gems <inspect|validate|build|status|read|write|versions|checks> [...]
1285
1387
 
@@ -1290,8 +1392,9 @@ same platform operations used by MCP and the web UI.
1290
1392
  uru gems versions deploy <version-id> [library/name.gem] [--pct 100]
1291
1393
 
1292
1394
  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.
1395
+ Upload accepts --message, --tag/--tags, and --preview-alias. Non-100
1396
+ percentages are accepted only with --skip-promote to stage rollout intent; live
1397
+ promote/rollback requires 100 until weighted link routing is enabled.
1295
1398
  `,
1296
1399
  "gems checks": `Usage: uru gems checks <version-id> [library/name.gem] [--deployment-id <id>]
1297
1400
  uru gems checks run browser|security [library/name.gem] [--deployment-id <id>] [--blocking]
@@ -1303,12 +1406,39 @@ the check a release gate; security also accepts --deep.
1303
1406
  library: `Usage: uru library ls [path]
1304
1407
  uru library search <query>
1305
1408
  uru library mkdir <path>
1409
+ uru library read <path>
1410
+ uru library write <path> (--file <local-path>|--content <text>|--stdin) [--create]
1411
+ uru library patch <path> (--file <local-path>|--content <text>|--stdin)
1412
+ uru library cp <path> <target-path>
1413
+ uru library mv <path> <target-path>
1414
+ uru library rm <path> [--permanent] [--yes]
1415
+ uru library restore <path>
1416
+ uru library empty-trash --yes
1306
1417
 
1307
- Operate on Library items through platform tools.
1418
+ Operate on Library items through platform tools. Use write --create to create
1419
+ text-backed Context, Prompt, Persona, note, and plain .md/.txt resources at
1420
+ visible Library paths. For binary files, use library_upload through
1421
+ \`uru operations run\` or \`uru tools run\`; for Gem child source files, use
1422
+ \`uru gems read|write\`.
1308
1423
  `,
1309
- datasets: `Usage: uru datasets query <dataset-slug> [--limit 50] [--offset 0]
1424
+ datasets: `Usage: uru datasets query <dataset-slug> [--limit 50] [--offset 0] [--response-format detailed|concise]
1310
1425
 
1311
- Query dataset rows through the platform dataset operation.
1426
+ Query dataset rows through the platform dataset operation. Use concise for id plus selected/display fields while keeping server totals and aggregates.
1427
+ `,
1428
+ automations: `Usage: uru automations ls [--status draft|active|paused|archived] [--limit 100] [--view summary|detailed]
1429
+ uru automations get <automation-id>
1430
+ uru automations create <name> [--description <text>] [--status draft|active|paused]
1431
+ uru automations enable <automation-id>
1432
+ uru automations disable <automation-id>
1433
+ uru automations trigger <automation-id> [--input-message <text>] [--idempotency-key <key>]
1434
+ uru automations runs ls [automation-id] [--scope personal|workspace] [--limit 100]
1435
+ uru automations runs tail <run-id> [--limit 1000] [--level debug|info|warn|error]
1436
+ uru automations runs debug|timeline <run-id>
1437
+ uru automations runs cancel <run-id> --yes
1438
+
1439
+ Manage automations through first-class CLI commands backed by the shared
1440
+ operation registry. Reads default to compact summary views; use --view detailed
1441
+ only when you need full graph/config payloads.
1312
1442
  `,
1313
1443
  tokens: `Usage: uru tokens create [library/name.gem] --expiry <duration> --name <name>
1314
1444
  uru tokens ls [library/name.gem]
@@ -1412,6 +1542,19 @@ async function emitTool(ctx, operationId, params, options = {}) {
1412
1542
  operation: options.streamStart["operation"] ?? operationId,
1413
1543
  phase: "requesting"
1414
1544
  });
1545
+ const streamed = await executeOperationStream(ctx, operationId, params, (event) => {
1546
+ writeStreamJsonEvent(ctx.io, event);
1547
+ });
1548
+ if (streamed.streamed) {
1549
+ return;
1550
+ }
1551
+ const unwrapped2 = unwrapToolResult(streamed.payload ?? null);
1552
+ const output2 = options.transform?.(unwrapped2) ?? unwrapped2;
1553
+ writeStreamJson(ctx.io, "result", output2);
1554
+ writeStreamJson(ctx.io, "done", {
1555
+ operation: options.streamStart["operation"] ?? operationId
1556
+ });
1557
+ return;
1415
1558
  }
1416
1559
  const result = await executeOperation(ctx, operationId, params);
1417
1560
  const unwrapped = unwrapToolResult(result);
@@ -1429,6 +1572,17 @@ async function emitTool(ctx, operationId, params, options = {}) {
1429
1572
  writeText(ctx.io, options.textExtractor?.(output) ?? JSON.stringify(output, null, 2));
1430
1573
  }
1431
1574
  }
1575
+ async function executeOperationStream(ctx, operationId, params, onEvent) {
1576
+ const operation = getPlatformOperation(operationId);
1577
+ if (operation === undefined) {
1578
+ throw new CliError(`Unknown operation registry id: ${operationId}`);
1579
+ }
1580
+ const toolParams = { ...params };
1581
+ if (operation.backendOp !== undefined) {
1582
+ toolParams["op"] = operation.backendOp;
1583
+ }
1584
+ return client(ctx).streamTool(operation.backendToolName, toolParams, onEvent);
1585
+ }
1432
1586
  async function executeOperation(ctx, operationId, params) {
1433
1587
  const operation = getPlatformOperation(operationId);
1434
1588
  if (operation === undefined) {
@@ -1474,7 +1628,7 @@ function tokenGemRefParams(flags, linkedProject, options = {}) {
1474
1628
  }
1475
1629
  const explicitGem = flags.values["--gem"];
1476
1630
  if (explicitGem !== undefined) {
1477
- return { gem_id: explicitGem };
1631
+ return explicitGem.startsWith("library/") ? { path: explicitGem } : { gem_id: explicitGem };
1478
1632
  }
1479
1633
  const positional = flags.positionals[0];
1480
1634
  if (positional !== undefined) {
@@ -1624,6 +1778,18 @@ function sanitizeConfig(value) {
1624
1778
  if (value["insecureStorage"] === true) {
1625
1779
  config.insecureStorage = true;
1626
1780
  }
1781
+ if (isRecord(value["update"])) {
1782
+ const update = {};
1783
+ for (const key of ["lastCheckedAt", "latestVersion"]) {
1784
+ const item = value["update"][key];
1785
+ if (typeof item === "string" && item.trim() !== "") {
1786
+ update[key] = item;
1787
+ }
1788
+ }
1789
+ if (Object.keys(update).length > 0) {
1790
+ config.update = update;
1791
+ }
1792
+ }
1627
1793
  return config;
1628
1794
  }
1629
1795
  function isNodeError(error) {
@@ -2008,10 +2174,15 @@ function credentialStoreUnavailableError(backend, message, error) {
2008
2174
  var CLI_DOCS_URL = "https://docs.uruintelligence.com/cli/errors";
2009
2175
  var ERROR_TIPS = {
2010
2176
  auth_required: "Run `uru login`, pass `--token`, or set `URU_TOKEN` before retrying.",
2177
+ interactive_login_required: "Run `uru login` before this command, use `uru login --device` on remote shells, or pass `--token`/`URU_TOKEN` in CI.",
2011
2178
  conflict: "Run `uru status` or `uru pull` to inspect remote changes; use `--force` only when you intentionally want to overwrite.",
2012
2179
  api_request_failed: "Check the request parameters, workspace context, and server response before retrying.",
2013
2180
  keychain_access_failed: "Unlock the OS credential store, or retry with `--insecure-storage` only if plaintext local storage is acceptable.",
2014
2181
  keychain_unavailable: "Install or unlock the OS credential helper, or retry with `--insecure-storage` only if plaintext local storage is acceptable.",
2182
+ oauth_state_mismatch: "Restart `uru login`; this usually means the browser callback came from an old or mismatched login tab.",
2183
+ oauth_authorization_failed: "Restart `uru login` and approve access in the browser, or use `uru login --device` if the browser cannot reach the callback.",
2184
+ oauth_missing_code: "Restart `uru login`; if the browser callback keeps failing, retry with `uru login --device`.",
2185
+ oauth_request_failed: "Check the API URL and network access, then retry `uru login`; use `uru login --device` from SSH/headless environments.",
2015
2186
  oauth_device_failed: "Restart `uru login --device` and complete authorization in the browser before the code expires.",
2016
2187
  oauth_device_timeout: "Run `uru login --device` again and complete browser authorization before the timeout.",
2017
2188
  cancelled: "The command was cancelled; rerun it only if you still want the operation to continue."
@@ -2257,6 +2428,120 @@ function terminalWidth(stream, env) {
2257
2428
  return 80;
2258
2429
  }
2259
2430
 
2431
+ // src/update-notifier.ts
2432
+ var PACKAGE_NAME = "@uru-intelligence/cli";
2433
+ var REGISTRY_LATEST_URL = "https://registry.npmjs.org/@uru-intelligence%2Fcli/latest";
2434
+ var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
2435
+ var UPDATE_CHECK_TIMEOUT_MS = 750;
2436
+ async function maybeNotifyCliUpdate(ctx, options = {}) {
2437
+ if (!shouldCheckForUpdate(ctx, options.env ?? process.env)) {
2438
+ return;
2439
+ }
2440
+ const now = options.now?.() ?? Date.now();
2441
+ if (!isUpdateCheckDue(ctx.config, now)) {
2442
+ return;
2443
+ }
2444
+ const checkedConfig = withUpdateCheckTimestamp(ctx.config, now);
2445
+ await writeConfigBestEffort(ctx, checkedConfig);
2446
+ const latestVersion = await fetchLatestVersion({
2447
+ fetchImpl: ctx.fetch ?? fetch,
2448
+ parentSignal: ctx.abortSignal
2449
+ });
2450
+ if (latestVersion === undefined) {
2451
+ return;
2452
+ }
2453
+ await writeConfigBestEffort(ctx, {
2454
+ ...checkedConfig,
2455
+ update: {
2456
+ ...checkedConfig.update,
2457
+ latestVersion
2458
+ }
2459
+ });
2460
+ if (compareVersions(latestVersion, CLI_VERSION) <= 0) {
2461
+ return;
2462
+ }
2463
+ ctx.io.stderr.write(`A newer Uru CLI is available: ${CLI_VERSION} → ${latestVersion}. ` + `Run npm install -g ${PACKAGE_NAME}@latest
2464
+ `);
2465
+ }
2466
+ function shouldCheckForUpdate(ctx, env) {
2467
+ return ctx.outputFormat === "text" && ctx.terminal.stderrIsTty && !ctx.terminal.ci && env["NO_UPDATE_NOTIFIER"] === undefined && env["URU_NO_UPDATE_NOTIFIER"] === undefined;
2468
+ }
2469
+ function isUpdateCheckDue(config, now) {
2470
+ const raw = config.update?.lastCheckedAt;
2471
+ if (raw === undefined) {
2472
+ return true;
2473
+ }
2474
+ const lastCheckedAt = Date.parse(raw);
2475
+ return !Number.isFinite(lastCheckedAt) || now - lastCheckedAt >= UPDATE_CHECK_INTERVAL_MS;
2476
+ }
2477
+ function withUpdateCheckTimestamp(config, now) {
2478
+ return {
2479
+ ...config,
2480
+ update: {
2481
+ ...config.update,
2482
+ lastCheckedAt: new Date(now).toISOString()
2483
+ }
2484
+ };
2485
+ }
2486
+ async function fetchLatestVersion(args) {
2487
+ const controller = new AbortController;
2488
+ const timeout = setTimeout(() => {
2489
+ controller.abort();
2490
+ }, UPDATE_CHECK_TIMEOUT_MS);
2491
+ timeout.unref?.();
2492
+ const abortFromParent = () => {
2493
+ controller.abort(args.parentSignal.reason);
2494
+ };
2495
+ if (args.parentSignal.aborted) {
2496
+ abortFromParent();
2497
+ } else {
2498
+ args.parentSignal.addEventListener("abort", abortFromParent, { once: true });
2499
+ }
2500
+ try {
2501
+ const response = await args.fetchImpl(REGISTRY_LATEST_URL, {
2502
+ headers: { Accept: "application/json" },
2503
+ signal: controller.signal
2504
+ });
2505
+ if (!response.ok) {
2506
+ return;
2507
+ }
2508
+ const payload = await response.json();
2509
+ if (!isRecord2(payload)) {
2510
+ return;
2511
+ }
2512
+ const version = payload["version"];
2513
+ return typeof version === "string" && version.trim() !== "" ? version.trim() : undefined;
2514
+ } catch {
2515
+ return;
2516
+ } finally {
2517
+ clearTimeout(timeout);
2518
+ args.parentSignal.removeEventListener("abort", abortFromParent);
2519
+ }
2520
+ }
2521
+ async function writeConfigBestEffort(ctx, config) {
2522
+ try {
2523
+ await ctx.store.write(config);
2524
+ } catch {}
2525
+ }
2526
+ function compareVersions(left, right) {
2527
+ const leftParts = versionParts(left);
2528
+ const rightParts = versionParts(right);
2529
+ const width = Math.max(leftParts.length, rightParts.length);
2530
+ for (let index = 0;index < width; index += 1) {
2531
+ const delta = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
2532
+ if (delta !== 0) {
2533
+ return delta;
2534
+ }
2535
+ }
2536
+ return 0;
2537
+ }
2538
+ function versionParts(version) {
2539
+ return version.replace(/^v/, "").split(/[.-]/).map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
2540
+ }
2541
+ function isRecord2(value) {
2542
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2543
+ }
2544
+
2260
2545
  // src/auth-commands.ts
2261
2546
  import { readFile as readFile3 } from "node:fs/promises";
2262
2547
  import { createServer } from "node:http";
@@ -2633,14 +2918,19 @@ async function resolveFlagToken(flags, stdin) {
2633
2918
  return;
2634
2919
  }
2635
2920
  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();
2921
+ const existingToken = ctx.token === undefined || ctx.token.trim() === "" ? undefined : ctx.token;
2922
+ let token = existingToken ?? await obtainLoginSessionToken(ctx);
2923
+ let result;
2924
+ try {
2925
+ result = await validateSessionToken(ctx, token);
2926
+ } catch (error) {
2927
+ const storedTokenIsStale = existingToken !== undefined && !ctx.tokenIsExplicit && isAuthRequiredError(error);
2928
+ if (!storedTokenIsStale) {
2929
+ throw error;
2930
+ }
2931
+ token = await obtainLoginSessionToken(ctx);
2932
+ result = await validateSessionToken(ctx, token);
2933
+ }
2644
2934
  const selectedWorkspace = selectWorkspace(result, ctx.workspace);
2645
2935
  await persistLoginToken(ctx, token, selectedWorkspace);
2646
2936
  if (options.silentSuccess === true) {
@@ -2661,6 +2951,18 @@ async function login(ctx, options = {}) {
2661
2951
  writeText(ctx.io, ctx.insecureStorage ? "Logged in (token stored in local config)" : "Logged in");
2662
2952
  }
2663
2953
  }
2954
+ async function validateSessionToken(ctx, token) {
2955
+ const loginClient = new UruApiClient({
2956
+ apiUrl: ctx.apiUrl,
2957
+ token,
2958
+ ...ctx.workspace !== undefined ? { workspaceId: ctx.workspace } : {},
2959
+ ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
2960
+ });
2961
+ return loginClient.validateToken();
2962
+ }
2963
+ function isAuthRequiredError(error) {
2964
+ return error instanceof CliError && error.exitCode === ExitCode.AuthRequired;
2965
+ }
2664
2966
  async function readAllStdin(input) {
2665
2967
  const chunks = [];
2666
2968
  for await (const chunk of input) {
@@ -2900,13 +3202,11 @@ async function switchWorkspace(ctx, workspaceId) {
2900
3202
  }
2901
3203
 
2902
3204
  // src/local-commands.ts
2903
- import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
2904
- import { dirname as dirname3 } from "node:path";
3205
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
3206
+ import { dirname as dirname3, resolve as resolve2 } from "node:path";
2905
3207
  var GENERIC_FAMILY_COMMANDS = {
2906
3208
  workspace: "workspace",
2907
3209
  sharing: "sharing",
2908
- automations: "automations",
2909
- automation: "automations",
2910
3210
  web: "web",
2911
3211
  files: "files",
2912
3212
  file: "files",
@@ -2942,6 +3242,7 @@ var TOP_LEVEL_COMMANDS = [
2942
3242
  "open",
2943
3243
  "rollback",
2944
3244
  "gems",
3245
+ "automations",
2945
3246
  "library",
2946
3247
  "datasets",
2947
3248
  "tokens",
@@ -3131,7 +3432,109 @@ async function dispatchLibrary(ctx, sub, args) {
3131
3432
  });
3132
3433
  return;
3133
3434
  }
3134
- throw new CliError("Usage: uru library ls|search|mkdir");
3435
+ if (sub === "read" || sub === "cat") {
3436
+ await emitTool(ctx, "library.read", {
3437
+ path: requireCommandValue(args[0], "Usage: uru library read <path>")
3438
+ }, { textExtractor: extractLibraryTextContent });
3439
+ return;
3440
+ }
3441
+ if (sub === "write") {
3442
+ const flags = parseLocalFlags(args);
3443
+ const path = requireCommandValue(flags.positionals[0], "Usage: uru library write <path> (--file <local-path>|--content <text>|--stdin) [--create]");
3444
+ await emitTool(ctx, "library.write", {
3445
+ path,
3446
+ content: await libraryTextFromFlags(ctx, flags, "write"),
3447
+ create: flags.booleans.has("--create")
3448
+ });
3449
+ return;
3450
+ }
3451
+ if (sub === "patch") {
3452
+ const flags = parseLocalFlags(args);
3453
+ const path = requireCommandValue(flags.positionals[0], "Usage: uru library patch <path> (--file <local-path>|--content <text>|--stdin)");
3454
+ await emitTool(ctx, "library.patch", {
3455
+ path,
3456
+ content: await libraryTextFromFlags(ctx, flags, "patch")
3457
+ });
3458
+ return;
3459
+ }
3460
+ if (sub === "cp" || sub === "copy") {
3461
+ const [path, targetPath] = args;
3462
+ await emitTool(ctx, "library.cp", {
3463
+ path: requireCommandValue(path, "Usage: uru library cp <path> <target-path>"),
3464
+ target_path: requireCommandValue(targetPath, "Usage: uru library cp <path> <target-path>")
3465
+ });
3466
+ return;
3467
+ }
3468
+ if (sub === "mv" || sub === "move" || sub === "rename") {
3469
+ const [path, targetPath] = args;
3470
+ await emitTool(ctx, "library.mv", {
3471
+ path: requireCommandValue(path, "Usage: uru library mv <path> <target-path>"),
3472
+ target_path: requireCommandValue(targetPath, "Usage: uru library mv <path> <target-path>")
3473
+ });
3474
+ return;
3475
+ }
3476
+ if (sub === "rm" || sub === "remove" || sub === "delete") {
3477
+ const flags = parseLocalFlags(args);
3478
+ if ((flags.booleans.has("--permanent") || flags.booleans.has("--hard-delete")) && !ctx.yes && !flags.booleans.has("--yes")) {
3479
+ throw new CliError("Permanent Library deletion cannot be undone. Re-run with --yes to confirm.");
3480
+ }
3481
+ const path = requireCommandValue(flags.positionals[0], "Usage: uru library rm <path> [--permanent] [--yes]");
3482
+ await emitTool(ctx, "library.rm", {
3483
+ path,
3484
+ permanent: flags.booleans.has("--permanent") || flags.booleans.has("--hard-delete")
3485
+ });
3486
+ return;
3487
+ }
3488
+ if (sub === "restore") {
3489
+ await emitTool(ctx, "library.restore", {
3490
+ path: requireCommandValue(args[0], "Usage: uru library restore <path>"),
3491
+ include_trashed: true
3492
+ });
3493
+ return;
3494
+ }
3495
+ if (sub === "empty-trash") {
3496
+ const flags = parseLocalFlags(args);
3497
+ if (!ctx.yes && !flags.booleans.has("--yes")) {
3498
+ throw new CliError("Emptying Library trash permanently deletes all trashed items. Re-run with --yes to confirm.");
3499
+ }
3500
+ await emitTool(ctx, "library.empty_trash", {});
3501
+ return;
3502
+ }
3503
+ throw new CliError("Usage: uru library ls|search|mkdir|read|write|patch|cp|mv|rm|restore|empty-trash");
3504
+ }
3505
+ async function libraryTextFromFlags(ctx, flags, verb) {
3506
+ const content = flags.values["--content"];
3507
+ const file = flags.values["--file"];
3508
+ const fromStdin = flags.booleans.has("--stdin");
3509
+ const sources = [content !== undefined, file !== undefined, fromStdin].filter(Boolean).length;
3510
+ if (sources !== 1) {
3511
+ throw new CliError(`Library ${verb} requires exactly one of --file <path>, --content <text>, or --stdin.`);
3512
+ }
3513
+ if (content !== undefined) {
3514
+ return content;
3515
+ }
3516
+ if (file !== undefined) {
3517
+ return await readFile4(resolve2(ctx.cwd, file), "utf8");
3518
+ }
3519
+ return await readStdinText(ctx.stdin ?? process.stdin);
3520
+ }
3521
+ async function readStdinText(stdin) {
3522
+ const chunks = [];
3523
+ for await (const chunk of stdin) {
3524
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
3525
+ }
3526
+ return Buffer.concat(chunks).toString("utf8");
3527
+ }
3528
+ function extractLibraryTextContent(value) {
3529
+ if (!isJsonObject2(value)) {
3530
+ return;
3531
+ }
3532
+ const content = value["content"];
3533
+ if (typeof content === "string") {
3534
+ return content;
3535
+ }
3536
+ const text = value["text"];
3537
+ return typeof text === "string" ? text : undefined;
3135
3538
  }
3136
3539
  async function dispatchDatasets(ctx, sub, args) {
3137
3540
  if (sub !== "query") {
@@ -3139,13 +3542,154 @@ async function dispatchDatasets(ctx, sub, args) {
3139
3542
  return;
3140
3543
  }
3141
3544
  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", {
3545
+ const datasetId = requireCommandValue(flags.positionals[0], "Usage: uru datasets query <dataset-slug> [--limit 50] [--response-format detailed|concise]");
3546
+ const params = {
3144
3547
  dataset_id: datasetId,
3145
3548
  datasetId,
3146
3549
  limit: integerFlag(flags, "--limit", 50),
3147
3550
  offset: integerFlag(flags, "--offset", 0)
3148
- });
3551
+ };
3552
+ const responseFormat = flags.values["--response-format"];
3553
+ if (responseFormat !== undefined) {
3554
+ if (responseFormat !== "detailed" && responseFormat !== "concise") {
3555
+ throw new CliError('--response-format must be "detailed" or "concise"');
3556
+ }
3557
+ params["response_format"] = responseFormat;
3558
+ }
3559
+ await emitTool(ctx, "dataset.query", params);
3560
+ }
3561
+ async function dispatchAutomations(ctx, sub, args) {
3562
+ if (sub === "ls" || sub === "list") {
3563
+ const flags = parseLocalFlags(args);
3564
+ const params = {
3565
+ limit: integerFlag(flags, "--limit", 100)
3566
+ };
3567
+ copyOptionalFlag(flags, params, "--status", "status_filter");
3568
+ copyOptionalFlag(flags, params, "--parent-folder-id", "parent_folder_id");
3569
+ copyOptionalFlag(flags, params, "--folder-id", "parent_folder_id");
3570
+ copyAutomationView(flags, params);
3571
+ await emitTool(ctx, "automation.list", params);
3572
+ return;
3573
+ }
3574
+ if (sub === "get" || sub === "inspect") {
3575
+ const flags = parseLocalFlags(args);
3576
+ await emitTool(ctx, "automation.get", {
3577
+ automation_id: requireCommandValue(flags.positionals[0], "Usage: uru automations get <automation-id>")
3578
+ });
3579
+ return;
3580
+ }
3581
+ if (sub === "create") {
3582
+ const flags = parseLocalFlags(args);
3583
+ const params = {
3584
+ name: requireCommandValue(flags.positionals[0], "Usage: uru automations create <name> [--description <text>] [--status draft|active|paused] [--graph-json {...}]")
3585
+ };
3586
+ copyOptionalFlag(flags, params, "--description", "description");
3587
+ copyOptionalFlag(flags, params, "--status", "status");
3588
+ copyOptionalFlag(flags, params, "--parent-folder-id", "parent_folder_id");
3589
+ copyOptionalFlag(flags, params, "--folder-id", "parent_folder_id");
3590
+ const graphJson = flags.values["--graph-json"];
3591
+ if (graphJson !== undefined) {
3592
+ params["graph_json"] = parseParamsJson(graphJson, "--graph-json");
3593
+ }
3594
+ await emitTool(ctx, "automation.create", params);
3595
+ return;
3596
+ }
3597
+ if (sub === "enable" || sub === "disable") {
3598
+ const flags = parseLocalFlags(args);
3599
+ await emitTool(ctx, "automation.update", {
3600
+ automation_id: requireCommandValue(flags.positionals[0], `Usage: uru automations ${sub} <automation-id>`),
3601
+ status: sub === "enable" ? "active" : "paused"
3602
+ });
3603
+ return;
3604
+ }
3605
+ if (sub === "trigger" || sub === "run" || sub === "start") {
3606
+ const flags = parseLocalFlags(args);
3607
+ const params = {
3608
+ automation_id: requireCommandValue(flags.values["--automation-id"] ?? flags.positionals[0], "Usage: uru automations trigger <automation-id> [--input-message <text>] [--idempotency-key <key>]")
3609
+ };
3610
+ copyOptionalFlag(flags, params, "--input-message", "input_message");
3611
+ copyOptionalFlag(flags, params, "--idempotency-key", "idempotency_key");
3612
+ await emitTool(ctx, "automation.run_start", params);
3613
+ return;
3614
+ }
3615
+ if (sub === "runs") {
3616
+ await dispatchAutomationRuns(ctx, args[0], args.slice(1));
3617
+ return;
3618
+ }
3619
+ throw new CliError("Usage: uru automations ls|get|create|enable|disable|trigger|runs");
3620
+ }
3621
+ async function dispatchAutomationRuns(ctx, sub, args) {
3622
+ if (sub === "ls" || sub === "list") {
3623
+ const flags = parseLocalFlags(args);
3624
+ const params = {
3625
+ limit: integerFlag(flags, "--limit", 100)
3626
+ };
3627
+ copyOptionalFlag(flags, params, "--automation-id", "automation_id");
3628
+ copyOptionalFlag(flags, params, "--scope", "scope");
3629
+ copyOptionalFlag(flags, params, "--cursor", "cursor");
3630
+ copyOptionalFlag(flags, params, "--include", "include");
3631
+ if (params["automation_id"] === undefined && flags.positionals[0] !== undefined) {
3632
+ params["automation_id"] = flags.positionals[0];
3633
+ }
3634
+ await emitTool(ctx, "automation.runs_list", params);
3635
+ return;
3636
+ }
3637
+ if (sub === "tail" || sub === "logs") {
3638
+ const flags = parseLocalFlags(args);
3639
+ const params = {
3640
+ run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs tail <run-id> [--limit 1000] [--level debug|info|warn|error]"),
3641
+ limit: integerFlag(flags, "--limit", 1000)
3642
+ };
3643
+ copyOptionalFlag(flags, params, "--level", "level");
3644
+ await emitTool(ctx, "automation.run_logs", params);
3645
+ return;
3646
+ }
3647
+ if (sub === "debug") {
3648
+ const flags = parseLocalFlags(args);
3649
+ const params = {
3650
+ run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs debug <run-id> [--log-limit 100] [--transcript-limit 50]"),
3651
+ log_limit: integerFlag(flags, "--log-limit", 100),
3652
+ transcript_limit: integerFlag(flags, "--transcript-limit", 50)
3653
+ };
3654
+ copyOptionalFlag(flags, params, "--log-level", "log_level");
3655
+ await emitTool(ctx, "automation.run_debug", params);
3656
+ return;
3657
+ }
3658
+ if (sub === "timeline") {
3659
+ const flags = parseLocalFlags(args);
3660
+ await emitTool(ctx, "automation.run_timeline", {
3661
+ run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs timeline <run-id> [--limit 5000]"),
3662
+ limit: integerFlag(flags, "--limit", 5000)
3663
+ });
3664
+ return;
3665
+ }
3666
+ if (sub === "cancel") {
3667
+ if (!ctx.yes) {
3668
+ throw new CliError("Cancelling an automation run requires --yes because it mutates execution state.");
3669
+ }
3670
+ const flags = parseLocalFlags(args);
3671
+ await emitTool(ctx, "automation.run_cancel", {
3672
+ run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs cancel <run-id> --yes")
3673
+ });
3674
+ return;
3675
+ }
3676
+ throw new CliError("Usage: uru automations runs ls|tail|debug|timeline|cancel");
3677
+ }
3678
+ function copyOptionalFlag(flags, target, flagName, paramName) {
3679
+ const value = flags.values[flagName];
3680
+ if (value !== undefined) {
3681
+ target[paramName] = value;
3682
+ }
3683
+ }
3684
+ function copyAutomationView(flags, target) {
3685
+ const value = flags.values["--view"];
3686
+ if (value === undefined) {
3687
+ return;
3688
+ }
3689
+ if (value !== "summary" && value !== "detailed") {
3690
+ throw new CliError('--view must be "summary" or "detailed"');
3691
+ }
3692
+ target["view"] = value;
3149
3693
  }
3150
3694
  function mcpServerConfig(args) {
3151
3695
  const apiKey = requireCommandValue(args.apiKey, "MCP config requires --api-key, --token, URU_TOKEN, or a logged-in CLI token.");
@@ -3199,9 +3743,9 @@ function levenshtein(left, right) {
3199
3743
  }
3200
3744
 
3201
3745
  // src/gem-lifecycle-commands.ts
3202
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
3746
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
3203
3747
  import { setTimeout as sleep } from "node:timers/promises";
3204
- import { join as join3, resolve as resolve2 } from "node:path";
3748
+ import { join as join3, resolve as resolve3 } from "node:path";
3205
3749
  async function dispatchGems(ctx, sub, args, linkedProject) {
3206
3750
  const gemPath = () => gemPathFromArgs(args, linkedProject);
3207
3751
  if (sub === "inspect") {
@@ -3254,7 +3798,7 @@ async function gemInit(ctx, args) {
3254
3798
  params["content"] = flags.values["--content"];
3255
3799
  }
3256
3800
  if (flags.values["--file"] !== undefined) {
3257
- params["content"] = await readFile4(flags.values["--file"], "utf8");
3801
+ params["content"] = await readFile5(flags.values["--file"], "utf8");
3258
3802
  }
3259
3803
  await emitGemTool(ctx, "gem.init", params);
3260
3804
  }
@@ -3353,10 +3897,10 @@ async function gemBuild(ctx, args, linkedProject) {
3353
3897
  }
3354
3898
  async function gemPromote(ctx, args, linkedProject) {
3355
3899
  const flags = parseLocalFlags(args);
3356
- assertAllowedLocalFlags(flags, ["--gem", "--link-id", "--no-wait", "--path", "--pct"], "uru promote");
3900
+ assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--link-id", "--no-wait", "--path", "--pct"], "uru promote");
3357
3901
  await deployVersionToPublishedLink(ctx, flags, linkedProject, {
3358
3902
  operation: "promote",
3359
- usage: "Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>]"
3903
+ usage: "Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] | uru promote --deployment-id <deployment-id> [library/name.gem]"
3360
3904
  });
3361
3905
  }
3362
3906
  async function gemChecks(ctx, args, linkedProject) {
@@ -3366,10 +3910,11 @@ async function gemChecks(ctx, args, linkedProject) {
3366
3910
  return;
3367
3911
  }
3368
3912
  assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--path"], "uru gems checks");
3913
+ const explicitDeploymentId = flags.values["--deployment-id"];
3914
+ const gemPathArgs = explicitDeploymentId === undefined ? flags.positionals.slice(1) : flags.positionals;
3369
3915
  const params = {
3370
- path: gemPathFromArgs(flags.positionals.slice(1), linkedProject)
3916
+ path: gemPathFromArgs(gemPathArgs, linkedProject)
3371
3917
  };
3372
- const explicitDeploymentId = flags.values["--deployment-id"];
3373
3918
  if (explicitDeploymentId !== undefined) {
3374
3919
  params["deployment_id"] = explicitDeploymentId;
3375
3920
  } else if (flags.positionals[0] !== undefined) {
@@ -3576,20 +4121,28 @@ async function gemOpen(ctx, args, linkedProject) {
3576
4121
  }
3577
4122
  async function gemRollback(ctx, args, linkedProject) {
3578
4123
  const flags = parseLocalFlags(args);
3579
- assertAllowedLocalFlags(flags, ["--gem", "--link-id", "--no-wait", "--path", "--pct", "--yes"], "uru rollback");
4124
+ assertAllowedLocalFlags(flags, [
4125
+ "--deployment-id",
4126
+ "--gem",
4127
+ "--link-id",
4128
+ "--no-wait",
4129
+ "--path",
4130
+ "--pct",
4131
+ "--yes"
4132
+ ], "uru rollback");
3580
4133
  if (!ctx.yes && !flags.booleans.has("--yes")) {
3581
4134
  throw new CliError("Rollback requires --yes to confirm the link pointer change.");
3582
4135
  }
3583
4136
  await deployVersionToPublishedLink(ctx, flags, linkedProject, {
3584
4137
  operation: "rollback",
3585
- usage: "Usage: uru rollback <version-id> [library/name.gem] [--link-id <published-link-id>] --yes"
4138
+ usage: "Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes"
3586
4139
  });
3587
4140
  }
3588
4141
  async function gemWrite(ctx, args) {
3589
4142
  const flags = parseLocalFlags(args);
3590
4143
  const gemPath = requireCommandValue(flags.positionals[0], "Usage: uru gems write <gem-path> <file> --content <text>|--file <local-file>");
3591
4144
  const filePath = requireCommandValue(flags.positionals[1], "Usage: uru gems write <gem-path> <file> --content <text>|--file <local-file>");
3592
- const content = flags.values["--content"] ?? (flags.values["--file"] === undefined ? undefined : await readFile4(flags.values["--file"], "utf8"));
4145
+ const content = flags.values["--content"] ?? (flags.values["--file"] === undefined ? undefined : await readFile5(flags.values["--file"], "utf8"));
3593
4146
  if (content === undefined) {
3594
4147
  throw new CliError("Gem write requires --content or --file.");
3595
4148
  }
@@ -3619,12 +4172,15 @@ async function dispatchGemVersions(ctx, args, linkedProject) {
3619
4172
  const gemPath = gemPathFromArgs(flags.positionals.slice(1), linkedProject);
3620
4173
  const pct = flags.values["--pct"] ?? trafficPercent;
3621
4174
  const trafficPercentValue = pct === undefined || pct === "" ? 100 : parseTrafficPercent(pct);
3622
- assertCurrentBackendTrafficSupport(trafficPercentValue);
4175
+ const publish = !flags.booleans.has("--skip-promote");
4176
+ if (publish && trafficPercentValue !== 100) {
4177
+ 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.");
4178
+ }
3623
4179
  const params = {
3624
4180
  path: gemPath,
3625
4181
  version_id: requireCommandValue(versionId, "Usage: uru gems versions deploy <version-id> [library/name.gem] [--pct 100] [--skip-promote]"),
3626
4182
  traffic_percent: trafficPercentValue,
3627
- publish: !flags.booleans.has("--skip-promote")
4183
+ publish
3628
4184
  };
3629
4185
  copyStringFlag(flags, params, "--link-id", "link_id");
3630
4186
  if (flags.booleans.has("--no-wait")) {
@@ -3659,13 +4215,17 @@ async function gemVersionUpload(ctx, args, linkedProject) {
3659
4215
  }
3660
4216
  copyStringFlag(flags, params, "--source-version-id", "source_version_id");
3661
4217
  copyStringFlag(flags, params, "--message", "message");
3662
- copyStringFlag(flags, params, "--preview-alias", "preview_alias");
4218
+ const previewAlias = flags.values["--preview-alias"];
4219
+ if (previewAlias !== undefined) {
4220
+ params["preview_alias"] = validatePreviewAlias(previewAlias);
4221
+ }
3663
4222
  const tags = tagsFromFlags(flags);
3664
4223
  if (tags.length > 0) {
3665
4224
  params["tags"] = tags;
3666
4225
  }
3667
4226
  await emitGemTool(ctx, "gem.version_upload", params, {
3668
- streamStart: { operation: "gem.version_upload", path: gemPath }
4227
+ streamStart: { operation: "gem.version_upload", path: gemPath },
4228
+ textExtractor: extractVersionUploadSummary
3669
4229
  });
3670
4230
  }
3671
4231
  function parseTrafficPercent(raw) {
@@ -3675,29 +4235,33 @@ function parseTrafficPercent(raw) {
3675
4235
  }
3676
4236
  return value;
3677
4237
  }
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
4238
  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);
4239
+ const deploymentId = flags.values["--deployment-id"];
4240
+ const firstPositional = flags.positionals[0];
4241
+ const versionId = deploymentId === undefined && firstPositional !== undefined ? firstPositional.startsWith("library/") ? undefined : firstPositional : undefined;
4242
+ if (deploymentId === undefined && versionId === undefined) {
4243
+ throw new CliError(options.usage);
4244
+ }
4245
+ const gemPath = gemPathFromArgs(versionId === undefined ? flags.positionals : flags.positionals.slice(1), linkedProject);
3686
4246
  const trafficPercent = parseTrafficPercent(flags.values["--pct"] ?? "100");
3687
- assertCurrentBackendTrafficSupport(trafficPercent);
4247
+ if (trafficPercent !== 100) {
4248
+ 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.`);
4249
+ }
3688
4250
  const params = {
3689
- path: gemPath,
3690
- version_id: versionId,
3691
- traffic_percent: trafficPercent,
3692
- publish: true,
3693
- operation: options.operation
4251
+ path: gemPath
3694
4252
  };
4253
+ if (deploymentId === undefined) {
4254
+ params["version_id"] = requireCommandValue(versionId, options.usage);
4255
+ } else {
4256
+ params["app_deployment_id"] = deploymentId;
4257
+ }
3695
4258
  copyStringFlag(flags, params, "--link-id", "link_id");
3696
4259
  if (flags.booleans.has("--no-wait")) {
3697
4260
  params["wait"] = false;
3698
4261
  }
3699
- await emitGemTool(ctx, "gem.version_deploy", params, {
3700
- streamStart: { operation: "gem.version_deploy", path: gemPath },
4262
+ const operation = `gem.${options.operation}`;
4263
+ await emitGemTool(ctx, operation, params, {
4264
+ streamStart: { operation, path: gemPath },
3701
4265
  textExtractor: extractDeploymentUrl
3702
4266
  });
3703
4267
  }
@@ -3708,6 +4272,13 @@ function tagsFromFlags(flags) {
3708
4272
  }
3709
4273
  return raw.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
3710
4274
  }
4275
+ function validatePreviewAlias(rawAlias) {
4276
+ const alias = rawAlias.trim();
4277
+ if (alias.length === 0 || alias.length > 63 || !/^[a-z][a-z0-9-]*[a-z0-9]$/u.test(alias)) {
4278
+ 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.");
4279
+ }
4280
+ return alias;
4281
+ }
3711
4282
  function assertAllowedLocalFlags(flags, allowedFlags, commandName) {
3712
4283
  const allowed = new Set(allowedFlags);
3713
4284
  const unexpected = localFlagNames(flags).filter((flag) => !allowed.has(flag));
@@ -3727,9 +4298,12 @@ function localFlagNames(flags) {
3727
4298
  }
3728
4299
  async function writeBuildManifest(ctx, outdir, buildResult) {
3729
4300
  const manifest = gemBuildManifestFromBuildResult(buildResult);
3730
- const absoluteOutdir = resolve2(ctx.cwd, outdir);
4301
+ const providerDecision = gemProviderDecisionFromBuildResult(buildResult, manifest);
4302
+ const absoluteOutdir = resolve3(ctx.cwd, outdir);
3731
4303
  await mkdir4(absoluteOutdir, { recursive: true });
3732
4304
  await writeFile4(join3(absoluteOutdir, "gem-build-manifest.json"), `${JSON.stringify(manifest, null, 2)}
4305
+ `, { mode: 384 });
4306
+ await writeFile4(join3(absoluteOutdir, "gem-provider-decision.json"), `${JSON.stringify(providerDecision, null, 2)}
3733
4307
  `, { mode: 384 });
3734
4308
  }
3735
4309
  function gemBuildManifestFromBuildResult(buildResult) {
@@ -3782,6 +4356,80 @@ function normalizeGemBuildManifest(value) {
3782
4356
  manifest_kind: "gem_build_manifest"
3783
4357
  };
3784
4358
  }
4359
+ function gemProviderDecisionFromBuildResult(buildResult, manifest) {
4360
+ const root = jsonObjectOrNull(buildResult);
4361
+ const build = jsonObjectOrNull(root?.["build"]);
4362
+ const buildResultDetails = jsonObjectOrNull(build?.["build_result"]);
4363
+ const buildMetadata = jsonObjectOrNull(buildResultDetails?.["metadata"]);
4364
+ const runtimeProfile = firstJsonObject([
4365
+ root?.["runtime_profile"],
4366
+ root?.["runtimeProfile"],
4367
+ build?.["runtime_profile"],
4368
+ build?.["runtimeProfile"],
4369
+ buildResultDetails?.["runtime_profile"],
4370
+ buildResultDetails?.["runtimeProfile"],
4371
+ buildMetadata?.["runtime_profile"],
4372
+ buildMetadata?.["runtimeProfile"]
4373
+ ]) ?? {};
4374
+ const providerDecision = firstJsonObject([
4375
+ root?.["provider_decision"],
4376
+ root?.["providerDecision"],
4377
+ build?.["provider_decision"],
4378
+ build?.["providerDecision"],
4379
+ buildResultDetails?.["provider_decision"],
4380
+ buildResultDetails?.["providerDecision"],
4381
+ buildMetadata?.["provider_decision"],
4382
+ buildMetadata?.["providerDecision"]
4383
+ ]) ?? {};
4384
+ const runtimeContract = firstJsonObject([
4385
+ root?.["runtime_contract"],
4386
+ root?.["runtimeContract"],
4387
+ build?.["runtime_contract"],
4388
+ build?.["runtimeContract"],
4389
+ buildResultDetails?.["runtime_contract"],
4390
+ buildResultDetails?.["runtimeContract"],
4391
+ buildMetadata?.["runtime_contract"],
4392
+ buildMetadata?.["runtimeContract"],
4393
+ manifest["runtime_contract"],
4394
+ manifest["runtimeContract"]
4395
+ ]) ?? {};
4396
+ return {
4397
+ schema: "uru.gem.provider_decision.v1",
4398
+ schema_version: 1,
4399
+ manifest_kind: "gem_provider_decision",
4400
+ provider: stringFieldFromJsonObjects([providerDecision, runtimeProfile, manifest], ["provider", "runtime_provider", "runtimeProvider", "selected_provider"]),
4401
+ runtime_class: stringFieldFromJsonObjects([providerDecision, runtimeProfile, manifest], ["runtime_class", "runtimeClass", "class", "serving_mode"]),
4402
+ runtime_profile: runtimeProfile,
4403
+ provider_decision: providerDecision,
4404
+ runtime_contract: runtimeContract,
4405
+ manifest_summary: {
4406
+ serving_mode: manifest["serving_mode"] ?? null,
4407
+ worker_compatibility: manifest["worker_compatibility"] ?? null,
4408
+ serverful_compatibility: manifest["serverful_compatibility"] ?? null,
4409
+ runtime_requirements: manifest["runtime_requirements"] ?? null
4410
+ }
4411
+ };
4412
+ }
4413
+ function firstJsonObject(values) {
4414
+ for (const value of values) {
4415
+ const object = jsonObjectOrNull(value);
4416
+ if (object !== null) {
4417
+ return object;
4418
+ }
4419
+ }
4420
+ return null;
4421
+ }
4422
+ function stringFieldFromJsonObjects(objects, keys) {
4423
+ for (const object of objects) {
4424
+ for (const key of keys) {
4425
+ const value = object[key];
4426
+ if (typeof value === "string" && value.trim() !== "") {
4427
+ return value.trim();
4428
+ }
4429
+ }
4430
+ }
4431
+ return null;
4432
+ }
3785
4433
  function jsonObjectOrNull(value) {
3786
4434
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
3787
4435
  return null;
@@ -3791,6 +4439,48 @@ function jsonObjectOrNull(value) {
3791
4439
  function extractDeploymentUrl(value) {
3792
4440
  return firstUrl(value);
3793
4441
  }
4442
+ function extractVersionUploadSummary(value) {
4443
+ const objects = collectJsonObjects(value);
4444
+ const versionId = stringFieldFromJsonObjects(objects, [
4445
+ "version_id",
4446
+ "versionId",
4447
+ "id"
4448
+ ]);
4449
+ const previewUrl = stringFieldFromJsonObjects(objects, [
4450
+ "preview_url",
4451
+ "previewUrl",
4452
+ "preview_alias_url",
4453
+ "previewAliasUrl",
4454
+ "url"
4455
+ ]) ?? firstUrl(value);
4456
+ const lines = [
4457
+ versionId === null ? null : `Version: ${versionId}`,
4458
+ previewUrl === undefined || previewUrl === null ? null : `Preview: ${previewUrl}`
4459
+ ].filter((line) => line !== null);
4460
+ return lines.length === 0 ? undefined : lines.join(`
4461
+ `);
4462
+ }
4463
+ function collectJsonObjects(value) {
4464
+ const objects = [];
4465
+ collectJsonObjectsInto(value, objects);
4466
+ return objects;
4467
+ }
4468
+ function collectJsonObjectsInto(value, objects) {
4469
+ if (Array.isArray(value)) {
4470
+ for (const item of value) {
4471
+ collectJsonObjectsInto(item, objects);
4472
+ }
4473
+ return;
4474
+ }
4475
+ const object = jsonObjectOrNull(value);
4476
+ if (object === null) {
4477
+ return;
4478
+ }
4479
+ objects.push(object);
4480
+ for (const child of Object.values(object)) {
4481
+ collectJsonObjectsInto(child, objects);
4482
+ }
4483
+ }
3794
4484
  function trimTrailingSlashes(value) {
3795
4485
  let end = value.length;
3796
4486
  while (end > 0 && value.charCodeAt(end - 1) === 47) {
@@ -3807,10 +4497,20 @@ function trimLeadingSlashes(value) {
3807
4497
  }
3808
4498
 
3809
4499
  // src/gem-source-commands.ts
3810
- import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
3811
- import { dirname as dirname4, join as join4, resolve as resolve3 } from "node:path";
4500
+ import { mkdir as mkdir5, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
4501
+ import { dirname as dirname4, join as join4, resolve as resolve4 } from "node:path";
3812
4502
  async function gemSourcePull(ctx, args, linkedProject) {
3813
- const gemPath = gemPathFromArgs(args, linkedProject);
4503
+ const flags = parseLocalFlags(args);
4504
+ const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
4505
+ const force = flags.booleans.has("--force");
4506
+ const previousLock = await readSourceLock(ctx.cwd);
4507
+ const localFilesBeforePull = await collectLocalSourceFiles(ctx.cwd);
4508
+ const localStatusBeforePull = compareSourceLock(previousLock, localFilesBeforePull);
4509
+ if (!force && !localStatusBeforePull.clean) {
4510
+ throw new CliError(dirtyPullErrorMessage(localStatusBeforePull), {
4511
+ code: ExitCode.Conflict
4512
+ });
4513
+ }
3814
4514
  const tree = unwrapToolResult(await executeOperation(ctx, "gem.fs_tree", {
3815
4515
  path: gemPath,
3816
4516
  recursive: true
@@ -3830,6 +4530,7 @@ async function gemSourcePull(ctx, args, linkedProject) {
3830
4530
  await writePulledSourceFile(ctx.cwd, filePath, content);
3831
4531
  files[filePath] = sha256Hex(content);
3832
4532
  }
4533
+ const pruned = await pruneRemoteDeletedSourceFiles(ctx.cwd, previousLock, localFilesBeforePull, new Set(filePaths));
3833
4534
  const lock = {
3834
4535
  ...sourceVersionId === undefined ? {} : { sourceVersionId },
3835
4536
  files,
@@ -3840,8 +4541,10 @@ async function gemSourcePull(ctx, args, linkedProject) {
3840
4541
  ok: true,
3841
4542
  path: gemPath,
3842
4543
  files: filePaths,
4544
+ prunedFiles: pruned.deleted,
4545
+ preservedFiles: pruned.preserved,
3843
4546
  ...sourceVersionId === undefined ? {} : { sourceVersionId }
3844
- }, `Pulled ${filePaths.length} file${filePaths.length === 1 ? "" : "s"} from ${gemPath}`);
4547
+ }, formatPullSummary(gemPath, filePaths.length, pruned));
3845
4548
  }
3846
4549
  async function gemSourceStatus(ctx, args, linkedProject) {
3847
4550
  const flags = parseLocalFlags(args);
@@ -3901,24 +4604,16 @@ async function gemSourcePush(ctx, args, linkedProject) {
3901
4604
  ...lock.sourceVersionId === undefined ? {} : { sourceVersionId: lock.sourceVersionId }
3902
4605
  };
3903
4606
  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
- }
4607
+ const syncResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_sync", {
4608
+ path: gemPath,
4609
+ files: sortedFiles.map(localFileBatchWritePayload),
4610
+ delete_paths: localStatus.deleted.map((deletedPath) => ({
4611
+ path: deletedPath
4612
+ })),
4613
+ ...sourceMutationCasParams(sourceCas, force),
4614
+ change_summary: force ? "Force-push local Gem source via Uru CLI." : "Push local Gem source via Uru CLI."
4615
+ }));
4616
+ sourceCas = nextSourceMutationCas(sourceCas, syncResult);
3922
4617
  } catch (error) {
3923
4618
  if (isConflictLikeError(error)) {
3924
4619
  throw new CliError(error instanceof Error ? error.message : "Gem source conflict", { code: ExitCode.Conflict, cause: error });
@@ -3945,6 +4640,44 @@ function localFileBatchWritePayload(file) {
3945
4640
  content_base64: file.contentBase64
3946
4641
  };
3947
4642
  }
4643
+ async function pruneRemoteDeletedSourceFiles(cwd, previousLock, localFilesBeforePull, remotePaths) {
4644
+ const deleted = [];
4645
+ const preserved = [];
4646
+ const lockedFiles = Object.entries(previousLock.files ?? {}).sort(([left], [right]) => left.localeCompare(right));
4647
+ for (const [projectPath2, lockedHash] of lockedFiles) {
4648
+ if (remotePaths.has(projectPath2)) {
4649
+ continue;
4650
+ }
4651
+ const localFile = localFilesBeforePull[projectPath2];
4652
+ if (localFile === undefined) {
4653
+ continue;
4654
+ }
4655
+ if (localFile.sha256 !== lockedHash) {
4656
+ preserved.push(projectPath2);
4657
+ continue;
4658
+ }
4659
+ const safePath = safeProjectPath(projectPath2);
4660
+ if (safePath === null) {
4661
+ preserved.push(projectPath2);
4662
+ continue;
4663
+ }
4664
+ await rm3(join4(resolve4(cwd), ...safePath.split("/")), { force: true });
4665
+ deleted.push(projectPath2);
4666
+ }
4667
+ return { deleted, preserved };
4668
+ }
4669
+ function formatPullSummary(gemPath, writtenCount, pruned) {
4670
+ const parts = [
4671
+ `Pulled ${writtenCount} file${writtenCount === 1 ? "" : "s"} from ${gemPath}`
4672
+ ];
4673
+ if (pruned.deleted.length > 0) {
4674
+ parts.push(`pruned ${pruned.deleted.length} remote-deleted file${pruned.deleted.length === 1 ? "" : "s"}`);
4675
+ }
4676
+ if (pruned.preserved.length > 0) {
4677
+ parts.push(`preserved ${pruned.preserved.length} locally changed remote-deleted file${pruned.preserved.length === 1 ? "" : "s"}`);
4678
+ }
4679
+ return parts.join("; ");
4680
+ }
3948
4681
  function sourceMutationCasParams(cas, force) {
3949
4682
  if (force || cas.sourceVersionId === undefined) {
3950
4683
  return { force: true };
@@ -4036,6 +4769,17 @@ function formatLocalStatusEntries(status) {
4036
4769
  ...status.deleted.map((path) => `D ${path}`)
4037
4770
  ];
4038
4771
  }
4772
+ function dirtyPullErrorMessage(status) {
4773
+ const entries = formatLocalStatusEntries(status);
4774
+ const preview = entries.slice(0, 10).map((entry) => ` ${entry}`).join(`
4775
+ `);
4776
+ const moreCount = Math.max(0, entries.length - 10);
4777
+ const moreLine = moreCount > 0 ? `
4778
+ ...and ${moreCount} more local change(s)` : "";
4779
+ return `Refusing to pull because local Gem source has uncommitted changes that could be overwritten.
4780
+ Run \`uru status\` to inspect them, save or push your changes, or re-run \`uru pull --force\` to replace local files with server source.
4781
+ ${preview}${moreLine}`;
4782
+ }
4039
4783
  async function tryRemoteSourceOperation(ctx, operationId, params) {
4040
4784
  try {
4041
4785
  return {
@@ -4138,7 +4882,7 @@ async function writePulledSourceFile(cwd, projectPath2, content) {
4138
4882
  if (safePath === null) {
4139
4883
  throw new CliError(`Refusing to write unsafe Gem source path: ${projectPath2}`);
4140
4884
  }
4141
- const localPath = join4(resolve3(cwd), ...safePath.split("/"));
4885
+ const localPath = join4(resolve4(cwd), ...safePath.split("/"));
4142
4886
  await mkdir5(dirname4(localPath), { recursive: true });
4143
4887
  await writeFile5(localPath, content);
4144
4888
  }
@@ -4193,20 +4937,34 @@ function isConflictLikeError(error) {
4193
4937
 
4194
4938
  // src/token-env-secret-commands.ts
4195
4939
  import { spawn as spawn2 } from "node:child_process";
4196
- import { chmod, mkdir as mkdir6, readFile as readFile5, writeFile as writeFile6 } from "node:fs/promises";
4197
- import { dirname as dirname5, resolve as resolve4 } from "node:path";
4940
+ import { chmod, mkdir as mkdir6, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
4941
+ import { dirname as dirname5, resolve as resolve5 } from "node:path";
4198
4942
  async function dispatchTokens(ctx, sub, args, linkedProject) {
4199
4943
  if (sub === "create") {
4200
4944
  const flags = parseLocalFlags(args);
4945
+ const usage = "Usage: uru tokens create [library/name.gem] [--gem <id>] --name <name> (--expiry 30d|--expires-at <iso>)";
4946
+ const name = requireCommandValue(flags.values["--name"], usage);
4947
+ const expiry = flags.values["--expiry"];
4948
+ const expiresAt = flags.values["--expires-at"];
4949
+ if (expiry === undefined && expiresAt === undefined) {
4950
+ throw new CliError("Gem deploy token creation requires --expiry <duration> or --expires-at <iso timestamp>.");
4951
+ }
4952
+ if (expiry !== undefined && expiresAt !== undefined) {
4953
+ throw new CliError("Pass only one token lifetime flag: --expiry or --expires-at.");
4954
+ }
4201
4955
  const params = {
4202
4956
  ...tokenGemRefParams(flags, linkedProject, {
4203
4957
  required: true,
4204
- usage: "Usage: uru tokens create [library/name.gem] [--gem <id>] [--name <name>] [--expiry 30d]"
4205
- })
4958
+ usage
4959
+ }),
4960
+ name
4206
4961
  };
4207
- copyStringFlag(flags, params, "--name", "name");
4208
- copyStringFlag(flags, params, "--expiry", "expiry");
4209
- copyStringFlag(flags, params, "--expires-at", "expires_at");
4962
+ if (expiry !== undefined) {
4963
+ params["expiry"] = expiry;
4964
+ }
4965
+ if (expiresAt !== undefined) {
4966
+ params["expires_at"] = expiresAt;
4967
+ }
4210
4968
  const scopes = tokenScopes(flags);
4211
4969
  if (scopes.length > 0) {
4212
4970
  params["scopes"] = scopes;
@@ -4294,7 +5052,7 @@ async function dispatchEnv(ctx, sub, args, linkedProject) {
4294
5052
  })));
4295
5053
  const secrets = extractSecretRows(result);
4296
5054
  const content = formatEnvTemplate(secrets);
4297
- const absolutePath = resolve4(ctx.cwd, outputPath);
5055
+ const absolutePath = resolve5(ctx.cwd, outputPath);
4298
5056
  await mkdir6(dirname5(absolutePath), { recursive: true });
4299
5057
  await writeFile6(absolutePath, content, { mode: 384 });
4300
5058
  await chmod(absolutePath, 384);
@@ -4311,8 +5069,8 @@ async function dispatchEnv(ctx, sub, args, linkedProject) {
4311
5069
  }
4312
5070
  if (sub === "run") {
4313
5071
  const parsed = parseEnvRunArgs(args);
4314
- const envFilePath = resolve4(ctx.cwd, parsed.envFile);
4315
- const envVars = parseEnvFile(await readFile5(envFilePath, "utf8"));
5072
+ const envFilePath = resolve5(ctx.cwd, parsed.envFile);
5073
+ const envVars = parseEnvFile(await readFile6(envFilePath, "utf8"));
4316
5074
  await runEnvCommand(ctx, parsed.command, envVars);
4317
5075
  return;
4318
5076
  }
@@ -4332,10 +5090,10 @@ async function secretValueFromFlags(ctx, flags) {
4332
5090
  }
4333
5091
  const valueFile = flags.values["--value-file"];
4334
5092
  if (valueFile !== undefined) {
4335
- return (await readFile5(resolve4(ctx.cwd, valueFile), "utf8")).trimEnd();
5093
+ return (await readFile6(resolve5(ctx.cwd, valueFile), "utf8")).trimEnd();
4336
5094
  }
4337
5095
  if (flags.booleans.has("--value-stdin")) {
4338
- return await readStdinText(ctx.stdin ?? process.stdin);
5096
+ return await readStdinText2(ctx.stdin ?? process.stdin);
4339
5097
  }
4340
5098
  throw new CliError("Secret put requires --value-file <path> or --value-stdin. Values are never printed by the CLI.");
4341
5099
  }
@@ -4480,7 +5238,7 @@ function redactValues2(content, values) {
4480
5238
  }
4481
5239
  return redacted;
4482
5240
  }
4483
- async function readStdinText(stdin) {
5241
+ async function readStdinText2(stdin) {
4484
5242
  const chunks = [];
4485
5243
  for await (const chunk of stdin) {
4486
5244
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
@@ -4640,6 +5398,7 @@ async function runCli(options) {
4640
5398
  credentialStore,
4641
5399
  apiUrl,
4642
5400
  ...token !== undefined ? { token } : {},
5401
+ tokenIsExplicit: flagToken !== undefined || envToken !== undefined,
4643
5402
  ...workspace !== undefined ? { workspace } : {},
4644
5403
  cwd,
4645
5404
  json: outputFormat === "json",
@@ -4661,6 +5420,9 @@ async function runCli(options) {
4661
5420
  };
4662
5421
  throwIfCancelled(cancellation.signal);
4663
5422
  await dispatchWithImplicitLogin(ctx, parsed.command, linkedProject);
5423
+ await maybeNotifyCliUpdate(ctx, {
5424
+ ...options.env === undefined ? {} : { env: options.env }
5425
+ });
4664
5426
  return ExitCode.Ok;
4665
5427
  } catch (error) {
4666
5428
  const cliError = normalizeError(error);
@@ -4894,6 +5656,11 @@ async function dispatch(ctx, command, linkedProject) {
4894
5656
  await dispatchDatasets(ctx, sub, command.slice(2));
4895
5657
  return;
4896
5658
  }
5659
+ if (head === "automations") {
5660
+ await withWorkspace(ctx);
5661
+ await dispatchAutomations(ctx, sub, command.slice(2));
5662
+ return;
5663
+ }
4897
5664
  if (head === "tokens" || head === "token") {
4898
5665
  await withWorkspace(ctx);
4899
5666
  await dispatchTokens(ctx, sub, command.slice(2), linkedProject);
@@ -4947,14 +5714,14 @@ function writeSystemClipboard(text) {
4947
5714
  const platform = process.platform;
4948
5715
  const command = platform === "darwin" ? "pbcopy" : platform === "win32" ? "clip" : "xclip";
4949
5716
  const args = platform === "linux" ? ["-selection", "clipboard"] : [];
4950
- return new Promise((resolve5, reject) => {
5717
+ return new Promise((resolve6, reject) => {
4951
5718
  const child = spawn3(command, args, { stdio: ["pipe", "ignore", "ignore"] });
4952
5719
  child.on("error", (error) => {
4953
5720
  reject(new CliError("Could not copy login URL to clipboard. Re-run without --clipboard to print the URL.", { cause: error }));
4954
5721
  });
4955
5722
  child.on("close", (code) => {
4956
5723
  if (code === 0) {
4957
- resolve5();
5724
+ resolve6();
4958
5725
  return;
4959
5726
  }
4960
5727
  reject(new CliError(`Could not copy login URL to clipboard (${command} exited ${code ?? "unknown"}). Re-run without --clipboard to print the URL.`));