@overaihq/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -48,15 +48,7 @@ var appBaseUrl = trimSlash(
48
48
 
49
49
  // src/api.ts
50
50
  var apiBaseUrl = () => apiBaseUrlOverride ?? readConfig()?.apiBaseUrl ?? DEFAULT_API_BASE_URL;
51
- var trpcMutation = async (procedure, input) => {
52
- const res = await fetch(
53
- `${apiBaseUrl()}/v1/utils/trpc/unauthenticated/${procedure}`,
54
- {
55
- method: "POST",
56
- headers: { "Content-Type": "application/json" },
57
- body: JSON.stringify(input)
58
- }
59
- );
51
+ var trpcResult = async (res, procedure) => {
60
52
  const body = await res.json().catch(() => null);
61
53
  if (body != null && "error" in body) {
62
54
  throw new Error(
@@ -68,6 +60,17 @@ var trpcMutation = async (procedure, input) => {
68
60
  }
69
61
  return body.result.data;
70
62
  };
63
+ var trpcMutation = async (procedure, input) => {
64
+ const res = await fetch(
65
+ `${apiBaseUrl()}/v1/utils/trpc/unauthenticated/${procedure}`,
66
+ {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify(input)
70
+ }
71
+ );
72
+ return trpcResult(res, procedure);
73
+ };
71
74
  var exchangeCliAuthCode = (code, codeVerifier) => trpcMutation("exchangeCliAuthCode", { code, codeVerifier });
72
75
  var exchangeSessionToken = (refreshToken, destinationOrgId) => trpcMutation("exchangeSessionToken", {
73
76
  refreshToken,
@@ -136,6 +139,14 @@ var v2Fetch = async (path, init) => {
136
139
  headers.set("Authorization", `Bearer ${auth.token}`);
137
140
  return fetch(`${apiBaseUrl()}${path}`, { ...init, headers });
138
141
  };
142
+ var userTrpcMutation = async (procedure, input) => {
143
+ const res = await v2Fetch(`/v1/utils/trpc/${procedure}`, {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify(input)
147
+ });
148
+ return trpcResult(res, procedure);
149
+ };
139
150
 
140
151
  // src/browser.ts
141
152
  import { spawn } from "child_process";
@@ -284,6 +295,7 @@ var whoami = () => {
284
295
  };
285
296
 
286
297
  // src/args.ts
298
+ import { readFileSync as readFileSync2 } from "fs";
287
299
  var flagValue = (args, ...names) => {
288
300
  for (const name of names) {
289
301
  const i = args.indexOf(name);
@@ -291,21 +303,44 @@ var flagValue = (args, ...names) => {
291
303
  }
292
304
  return void 0;
293
305
  };
294
- var readStdin = async () => {
306
+ var stdinClaimedBy = null;
307
+ var readStdin = async (claimant = "stdin") => {
308
+ if (stdinClaimedBy != null) {
309
+ throw new Error(
310
+ `Only one value can be read from stdin; ${stdinClaimedBy} already consumed it.`
311
+ );
312
+ }
313
+ stdinClaimedBy = claimant;
295
314
  const chunks = [];
296
315
  for await (const chunk of process.stdin) {
297
316
  chunks.push(chunk);
298
317
  }
299
318
  return Buffer.concat(chunks).toString("utf8");
300
319
  };
320
+ var readValue = async (value, label) => {
321
+ if (value?.startsWith("@") !== true) return value;
322
+ if (value.startsWith("@@")) return value.slice(1);
323
+ const path = value.slice(1);
324
+ if (path === "-") return (await readStdin(`${label} @-`)).trim();
325
+ try {
326
+ return readFileSync2(path, "utf8").trim();
327
+ } catch {
328
+ throw new Error(
329
+ `${label}: could not read file "${path}". Write "@@" to send a literal value starting with "@".`
330
+ );
331
+ }
332
+ };
301
333
 
302
334
  // src/gql.ts
303
- var gqlRequest = async (query, variables) => {
304
- const res = await v2Fetch("/v2/graphql", {
305
- method: "POST",
306
- headers: { "Content-Type": "application/json" },
307
- body: JSON.stringify({ query, variables })
308
- });
335
+ var gqlRequest = async (query, variables, options) => {
336
+ const res = await v2Fetch(
337
+ options?.v1 === true ? "/v1/graphql" : "/v2/graphql",
338
+ {
339
+ method: "POST",
340
+ headers: { "Content-Type": "application/json" },
341
+ body: JSON.stringify({ query, variables })
342
+ }
343
+ );
309
344
  const body = await res.json().catch(() => null);
310
345
  if (body?.errors != null && body.errors.length > 0) {
311
346
  throw new Error(body.errors.map((e) => e.message).join("; "));
@@ -467,7 +502,7 @@ import { createServer as createServer2 } from "http";
467
502
  import { dirname as dirname2, join as join4 } from "path";
468
503
 
469
504
  // src/workspace/manifest.ts
470
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
505
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
471
506
  import { dirname, join as join2, resolve } from "path";
472
507
  var MANIFEST_DIR = ".overai";
473
508
  var MANIFEST_FILE = "manifest.json";
@@ -486,7 +521,7 @@ var findWorkspaceRoot = (from = process.cwd()) => {
486
521
  }
487
522
  };
488
523
  var readManifest = (root) => JSON.parse(
489
- readFileSync2(join2(root, MANIFEST_DIR, MANIFEST_FILE), "utf8")
524
+ readFileSync3(join2(root, MANIFEST_DIR, MANIFEST_FILE), "utf8")
490
525
  );
491
526
  var openClone = () => {
492
527
  const root = findWorkspaceRoot();
@@ -511,14 +546,14 @@ var writeSynced = (root, relPath, content) => {
511
546
  };
512
547
  var readWorkingFile = (root, relPath) => {
513
548
  try {
514
- return readFileSync2(join2(root, relPath), "utf8");
549
+ return readFileSync3(join2(root, relPath), "utf8");
515
550
  } catch {
516
551
  return null;
517
552
  }
518
553
  };
519
554
  var readBaseFile = (root, relPath) => {
520
555
  try {
521
- return readFileSync2(basePath(root, relPath), "utf8");
556
+ return readFileSync3(basePath(root, relPath), "utf8");
522
557
  } catch {
523
558
  return null;
524
559
  }
@@ -542,10 +577,14 @@ var localState = (root, resource) => {
542
577
  };
543
578
 
544
579
  // src/workspace/validateElements.ts
580
+ var findings = (parsed) => parsed.success ? [] : parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
545
581
  var validateAutomationElements = async (elements) => {
546
- const { automationNodesSchema } = await import("./types-5J3QQFGH.js");
547
- const parsed = automationNodesSchema.safeParse(elements);
548
- return parsed.success ? [] : parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
582
+ const { automationNodesSchema } = await import("./types-7F2H2ASS.js");
583
+ return findings(automationNodesSchema.safeParse(elements));
584
+ };
585
+ var validateWebPageConfig = async (config) => {
586
+ const { webPageConfigWriteSchema } = await import("./webPage-FN3A45OT.js");
587
+ return findings(webPageConfigWriteSchema.safeParse(config));
549
588
  };
550
589
 
551
590
  // src/workspace/adapters.ts
@@ -633,7 +672,7 @@ var functionAdapter = {
633
672
  push: async (id, files, message) => {
634
673
  const meta = JSON.parse(fileBySuffix(files, ".meta.json"));
635
674
  const data = await gqlRequest(
636
- `mutation CliCreateFunctionVersion($functionId: String!, $code: String!, $message: String, $inputSchema: JSON, $executionTimeout: Int, $executionMemory: Int) {
675
+ `mutation CliCreateFunctionVersion($functionId: String!, $code: String!, $message: String, $inputSchema: String, $executionTimeout: Int, $executionMemory: Int) {
637
676
  createFunctionVersion(input: { functionVersion: {
638
677
  functionId: $functionId, code: $code, message: $message,
639
678
  inputSchema: $inputSchema, executionTimeout: $executionTimeout, executionMemory: $executionMemory
@@ -643,8 +682,9 @@ var functionAdapter = {
643
682
  functionId: id,
644
683
  code: fileBySuffix(files, ".ts"),
645
684
  message,
646
- inputSchema: meta.inputSchema,
647
- // Match the webapp's save defaults so a missing meta doesn't zero out limits.
685
+ // Match the webapp's save defaults so a missing meta doesn't zero out
686
+ // limits, and so an explicit null doesn't hit the NOT NULL column.
687
+ inputSchema: meta.inputSchema ?? "",
648
688
  executionTimeout: meta.executionTimeout ?? 1e4,
649
689
  executionMemory: meta.executionMemory ?? 512
650
690
  }
@@ -757,6 +797,13 @@ var pageAdapter = (opts) => {
757
797
  },
758
798
  push: async (id, files, message) => {
759
799
  const config = pageConfigFromFiles(files);
800
+ const findings2 = await opts.validateConfig?.(config) ?? [];
801
+ if (findings2.length > 0) {
802
+ throw new Error(
803
+ `Invalid page config:
804
+ ${findings2.map((f) => ` ${f}`).join("\n")}`
805
+ );
806
+ }
760
807
  const data = await gqlRequest(
761
808
  `mutation CliCreatePageVersion($id: String!, $message: String, $config: JSON!) {
762
809
  create${capitalized}Version(input: { ${gqlName}Version: { ${idArg}: $id, message: $message, config: $config } }) {
@@ -860,11 +907,11 @@ var automationAdapter = {
860
907
  },
861
908
  push: async (id, files, message) => {
862
909
  const doc = JSON.parse(fileBySuffix(files, ".json"));
863
- const findings = await validateAutomationElements(doc.elements);
864
- if (findings.length > 0) {
910
+ const findings2 = await validateAutomationElements(doc.elements);
911
+ if (findings2.length > 0) {
865
912
  throw new Error(
866
913
  `Invalid automation elements:
867
- ${findings.map((f) => ` ${f}`).join("\n")}`
914
+ ${findings2.map((f) => ` ${f}`).join("\n")}`
868
915
  );
869
916
  }
870
917
  const data = await gqlRequest(
@@ -908,7 +955,8 @@ ${findings.map((f) => ` ${f}`).join("\n")}`
908
955
  var webPageAdapter = pageAdapter({
909
956
  type: "webPage",
910
957
  dir: "web-pages",
911
- gqlName: "webPage"
958
+ gqlName: "webPage",
959
+ validateConfig: validateWebPageConfig
912
960
  });
913
961
  var printablePageAdapter = pageAdapter({
914
962
  type: "printablePage",
@@ -1443,16 +1491,29 @@ var runFn = async (args) => {
1443
1491
  const functionId = args[0];
1444
1492
  if (functionId == null || functionId.startsWith("-")) {
1445
1493
  throw new Error(
1446
- "Usage: overai fn <functionId> [--input key=value ... | -]\n overai fn logs <functionId> [--limit N] [--execution <id>]"
1494
+ "Usage: overai fn <functionId> [--method GET] [--path /features/123] [--input key=value ... | -]\n overai fn logs <functionId> [--limit N] [--execution <id>]"
1495
+ );
1496
+ }
1497
+ const method = (flagValue(args, "--method") ?? "POST").toUpperCase();
1498
+ const rawPath = flagValue(args, "--path") ?? "";
1499
+ const subPath = rawPath && !rawPath.startsWith("/") ? `/${rawPath}` : rawPath;
1500
+ const sendsBody = !["GET", "HEAD", "DELETE"].includes(method);
1501
+ if (!sendsBody && (args.includes("--input") || args.includes("-"))) {
1502
+ process.stderr.write(
1503
+ `warning: ${method} sends no request body, so --input is ignored. Pass values as a query string on --path instead, e.g. --path '/search?a=1'.
1504
+ `
1447
1505
  );
1448
1506
  }
1449
- const input = await collectInput(args);
1507
+ const input = sendsBody ? await collectInput(args) : void 0;
1450
1508
  const { organizationId } = await getAccessToken();
1451
- const res = await v2Fetch(`/v2/functions/${organizationId}/${functionId}`, {
1452
- method: "POST",
1453
- headers: { "Content-Type": "application/json" },
1454
- body: JSON.stringify(input)
1455
- });
1509
+ const res = await v2Fetch(
1510
+ `/v2/functions/${organizationId}/${functionId}${subPath}`,
1511
+ {
1512
+ method,
1513
+ headers: sendsBody ? { "Content-Type": "application/json" } : {},
1514
+ body: sendsBody ? JSON.stringify(input) : void 0
1515
+ }
1516
+ );
1456
1517
  const executionId = res.headers.get("x-execution-id");
1457
1518
  if (executionId != null) {
1458
1519
  process.stderr.write(`execution: ${executionId}
@@ -1476,21 +1537,32 @@ var runFn = async (args) => {
1476
1537
  };
1477
1538
 
1478
1539
  // src/commands/graphql.ts
1479
- import { readFileSync as readFileSync3 } from "fs";
1540
+ import { readFileSync as readFileSync4 } from "fs";
1480
1541
  var runGraphql = async (args) => {
1481
- const inline = flagValue(args, "-q", "--query");
1542
+ const inline = await readValue(flagValue(args, "-q", "--query"), "-q");
1482
1543
  const file = flagValue(args, "-f", "--file");
1483
- const query = inline ?? (file != null ? readFileSync3(file, "utf8") : process.stdin.isTTY ? "" : (await readStdin()).trim());
1544
+ const query = inline ?? (file != null ? readFileSync4(file, "utf8") : process.stdin.isTTY ? "" : (await readStdin("the query")).trim());
1484
1545
  if (query.length === 0) {
1485
- throw new Error("Provide a query with -q '<query>', -f <file>, or stdin.");
1546
+ throw new Error(
1547
+ "Provide a query with -q '<query>', -q @<file>, -f <file>, or stdin."
1548
+ );
1486
1549
  }
1487
- const variablesJson = flagValue(args, "--variables");
1550
+ const variablesArg = flagValue(args, "--variables");
1551
+ const variablesJson = await readValue(variablesArg, "--variables");
1488
1552
  const variables = (() => {
1489
1553
  if (variablesJson == null) return void 0;
1490
1554
  try {
1491
1555
  return JSON.parse(variablesJson);
1492
- } catch {
1493
- throw new Error(`--variables must be valid JSON. Got: ${variablesJson}`);
1556
+ } catch (error) {
1557
+ throw new Error(
1558
+ [
1559
+ `--variables is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
1560
+ `Got ${String(variablesJson.length)} chars starting: ${variablesJson.slice(0, 200)}`,
1561
+ ...variablesArg?.startsWith("@") === true ? [] : [
1562
+ `Pass --variables @<file.json> instead \u2014 an inline value is re-read by the shell, which expands $names and drops quotes.`
1563
+ ]
1564
+ ].join("\n")
1565
+ );
1494
1566
  }
1495
1567
  })();
1496
1568
  const res = await v2Fetch(
@@ -1636,6 +1708,154 @@ ${result.redirectUrl}
1636
1708
  `);
1637
1709
  };
1638
1710
 
1711
+ // src/commands/update.ts
1712
+ import { spawnSync } from "child_process";
1713
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
1714
+ import { fileURLToPath } from "url";
1715
+ var PACKAGE_NAME = "@overaihq/cli";
1716
+ var packageRootUrl = () => {
1717
+ for (const up of ["..", "../..", "../../.."]) {
1718
+ if (existsSync3(new URL(`${up}/package.json`, import.meta.url))) {
1719
+ return new URL(`${up}/`, import.meta.url);
1720
+ }
1721
+ }
1722
+ throw new Error("package.json not found relative to the CLI bundle");
1723
+ };
1724
+ var cliVersion = () => {
1725
+ const pkg = JSON.parse(
1726
+ readFileSync5(new URL("package.json", packageRootUrl()), "utf8")
1727
+ );
1728
+ return pkg.version;
1729
+ };
1730
+ var detectInstaller = () => {
1731
+ const path = fileURLToPath(import.meta.url);
1732
+ if (path.includes("_npx") || path.includes("dlx-")) return "ephemeral";
1733
+ if (path.includes(".pnpm") || /[\\/]pnpm[\\/]/.test(path)) return "pnpm";
1734
+ if (path.includes(".bun")) return "bun";
1735
+ return "npm";
1736
+ };
1737
+ var runUpdate = async () => {
1738
+ const current = cliVersion();
1739
+ if (existsSync3(new URL("src", packageRootUrl()))) {
1740
+ process.stdout.write(
1741
+ "Development build \u2014 rebuild from the repo instead of updating from npm.\n"
1742
+ );
1743
+ return;
1744
+ }
1745
+ const res = await fetch(
1746
+ `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`,
1747
+ // Bound the lookup so a slow/unreachable registry can't hang the command.
1748
+ { signal: AbortSignal.timeout(1e4) }
1749
+ );
1750
+ if (!res.ok) {
1751
+ throw new Error(`npm registry lookup failed (HTTP ${String(res.status)})`);
1752
+ }
1753
+ const body = await res.json();
1754
+ const latest = typeof body.version === "string" ? body.version : null;
1755
+ if (latest == null) {
1756
+ throw new Error("npm registry returned an unexpected response");
1757
+ }
1758
+ if (latest === current) {
1759
+ process.stdout.write(`overai ${current} is up to date.
1760
+ `);
1761
+ return;
1762
+ }
1763
+ const cur = current.split(".").map(Number);
1764
+ const lat = latest.split(".").map(Number);
1765
+ const cmp = cur.map((n, i) => n - (lat[i] ?? 0)).find((d) => d !== 0) ?? 0;
1766
+ if (cmp > 0) {
1767
+ process.stdout.write(
1768
+ `overai ${current} is ahead of latest (${latest}) \u2014 likely a @next install; nothing to do.
1769
+ `
1770
+ );
1771
+ return;
1772
+ }
1773
+ const installer = detectInstaller();
1774
+ if (installer === "ephemeral") {
1775
+ process.stdout.write(
1776
+ `Running from an npx/dlx cache (current ${current}, latest ${latest}) \u2014 invoke as \`npx ${PACKAGE_NAME}@latest\` to always get the newest release.
1777
+ `
1778
+ );
1779
+ return;
1780
+ }
1781
+ const spec = `${PACKAGE_NAME}@${latest}`;
1782
+ const argsByInstaller = {
1783
+ npm: ["npm", "install", "-g", spec],
1784
+ pnpm: ["pnpm", "add", "-g", spec],
1785
+ bun: ["bun", "add", "-g", spec]
1786
+ };
1787
+ const [cmd, ...args] = argsByInstaller[installer];
1788
+ process.stdout.write(
1789
+ `Updating overai ${current} \u2192 ${latest} via ${installer}\u2026
1790
+ `
1791
+ );
1792
+ const result = spawnSync(cmd, args, {
1793
+ stdio: "inherit",
1794
+ // npm/pnpm are .cmd shims on Windows; a shell is required to resolve them.
1795
+ shell: process.platform === "win32"
1796
+ });
1797
+ if (result.status !== 0) {
1798
+ throw new Error(
1799
+ `${[cmd, ...args].join(" ")} failed (exit ${String(result.status ?? "?")})`
1800
+ );
1801
+ }
1802
+ process.stdout.write(`Updated to ${latest}.
1803
+ `);
1804
+ };
1805
+
1806
+ // src/commands/report.ts
1807
+ var reportInput = async (args, stdin = readStdin) => {
1808
+ const [type, ...bodyArgs] = args;
1809
+ if (type !== "bug" && type !== "feature") {
1810
+ throw new Error(
1811
+ 'Usage: overai report bug|feature "<details>" (or pass - to read stdin)'
1812
+ );
1813
+ }
1814
+ const detailedBugReport = (bodyArgs.length === 1 && bodyArgs[0] === "-" ? await stdin() : bodyArgs.join(" ")).trim();
1815
+ if (detailedBugReport.length === 0) {
1816
+ throw new Error(
1817
+ `${type === "bug" ? "Bug report" : "Feature request"} details cannot be empty.`
1818
+ );
1819
+ }
1820
+ if (detailedBugReport.length > 8e3) {
1821
+ throw new Error("Report details cannot exceed 8000 characters.");
1822
+ }
1823
+ return { feedbackType: type, detailedBugReport };
1824
+ };
1825
+ var runReport = async (args) => {
1826
+ const input = await reportInput(args);
1827
+ const manifest = (() => {
1828
+ try {
1829
+ return openClone().manifest;
1830
+ } catch {
1831
+ return null;
1832
+ }
1833
+ })();
1834
+ const resources = manifest ? Object.entries(manifest.resources) : [];
1835
+ await userTrpcMutation("architectIssues.report", {
1836
+ ...input,
1837
+ source: "cli",
1838
+ cliContext: {
1839
+ cliVersion: cliVersion(),
1840
+ nodeVersion: process.version,
1841
+ platform: process.platform,
1842
+ architecture: process.arch,
1843
+ workspaceId: manifest?.workspaceId.slice(0, 200),
1844
+ workspaceName: manifest?.workspaceName.slice(0, 500),
1845
+ resources: manifest ? resources.slice(0, 500).map(([id, resource]) => ({
1846
+ id: id.slice(0, 200),
1847
+ name: resource.name.slice(0, 500),
1848
+ type: resource.type.slice(0, 100)
1849
+ })) : void 0,
1850
+ omittedResourceCount: Math.max(0, resources.length - 500)
1851
+ }
1852
+ });
1853
+ process.stdout.write(
1854
+ `${input.feedbackType === "bug" ? "Bug report" : "Feature request"} sent to OverAI.
1855
+ `
1856
+ );
1857
+ };
1858
+
1639
1859
  // src/docs/bestPractices.ts
1640
1860
  var platform2 = `# OverAI platform primer
1641
1861
 
@@ -1651,11 +1871,23 @@ deploy\` makes it live. Pick primitives by trigger:
1651
1871
  **Data Files** (env.DFxxx) for versioned single-file datasets. Prefer
1652
1872
  first-class platform features over third-party integrations.
1653
1873
 
1874
+ When broken or unexplained platform behavior blocks progress, send a detailed
1875
+ report with \`overai report bug -\`. When a missing platform capability forces
1876
+ a significant workaround, use \`overai report feature -\`. Include the goal,
1877
+ actual limitation, desired behavior, and relevant errors or resource IDs. Do
1878
+ not report speculative niceties or every failed attempt; no separate user
1879
+ confirmation is needed because invoking the command explicitly submits it.
1880
+
1654
1881
  The env SDK is the same in pages, functions, and automation code nodes:
1655
1882
  env.TBSxxx.sql\`...\` (postgres.js tagged template \u2014 interpolations are
1656
- auto-parameterized, never string-concatenate user input), env.FNxxx.call(),
1883
+ auto-parameterized, never string-concatenate user input), env.FNxxx.call()
1884
+ (pass \`path\` to hit a sub-path of a function that routes internally),
1657
1885
  env.ATxxx.start(inputs?) (fire-and-forget), env.DFxxx.read.json()/.saveVersion(),
1658
- env.STORAGE, env.AI.generateText/generateObject, env.GRAPHQL.request().`;
1886
+ env.STORAGE (incl. zipTree/zipExtract to list a zip without decompressing it and
1887
+ pull out only the entries you need; upload and zipExtract shorten stored names over
1888
+ 128 characters in the middle while preserving their ends),
1889
+ env.AI.generateText/generateObject,
1890
+ env.GRAPHQL.request().`;
1659
1891
  var automations = `# Automation best practices
1660
1892
 
1661
1893
  An automation's elements document is the node graph: an array of nodes, each
@@ -1673,6 +1905,10 @@ Rules that keep graphs healthy:
1673
1905
  {{S1.field}}). After renaming/removing a node, grep the document for its
1674
1906
  variableId \u2014 dangling references fail at runtime, not at push.
1675
1907
  - Bring IF_ELSE branches back together with an ALWAYS node.
1908
+ - A loop (LOOP_ARRAY/LOOP_RANGE) always resolves to its success handle once all
1909
+ iterations finish, even if some or all iterations error \u2014 it never takes a
1910
+ failure path. Handle errors on the child nodes inside the loop, or inspect
1911
+ \`iterations\`, to catch per-iteration failures.
1676
1912
  - Activation requires a deployed version with an EVENT trigger node \u2014
1677
1913
  activating without one silently no-ops.
1678
1914
 
@@ -1709,6 +1945,31 @@ language, customTitle, hideTopBar, ...). Push reassembles them.
1709
1945
  - For guest-facing workspaces, add page-to-page navigation (guests land on the
1710
1946
  workspace homepage and can only follow links). "Hide Top Bar" gives an
1711
1947
  app-like experience.
1948
+ - Public pages (/public/:id) have no viewer identity \u2014 useCurrentUser() is
1949
+ null. Identity-scoped features degrade: useAgentChat streams but persists
1950
+ nothing (conversationId stays null, useAgentConversations is empty, the
1951
+ conversation helpers throw), and author-requiring writes like
1952
+ env.DFxxx.saveVersion 401. Reads use the workspace's own access, so treat
1953
+ anything the page can query as public.
1954
+ - Write meta.json flags (noSandbox, hideTopBar) as JSON booleans. A page cloned
1955
+ before the shape settled may hold the strings "true"/"false" \u2014 those still
1956
+ push, anything else is rejected. noSandbox drops the iframe sandbox, which
1957
+ localStorage/sessionStorage, cookies, form submission and
1958
+ alert/confirm/prompt all require.
1959
+ - dataSourceUrl and filter in meta.json are legacy: they populate the \`data\` and
1960
+ \`items\` globals, frozen at render start, and filter reads the old
1961
+ item/attribute model. For new pages load data in the page instead \u2014 web pages
1962
+ via env.TBSxxx.sql / env.DFxxx, printable pages via fetch with the \`token\`
1963
+ global (no env proxy there). Don't rewrite a working page just to migrate it.
1964
+ - dataSourceUrl in meta.json is fetched verbatim and its response becomes the
1965
+ \`data\` global. It is a static URL, not a template \u2014 search params are NOT
1966
+ substituted into it (neither {x} nor Liquid {{ x }}). If the data depends on
1967
+ a search param, fetch it in the page instead. To clear it use JSON null; the
1968
+ string "null" is a four-character URL that fails at fetch time.
1969
+ - Printable page PDFs render in a headless browser that waits for network idle,
1970
+ so useEffect fetches DO resolve before capture. Load data in the page \u2014 you
1971
+ don't need to pre-fetch it into searchParams or stage it in storage. A blank
1972
+ PDF is far more often an undeployed version than a timing problem.
1712
1973
  - Console logs are visible in the webapp editor; use them to verify behavior
1713
1974
  after deploying.`;
1714
1975
  var functions = `# Function best practices
@@ -1718,10 +1979,14 @@ esm.sh imports allowed. The CLI materializes <slug>.<id>.ts (code) and
1718
1979
  <slug>.<id>.meta.json (inputSchema, executionTimeout ms, executionMemory MB).
1719
1980
 
1720
1981
  - Functions are full HTTP apps: method + sub-path are forwarded to your code,
1721
- and each execution records requestMethod/requestPath.
1982
+ and each execution records requestMethod/requestPath. Callers reach a route
1983
+ with env.FNxxx.call({ method, path: "/features/123" }) \u2014 path defaults to the
1984
+ base path and method to POST.
1722
1985
  - Keep the inputSchema in sync with what the code actually reads \u2014 callers
1723
- (agents, pages, automations) rely on it.
1724
- - Verify with \`overai fn <id> --input k=v\` (invokes the DEPLOYED version) and
1986
+ (agents, pages, automations) rely on it. It is a JSON-encoded string, so it
1987
+ appears escaped inside meta.json.
1988
+ - Verify with \`overai fn <id> --input k=v\` (invokes the DEPLOYED version;
1989
+ add \`--method\`/\`--path\` to exercise one route of a router) and
1725
1990
  \`overai fn logs <id>\` / \`--execution <FEX id>\` for status, timing, logs,
1726
1991
  and input/output bodies.
1727
1992
  - Version discipline: function_execution.versionNumber tells you exactly which
@@ -1729,7 +1994,10 @@ esm.sh imports allowed. The CLI materializes <slug>.<id>.ts (code) and
1729
1994
  failure.`;
1730
1995
  var workflow = `# CLI editing workflow
1731
1996
 
1732
- 1. \`overai clone <workspaceId>\` (or \`overai pull\` in an existing clone).
1997
+ 1. \`overai workspace list\` to find the id, then \`overai clone <workspaceId>\`
1998
+ (or \`overai pull\` in an existing clone). Workspaces nest, and clone pulls
1999
+ ONE workspace \u2014 a parent's clone does not include its children, so clone
2000
+ each nested workspace you need separately.
1733
2001
  2. Edit files. The id in each filename is authoritative; slugs are cosmetic.
1734
2002
  3. \`overai status\` / \`overai diff\` \u2014 review what changed.
1735
2003
  4. \`overai review\` \u2014 validation + these guides; fix findings first.
@@ -1772,7 +2040,7 @@ var runBestPractices = (args) => {
1772
2040
  var runReview = async () => {
1773
2041
  const root = findWorkspaceRoot();
1774
2042
  const manifest = readManifest(root);
1775
- const findings = [];
2043
+ const findings2 = [];
1776
2044
  const touchedTypes = /* @__PURE__ */ new Set();
1777
2045
  const changed = [];
1778
2046
  for (const [id, resource] of Object.entries(manifest.resources)) {
@@ -1789,14 +2057,14 @@ var runReview = async () => {
1789
2057
  try {
1790
2058
  JSON.parse(content);
1791
2059
  } catch (error) {
1792
- findings.push(
2060
+ findings2.push(
1793
2061
  `${relPath}: invalid JSON \u2014 ${error instanceof Error ? error.message : String(error)}`
1794
2062
  );
1795
2063
  continue;
1796
2064
  }
1797
2065
  }
1798
2066
  if (!relPath.endsWith(".meta.json") && content.trim().length === 0) {
1799
- findings.push(
2067
+ findings2.push(
1800
2068
  `${relPath}: file is empty \u2014 push would blank the resource.`
1801
2069
  );
1802
2070
  }
@@ -1805,17 +2073,17 @@ var runReview = async () => {
1805
2073
  for (const match of content.matchAll(importRe)) {
1806
2074
  const specifier = match[1];
1807
2075
  if (specifier == null || specifier.startsWith("https://")) continue;
1808
- findings.push(
2076
+ findings2.push(
1809
2077
  /^react(-dom)?(\/|$)/.test(specifier) ? `${relPath}: imports "${specifier}" \u2014 React and its hooks are already in scope; remove the import or the page renders blank.` : `${relPath}: imports "${specifier}" \u2014 the page sandbox can only import https URLs (use https://esm.sh/...).`
1810
2078
  );
1811
2079
  }
1812
2080
  if (/^\s*export\s+default\b/m.test(content)) {
1813
- findings.push(
2081
+ findings2.push(
1814
2082
  `${relPath}: uses \`export default\` \u2014 declare a plain \`function App() {}\`; the runtime appends the render call.`
1815
2083
  );
1816
2084
  }
1817
2085
  if (!/\b(function|const|let)\s+App\b/.test(content)) {
1818
- findings.push(
2086
+ findings2.push(
1819
2087
  `${relPath}: no App component found \u2014 page code must declare \`function App() {}\`.`
1820
2088
  );
1821
2089
  }
@@ -1827,7 +2095,7 @@ var runReview = async () => {
1827
2095
  if (docFile != null && raw != null) {
1828
2096
  try {
1829
2097
  const doc = JSON.parse(raw);
1830
- findings.push(
2098
+ findings2.push(
1831
2099
  ...(await validateAutomationElements(doc.elements)).map(
1832
2100
  (f) => `${docFile} elements.${f}`
1833
2101
  )
@@ -1849,7 +2117,7 @@ var runReview = async () => {
1849
2117
  ...changed.map((c) => `- ${c}`),
1850
2118
  "",
1851
2119
  "## Validation findings",
1852
- findings.length > 0 ? findings.map((f) => `- ${f}`).join("\n") : "- none (mechanical checks passed \u2014 still review the diff against the guides below)",
2120
+ findings2.length > 0 ? findings2.map((f) => `- ${f}`).join("\n") : "- none (mechanical checks passed \u2014 still review the diff against the guides below)",
1853
2121
  "",
1854
2122
  "## Review guides",
1855
2123
  "Review `overai diff` output against these before pushing:",
@@ -1860,6 +2128,155 @@ var runReview = async () => {
1860
2128
  );
1861
2129
  };
1862
2130
 
2131
+ // src/commands/runner.ts
2132
+ var runCreate = async (workspaceId, args) => {
2133
+ const name = flagValue(args, "--name") ?? "Action Runner";
2134
+ const identityResult = await gqlRequest(
2135
+ `mutation CliCreateMachineIdentity($name: String!) {
2136
+ identityInsert(object: { name: $name, type: "machine" }) { id }
2137
+ }`,
2138
+ { name },
2139
+ { v1: true }
2140
+ );
2141
+ const identityId = identityResult.identityInsert?.id;
2142
+ if (identityId == null) throw new Error("Failed to create machine identity");
2143
+ await gqlRequest(
2144
+ `mutation CliCreateIdentityOrganization($identityId: String!) {
2145
+ createIdentityOrganization(
2146
+ input: { identityOrganization: { identityId: $identityId } }
2147
+ ) { identityOrganization { identityId } }
2148
+ }`,
2149
+ { identityId }
2150
+ );
2151
+ const runnerResult = await gqlRequest(
2152
+ `mutation CliCreateActionRunner($identityId: String!, $workspaceId: String!) {
2153
+ createActionRunner(
2154
+ input: { actionRunner: { identityId: $identityId, workspaceId: $workspaceId } }
2155
+ ) { actionRunner { id } }
2156
+ }`,
2157
+ { identityId, workspaceId }
2158
+ );
2159
+ const actionRunnerId = runnerResult.createActionRunner?.actionRunner?.id;
2160
+ if (actionRunnerId == null) throw new Error("Failed to create action runner");
2161
+ printResult(
2162
+ { actionRunnerId, identityId, name, workspaceId },
2163
+ resolveFormat(flagValue(args, "-o", "--output"))
2164
+ );
2165
+ };
2166
+ var runToken = async (runnerId, args) => {
2167
+ const note = flagValue(args, "--note") ?? "cli";
2168
+ const result = await gqlRequest(
2169
+ `mutation CliCreateActionRunnerToken($actionRunnerId: String!, $note: String) {
2170
+ createActionRunnerToken(
2171
+ input: { actionRunnerId: $actionRunnerId, note: $note }
2172
+ ) { result }
2173
+ }`,
2174
+ { actionRunnerId: runnerId, note }
2175
+ );
2176
+ const secret = result.createActionRunnerToken?.result;
2177
+ if (secret == null) throw new Error("Failed to mint runner token");
2178
+ printResult(
2179
+ { runnerId, secret },
2180
+ resolveFormat(flagValue(args, "-o", "--output"))
2181
+ );
2182
+ };
2183
+ var resolveWorkspaceId = async (runnerId) => {
2184
+ const data = await gqlRequest(
2185
+ `query CliResolveRunner($id: String!) {
2186
+ actionRunners(condition: { id: $id }, first: 1) { nodes { workspaceId } }
2187
+ }`,
2188
+ { id: runnerId }
2189
+ );
2190
+ const workspaceId = data.actionRunners.nodes[0]?.workspaceId;
2191
+ if (workspaceId == null) {
2192
+ throw new Error(
2193
+ `Unknown runner ${runnerId} (or it has no workspace). Try: overai runner list`
2194
+ );
2195
+ }
2196
+ return workspaceId;
2197
+ };
2198
+ var enqueueAction = async (runnerId, actionId, inputs, args) => {
2199
+ const { organizationId } = await getAccessToken();
2200
+ const workspaceId = await resolveWorkspaceId(runnerId);
2201
+ const res = await v2Fetch(
2202
+ `/v2/actions/${organizationId}/${workspaceId}/run`,
2203
+ {
2204
+ method: "POST",
2205
+ headers: { "Content-Type": "application/json" },
2206
+ body: JSON.stringify({ actionId, inputs, actionRunnerId: runnerId })
2207
+ }
2208
+ );
2209
+ const body = await res.json().catch(() => null);
2210
+ if (!res.ok) {
2211
+ throw new Error(body?.error ?? `run failed (${String(res.status)})`);
2212
+ }
2213
+ printResult(body, resolveFormat(flagValue(args, "-o", "--output")));
2214
+ };
2215
+ var runRun = async (runnerId, args) => {
2216
+ const code = await readValue(flagValue(args, "--code"), "--code") ?? (process.stdin.isTTY ? "" : (await readStdin("the code")).trim());
2217
+ if (code.length === 0) {
2218
+ throw new Error(
2219
+ "Provide a command with --code '<code>', --code @<file>, or piped stdin."
2220
+ );
2221
+ }
2222
+ const interpreter = flagValue(args, "--interpreter") ?? "shell";
2223
+ if (interpreter !== "shell" && interpreter !== "powershell") {
2224
+ throw new Error("--interpreter must be 'shell' or 'powershell'.");
2225
+ }
2226
+ await enqueueAction(
2227
+ runnerId,
2228
+ "RUNNER_RUN_COMMAND",
2229
+ { actionRunnerId: runnerId, interpreter, code },
2230
+ args
2231
+ );
2232
+ };
2233
+ var runUpload = async (runnerId, args) => {
2234
+ const path = flagValue(args, "--path");
2235
+ if (path == null || path.length === 0) {
2236
+ throw new Error("Provide a file with --path '<path>'.");
2237
+ }
2238
+ await enqueueAction(
2239
+ runnerId,
2240
+ "RUNNER_UPLOAD_FILE",
2241
+ { actionRunnerId: runnerId, filePath: path },
2242
+ args
2243
+ );
2244
+ };
2245
+ var runList = async (args) => {
2246
+ const workspaceId = args.find((a) => !a.startsWith("-"));
2247
+ const limit = Number(flagValue(args, "--limit") ?? 50);
2248
+ const data = await gqlRequest(
2249
+ `query CliRunnerList($workspaceId: String, $first: Int!) {
2250
+ actionRunners(condition: { workspaceId: $workspaceId }, first: $first) {
2251
+ nodes { id identity { name } workspaceId isCloud isArchived createdAt }
2252
+ }
2253
+ }`,
2254
+ { workspaceId: workspaceId ?? null, first: limit }
2255
+ );
2256
+ printResult(
2257
+ data.actionRunners.nodes.map((r) => ({
2258
+ id: r.id,
2259
+ name: r.identity?.name ?? null,
2260
+ workspaceId: r.workspaceId,
2261
+ kind: r.isCloud ? "cloud" : "self-hosted",
2262
+ archived: r.isArchived,
2263
+ createdAt: r.createdAt
2264
+ })),
2265
+ resolveFormat(flagValue(args, "-o", "--output"))
2266
+ );
2267
+ };
2268
+ var USAGE = "Usage: overai runner create <workspaceId> [--name <name>]\n overai runner token <runnerId> [--note <note>]\n overai runner run <runnerId> [--interpreter shell|powershell] --code '<code>'\n overai runner upload <runnerId> --path '<path>'\n overai runner list [workspaceId] [--limit <n>]\n";
2269
+ var runRunner = async (args) => {
2270
+ const [sub, id, ...rest] = args;
2271
+ if (sub === "create" && id != null) return runCreate(id, rest);
2272
+ if (sub === "token" && id != null) return runToken(id, rest);
2273
+ if (sub === "run" && id != null) return runRun(id, rest);
2274
+ if (sub === "upload" && id != null) return runUpload(id, rest);
2275
+ if (sub === "list") return runList([id, ...rest].filter((a) => a != null));
2276
+ process.stderr.write(USAGE);
2277
+ process.exitCode = 1;
2278
+ };
2279
+
1863
2280
  // src/commands/sql.ts
1864
2281
  import { createInterface } from "readline";
1865
2282
  var runQuery = async (organizationId, tableSpaceId, query, params) => {
@@ -2032,7 +2449,11 @@ var runSql = async (args) => {
2032
2449
  return void 0;
2033
2450
  })();
2034
2451
  if (query == null) return runRepl(organizationId, tableSpaceId, format);
2035
- const result = await runQuery(organizationId, tableSpaceId, query);
2452
+ const result = await runQuery(
2453
+ organizationId,
2454
+ tableSpaceId,
2455
+ await readValue(query, "the query") ?? query
2456
+ );
2036
2457
  printResult(result.rows, format);
2037
2458
  if (process.stderr.isTTY) {
2038
2459
  process.stderr.write(`${summarize(result)}
@@ -2040,101 +2461,6 @@ var runSql = async (args) => {
2040
2461
  }
2041
2462
  };
2042
2463
 
2043
- // src/commands/update.ts
2044
- import { spawnSync } from "child_process";
2045
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
2046
- import { fileURLToPath } from "url";
2047
- var PACKAGE_NAME = "@overaihq/cli";
2048
- var packageRootUrl = () => {
2049
- for (const up of ["..", "../..", "../../.."]) {
2050
- if (existsSync3(new URL(`${up}/package.json`, import.meta.url))) {
2051
- return new URL(`${up}/`, import.meta.url);
2052
- }
2053
- }
2054
- throw new Error("package.json not found relative to the CLI bundle");
2055
- };
2056
- var cliVersion = () => {
2057
- const pkg = JSON.parse(
2058
- readFileSync4(new URL("package.json", packageRootUrl()), "utf8")
2059
- );
2060
- return pkg.version;
2061
- };
2062
- var detectInstaller = () => {
2063
- const path = fileURLToPath(import.meta.url);
2064
- if (path.includes("_npx") || path.includes("dlx-")) return "ephemeral";
2065
- if (path.includes(".pnpm") || /[\\/]pnpm[\\/]/.test(path)) return "pnpm";
2066
- if (path.includes(".bun")) return "bun";
2067
- return "npm";
2068
- };
2069
- var runUpdate = async () => {
2070
- const current = cliVersion();
2071
- if (existsSync3(new URL("src", packageRootUrl()))) {
2072
- process.stdout.write(
2073
- "Development build \u2014 rebuild from the repo instead of updating from npm.\n"
2074
- );
2075
- return;
2076
- }
2077
- const res = await fetch(
2078
- `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`,
2079
- // Bound the lookup so a slow/unreachable registry can't hang the command.
2080
- { signal: AbortSignal.timeout(1e4) }
2081
- );
2082
- if (!res.ok) {
2083
- throw new Error(`npm registry lookup failed (HTTP ${String(res.status)})`);
2084
- }
2085
- const body = await res.json();
2086
- const latest = typeof body.version === "string" ? body.version : null;
2087
- if (latest == null) {
2088
- throw new Error("npm registry returned an unexpected response");
2089
- }
2090
- if (latest === current) {
2091
- process.stdout.write(`overai ${current} is up to date.
2092
- `);
2093
- return;
2094
- }
2095
- const cur = current.split(".").map(Number);
2096
- const lat = latest.split(".").map(Number);
2097
- const cmp = cur.map((n, i) => n - (lat[i] ?? 0)).find((d) => d !== 0) ?? 0;
2098
- if (cmp > 0) {
2099
- process.stdout.write(
2100
- `overai ${current} is ahead of latest (${latest}) \u2014 likely a @next install; nothing to do.
2101
- `
2102
- );
2103
- return;
2104
- }
2105
- const installer = detectInstaller();
2106
- if (installer === "ephemeral") {
2107
- process.stdout.write(
2108
- `Running from an npx/dlx cache (current ${current}, latest ${latest}) \u2014 invoke as \`npx ${PACKAGE_NAME}@latest\` to always get the newest release.
2109
- `
2110
- );
2111
- return;
2112
- }
2113
- const spec = `${PACKAGE_NAME}@${latest}`;
2114
- const argsByInstaller = {
2115
- npm: ["npm", "install", "-g", spec],
2116
- pnpm: ["pnpm", "add", "-g", spec],
2117
- bun: ["bun", "add", "-g", spec]
2118
- };
2119
- const [cmd, ...args] = argsByInstaller[installer];
2120
- process.stdout.write(
2121
- `Updating overai ${current} \u2192 ${latest} via ${installer}\u2026
2122
- `
2123
- );
2124
- const result = spawnSync(cmd, args, {
2125
- stdio: "inherit",
2126
- // npm/pnpm are .cmd shims on Windows; a shell is required to resolve them.
2127
- shell: process.platform === "win32"
2128
- });
2129
- if (result.status !== 0) {
2130
- throw new Error(
2131
- `${[cmd, ...args].join(" ")} failed (exit ${String(result.status ?? "?")})`
2132
- );
2133
- }
2134
- process.stdout.write(`Updated to ${latest}.
2135
- `);
2136
- };
2137
-
2138
2464
  // src/commands/versions.ts
2139
2465
  import { spawnSync as spawnSync2 } from "child_process";
2140
2466
  import { mkdirSync as mkdirSync4, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
@@ -2270,19 +2596,59 @@ var runDiff = async (args) => {
2270
2596
  }
2271
2597
  };
2272
2598
 
2599
+ // src/commands/workspace.ts
2600
+ var listWorkspaces = async (args) => {
2601
+ const archived = args.includes("--archived");
2602
+ const limit = Number(flagValue(args, "--limit") ?? 200);
2603
+ const data = await gqlRequest(
2604
+ `query CliWorkspaceList($first: Int!, $includeArchived: IncludeArchivedOption!) {
2605
+ workspaces(first: $first, includeArchived: $includeArchived) {
2606
+ nodes { id name isArchived workspaceWorkspace { parentWorkspaceId } }
2607
+ }
2608
+ myWorkspaceRoles { nodes { workspaceId role } }
2609
+ }`,
2610
+ { first: limit, includeArchived: archived ? "YES" : "NO" }
2611
+ );
2612
+ const roles = new Map(
2613
+ data.myWorkspaceRoles.nodes.map((r) => [r.workspaceId, r.role])
2614
+ );
2615
+ printResult(
2616
+ data.workspaces.nodes.map((w) => ({
2617
+ id: w.id,
2618
+ name: w.name,
2619
+ role: roles.get(w.id) ?? "",
2620
+ ...archived ? { archived: w.isArchived } : {},
2621
+ parentId: w.workspaceWorkspace?.parentWorkspaceId ?? ""
2622
+ })).sort((a, b) => a.name.localeCompare(b.name)),
2623
+ resolveFormat(flagValue(args, "-o", "--output"))
2624
+ );
2625
+ };
2626
+ var USAGE2 = "Usage: overai workspace list [--archived] [--limit <n>]\n";
2627
+ var runWorkspace = async (args) => {
2628
+ if (args[0] !== "list") {
2629
+ process.stderr.write(USAGE2);
2630
+ process.exitCode = 1;
2631
+ return;
2632
+ }
2633
+ await listWorkspaces(args.slice(1));
2634
+ };
2635
+
2273
2636
  // src/index.ts
2274
2637
  var HELP = `overai \u2014 OverAI command-line interface
2275
2638
 
2276
2639
  Usage:
2277
- overai signin Sign in via your browser
2640
+ overai signin Sign in via your browser; choose your
2641
+ organization there to complete SSO
2278
2642
  overai signout Sign out and revoke this session
2279
2643
  overai whoami Show the current identity and org
2280
2644
  overai org list List your organizations
2281
2645
  overai org switch <orgId> Switch the active org
2282
- overai graphql -q '<query>' | -f <file> [--v1]
2646
+ overai workspace list [--archived] List workspaces you can see in this org
2647
+ overai graphql -q '<query>' | -f <file> [--variables <json>] [--v1]
2283
2648
  Run a GraphQL operation (/v2; --v1 = legacy Hasura)
2284
- overai fn <functionId> [--input k=v ... | -]
2285
- Invoke a function
2649
+ overai fn <functionId> [--method GET] [--path /features/123] [--input k=v ... | -]
2650
+ Invoke a function; --path reaches a
2651
+ sub-path of one that routes internally
2286
2652
  overai fn logs <functionId> [--limit N] [--execution <id>]
2287
2653
  Execution history / logs
2288
2654
  overai sql <tableSpaceId> ["<SQL>"] Run SQL against a tablespace; no query
@@ -2306,6 +2672,8 @@ Workspace sync (edit resources as local files):
2306
2672
  Live-preview a web page from local files
2307
2673
  overai review Validation + best-practice review packet
2308
2674
  overai best-practices [topic] Print bundled guides
2675
+ overai report bug|feature "<details>"
2676
+ Send product feedback (use - for stdin)
2309
2677
  overai update Update the global install to the latest release
2310
2678
  overai version Print the CLI version
2311
2679
 
@@ -2314,8 +2682,21 @@ Automations:
2314
2682
  overai automation state <runId> [--node <variableId>]
2315
2683
  overai automation run <automationId> [--input k=v ... | -]
2316
2684
 
2685
+ Self-hosted runners (enabling remote access stays on-device \u2014 not a CLI action):
2686
+ overai runner create <workspaceId> [--name <name>]
2687
+ overai runner token <runnerId> [--note <note>] Mint a pairing secret (shown once)
2688
+ overai runner run <runnerId> [--interpreter shell|powershell] --code '<code>'
2689
+ overai runner upload <runnerId> --path '<path>'
2690
+ overai runner list [workspaceId]
2691
+
2692
+ Arguments that carry a document \u2014 the graphql query and --variables, the sql
2693
+ statement, runner --code \u2014 also take @<file> to read it from disk, or @- to read
2694
+ it from stdin. Prefer that for anything long or quote-heavy: an inline value is
2695
+ parsed by your shell first, and one apostrophe inside it silently truncates the
2696
+ rest. Write @@ to send a literal value that starts with "@".
2697
+
2317
2698
  Output: table on a TTY, JSON when piped; -o json|table|csv to force.
2318
- Aliases: login = signin, logout = signout.
2699
+ Aliases: login = signin, logout = signout, ws = workspace.
2319
2700
  Env: OVERAI_API_URL, OVERAI_APP_URL override the target deployment.
2320
2701
  `;
2321
2702
  process.stdout.on("error", (error) => {
@@ -2348,6 +2729,10 @@ var run = async () => {
2348
2729
  process.stderr.write("Usage: overai org list | org switch <orgId>\n");
2349
2730
  process.exitCode = 1;
2350
2731
  return;
2732
+ case "workspace":
2733
+ case "ws":
2734
+ await runWorkspace(rest);
2735
+ return;
2351
2736
  case "graphql":
2352
2737
  await runGraphql(rest);
2353
2738
  return;
@@ -2393,9 +2778,15 @@ var run = async () => {
2393
2778
  case "best-practices":
2394
2779
  runBestPractices(rest);
2395
2780
  return;
2781
+ case "report":
2782
+ await runReport(rest);
2783
+ return;
2396
2784
  case "automation":
2397
2785
  await runAutomation(rest);
2398
2786
  return;
2787
+ case "runner":
2788
+ await runRunner(rest);
2789
+ return;
2399
2790
  case "update":
2400
2791
  case "upgrade":
2401
2792
  await runUpdate();