@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,123 @@
1
+ import { join, resolve } from "node:path";
2
+ import { z } from "zod";
3
+ import { vendoSync } from "@vendoai/actions";
4
+ import { detectDepVersions } from "../dep-versions.js";
5
+ import { detectFramework } from "../framework.js";
6
+ import { applyDraft, reportApplied } from "./extraction.js";
7
+ import { parseDraft } from "./harness.js";
8
+ import { staticToolSchema } from "./stages.js";
9
+ import { consoleOutput, readOptional, withCommandRun, } from "../shared.js";
10
+ function describeError(error) {
11
+ if (error instanceof z.ZodError) {
12
+ return error.issues
13
+ .slice(0, 5)
14
+ .map((issue) => `${issue.path.join(".") || "draft"}: ${issue.message}`)
15
+ .join("; ");
16
+ }
17
+ return error instanceof Error ? error.message : "unknown error";
18
+ }
19
+ export async function runExtractApply(options) {
20
+ const output = options.output ?? consoleOutput;
21
+ const root = resolve(options.targetDir);
22
+ const started = Date.now();
23
+ return withCommandRun({
24
+ command: "extract",
25
+ root,
26
+ ...(options.telemetry === undefined ? {} : { telemetry: options.telemetry }),
27
+ }, (failure, telemetry) => extractApply(options, output, root, started, failure, telemetry));
28
+ }
29
+ async function extractApply(options, output, root, started, failure, telemetry) {
30
+ const toolsRaw = await readOptional(join(root, ".vendo", "tools.json"));
31
+ if (toolsRaw === null) {
32
+ failure.failedStep = "tools";
33
+ output.error("No .vendo/tools.json here — run `vendo init` first, then apply the draft.");
34
+ return 1;
35
+ }
36
+ let tools;
37
+ try {
38
+ tools = z.object({ tools: z.array(staticToolSchema) }).parse(JSON.parse(toolsRaw)).tools;
39
+ }
40
+ catch (error) {
41
+ failure.failedStep = "tools";
42
+ output.error(`.vendo/tools.json is unreadable (${describeError(error)}) — run \`vendo sync\` and try again.`);
43
+ return 1;
44
+ }
45
+ // readOptional only maps ENOENT to null — a directory (EISDIR) or a
46
+ // permission error must still exit honestly, not as an uncaught crash.
47
+ let draftRaw;
48
+ try {
49
+ draftRaw = await readOptional(resolve(options.apply));
50
+ }
51
+ catch (error) {
52
+ failure.failedStep = "draft";
53
+ output.error(`Draft file unreadable: ${options.apply} (${describeError(error)})`);
54
+ return 1;
55
+ }
56
+ if (draftRaw === null) {
57
+ failure.failedStep = "draft";
58
+ output.error(`Draft file not found: ${options.apply}`);
59
+ return 1;
60
+ }
61
+ let draft;
62
+ try {
63
+ draft = parseDraft(draftRaw);
64
+ }
65
+ catch (error) {
66
+ failure.failedStep = "draft";
67
+ output.error(`Draft rejected — it must match the aiPolish.draftSchema from \`vendo init --agent\` (${describeError(error)}).`);
68
+ return 1;
69
+ }
70
+ // applyDraft throws on a hand-edited overrides.json it cannot parse — the
71
+ // same honest-exit contract applies, never an unhandled stack trace.
72
+ let applied;
73
+ try {
74
+ applied = await applyDraft({
75
+ root,
76
+ draft,
77
+ tools,
78
+ ...(options.force === undefined ? {} : { force: options.force }),
79
+ });
80
+ }
81
+ catch (error) {
82
+ failure.failedStep = "apply";
83
+ output.error(`Could not apply the draft (${describeError(error)}) — check .vendo/overrides.json and re-apply.`);
84
+ return 1;
85
+ }
86
+ // Same as init's built-in pass: a successful apply re-syncs so tools.json
87
+ // reflects the polish immediately.
88
+ try {
89
+ const resynced = await (options.sync ?? vendoSync)({ root, out: join(root, ".vendo") });
90
+ for (const warning of resynced.warnings)
91
+ output.error(`warning: ${warning}`);
92
+ }
93
+ catch (error) {
94
+ failure.failedStep = "resync";
95
+ output.error(`Draft applied, but re-sync failed (${describeError(error)}) — run \`vendo sync\` to refresh tools.json.`);
96
+ return 1;
97
+ }
98
+ reportApplied({ output, applied, briefDrafted: applied.briefWritten });
99
+ // Result metrics (counts and enums only; TELEMETRY.md extract_completed).
100
+ // Guarded like the wrapper: telemetry can never change the exit code.
101
+ try {
102
+ const framework = await detectFramework(root);
103
+ // staticToolSchema strips bindings — count route tools off the raw file.
104
+ const bindings = JSON.parse(toolsRaw).tools ?? [];
105
+ const routeCount = bindings.filter((tool) => tool.binding?.kind === "route").length;
106
+ const versions = await detectDepVersions(root, framework);
107
+ await telemetry.track("extract_completed", {
108
+ framework,
109
+ // route-scan today; "zod" reserved for a future oracle-backed detect.
110
+ method: routeCount > 0 ? "route-scan" : "none",
111
+ routeCount,
112
+ toolCount: tools.length,
113
+ ok: true,
114
+ durationMs: Date.now() - started,
115
+ ...(versions.frameworkVersion === undefined ? {} : { frameworkVersion: versions.frameworkVersion }),
116
+ ...(versions.zodVersion === undefined ? {} : { zodVersion: versions.zodVersion }),
117
+ });
118
+ }
119
+ catch {
120
+ // Intentional silent failure.
121
+ }
122
+ return 0;
123
+ }
@@ -0,0 +1,18 @@
1
+ import type { ExtractionHarness } from "./harness.js";
2
+ interface ExecResult {
3
+ stdout: string;
4
+ stderr: string;
5
+ code: number;
6
+ }
7
+ type Exec = (args: string[], options: {
8
+ cwd: string;
9
+ env: NodeJS.ProcessEnv;
10
+ }) => Promise<ExecResult>;
11
+ export interface ClaudeCliHarnessOptions {
12
+ /** Test seams. */
13
+ probeBinary?: () => Promise<boolean>;
14
+ probeLogin?: () => Promise<boolean>;
15
+ exec?: Exec;
16
+ }
17
+ export declare function claudeCliHarness(options?: ClaudeCliHarnessOptions): ExtractionHarness;
18
+ export {};
@@ -0,0 +1,132 @@
1
+ import { execFile } from "node:child_process";
2
+ import { composeGatewayFuel, hasOwnAnthropicEnvOverride } from "./gateway-fuel.js";
3
+ /**
4
+ * Fallback extraction harness: the `claude` CLI already on the dev's PATH,
5
+ * driven headless (`-p`/print mode) with READ-ONLY code tools (Read/Glob/
6
+ * Grep) over the host root. Credential = the dev's Claude Code login, or
7
+ * their ANTHROPIC_API_KEY — same model as the SDK harness, just spawned as a
8
+ * subprocess instead of imported as a module. This is the path that works
9
+ * without shipping the Claude Agent SDK's ~245MB bundled native binary
10
+ * (see claude-harness.ts) — most devs doing this already have Claude Code
11
+ * installed for their own use.
12
+ *
13
+ * Isolation: `--setting-sources ""` (never inherit the dev's personal Claude
14
+ * Code settings/hooks — same intent as claude-harness.ts's
15
+ * `settingSources: []`), read-only tool allowlist, no shell/web/write surface.
16
+ *
17
+ * Gateway fuel: when the dev has none of ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN,
18
+ * CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_BASE_URL (the corporate-gateway/custom-
19
+ * endpoint path), or a Claude Code login, but VENDO_API_KEY is set, the rung
20
+ * runs on Vendo Cloud's model gateway instead of degrading to unavailable
21
+ * (see gateway-fuel.ts). Own credential always wins — this never overrides a
22
+ * working ANTHROPIC_API_KEY, login, or any of those env vars.
23
+ */
24
+ const ALLOWED_TOOLS = ["Read", "Glob", "Grep"];
25
+ const DISALLOWED_TOOLS = [
26
+ "Bash", "Write", "Edit", "WebFetch", "WebSearch", "Task",
27
+ "TodoWrite", "NotebookEdit", "KillShell", "BashOutput",
28
+ ];
29
+ const PROBE_TIMEOUT_MS = 5_000;
30
+ // Extraction stages can run for minutes over a real codebase.
31
+ const RUN_TIMEOUT_MS = 10 * 60 * 1_000;
32
+ const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
33
+ function execClaude(args, options) {
34
+ return new Promise((resolve) => {
35
+ execFile("claude", args, { cwd: options.cwd, env: options.env, timeout: RUN_TIMEOUT_MS, maxBuffer: MAX_BUFFER_BYTES }, (error, stdout, stderr) => {
36
+ const code = error === null ? 0 : typeof error.code === "number" ? error.code : 1;
37
+ resolve({ stdout, stderr, code });
38
+ });
39
+ });
40
+ }
41
+ /** Cheap presence check: does `claude` resolve and run at all? */
42
+ function probeClaudeBinary() {
43
+ return new Promise((resolve) => {
44
+ execFile("claude", ["--version"], { timeout: PROBE_TIMEOUT_MS }, (error) => resolve(error === null));
45
+ });
46
+ }
47
+ /** `claude auth status` prints JSON with a `loggedIn` boolean. */
48
+ function probeClaudeLogin() {
49
+ return new Promise((resolve) => {
50
+ execFile("claude", ["auth", "status"], { timeout: PROBE_TIMEOUT_MS }, (error, stdout) => {
51
+ if (error !== null)
52
+ return resolve(false);
53
+ try {
54
+ resolve(JSON.parse(stdout).loggedIn === true);
55
+ }
56
+ catch {
57
+ resolve(false);
58
+ }
59
+ });
60
+ });
61
+ }
62
+ function isSet(value) {
63
+ return typeof value === "string" && value.trim().length > 0;
64
+ }
65
+ /** Gateway fuel only ever matters when VENDO_API_KEY is set — skip the
66
+ * (async) login probe entirely otherwise, so the own-credential path never
67
+ * pays for or is affected by this check. Env-based own-credential checks
68
+ * (ANTHROPIC_API_KEY, then OWN_CREDENTIAL_ENV_VARS) run before the probe
69
+ * too, since they're cheap and the probe is a subprocess spawn. */
70
+ async function resolveGatewayFuelOverlay(env, probeLogin) {
71
+ const cloudKey = env["VENDO_API_KEY"];
72
+ if (!isSet(cloudKey))
73
+ return null;
74
+ const ownCredentialAvailable = isSet(env["ANTHROPIC_API_KEY"])
75
+ || hasOwnAnthropicEnvOverride(env)
76
+ || (await probeLogin());
77
+ return composeGatewayFuel({ env, ownCredentialAvailable });
78
+ }
79
+ export function claudeCliHarness(options = {}) {
80
+ const hasBinary = options.probeBinary ?? probeClaudeBinary;
81
+ const probe = options.probeLogin ?? probeClaudeLogin;
82
+ const exec = options.exec ?? execClaude;
83
+ return {
84
+ id: "claude-cli",
85
+ async availability({ env }) {
86
+ if (!(await hasBinary()))
87
+ return null;
88
+ // The child spawns with {...process.env, ...input.env} (see run()), so
89
+ // credential checks and labels must see that SAME merged view: a
90
+ // programmatic caller passing a partial env (say VENDO_API_KEY only)
91
+ // while ambient process.env carries an ANTHROPIC_AUTH_TOKEN really
92
+ // runs on that ambient token — labeling (or fueling) it as the Vendo
93
+ // Cloud key would lie about, or worse clobber, the dev's own endpoint.
94
+ const merged = { ...process.env, ...env };
95
+ if (isSet(merged["ANTHROPIC_API_KEY"]))
96
+ return "your ANTHROPIC_API_KEY";
97
+ if (isSet(merged["ANTHROPIC_AUTH_TOKEN"]))
98
+ return "your ANTHROPIC_AUTH_TOKEN";
99
+ if (isSet(merged["CLAUDE_CODE_OAUTH_TOKEN"]))
100
+ return "your CLAUDE_CODE_OAUTH_TOKEN";
101
+ if (isSet(merged["ANTHROPIC_BASE_URL"]))
102
+ return "your ANTHROPIC_BASE_URL";
103
+ if (await probe())
104
+ return "your Claude Code login";
105
+ if (isSet(merged["VENDO_API_KEY"]))
106
+ return "your Vendo Cloud key (managed inference)";
107
+ return null;
108
+ },
109
+ async run(input) {
110
+ const model = input.env["VENDO_EXTRACTION_MODEL"];
111
+ const args = [
112
+ "-p", input.instructions,
113
+ "--allowedTools", ...ALLOWED_TOOLS,
114
+ "--disallowedTools", ...DISALLOWED_TOOLS,
115
+ "--setting-sources", "",
116
+ ...(model === undefined ? [] : ["--model", model]),
117
+ ];
118
+ // Merge FIRST, then guard: the child is spawned with the caller's env
119
+ // over process.env, so the own-credential check and gateway fuel must
120
+ // evaluate that same merged env — guarding input.env alone would let
121
+ // the overlay clobber an ambient (process.env) BYO endpoint the child
122
+ // would otherwise have used. Gateway fuel (if applicable) wins last.
123
+ const merged = { ...process.env, ...input.env };
124
+ const overlay = await resolveGatewayFuelOverlay(merged, probe);
125
+ const result = await exec(args, { cwd: input.root, env: { ...merged, ...overlay } });
126
+ if (result.code !== 0) {
127
+ throw new Error(`claude exited with code ${result.code}: ${result.stderr.trim() || "(no stderr)"}`);
128
+ }
129
+ return result.stdout;
130
+ },
131
+ };
132
+ }
@@ -0,0 +1,14 @@
1
+ import type { ExtractionHarness } from "./harness.js";
2
+ interface SdkModule {
3
+ query(params: {
4
+ prompt: string;
5
+ options: Record<string, unknown>;
6
+ }): AsyncIterable<Record<string, unknown>>;
7
+ }
8
+ export interface ClaudeHarnessOptions {
9
+ /** Test seams. */
10
+ loadSdk?: (root: string) => Promise<SdkModule | null>;
11
+ probeLogin?: () => Promise<boolean>;
12
+ }
13
+ export declare function claudeHarness(options?: ClaudeHarnessOptions): ExtractionHarness;
14
+ export {};
@@ -0,0 +1,127 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ import { join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ /**
6
+ * V1 extraction harness: the Claude Agent SDK driven headless with READ-ONLY
7
+ * code tools (Read/Glob/Grep) over the host root. Credential = the dev's
8
+ * Claude Code login, or their ANTHROPIC_API_KEY. The SDK resolves from the
9
+ * CLI's own installation first, then the host app — it is never installed
10
+ * into the host app by init.
11
+ *
12
+ * Isolation: `settingSources: []` (never inherit the dev's personal Claude
13
+ * Code settings/hooks), read-only tool allowlist, no shell/web/write surface.
14
+ */
15
+ const SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
16
+ const PROBE_TIMEOUT_MS = 5_000;
17
+ /** Bundler-proof dynamic import (same pattern as dev-creds): the
18
+ * Function body is a FIXED literal — the specifier is a parameter, never
19
+ * interpolated into code — so there is no injection surface. */
20
+ async function dynamicImport(url) {
21
+ try {
22
+ return await import(url);
23
+ }
24
+ catch (nativeError) {
25
+ try {
26
+ const escaped = new Function("specifier", "return import(specifier)");
27
+ return await escaped(url);
28
+ }
29
+ catch {
30
+ throw nativeError;
31
+ }
32
+ }
33
+ }
34
+ /** Resolve the SDK from the CLI package itself, then from the host app. */
35
+ async function loadSdk(root) {
36
+ for (const base of [import.meta.url, pathToFileURL(join(root, "package.json")).href]) {
37
+ try {
38
+ const require = createRequire(base);
39
+ return await dynamicImport(pathToFileURL(require.resolve(SDK_PACKAGE)).href);
40
+ }
41
+ catch {
42
+ // try the next resolution base
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+ /** `claude auth status` prints JSON with a `loggedIn` boolean. */
48
+ function probeClaudeLogin() {
49
+ return new Promise((resolve) => {
50
+ execFile("claude", ["auth", "status"], { timeout: PROBE_TIMEOUT_MS }, (error, stdout) => {
51
+ if (error !== null)
52
+ return resolve(false);
53
+ try {
54
+ resolve(JSON.parse(stdout).loggedIn === true);
55
+ }
56
+ catch {
57
+ resolve(false);
58
+ }
59
+ });
60
+ });
61
+ }
62
+ export function claudeHarness(options = {}) {
63
+ const load = options.loadSdk ?? loadSdk;
64
+ const probe = options.probeLogin ?? probeClaudeLogin;
65
+ return {
66
+ id: "claude-agent-sdk",
67
+ async availability({ root, env }) {
68
+ if ((await load(root)) === null)
69
+ return null;
70
+ const key = env["ANTHROPIC_API_KEY"];
71
+ if (typeof key === "string" && key.trim().length > 0)
72
+ return "your ANTHROPIC_API_KEY";
73
+ if (await probe())
74
+ return "your Claude Code login";
75
+ return null;
76
+ },
77
+ async run(input) {
78
+ const sdk = await load(input.root);
79
+ if (sdk === null)
80
+ throw new Error(`${SDK_PACKAGE} is not available`);
81
+ const model = input.env["VENDO_EXTRACTION_MODEL"];
82
+ const stream = sdk.query({
83
+ prompt: input.instructions,
84
+ options: {
85
+ cwd: input.root,
86
+ settingSources: [],
87
+ allowedTools: ["Read", "Glob", "Grep"],
88
+ disallowedTools: [
89
+ "Bash", "Write", "Edit", "WebFetch", "WebSearch", "Task",
90
+ "TodoWrite", "NotebookEdit", "KillShell", "BashOutput",
91
+ ],
92
+ permissionMode: "default",
93
+ maxTurns: 40,
94
+ // Forward the caller's env so a key present only in the passed map
95
+ // (not process.env) still authenticates the SDK subprocess.
96
+ env: { ...process.env, ...input.env },
97
+ ...(model === undefined ? {} : { model }),
98
+ },
99
+ });
100
+ let finalText = "";
101
+ const assistantText = [];
102
+ for await (const message of stream) {
103
+ const type = message["type"];
104
+ if (type === "assistant") {
105
+ const content = message["message"]?.content;
106
+ for (const part of content ?? []) {
107
+ if (part["type"] === "text" && typeof part["text"] === "string") {
108
+ assistantText.push(part["text"]);
109
+ }
110
+ else if (part["type"] === "tool_use") {
111
+ const name = part["name"];
112
+ const file = part["input"];
113
+ const target = typeof file?.file_path === "string" ? file.file_path
114
+ : typeof file?.pattern === "string" ? file.pattern : "";
115
+ if (typeof name === "string")
116
+ input.onProgress?.(`${name.toLowerCase()} ${target}`.trim());
117
+ }
118
+ }
119
+ }
120
+ else if (type === "result" && typeof message["result"] === "string") {
121
+ finalText = message["result"];
122
+ }
123
+ }
124
+ return finalText.length > 0 ? finalText : assistantText.join("\n");
125
+ },
126
+ };
127
+ }
@@ -0,0 +1,31 @@
1
+ import type { ExtractionHarness } from "./harness.js";
2
+ interface ExecResult {
3
+ stdout: string;
4
+ stderr: string;
5
+ code: number;
6
+ }
7
+ type Exec = (args: string[], options: {
8
+ cwd: string;
9
+ env: NodeJS.ProcessEnv;
10
+ }) => Promise<ExecResult>;
11
+ /** Pure mapping from a completed `codex exec` subprocess (exit code/stderr)
12
+ * plus the outcome of reading its `--output-last-message` file into the
13
+ * harness's ExecResult. Pulled out of execCodex's callback and exported so
14
+ * the missing-file fallback — codex exited 0 but somehow wrote nothing —
15
+ * has direct unit coverage instead of living only behind the exec seam. */
16
+ export declare function resolveCodexExecResult(code: number, processStdout: string, stderr: string, finalMessage: string | undefined): ExecResult;
17
+ /** Insert `--output-last-message <file>` immediately before the free-text
18
+ * instructions positional (always the last element of a codex exec argv)
19
+ * rather than appending after it — a flag placed after a long, arbitrary
20
+ * free-text argument is fragile (positional/option disambiguation, and any
21
+ * prompt that happens to start with `-`). Exported and pure so the ordering
22
+ * is unit-tested without spawning a real process. */
23
+ export declare function insertOutputLastMessageFlag(args: string[], outputFile: string): string[];
24
+ export interface CodexCliHarnessOptions {
25
+ /** Test seams. */
26
+ probeBinary?: () => Promise<boolean>;
27
+ probeLogin?: (env: Record<string, string | undefined>) => Promise<boolean>;
28
+ exec?: Exec;
29
+ }
30
+ export declare function codexCliHarness(options?: CodexCliHarnessOptions): ExtractionHarness;
31
+ export {};
@@ -0,0 +1,154 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ /**
6
+ * Fallback extraction harness: the `codex` CLI already on the dev's PATH,
7
+ * driven headless (`codex exec`) with a READ-ONLY sandbox over the host
8
+ * root. Credential = the dev's ChatGPT login (`codex login`), or their
9
+ * OPENAI_API_KEY — this is the third rung of the ladder (after the Agent SDK
10
+ * and claude-cli-harness.ts), for devs whose daily driver is Codex instead of
11
+ * Claude Code and who don't want to install anything new.
12
+ *
13
+ * Isolation: `--sandbox read-only` (model-generated shell commands can only
14
+ * read, never write or reach the network), `--skip-git-repo-check` (the host
15
+ * root may not itself be a git repo — codex refuses to run outside one by
16
+ * default, and extraction has no business caring), `--ignore-user-config`
17
+ * (never load the dev's personal `~/.codex/config.toml` — same intent as
18
+ * claude-cli-harness.ts's `--setting-sources ""`: their own MCP servers,
19
+ * custom instructions, or notify hooks must not leak into extraction; auth
20
+ * still resolves via CODEX_HOME regardless of this flag), `-C <root>` pins
21
+ * the agent's working root explicitly.
22
+ */
23
+ const PROBE_TIMEOUT_MS = 5_000;
24
+ // Extraction stages can run for minutes over a real codebase.
25
+ const RUN_TIMEOUT_MS = 10 * 60 * 1_000;
26
+ // codex's stdout under `exec` is throwaway tool-call narration — the actual
27
+ // payload comes back via --output-last-message and is read separately below.
28
+ // A narration stream over 10MB still trips execFile's maxBuffer and throws;
29
+ // that's a known, accepted failure mode (loud, not silent), not something
30
+ // worth raising just to tolerate noisier runs.
31
+ const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
32
+ /** Pure mapping from a completed `codex exec` subprocess (exit code/stderr)
33
+ * plus the outcome of reading its `--output-last-message` file into the
34
+ * harness's ExecResult. Pulled out of execCodex's callback and exported so
35
+ * the missing-file fallback — codex exited 0 but somehow wrote nothing —
36
+ * has direct unit coverage instead of living only behind the exec seam. */
37
+ export function resolveCodexExecResult(code, processStdout, stderr, finalMessage) {
38
+ if (code !== 0)
39
+ return { stdout: processStdout, stderr, code };
40
+ if (finalMessage !== undefined)
41
+ return { stdout: finalMessage, stderr, code };
42
+ // codex exited 0 but wrote no final-message file — there is nothing for
43
+ // extraction.ts to parse; surface it as a failure through the same
44
+ // code-path run() already uses for errors.
45
+ return {
46
+ stdout: "",
47
+ stderr: "codex produced no final message (--output-last-message file missing)",
48
+ code: 1,
49
+ };
50
+ }
51
+ /** Insert `--output-last-message <file>` immediately before the free-text
52
+ * instructions positional (always the last element of a codex exec argv)
53
+ * rather than appending after it — a flag placed after a long, arbitrary
54
+ * free-text argument is fragile (positional/option disambiguation, and any
55
+ * prompt that happens to start with `-`). Exported and pure so the ordering
56
+ * is unit-tested without spawning a real process. */
57
+ export function insertOutputLastMessageFlag(args, outputFile) {
58
+ return [...args.slice(0, -1), "--output-last-message", outputFile, ...args.slice(-1)];
59
+ }
60
+ /** `codex exec` has no print-mode equivalent to claude's `-p` stdout capture
61
+ * — the terminal stream is full of tool-call narration. `--output-last-
62
+ * message <file>` is codex's own mechanism for isolating just the agent's
63
+ * final text, so the real spawn writes it to a throwaway temp file and
64
+ * reads it back once the process exits cleanly. */
65
+ function execCodex(args, options) {
66
+ return (async () => {
67
+ const outDir = await mkdtemp(join(tmpdir(), "vendo-codex-extract-"));
68
+ const outputFile = join(outDir, "last-message.txt");
69
+ try {
70
+ return await new Promise((resolve) => {
71
+ execFile("codex", insertOutputLastMessageFlag(args, outputFile), { cwd: options.cwd, env: options.env, timeout: RUN_TIMEOUT_MS, maxBuffer: MAX_BUFFER_BYTES }, (error, stdout, stderr) => {
72
+ const code = error === null ? 0 : typeof error.code === "number" ? error.code : 1;
73
+ if (code !== 0) {
74
+ resolve(resolveCodexExecResult(code, stdout, stderr, undefined));
75
+ return;
76
+ }
77
+ readFile(outputFile, "utf8").then((finalMessage) => resolve(resolveCodexExecResult(code, stdout, stderr, finalMessage)), () => resolve(resolveCodexExecResult(code, stdout, stderr, undefined)));
78
+ });
79
+ });
80
+ }
81
+ finally {
82
+ await rm(outDir, { recursive: true, force: true });
83
+ }
84
+ })();
85
+ }
86
+ /** Cheap presence check: does `codex` resolve and run at all? */
87
+ function probeCodexBinary() {
88
+ return new Promise((resolve) => {
89
+ execFile("codex", ["--version"], { timeout: PROBE_TIMEOUT_MS }, (error) => resolve(error === null));
90
+ });
91
+ }
92
+ /** Codex persists login state to `$CODEX_HOME/auth.json` (default
93
+ * `~/.codex/auth.json`) — either a ChatGPT OAuth token pair (`auth_mode:
94
+ * "chatgpt"`) or an API key saved via `codex login --with-api-key`.
95
+ * `codex login status` has no machine-readable output, so this reads the
96
+ * file directly: a plain fs read that's cheap and trivially fake-able in
97
+ * tests without a real login. CODEX_HOME resolution mirrors run()'s own
98
+ * env merge (the caller's env wins over process.env) so availability's
99
+ * login check and the actual subprocess spawn never disagree about which
100
+ * codex home is in play. */
101
+ async function probeCodexLogin(env) {
102
+ const codexHome = env["CODEX_HOME"] ?? process.env["CODEX_HOME"] ?? join(homedir(), ".codex");
103
+ const authPath = join(codexHome, "auth.json");
104
+ try {
105
+ const parsed = JSON.parse(await readFile(authPath, "utf8"));
106
+ if (typeof parsed.OPENAI_API_KEY === "string" && parsed.OPENAI_API_KEY.trim().length > 0)
107
+ return true;
108
+ if (typeof parsed.tokens?.id_token === "string" && parsed.tokens.id_token.trim().length > 0)
109
+ return true;
110
+ if (typeof parsed.tokens?.access_token === "string" && parsed.tokens.access_token.trim().length > 0)
111
+ return true;
112
+ return false;
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ export function codexCliHarness(options = {}) {
119
+ const hasBinary = options.probeBinary ?? probeCodexBinary;
120
+ const probe = options.probeLogin ?? probeCodexLogin;
121
+ const exec = options.exec ?? execCodex;
122
+ return {
123
+ id: "codex-cli",
124
+ async availability({ env }) {
125
+ if (!(await hasBinary()))
126
+ return null;
127
+ const key = env["OPENAI_API_KEY"];
128
+ if (typeof key === "string" && key.trim().length > 0)
129
+ return "your OPENAI_API_KEY";
130
+ if (await probe(env))
131
+ return "your ChatGPT login";
132
+ return null;
133
+ },
134
+ async run(input) {
135
+ const model = input.env["VENDO_EXTRACTION_MODEL"];
136
+ const args = [
137
+ "exec",
138
+ "--sandbox", "read-only",
139
+ "--skip-git-repo-check",
140
+ "--ignore-user-config",
141
+ "-C", input.root,
142
+ ...(model === undefined ? [] : ["--model", model]),
143
+ input.instructions,
144
+ ];
145
+ // Forward the caller's env so a key present only in the passed map
146
+ // (not process.env) still authenticates the subprocess.
147
+ const result = await exec(args, { cwd: input.root, env: { ...process.env, ...input.env } });
148
+ if (result.code !== 0) {
149
+ throw new Error(`codex exited with code ${result.code}: ${result.stderr.trim() || "(no stderr)"}`);
150
+ }
151
+ return result.stdout;
152
+ },
153
+ };
154
+ }
@@ -0,0 +1,21 @@
1
+ import { type StaticTool } from "./stages.js";
2
+ /**
3
+ * The delegated-extraction contract (install-dx: external-agent delegation).
4
+ * Vendo's IP is the CONTRACT — the composed instructions, the draft schema,
5
+ * and the deterministic guards — not the reader: any competent coding agent
6
+ * (Claude Code, Cursor, Codex, …) can do the reading. `vendo init --agent`
7
+ * emits this contract in its plan; the dev's own agent reads the codebase,
8
+ * writes a draft, and `vendo extract --apply` runs it through the SAME
9
+ * applyDraft guards as init's built-in pass — delegation never becomes a
10
+ * second, weaker path into `.vendo/`.
11
+ */
12
+ export declare const APPLY_COMMAND = "npx vendo extract --apply <draft.json>";
13
+ /** JSON Schema mirror of extractionDraftSchema in harness.ts (hand-kept in
14
+ * sync — zod v3 has no schema emitter). Stricter than parseDraft on unknown
15
+ * keys, deliberately: the contract steers agents to the exact shape while
16
+ * apply stays lenient. */
17
+ export declare const EXTRACTION_DRAFT_JSON_SCHEMA: Record<string, unknown>;
18
+ /** One-shot instructions for an external agent producing the FULL draft
19
+ * (judgment on every tool plus the brief) — the same rules the staged
20
+ * pipeline enforces per stage, phrased for a single delegated pass. */
21
+ export declare function composeDelegatedInstructions(tools: StaticTool[], appName: string): string;