@vendoai/vendo 0.4.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.
Files changed (212) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +28 -0
  3. package/bin/vendo.mjs +4 -0
  4. package/dist/ai-sdk.d.ts +35 -0
  5. package/dist/ai-sdk.js +62 -0
  6. package/dist/auth-presets/auth-js.d.ts +14 -0
  7. package/dist/auth-presets/auth-js.js +91 -0
  8. package/dist/auth-presets/auth0.d.ts +23 -0
  9. package/dist/auth-presets/auth0.js +98 -0
  10. package/dist/auth-presets/clerk.d.ts +23 -0
  11. package/dist/auth-presets/clerk.js +65 -0
  12. package/dist/auth-presets/conformance.d.ts +42 -0
  13. package/dist/auth-presets/conformance.js +139 -0
  14. package/dist/auth-presets/identity.d.ts +56 -0
  15. package/dist/auth-presets/identity.js +152 -0
  16. package/dist/auth-presets/index.d.ts +14 -0
  17. package/dist/auth-presets/index.js +12 -0
  18. package/dist/auth-presets/jwt.d.ts +16 -0
  19. package/dist/auth-presets/jwt.js +54 -0
  20. package/dist/auth-presets/shared.d.ts +35 -0
  21. package/dist/auth-presets/shared.js +1 -0
  22. package/dist/auth-presets/supabase.d.ts +42 -0
  23. package/dist/auth-presets/supabase.js +229 -0
  24. package/dist/byo-approvals.d.ts +37 -0
  25. package/dist/byo-approvals.js +189 -0
  26. package/dist/capability-misses.d.ts +38 -0
  27. package/dist/capability-misses.js +172 -0
  28. package/dist/catalog.d.ts +19 -0
  29. package/dist/catalog.js +152 -0
  30. package/dist/cli/cloud/args.d.ts +3 -0
  31. package/dist/cli/cloud/args.js +32 -0
  32. package/dist/cli/cloud/auth.d.ts +17 -0
  33. package/dist/cli/cloud/auth.js +123 -0
  34. package/dist/cli/cloud/client.d.ts +28 -0
  35. package/dist/cli/cloud/client.js +133 -0
  36. package/dist/cli/cloud/command.d.ts +22 -0
  37. package/dist/cli/cloud/command.js +73 -0
  38. package/dist/cli/cloud/deploy.d.ts +26 -0
  39. package/dist/cli/cloud/deploy.js +187 -0
  40. package/dist/cli/cloud/device-login.d.ts +32 -0
  41. package/dist/cli/cloud/device-login.js +259 -0
  42. package/dist/cli/cloud/index.d.ts +5 -0
  43. package/dist/cli/cloud/index.js +59 -0
  44. package/dist/cli/cloud/keys.d.ts +2 -0
  45. package/dist/cli/cloud/keys.js +31 -0
  46. package/dist/cli/cloud/members.d.ts +3 -0
  47. package/dist/cli/cloud/members.js +25 -0
  48. package/dist/cli/cloud/output.d.ts +4 -0
  49. package/dist/cli/cloud/output.js +12 -0
  50. package/dist/cli/cloud/pending-claim.d.ts +29 -0
  51. package/dist/cli/cloud/pending-claim.js +38 -0
  52. package/dist/cli/cloud/read.d.ts +3 -0
  53. package/dist/cli/cloud/read.js +12 -0
  54. package/dist/cli/cloud/services.d.ts +2 -0
  55. package/dist/cli/cloud/services.js +9 -0
  56. package/dist/cli/cloud/session.d.ts +13 -0
  57. package/dist/cli/cloud/session.js +41 -0
  58. package/dist/cli/cloud-init.d.ts +60 -0
  59. package/dist/cli/cloud-init.js +149 -0
  60. package/dist/cli/dep-versions.d.ts +26 -0
  61. package/dist/cli/dep-versions.js +77 -0
  62. package/dist/cli/doctor-codes.d.ts +58 -0
  63. package/dist/cli/doctor-codes.js +60 -0
  64. package/dist/cli/doctor-live.d.ts +67 -0
  65. package/dist/cli/doctor-live.js +169 -0
  66. package/dist/cli/doctor.d.ts +52 -0
  67. package/dist/cli/doctor.js +467 -0
  68. package/dist/cli/eject.d.ts +29 -0
  69. package/dist/cli/eject.js +141 -0
  70. package/dist/cli/extract/apply.d.ts +26 -0
  71. package/dist/cli/extract/apply.js +123 -0
  72. package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
  73. package/dist/cli/extract/claude-cli-harness.js +132 -0
  74. package/dist/cli/extract/claude-harness.d.ts +14 -0
  75. package/dist/cli/extract/claude-harness.js +127 -0
  76. package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
  77. package/dist/cli/extract/codex-cli-harness.js +154 -0
  78. package/dist/cli/extract/delegate.d.ts +21 -0
  79. package/dist/cli/extract/delegate.js +77 -0
  80. package/dist/cli/extract/extraction.d.ts +65 -0
  81. package/dist/cli/extract/extraction.js +300 -0
  82. package/dist/cli/extract/gateway-fuel.d.ts +69 -0
  83. package/dist/cli/extract/gateway-fuel.js +69 -0
  84. package/dist/cli/extract/harness.d.ts +114 -0
  85. package/dist/cli/extract/harness.js +84 -0
  86. package/dist/cli/extract/index.d.ts +9 -0
  87. package/dist/cli/extract/index.js +9 -0
  88. package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
  89. package/dist/cli/extract/npx-engine-harness.js +225 -0
  90. package/dist/cli/extract/stages.d.ts +149 -0
  91. package/dist/cli/extract/stages.js +398 -0
  92. package/dist/cli/framework.d.ts +8 -0
  93. package/dist/cli/framework.js +36 -0
  94. package/dist/cli/init-auth.d.ts +67 -0
  95. package/dist/cli/init-auth.js +142 -0
  96. package/dist/cli/init-scaffolds.d.ts +28 -0
  97. package/dist/cli/init-scaffolds.js +235 -0
  98. package/dist/cli/init.d.ts +115 -0
  99. package/dist/cli/init.js +1008 -0
  100. package/dist/cli/mcp/index.d.ts +7 -0
  101. package/dist/cli/mcp/index.js +59 -0
  102. package/dist/cli/mcp/registry.d.ts +9 -0
  103. package/dist/cli/mcp/registry.js +117 -0
  104. package/dist/cli/mcp/server-json.d.ts +12 -0
  105. package/dist/cli/mcp/server-json.js +65 -0
  106. package/dist/cli/mcp/server.schema.json +574 -0
  107. package/dist/cli/mcp/verify-domain.d.ts +11 -0
  108. package/dist/cli/mcp/verify-domain.js +48 -0
  109. package/dist/cli/playground/app/embed-entry.d.ts +20 -0
  110. package/dist/cli/playground/app/embed-entry.js +46 -0
  111. package/dist/cli/playground/app/fake-client.d.ts +3 -0
  112. package/dist/cli/playground/app/fake-client.js +215 -0
  113. package/dist/cli/playground/app/fixtures.d.ts +47 -0
  114. package/dist/cli/playground/app/fixtures.js +472 -0
  115. package/dist/cli/playground/app/main.d.ts +1 -0
  116. package/dist/cli/playground/app/main.js +108 -0
  117. package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
  118. package/dist/cli/playground/app/scenario-mount.js +48 -0
  119. package/dist/cli/playground/app/scenarios.d.ts +25 -0
  120. package/dist/cli/playground/app/scenarios.js +152 -0
  121. package/dist/cli/playground/app/theme-editor.d.ts +13 -0
  122. package/dist/cli/playground/app/theme-editor.js +149 -0
  123. package/dist/cli/playground/app/theme-state.d.ts +29 -0
  124. package/dist/cli/playground/app/theme-state.js +151 -0
  125. package/dist/cli/playground/bundle.gen.d.ts +2 -0
  126. package/dist/cli/playground/bundle.gen.js +2 -0
  127. package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
  128. package/dist/cli/playground/embed-bundle.gen.js +2 -0
  129. package/dist/cli/playground.d.ts +33 -0
  130. package/dist/cli/playground.js +120 -0
  131. package/dist/cli/pretty.d.ts +48 -0
  132. package/dist/cli/pretty.js +330 -0
  133. package/dist/cli/refine.d.ts +43 -0
  134. package/dist/cli/refine.js +181 -0
  135. package/dist/cli/semantics.d.ts +31 -0
  136. package/dist/cli/semantics.js +122 -0
  137. package/dist/cli/shared.d.ts +68 -0
  138. package/dist/cli/shared.js +184 -0
  139. package/dist/cli/sync.d.ts +36 -0
  140. package/dist/cli/sync.js +202 -0
  141. package/dist/cli/theme/color.d.ts +15 -0
  142. package/dist/cli/theme/color.js +188 -0
  143. package/dist/cli/theme/css-vars.d.ts +13 -0
  144. package/dist/cli/theme/css-vars.js +40 -0
  145. package/dist/cli/theme/entry-candidates.d.ts +5 -0
  146. package/dist/cli/theme/entry-candidates.js +22 -0
  147. package/dist/cli/theme/extract-theme.d.ts +184 -0
  148. package/dist/cli/theme/extract-theme.js +358 -0
  149. package/dist/cli/theme/walk.d.ts +2 -0
  150. package/dist/cli/theme/walk.js +24 -0
  151. package/dist/cli.d.ts +2 -0
  152. package/dist/cli.js +333 -0
  153. package/dist/cloud-apps.d.ts +17 -0
  154. package/dist/cloud-apps.js +46 -0
  155. package/dist/cloud-console.d.ts +32 -0
  156. package/dist/cloud-console.js +72 -0
  157. package/dist/cloud-tools.d.ts +23 -0
  158. package/dist/cloud-tools.js +196 -0
  159. package/dist/connections.d.ts +71 -0
  160. package/dist/connections.js +192 -0
  161. package/dist/deployment-identity.d.ts +12 -0
  162. package/dist/deployment-identity.js +69 -0
  163. package/dist/dev-creds/model.d.ts +65 -0
  164. package/dist/dev-creds/model.js +159 -0
  165. package/dist/dev-creds/resolve.d.ts +36 -0
  166. package/dist/dev-creds/resolve.js +57 -0
  167. package/dist/hosted-store.d.ts +52 -0
  168. package/dist/hosted-store.js +322 -0
  169. package/dist/index.d.ts +13 -0
  170. package/dist/index.js +1 -0
  171. package/dist/mastra.d.ts +46 -0
  172. package/dist/mastra.js +88 -0
  173. package/dist/react.d.ts +10 -0
  174. package/dist/react.js +32 -0
  175. package/dist/refine.d.ts +244 -0
  176. package/dist/refine.js +599 -0
  177. package/dist/remixable.d.ts +18 -0
  178. package/dist/remixable.js +42 -0
  179. package/dist/runtime-capture.d.ts +18 -0
  180. package/dist/runtime-capture.js +111 -0
  181. package/dist/sandbox-wire.d.ts +101 -0
  182. package/dist/sandbox-wire.js +101 -0
  183. package/dist/sandbox.d.ts +38 -0
  184. package/dist/sandbox.js +389 -0
  185. package/dist/server.d.ts +216 -0
  186. package/dist/server.js +1320 -0
  187. package/dist/sync-impact.d.ts +14 -0
  188. package/dist/sync-impact.js +75 -0
  189. package/dist/turn-liveness.d.ts +33 -0
  190. package/dist/turn-liveness.js +105 -0
  191. package/dist/wire/approvals.d.ts +10 -0
  192. package/dist/wire/approvals.js +59 -0
  193. package/dist/wire/apps.d.ts +5 -0
  194. package/dist/wire/apps.js +155 -0
  195. package/dist/wire/automations.d.ts +5 -0
  196. package/dist/wire/automations.js +57 -0
  197. package/dist/wire/box.d.ts +3 -0
  198. package/dist/wire/box.js +268 -0
  199. package/dist/wire/connections.d.ts +5 -0
  200. package/dist/wire/connections.js +47 -0
  201. package/dist/wire/context.d.ts +22 -0
  202. package/dist/wire/context.js +190 -0
  203. package/dist/wire/doctor.d.ts +19 -0
  204. package/dist/wire/doctor.js +116 -0
  205. package/dist/wire/misc.d.ts +18 -0
  206. package/dist/wire/misc.js +227 -0
  207. package/dist/wire/shared.d.ts +158 -0
  208. package/dist/wire/shared.js +134 -0
  209. package/dist/wire/threads.d.ts +3 -0
  210. package/dist/wire/threads.js +68 -0
  211. package/package.json +151 -0
  212. package/skills/vendo-setup/SKILL.md +128 -0
@@ -0,0 +1,187 @@
1
+ import { appStore, createStore, grantStore, } from "@vendoai/store";
2
+ import { join } from "node:path";
3
+ import { option, options as repeatedOptions } from "./args.js";
4
+ import { CloudError, resolveCloudBaseUrl } from "./client.js";
5
+ import { commandContext, } from "./command.js";
6
+ import { errorMessage, formatTable, printJson } from "./output.js";
7
+ export async function readLocalProject(options = {}) {
8
+ const store = options.storeFactory?.() ?? createStore({
9
+ dataDir: join(options.cwd ?? process.cwd(), ".vendo/data"),
10
+ });
11
+ try {
12
+ await store.ensureSchema();
13
+ const driver = store.raw();
14
+ const subjectRows = await driver.query("SELECT DISTINCT subject FROM vendo_apps ORDER BY subject ASC");
15
+ const subjects = subjectRows.rows.map((row) => row.subject);
16
+ if (subjects.length === 0)
17
+ throw new Error("No local Vendo apps found in .vendo/data");
18
+ let subject = options.subject;
19
+ if (subject === undefined) {
20
+ if (subjects.length > 1) {
21
+ throw new Error(`Multiple local Vendo subjects found: ${subjects.join(", ")}. Pass --subject <subject>.`);
22
+ }
23
+ subject = subjects[0];
24
+ }
25
+ else if (!subjects.includes(subject)) {
26
+ throw new Error(`Subject ${subject} was not found in the local Vendo store`);
27
+ }
28
+ const appIds = await driver.query("SELECT id FROM vendo_apps WHERE subject = $1 ORDER BY created_at ASC, id ASC", [subject]);
29
+ const apps = (await Promise.all(appIds.rows.map(async ({ id }) => {
30
+ const row = await appStore(store).get(id);
31
+ if (row === null || row.subject !== subject)
32
+ return null;
33
+ return { doc: row.doc, enabled: row.enabled };
34
+ }))).filter((row) => row !== null);
35
+ const grantIds = await driver.query(`SELECT id FROM vendo_grants
36
+ WHERE subject = $1 AND source = 'automation'
37
+ AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())
38
+ ORDER BY granted_at ASC, id ASC`, [subject]);
39
+ const grants = (await Promise.all(grantIds.rows.map(({ id }) => grantStore(store).get(id))))
40
+ .filter((grant) => grant !== null);
41
+ return { subject, apps, grants };
42
+ }
43
+ finally {
44
+ await store.close();
45
+ }
46
+ }
47
+ function machineOptions(args, context) {
48
+ return {
49
+ auth: "key",
50
+ apiKey: option(args, "--key"),
51
+ apiUrl: option(args, "--api-url"),
52
+ env: context.env,
53
+ };
54
+ }
55
+ function secretValues(args) {
56
+ const values = new Map();
57
+ for (const pair of repeatedOptions(args, "--secret")) {
58
+ const separator = pair.indexOf("=");
59
+ if (separator <= 0)
60
+ throw new Error("--secret must use NAME=VALUE");
61
+ values.set(pair.slice(0, separator), pair.slice(separator + 1));
62
+ }
63
+ return values;
64
+ }
65
+ function selectedApps(project, args) {
66
+ const requested = [...new Set(repeatedOptions(args, "--app"))];
67
+ if (requested.length === 0) {
68
+ const selected = project.apps.filter((app) => app.enabled && app.doc.trigger !== undefined);
69
+ if (selected.length === 0)
70
+ throw new Error("No enabled local automations found");
71
+ return selected;
72
+ }
73
+ const requestedIds = new Set(requested);
74
+ for (const appId of requested) {
75
+ const app = project.apps.find((candidate) => candidate.doc.id === appId);
76
+ if (app === undefined)
77
+ throw new Error(`App ${appId} was not found for subject ${project.subject}`);
78
+ if (app.doc.trigger === undefined)
79
+ throw new Error(`App ${appId} is not an automation`);
80
+ }
81
+ return project.apps.filter((app) => requestedIds.has(app.doc.id));
82
+ }
83
+ function warnUnsupportedFnSteps(context, apps) {
84
+ for (const app of apps) {
85
+ const run = app.doc.trigger?.run;
86
+ if (run?.kind === "steps" && run.steps.some((step) => step.tool.startsWith("fn:"))) {
87
+ context.output.error(`WARNING: Automation ${app.doc.id} has fn: steps that target the app's machine, which is unreachable from hosted sandboxes in v1; those steps will fail/park when fired hosted.`);
88
+ }
89
+ }
90
+ }
91
+ function deployResponse(value) {
92
+ if (typeof value !== "object" || value === null)
93
+ throw new Error("Vendo Cloud returned an invalid deploy response");
94
+ const result = value;
95
+ if (typeof result.org?.id !== "string"
96
+ || typeof result.org.slug !== "string"
97
+ || typeof result.instance?.status !== "string"
98
+ || typeof result.applied?.apps !== "number"
99
+ || typeof result.applied.grants !== "number"
100
+ || typeof result.applied.secrets !== "number"
101
+ || !Array.isArray(result.webhooks)
102
+ || result.webhooks.some((webhook) => typeof webhook.app_id !== "string"
103
+ || typeof webhook.source !== "string"
104
+ || typeof webhook.url !== "string")) {
105
+ throw new Error("Vendo Cloud returned an invalid deploy response");
106
+ }
107
+ return result;
108
+ }
109
+ function printDeploySummary(context, response) {
110
+ const lines = [
111
+ `Vendo Cloud deploy: ${response.org.slug} (${response.instance.status})`,
112
+ "",
113
+ formatTable(["APPLIED", "COUNT"], [
114
+ ["apps", String(response.applied.apps)],
115
+ ["grants", String(response.applied.grants)],
116
+ ["secrets", String(response.applied.secrets)],
117
+ ]),
118
+ "",
119
+ ];
120
+ if (response.webhooks.length === 0) {
121
+ lines.push("Webhooks: none");
122
+ }
123
+ else {
124
+ lines.push(formatTable(["AUTOMATION", "SOURCE", "WEBHOOK URL"], response.webhooks.map((webhook) => [
125
+ webhook.app_id,
126
+ webhook.source,
127
+ webhook.url,
128
+ ])));
129
+ }
130
+ context.output.log(lines.join("\n"));
131
+ }
132
+ export async function runDeploy(args, options = {}) {
133
+ const context = commandContext(options);
134
+ try {
135
+ const project = await (options.localProjectReader ?? readLocalProject)({
136
+ subject: option(args, "--subject"),
137
+ cwd: options.cwd ?? process.cwd(),
138
+ });
139
+ const apps = selectedApps(project, args);
140
+ warnUnsupportedFnSteps(context, apps);
141
+ const appIds = new Set(apps.map((app) => app.doc.id));
142
+ const grants = project.grants.filter((grant) => grant.subject === project.subject
143
+ && grant.source === "automation"
144
+ && grant.appId !== undefined
145
+ && appIds.has(grant.appId));
146
+ const provided = secretValues(args);
147
+ const referenced = new Set();
148
+ for (const app of apps) {
149
+ for (const name of app.doc.secrets ?? []) {
150
+ referenced.add(name);
151
+ if (!provided.has(name)) {
152
+ context.output.error(`Automation ${app.doc.id} references missing secret ${name}; pass --secret ${name}=VALUE`);
153
+ }
154
+ }
155
+ }
156
+ const secrets = [...referenced]
157
+ .filter((name) => provided.has(name))
158
+ .map((name) => ({ name, value: provided.get(name) }));
159
+ const requestOptions = machineOptions(args, context);
160
+ if (secrets.length > 0 && new URL(resolveCloudBaseUrl(requestOptions)).protocol !== "https:") {
161
+ throw new Error("Deploying secret values requires an HTTPS Vendo Cloud URL");
162
+ }
163
+ const response = deployResponse(await context.fetcher("/api/v1/hosted/deploy", {
164
+ ...requestOptions,
165
+ method: "POST",
166
+ body: {
167
+ apps: apps.map((app) => ({ doc: app.doc, enabled: app.enabled })),
168
+ grants,
169
+ secrets,
170
+ },
171
+ }));
172
+ if (args.includes("--json"))
173
+ printJson(context.output, response);
174
+ else
175
+ printDeploySummary(context, response);
176
+ return 0;
177
+ }
178
+ catch (error) {
179
+ if (error instanceof CloudError && error.code === "cloud-required") {
180
+ context.output.error("This key's org needs a Cloud plan (cloud-required).");
181
+ }
182
+ else {
183
+ context.output.error(errorMessage(error));
184
+ }
185
+ return 1;
186
+ }
187
+ }
@@ -0,0 +1,32 @@
1
+ import { type Output, type TelemetryOptions } from "../shared.js";
2
+ export interface DeviceLoginOptions {
3
+ output?: Output;
4
+ fetchImpl?: typeof fetch;
5
+ env?: Record<string, string | undefined>;
6
+ /** Where .env.local lives (default: the current working directory). */
7
+ root?: string;
8
+ /** Injectable pacing seam — tests run the ceremony in microseconds. */
9
+ sleep?: (ms: number) => Promise<void>;
10
+ now?: () => number;
11
+ /** TTY seam — a watching human gets the browser opened for them; a non-TTY
12
+ (agent) caller keeps the URL + code contract untouched. */
13
+ isTty?: boolean;
14
+ openBrowser?: (url: string) => void;
15
+ /** init runs the ceremony inline and picks the key up in the same run —
16
+ it suppresses the standalone "re-run `vendo init`" tail. */
17
+ rerunHint?: boolean;
18
+ /** Where ~/.vendo lives (default: the home directory) — the pending-claim
19
+ file that lets a fresh run resume a still-open ceremony (#479). */
20
+ home?: string;
21
+ }
22
+ /**
23
+ * `vendo login` — the top-level command surface: the identical ceremony
24
+ * wrapped in one `command_run` row (command "login", TELEMETRY.md). The
25
+ * ceremony's other two callers stay untracked here: `vendo cloud
26
+ * device-login` (the alias) calls runDeviceLogin directly, and init's
27
+ * embedded step already tracks itself as "cloud-init".
28
+ */
29
+ export declare function runLoginCommand(args: string[], options?: DeviceLoginOptions & {
30
+ telemetry?: TelemetryOptions;
31
+ }): Promise<number>;
32
+ export declare function runDeviceLogin(args: string[], options?: DeviceLoginOptions): Promise<number>;
@@ -0,0 +1,259 @@
1
+ import { execFile } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { option, positionals } from "./args.js";
4
+ import { isVendoKey, resolveCloudBaseUrl } from "./client.js";
5
+ import { errorMessage, printJson } from "./output.js";
6
+ import { deletePendingClaim, readPendingClaim, writePendingClaim } from "./pending-claim.js";
7
+ import { upsertEnvLocal } from "../cloud-init.js";
8
+ import { browserOpenCommand } from "../playground.js";
9
+ import { CLI_VERSION, consoleOutput, withCommandRun } from "../shared.js";
10
+ /**
11
+ * `vendo login` (alias: `vendo cloud device-login`) — the auth.md
12
+ * user-claimed flow end to end (https://vendo.run/auth.md): open a claim on
13
+ * the console, show the human the pairing code + approval URL (a TTY gets
14
+ * the browser opened too), poll the RFC 8628 token endpoint, and land the
15
+ * minted VENDO_API_KEY in .env.local — exactly where init's --cloud-key flag
16
+ * and the interactive ceremony put it, so a re-run of `vendo init` picks it
17
+ * up with no key ever pasted or printed.
18
+ *
19
+ * The token endpoint speaks RFC 8628 §3.5 (top-level `error` string), which
20
+ * the console-envelope-shaped cloudFetch would flatten to http-400 — so this
21
+ * command talks to both endpoints with a raw (injectable) fetch instead.
22
+ */
23
+ const CLAIM_PATH = "/api/v1/agent/claim";
24
+ const TOKEN_PATH = "/api/v1/oauth/token";
25
+ const CLAIM_GRANT_TYPE = "urn:workos:agent-auth:grant-type:claim";
26
+ function defaultOpenBrowser(url) {
27
+ const { command, args } = browserOpenCommand(process.platform, url);
28
+ execFile(command, args, () => undefined); // best-effort: the printed URL is the fallback
29
+ }
30
+ function ceremonyFrom(value) {
31
+ const body = value;
32
+ if (typeof body?.claim_token !== "string"
33
+ || typeof body.user_code !== "string"
34
+ || typeof body.verification_uri !== "string"
35
+ || typeof body.expires_in !== "number"
36
+ || typeof body.interval !== "number") {
37
+ throw new Error("Vendo Cloud returned an invalid claim ceremony");
38
+ }
39
+ return body;
40
+ }
41
+ async function postJson(fetchImpl, url, contentType, body) {
42
+ const response = await fetchImpl(url, {
43
+ method: "POST",
44
+ headers: {
45
+ accept: "application/json",
46
+ "content-type": contentType,
47
+ "user-agent": `vendo-cli/${CLI_VERSION}`,
48
+ },
49
+ body,
50
+ });
51
+ const text = await response.text();
52
+ let parsed;
53
+ try {
54
+ parsed = text.length === 0 ? undefined : JSON.parse(text);
55
+ }
56
+ catch {
57
+ parsed = text;
58
+ }
59
+ return { status: response.status, body: parsed };
60
+ }
61
+ /**
62
+ * `vendo login` — the top-level command surface: the identical ceremony
63
+ * wrapped in one `command_run` row (command "login", TELEMETRY.md). The
64
+ * ceremony's other two callers stay untracked here: `vendo cloud
65
+ * device-login` (the alias) calls runDeviceLogin directly, and init's
66
+ * embedded step already tracks itself as "cloud-init".
67
+ */
68
+ export async function runLoginCommand(args, options = {}) {
69
+ return withCommandRun({
70
+ command: "login",
71
+ // Where the key lands is the project the ceremony is for.
72
+ root: options.root ?? process.cwd(),
73
+ ...(options.telemetry === undefined ? {} : { telemetry: options.telemetry }),
74
+ }, () => runDeviceLogin(args, options));
75
+ }
76
+ export async function runDeviceLogin(args, options = {}) {
77
+ const output = options.output ?? consoleOutput;
78
+ const fetchImpl = options.fetchImpl ?? fetch;
79
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
80
+ const now = options.now ?? Date.now;
81
+ const base = resolveCloudBaseUrl({
82
+ apiUrl: option(args, "--api-url"),
83
+ env: options.env ?? process.env,
84
+ });
85
+ // `--wait <seconds>` (#479): the MAX wall-clock THIS invocation polls before
86
+ // giving up resumably — independent of, and capped by, the claim deadline.
87
+ // Absent: unchanged, block until the claim deadline (~10 min). `--wait 0`:
88
+ // one immediate poll, then exit resumably if still pending. An invalid value
89
+ // is ignored here (the `vendo login` entry validates it up front).
90
+ const waitRaw = option(args, "--wait");
91
+ const waitSeconds = waitRaw !== undefined && /^\d+$/.test(waitRaw) ? Number(waitRaw) : undefined;
92
+ const waitMs = waitSeconds === undefined ? undefined : waitSeconds * 1000;
93
+ const pendingHome = options.home === undefined ? {} : { home: options.home };
94
+ try {
95
+ // A still-open claim from a dead process (#479): resume polling it so the
96
+ // human's late approval — against the code they were already shown —
97
+ // still lands the key. An expired or unreadable file is discarded (a
98
+ // fresh ceremony overwrites it below).
99
+ const pending = await readPendingClaim(pendingHome);
100
+ const resume = pending !== null && pending.api_url === base && pending.expires_at > now()
101
+ ? pending
102
+ : null;
103
+ let claimToken;
104
+ let deadline;
105
+ let intervalMs;
106
+ let root;
107
+ let userCode;
108
+ let verificationUriComplete;
109
+ if (resume !== null) {
110
+ output.log(`Resuming pending approval — code ${resume.user_code}, approve at ${resume.verification_uri_complete}`);
111
+ output.log(`Waiting for approval (the code expires in ${Math.max(1, Math.round((resume.expires_at - now()) / 60_000))} minutes)…`);
112
+ claimToken = resume.claim_token;
113
+ deadline = resume.expires_at;
114
+ intervalMs = Math.max(resume.interval, 1) * 1000;
115
+ // The key lands where the ORIGINAL run intended, not where the resume runs.
116
+ root = resume.cwd;
117
+ userCode = resume.user_code;
118
+ verificationUriComplete = resume.verification_uri_complete;
119
+ }
120
+ else {
121
+ // Optional email hint — shown to the human on the approval page.
122
+ const email = option(args, "--email") ?? positionals(args, ["--api-url", "--email", "--wait"])[0];
123
+ const claim = await postJson(fetchImpl, `${base}${CLAIM_PATH}`, "application/json", JSON.stringify(email === undefined ? {} : { login_hint: email }));
124
+ if (claim.status !== 200) {
125
+ const envelope = claim.body;
126
+ throw new Error(typeof envelope?.error?.message === "string"
127
+ ? envelope.error.message
128
+ : `Vendo Cloud could not open a claim (${claim.status})`);
129
+ }
130
+ const ceremony = ceremonyFrom(claim.body);
131
+ const approvalUrl = ceremony.verification_uri_complete ?? ceremony.verification_uri;
132
+ output.log("Vendo Cloud device login — ask your human to approve this request:");
133
+ output.log(` 1. Open ${approvalUrl}`);
134
+ output.log(` 2. Confirm the code: ${ceremony.user_code}`);
135
+ const tty = options.isTty ?? (process.stdout.isTTY === true);
136
+ if (tty) {
137
+ output.log("Opening your browser… (approve there, then come back here)");
138
+ (options.openBrowser ?? defaultOpenBrowser)(approvalUrl);
139
+ }
140
+ output.log(`Waiting for approval (the code expires in ${Math.round(ceremony.expires_in / 60)} minutes)…`);
141
+ claimToken = ceremony.claim_token;
142
+ deadline = now() + ceremony.expires_in * 1000;
143
+ intervalMs = Math.max(ceremony.interval, 1) * 1000;
144
+ root = options.root ?? process.cwd();
145
+ userCode = ceremony.user_code;
146
+ verificationUriComplete = approvalUrl;
147
+ // Persist the ceremony so a fresh run can resume it if this process
148
+ // dies mid-poll. Deleted on redemption/denial/expiry; deliberately left
149
+ // in place on transient errors and interrupts.
150
+ await writePendingClaim({
151
+ claim_token: claimToken,
152
+ user_code: ceremony.user_code,
153
+ verification_uri_complete: approvalUrl,
154
+ expires_at: deadline,
155
+ interval: ceremony.interval,
156
+ api_url: base,
157
+ cwd: root,
158
+ }, pendingHome);
159
+ }
160
+ const pollBody = new URLSearchParams({
161
+ grant_type: CLAIM_GRANT_TYPE,
162
+ claim_token: claimToken,
163
+ }).toString();
164
+ if (waitMs !== undefined) {
165
+ output.log(`This call polls for up to ${waitSeconds}s — re-run \`vendo login\` to continue this same request.`);
166
+ }
167
+ const pollOnce = async () => {
168
+ const poll = await postJson(fetchImpl, `${base}${TOKEN_PATH}`, "application/x-www-form-urlencoded", pollBody);
169
+ if (poll.status === 200) {
170
+ const key = poll.body?.access_token;
171
+ if (typeof key !== "string" || !isVendoKey(key)) {
172
+ throw new Error("Vendo Cloud returned an invalid credential");
173
+ }
174
+ await upsertEnvLocal(root, "VENDO_API_KEY", key);
175
+ await deletePendingClaim(pendingHome);
176
+ // Never print the key itself — .env.local is the hand-off, last4 the
177
+ // receipt. A resumed run names the full path: it may differ from cwd.
178
+ output.log(`Approved — wrote VENDO_API_KEY (…${key.slice(-4)}) to ${resume !== null ? join(root, ".env.local") : ".env.local"}.`);
179
+ if (options.rerunHint !== false) {
180
+ output.log("Re-run `vendo init` to finish wiring (it picks the key up from .env.local).");
181
+ }
182
+ printJson(output, { deviceLogin: true, wroteEnvLocal: true, keyLast4: key.slice(-4) });
183
+ return "approved";
184
+ }
185
+ const error = poll.body?.error;
186
+ if (error === "authorization_pending")
187
+ return "pending";
188
+ if (error === "slow_down")
189
+ return "slow_down"; // RFC 8628 §3.5
190
+ if (error === "expired_token") {
191
+ await deletePendingClaim(pendingHome);
192
+ throw new Error("The code expired before it was approved; run `vendo login` again.");
193
+ }
194
+ if (error === "access_denied") {
195
+ await deletePendingClaim(pendingHome);
196
+ throw new Error("Your human denied the request — no key was minted.");
197
+ }
198
+ // Any other token error is terminal for THIS claim (invalid_grant =
199
+ // consumed/denied server-side; single-use means it can never succeed
200
+ // again). Delete the pending file so the next `vendo login` opens a
201
+ // fresh claim instead of resuming into the same error forever — the
202
+ // exact trap a live install hit.
203
+ await deletePendingClaim(pendingHome);
204
+ const description = poll.body?.error_description;
205
+ throw new Error(typeof description === "string"
206
+ ? description
207
+ : `Vendo Cloud token polling failed (${typeof error === "string" ? error : poll.status})`);
208
+ };
209
+ // Budget still pending: leave the claim file in place and exit 0 — pending
210
+ // is not a failure. A re-run resumes this same claim (#479).
211
+ const pendingExit = () => {
212
+ output.log(`Still waiting on approval — code ${userCode}. Re-run \`vendo login\` to resume (it continues this same request).`);
213
+ printJson(output, {
214
+ deviceLogin: true,
215
+ pending: true,
216
+ userCode,
217
+ verificationUriComplete,
218
+ });
219
+ return 0;
220
+ };
221
+ if (waitMs === undefined) {
222
+ // No budget: block to the claim deadline — unchanged TTY behavior.
223
+ while (now() < deadline) {
224
+ await sleep(intervalMs);
225
+ const result = await pollOnce();
226
+ if (result === "approved")
227
+ return 0;
228
+ if (result === "slow_down")
229
+ intervalMs += 5000;
230
+ }
231
+ await deletePendingClaim(pendingHome);
232
+ throw new Error("The code expired before it was approved; run `vendo login` again.");
233
+ }
234
+ // Bounded budget: poll immediately, then pace by interval, stopping at
235
+ // min(now+wait, deadline). `--wait 0` polls exactly once.
236
+ const pollDeadline = Math.min(deadline, now() + waitMs);
237
+ while (true) {
238
+ const result = await pollOnce();
239
+ if (result === "approved")
240
+ return 0;
241
+ if (result === "slow_down")
242
+ intervalMs += 5000;
243
+ if (now() >= deadline) {
244
+ await deletePendingClaim(pendingHome);
245
+ throw new Error("The code expired before it was approved; run `vendo login` again.");
246
+ }
247
+ if (now() >= pollDeadline)
248
+ return pendingExit();
249
+ // Cap the wait to the remaining budget so a small `--wait` (e.g. 1s
250
+ // against the default 5s interval) exits within its bound instead of
251
+ // sleeping a whole interval past it.
252
+ await sleep(Math.min(intervalMs, pollDeadline - now()));
253
+ }
254
+ }
255
+ catch (error) {
256
+ output.error(errorMessage(error));
257
+ return 1;
258
+ }
259
+ }
@@ -0,0 +1,5 @@
1
+ import { type CloudAuthOptions } from "./auth.js";
2
+ import type { CloudCommandOptions } from "./command.js";
3
+ import { type DeviceLoginOptions } from "./device-login.js";
4
+ export type RunCloudOptions = CloudCommandOptions & CloudAuthOptions & DeviceLoginOptions;
5
+ export declare function runCloud(args: string[], options?: RunCloudOptions): Promise<number>;
@@ -0,0 +1,59 @@
1
+ import { runLogin, runLogout, runWhoami } from "./auth.js";
2
+ import { runDeviceLogin } from "./device-login.js";
3
+ import { runKeys } from "./keys.js";
4
+ import { runInvite, runMembers } from "./members.js";
5
+ import { consoleOutput } from "../shared.js";
6
+ import { runOrgs, runUsage } from "./read.js";
7
+ const CLOUD_HELP = `vendo cloud — Vendo Cloud API client
8
+
9
+ Usage: vendo cloud <command> [options]
10
+
11
+ User commands:
12
+ device-login [EMAIL] alias of \`vendo login\` — the auth.md user-claimed
13
+ flow: your human approves a code in the browser;
14
+ the minted VENDO_API_KEY is written to .env.local
15
+ (never printed)
16
+ login EMAIL Fallback: send an email OTP (6-10 digits) and prompt for it
17
+ login --token <jwt> Fallback: store an access token directly
18
+ logout Delete the stored cloud session
19
+ whoami [--token <jwt>] List organizations for the current user
20
+ orgs List organizations
21
+ keys list --project <id> List API keys
22
+ keys create --project <id> --name <name> Create an API key
23
+ keys revoke --project <id> --id <keyId> Revoke an API key
24
+ usage --project <id> [--days <days>] Show usage (default 30 days)
25
+ members --org <id> List organization members
26
+ invite --org <id> --email <email> --role <admin|member>
27
+
28
+ Global options:
29
+ --api-url <url> Override VENDO_CLOUD_URL / https://console.vendo.run
30
+ --json JSON output
31
+ `;
32
+ export async function runCloud(args, options = {}) {
33
+ const [command, ...commandArgs] = args;
34
+ const output = options.output ?? consoleOutput;
35
+ if (command === undefined || command === "--help" || command === "-h" || command === "help") {
36
+ output.log(CLOUD_HELP);
37
+ return 0;
38
+ }
39
+ if (command === "login")
40
+ return runLogin(commandArgs, options);
41
+ if (command === "device-login")
42
+ return runDeviceLogin(commandArgs, options);
43
+ if (command === "logout")
44
+ return runLogout(commandArgs, options);
45
+ if (command === "whoami")
46
+ return runWhoami(commandArgs, options);
47
+ if (command === "orgs")
48
+ return runOrgs(commandArgs, options);
49
+ if (command === "keys")
50
+ return runKeys(commandArgs, options);
51
+ if (command === "usage")
52
+ return runUsage(commandArgs, options);
53
+ if (command === "members")
54
+ return runMembers(commandArgs, options);
55
+ if (command === "invite")
56
+ return runInvite(commandArgs, options);
57
+ output.error(`Unknown cloud command: ${command}\n\n${CLOUD_HELP}`);
58
+ return 1;
59
+ }
@@ -0,0 +1,2 @@
1
+ import { type CloudCommandOptions } from "./command.js";
2
+ export declare function runKeys(args: string[], options?: CloudCommandOptions): Promise<number>;
@@ -0,0 +1,31 @@
1
+ import { option } from "./args.js";
2
+ import { resolveProjectId, runCommand, userOptions, } from "./command.js";
3
+ export function runKeys(args, options = {}) {
4
+ return runCommand(options, async (context) => {
5
+ const [action] = args;
6
+ if (!action || !["list", "create", "revoke"].includes(action)) {
7
+ throw new Error("Usage: vendo cloud keys <list|create|revoke> --project <id>");
8
+ }
9
+ const projectId = await resolveProjectId(args, context);
10
+ const root = `/api/v1/projects/${encodeURIComponent(projectId)}/keys`;
11
+ if (action === "list")
12
+ return context.fetcher(root, userOptions(args, context));
13
+ if (action === "create") {
14
+ const name = option(args, "--name");
15
+ if (!name)
16
+ throw new Error("Key creation requires --name <name>");
17
+ return context.fetcher(root, {
18
+ ...userOptions(args, context),
19
+ method: "POST",
20
+ body: { name },
21
+ });
22
+ }
23
+ const keyId = option(args, "--id");
24
+ if (!keyId)
25
+ throw new Error("Key revocation requires --id <keyId>");
26
+ return context.fetcher(`${root}/${encodeURIComponent(keyId)}/revoke`, {
27
+ ...userOptions(args, context),
28
+ method: "POST",
29
+ });
30
+ });
31
+ }
@@ -0,0 +1,3 @@
1
+ import { type CloudCommandOptions } from "./command.js";
2
+ export declare function runMembers(args: string[], options?: CloudCommandOptions): Promise<number>;
3
+ export declare function runInvite(args: string[], options?: CloudCommandOptions): Promise<number>;
@@ -0,0 +1,25 @@
1
+ import { option } from "./args.js";
2
+ import { resolveOrgId, runCommand, userOptions, } from "./command.js";
3
+ export function runMembers(args, options = {}) {
4
+ return runCommand(options, async (context) => {
5
+ const orgId = await resolveOrgId(args, context);
6
+ return context.fetcher(`/api/v1/orgs/${encodeURIComponent(orgId)}/members`, userOptions(args, context));
7
+ });
8
+ }
9
+ export function runInvite(args, options = {}) {
10
+ return runCommand(options, async (context) => {
11
+ const email = option(args, "--email");
12
+ const role = option(args, "--role");
13
+ if (!email)
14
+ throw new Error("Inviting a member requires --email <email>");
15
+ if (role !== "admin" && role !== "member") {
16
+ throw new Error("Inviting a member requires --role <admin|member>");
17
+ }
18
+ const orgId = await resolveOrgId(args, context);
19
+ return context.fetcher(`/api/v1/orgs/${encodeURIComponent(orgId)}/invites`, {
20
+ ...userOptions(args, context),
21
+ method: "POST",
22
+ body: { email, role },
23
+ });
24
+ });
25
+ }
@@ -0,0 +1,4 @@
1
+ import type { Output } from "../shared.js";
2
+ export declare function printJson(output: Output, value: unknown): void;
3
+ export declare function formatTable(headers: string[], rows: string[][]): string;
4
+ export declare function errorMessage(error: unknown): string;
@@ -0,0 +1,12 @@
1
+ export function printJson(output, value) {
2
+ output.log(JSON.stringify(value, null, 2));
3
+ }
4
+ export function formatTable(headers, rows) {
5
+ const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)));
6
+ return [headers, ...rows]
7
+ .map((row) => row.map((cell, column) => cell.padEnd(widths[column] ?? cell.length)).join(" ").trimEnd())
8
+ .join("\n");
9
+ }
10
+ export function errorMessage(error) {
11
+ return error instanceof Error ? error.message : "Vendo Cloud request failed";
12
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The pending-claim file (#479): a `vendo login` claim outlives the process
3
+ * that opened it. Coding agents' processes often live exactly one turn — if
4
+ * the poller dies and the human approves afterwards, the approval succeeds
5
+ * server-side but the claim_token existed only in the dead process's memory.
6
+ * Persisting the claim lets a fresh `vendo login` resume polling the SAME
7
+ * claim, so the late approval still lands the key.
8
+ */
9
+ export interface PendingClaim {
10
+ claim_token: string;
11
+ user_code: string;
12
+ verification_uri_complete: string;
13
+ /** Unix timestamp in milliseconds — computed from the ceremony's expires_in. */
14
+ expires_at: number;
15
+ /** Poll pacing in seconds (RFC 8628). */
16
+ interval: number;
17
+ /** The console the claim was opened against — resume only on a match. */
18
+ api_url: string;
19
+ /** Where the original run intended .env.local to land. */
20
+ cwd: string;
21
+ }
22
+ export interface PendingClaimOptions {
23
+ home?: string;
24
+ }
25
+ /** An unreadable or malformed file reads as "no pending claim" — the caller
26
+ discards it by opening (and persisting) a fresh ceremony. */
27
+ export declare function readPendingClaim(options?: PendingClaimOptions): Promise<PendingClaim | null>;
28
+ export declare function writePendingClaim(claim: PendingClaim, options?: PendingClaimOptions): Promise<void>;
29
+ export declare function deletePendingClaim(options?: PendingClaimOptions): Promise<void>;