sim 2.0.0 → 2.1.0-dev.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -4
  2. package/dist/index.js +1653 -506
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2403,7 +2403,7 @@ function removeSection(doc, name) {
2403
2403
 
2404
2404
  // src/config/profile.ts
2405
2405
  var DEFAULT_PROFILE = "default";
2406
- var DEFAULT_ENDPOINT = "https://sim.ai";
2406
+ var DEFAULT_ENDPOINT = "https://www.sim.ai";
2407
2407
  var OUTPUT_FORMATS = ["table", "json", "yaml", "text"];
2408
2408
 
2409
2409
  class ProfileConfigError extends Error {
@@ -2466,8 +2466,18 @@ function deleteProfile(profile) {
2466
2466
  writeIni(credentialsPath(), credentialsDoc, true);
2467
2467
  return { config, credentials };
2468
2468
  }
2469
- function normalizeEndpoint(endpoint) {
2470
- return endpoint.replace(/\/+$/, "");
2469
+ function normalizeEndpoint(endpoint, source) {
2470
+ const trimmed = endpoint.replace(/\/+$/, "");
2471
+ let parsed;
2472
+ try {
2473
+ parsed = new URL(trimmed);
2474
+ } catch {
2475
+ throw new ProfileConfigError(`Invalid endpoint "${endpoint}" from ${source}. Use an absolute URL, e.g. ${DEFAULT_ENDPOINT} or http://localhost:3000`);
2476
+ }
2477
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2478
+ throw new ProfileConfigError(`Unsupported endpoint scheme "${parsed.protocol.replace(/:$/, "")}" from ${source}. Use http or https, e.g. ${DEFAULT_ENDPOINT}`);
2479
+ }
2480
+ return trimmed;
2471
2481
  }
2472
2482
  function resolve(candidates, fallback, fallbackSource) {
2473
2483
  for (const [source, value] of candidates) {
@@ -2505,7 +2515,7 @@ function resolveProfile(overrides = {}) {
2505
2515
  }
2506
2516
  return {
2507
2517
  name,
2508
- endpoint: normalizeEndpoint(endpoint.value),
2518
+ endpoint: normalizeEndpoint(endpoint.value, endpoint.source),
2509
2519
  apiKey: apiKey.value,
2510
2520
  workspaceId: workspaceId.value,
2511
2521
  output: output.value,
@@ -2517,6 +2527,65 @@ function resolveProfile(overrides = {}) {
2517
2527
  }
2518
2528
  };
2519
2529
  }
2530
+ // src/version.ts
2531
+ import { readFileSync as readFileSync2 } from "node:fs";
2532
+ function readPackageVersion() {
2533
+ const metadata = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
2534
+ if (typeof metadata !== "object" || metadata === null || !("version" in metadata) || typeof metadata.version !== "string") {
2535
+ throw new Error("CLI package metadata is missing a valid version");
2536
+ }
2537
+ return metadata.version;
2538
+ }
2539
+ var CLI_VERSION = readPackageVersion();
2540
+ var USER_AGENT = `sim-cli/${CLI_VERSION} node/${process.versions.node} (${process.platform}; ${process.arch})`;
2541
+
2542
+ // src/http/environment.ts
2543
+ var reported = new Set;
2544
+ function once(key, message) {
2545
+ if (reported.has(key))
2546
+ return;
2547
+ reported.add(key);
2548
+ process.stderr.write(`warning: ${message}
2549
+ `);
2550
+ }
2551
+ var PROXY_VARIABLES = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
2552
+ var PROXY_SUPPORT = { 22: 21, 24: 5 };
2553
+ var FIRST_SUPPORTED_MAJOR = 24;
2554
+ function runtimeCanProxy(version) {
2555
+ const [major, minor] = version.replace(/^v/, "").split(".").map(Number);
2556
+ if (!Number.isFinite(major) || !Number.isFinite(minor))
2557
+ return false;
2558
+ const firstSupportedMinor = PROXY_SUPPORT[major];
2559
+ if (firstSupportedMinor !== undefined)
2560
+ return minor >= firstSupportedMinor;
2561
+ return major > FIRST_SUPPORTED_MAJOR;
2562
+ }
2563
+ function warnIfProxyIgnored(env2 = process.env, version = process.version) {
2564
+ const variable = PROXY_VARIABLES.find((name) => env2[name]);
2565
+ if (!variable)
2566
+ return;
2567
+ if (env2.NODE_USE_ENV_PROXY && runtimeCanProxy(version))
2568
+ return;
2569
+ once("proxy", runtimeCanProxy(version) ? `${variable} is set but Node only uses it when NODE_USE_ENV_PROXY=1. Re-run with NODE_USE_ENV_PROXY=1 to route through the proxy.` : `${variable} is set but Node ${version} cannot use it. Upgrade to Node 22.21 or 24.5 and set NODE_USE_ENV_PROXY=1 to route through the proxy.`);
2570
+ }
2571
+ var LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0.0.0.0"]);
2572
+ function isLoopback(hostname) {
2573
+ return LOOPBACK.has(hostname) || hostname.endsWith(".localhost");
2574
+ }
2575
+ function warnIfKeyOverCleartext(endpoint, hasApiKey) {
2576
+ if (!hasApiKey)
2577
+ return;
2578
+ let url;
2579
+ try {
2580
+ url = new URL(endpoint);
2581
+ } catch {
2582
+ return;
2583
+ }
2584
+ if (url.protocol !== "http:" || isLoopback(url.hostname))
2585
+ return;
2586
+ once("cleartext", `sending your API key to ${url.host} over http. Anything on the path can read it — use https unless this network is trusted.`);
2587
+ }
2588
+
2520
2589
  // src/http/client.ts
2521
2590
  class SimApiError extends Error {
2522
2591
  status;
@@ -2539,13 +2608,28 @@ function buildUrl(endpoint, path, query) {
2539
2608
  }
2540
2609
  return url.toString();
2541
2610
  }
2542
- function toApiError(status, raw) {
2611
+ var REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
2612
+ var MARKUP_PREFIX = /^\s*<(?:!doctype|html|\?xml)/i;
2613
+ var KEY_SCOPE_REFUSALS = new Set([
2614
+ "WORKSPACE_KEY_OPERATION_NOT_PERMITTED",
2615
+ "PRINCIPAL_KIND_NOT_PERMITTED"
2616
+ ]);
2617
+ function toNonJsonError(url, status, contentType, raw) {
2618
+ const type = contentType?.split(";")[0]?.trim().toLowerCase();
2619
+ const isMarkup = type === "text/html" || type === "application/xhtml+xml" || MARKUP_PREFIX.test(raw);
2620
+ const kind = isMarkup ? "HTML" : type && type !== "application/json" ? type : "a non-JSON response";
2621
+ const text = raw.trim();
2622
+ const keepSnippet = !isMarkup && text.length > 0 && text.length <= 200;
2623
+ return new SimApiError(`${url} returned ${kind}, not JSON (HTTP ${status}) — check your endpoint.${keepSnippet ? ` Response: ${truncate(text, 200)}` : ""}`, status);
2624
+ }
2625
+ function toApiError(url, status, contentType, raw) {
2543
2626
  let parsed;
2544
2627
  try {
2545
2628
  parsed = JSON.parse(raw);
2546
2629
  } catch {
2547
- const text = raw.trim();
2548
- return new SimApiError(text ? truncate(text, 300) : `Request failed with status ${status}`, status);
2630
+ if (!raw.trim())
2631
+ return new SimApiError(`Request failed with status ${status}`, status);
2632
+ return toNonJsonError(url, status, contentType, raw);
2549
2633
  }
2550
2634
  const body = parsed;
2551
2635
  if (body.error && typeof body.error === "object") {
@@ -2561,8 +2645,79 @@ function toApiError(status, raw) {
2561
2645
  function truncate(value, max) {
2562
2646
  return value.length <= max ? value : `${value.slice(0, max)}…`;
2563
2647
  }
2648
+ function namesKeyScopeRefusal(error) {
2649
+ if (typeof error.code === "string" && KEY_SCOPE_REFUSALS.has(error.code))
2650
+ return true;
2651
+ const details = error.details;
2652
+ if (!details || typeof details !== "object")
2653
+ return false;
2654
+ const code = details.code;
2655
+ return typeof code === "string" && KEY_SCOPE_REFUSALS.has(code);
2656
+ }
2657
+ function isStrictPrefix(path, other) {
2658
+ if (path.length >= other.length)
2659
+ return false;
2660
+ return path.every((segment, index) => segment === other[index]);
2661
+ }
2662
+ function rejectsAKeyThatValidated(issue, other) {
2663
+ if (!issue.unrecognizedKeys || !isStrictPrefix(issue.path, other.path))
2664
+ return false;
2665
+ return issue.unrecognizedKeys.includes(other.path[issue.path.length]);
2666
+ }
2667
+ function dropUnionBranchNoise(issues) {
2668
+ if (issues.length < 2)
2669
+ return issues;
2670
+ const kept = issues.filter((issue) => !issues.some((other) => rejectsAKeyThatValidated(issue, other)));
2671
+ return kept.length > 0 ? kept : issues;
2672
+ }
2673
+ var DEFAULT_TIMEOUT_SECONDS = 3600;
2674
+ var MAX_TIMEOUT_MS = 2 ** 31 - 1;
2675
+ var RAISE_TIMEOUT_HINT = "Raise SIM_TIMEOUT_SECONDS, or set it to 0 to wait indefinitely.";
2676
+ function isRequestTimeout(error) {
2677
+ return error instanceof DOMException && error.name === "TimeoutError";
2678
+ }
2679
+ function resolveTimeoutMs(env2 = process.env) {
2680
+ const raw = env2.SIM_TIMEOUT_SECONDS;
2681
+ if (raw === undefined || raw.trim() === "")
2682
+ return DEFAULT_TIMEOUT_SECONDS * 1000;
2683
+ const seconds = Number(raw);
2684
+ if (!Number.isFinite(seconds) || seconds < 0) {
2685
+ throw new SimApiError(`Invalid SIM_TIMEOUT_SECONDS "${raw}". Use a non-negative number of seconds, or 0 to disable.`, 0);
2686
+ }
2687
+ const ms = seconds === 0 ? 0 : Math.max(1, Math.round(seconds * 1000));
2688
+ if (ms > MAX_TIMEOUT_MS) {
2689
+ throw new SimApiError(`SIM_TIMEOUT_SECONDS "${raw}" is longer than Node can wait (${Math.floor(MAX_TIMEOUT_MS / 1000)}s). Use 0 to wait indefinitely.`, 0);
2690
+ }
2691
+ return ms;
2692
+ }
2693
+ function combineSignals(caller, timeout) {
2694
+ if (!caller)
2695
+ return timeout;
2696
+ if (!timeout)
2697
+ return caller;
2698
+ if (typeof AbortSignal.any === "function")
2699
+ return AbortSignal.any([caller, timeout]);
2700
+ const controller = new AbortController;
2701
+ for (const signal of [caller, timeout]) {
2702
+ if (signal.aborted) {
2703
+ controller.abort(signal.reason);
2704
+ break;
2705
+ }
2706
+ signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
2707
+ }
2708
+ return controller.signal;
2709
+ }
2710
+ function debugEnabled(env2 = process.env) {
2711
+ const raw = env2.SIM_DEBUG;
2712
+ return raw !== undefined && raw !== "" && raw !== "0" && raw.toLowerCase() !== "false";
2713
+ }
2714
+ function traceRequest(method, url, status, startedAt) {
2715
+ process.stderr.write(`${source_default.dim(`[sim] ${method} ${url} → ${status} ${Math.round(performance.now() - startedAt)}ms`)}
2716
+ `);
2717
+ }
2564
2718
  function formatApiErrorDetails(details) {
2565
- const issues = new Set;
2719
+ const issues = [];
2720
+ const seen = new Set;
2566
2721
  const visit = (value, parentPath = []) => {
2567
2722
  if (Array.isArray(value)) {
2568
2723
  value.forEach((item) => visit(item, parentPath));
@@ -2580,15 +2735,27 @@ function formatApiErrorDetails(details) {
2580
2735
  }
2581
2736
  if (typeof issue.message !== "string" || issue.message === "Invalid input")
2582
2737
  return;
2583
- issues.add(`${path.length > 0 ? path.join(".") : "request"}: ${issue.message}`);
2738
+ const line = `${path.join(".")}: ${issue.message}`;
2739
+ if (seen.has(line))
2740
+ return;
2741
+ seen.add(line);
2742
+ issues.push({
2743
+ path,
2744
+ message: issue.message,
2745
+ unrecognizedKeys: issue.code === "unrecognized_keys" && Array.isArray(issue.keys) ? issue.keys.map(String) : null
2746
+ });
2584
2747
  };
2585
2748
  visit(details);
2586
- if (issues.size === 0)
2749
+ if (issues.length === 0)
2587
2750
  return [` details: ${truncate(JSON.stringify(details), 1000)}`];
2588
- const visible = [...issues].slice(0, 8);
2589
- const lines = [" details:", ...visible.map((issue) => ` ${issue}`)];
2590
- if (issues.size > visible.length)
2591
- lines.push(` … ${issues.size - visible.length} more issues`);
2751
+ const kept = dropUnionBranchNoise(issues);
2752
+ const visible = kept.slice(0, 8);
2753
+ const lines = [
2754
+ " details:",
2755
+ ...visible.map((issue) => ` ${issue.path.length > 0 ? issue.path.join(".") : "request"}: ${issue.message}`)
2756
+ ];
2757
+ if (kept.length > visible.length)
2758
+ lines.push(` … ${kept.length - visible.length} more issues`);
2592
2759
  return lines;
2593
2760
  }
2594
2761
 
@@ -2614,65 +2781,140 @@ class SimClient {
2614
2781
  return workspaceId;
2615
2782
  }
2616
2783
  async requestRaw(path, options = {}) {
2784
+ return (await this.send(path, options)).response;
2785
+ }
2786
+ async request(path, options = {}) {
2787
+ const { response, url } = await this.send(path, options);
2788
+ const raw = await response.text();
2789
+ if (!raw)
2790
+ return;
2791
+ try {
2792
+ return JSON.parse(raw);
2793
+ } catch {
2794
+ throw toNonJsonError(url, response.status, response.headers.get("content-type"), raw);
2795
+ }
2796
+ }
2797
+ async send(path, options) {
2617
2798
  const apiKey = this.resolveApiKey(options.auth);
2618
2799
  const url = buildUrl(this.profile.endpoint, path, options.query);
2619
2800
  const hasBody = options.body !== undefined;
2801
+ const method = options.method ?? "GET";
2802
+ warnIfProxyIgnored();
2803
+ warnIfKeyOverCleartext(this.profile.endpoint, Boolean(apiKey));
2804
+ const timeoutMs = resolveTimeoutMs();
2805
+ const timeout = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
2806
+ const signal = combineSignals(options.signal, timeout);
2807
+ const trace = debugEnabled();
2808
+ const startedAt = performance.now();
2620
2809
  let response;
2621
2810
  try {
2622
2811
  response = await fetch(url, {
2623
- method: options.method ?? "GET",
2812
+ method,
2624
2813
  headers: {
2625
2814
  ...apiKey ? { "x-api-key": apiKey } : {},
2626
2815
  accept: "application/json",
2816
+ "user-agent": USER_AGENT,
2627
2817
  ...hasBody ? { "content-type": "application/json" } : {},
2628
2818
  ...options.headers
2629
2819
  },
2630
2820
  body: hasBody ? JSON.stringify(options.body) : undefined,
2631
- signal: options.signal
2821
+ signal,
2822
+ redirect: "manual"
2632
2823
  });
2633
2824
  } catch (cause) {
2825
+ if (trace)
2826
+ traceRequest(method, url, "failed", startedAt);
2634
2827
  if (options.signal?.aborted) {
2635
2828
  throw new SimApiError("Request cancelled.", 0);
2636
2829
  }
2830
+ if (timeout?.aborted) {
2831
+ throw new SimApiError(`${url} did not answer within ${timeoutMs / 1000}s. ${RAISE_TIMEOUT_HINT}`, 0);
2832
+ }
2637
2833
  throw new SimApiError(`Could not reach ${this.profile.endpoint}: ${cause.message}`, 0);
2638
2834
  }
2835
+ if (trace)
2836
+ traceRequest(method, url, response.status, startedAt);
2837
+ if (REDIRECT_STATUSES.has(response.status))
2838
+ throw this.toRedirectError(url, path, response);
2639
2839
  if (!response.ok) {
2640
2840
  const raw = await response.text();
2641
- const error = toApiError(response.status, raw);
2841
+ const error = toApiError(url, response.status, response.headers.get("content-type"), raw);
2642
2842
  if (response.status === 401) {
2643
2843
  error.message = `${error.message} — run: sim login --profile ${this.profile.name}`;
2644
2844
  }
2845
+ if (namesKeyScopeRefusal(error)) {
2846
+ error.message = `${error.message} — this operation needs a personal API key: sim login --profile ${this.profile.name}`;
2847
+ }
2645
2848
  throw error;
2646
2849
  }
2647
- return response;
2850
+ return { response, url };
2648
2851
  }
2649
- async request(path, options = {}) {
2650
- const response = await this.requestRaw(path, options);
2651
- const raw = await response.text();
2652
- if (!raw)
2653
- return;
2654
- return JSON.parse(raw);
2852
+ toRedirectError(url, path, response) {
2853
+ const location = response.headers.get("location")?.trim();
2854
+ let target = null;
2855
+ if (location) {
2856
+ try {
2857
+ target = new URL(location, url);
2858
+ } catch {
2859
+ target = null;
2860
+ }
2861
+ }
2862
+ if (!target) {
2863
+ return new SimApiError(`${url} answered HTTP ${response.status} with no usable redirect target. Check the endpoint for profile "${this.profile.name}".`, response.status);
2864
+ }
2865
+ const suggested = redirectEndpoint(this.profile.endpoint, path, target);
2866
+ if (!suggested) {
2867
+ return new SimApiError(`${url} redirected to ${target.href}. The CLI does not follow redirects, because a redirect can drop the request body and turn a write into a silent no-op.`, response.status);
2868
+ }
2869
+ return new SimApiError(`Endpoint redirected to ${suggested}. Run: sim configure --profile ${this.profile.name} --set-endpoint ${suggested}`, response.status);
2655
2870
  }
2656
2871
  }
2872
+ function redirectEndpoint(endpoint, requestPath, target) {
2873
+ const prefix = target.pathname.endsWith(requestPath) ? target.pathname.slice(0, target.pathname.length - requestPath.length) : "";
2874
+ const suggested = `${target.origin}${prefix}`.replace(/\/+$/, "");
2875
+ return suggested === endpoint.replace(/\/+$/, "") ? null : suggested;
2876
+ }
2877
+ function pageProgress() {
2878
+ let reported2 = false;
2879
+ return {
2880
+ advance: (fetched) => {
2881
+ if (!process.stderr.isTTY)
2882
+ return;
2883
+ reported2 = true;
2884
+ process.stderr.write(`\r${source_default.dim(`fetched ${fetched}…`)}\x1B[K`);
2885
+ },
2886
+ finish: () => {
2887
+ if (reported2)
2888
+ process.stderr.write("\r\x1B[K");
2889
+ }
2890
+ };
2891
+ }
2657
2892
  async function requestAllPages(client, path, options) {
2658
2893
  const { query, pageSize, limit: requestedLimit, ...requestOptions } = options;
2659
2894
  const limit = requestedLimit ?? Number.POSITIVE_INFINITY;
2660
2895
  if (limit <= 0)
2661
2896
  return [];
2662
2897
  const items = [];
2898
+ const progress = pageProgress();
2663
2899
  let cursor = null;
2664
- do {
2665
- const page = await client.request(path, {
2666
- ...requestOptions,
2667
- query: {
2668
- ...query,
2669
- limit: Math.min(pageSize, limit - items.length),
2670
- cursor
2671
- }
2672
- });
2673
- items.push(...page.data);
2674
- cursor = page.nextCursor;
2675
- } while (cursor && items.length < limit);
2900
+ try {
2901
+ do {
2902
+ const page = await client.request(path, {
2903
+ ...requestOptions,
2904
+ query: {
2905
+ ...query,
2906
+ limit: Math.min(pageSize, limit - items.length),
2907
+ cursor
2908
+ }
2909
+ });
2910
+ items.push(...page.data);
2911
+ cursor = page.nextCursor;
2912
+ if (cursor && items.length < limit)
2913
+ progress.advance(items.length);
2914
+ } while (cursor && items.length < limit);
2915
+ } finally {
2916
+ progress.finish();
2917
+ }
2676
2918
  return items.slice(0, limit);
2677
2919
  }
2678
2920
  function resolvePath(template, params = {}) {
@@ -5894,7 +6136,7 @@ function duration(ms) {
5894
6136
  if (ms === null || ms === undefined)
5895
6137
  return EMPTY;
5896
6138
  if (ms < 1000)
5897
- return `${ms}ms`;
6139
+ return `${Math.round(ms)}ms`;
5898
6140
  if (ms < 60000)
5899
6141
  return `${(ms / 1000).toFixed(1)}s`;
5900
6142
  return `${Math.floor(ms / 60000)}m${Math.round(ms % 60000 / 1000)}s`;
@@ -5914,17 +6156,18 @@ function oneLine(value) {
5914
6156
  return value.replace(/\s*[\r\n\t]+\s*/g, " ");
5915
6157
  }
5916
6158
  var MAX_CELL_WIDTH = 60;
5917
- function clampCell(value) {
5918
- if (visibleWidth(value) <= MAX_CELL_WIDTH || value !== value.replace(ANSI_PATTERN, "")) {
6159
+ var MAX_RECORD_WIDTH = 160;
6160
+ function clamp(value, width) {
6161
+ if (visibleWidth(value) <= width || value !== value.replace(ANSI_PATTERN, "")) {
5919
6162
  return value;
5920
6163
  }
5921
- return `${value.slice(0, MAX_CELL_WIDTH - 1)}…`;
6164
+ return `${value.slice(0, width - 1)}…`;
5922
6165
  }
5923
6166
  function renderTable(rows, columns) {
5924
6167
  if (rows.length === 0)
5925
6168
  return source_default.dim("No results.");
5926
6169
  const headers = columns.map((column) => sanitize(column.header));
5927
- const cells = rows.map((row) => columns.map((column) => clampCell(oneLine(column.value(row)))));
6170
+ const cells = rows.map((row) => columns.map((column) => clamp(oneLine(column.value(row)), MAX_CELL_WIDTH)));
5928
6171
  const widths = columns.map((_column, index) => Math.max(visibleWidth(headers[index]), ...cells.map((line) => visibleWidth(line[index]))));
5929
6172
  const header = headers.map((label, index) => source_default.dim(pad(label.toUpperCase(), widths[index]))).join(" ").trimEnd();
5930
6173
  const body = cells.map((line) => line.map((cell, index) => pad(cell, widths[index])).join(" ").trimEnd());
@@ -5970,13 +6213,10 @@ function printRecord(format, fields, raw) {
5970
6213
  }
5971
6214
  const width = Math.max(...safeFields.map(([label]) => visibleWidth(label)));
5972
6215
  for (const [label, value] of safeFields) {
5973
- console.log(`${source_default.dim(pad(`${label}:`, width + 1))} ${oneLine(value)}`);
6216
+ console.log(`${source_default.dim(pad(`${label}:`, width + 1))} ${clamp(oneLine(value), MAX_RECORD_WIDTH)}`);
5974
6217
  }
5975
6218
  }
5976
6219
 
5977
- // src/program.ts
5978
- import { readFileSync as readFileSync3 } from "node:fs";
5979
-
5980
6220
  // ../../node_modules/commander/esm.mjs
5981
6221
  var import__ = __toESM(require_commander(), 1);
5982
6222
  var {
@@ -6009,6 +6249,8 @@ function sleep(ms) {
6009
6249
  var PAIRING_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
6010
6250
  var POLL_INTERVAL_MS = 2000;
6011
6251
  var POLL_TIMEOUT_MS = 15 * 60 * 1000;
6252
+ var APPROVAL_PATH = "/cli/auth";
6253
+ var POLL_PATH = "/api/cli/auth/poll";
6012
6254
  var RETRYABLE_POLL_STATUSES = new Set([409, 429, 500, 502, 503, 504]);
6013
6255
  function token() {
6014
6256
  return randomBytes(32).toString("base64url");
@@ -6027,14 +6269,30 @@ function createAuthRequest() {
6027
6269
  };
6028
6270
  }
6029
6271
  function buildApprovalUrl(endpoint, auth, scope, workspaceId) {
6030
- const url = new URL("/cli/auth", endpoint);
6031
- url.searchParams.set("request", auth.request);
6032
- url.searchParams.set("challenge", auth.challenge);
6033
- url.searchParams.set("pairing", auth.pairing);
6034
- url.searchParams.set("scope", scope);
6035
- if (workspaceId)
6036
- url.searchParams.set("workspace", workspaceId);
6037
- return url.toString();
6272
+ return buildUrl(endpoint, APPROVAL_PATH, {
6273
+ request: auth.request,
6274
+ challenge: auth.challenge,
6275
+ pairing: auth.pairing,
6276
+ scope,
6277
+ workspace: workspaceId
6278
+ });
6279
+ }
6280
+ function toRedirectError(endpoint, response) {
6281
+ const location = response.headers.get("location")?.trim();
6282
+ let target = null;
6283
+ if (location) {
6284
+ try {
6285
+ target = new URL(location, endpoint);
6286
+ } catch {
6287
+ target = null;
6288
+ }
6289
+ }
6290
+ if (!target) {
6291
+ return new SimApiError(`${endpoint} answered the login poll with HTTP ${response.status} and no usable redirect target. Check the endpoint.`, response.status);
6292
+ }
6293
+ const refusal = `${endpoint} redirected the login poll to ${target.href}. The CLI does not follow redirects, because a redirect drops the request body and would carry the login secret to another origin.`;
6294
+ const suggested = redirectEndpoint(endpoint, POLL_PATH, target);
6295
+ return new SimApiError(suggested ? `${refusal} Re-run with --endpoint ${suggested}, or run: sim configure --set-endpoint ${suggested}` : refusal, response.status);
6038
6296
  }
6039
6297
  async function pollForKey(endpoint, auth, signal) {
6040
6298
  const deadline = Date.now() + POLL_TIMEOUT_MS;
@@ -6043,16 +6301,23 @@ async function pollForKey(endpoint, auth, signal) {
6043
6301
  throw new SimApiError("Login cancelled.", 0);
6044
6302
  let response = null;
6045
6303
  try {
6046
- response = await fetch(new URL("/api/cli/auth/poll", endpoint), {
6304
+ response = await fetch(buildUrl(endpoint, POLL_PATH), {
6047
6305
  method: "POST",
6048
- headers: { "content-type": "application/json", accept: "application/json" },
6306
+ headers: {
6307
+ "content-type": "application/json",
6308
+ accept: "application/json",
6309
+ "user-agent": USER_AGENT
6310
+ },
6049
6311
  body: JSON.stringify({ request: auth.request, verifier: auth.pollSecret }),
6050
- signal
6312
+ signal,
6313
+ redirect: "manual"
6051
6314
  });
6052
6315
  } catch {
6053
6316
  response = null;
6054
6317
  }
6055
6318
  if (response) {
6319
+ if (REDIRECT_STATUSES.has(response.status))
6320
+ throw toRedirectError(endpoint, response);
6056
6321
  const raw = await response.text();
6057
6322
  if (!response.ok) {
6058
6323
  if (!RETRYABLE_POLL_STATUSES.has(response.status)) {
@@ -6106,263 +6371,76 @@ function clientFrom(command) {
6106
6371
  return { client: new SimClient(profile), profile };
6107
6372
  }
6108
6373
 
6109
- // src/commands/auth.ts
6110
- function openBrowser(url) {
6111
- const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : [process.platform === "darwin" ? "open" : "xdg-open", [url]];
6112
- try {
6113
- const child = spawn(command, args, { stdio: "ignore", detached: true });
6114
- child.on("error", () => {});
6115
- child.unref();
6116
- } catch {}
6117
- }
6118
- function presentAuthentication(source) {
6119
- switch (source) {
6120
- case "flag":
6121
- return { authenticated: true, source: "flag" };
6122
- case "env":
6123
- return { authenticated: true, source: "env" };
6124
- case "credentials":
6125
- return { authenticated: true, source: "credentials" };
6126
- case "unset":
6127
- return { authenticated: false, source: "unset" };
6128
- case "config":
6129
- case "default":
6130
- throw new SimApiError(`Unexpected API key source "${source}".`, 0);
6131
- }
6132
- }
6133
- async function confirmProfileOverwrite(profileName) {
6134
- if (!process.stdin.isTTY) {
6135
- throw new SimApiError(`Profile "${profileName}" already exists. Re-run with --yes to overwrite it.`, 0);
6136
- }
6137
- const prompt = createInterface({ input: process.stdin, output: process.stderr });
6138
- try {
6139
- const answer = await prompt.question(`Profile "${profileName}" already exists. Replace its API key and login defaults? (y/N) `);
6140
- return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
6141
- } finally {
6142
- prompt.close();
6143
- }
6144
- }
6145
- function loginCommand() {
6146
- return new Command("login").description("Authorize this terminal and store an API key for the profile").option("--scope <scope>", "Key space to mint from: platform or copilot", "platform").option("--no-browser", "Print the URL instead of opening a browser").option("-y, --yes", "Overwrite an existing profile without prompting").action(async (options, command) => {
6147
- const profile = profileFrom(command);
6148
- if (options.scope !== "platform" && options.scope !== "copilot") {
6149
- throw new SimApiError(`Unknown scope "${options.scope}". Use platform or copilot.`, 0);
6150
- }
6151
- const scope = options.scope;
6152
- if (readCredentialsProfile(profile.name).api_key && !options.yes) {
6153
- const confirmed = await confirmProfileOverwrite(profile.name);
6154
- if (!confirmed) {
6155
- console.log(source_default.dim("Login cancelled; the existing profile was not changed."));
6156
- return;
6374
+ // src/generated/v2-api.ts
6375
+ var V2_OPERATIONS = {
6376
+ abortFileUpload: {
6377
+ method: "DELETE",
6378
+ path: "/api/v2/files/uploads/[uploadId]",
6379
+ pathParams: ["uploadId"],
6380
+ pathParamDocs: { uploadId: "Upload session identifier." },
6381
+ responseMode: "json",
6382
+ summary: "Abort File Upload",
6383
+ query: {
6384
+ workspaceId: {
6385
+ kind: "string",
6386
+ required: true,
6387
+ describe: "Workspace that owns the upload session."
6157
6388
  }
6158
6389
  }
6159
- const auth = createAuthRequest();
6160
- const url = buildApprovalUrl(profile.endpoint, auth, scope, profile.workspaceId ?? undefined);
6161
- console.log(`Signing in to ${source_default.bold(profile.endpoint)} as profile ${source_default.bold(profile.name)}`);
6162
- console.log(`
6163
- Pairing code: ${source_default.bold(auth.pairing)}`);
6164
- console.log(source_default.dim(`Confirm this code matches what the browser shows before approving.
6165
- `));
6166
- console.log(url);
6167
- if (options.browser)
6168
- openBrowser(url);
6169
- console.log(source_default.dim(`
6170
- Waiting for approval…`));
6171
- const key = await pollForKey(profile.endpoint, auth);
6172
- if (key.scope !== scope) {
6173
- throw new SimApiError(`Server issued a ${key.scope} key but this profile needs a ${scope} key. Update the Sim deployment, or run: sim login --scope ${key.scope}`, 0);
6390
+ },
6391
+ abortKnowledgeDocumentUpload: {
6392
+ method: "DELETE",
6393
+ path: "/api/v2/knowledge/[id]/documents/uploads/[uploadId]",
6394
+ pathParams: ["id", "uploadId"],
6395
+ pathParamDocs: {
6396
+ id: "Unique knowledge base identifier.",
6397
+ uploadId: "Upload session identifier returned when the upload was created."
6398
+ },
6399
+ responseMode: "json",
6400
+ summary: "Abort Document Upload",
6401
+ query: {
6402
+ workspaceId: {
6403
+ kind: "string",
6404
+ required: true,
6405
+ describe: "Workspace that owns the knowledge base."
6406
+ }
6174
6407
  }
6175
- writeCredentialsProfile(profile.name, key.apiKey);
6176
- const settings = {
6177
- endpoint: profile.endpoint,
6178
- workspace: key.workspaceId ?? null
6179
- };
6180
- writeConfigProfile(profile.name, settings);
6181
- console.log(source_default.green(`
6182
- Logged in. Key stored in ${credentialsPath()}`));
6183
- if (key.workspaceBound && key.workspaceId) {
6184
- console.log(source_default.dim(` Workspace-scoped key it can only reach ${key.workspaceId}.`));
6185
- } else if (key.workspaceId) {
6186
- console.log(source_default.dim(` Personal key, defaulting to ${key.workspaceId}. Override per command with --workspace.`));
6187
- } else {
6188
- console.log(source_default.dim(" Personal key with no default workspace. Set one with: sim configure --set-workspace <id>"));
6408
+ },
6409
+ addTableColumn: {
6410
+ method: "POST",
6411
+ path: "/api/v2/tables/[tableId]/columns",
6412
+ pathParams: ["tableId"],
6413
+ pathParamDocs: { tableId: "Unique table identifier." },
6414
+ responseMode: "json",
6415
+ summary: "Add Column",
6416
+ body: {
6417
+ workspaceId: { kind: "string", required: true, describe: "Workspace that owns the table." },
6418
+ column: { kind: "object", required: true, describe: "Column definition to add." }
6189
6419
  }
6190
- });
6191
- }
6192
- function logoutCommand() {
6193
- return new Command("logout").description("Remove the profile's stored API key").option("--all", "Remove the profile entirely, including its settings").action((options, command) => {
6194
- const profile = profileFrom(command);
6195
- if (options.all) {
6196
- const removed = deleteProfile(profile.name);
6197
- if (!removed.config && !removed.credentials) {
6198
- console.log(source_default.dim(`Nothing stored for profile "${profile.name}".`));
6199
- return;
6200
- }
6201
- console.log(source_default.green(`✓ Removed profile "${profile.name}".`));
6202
- return;
6203
- }
6204
- if (!readCredentialsProfile(profile.name).api_key) {
6205
- console.log(source_default.dim(`No stored key for profile "${profile.name}".`));
6206
- return;
6207
- }
6208
- writeCredentialsProfile(profile.name, null);
6209
- console.log(source_default.green(`✓ Removed the stored key for profile "${profile.name}".`));
6210
- console.log(source_default.dim(" The key itself is still active — revoke it in Settings → API keys."));
6211
- });
6212
- }
6213
- function whoamiCommand() {
6214
- return new Command("whoami").description("Show the resolved profile and where each setting came from").action((_options, command) => {
6215
- const profile = profileFrom(command);
6216
- const { sources } = profile;
6217
- const authentication = presentAuthentication(sources.apiKey);
6218
- const annotate = (value, source) => source === "unset" ? source_default.dim("not set") : `${value} ${source_default.dim(`(${source})`)}`;
6219
- printRecord(profile.output, [
6220
- ["Profile", profile.name],
6221
- ["Endpoint", annotate(profile.endpoint, sources.endpoint)],
6222
- [
6223
- "API key",
6224
- authentication.authenticated ? annotate("configured", authentication.source) : source_default.yellow("not logged in")
6225
- ],
6226
- ["Workspace", annotate(profile.workspaceId ?? "", sources.workspaceId)],
6227
- ["Output", annotate(profile.output, sources.output)]
6228
- ], {
6229
- profile: profile.name,
6230
- endpoint: profile.endpoint,
6231
- workspaceId: profile.workspaceId,
6232
- output: profile.output,
6233
- authenticated: authentication.authenticated,
6234
- sources: {
6235
- endpoint: sources.endpoint,
6236
- authentication: authentication.source,
6237
- workspaceId: sources.workspaceId,
6238
- output: sources.output
6239
- }
6240
- });
6241
- });
6242
- }
6243
- function profilesCommand() {
6244
- return new Command("profiles").alias("profile").description("List the profiles defined in the config and credentials files").action((_options, command) => {
6245
- const profiles = listProfiles();
6246
- if (profiles.length === 0) {
6247
- console.log(source_default.dim("No profiles yet. Run: sim login"));
6248
- return;
6249
- }
6250
- const active = profileFrom(command).name;
6251
- for (const name of profiles) {
6252
- const marker = name === active ? source_default.green("*") : " ";
6253
- const hasKey = Boolean(readCredentialsProfile(name).api_key);
6254
- console.log(`${marker} ${name}${hasKey ? "" : source_default.dim(" (no key)")}`);
6255
- }
6256
- });
6257
- }
6258
-
6259
- // src/commands/configure.ts
6260
- function configureCommand() {
6261
- return new Command("configure").description("Set a profile's endpoint, default workspace, or output format").option("--set-endpoint <url>", "Sim deployment to talk to").option("--set-workspace <id>", "Default workspace for workspace-scoped commands").option("--set-output <format>", `Default output format (${OUTPUT_FORMATS.join(" | ")})`).option("--unset <key...>", "Remove settings (endpoint, workspace, output)").action((options, command) => {
6262
- const profile = profileFrom(command);
6263
- const updates = {};
6264
- if (options.setEndpoint)
6265
- updates.endpoint = options.setEndpoint.replace(/\/+$/, "");
6266
- if (options.setWorkspace)
6267
- updates.workspace = options.setWorkspace;
6268
- if (options.setOutput) {
6269
- if (!OUTPUT_FORMATS.includes(options.setOutput)) {
6270
- throw new SimApiError(`Unknown output format "${options.setOutput}". Use one of: ${OUTPUT_FORMATS.join(", ")}`, 0);
6271
- }
6272
- updates.output = options.setOutput;
6273
- }
6274
- for (const key of options.unset ?? []) {
6275
- if (!["endpoint", "workspace", "output"].includes(key)) {
6276
- throw new SimApiError(`Cannot unset "${key}". Use endpoint, workspace, or output.`, 0);
6277
- }
6278
- updates[key] = null;
6279
- }
6280
- if (Object.keys(updates).length === 0) {
6281
- const current = readConfigProfile(profile.name);
6282
- if (Object.keys(current).length === 0) {
6283
- console.log(source_default.dim(`No settings stored for profile "${profile.name}".`));
6284
- return;
6285
- }
6286
- for (const [key, value] of Object.entries(current)) {
6287
- console.log(`${source_default.dim(`${key}:`)} ${value}`);
6288
- }
6289
- return;
6290
- }
6291
- writeConfigProfile(profile.name, updates);
6292
- console.log(source_default.green(`✓ Updated profile "${profile.name}" in ${configPath()}`));
6293
- });
6294
- }
6295
-
6296
- // src/generated/v2-api.ts
6297
- var V2_OPERATIONS = {
6298
- abortFileUpload: {
6299
- method: "DELETE",
6300
- path: "/api/v2/files/uploads/[uploadId]",
6301
- pathParams: ["uploadId"],
6302
- pathParamDocs: { uploadId: "Upload session identifier." },
6303
- responseMode: "json",
6304
- summary: "Abort File Upload",
6305
- query: {
6306
- workspaceId: {
6307
- kind: "string",
6308
- required: true,
6309
- describe: "Workspace that owns the upload session."
6310
- }
6311
- }
6312
- },
6313
- abortKnowledgeDocumentUpload: {
6314
- method: "DELETE",
6315
- path: "/api/v2/knowledge/[id]/documents/uploads/[uploadId]",
6316
- pathParams: ["id", "uploadId"],
6317
- pathParamDocs: {
6318
- id: "Unique knowledge base identifier.",
6319
- uploadId: "Upload session identifier returned when the upload was created."
6320
- },
6321
- responseMode: "json",
6322
- summary: "Abort Document Upload",
6323
- query: {
6324
- workspaceId: {
6325
- kind: "string",
6326
- required: true,
6327
- describe: "Workspace that owns the knowledge base."
6328
- }
6329
- }
6330
- },
6331
- addTableColumn: {
6332
- method: "POST",
6333
- path: "/api/v2/tables/[tableId]/columns",
6334
- pathParams: ["tableId"],
6335
- pathParamDocs: { tableId: "Unique table identifier." },
6336
- responseMode: "json",
6337
- summary: "Add Column",
6338
- body: {
6339
- workspaceId: { kind: "string", required: true, describe: "Workspace that owns the table." },
6340
- column: { kind: "object", required: true, describe: "Column definition to add." }
6341
- }
6342
- },
6343
- addWorkflowGroup: {
6344
- method: "POST",
6345
- path: "/api/v2/tables/[tableId]/groups",
6346
- pathParams: ["tableId"],
6347
- pathParamDocs: { tableId: "Unique table identifier." },
6348
- responseMode: "json",
6349
- summary: "Add Workflow Group",
6350
- body: {
6351
- workspaceId: { kind: "string", required: true, describe: "Unique workspace identifier." },
6352
- group: {
6353
- kind: "object",
6354
- required: true,
6355
- describe: "Workflow or enrichment producer definition."
6356
- },
6357
- outputColumns: {
6358
- kind: "array",
6359
- required: true,
6360
- describe: "Columns created for producer outputs."
6361
- },
6362
- autoRun: {
6363
- kind: "boolean",
6364
- default: false,
6365
- describe: "Whether to schedule existing rows after group creation."
6420
+ },
6421
+ addWorkflowGroup: {
6422
+ method: "POST",
6423
+ path: "/api/v2/tables/[tableId]/groups",
6424
+ pathParams: ["tableId"],
6425
+ pathParamDocs: { tableId: "Unique table identifier." },
6426
+ responseMode: "json",
6427
+ summary: "Add Workflow Group",
6428
+ body: {
6429
+ workspaceId: { kind: "string", required: true, describe: "Unique workspace identifier." },
6430
+ group: {
6431
+ kind: "object",
6432
+ required: true,
6433
+ describe: "Workflow or enrichment producer definition."
6434
+ },
6435
+ outputColumns: {
6436
+ kind: "array",
6437
+ required: true,
6438
+ describe: "Columns created for producer outputs."
6439
+ },
6440
+ autoRun: {
6441
+ kind: "boolean",
6442
+ default: false,
6443
+ describe: "Whether to schedule existing rows after group creation."
6366
6444
  }
6367
6445
  }
6368
6446
  },
@@ -7993,7 +8071,25 @@ var V2_OPERATIONS = {
7993
8071
  },
7994
8072
  folderPath: {
7995
8073
  kind: "string",
7996
- describe: "Restrict results to files directly inside this folder. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error."
8074
+ describe: "Restrict results to files inside this folder — its direct children, or its whole subtree when `recursive` is true. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error."
8075
+ },
8076
+ recursive: {
8077
+ kind: "enum",
8078
+ values: [
8079
+ "true",
8080
+ "1",
8081
+ "yes",
8082
+ "on",
8083
+ "y",
8084
+ "enabled",
8085
+ "false",
8086
+ "0",
8087
+ "no",
8088
+ "off",
8089
+ "n",
8090
+ "disabled"
8091
+ ],
8092
+ describe: "Whether the folder filter includes files in subfolders. Defaults to true when a search is set, false otherwise, so listing a folder shows that folder while searching one looks through everything in it. Ignored when no folder filter is set, which already spans the workspace. The listed spellings are the whole accepted vocabulary and are case-sensitive; any other value is rejected."
7997
8093
  },
7998
8094
  scope: {
7999
8095
  kind: "enum",
@@ -8968,6 +9064,10 @@ var V2_OPERATIONS = {
8968
9064
  kind: "string",
8969
9065
  required: true,
8970
9066
  describe: "Write-only secret value. It is never returned."
9067
+ },
9068
+ description: {
9069
+ kind: "string",
9070
+ describe: "What the secret is for, shown to teammates. Workspace scope only — sending it for a personal secret is rejected. Omit it to leave an existing description untouched; send null or an empty string to clear one."
8971
9071
  }
8972
9072
  }
8973
9073
  },
@@ -9380,71 +9480,328 @@ var V2_OPERATIONS = {
9380
9480
  }
9381
9481
  };
9382
9482
 
9383
- // src/runtime/request.ts
9384
- import { existsSync as existsSync2, readFileSync as readFileSync2, readSync } from "node:fs";
9385
-
9386
- // src/contract/commands.ts
9387
- var TABLE_NAME_HELP = "Identifier: letters, numbers, and underscores; cannot start with a number";
9388
- var TABLE_FILTER_HELP = 'Predicate: {"all":[{"field":"status","op":"eq","value":"active"}]}; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull';
9389
- var TABLE_SORT_HELP = 'Ordered sort keys: [{"field":"createdAt","direction":"desc"}] (direction: asc or desc)';
9390
- var CUSTOM_TOOL_SCHEMA_HELP = 'OpenAI function schema: {"type":"function","function":{"name":"...","parameters":{"type":"object","properties":{}}}}';
9391
- var FOLDER_PATH_INPUT = {
9392
- describe: "Folder path; the leading / is optional"
9393
- };
9394
- var FOLDER_PATH_FLAG = {
9395
- ...FOLDER_PATH_INPUT,
9396
- name: "folder"
9397
- };
9398
- var FOLDER_DELETE_FLAGS = {
9399
- path: FOLDER_PATH_INPUT,
9400
- recursive: { boolean: true, describe: "Delete the folder and its descendants" }
9401
- };
9402
- var KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS = { id: "knowledgeBaseId" };
9403
- var WORKFLOW_RUN_SCOPE = {
9404
- id: {
9405
- name: "workflow",
9406
- placeholder: "workflowId",
9407
- describe: "Workflow ID"
9483
+ // src/commands/auth.ts
9484
+ function openBrowser(url) {
9485
+ const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : [process.platform === "darwin" ? "open" : "xdg-open", [url]];
9486
+ try {
9487
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
9488
+ child.on("error", () => {});
9489
+ child.unref();
9490
+ } catch {}
9491
+ }
9492
+ function presentAuthentication(source) {
9493
+ switch (source) {
9494
+ case "flag":
9495
+ return { authenticated: true, source: "flag" };
9496
+ case "env":
9497
+ return { authenticated: true, source: "env" };
9498
+ case "credentials":
9499
+ return { authenticated: true, source: "credentials" };
9500
+ case "unset":
9501
+ return { authenticated: false, source: "unset" };
9502
+ case "config":
9503
+ case "default":
9504
+ throw new SimApiError(`Unexpected API key source "${source}".`, 0);
9408
9505
  }
9409
- };
9410
- var FOLDER_LIST_COLUMNS = [
9411
- { header: "path" },
9412
- { header: "name" },
9413
- { header: "parent", path: "parentPath" },
9414
- { header: "updated", path: "updatedAt", format: "timestamp" }
9415
- ];
9416
- function moveResource(command, resource) {
9417
- return {
9418
- command,
9419
- positionals: ["folderPath"],
9420
- requestFields: ["folderPath"],
9421
- describe: `Move a ${resource} to a folder`
9422
- };
9423
9506
  }
9424
- var CLI_CONTRACT = {
9425
- createCredentialConnection: { hidden: true },
9426
- createServiceAccountCredential: { hidden: true },
9427
- getBillingStatus: {
9428
- command: "billing status",
9429
- allWorkspaces: true,
9430
- describe: "Show billing status and current-period credit usage",
9431
- fields: [
9432
- { header: "plan" },
9433
- { header: "status" },
9434
- { header: "workspace", path: "workspaceId" },
9435
- { header: "period start", path: "period.start", format: "timestamp" },
9436
- { header: "period end", path: "period.end", format: "timestamp" },
9437
- { header: "used credits", path: "credits.used" },
9438
- { header: "limit credits", path: "credits.limit" },
9439
- { header: "remaining credits", path: "credits.remaining" }
9440
- ]
9441
- },
9442
- listBillingLogs: {
9443
- command: "billing logs",
9444
- allWorkspaces: true,
9445
- describe: "List credit usage events",
9446
- flags: {
9447
- source: { describe: "Filter by usage source; sim-chat combines Copilot and workspace chat" },
9507
+ async function confirmProfileOverwrite(profileName) {
9508
+ if (!process.stdin.isTTY) {
9509
+ throw new SimApiError(`Profile "${profileName}" already exists. Re-run with --yes to overwrite it.`, 0);
9510
+ }
9511
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
9512
+ try {
9513
+ const answer = await prompt.question(`Profile "${profileName}" already exists. Replace its API key and login defaults? (y/N) `);
9514
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
9515
+ } finally {
9516
+ prompt.close();
9517
+ }
9518
+ }
9519
+ function loginCommand() {
9520
+ return new Command("login").description("Authorize this terminal and store an API key for the profile").option("--scope <scope>", "Key space to mint from: platform or copilot", "platform").option("--no-browser", "Print the URL instead of opening a browser").option("-y, --yes", "Overwrite an existing profile without prompting").action(async (options, command) => {
9521
+ const profile = profileFrom(command);
9522
+ if (options.scope !== "platform" && options.scope !== "copilot") {
9523
+ throw new SimApiError(`Unknown scope "${options.scope}". Use platform or copilot.`, 0);
9524
+ }
9525
+ const scope = options.scope;
9526
+ if (readCredentialsProfile(profile.name).api_key && !options.yes) {
9527
+ const confirmed = await confirmProfileOverwrite(profile.name);
9528
+ if (!confirmed) {
9529
+ console.log(source_default.dim("Login cancelled; the existing profile was not changed."));
9530
+ return;
9531
+ }
9532
+ }
9533
+ const auth = createAuthRequest();
9534
+ const url = buildApprovalUrl(profile.endpoint, auth, scope, profile.workspaceId ?? undefined);
9535
+ console.log(`Signing in to ${source_default.bold(profile.endpoint)} as profile ${source_default.bold(profile.name)}`);
9536
+ console.log(`
9537
+ Pairing code: ${source_default.bold(auth.pairing)}`);
9538
+ console.log(source_default.dim(`Confirm this code matches what the browser shows before approving.
9539
+ `));
9540
+ console.log(url);
9541
+ if (options.browser)
9542
+ openBrowser(url);
9543
+ console.log(source_default.dim(`
9544
+ Waiting for approval…`));
9545
+ const key = await pollForKey(profile.endpoint, auth);
9546
+ if (key.scope !== scope) {
9547
+ throw new SimApiError(`Server issued a ${key.scope} key but this profile needs a ${scope} key. Update the Sim deployment, or run: sim login --scope ${key.scope}`, 0);
9548
+ }
9549
+ writeCredentialsProfile(profile.name, key.apiKey);
9550
+ const settings = {
9551
+ endpoint: profile.endpoint,
9552
+ workspace: key.workspaceId ?? null
9553
+ };
9554
+ writeConfigProfile(profile.name, settings);
9555
+ console.log(source_default.green(`
9556
+ ✓ Logged in. Key stored in ${credentialsPath()}`));
9557
+ if (key.workspaceBound && key.workspaceId) {
9558
+ console.log(source_default.dim(` Workspace-scoped key — it can only reach ${key.workspaceId}.`));
9559
+ } else if (key.workspaceId) {
9560
+ console.log(source_default.dim(` Personal key, defaulting to ${key.workspaceId}. Override per command with --workspace.`));
9561
+ } else {
9562
+ console.log(source_default.dim(" Personal key with no default workspace. Set one with: sim configure --set-workspace <id>"));
9563
+ }
9564
+ });
9565
+ }
9566
+ function logoutCommand() {
9567
+ return new Command("logout").description("Remove the profile's stored API key").option("--all", "Remove the profile entirely, including its settings").action((options, command) => {
9568
+ const profile = profileFrom(command);
9569
+ if (options.all) {
9570
+ const removed = deleteProfile(profile.name);
9571
+ if (!removed.config && !removed.credentials) {
9572
+ console.log(source_default.dim(`Nothing stored for profile "${profile.name}".`));
9573
+ return;
9574
+ }
9575
+ console.log(source_default.green(`✓ Removed profile "${profile.name}".`));
9576
+ return;
9577
+ }
9578
+ if (!readCredentialsProfile(profile.name).api_key) {
9579
+ console.log(source_default.dim(`No stored key for profile "${profile.name}".`));
9580
+ return;
9581
+ }
9582
+ writeCredentialsProfile(profile.name, null);
9583
+ console.log(source_default.green(`✓ Removed the stored key for profile "${profile.name}".`));
9584
+ console.log(source_default.dim(" The key itself is still active — revoke it in Settings → API keys."));
9585
+ });
9586
+ }
9587
+ var CREDENTIAL_VERDICT_STATUSES = new Set([401, 403, 404]);
9588
+ var WHOAMI_EXIT_CODES = {
9589
+ verified: 0,
9590
+ disabled: 0,
9591
+ unauthenticated: 1,
9592
+ rejected: 1,
9593
+ unreachable: 2,
9594
+ "no-workspace": 2
9595
+ };
9596
+ async function verifyProfile(client, profile) {
9597
+ if (!profile.apiKey) {
9598
+ return {
9599
+ status: "unauthenticated",
9600
+ workspace: null,
9601
+ detail: `no API key — run: sim login --profile ${profile.name}`
9602
+ };
9603
+ }
9604
+ if (!profile.workspaceId) {
9605
+ return {
9606
+ status: "no-workspace",
9607
+ workspace: null,
9608
+ detail: `no workspace to check against — run: sim configure --profile ${profile.name} --set-workspace <id>`
9609
+ };
9610
+ }
9611
+ const operation = V2_OPERATIONS.getWorkspace;
9612
+ try {
9613
+ const response = await client.request(resolvePath(operation.path, { workspaceId: profile.workspaceId }), { method: operation.method });
9614
+ const { id, name, memberCount } = response.data;
9615
+ return { status: "verified", workspace: { id, name, memberCount }, detail: null };
9616
+ } catch (error) {
9617
+ if (!(error instanceof SimApiError))
9618
+ throw error;
9619
+ return {
9620
+ status: CREDENTIAL_VERDICT_STATUSES.has(error.status) ? "rejected" : "unreachable",
9621
+ workspace: null,
9622
+ detail: error.message
9623
+ };
9624
+ }
9625
+ }
9626
+ function presentVerification(verification) {
9627
+ if (verification.status === "verified") {
9628
+ const { name, memberCount } = verification.workspace;
9629
+ const members = `${memberCount} ${memberCount === 1 ? "member" : "members"}`;
9630
+ return `${source_default.green("✓")} ${safeOneLine(name)} · ${members}`;
9631
+ }
9632
+ const detail = safeOneLine(verification.detail);
9633
+ switch (verification.status) {
9634
+ case "rejected":
9635
+ return `${source_default.red("✗")} ${detail}`;
9636
+ case "unauthenticated":
9637
+ return source_default.yellow(`not logged in — ${detail}`);
9638
+ case "disabled":
9639
+ return source_default.dim(detail);
9640
+ default:
9641
+ return source_default.yellow(`could not check — ${detail}`);
9642
+ }
9643
+ }
9644
+ function whoamiCommand() {
9645
+ return new Command("whoami").description("Show the resolved profile, where each setting came from, and whether it works").option("--no-verify", "Skip the API check and only print the resolved settings").action(async (options, command) => {
9646
+ const { client, profile } = clientFrom(command);
9647
+ const { sources } = profile;
9648
+ const authentication = presentAuthentication(sources.apiKey);
9649
+ const verification = options.verify ? await verifyProfile(client, profile) : { status: "disabled", workspace: null, detail: "not checked (--no-verify)" };
9650
+ const annotate = (value, source) => source === "unset" ? source_default.dim("not set") : `${value} ${source_default.dim(`(${source})`)}`;
9651
+ printRecord(profile.output, [
9652
+ ["Profile", profile.name],
9653
+ ["Endpoint", annotate(profile.endpoint, sources.endpoint)],
9654
+ [
9655
+ "API key",
9656
+ authentication.authenticated ? annotate("configured", authentication.source) : source_default.yellow("not logged in")
9657
+ ],
9658
+ ["Workspace", annotate(profile.workspaceId ?? "", sources.workspaceId)],
9659
+ ["Output", annotate(profile.output, sources.output)],
9660
+ ["Verified", presentVerification(verification)]
9661
+ ], {
9662
+ profile: profile.name,
9663
+ endpoint: profile.endpoint,
9664
+ workspaceId: profile.workspaceId,
9665
+ output: profile.output,
9666
+ authenticated: authentication.authenticated,
9667
+ sources: {
9668
+ endpoint: sources.endpoint,
9669
+ authentication: authentication.source,
9670
+ workspaceId: sources.workspaceId,
9671
+ output: sources.output
9672
+ },
9673
+ verification: {
9674
+ status: verification.status,
9675
+ workspace: verification.workspace,
9676
+ detail: verification.detail
9677
+ }
9678
+ });
9679
+ const exitCode = WHOAMI_EXIT_CODES[verification.status];
9680
+ if (exitCode !== 0)
9681
+ process.exitCode = exitCode;
9682
+ });
9683
+ }
9684
+ function profilesCommand() {
9685
+ return new Command("profiles").alias("profile").description("List the profiles defined in the config and credentials files").action((_options, command) => {
9686
+ const profiles = listProfiles();
9687
+ if (profiles.length === 0) {
9688
+ console.log(source_default.dim("No profiles yet. Run: sim login"));
9689
+ return;
9690
+ }
9691
+ const active = profileFrom(command).name;
9692
+ for (const name of profiles) {
9693
+ const marker = name === active ? source_default.green("*") : " ";
9694
+ const hasKey = Boolean(readCredentialsProfile(name).api_key);
9695
+ console.log(`${marker} ${name}${hasKey ? "" : source_default.dim(" (no key)")}`);
9696
+ }
9697
+ });
9698
+ }
9699
+
9700
+ // src/commands/configure.ts
9701
+ function configureCommand() {
9702
+ return new Command("configure").description("Set a profile's endpoint, default workspace, or output format").option("--set-endpoint <url>", "Sim deployment to talk to").option("--set-workspace <id>", "Default workspace for workspace-scoped commands").option("--set-output <format>", `Default output format (${OUTPUT_FORMATS.join(" | ")})`).option("--unset <key...>", "Remove settings (endpoint, workspace, output)").action((options, command) => {
9703
+ const profile = profileFrom(command);
9704
+ const updates = {};
9705
+ if (options.setEndpoint) {
9706
+ updates.endpoint = normalizeEndpoint(options.setEndpoint, "--set-endpoint");
9707
+ }
9708
+ if (options.setWorkspace)
9709
+ updates.workspace = options.setWorkspace;
9710
+ if (options.setOutput) {
9711
+ if (!OUTPUT_FORMATS.includes(options.setOutput)) {
9712
+ throw new SimApiError(`Unknown output format "${options.setOutput}". Use one of: ${OUTPUT_FORMATS.join(", ")}`, 0);
9713
+ }
9714
+ updates.output = options.setOutput;
9715
+ }
9716
+ for (const key of options.unset ?? []) {
9717
+ if (!["endpoint", "workspace", "output"].includes(key)) {
9718
+ throw new SimApiError(`Cannot unset "${key}". Use endpoint, workspace, or output.`, 0);
9719
+ }
9720
+ updates[key] = null;
9721
+ }
9722
+ if (Object.keys(updates).length === 0) {
9723
+ const current = readConfigProfile(profile.name);
9724
+ if (Object.keys(current).length === 0) {
9725
+ console.log(source_default.dim(`No settings stored for profile "${profile.name}".`));
9726
+ return;
9727
+ }
9728
+ for (const [key, value] of Object.entries(current)) {
9729
+ console.log(`${source_default.dim(`${key}:`)} ${value}`);
9730
+ }
9731
+ return;
9732
+ }
9733
+ writeConfigProfile(profile.name, updates);
9734
+ console.log(source_default.green(`✓ Updated profile "${profile.name}" in ${configPath()}`));
9735
+ });
9736
+ }
9737
+
9738
+ // src/runtime/request.ts
9739
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readSync } from "node:fs";
9740
+
9741
+ // src/contract/commands.ts
9742
+ var TABLE_NAME_HELP = "Identifier: letters, numbers, and underscores; cannot start with a number";
9743
+ var TABLE_FILTER_HELP = 'Predicate: {"all":[{"field":"status","op":"eq","value":"active"}]}; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull';
9744
+ var TABLE_SORT_HELP = 'Ordered sort keys: [{"field":"createdAt","direction":"desc"}] (direction: asc or desc)';
9745
+ var CUSTOM_TOOL_SCHEMA_HELP = 'OpenAI function schema: {"type":"function","function":{"name":"...","parameters":{"type":"object","properties":{}}}}';
9746
+ var FOLDER_PATH_INPUT = {
9747
+ describe: "Folder path as shown in the app; the leading / is optional",
9748
+ folderPath: true
9749
+ };
9750
+ var FOLDER_PATH_FLAG = {
9751
+ ...FOLDER_PATH_INPUT,
9752
+ name: "folder"
9753
+ };
9754
+ var FOLDER_DELETE_FLAGS = {
9755
+ path: FOLDER_PATH_INPUT,
9756
+ recursive: { boolean: true, describe: "Delete the folder and its descendants" }
9757
+ };
9758
+ var KNOWLEDGE_BASE_PATH_ARGUMENT = { id: "knowledgeBaseId" };
9759
+ var WORKFLOW_RUN_SCOPE = {
9760
+ id: {
9761
+ name: "workflow",
9762
+ placeholder: "workflowId",
9763
+ describe: "Workflow ID"
9764
+ }
9765
+ };
9766
+ var FOLDER_COLUMN = { header: "folder", path: "folderPath", format: "folder-path" };
9767
+ var FOLDER_LIST_COLUMNS = [
9768
+ { header: "path", format: "folder-path" },
9769
+ { header: "name" },
9770
+ { header: "parent", path: "parentPath", format: "folder-path" },
9771
+ { header: "updated", path: "updatedAt", format: "timestamp" }
9772
+ ];
9773
+ function moveResource(command, resource) {
9774
+ return {
9775
+ command,
9776
+ positionals: ["folderPath"],
9777
+ requestFields: ["folderPath"],
9778
+ describe: `Move a ${resource} to a folder`
9779
+ };
9780
+ }
9781
+ var CLI_CONTRACT = {
9782
+ createCredentialConnection: { hidden: true },
9783
+ createServiceAccountCredential: { hidden: true },
9784
+ getBillingStatus: {
9785
+ command: "billing status",
9786
+ allWorkspaces: true,
9787
+ describe: "Show billing status and current-period credit usage",
9788
+ fields: [
9789
+ { header: "plan" },
9790
+ { header: "status" },
9791
+ { header: "workspace", path: "workspaceId" },
9792
+ { header: "period start", path: "period.start", format: "timestamp" },
9793
+ { header: "period end", path: "period.end", format: "timestamp" },
9794
+ { header: "used credits", path: "credits.used" },
9795
+ { header: "limit credits", path: "credits.limit" },
9796
+ { header: "remaining credits", path: "credits.remaining" }
9797
+ ]
9798
+ },
9799
+ listBillingLogs: {
9800
+ command: "billing logs",
9801
+ allWorkspaces: true,
9802
+ describe: "List credit usage events",
9803
+ flags: {
9804
+ source: { describe: "Filter by usage source; sim-chat combines Copilot and workspace chat" },
9448
9805
  period: { describe: "Billing period" },
9449
9806
  startDate: { describe: "Custom period start (ISO 8601)" },
9450
9807
  endDate: { describe: "Custom period end (ISO 8601)" }
@@ -9480,7 +9837,7 @@ var CLI_CONTRACT = {
9480
9837
  bulkUpdateKnowledgeDocuments: {
9481
9838
  command: "knowledge documents batch-update",
9482
9839
  describe: "Enable or disable every matching document",
9483
- pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS,
9840
+ pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
9484
9841
  flags: {
9485
9842
  documentIds: { name: "document", list: true },
9486
9843
  selectAll: { boolean: true, describe: "Apply to every document in the knowledge base" }
@@ -9490,6 +9847,11 @@ var CLI_CONTRACT = {
9490
9847
  command: "workflows undeploy",
9491
9848
  describe: "Take a workflow out of deployment"
9492
9849
  },
9850
+ getWorkflowDeployment: {
9851
+ command: "workflows deployment status",
9852
+ renamedFrom: ["workflows deployment list"],
9853
+ describe: "Show a workflow’s current deployment"
9854
+ },
9493
9855
  setSecret: { hidden: true },
9494
9856
  deleteTable: { confirm: "This deletes the table and all of its rows." },
9495
9857
  deleteTableRow: { confirm: "This deletes the row." },
@@ -9499,7 +9861,7 @@ var CLI_CONTRACT = {
9499
9861
  },
9500
9862
  deleteKnowledgeBase: { confirm: "This deletes the knowledge base and every document in it." },
9501
9863
  deleteKnowledgeDocument: {
9502
- pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS,
9864
+ pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
9503
9865
  confirm: "This deletes the document and its embeddings."
9504
9866
  },
9505
9867
  deleteFile: { confirm: "This archives the file." },
@@ -9529,7 +9891,10 @@ var CLI_CONTRACT = {
9529
9891
  workflowIds: { name: "workflow", list: true },
9530
9892
  folderPaths: { ...FOLDER_PATH_FLAG, list: true },
9531
9893
  triggers: { name: "trigger", list: true },
9532
- details: { describe: "Response detail level" },
9894
+ details: {
9895
+ requestDefault: "full",
9896
+ describe: "Response detail level; full is requested by default to name each run’s workflow"
9897
+ },
9533
9898
  includeTraceSpans: {
9534
9899
  boolean: true,
9535
9900
  describe: "Include trace spans in JSON or YAML output (implies full detail)"
@@ -9582,7 +9947,7 @@ var CLI_CONTRACT = {
9582
9947
  },
9583
9948
  itemsPath: "results",
9584
9949
  columns: [
9585
- { header: "score", path: "similarity" },
9950
+ { header: "score", path: "similarity", format: "score" },
9586
9951
  { header: "document", path: "documentName" },
9587
9952
  { header: "chunk", path: "chunkIndex" },
9588
9953
  { header: "content" }
@@ -9656,7 +10021,7 @@ var CLI_CONTRACT = {
9656
10021
  columns: [
9657
10022
  { header: "id" },
9658
10023
  { header: "name" },
9659
- { header: "folder", path: "folderPath" },
10024
+ FOLDER_COLUMN,
9660
10025
  { header: "rows", path: "rowCount" },
9661
10026
  { header: "updated", path: "updatedAt", format: "timestamp" }
9662
10027
  ]
@@ -9666,7 +10031,7 @@ var CLI_CONTRACT = {
9666
10031
  columns: [
9667
10032
  { header: "id" },
9668
10033
  { header: "name" },
9669
- { header: "folder", path: "folderPath" },
10034
+ FOLDER_COLUMN,
9670
10035
  { header: "deployed", path: "isDeployed", format: "bool" },
9671
10036
  { header: "runs", path: "runCount" },
9672
10037
  { header: "last run", path: "lastRunAt", format: "timestamp" }
@@ -9677,7 +10042,7 @@ var CLI_CONTRACT = {
9677
10042
  columns: [
9678
10043
  { header: "id" },
9679
10044
  { header: "name" },
9680
- { header: "folder", path: "folderPath" },
10045
+ FOLDER_COLUMN,
9681
10046
  { header: "size", format: "bytes" },
9682
10047
  { header: "type" },
9683
10048
  { header: "uploaded by", path: "uploadedByEmail" },
@@ -9690,15 +10055,17 @@ var CLI_CONTRACT = {
9690
10055
  columns: [
9691
10056
  { header: "id" },
9692
10057
  { header: "name" },
9693
- { header: "folder", path: "folderPath" },
10058
+ FOLDER_COLUMN,
9694
10059
  { header: "docs", path: "docCount" },
9695
10060
  { header: "tokens", path: "tokenCount" },
9696
10061
  { header: "model", path: "embeddingModel" }
9697
10062
  ]
9698
10063
  },
9699
- getKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS },
10064
+ getKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
10065
+ updateKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
10066
+ listKnowledgeTags: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
9700
10067
  listKnowledgeDocuments: {
9701
- pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS,
10068
+ pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
9702
10069
  columns: [
9703
10070
  { header: "id" },
9704
10071
  { header: "filename" },
@@ -9742,12 +10109,24 @@ var CLI_CONTRACT = {
9742
10109
  { header: "updated", path: "updatedAt", format: "timestamp" }
9743
10110
  ]
9744
10111
  },
10112
+ listCredentialProviders: {
10113
+ columns: [
10114
+ { header: "type" },
10115
+ { header: "service", path: "serviceId" },
10116
+ { header: "provider", path: "providerId" },
10117
+ { header: "name" },
10118
+ { header: "family", path: "providerFamily" },
10119
+ { header: "available", format: "bool" },
10120
+ { header: "description" }
10121
+ ]
10122
+ },
9745
10123
  listSecrets: {
9746
10124
  columns: [
9747
10125
  { header: "name" },
9748
10126
  { header: "scope" },
9749
10127
  { header: "role" },
9750
- { header: "updated", path: "updatedAt", format: "timestamp" }
10128
+ { header: "updated", path: "updatedAt", format: "timestamp" },
10129
+ { header: "description" }
9751
10130
  ]
9752
10131
  },
9753
10132
  getWorkspace: {
@@ -9813,7 +10192,7 @@ var CLI_CONTRACT = {
9813
10192
  { header: "name" },
9814
10193
  { header: "size", format: "bytes" },
9815
10194
  { header: "type" },
9816
- { header: "folder", path: "folderPath" },
10195
+ FOLDER_COLUMN,
9817
10196
  { header: "uploaded by", path: "uploadedByEmail" },
9818
10197
  { header: "uploaded", path: "uploadedAt", format: "timestamp" },
9819
10198
  { header: "updated", path: "updatedAt", format: "timestamp" },
@@ -9840,6 +10219,11 @@ var CLI_CONTRACT = {
9840
10219
  command: "files rename",
9841
10220
  describe: "Rename a file"
9842
10221
  },
10222
+ restoreFile: {
10223
+ command: "files restore",
10224
+ renamedFrom: ["files restore create"],
10225
+ describe: "Restore an archived file"
10226
+ },
9843
10227
  updateFileContent: {
9844
10228
  command: "files set-content",
9845
10229
  describe: "Replace a file’s contents",
@@ -9992,13 +10376,26 @@ var CLI_CONTRACT = {
9992
10376
  command: "tables rows find",
9993
10377
  describe: "Find rows matching a predicate",
9994
10378
  flags: {
9995
- q: { describe: "Value to find" },
10379
+ q: { name: "query", renamedFrom: ["q"], describe: "Value to find" },
9996
10380
  predicate: { name: "filter", json: true, describe: TABLE_FILTER_HELP },
9997
10381
  sort: { json: true, describe: TABLE_SORT_HELP }
9998
10382
  },
9999
10383
  itemsPath: "matches",
10000
10384
  columns: [{ header: "ordinal" }, { header: "row", path: "rowId" }, { header: "column" }]
10001
10385
  },
10386
+ queryRowsCount: {
10387
+ command: "tables rows count",
10388
+ renamedFrom: ["tables count create"],
10389
+ describe: "Count rows matching a filter",
10390
+ flags: {
10391
+ predicate: {
10392
+ name: "filter",
10393
+ renamedFrom: ["predicate"],
10394
+ json: true,
10395
+ describe: TABLE_FILTER_HELP
10396
+ }
10397
+ }
10398
+ },
10002
10399
  runTableColumn: {
10003
10400
  command: "tables columns run",
10004
10401
  describe: "Run a column’s workflow",
@@ -10216,7 +10613,7 @@ function readArgumentSource(raw, flagName) {
10216
10613
  }
10217
10614
  }
10218
10615
  try {
10219
- return { text: readFileSync2(path, "utf8"), from: ` (read from ${path})` };
10616
+ return { text: readFileSync3(path, "utf8"), from: ` (read from ${path})` };
10220
10617
  } catch (error) {
10221
10618
  throw new SimApiError(`--${flagName} cannot read ${path}: ${error.message}`, 0);
10222
10619
  }
@@ -10251,6 +10648,26 @@ function readListValues(raw, flagName) {
10251
10648
  return trimmed;
10252
10649
  });
10253
10650
  }
10651
+ var PERCENT_ESCAPE = /%[0-9A-Fa-f]{2}/;
10652
+ var SUB_DELIMITERS = /[!'()*]/g;
10653
+ function encodeFolderPathSegment(name) {
10654
+ if (name === ".")
10655
+ return "%2E";
10656
+ if (name === "..")
10657
+ return "%2E%2E";
10658
+ return encodeURIComponent(name).replace(SUB_DELIMITERS, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
10659
+ }
10660
+ function encodeFolderPath(value) {
10661
+ return value.split("/").map((segment) => {
10662
+ if (!PERCENT_ESCAPE.test(segment))
10663
+ return encodeFolderPathSegment(segment);
10664
+ try {
10665
+ return encodeFolderPathSegment(decodeURIComponent(segment));
10666
+ } catch {
10667
+ return encodeFolderPathSegment(segment);
10668
+ }
10669
+ }).join("/");
10670
+ }
10254
10671
  function pathHint(raw) {
10255
10672
  if (raw.startsWith("@") || /^\s*[[{"\-\d]|^\s*(true|false|null)/.test(raw))
10256
10673
  return "";
@@ -10260,7 +10677,7 @@ function coerce(raw, field, flag, flagName) {
10260
10677
  if (raw === undefined)
10261
10678
  return;
10262
10679
  if (flag.list) {
10263
- const values = readListValues(raw, flagName);
10680
+ const values = readListValues(raw, flagName).map((value) => flag.folderPath ? encodeFolderPath(value) : value);
10264
10681
  return field.kind === "string" ? values.join(",") : values;
10265
10682
  }
10266
10683
  if (takesJson(field, flag)) {
@@ -10285,6 +10702,8 @@ function coerce(raw, field, flag, flagName) {
10285
10702
  if (choices && !choices.includes(String(raw))) {
10286
10703
  throw new SimApiError(`--${flagName} must be one of: ${choices.join(", ")}`, 0);
10287
10704
  }
10705
+ if (flag.folderPath && typeof raw === "string")
10706
+ return encodeFolderPath(raw);
10288
10707
  return raw;
10289
10708
  }
10290
10709
  function asQueryValue(value) {
@@ -10325,7 +10744,8 @@ function buildRequest(operation, positional, flags, workspaceId) {
10325
10744
  continue;
10326
10745
  const flagName = flagNameFor(operation, field);
10327
10746
  const omitProfileWorkspace = commandSpec.allWorkspaces && flags.allWorkspaces === true;
10328
- const raw = field === PROFILE_INJECTED_FIELD ? omitProfileWorkspace ? undefined : workspaceId : flags[camel(flagName)];
10747
+ const provided = field === PROFILE_INJECTED_FIELD ? omitProfileWorkspace ? undefined : workspaceId : flags[camel(flagName)];
10748
+ const raw = provided ?? flag.requestDefault;
10329
10749
  const value = coerce(raw ?? undefined, descriptor, flag, flagName);
10330
10750
  if (value === undefined) {
10331
10751
  if (descriptor.required) {
@@ -10498,6 +10918,15 @@ function countTraceSpans(value) {
10498
10918
  function at(row, path) {
10499
10919
  return path.split(".").reduce((value, key) => value && typeof value === "object" ? value[key] : undefined, row);
10500
10920
  }
10921
+ function decodeFolderPath(value) {
10922
+ return value.split("/").map((segment) => {
10923
+ try {
10924
+ return decodeURIComponent(segment);
10925
+ } catch {
10926
+ return segment;
10927
+ }
10928
+ }).join("/");
10929
+ }
10501
10930
  function renderCell(value, format, options = {}) {
10502
10931
  switch (format) {
10503
10932
  case "timestamp":
@@ -10510,8 +10939,12 @@ function renderCell(value, format, options = {}) {
10510
10939
  return bool2(value);
10511
10940
  case "cost":
10512
10941
  return typeof value === "number" ? `$${value.toFixed(4)}` : text(null);
10942
+ case "score":
10943
+ return typeof value === "number" ? value.toFixed(4) : text(null);
10513
10944
  case "count":
10514
10945
  return Array.isArray(value) ? String(value.length) : text(null);
10946
+ case "folder-path":
10947
+ return typeof value === "string" ? text(decodeFolderPath(value)) : text(value);
10515
10948
  case "trace-count": {
10516
10949
  const count = countTraceSpans(value);
10517
10950
  return `${count} ${count === 1 ? "span" : "spans"}${options.expandedTrace ? "" : " (use --trace)"}`;
@@ -10522,10 +10955,42 @@ function renderCell(value, format, options = {}) {
10522
10955
  return sanitize(typeof value === "object" ? JSON.stringify(value) : String(value));
10523
10956
  }
10524
10957
  }
10525
- var NESTED_CELL_WIDTH = 160;
10526
- function recordCell(value) {
10527
- const rendered = renderCell(value, "auto");
10528
- return rendered.length > NESTED_CELL_WIDTH ? `${rendered.slice(0, NESTED_CELL_WIDTH)}…` : rendered;
10958
+ var TIMESTAMP_KEY = /(?:At|Date)$/;
10959
+ var DURATION_KEY = /Ms$|^duration/;
10960
+ var BYTES_KEY = /^size$|(?:Size|Bytes)$/;
10961
+ var BOOL_KEY = /^(?:is|has)[A-Z]/;
10962
+ var RATIO_KEY = /^(?:similarity|score)$|(?:Similarity|Score)$/;
10963
+ var FOLDER_PATH_KEY = /^(?:path|parentPath|folderPath)$/;
10964
+ var ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
10965
+ var RATIO_PRECISION = 4;
10966
+ function inferFormat(key, value) {
10967
+ if (typeof value === "boolean")
10968
+ return BOOL_KEY.test(key) ? "bool" : null;
10969
+ if (typeof value === "string") {
10970
+ if (FOLDER_PATH_KEY.test(key))
10971
+ return "folder-path";
10972
+ return TIMESTAMP_KEY.test(key) && ISO_TIMESTAMP.test(value) && !Number.isNaN(Date.parse(value)) ? "timestamp" : null;
10973
+ }
10974
+ if (typeof value !== "number" || !Number.isFinite(value))
10975
+ return null;
10976
+ if (DURATION_KEY.test(key))
10977
+ return "duration";
10978
+ if (BYTES_KEY.test(key))
10979
+ return "bytes";
10980
+ return null;
10981
+ }
10982
+ function inferredCell(key, value) {
10983
+ if (typeof value === "number" && Number.isFinite(value) && RATIO_KEY.test(key)) {
10984
+ return value.toFixed(RATIO_PRECISION);
10985
+ }
10986
+ return renderCell(value, inferFormat(key, value) ?? "auto");
10987
+ }
10988
+ function humanizeKey(key) {
10989
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
10990
+ }
10991
+ function inferHeader(key, format) {
10992
+ const trimmed = format === "duration" || format === "bytes" ? key.replace(/(?:Ms|Bytes)$/, "") : format === "bool" ? key.replace(/^is(?=[A-Z])/, "") : key;
10993
+ return humanizeKey(trimmed || key);
10529
10994
  }
10530
10995
  function columnsFrom(specs) {
10531
10996
  return specs.map((spec) => ({
@@ -10534,9 +10999,9 @@ function columnsFrom(specs) {
10534
10999
  }));
10535
11000
  }
10536
11001
  function fieldsFrom(data, specs, options = {}) {
10537
- return specs.flatMap((spec) => {
11002
+ return specs.map((spec) => {
10538
11003
  const value = at(data, spec.path ?? spec.header);
10539
- return value === undefined ? [] : [[spec.header, renderCell(value, spec.format, options)]];
11004
+ return [spec.header, value === undefined ? text(null) : renderCell(value, spec.format, options)];
10540
11005
  });
10541
11006
  }
10542
11007
  function inferColumns(rows, expand) {
@@ -10551,7 +11016,7 @@ function inferColumns(rows, expand) {
10551
11016
  if (value !== null && typeof value === "object")
10552
11017
  continue;
10553
11018
  seen.add(key);
10554
- paths.push({ path: key, header: key });
11019
+ paths.push({ path: key, key, header: inferHeader(key, inferFormat(key, value)), owned: true });
10555
11020
  }
10556
11021
  }
10557
11022
  if (expand) {
@@ -10564,13 +11029,18 @@ function inferColumns(rows, expand) {
10564
11029
  if (nested.has(key))
10565
11030
  continue;
10566
11031
  nested.add(key);
10567
- paths.push({ path: `${expand}.${key}`, header: seen.has(key) ? `${expand}.${key}` : key });
11032
+ paths.push({
11033
+ path: `${expand}.${key}`,
11034
+ key,
11035
+ header: seen.has(key) ? `${expand}.${key}` : key,
11036
+ owned: false
11037
+ });
10568
11038
  }
10569
11039
  }
10570
11040
  }
10571
- return paths.map(({ path, header }) => ({
11041
+ return paths.map(({ path, key, header, owned }) => ({
10572
11042
  header: sanitize(header),
10573
- value: (row) => renderCell(at(row, path), "auto")
11043
+ value: (row) => owned ? inferredCell(key, at(row, path)) : renderCell(at(row, path), "auto")
10574
11044
  }));
10575
11045
  }
10576
11046
  function unwrapResource(data) {
@@ -10603,7 +11073,10 @@ function renderResult(operation, format, raw, spec, options = {}) {
10603
11073
  printList(format, data, spec.columns ? columnsFrom(spec.columns) : inferColumns(data, spec.expand));
10604
11074
  return;
10605
11075
  }
10606
- const fields = spec.fields ? fieldsFrom(data, spec.fields, options) : data && typeof data === "object" ? Object.entries(data).map(([key, value]) => [key, recordCell(value)]) : [];
11076
+ const fields = spec.fields ? fieldsFrom(data, spec.fields, options) : data && typeof data === "object" ? Object.entries(data).map(([key, value]) => [
11077
+ inferHeader(key, inferFormat(key, value)),
11078
+ inferredCell(key, value)
11079
+ ]) : [];
10607
11080
  printRecord(format, fields, data);
10608
11081
  if (spec.expandedTrace && options.expandedTrace) {
10609
11082
  const traceSpans = at(data, "traceSpans");
@@ -10716,7 +11189,7 @@ function attachCredentialCommands(program2) {
10716
11189
  }
10717
11190
 
10718
11191
  // src/commands/protocol/files-get.ts
10719
- import { once } from "node:events";
11192
+ import { once as once2 } from "node:events";
10720
11193
  import { createWriteStream } from "node:fs";
10721
11194
  import { link, lstat, mkdtemp, readlink, rename, rm } from "node:fs/promises";
10722
11195
  import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
@@ -10731,6 +11204,9 @@ function printProtocolResult(format, result) {
10731
11204
 
10732
11205
  // src/commands/protocol/files-get.ts
10733
11206
  function writeFailure(path, error) {
11207
+ if (isRequestTimeout(error)) {
11208
+ return new SimApiError(`Downloading ${path} timed out. ${RAISE_TIMEOUT_HINT}`, 0);
11209
+ }
10734
11210
  const code = error.code;
10735
11211
  if (code === "EEXIST") {
10736
11212
  return new SimApiError(`${path} already exists. Pass --force to overwrite it, or choose another output path.`, 0);
@@ -10821,7 +11297,7 @@ async function streamToStdout(body, output = process.stdout) {
10821
11297
  if (done)
10822
11298
  return;
10823
11299
  if (!output.write(value))
10824
- await once(output, "drain");
11300
+ await once2(output, "drain");
10825
11301
  }
10826
11302
  } finally {
10827
11303
  reader.releaseLock();
@@ -11001,7 +11477,7 @@ async function finishUploadSession(client, workspaceId, session, path) {
11001
11477
 
11002
11478
  // src/commands/protocol/files-upload.ts
11003
11479
  function attachFileUpload(files) {
11004
- files.command("upload").argument("<path>", "Local file to upload").description("Upload a file to the workspace").option("--folder <path>", "Destination folder path (defaults to /)").option("--name <name>", "Store it under a different name").action(async (path, options, command) => {
11480
+ files.command("upload").argument("<path>", "Local file to upload").description("Upload a file to the workspace").option("--folder <path>", "Folder path as shown in the app; defaults to the root folder").option("--name <name>", "Store it under a different name").action(async (path, options, command) => {
11005
11481
  const { client, profile } = clientFrom(command);
11006
11482
  const workspaceId = client.requireWorkspace();
11007
11483
  const { name, size } = await localFile(path, options.name);
@@ -11012,7 +11488,7 @@ function attachFileUpload(files) {
11012
11488
  name,
11013
11489
  contentType: contentTypeFor(name),
11014
11490
  size,
11015
- ...options.folder !== undefined ? { folderPath: options.folder } : {}
11491
+ ...options.folder !== undefined ? { folderPath: encodeFolderPath(options.folder) } : {}
11016
11492
  }
11017
11493
  });
11018
11494
  const { session, uploadToken, transfer } = created.data;
@@ -11071,13 +11547,285 @@ function attachKnowledgeDocumentUpload(documents) {
11071
11547
  if (!completed.document) {
11072
11548
  throw new Error(`Knowledge upload ${session.id} completed without a document`);
11073
11549
  }
11074
- printProtocolResult(profile.output, {
11075
- id: completed.document.id,
11076
- knowledgeBaseId: completed.document.knowledgeBaseId,
11077
- name: completed.document.filename,
11078
- size: completed.document.fileSize,
11079
- status: completed.document.processingStatus
11080
- });
11550
+ printProtocolResult(profile.output, {
11551
+ id: completed.document.id,
11552
+ knowledgeBaseId: completed.document.knowledgeBaseId,
11553
+ name: completed.document.filename,
11554
+ size: completed.document.fileSize,
11555
+ status: completed.document.processingStatus
11556
+ });
11557
+ });
11558
+ }
11559
+
11560
+ // src/commands/protocol/logs-follow.ts
11561
+ var DEFAULT_BACKLOG = 10;
11562
+ var DEFAULT_INTERVAL_SECONDS = 3;
11563
+ var MIN_INTERVAL_SECONDS = 0.1;
11564
+ var MAX_BACKOFF_MS = 30000;
11565
+ var POLL_PAGE_SIZE = 100;
11566
+ var MAX_PAGES_PER_POLL = 10;
11567
+ var MAX_REMEMBERED_RUNS = 5000;
11568
+ var MAX_CELL_WIDTH2 = 60;
11569
+ var WAIT_SLICE_MS = 250;
11570
+ var RETRYABLE_CLIENT_STATUSES = new Set([408, 425, 429]);
11571
+ var ERASE_LINE = `${String.fromCharCode(27)}[K`;
11572
+ function collect(value, previous) {
11573
+ return [...previous, value];
11574
+ }
11575
+ function at2(row, path) {
11576
+ return path.split(".").reduce((value, key) => value && typeof value === "object" ? value[key] : undefined, row);
11577
+ }
11578
+ function renderCell2(value, format) {
11579
+ switch (format) {
11580
+ case "timestamp":
11581
+ return timestamp2(value);
11582
+ case "duration":
11583
+ return duration(value);
11584
+ case "bytes":
11585
+ return bytes(value);
11586
+ case "bool":
11587
+ return bool2(value);
11588
+ case "cost":
11589
+ return typeof value === "number" ? `$${value.toFixed(4)}` : text(null);
11590
+ default:
11591
+ return text(typeof value === "object" && value !== null ? JSON.stringify(value) : value);
11592
+ }
11593
+ }
11594
+ var COLUMNS = (CLI_CONTRACT.listLogs?.columns ?? []).map((spec) => ({
11595
+ header: spec.header,
11596
+ value: (row) => renderCell2(at2(row, spec.path ?? spec.header), spec.format)
11597
+ }));
11598
+ function oneLine2(value) {
11599
+ return value.replace(/\s*[\r\n\t]+\s*/g, " ");
11600
+ }
11601
+ function pad2(value, width) {
11602
+ return value + " ".repeat(Math.max(0, width - visibleWidth(value)));
11603
+ }
11604
+ function clamp2(value, width) {
11605
+ if (visibleWidth(value) <= width || visibleWidth(value) !== value.length)
11606
+ return value;
11607
+ return `${value.slice(0, Math.max(1, width - 1))}…`;
11608
+ }
11609
+ function createTableWriter() {
11610
+ let widths = null;
11611
+ return (rows) => {
11612
+ const lines = rows.map((row) => COLUMNS.map((column) => oneLine2(column.value(row))));
11613
+ if (!widths) {
11614
+ widths = COLUMNS.map((column, index) => Math.min(MAX_CELL_WIDTH2, Math.max(visibleWidth(column.header), ...lines.map((line) => visibleWidth(line[index])))));
11615
+ const header = widths;
11616
+ console.log(source_default.dim(COLUMNS.map((column, index) => pad2(column.header.toUpperCase(), header[index])).join(" ").trimEnd()));
11617
+ }
11618
+ const locked = widths;
11619
+ for (const line of lines) {
11620
+ console.log(line.map((cell, index) => pad2(clamp2(cell, locked[index]), locked[index])).join(" ").trimEnd());
11621
+ }
11622
+ };
11623
+ }
11624
+ function createWriter(format) {
11625
+ if (format === "json") {
11626
+ return (rows) => {
11627
+ for (const row of rows)
11628
+ console.log(JSON.stringify(row));
11629
+ };
11630
+ }
11631
+ if (format === "yaml") {
11632
+ return (rows) => {
11633
+ for (const row of rows) {
11634
+ console.log(`---
11635
+ ${dump(row, { lineWidth: 0, noRefs: true }).trimEnd()}`);
11636
+ }
11637
+ };
11638
+ }
11639
+ if (format === "text") {
11640
+ return (rows) => {
11641
+ if (rows.length > 0)
11642
+ printList("text", rows, COLUMNS);
11643
+ };
11644
+ }
11645
+ return createTableWriter();
11646
+ }
11647
+ function followStatus() {
11648
+ let reported2 = false;
11649
+ return {
11650
+ note: (message) => {
11651
+ if (!process.stderr.isTTY)
11652
+ return;
11653
+ reported2 = true;
11654
+ process.stderr.write(`\r${source_default.dim(message)}${ERASE_LINE}`);
11655
+ },
11656
+ warn: (message) => {
11657
+ if (reported2) {
11658
+ reported2 = false;
11659
+ process.stderr.write(`\r${ERASE_LINE}`);
11660
+ }
11661
+ process.stderr.write(`warning: ${message}
11662
+ `);
11663
+ },
11664
+ clear: () => {
11665
+ if (!reported2)
11666
+ return;
11667
+ reported2 = false;
11668
+ process.stderr.write(`\r${ERASE_LINE}`);
11669
+ }
11670
+ };
11671
+ }
11672
+ function watchForInterrupt() {
11673
+ let stopped = false;
11674
+ const stop = () => {
11675
+ stopped = true;
11676
+ };
11677
+ process.on("SIGINT", stop);
11678
+ process.on("SIGTERM", stop);
11679
+ return {
11680
+ interrupted: () => stopped,
11681
+ dispose: () => {
11682
+ process.off("SIGINT", stop);
11683
+ process.off("SIGTERM", stop);
11684
+ }
11685
+ };
11686
+ }
11687
+ async function waitFor(ms, interrupted) {
11688
+ let remaining = ms;
11689
+ while (remaining > 0 && !interrupted()) {
11690
+ const step = Math.min(WAIT_SLICE_MS, remaining);
11691
+ await sleep(step);
11692
+ remaining -= step;
11693
+ }
11694
+ }
11695
+ function isUnprinted(state, row) {
11696
+ if (state.seen.has(row.runId))
11697
+ return false;
11698
+ return state.floor === null || row.startedAt >= state.floor;
11699
+ }
11700
+ function remember(state, rows) {
11701
+ for (const row of rows)
11702
+ state.seen.set(row.runId, row.startedAt);
11703
+ let excess = state.seen.size - MAX_REMEMBERED_RUNS;
11704
+ if (excess <= 0)
11705
+ return;
11706
+ for (const [runId, startedAt] of state.seen) {
11707
+ if (excess <= 0)
11708
+ break;
11709
+ if (state.floor === null || startedAt > state.floor)
11710
+ state.floor = startedAt;
11711
+ state.seen.delete(runId);
11712
+ excess -= 1;
11713
+ }
11714
+ }
11715
+ async function collectUnprinted(client, path, query, state, pageSize, maxPages) {
11716
+ const rows = [];
11717
+ let cursor = null;
11718
+ let truncated = false;
11719
+ for (let page = 0;page < maxPages; page += 1) {
11720
+ const response = await client.request(path, {
11721
+ query: { ...query, limit: pageSize, cursor }
11722
+ });
11723
+ const page_rows = response?.data ?? [];
11724
+ const unprinted = page_rows.filter((row) => isUnprinted(state, row));
11725
+ rows.push(...unprinted);
11726
+ cursor = response?.nextCursor ?? null;
11727
+ if (!cursor || page_rows.length === 0 || unprinted.length < page_rows.length)
11728
+ break;
11729
+ if (page === maxPages - 1)
11730
+ truncated = true;
11731
+ }
11732
+ return { rows, truncated };
11733
+ }
11734
+ function isTransient(error) {
11735
+ if (!(error instanceof SimApiError))
11736
+ return false;
11737
+ if (error.status === 0 || error.status >= 500)
11738
+ return true;
11739
+ return RETRYABLE_CLIENT_STATUSES.has(error.status);
11740
+ }
11741
+ function nonNegativeInteger(raw, flag) {
11742
+ const value = Number(raw);
11743
+ if (!Number.isSafeInteger(value) || value < 0) {
11744
+ throw new SimApiError(`${flag} must be a non-negative integer`, 0);
11745
+ }
11746
+ return value;
11747
+ }
11748
+ function intervalMs(raw) {
11749
+ const seconds = Number(raw);
11750
+ if (!Number.isFinite(seconds) || seconds < MIN_INTERVAL_SECONDS) {
11751
+ throw new SimApiError(`--interval must be at least ${MIN_INTERVAL_SECONDS} seconds`, 0);
11752
+ }
11753
+ return Math.round(seconds * 1000);
11754
+ }
11755
+ function inSeconds(ms) {
11756
+ return Math.round(ms / 100) / 10;
11757
+ }
11758
+ function attachLogsFollow(logs) {
11759
+ logs.command("follow").description("Watch runs as they arrive, printing each new run once").option("--workflow <id>", "Only follow runs of this workflow (repeatable)", collect, []).option("--folder <path>", "Only follow runs of workflows in this folder (repeatable)", collect, []).option("--trigger <type>", "Only follow runs with this trigger type (repeatable)", collect, []).addOption(new Option("--level <level>", "Only follow runs at this severity").choices([
11760
+ ...V2_OPERATIONS.listLogs.query.level.values
11761
+ ])).addOption(new Option("--details <level>", "Response detail level; full names each run’s workflow").choices([...V2_OPERATIONS.listLogs.query.details.values]).default("full")).option("-n, --lines <count>", "Recent runs to print before watching", String(DEFAULT_BACKLOG)).option("--interval <seconds>", "Seconds between polls", String(DEFAULT_INTERVAL_SECONDS)).addHelpText("after", `
11762
+ Each run prints once, when it is first seen, so its status is the status it had
11763
+ at that moment. With --output json every run is a JSON object on its own line
11764
+ (JSONL) rather than a member of an array, because a follow never ends and so can
11765
+ never close one; --output yaml emits a --- separated document stream. Progress
11766
+ and retries go to stderr, leaving stdout a clean stream of rows. Ctrl-C stops the
11767
+ follow.
11768
+
11769
+ Examples:
11770
+ $ sim logs follow --level error
11771
+ $ sim logs follow --workflow wf_123 -n 0
11772
+ $ sim --output json logs follow | jq -r '.runId'
11773
+ `).action(async (options, command) => {
11774
+ const lines = nonNegativeInteger(options.lines, "--lines");
11775
+ const delay = intervalMs(options.interval);
11776
+ const { client, profile } = clientFrom(command);
11777
+ const path = V2_OPERATIONS.listLogs.path;
11778
+ const query = {
11779
+ workspaceId: client.requireWorkspace(),
11780
+ workflowIds: options.workflow?.length ? options.workflow.join(",") : undefined,
11781
+ folderPaths: options.folder?.length ? options.folder.map(encodeFolderPath).join(",") : undefined,
11782
+ triggers: options.trigger?.length ? options.trigger.join(",") : undefined,
11783
+ level: options.level,
11784
+ details: options.details,
11785
+ order: "desc"
11786
+ };
11787
+ const write = createWriter(profile.output);
11788
+ const status = followStatus();
11789
+ const interrupt = watchForInterrupt();
11790
+ const state = { seen: new Map, floor: null };
11791
+ try {
11792
+ const seed = await collectUnprinted(client, path, query, state, Math.max(lines, 1), 1);
11793
+ remember(state, seed.rows);
11794
+ state.floor = seed.rows.at(-1)?.startedAt ?? null;
11795
+ if (seed.truncated && seed.rows.length < lines) {
11796
+ status.warn(`asked for ${lines} earlier runs but a page holds ${seed.rows.length}; following from there — see sim logs list for more`);
11797
+ }
11798
+ write(lines > 0 ? seed.rows.slice(0, lines).reverse() : []);
11799
+ let failures = 0;
11800
+ while (!interrupt.interrupted()) {
11801
+ await waitFor(failures === 0 ? delay : Math.min(delay * 2 ** failures, MAX_BACKOFF_MS), interrupt.interrupted);
11802
+ if (interrupt.interrupted())
11803
+ break;
11804
+ let fresh;
11805
+ try {
11806
+ fresh = await collectUnprinted(client, path, query, state, POLL_PAGE_SIZE, MAX_PAGES_PER_POLL);
11807
+ } catch (error) {
11808
+ if (!isTransient(error))
11809
+ throw error;
11810
+ failures += 1;
11811
+ const next = Math.min(delay * 2 ** failures, MAX_BACKOFF_MS);
11812
+ status.note(`poll failed (${error.message}); retrying in ${inSeconds(next)}s…`);
11813
+ continue;
11814
+ }
11815
+ failures = 0;
11816
+ status.clear();
11817
+ if (fresh.truncated) {
11818
+ status.warn(`more than ${MAX_PAGES_PER_POLL * POLL_PAGE_SIZE} runs arrived at once; older ones were skipped — see sim logs list`);
11819
+ }
11820
+ if (fresh.rows.length === 0)
11821
+ continue;
11822
+ remember(state, fresh.rows);
11823
+ write(fresh.rows.reverse());
11824
+ }
11825
+ } finally {
11826
+ status.clear();
11827
+ interrupt.dispose();
11828
+ }
11081
11829
  });
11082
11830
  }
11083
11831
 
@@ -11114,15 +11862,22 @@ function addFieldOption(command, operation, field, descriptor) {
11114
11862
  const placeholder = takesList ? "<value...>" : wantsJson ? "<json|@file>" : "<value>";
11115
11863
  const choices = flag.choices ?? descriptor.values;
11116
11864
  const describe = `${documented}${takesList ? " (space-separated, or @path / @- with one value per line)" : wantsJson ? " (JSON, or @path / @- to read a file or stdin)" : ""}${descriptor.required ? " (required)" : ""}`;
11865
+ const renamedFrom = flag.renamedFrom ?? [];
11117
11866
  const option = new Option(`${short}--${name} ${placeholder}`, describe);
11118
11867
  if (choices && !takesList)
11119
11868
  option.choices([...choices]);
11120
11869
  if (descriptor.default !== undefined && field !== "limit") {
11121
11870
  option.default(undefined, String(descriptor.default));
11122
11871
  }
11123
- if (descriptor.required)
11872
+ if (descriptor.required && renamedFrom.length === 0)
11124
11873
  option.makeOptionMandatory();
11125
11874
  command.addOption(option);
11875
+ for (const previous of renamedFrom) {
11876
+ const retired = new Option(`--${previous} ${placeholder}`).hideHelp();
11877
+ if (choices && !takesList)
11878
+ retired.choices([...choices]);
11879
+ command.addOption(retired);
11880
+ }
11126
11881
  }
11127
11882
  function addOperationOptions(command, operation, commandSpec, operationSpec) {
11128
11883
  for (const param of operationSpec.pathParams) {
@@ -11158,16 +11913,19 @@ function addOperationOptions(command, operation, commandSpec, operationSpec) {
11158
11913
  }
11159
11914
  }
11160
11915
  if (commandSpec.confirm) {
11161
- command.option("-y, --yes", "Skip the confirmation");
11916
+ command.option("-y, --yes", "Confirm this destructive operation (required)");
11162
11917
  }
11163
11918
  }
11164
11919
 
11165
11920
  // src/commands/protocol/resource-directory.ts
11166
- var COLUMNS = [
11921
+ var COLUMNS2 = [
11167
11922
  { header: "kind", value: (entry) => text(entry.kind) },
11168
11923
  { header: "name", value: (entry) => text(entry.name) },
11169
- { header: "ref", value: (entry) => text(entry.ref) },
11170
- { header: "folder", value: (entry) => text(entry.folderPath) },
11924
+ {
11925
+ header: "ref",
11926
+ value: (entry) => text(entry.kind === "folder" ? decodeFolderPath(entry.ref) : entry.ref)
11927
+ },
11928
+ { header: "folder", value: (entry) => text(decodeFolderPath(entry.folderPath)) },
11171
11929
  { header: "updated", value: (entry) => timestamp2(entry.updatedAt) }
11172
11930
  ];
11173
11931
  function operationPath(operation) {
@@ -11218,7 +11976,7 @@ function attachResourceDirectoryCommands(group, config) {
11218
11976
  throw new SimApiError("--limit must be a non-negative integer", 0);
11219
11977
  }
11220
11978
  const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit;
11221
- const folderPath = path ?? "/";
11979
+ const folderPath = encodeFolderPath(path ?? "/");
11222
11980
  const { client, profile } = clientFrom(command);
11223
11981
  const workspaceId = client.requireWorkspace();
11224
11982
  const [folders, resources] = await Promise.all([
@@ -11226,14 +11984,14 @@ function attachResourceDirectoryCommands(group, config) {
11226
11984
  listResources(client, config, workspaceId, folderPath, options.search, limit)
11227
11985
  ]);
11228
11986
  const entries = entriesFor(config, folders, resources);
11229
- printList(profile.output, entries.slice(0, limit), COLUMNS);
11987
+ printList(profile.output, entries.slice(0, limit), COLUMNS2);
11230
11988
  });
11231
11989
  group.command("mkdir").argument("<path>", "Folder path to create; the leading / is optional").allowExcessArguments(false).description(`Create a ${config.kind} directory at a path`).action(async (path, _options, command) => {
11232
11990
  const { client, profile } = clientFrom(command);
11233
11991
  const operation = V2_OPERATIONS[config.createFolder];
11234
11992
  const result = await client.request(operation.path, {
11235
11993
  method: operation.method,
11236
- body: { workspaceId: client.requireWorkspace(), path }
11994
+ body: { workspaceId: client.requireWorkspace(), path: encodeFolderPath(path) }
11237
11995
  });
11238
11996
  renderResult(config.createFolder, profile.output, result.data ?? result, {});
11239
11997
  });
@@ -11255,17 +12013,17 @@ function jsonFlag(raw, flagName, kind) {
11255
12013
  }
11256
12014
  async function watchImport(client, workspaceId, job) {
11257
12015
  let current = job;
11258
- let reported = -1;
12016
+ let reported2 = -1;
11259
12017
  while (!IMPORT_SETTLED.has(current.status)) {
11260
12018
  await sleep2(IMPORT_POLL_MS);
11261
12019
  const next = await client.request(`/api/v2/tables/imports/${encodeURIComponent(current.id)}`, { query: { workspaceId } });
11262
12020
  current = next.data;
11263
- if (process.stderr.isTTY && current.rowsProcessed !== reported) {
11264
- reported = current.rowsProcessed;
11265
- process.stderr.write(`\r${source_default.dim(`${current.status}… ${reported} rows`)}\x1B[K`);
12021
+ if (process.stderr.isTTY && current.rowsProcessed !== reported2) {
12022
+ reported2 = current.rowsProcessed;
12023
+ process.stderr.write(`\r${source_default.dim(`${current.status}… ${reported2} rows`)}\x1B[K`);
11266
12024
  }
11267
12025
  }
11268
- if (process.stderr.isTTY && reported >= 0)
12026
+ if (process.stderr.isTTY && reported2 >= 0)
11269
12027
  process.stderr.write("\r\x1B[K");
11270
12028
  return current;
11271
12029
  }
@@ -11287,7 +12045,7 @@ function validateTargetOptions(options) {
11287
12045
  return intoExisting;
11288
12046
  }
11289
12047
  function attachTableImport(tables) {
11290
- tables.command("import").argument("[path]", "Local CSV file to import; omit when using --file-id").description("Import a CSV, into a new table by default").option("--name <name>", "Identifier for the new table: letters, numbers, and underscores; defaults to the sanitized file name").option("--table-id <id>", "Import into this existing table instead of creating one").addOption(new Option("--mode <append|replace>", "How to write into --table-id (default: append)").choices(["append", "replace"])).option("--folder <path>", "Folder path for the new table").option("--file-id <id>", "Import a file already in the workspace instead of a local path").option("--mapping <json|@file>", "Column mapping (--table-id only)").option("--create-columns <json|@file>", "Columns to create (--table-id only)").option("--timezone <iana>", "Timezone for date parsing, e.g. America/New_York").option("--no-wait", "Return once the import is queued instead of watching it").action(async (path, options, command) => {
12048
+ tables.command("import").argument("[path]", "Local CSV file to import; omit when using --file-id").description("Import a CSV, into a new table by default").option("--name <name>", "Identifier for the new table: letters, numbers, and underscores; defaults to the sanitized file name").option("--table-id <id>", "Import into this existing table instead of creating one").addOption(new Option("--mode <append|replace>", "How to write into --table-id (default: append)").choices(["append", "replace"])).option("--folder <path>", "Folder path for the new table, as shown in the app").option("--file-id <id>", "Import a file already in the workspace instead of a local path").option("--mapping <json|@file>", "Column mapping (--table-id only)").option("--create-columns <json|@file>", "Columns to create (--table-id only)").option("--timezone <iana>", "Timezone for date parsing, e.g. America/New_York").option("--no-wait", "Return once the import is queued instead of watching it").action(async (path, options, command) => {
11291
12049
  const { client, profile } = clientFrom(command);
11292
12050
  const workspaceId = client.requireWorkspace();
11293
12051
  if (Boolean(path) === Boolean(options.fileId)) {
@@ -11312,7 +12070,7 @@ function attachTableImport(tables) {
11312
12070
  target = {
11313
12071
  type: "new",
11314
12072
  name,
11315
- ...options.folder !== undefined ? { folderPath: options.folder } : {}
12073
+ ...options.folder !== undefined ? { folderPath: encodeFolderPath(options.folder) } : {}
11316
12074
  };
11317
12075
  }
11318
12076
  const started = await client.request(V2_OPERATIONS.createTableImport.path, {
@@ -11360,6 +12118,427 @@ function attachTableImport(tables) {
11360
12118
  });
11361
12119
  }
11362
12120
 
12121
+ // src/runtime/renamed.ts
12122
+ var warned = new Set;
12123
+ function warn(kind, from, to) {
12124
+ const key = `${kind}:${from}`;
12125
+ if (warned.has(key))
12126
+ return;
12127
+ warned.add(key);
12128
+ process.stderr.write(`warning: ${kind} "${from}" has been renamed to "${to}". The old name still works.
12129
+ `);
12130
+ }
12131
+ function warnRenamedCommand(from, to) {
12132
+ warn("command", `sim ${from}`, `sim ${to}`);
12133
+ }
12134
+ function warnRenamedFlag(from, to) {
12135
+ warn("flag", `--${from}`, `--${to}`);
12136
+ }
12137
+
12138
+ // src/runtime/execute.ts
12139
+ function cursorSlot(operationSpec) {
12140
+ if (operationSpec.query && "cursor" in operationSpec.query)
12141
+ return "query";
12142
+ if (operationSpec.body && "cursor" in operationSpec.body)
12143
+ return "body";
12144
+ return null;
12145
+ }
12146
+ function foldRenamedFlags(operation, commandSpec, flags) {
12147
+ for (const [field, flag] of Object.entries(commandSpec.flags ?? {})) {
12148
+ if (!flag.renamedFrom?.length)
12149
+ continue;
12150
+ const current = flagNameFor(operation, field);
12151
+ for (const previous of flag.renamedFrom) {
12152
+ const supplied = flags[camel(previous)];
12153
+ if (supplied === undefined)
12154
+ continue;
12155
+ if (flags[camel(current)] !== undefined) {
12156
+ throw new SimApiError(`--${previous} is the former name of --${current}; pass one, not both`, 0);
12157
+ }
12158
+ warnRenamedFlag(previous, current);
12159
+ flags[camel(current)] = supplied;
12160
+ }
12161
+ }
12162
+ }
12163
+ async function executeOperation(operation, commandSpec, operationSpec, invocation) {
12164
+ const host = invocation[invocation.length - 1];
12165
+ const inheritedFlags = host.optsWithGlobals();
12166
+ const flags = {
12167
+ ...inheritedFlags.workspace === undefined ? {} : { workspace: inheritedFlags.workspace },
12168
+ ...inheritedFlags.allWorkspaces === undefined ? {} : { allWorkspaces: inheritedFlags.allWorkspaces },
12169
+ ...invocation[invocation.length - 2]
12170
+ };
12171
+ const pathPositionalCount = operationSpec.pathParams.filter((param) => !commandSpec.pathFlags?.[param] && !isProfileWorkspacePath(commandSpec, param)).length;
12172
+ const positional = invocation.slice(0, pathPositionalCount);
12173
+ const requestFlags = { ...flags };
12174
+ for (const [index, field] of (commandSpec.positionals ?? []).entries()) {
12175
+ requestFlags[camel(flagNameFor(operation, field))] = invocation[pathPositionalCount + index];
12176
+ }
12177
+ foldRenamedFlags(operation, commandSpec, requestFlags);
12178
+ if (commandSpec.confirm && !requestFlags.yes) {
12179
+ throw new SimApiError(`${commandSpec.confirm} Re-run with --yes to confirm.`, 0);
12180
+ }
12181
+ if (commandSpec.allWorkspaces && requestFlags.allWorkspaces && requestFlags.workspace) {
12182
+ throw new SimApiError("--all-workspaces cannot be combined with --workspace", 0);
12183
+ }
12184
+ const { client, profile } = clientFrom(host);
12185
+ const hasWorkspaceField = Boolean(operationSpec.query && PROFILE_INJECTED_FIELD in operationSpec.query || operationSpec.body && PROFILE_INJECTED_FIELD in operationSpec.body);
12186
+ const omitsWorkspace = commandSpec.allWorkspaces && requestFlags.allWorkspaces === true;
12187
+ const request = buildRequest(operation, positional, requestFlags, hasWorkspaceField && !omitsWorkspace ? client.requireWorkspace() : profile.workspaceId);
12188
+ const paging = cursorSlot(operationSpec);
12189
+ if (paging) {
12190
+ const rawLimit = Number.parseInt(String(requestFlags.limit ?? DEFAULT_LIMIT), 10);
12191
+ if (Number.isNaN(rawLimit) || rawLimit < 0) {
12192
+ throw new SimApiError("--limit must be a non-negative number", 0);
12193
+ }
12194
+ const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit;
12195
+ const pageSize = Math.min(Number.isFinite(limit) ? limit : DEFAULT_LIMIT, DEFAULT_LIMIT);
12196
+ const pageLimit = "limit" in (operationSpec[paging] ?? {}) ? { limit: pageSize } : {};
12197
+ const rows = [];
12198
+ const progress = pageProgress();
12199
+ let cursor = null;
12200
+ try {
12201
+ do {
12202
+ const page = await client.request(request.path, {
12203
+ method: operationSpec.method,
12204
+ query: paging === "query" ? { ...request.query, ...pageLimit, cursor } : request.query,
12205
+ body: paging === "body" ? { ...request.body ?? {}, ...pageLimit, ...cursor ? { cursor } : {} } : request.body
12206
+ });
12207
+ rows.push(...page.data);
12208
+ cursor = page.nextCursor;
12209
+ if (cursor && rows.length < limit)
12210
+ progress.advance(rows.length);
12211
+ } while (cursor && rows.length < limit);
12212
+ } finally {
12213
+ progress.finish();
12214
+ }
12215
+ renderPage(profile.output, Number.isFinite(limit) ? rows.slice(0, limit) : rows, commandSpec);
12216
+ return;
12217
+ }
12218
+ const result = await client.request(request.path, {
12219
+ method: operationSpec.method,
12220
+ query: request.query,
12221
+ body: request.body
12222
+ });
12223
+ renderResult(operation, profile.output, result?.data ?? result, commandSpec, {
12224
+ expandedTrace: requestFlags.trace === true
12225
+ });
12226
+ }
12227
+
12228
+ // src/commands/protocol/workflow-run-follow.ts
12229
+ var AGENT_STREAM_PROTOCOL_HEADER = "x-sim-stream-protocol";
12230
+ var AGENT_STREAM_PROTOCOL_V1 = "agent-events-v1";
12231
+ var DONE_SENTINEL = "[DONE]";
12232
+ function isRecord(value) {
12233
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12234
+ }
12235
+ function stringField(frame, key) {
12236
+ const value = frame[key];
12237
+ return typeof value === "string" ? value : null;
12238
+ }
12239
+ async function* sseData(body) {
12240
+ const reader = body.getReader();
12241
+ const decoder = new TextDecoder;
12242
+ let buffer = "";
12243
+ try {
12244
+ while (true) {
12245
+ const { done, value } = await reader.read();
12246
+ buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
12247
+ const lines = buffer.split(`
12248
+ `);
12249
+ buffer = done ? "" : lines.pop() ?? "";
12250
+ for (const rawLine of lines) {
12251
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
12252
+ if (!line.startsWith("data:"))
12253
+ continue;
12254
+ const payload = line.slice(5).startsWith(" ") ? line.slice(6) : line.slice(5);
12255
+ if (payload.length > 0)
12256
+ yield payload;
12257
+ }
12258
+ if (done)
12259
+ return;
12260
+ }
12261
+ } finally {
12262
+ reader.releaseLock();
12263
+ }
12264
+ }
12265
+
12266
+ class Commentary {
12267
+ sink;
12268
+ atLineStart = true;
12269
+ constructor(sink) {
12270
+ this.sink = sink;
12271
+ }
12272
+ inline(text2) {
12273
+ if (text2.length === 0)
12274
+ return;
12275
+ this.sink.write(text2);
12276
+ this.atLineStart = text2.endsWith(`
12277
+ `);
12278
+ }
12279
+ line(text2) {
12280
+ this.sink.write(`${this.atLineStart ? "" : `
12281
+ `}${text2}
12282
+ `);
12283
+ this.atLineStart = true;
12284
+ }
12285
+ endLine() {
12286
+ if (this.atLineStart)
12287
+ return;
12288
+ this.sink.write(`
12289
+ `);
12290
+ this.atLineStart = true;
12291
+ }
12292
+ }
12293
+ function toolNotice(frame) {
12294
+ const name = safeOneLine(stringField(frame, "name") ?? "tool");
12295
+ if (frame.phase === "start")
12296
+ return source_default.dim(`→ ${name}`);
12297
+ const status = stringField(frame, "status");
12298
+ if (status && status !== "success")
12299
+ return source_default.yellow(`✗ ${name} (${safeOneLine(status)})`);
12300
+ return source_default.dim(`✓ ${name}`);
12301
+ }
12302
+ async function renderRunStream(body, options) {
12303
+ const commentary = new Commentary(options.stderr);
12304
+ let final = null;
12305
+ for await (const payload of sseData(body)) {
12306
+ let frame;
12307
+ try {
12308
+ frame = JSON.parse(payload);
12309
+ } catch {
12310
+ continue;
12311
+ }
12312
+ if (frame === DONE_SENTINEL)
12313
+ break;
12314
+ if (!isRecord(frame))
12315
+ continue;
12316
+ if (frame.event === undefined && typeof frame.chunk === "string") {
12317
+ commentary.inline(sanitize(frame.chunk));
12318
+ continue;
12319
+ }
12320
+ switch (frame.event) {
12321
+ case "chunk_reset":
12322
+ commentary.line(source_default.dim("… retracted; that turn resolved to tool calls"));
12323
+ break;
12324
+ case "thinking":
12325
+ if (options.includeThinking && typeof frame.data === "string") {
12326
+ commentary.inline(source_default.dim(sanitize(frame.data)));
12327
+ }
12328
+ break;
12329
+ case "tool":
12330
+ if (options.includeToolCalls)
12331
+ commentary.line(toolNotice(frame));
12332
+ break;
12333
+ case "stream_error":
12334
+ commentary.line(source_default.yellow(`warning: ${safeOneLine(stringField(frame, "error") ?? "stream read failed")}`));
12335
+ break;
12336
+ case "error":
12337
+ commentary.endLine();
12338
+ throw new SimApiError(safeOneLine(stringField(frame, "error") ?? "The workflow run failed."), 0);
12339
+ case "final":
12340
+ if (isRecord(frame.data))
12341
+ final = frame.data;
12342
+ break;
12343
+ default:
12344
+ break;
12345
+ }
12346
+ }
12347
+ commentary.endLine();
12348
+ if (!final) {
12349
+ throw new SimApiError("The run stream ended before the workflow reported a result. The run may still be in progress — check: sim workflows runs list", 0);
12350
+ }
12351
+ return final;
12352
+ }
12353
+ async function followRun(workflowId, command) {
12354
+ const flags = command.optsWithGlobals();
12355
+ if (flags.async === true) {
12356
+ throw new SimApiError("--follow streams a run as it happens and --async returns before it starts; pass one, not both", 0);
12357
+ }
12358
+ const includeThinking = flags.includeThinking === true;
12359
+ const includeToolCalls = flags.includeToolCalls === true;
12360
+ const negotiates = includeThinking || includeToolCalls;
12361
+ const { client, profile } = clientFrom(command);
12362
+ const operation = V2_OPERATIONS.executeWorkflow;
12363
+ const request = buildRequest("executeWorkflow", [workflowId], flags, profile.workspaceId);
12364
+ const response = await client.requestRaw(request.path, {
12365
+ method: "POST",
12366
+ query: request.query,
12367
+ body: {
12368
+ ...request.body ?? {},
12369
+ stream: true,
12370
+ ...includeThinking ? { includeThinking: true } : {},
12371
+ ...includeToolCalls ? { includeToolCalls: true } : {}
12372
+ },
12373
+ headers: {
12374
+ accept: "text/event-stream",
12375
+ ...negotiates ? { [AGENT_STREAM_PROTOCOL_HEADER]: AGENT_STREAM_PROTOCOL_V1 } : {}
12376
+ }
12377
+ });
12378
+ const contentType = response.headers.get("content-type") ?? "";
12379
+ if (!contentType.toLowerCase().includes("text/event-stream")) {
12380
+ await response.body?.cancel();
12381
+ throw new SimApiError(`${operation.path} answered ${contentType || "an unknown content type"} instead of an event stream. This deployment may predate streaming runs — re-run without --follow.`, response.status);
12382
+ }
12383
+ if (!response.body) {
12384
+ throw new SimApiError("The run stream had no body.", response.status);
12385
+ }
12386
+ const final = await renderRunStream(response.body, {
12387
+ includeThinking,
12388
+ includeToolCalls,
12389
+ stderr: process.stderr
12390
+ });
12391
+ renderResult("executeWorkflow", profile.output, final, CLI_CONTRACT.executeWorkflow ?? {});
12392
+ if (final.success === false) {
12393
+ throw new SimApiError(safeOneLine(typeof final.error === "string" ? final.error : "The workflow run failed."), 0);
12394
+ }
12395
+ }
12396
+ function followOrDelegate(previous) {
12397
+ return async (workflowId, _options, command) => {
12398
+ const flags = command.optsWithGlobals();
12399
+ if (flags.follow !== true) {
12400
+ if (flags.includeThinking === true || flags.includeToolCalls === true) {
12401
+ throw new SimApiError("--include-thinking and --include-tool-calls describe a stream; add --follow", 0);
12402
+ }
12403
+ if (previous) {
12404
+ await previous(command.processedArgs);
12405
+ return;
12406
+ }
12407
+ await executeOperation("executeWorkflow", CLI_CONTRACT.executeWorkflow ?? {}, V2_OPERATIONS.executeWorkflow, [workflowId, command.opts(), command]);
12408
+ return;
12409
+ }
12410
+ await followRun(workflowId, command);
12411
+ };
12412
+ }
12413
+ function attachWorkflowRunFollow(workflows) {
12414
+ const run = workflows.commands.find((command) => command.name() === "run");
12415
+ if (!run) {
12416
+ throw new Error("workflows run must be registered before --follow can be attached to it");
12417
+ }
12418
+ const held = run._actionHandler;
12419
+ const previous = typeof held === "function" ? held : null;
12420
+ run.option("--follow", "Stream the run as it happens; progress on stderr, result on stdout. The stream reports only success and output, so the result omits the run id and timings a non-streaming run returns").option("--include-thinking", "Show model reasoning while following (requires --follow)").option("--include-tool-calls", "Show tool calls while following (requires --follow)").action(followOrDelegate(previous));
12421
+ }
12422
+
12423
+ // src/commands/protocol/workflow-run-wait.ts
12424
+ var TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
12425
+ var WAIT_EXIT_CODES = {
12426
+ completed: 0,
12427
+ failed: 1,
12428
+ cancelled: 2,
12429
+ paused: 3,
12430
+ timeout: 4
12431
+ };
12432
+ var FIRST_POLL_DELAY_MS = 2000;
12433
+ var MAX_POLL_DELAY_MS = 15000;
12434
+ var POLL_BACKOFF_FACTOR = 2;
12435
+ var DEFAULT_WAIT_TIMEOUT_SECONDS = 3600;
12436
+ var WAIT_TIMEOUT_FLAG = "--wait-timeout <seconds>";
12437
+ function isRecord2(value) {
12438
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12439
+ }
12440
+ function optionalString(value) {
12441
+ return typeof value === "string" && value !== "" ? value : null;
12442
+ }
12443
+ function readRun(raw) {
12444
+ const run = isRecord2(raw) && isRecord2(raw.data) ? raw.data : raw;
12445
+ if (!isRecord2(run) || typeof run.status !== "string") {
12446
+ throw new SimApiError("Run status response carried no status.", 0);
12447
+ }
12448
+ const paused = isRecord2(run.paused) ? run.paused : null;
12449
+ return {
12450
+ status: run.status,
12451
+ pauseKind: paused ? optionalString(paused.pauseKind) : null,
12452
+ resumeAt: paused ? optionalString(paused.resumeAt) : null,
12453
+ contextId: paused ? optionalString(paused.contextId) : null
12454
+ };
12455
+ }
12456
+ function classify(snapshot) {
12457
+ if (snapshot.status === "paused")
12458
+ return snapshot.pauseKind === "time" ? null : "paused";
12459
+ if (!TERMINAL_STATUSES.has(snapshot.status))
12460
+ return null;
12461
+ return snapshot.status === "completed" ? "completed" : snapshot.status === "cancelled" ? "cancelled" : "failed";
12462
+ }
12463
+ function waitProgress() {
12464
+ let reported2 = false;
12465
+ return {
12466
+ advance: (status, elapsedMs) => {
12467
+ if (!process.stderr.isTTY)
12468
+ return;
12469
+ reported2 = true;
12470
+ process.stderr.write(`\r${source_default.dim(`${status} — waiting ${Math.round(elapsedMs / 1000)}s…`)}\x1B[K`);
12471
+ },
12472
+ finish: () => {
12473
+ if (!reported2)
12474
+ return;
12475
+ reported2 = false;
12476
+ process.stderr.write("\r\x1B[K");
12477
+ }
12478
+ };
12479
+ }
12480
+ function parseWaitTimeout(raw) {
12481
+ const seconds = Number(raw);
12482
+ if (!Number.isFinite(seconds) || seconds < 0) {
12483
+ throw new SimApiError(`Invalid ${WAIT_TIMEOUT_FLAG} "${raw}". Use a non-negative number of seconds, or 0 to wait indefinitely.`, 0);
12484
+ }
12485
+ return seconds;
12486
+ }
12487
+ function explain(outcome, runId, workflowId, snapshot) {
12488
+ if (outcome === "completed")
12489
+ return null;
12490
+ if (outcome === "failed")
12491
+ return `Run ${runId} failed.`;
12492
+ if (outcome === "cancelled")
12493
+ return `Run ${runId} was cancelled.`;
12494
+ const context = snapshot.contextId ? ` --context ${snapshot.contextId}` : "";
12495
+ return `Run ${runId} is paused waiting for input. Resume it: sim workflows runs resume ${runId} --workflow ${workflowId}${context}`;
12496
+ }
12497
+ function runSpec() {
12498
+ return CLI_CONTRACT.getWorkflowRun ?? {};
12499
+ }
12500
+ function attachWorkflowRunWait(runs) {
12501
+ runs.command("wait").argument("<runId>", V2_OPERATIONS.getWorkflowRun.pathParamDocs?.runId).description("Wait for a run to reach a terminal state, then show it").addOption(new Option("--workflow <workflowId>", "Workflow ID (required)").makeOptionMandatory()).addOption(new Option(WAIT_TIMEOUT_FLAG, `Give up after this many seconds, or 0 to wait indefinitely (default: ${DEFAULT_WAIT_TIMEOUT_SECONDS}). Bounds the whole wait; SIM_TIMEOUT_SECONDS bounds one request`)).action(async (runId, options, command) => {
12502
+ const timeoutSeconds = options.waitTimeout === undefined ? DEFAULT_WAIT_TIMEOUT_SECONDS : parseWaitTimeout(options.waitTimeout);
12503
+ const { client, profile } = clientFrom(command);
12504
+ const operation = V2_OPERATIONS.getWorkflowRun;
12505
+ const path = resolvePath(operation.path, { id: options.workflow, runId });
12506
+ const startedAt = Date.now();
12507
+ const deadline = timeoutSeconds === 0 ? Number.POSITIVE_INFINITY : startedAt + timeoutSeconds * 1000;
12508
+ const progress = waitProgress();
12509
+ let delayMs = FIRST_POLL_DELAY_MS;
12510
+ try {
12511
+ while (true) {
12512
+ const raw = await client.request(path, { method: operation.method });
12513
+ const snapshot = readRun(raw);
12514
+ const outcome = classify(snapshot);
12515
+ if (outcome) {
12516
+ progress.finish();
12517
+ renderResult("getWorkflowRun", profile.output, raw, runSpec());
12518
+ const message = explain(outcome, runId, options.workflow, snapshot);
12519
+ if (message)
12520
+ console.error(source_default.red(message));
12521
+ process.exitCode = WAIT_EXIT_CODES[outcome];
12522
+ return;
12523
+ }
12524
+ const remainingMs = deadline - Date.now();
12525
+ if (remainingMs <= 0) {
12526
+ progress.finish();
12527
+ renderResult("getWorkflowRun", profile.output, raw, runSpec());
12528
+ console.error(source_default.red(`Timed out after ${timeoutSeconds}s waiting for run ${runId} (status: ${snapshot.status}${snapshot.resumeAt ? `, resuming at ${snapshot.resumeAt}` : ""}). Raise ${WAIT_TIMEOUT_FLAG}, or set it to 0 to wait indefinitely.`));
12529
+ process.exitCode = WAIT_EXIT_CODES.timeout;
12530
+ return;
12531
+ }
12532
+ progress.advance(snapshot.status, Date.now() - startedAt);
12533
+ await sleep(Math.min(delayMs, remainingMs));
12534
+ delayMs = Math.min(delayMs * POLL_BACKOFF_FACTOR, MAX_POLL_DELAY_MS);
12535
+ }
12536
+ } finally {
12537
+ progress.finish();
12538
+ }
12539
+ });
12540
+ }
12541
+
11363
12542
  // src/commands/protocol/index.ts
11364
12543
  function group(program2, name) {
11365
12544
  const existing = program2.commands.find((command) => command.name() === name);
@@ -11395,12 +12574,16 @@ function attachProtocolCommands(program2) {
11395
12574
  folders: "listTableFolders",
11396
12575
  createFolder: "createTableFolder"
11397
12576
  });
11398
- attachResourceDirectoryCommands(group(program2, "workflows"), {
12577
+ const workflows = group(program2, "workflows");
12578
+ attachResourceDirectoryCommands(workflows, {
11399
12579
  kind: "workflow",
11400
12580
  resources: "listWorkflows",
11401
12581
  folders: "listWorkflowFolders",
11402
12582
  createFolder: "createWorkflowFolder"
11403
12583
  });
12584
+ attachWorkflowRunFollow(workflows);
12585
+ attachWorkflowRunWait(group(workflows, "runs"));
12586
+ attachLogsFollow(group(program2, "logs"));
11404
12587
  }
11405
12588
 
11406
12589
  // src/terminal/secret-input.ts
@@ -11481,7 +12664,8 @@ var SECRET_RESULT = {
11481
12664
  { header: "name" },
11482
12665
  { header: "scope" },
11483
12666
  { header: "role" },
11484
- { header: "updated", path: "updatedAt", format: "timestamp" }
12667
+ { header: "updated", path: "updatedAt", format: "timestamp" },
12668
+ { header: "description" }
11485
12669
  ]
11486
12670
  };
11487
12671
  function validateSecretValue(value) {
@@ -11492,7 +12676,16 @@ function validateSecretValue(value) {
11492
12676
  }
11493
12677
  return value;
11494
12678
  }
12679
+ function validateDescriptionScope(description, scope) {
12680
+ if (description === undefined)
12681
+ return;
12682
+ if (scope === "personal") {
12683
+ throw new SimApiError("--description is only supported for a workspace secret.", 0);
12684
+ }
12685
+ return description;
12686
+ }
11495
12687
  async function setSecret(name, options, command) {
12688
+ const description = validateDescriptionScope(options.description, options.scope);
11496
12689
  const value = validateSecretValue(options.value ?? await promptSecret());
11497
12690
  const { client, profile } = clientFrom(command);
11498
12691
  const operation = V2_OPERATIONS.setSecret;
@@ -11501,7 +12694,8 @@ async function setSecret(name, options, command) {
11501
12694
  body: {
11502
12695
  workspaceId: client.requireWorkspace(),
11503
12696
  scope: options.scope,
11504
- value
12697
+ value,
12698
+ description
11505
12699
  }
11506
12700
  });
11507
12701
  renderResult("setSecret", profile.output, response.data, SECRET_RESULT);
@@ -11510,72 +12704,7 @@ function attachSecretCommands(program2) {
11510
12704
  const secrets = program2.commands.find((command) => command.name() === "secrets");
11511
12705
  if (!secrets)
11512
12706
  throw new Error("The generated secrets command group is missing");
11513
- secrets.command("set").argument("<name>", "Secret name, as referenced in workflows").description("Create or replace a named secret").addOption(new Option("--scope <scope>", "Secret ownership scope").choices([...SECRET_SCOPES]).makeOptionMandatory()).option("--value <value>", "Secret value; visible to shell history when supplied directly").action((name, options, command) => setSecret(name, options, command));
11514
- }
11515
-
11516
- // src/runtime/execute.ts
11517
- function cursorSlot(operationSpec) {
11518
- if (operationSpec.query && "cursor" in operationSpec.query)
11519
- return "query";
11520
- if (operationSpec.body && "cursor" in operationSpec.body)
11521
- return "body";
11522
- return null;
11523
- }
11524
- async function executeOperation(operation, commandSpec, operationSpec, invocation) {
11525
- const host = invocation[invocation.length - 1];
11526
- const inheritedFlags = host.optsWithGlobals();
11527
- const flags = {
11528
- ...inheritedFlags.workspace === undefined ? {} : { workspace: inheritedFlags.workspace },
11529
- ...inheritedFlags.allWorkspaces === undefined ? {} : { allWorkspaces: inheritedFlags.allWorkspaces },
11530
- ...invocation[invocation.length - 2]
11531
- };
11532
- const pathPositionalCount = operationSpec.pathParams.filter((param) => !commandSpec.pathFlags?.[param] && !isProfileWorkspacePath(commandSpec, param)).length;
11533
- const positional = invocation.slice(0, pathPositionalCount);
11534
- const requestFlags = { ...flags };
11535
- for (const [index, field] of (commandSpec.positionals ?? []).entries()) {
11536
- requestFlags[camel(flagNameFor(operation, field))] = invocation[pathPositionalCount + index];
11537
- }
11538
- if (commandSpec.confirm && !requestFlags.yes) {
11539
- throw new SimApiError(`${commandSpec.confirm} Re-run with --yes to confirm.`, 0);
11540
- }
11541
- if (commandSpec.allWorkspaces && requestFlags.allWorkspaces && requestFlags.workspace) {
11542
- throw new SimApiError("--all-workspaces cannot be combined with --workspace", 0);
11543
- }
11544
- const { client, profile } = clientFrom(host);
11545
- const hasWorkspaceField = Boolean(operationSpec.query && PROFILE_INJECTED_FIELD in operationSpec.query || operationSpec.body && PROFILE_INJECTED_FIELD in operationSpec.body);
11546
- const omitsWorkspace = commandSpec.allWorkspaces && requestFlags.allWorkspaces === true;
11547
- const request = buildRequest(operation, positional, requestFlags, hasWorkspaceField && !omitsWorkspace ? client.requireWorkspace() : profile.workspaceId);
11548
- const paging = cursorSlot(operationSpec);
11549
- if (paging) {
11550
- const rawLimit = Number.parseInt(String(requestFlags.limit ?? DEFAULT_LIMIT), 10);
11551
- if (Number.isNaN(rawLimit) || rawLimit < 0) {
11552
- throw new SimApiError("--limit must be a non-negative number", 0);
11553
- }
11554
- const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit;
11555
- const pageSize = Math.min(Number.isFinite(limit) ? limit : DEFAULT_LIMIT, DEFAULT_LIMIT);
11556
- const pageLimit = "limit" in (operationSpec[paging] ?? {}) ? { limit: pageSize } : {};
11557
- const rows = [];
11558
- let cursor = null;
11559
- do {
11560
- const page = await client.request(request.path, {
11561
- method: operationSpec.method,
11562
- query: paging === "query" ? { ...request.query, ...pageLimit, cursor } : request.query,
11563
- body: paging === "body" ? { ...request.body ?? {}, ...pageLimit, ...cursor ? { cursor } : {} } : request.body
11564
- });
11565
- rows.push(...page.data);
11566
- cursor = page.nextCursor;
11567
- } while (cursor && rows.length < limit);
11568
- renderPage(profile.output, Number.isFinite(limit) ? rows.slice(0, limit) : rows, commandSpec);
11569
- return;
11570
- }
11571
- const result = await client.request(request.path, {
11572
- method: operationSpec.method,
11573
- query: request.query,
11574
- body: request.body
11575
- });
11576
- renderResult(operation, profile.output, result?.data ?? result, commandSpec, {
11577
- expandedTrace: requestFlags.trace === true
11578
- });
12707
+ secrets.command("set").argument("<name>", "Secret name, as referenced in workflows").description("Create or replace a named secret").addOption(new Option("--scope <scope>", "Secret ownership scope").choices([...SECRET_SCOPES]).makeOptionMandatory()).option("--value <value>", "Secret value; visible to shell history when supplied directly").option("--description <description>", "What the secret is for, shown to teammates; workspace scope only. Omit to leave an existing description unchanged").action((name, options, command) => setSecret(name, options, command));
11579
12708
  }
11580
12709
 
11581
12710
  // src/runtime/build.ts
@@ -11693,6 +12822,19 @@ function configureOperation(command, operation, spec) {
11693
12822
  function buildLeaf(operation, spec, leafName) {
11694
12823
  return addMissingArgumentExample(configureOperation(new Command(leafName), operation, spec));
11695
12824
  }
12825
+ function addRenamedCommand(groups, operation, spec, from, to) {
12826
+ const segments = from.split(" ");
12827
+ const [groupName, ...rest] = segments;
12828
+ if (rest.length === 0)
12829
+ throw new Error(`${operation}.renamedFrom "${from}" must include a verb`);
12830
+ let parent = groupFor(groups, groupName);
12831
+ for (const segment of rest.slice(0, -1)) {
12832
+ parent = nestedGroup(parent, segment, { hidden: true });
12833
+ }
12834
+ const leaf = buildLeaf(operation, spec, rest[rest.length - 1]);
12835
+ leaf.hook("preAction", () => warnRenamedCommand(from, to));
12836
+ parent.addCommand(leaf, { hidden: true });
12837
+ }
11696
12838
  function groupFor(groups, name) {
11697
12839
  const existing = groups.get(name);
11698
12840
  if (existing)
@@ -11708,12 +12850,12 @@ function resourceLabel(name) {
11708
12850
  const label = name.endsWith("s") ? name.slice(0, -1) : name;
11709
12851
  return label.replaceAll("-", " ");
11710
12852
  }
11711
- function nestedGroup(parent, name) {
12853
+ function nestedGroup(parent, name, options = {}) {
11712
12854
  const existing = parent.commands.find((candidate) => candidate.name() === name);
11713
12855
  if (existing)
11714
12856
  return existing;
11715
12857
  const created = new Command(name).description(`Manage ${resourceLabel(parent.name())} ${name.replaceAll("-", " ")}`);
11716
- parent.addCommand(created);
12858
+ parent.addCommand(created, { hidden: options.hidden });
11717
12859
  return created;
11718
12860
  }
11719
12861
  function addLeafCommand(groups, operation, spec, segments) {
@@ -11742,6 +12884,7 @@ function variantCommandSpec(spec, variant) {
11742
12884
  }
11743
12885
  function buildGeneratedCommands() {
11744
12886
  const groups = new Map;
12887
+ const renamed = [];
11745
12888
  for (const operation of Object.keys(V2_OPERATIONS)) {
11746
12889
  const spec = CLI_CONTRACT[operation] ?? {};
11747
12890
  const operationSpec = V2_OPERATIONS[operation];
@@ -11764,6 +12907,12 @@ function buildGeneratedCommands() {
11764
12907
  for (const variant of spec.variants ?? []) {
11765
12908
  addLeafCommand(groups, operation, variantCommandSpec(spec, variant), variant.command.split(" "));
11766
12909
  }
12910
+ for (const from of spec.renamedFrom ?? []) {
12911
+ renamed.push({ operation, spec, from, to: segments.join(" ") });
12912
+ }
12913
+ }
12914
+ for (const { operation, spec, from, to } of renamed) {
12915
+ addRenamedCommand(groups, operation, spec, from, to);
11767
12916
  }
11768
12917
  return [...groups.values()].sort((a, b) => a.name().localeCompare(b.name()));
11769
12918
  }
@@ -11772,7 +12921,8 @@ function buildGeneratedCommands() {
11772
12921
  var PROGRAM_DESCRIPTION = "Talk to the Sim API from your terminal";
11773
12922
  var HELP_EPILOGUE = `
11774
12923
  Profiles work like the AWS CLI: settings live in ~/.sim/config, keys in
11775
- ~/.sim/credentials (0600). Select one with -P, --profile, or SIM_PROFILE.
12924
+ ~/.sim/credentials (0600), or under SIM_CONFIG_DIR when it is set. Select one
12925
+ with -P, --profile, or SIM_PROFILE.
11776
12926
 
11777
12927
  Examples:
11778
12928
  $ sim login Authorize the default profile
@@ -11786,18 +12936,11 @@ Examples:
11786
12936
  $ sim workflows import --workflow @wf.json
11787
12937
  $ sim whoami --profile dev
11788
12938
  `;
11789
- function readPackageVersion() {
11790
- const metadata = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
11791
- if (typeof metadata !== "object" || metadata === null || !("version" in metadata) || typeof metadata.version !== "string") {
11792
- throw new Error("CLI package metadata is missing a valid version");
11793
- }
11794
- return metadata.version;
11795
- }
11796
12939
  function buildProgram(options = {}) {
11797
12940
  const program2 = new Command;
11798
12941
  program2.name("sim").description(PROGRAM_DESCRIPTION);
11799
12942
  if (options.version !== false)
11800
- program2.version(readPackageVersion());
12943
+ program2.version(CLI_VERSION);
11801
12944
  program2.option("-P, --profile <name>", "Profile to use (env: SIM_PROFILE)").option("--endpoint <url>", "Sim deployment to talk to (env: SIM_ENDPOINT)").option("-w, --workspace <id>", "Workspace to target (env: SIM_WORKSPACE)").addOption(new Option("--output <format>", "Output format for this command").choices([...OUTPUT_FORMATS]));
11802
12945
  program2.addCommand(loginCommand());
11803
12946
  program2.addCommand(logoutCommand());
@@ -11823,6 +12966,10 @@ async function main() {
11823
12966
  console.error(source_default.red(`Error: ${sanitize(error.message)}`));
11824
12967
  process.exit(1);
11825
12968
  }
12969
+ if (isRequestTimeout(error)) {
12970
+ console.error(source_default.red(`Error: the request timed out. ${RAISE_TIMEOUT_HINT}`));
12971
+ process.exit(1);
12972
+ }
11826
12973
  if (error instanceof SimApiError) {
11827
12974
  console.error(source_default.red(`Error: ${sanitize(error.message)}`));
11828
12975
  if (error.code)