@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,77 @@
1
+ import { staticFacts } 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 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 const EXTRACTION_DRAFT_JSON_SCHEMA = {
18
+ $schema: "https://json-schema.org/draft/2020-12/schema",
19
+ type: "object",
20
+ additionalProperties: false,
21
+ required: ["brief", "tools"],
22
+ properties: {
23
+ brief: {
24
+ type: "string",
25
+ minLength: 1,
26
+ maxLength: 4000,
27
+ description: "One-paragraph product brief drafted from the codebase.",
28
+ },
29
+ tools: {
30
+ type: "array",
31
+ items: {
32
+ type: "object",
33
+ additionalProperties: false,
34
+ required: ["name", "description"],
35
+ properties: {
36
+ name: { type: "string", minLength: 1 },
37
+ description: { type: "string", minLength: 1, maxLength: 500 },
38
+ risk: { enum: ["read", "write", "destructive"] },
39
+ critical: { type: "boolean" },
40
+ disabled: {
41
+ type: "boolean",
42
+ description: "false = wake a statically-unclassifiable tool (requires reasoning and risk).",
43
+ },
44
+ reasoning: { type: "string", maxLength: 500 },
45
+ },
46
+ },
47
+ },
48
+ missedSurfaces: {
49
+ type: "array",
50
+ items: { type: "string", maxLength: 300 },
51
+ description: "API surfaces the static extractor missed (path + one line). Surfaced to the dev, never written.",
52
+ },
53
+ },
54
+ };
55
+ /** One-shot instructions for an external agent producing the FULL draft
56
+ * (judgment on every tool plus the brief) — the same rules the staged
57
+ * pipeline enforces per stage, phrased for a single delegated pass. */
58
+ export function composeDelegatedInstructions(tools, appName) {
59
+ return [
60
+ "You are Vendo's extraction agent. Read this codebase (Read/Glob/Grep only) and draft",
61
+ "judgment on the API tools a static extractor already found, plus the product brief.",
62
+ "",
63
+ `Product/package name: ${appName}`,
64
+ "Statically extracted tools (name, method+path when known, current risk, disabled state):",
65
+ staticFacts(tools),
66
+ "",
67
+ "Rules:",
68
+ "- Produce ONE json document matching the provided draftSchema:",
69
+ ' { "brief": string, "tools": [{ "name", "description", "risk"?, "critical"?, "disabled"?, "reasoning"? }], "missedSurfaces"?: string[] }',
70
+ "- tools: include ONLY names from the list above. Rewrite each description so an agent choosing tools understands what it actually does (read the handler source). <= 200 chars each.",
71
+ "- risk: you may RAISE risk (read->write->destructive) when the handler is more dangerous than labeled; never lower it. Mark irreversible operations critical: true.",
72
+ "- A tool listed as disabled was statically unclassifiable. If you can read its handler and grade it, set disabled: false WITH a risk and one-line reasoning. Leave it out otherwise.",
73
+ "- missedSurfaces: API surfaces you found that the list is missing (path + one line). Do not invent tools for them.",
74
+ "- brief: one paragraph — what the product does, who uses it, the jobs the agent should help with. Written from the actual code, no marketing fluff.",
75
+ `- Save the draft to a json file and apply it with \`${APPLY_COMMAND}\` — deterministic guards validate every entry before anything lands in .vendo/.`,
76
+ ].join("\n");
77
+ }
@@ -0,0 +1,65 @@
1
+ import { parseDraft, type ExtractionHarness } from "./harness.js";
2
+ import { type SelectOption } from "../pretty.js";
3
+ import { type StagedExtractionInput, type StagedExtractionResult, type StaticTool } from "./stages.js";
4
+ import { type Output } from "../shared.js";
5
+ export { composeInstructions } from "./stages.js";
6
+ export interface AppliedSummary {
7
+ described: number;
8
+ riskRaised: number;
9
+ critical: number;
10
+ woken: number;
11
+ briefWritten: boolean;
12
+ refused: string[];
13
+ missedSurfaces: string[];
14
+ }
15
+ /** Deterministic verification + application of a parsed draft. Exported for
16
+ * tests; init uses runAiExtraction below. */
17
+ export declare function applyDraft(input: {
18
+ root: string;
19
+ draft: ReturnType<typeof parseDraft>;
20
+ tools: StaticTool[];
21
+ force?: boolean;
22
+ }): Promise<AppliedSummary>;
23
+ /** The one applied-polish summary both init's built-in pass and
24
+ * `vendo extract --apply` print — delegation must read identically. */
25
+ export declare function reportApplied(input: {
26
+ output: Output;
27
+ applied: AppliedSummary;
28
+ briefDrafted: boolean;
29
+ artifactsNote?: string;
30
+ notes?: string[];
31
+ }): void;
32
+ /** Consent-style one-line prompt — shared machinery: the AI-polish consent
33
+ here and init's detected-auth confirm both use it. */
34
+ export declare function askYesNo(question: string, defaultYes: boolean): Promise<boolean>;
35
+ export interface AiExtractionOptions {
36
+ root: string;
37
+ output: Output;
38
+ env: Record<string, string | undefined>;
39
+ /** Non-interactive (--yes / no TTY): no consent possible — skip silently. */
40
+ yes: boolean;
41
+ /** --ai-polish: consent granted as a flag — skip the prompt and run even
42
+ when non-interactive (the flag IS the answer). */
43
+ consent?: boolean;
44
+ force?: boolean;
45
+ /** --engine: pin the rung family (claude | codex | npx) instead of
46
+ first-available. An unavailable pin skips loudly — never a fallback. */
47
+ engine?: string;
48
+ /** Seams (tests / future harnesses). */
49
+ harnesses?: ExtractionHarness[];
50
+ confirm?: (question: string, defaultYes: boolean) => Promise<boolean>;
51
+ /** The multi-engine consent select (init passes pretty.select); default is
52
+ the plain numbered select. Resolves to an option value or "skip". */
53
+ choose?: (question: string, options: SelectOption[], defaultIndex: number) => Promise<string>;
54
+ interactive?: boolean;
55
+ /** Optional theme-stage input (init's exact-only summary, projected) — see
56
+ `StagedExtractionInput.theme`. Omitted when init has no theme pass to
57
+ run this call (a pre-existing theme.json, e.g.). */
58
+ theme?: StagedExtractionInput["theme"];
59
+ }
60
+ /** init's AI extraction step. Never changes init's exit code. */
61
+ export declare function runAiExtraction(options: AiExtractionOptions): Promise<{
62
+ ran: boolean;
63
+ engine?: "claude" | "codex" | "npx-engine";
64
+ theme?: StagedExtractionResult["theme"];
65
+ }>;
@@ -0,0 +1,300 @@
1
+ import { join } from "node:path";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin, stdout } from "node:process";
4
+ import { z } from "zod";
5
+ import { claudeCliHarness } from "./claude-cli-harness.js";
6
+ import { claudeHarness } from "./claude-harness.js";
7
+ import { codexCliHarness } from "./codex-cli-harness.js";
8
+ import { npxEngineHarness } from "./npx-engine-harness.js";
9
+ import { plainSelect } from "../pretty.js";
10
+ import { BRIEF_TEMPLATE, runStagedExtraction, staticToolSchema, } from "./stages.js";
11
+ import { readOptional, writeText } from "../shared.js";
12
+ export { composeInstructions } from "./stages.js";
13
+ /**
14
+ * The AI extraction pass (install-dx: staged pipeline + deterministic
15
+ * verification — see stages.ts for the survey / draft-per-surface /
16
+ * cross-check / brief orchestration). The agent reads the codebase and
17
+ * drafts judgment on top of the static facts: task-oriented tool
18
+ * descriptions, risk corrections, critical marks, waking unclassifiable
19
+ * tools, and the product brief.
20
+ *
21
+ * Output rides the channels that SURVIVE `vendo sync` regeneration:
22
+ * `.vendo/overrides.json` (per-tool description/risk/critical/disabled — the
23
+ * designed merge layer) and `.vendo/brief.md`. It never writes tools.json
24
+ * directly, so predev/prebuild re-extraction cannot clobber it.
25
+ *
26
+ * Deterministic guards (never cross fingers, PostHog lesson):
27
+ * - only tool names the static extraction produced are accepted;
28
+ * - risk may be RAISED, never lowered (fail-closed stays fail-closed);
29
+ * - waking a disabled tool requires reasoning and an explicit risk;
30
+ * - human decisions win: existing override fields are never overwritten, and
31
+ * a hand-written brief is never replaced (only the init template is).
32
+ */
33
+ const RISK_ORDER = { read: 0, write: 1, destructive: 2 };
34
+ const overridesSchema = z.object({
35
+ format: z.literal("vendo/overrides@1"),
36
+ tools: z.record(z.object({
37
+ risk: z.enum(["read", "write", "destructive"]).optional(),
38
+ critical: z.boolean().optional(),
39
+ disabled: z.boolean().optional(),
40
+ description: z.string().optional(),
41
+ }).passthrough()),
42
+ }).passthrough();
43
+ /** Deterministic verification + application of a parsed draft. Exported for
44
+ * tests; init uses runAiExtraction below. */
45
+ export async function applyDraft(input) {
46
+ const byName = new Map(input.tools.map((tool) => [tool.name, tool]));
47
+ const overridesPath = join(input.root, ".vendo", "overrides.json");
48
+ const raw = await readOptional(overridesPath);
49
+ const overrides = raw === null
50
+ ? { format: "vendo/overrides@1", tools: {} }
51
+ : overridesSchema.parse(JSON.parse(raw));
52
+ const summary = {
53
+ described: 0, riskRaised: 0, critical: 0, woken: 0,
54
+ briefWritten: false, refused: [], missedSurfaces: input.draft.missedSurfaces ?? [],
55
+ };
56
+ for (const entry of input.draft.tools) {
57
+ const fact = byName.get(entry.name);
58
+ if (fact === undefined) {
59
+ summary.refused.push(`${entry.name}: not an extracted tool`);
60
+ continue;
61
+ }
62
+ const existing = overrides.tools[entry.name] ?? {};
63
+ const next = { ...existing };
64
+ if (existing.description === undefined && entry.description !== fact.description) {
65
+ next.description = entry.description;
66
+ summary.described += 1;
67
+ }
68
+ // Waking a statically-unclassifiable tool is its own path: the static
69
+ // "destructive, disabled" grade is a fail-closed PLACEHOLDER, not
70
+ // evidence, so a reasoned wake carries the model's grade without tripping
71
+ // the downgrade guard (Greptile P1 / Devin on #363). A human-set risk or
72
+ // disabled decision always wins.
73
+ const isWake = entry.disabled === false && fact.disabled === true;
74
+ if (isWake && existing.disabled === undefined) {
75
+ if (entry.reasoning === undefined || entry.risk === undefined) {
76
+ summary.refused.push(`${entry.name}: waking a disabled tool needs reasoning and a risk grade`);
77
+ }
78
+ else {
79
+ next.disabled = false;
80
+ if (existing.risk === undefined)
81
+ next.risk = entry.risk;
82
+ summary.woken += 1;
83
+ }
84
+ }
85
+ else if (entry.risk !== undefined && existing.risk === undefined && fact.disabled !== true) {
86
+ const current = fact.risk ?? "destructive";
87
+ if (RISK_ORDER[entry.risk] > RISK_ORDER[current]) {
88
+ next.risk = entry.risk;
89
+ summary.riskRaised += 1;
90
+ }
91
+ else if (RISK_ORDER[entry.risk] < RISK_ORDER[current]) {
92
+ summary.refused.push(`${entry.name}: risk downgrade ${current}→${entry.risk} refused`);
93
+ }
94
+ }
95
+ if (entry.critical === true && existing.critical === undefined) {
96
+ next.critical = true;
97
+ summary.critical += 1;
98
+ }
99
+ if (JSON.stringify(next) !== JSON.stringify(existing))
100
+ overrides.tools[entry.name] = next;
101
+ }
102
+ await writeText(overridesPath, `${JSON.stringify(overrides, null, 2)}\n`);
103
+ const briefPath = join(input.root, ".vendo", "brief.md");
104
+ const currentBrief = ((await readOptional(briefPath)) ?? "").trim();
105
+ if (input.force === true || currentBrief === "" || currentBrief === BRIEF_TEMPLATE) {
106
+ await writeText(briefPath, `${input.draft.brief.trim()}\n`);
107
+ summary.briefWritten = true;
108
+ }
109
+ return summary;
110
+ }
111
+ /** The one applied-polish summary both init's built-in pass and
112
+ * `vendo extract --apply` print — delegation must read identically. */
113
+ export function reportApplied(input) {
114
+ const { output, applied } = input;
115
+ const parts = [
116
+ `${applied.described} descriptions`,
117
+ ...(applied.riskRaised > 0 ? [`${applied.riskRaised} risk raises`] : []),
118
+ ...(applied.critical > 0 ? [`${applied.critical} critical marks`] : []),
119
+ ...(applied.woken > 0 ? [`${applied.woken} tools woken`] : []),
120
+ ...(input.briefDrafted ? ["brief drafted"] : []),
121
+ ];
122
+ output.log(`AI polish applied: ${parts.join(" · ")} → .vendo/overrides.json, .vendo/brief.md${input.artifactsNote ?? ""}`);
123
+ for (const note of input.notes ?? [])
124
+ output.error(` ${note}`);
125
+ for (const refused of applied.refused)
126
+ output.error(` refused: ${refused}`);
127
+ for (const missed of applied.missedSurfaces)
128
+ output.log(` missed surface (not extracted yet): ${missed}`);
129
+ }
130
+ /** Consent-style one-line prompt — shared machinery: the AI-polish consent
131
+ here and init's detected-auth confirm both use it. */
132
+ export async function askYesNo(question, defaultYes) {
133
+ const prompt = createInterface({ input: stdin, output: stdout });
134
+ try {
135
+ const answer = (await prompt.question(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase();
136
+ if (answer === "")
137
+ return defaultYes;
138
+ return ["y", "yes"].includes(answer);
139
+ }
140
+ finally {
141
+ prompt.close();
142
+ }
143
+ }
144
+ /** The telemetry `engine` enum value for each ladder rung (both Claude rungs
145
+ are the same engine reached two ways). Unlisted ids (test seams) map to
146
+ undefined — the caller's "none" default covers them. Distinct from
147
+ ENGINE_FAMILIES below: telemetry's closed enum says "npx-engine" while the
148
+ user-facing `--engine` family is "npx". */
149
+ const ENGINE_BY_HARNESS_ID = {
150
+ "claude-agent-sdk": "claude",
151
+ "claude-cli": "claude",
152
+ "codex-cli": "codex",
153
+ "npx-engine": "npx-engine",
154
+ };
155
+ /** Rung id → user-facing engine family (`--engine` values). The Agent SDK and
156
+ the claude CLI are one family: same provider, same credential story. An
157
+ unknown id (test fakes, future rungs) is its own family. */
158
+ const ENGINE_FAMILIES = {
159
+ "claude-agent-sdk": "claude",
160
+ "claude-cli": "claude",
161
+ "codex-cli": "codex",
162
+ "npx-engine": "npx",
163
+ };
164
+ /** init's AI extraction step. Never changes init's exit code. */
165
+ export async function runAiExtraction(options) {
166
+ const { root, output, env } = options;
167
+ const toolsRaw = await readOptional(join(root, ".vendo", "tools.json"));
168
+ let tools = [];
169
+ // tools.json missing or unparseable degrades tool polish to a no-op — but
170
+ // must NOT also silently kill a requested theme pass (Task 4): the staged
171
+ // call below runs with an empty tool list, and only the tool-polish half
172
+ // (applyDraft/reportApplied) is skipped.
173
+ let toolsAvailable = false;
174
+ if (toolsRaw !== null) {
175
+ try {
176
+ tools = z.object({ tools: z.array(staticToolSchema) }).parse(JSON.parse(toolsRaw)).tools;
177
+ toolsAvailable = true;
178
+ }
179
+ catch {
180
+ // Unparseable tools.json — tool polish stays skipped below.
181
+ }
182
+ }
183
+ if (!toolsAvailable && options.theme === undefined)
184
+ return { ran: false };
185
+ const interactive = options.interactive ?? (Boolean(stdin.isTTY) && Boolean(stdout.isTTY));
186
+ if (options.consent !== true && (options.yes || !interactive)) {
187
+ output.log("AI polish (descriptions, risk review, brief, theme): skipped — needs an interactive run (`vendo init` in a terminal).");
188
+ return { ran: false };
189
+ }
190
+ // Ordered engine ladder (install-dx: init-selfcontained-engine, Task 2/4):
191
+ // Agent SDK -> claude CLI -> codex CLI -> npx-fetched engine. A rung whose
192
+ // availability() is null (binary missing or present-but-unauthenticated) is
193
+ // skipped; ladder order encodes preference (the npx rung is last on purpose:
194
+ // it's the only one with a real first-run cost, an npm fetch, so every rung
195
+ // that can run for free gets first refusal). Availability is checked across
196
+ // the WHOLE ladder (every check is local and cheap) so `--engine` can pin a
197
+ // family and the interactive consent can offer a choice when there is one;
198
+ // the first available rung of a family speaks for that family.
199
+ const harnesses = options.harnesses
200
+ ?? [claudeHarness(), claudeCliHarness(), codexCliHarness(), npxEngineHarness()];
201
+ const available = [];
202
+ for (const harness of harnesses) {
203
+ const family = ENGINE_FAMILIES[harness.id] ?? harness.id;
204
+ if (available.some((entry) => entry.family === family))
205
+ continue;
206
+ const credential = await harness.availability({ root, env });
207
+ if (credential !== null)
208
+ available.push({ harness, credential, family });
209
+ }
210
+ if (available.length === 0) {
211
+ output.log("AI polish: unavailable — needs Claude Code installed (`npm install -g @anthropic-ai/claude-code`) or @anthropic-ai/claude-agent-sdk resolvable, plus a Claude Code login or ANTHROPIC_API_KEY; or the `codex` CLI installed, plus a codex login (`codex login`) or OPENAI_API_KEY; or a VENDO_API_KEY (`vendo login`), which fetches Claude Code on the fly via npx. Extractor defaults stand; re-run `vendo init` once set up.");
212
+ return { ran: false };
213
+ }
214
+ // --engine pins a family. An unavailable pin never falls back to another
215
+ // provider (the pin is usually a privacy/policy choice about where source
216
+ // goes) — one loud line naming what IS available, exit code untouched.
217
+ let chosen;
218
+ if (options.engine !== undefined) {
219
+ const pinned = available.find((entry) => entry.family === options.engine);
220
+ if (pinned === undefined) {
221
+ const alternatives = available
222
+ .map((entry) => `\`--engine ${entry.family}\` (${entry.credential})`)
223
+ .join(", or ");
224
+ output.log(`AI polish: \`--engine ${options.engine}\` requested but that engine isn't available on this machine. Available: ${alternatives}. Extractor defaults stand; re-run \`vendo init\` once it's set up.`);
225
+ return { ran: false };
226
+ }
227
+ chosen = pinned;
228
+ }
229
+ else {
230
+ chosen = available[0];
231
+ }
232
+ if (options.consent !== true) {
233
+ if (options.engine === undefined && available.length > 1) {
234
+ // Several engines: the SAME single consent question, as a pick-with-
235
+ // default instead of yes/no — never a second question.
236
+ const choose = options.choose ?? plainSelect;
237
+ const picked = await choose("Let a coding agent read this codebase to draft tool descriptions, review risk, write the product brief, and fill unresolved theme slots? Source goes to the chosen provider under your account.", [
238
+ ...available.map((entry) => ({ value: entry.family, label: entry.credential, hint: `--engine ${entry.family}` })),
239
+ { value: "skip", label: "Skip — keep extractor defaults" },
240
+ ], 0);
241
+ const selected = available.find((entry) => entry.family === picked);
242
+ if (selected === undefined) {
243
+ output.log("Skipped — extractor defaults stand; re-run `vendo init` any time to add the AI polish.");
244
+ return { ran: false };
245
+ }
246
+ chosen = selected;
247
+ }
248
+ else {
249
+ const confirm = options.confirm ?? askYesNo;
250
+ const consented = await confirm(`Let ${chosen.credential} read this codebase to draft tool descriptions, review risk, write the product brief, and fill unresolved theme slots? Source goes to your model provider under your account.`, true);
251
+ if (!consented) {
252
+ output.log("Skipped — extractor defaults stand; re-run `vendo init` any time to add the AI polish.");
253
+ return { ran: false };
254
+ }
255
+ }
256
+ }
257
+ let appName = "app";
258
+ try {
259
+ appName = JSON.parse((await readOptional(join(root, "package.json"))) ?? "{}").name ?? "app";
260
+ }
261
+ catch {
262
+ // package.json is optional context
263
+ }
264
+ output.log(`\nReading your product (${chosen.credential})…`);
265
+ try {
266
+ const staged = await runStagedExtraction({
267
+ root,
268
+ env,
269
+ harness: chosen.harness,
270
+ tools,
271
+ appName,
272
+ onProgress: (line) => output.log(` ${line}`),
273
+ ...(options.theme === undefined ? {} : { theme: options.theme }),
274
+ });
275
+ if (toolsAvailable) {
276
+ const applied = await applyDraft({ root, draft: staged.draft, tools, ...(options.force === undefined ? {} : { force: options.force }) });
277
+ reportApplied({
278
+ output,
279
+ applied,
280
+ briefDrafted: applied.briefWritten && staged.briefFromStage,
281
+ artifactsNote: " (stage artifacts: .vendo/data/extract/)",
282
+ notes: staged.notes,
283
+ });
284
+ }
285
+ else {
286
+ for (const note of staged.notes)
287
+ output.error(` ${note}`);
288
+ }
289
+ const engine = ENGINE_BY_HARNESS_ID[chosen.harness.id];
290
+ return {
291
+ ran: true,
292
+ ...(engine === undefined ? {} : { engine }),
293
+ ...(staged.theme === undefined ? {} : { theme: staged.theme }),
294
+ };
295
+ }
296
+ catch (error) {
297
+ output.error(`AI polish did not complete (${error instanceof Error ? error.message : "unknown error"}); extractor defaults stand. Re-run \`vendo init\` to retry — stage artifacts in .vendo/data/extract/ show how far it got.`);
298
+ return { ran: false };
299
+ }
300
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Gateway fuel: the env overlay that makes Claude Code (any rung that reads
3
+ * ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_CUSTOM_HEADERS — the
4
+ * PATH `claude` binary today, the npx-fetched engine package tomorrow, per
5
+ * `docs/superpowers/plans/2026-07-20-init-selfcontained-engine.md` Task 3/4)
6
+ * speak to the Vendo Cloud model gateway instead of Anthropic directly, when
7
+ * the dev has no Anthropic credential of their own but does have
8
+ * VENDO_API_KEY. Own credential always wins — this module never overrides
9
+ * it, and callers must never call it when one is available.
10
+ *
11
+ * INVARIANT: a non-blank ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, or
12
+ * ANTHROPIC_BASE_URL in the passed env IS an own credential (the corporate-
13
+ * gateway / custom-endpoint path Claude Code also supports, none of which
14
+ * show up in a `claude auth status` login probe) and always wins, even if a
15
+ * caller mistakenly passes `ownCredentialAvailable: false`. Overwriting a
16
+ * dev's already-configured BYO endpoint would silently redirect their
17
+ * inference through Vendo's gateway and bill their org's meter — this
18
+ * module checks it directly rather than trusting callers to remember.
19
+ *
20
+ * The gateway must be able to refuse this traffic for free-plan orgs
21
+ * (spec's free-plan policy), so every request gets tagged with the
22
+ * INIT_PURPOSE_HEADER — a plain "name: value" line via Claude Code's
23
+ * ANTHROPIC_CUSTOM_HEADERS mechanism. The constant is the single source of
24
+ * truth; the console mirrors the literal in its own tests (plan Task 7).
25
+ */
26
+ export declare const INIT_PURPOSE_HEADER_NAME = "x-vendo-purpose";
27
+ export declare const INIT_PURPOSE_HEADER_VALUE = "init";
28
+ /** Env vars that Claude Code itself accepts as an own credential besides
29
+ * ANTHROPIC_API_KEY and a CLI login: a corporate-gateway auth token, a
30
+ * device-flow OAuth token, or a custom base URL with no token at all
31
+ * (mTLS/proxy auth). None of these are visible to a `claude auth status`
32
+ * probe, so any caller folding "own credential" into its own predicate
33
+ * (claude-cli-harness.ts's availability()/run()) must check these three
34
+ * directly rather than relying solely on the login probe. Exported so
35
+ * every harness checks the identical set. */
36
+ export declare const OWN_CREDENTIAL_ENV_VARS: readonly ["ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_BASE_URL"];
37
+ /** True when the env alone (no async probe needed) already carries one of
38
+ * Claude Code's own-credential env vars (see OWN_CREDENTIAL_ENV_VARS). */
39
+ export declare function hasOwnAnthropicEnvOverride(env: Record<string, string | undefined>): boolean;
40
+ export interface GatewayFuelOverlay {
41
+ ANTHROPIC_BASE_URL: string;
42
+ ANTHROPIC_AUTH_TOKEN: string;
43
+ ANTHROPIC_CUSTOM_HEADERS: string;
44
+ }
45
+ export interface GatewayFuelOptions {
46
+ /** The CHILD'S real env — i.e. the same merged {...process.env,
47
+ * ...input.env} the harness is about to spawn with, never the caller's
48
+ * partial input env alone. The INVARIANT above is only worth anything if
49
+ * it is checked against the env the subprocess will actually read: an
50
+ * ambient (process.env) ANTHROPIC_AUTH_TOKEN/ANTHROPIC_BASE_URL is a
51
+ * live BYO endpoint that an overlay composed from input.env alone would
52
+ * silently clobber. */
53
+ env: Record<string, string | undefined>;
54
+ /** True when the rung already has a working credential of its own (its
55
+ * own ANTHROPIC_API_KEY, a satisfied Claude Code login, ...). Each
56
+ * harness computes this itself — an env check, an async login probe,
57
+ * whatever its rung needs — and passes the verdict in, so this module
58
+ * stays a pure, harness-agnostic composition step reusable by every
59
+ * Claude-Code-shaped rung (claude-cli-harness.ts today; the future
60
+ * npx-engine harness reuses the same function). This module additionally
61
+ * checks OWN_CREDENTIAL_ENV_VARS itself regardless of this flag — see
62
+ * the module-level INVARIANT note. */
63
+ ownCredentialAvailable: boolean;
64
+ }
65
+ /** Compose the gateway-fuel env overlay for a Claude-Code-shaped rung, or
66
+ * null when gateway fuel does not apply: own credential wins (either the
67
+ * caller's verdict or a directly-detected env override), or there is no
68
+ * VENDO_API_KEY to fuel with. */
69
+ export declare function composeGatewayFuel(options: GatewayFuelOptions): GatewayFuelOverlay | null;
@@ -0,0 +1,69 @@
1
+ import { resolveCloudBaseUrl } from "../cloud/client.js";
2
+ /**
3
+ * Gateway fuel: the env overlay that makes Claude Code (any rung that reads
4
+ * ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_CUSTOM_HEADERS — the
5
+ * PATH `claude` binary today, the npx-fetched engine package tomorrow, per
6
+ * `docs/superpowers/plans/2026-07-20-init-selfcontained-engine.md` Task 3/4)
7
+ * speak to the Vendo Cloud model gateway instead of Anthropic directly, when
8
+ * the dev has no Anthropic credential of their own but does have
9
+ * VENDO_API_KEY. Own credential always wins — this module never overrides
10
+ * it, and callers must never call it when one is available.
11
+ *
12
+ * INVARIANT: a non-blank ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, or
13
+ * ANTHROPIC_BASE_URL in the passed env IS an own credential (the corporate-
14
+ * gateway / custom-endpoint path Claude Code also supports, none of which
15
+ * show up in a `claude auth status` login probe) and always wins, even if a
16
+ * caller mistakenly passes `ownCredentialAvailable: false`. Overwriting a
17
+ * dev's already-configured BYO endpoint would silently redirect their
18
+ * inference through Vendo's gateway and bill their org's meter — this
19
+ * module checks it directly rather than trusting callers to remember.
20
+ *
21
+ * The gateway must be able to refuse this traffic for free-plan orgs
22
+ * (spec's free-plan policy), so every request gets tagged with the
23
+ * INIT_PURPOSE_HEADER — a plain "name: value" line via Claude Code's
24
+ * ANTHROPIC_CUSTOM_HEADERS mechanism. The constant is the single source of
25
+ * truth; the console mirrors the literal in its own tests (plan Task 7).
26
+ */
27
+ export const INIT_PURPOSE_HEADER_NAME = "x-vendo-purpose";
28
+ export const INIT_PURPOSE_HEADER_VALUE = "init";
29
+ /** Env vars that Claude Code itself accepts as an own credential besides
30
+ * ANTHROPIC_API_KEY and a CLI login: a corporate-gateway auth token, a
31
+ * device-flow OAuth token, or a custom base URL with no token at all
32
+ * (mTLS/proxy auth). None of these are visible to a `claude auth status`
33
+ * probe, so any caller folding "own credential" into its own predicate
34
+ * (claude-cli-harness.ts's availability()/run()) must check these three
35
+ * directly rather than relying solely on the login probe. Exported so
36
+ * every harness checks the identical set. */
37
+ export const OWN_CREDENTIAL_ENV_VARS = [
38
+ "ANTHROPIC_AUTH_TOKEN",
39
+ "CLAUDE_CODE_OAUTH_TOKEN",
40
+ "ANTHROPIC_BASE_URL",
41
+ ];
42
+ function nonBlank(value) {
43
+ return typeof value === "string" && value.trim().length > 0;
44
+ }
45
+ /** True when the env alone (no async probe needed) already carries one of
46
+ * Claude Code's own-credential env vars (see OWN_CREDENTIAL_ENV_VARS). */
47
+ export function hasOwnAnthropicEnvOverride(env) {
48
+ return OWN_CREDENTIAL_ENV_VARS.some((name) => nonBlank(env[name]));
49
+ }
50
+ /** Compose the gateway-fuel env overlay for a Claude-Code-shaped rung, or
51
+ * null when gateway fuel does not apply: own credential wins (either the
52
+ * caller's verdict or a directly-detected env override), or there is no
53
+ * VENDO_API_KEY to fuel with. */
54
+ export function composeGatewayFuel(options) {
55
+ if (options.ownCredentialAvailable)
56
+ return null;
57
+ if (hasOwnAnthropicEnvOverride(options.env))
58
+ return null;
59
+ const key = options.env["VENDO_API_KEY"];
60
+ if (!nonBlank(key))
61
+ return null;
62
+ const base = resolveCloudBaseUrl({ env: options.env });
63
+ const baseURL = base.endsWith("/api/v1") ? base : `${base}/api/v1`;
64
+ return {
65
+ ANTHROPIC_BASE_URL: baseURL,
66
+ ANTHROPIC_AUTH_TOKEN: key.trim(),
67
+ ANTHROPIC_CUSTOM_HEADERS: `${INIT_PURPOSE_HEADER_NAME}: ${INIT_PURPOSE_HEADER_VALUE}`,
68
+ };
69
+ }