@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,114 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * The ExtractionHarness seam (install-dx v1, brainstorm 2026-07-18): Vendo
4
+ * owns the instructions, the deterministic validation, and the artifact
5
+ * contract; a world-class coding agent does the reading. Everything above
6
+ * this seam is vendor-neutral — swapping the harness (Claude Agent SDK today,
7
+ * anything tomorrow) is a config change, not an architecture change.
8
+ */
9
+ /** One drafted polish entry for an extracted tool. The AI pass proposes;
10
+ * deterministic guards in extraction.ts decide what applies. */
11
+ export declare const draftToolSchema: z.ZodObject<{
12
+ name: z.ZodString;
13
+ description: z.ZodString;
14
+ risk: z.ZodOptional<z.ZodEnum<["read", "write", "destructive"]>>;
15
+ critical: z.ZodOptional<z.ZodBoolean>;
16
+ /** false = wake a statically-unclassifiable tool (needs reasoning). */
17
+ disabled: z.ZodOptional<z.ZodBoolean>;
18
+ reasoning: z.ZodOptional<z.ZodString>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ name: string;
21
+ description: string;
22
+ risk?: "read" | "write" | "destructive" | undefined;
23
+ critical?: boolean | undefined;
24
+ disabled?: boolean | undefined;
25
+ reasoning?: string | undefined;
26
+ }, {
27
+ name: string;
28
+ description: string;
29
+ risk?: "read" | "write" | "destructive" | undefined;
30
+ critical?: boolean | undefined;
31
+ disabled?: boolean | undefined;
32
+ reasoning?: string | undefined;
33
+ }>;
34
+ export type DraftTool = z.infer<typeof draftToolSchema>;
35
+ export declare const extractionDraftSchema: z.ZodObject<{
36
+ /** One-paragraph product brief drafted from the codebase. */
37
+ brief: z.ZodString;
38
+ tools: z.ZodArray<z.ZodObject<{
39
+ name: z.ZodString;
40
+ description: z.ZodString;
41
+ risk: z.ZodOptional<z.ZodEnum<["read", "write", "destructive"]>>;
42
+ critical: z.ZodOptional<z.ZodBoolean>;
43
+ /** false = wake a statically-unclassifiable tool (needs reasoning). */
44
+ disabled: z.ZodOptional<z.ZodBoolean>;
45
+ reasoning: z.ZodOptional<z.ZodString>;
46
+ }, "strip", z.ZodTypeAny, {
47
+ name: string;
48
+ description: string;
49
+ risk?: "read" | "write" | "destructive" | undefined;
50
+ critical?: boolean | undefined;
51
+ disabled?: boolean | undefined;
52
+ reasoning?: string | undefined;
53
+ }, {
54
+ name: string;
55
+ description: string;
56
+ risk?: "read" | "write" | "destructive" | undefined;
57
+ critical?: boolean | undefined;
58
+ disabled?: boolean | undefined;
59
+ reasoning?: string | undefined;
60
+ }>, "many">;
61
+ /** API surfaces the static extractor missed — surfaced to the dev, not
62
+ * written anywhere (adding tools is future work on the sync side). */
63
+ missedSurfaces: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
64
+ }, "strip", z.ZodTypeAny, {
65
+ tools: {
66
+ name: string;
67
+ description: string;
68
+ risk?: "read" | "write" | "destructive" | undefined;
69
+ critical?: boolean | undefined;
70
+ disabled?: boolean | undefined;
71
+ reasoning?: string | undefined;
72
+ }[];
73
+ brief: string;
74
+ missedSurfaces?: string[] | undefined;
75
+ }, {
76
+ tools: {
77
+ name: string;
78
+ description: string;
79
+ risk?: "read" | "write" | "destructive" | undefined;
80
+ critical?: boolean | undefined;
81
+ disabled?: boolean | undefined;
82
+ reasoning?: string | undefined;
83
+ }[];
84
+ brief: string;
85
+ missedSurfaces?: string[] | undefined;
86
+ }>;
87
+ export type ExtractionDraft = z.infer<typeof extractionDraftSchema>;
88
+ export interface ExtractionRunInput {
89
+ root: string;
90
+ /** The complete staged instructions, composed by extraction.ts. */
91
+ instructions: string;
92
+ env: Record<string, string | undefined>;
93
+ /** Live narration line (surface discoveries, files being read). */
94
+ onProgress?: (line: string) => void;
95
+ }
96
+ export interface ExtractionHarness {
97
+ /** Stable identifier ("claude-agent-sdk"). */
98
+ id: string;
99
+ /** A short human label of the credential this harness would use
100
+ * ("your Claude Code login", "your ANTHROPIC_API_KEY"), or null when the
101
+ * harness cannot run on this machine. */
102
+ availability(input: {
103
+ root: string;
104
+ env: Record<string, string | undefined>;
105
+ }): Promise<string | null>;
106
+ /** Run the instructions and return the agent's final text (extraction.ts
107
+ * parses and validates; the harness never interprets the draft). */
108
+ run(input: ExtractionRunInput): Promise<string>;
109
+ }
110
+ /** Extract a stage artifact from an agent's final text: prefer a fenced
111
+ * block, else try each balanced object span until one validates against the
112
+ * stage's schema. Throws on unparseable output. */
113
+ export declare function parseArtifact<Schema extends z.ZodTypeAny>(text: string, schema: Schema): z.infer<Schema>;
114
+ export declare function parseDraft(text: string): ExtractionDraft;
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * The ExtractionHarness seam (install-dx v1, brainstorm 2026-07-18): Vendo
4
+ * owns the instructions, the deterministic validation, and the artifact
5
+ * contract; a world-class coding agent does the reading. Everything above
6
+ * this seam is vendor-neutral — swapping the harness (Claude Agent SDK today,
7
+ * anything tomorrow) is a config change, not an architecture change.
8
+ */
9
+ /** One drafted polish entry for an extracted tool. The AI pass proposes;
10
+ * deterministic guards in extraction.ts decide what applies. */
11
+ export const draftToolSchema = z.object({
12
+ name: z.string().min(1),
13
+ description: z.string().min(1).max(500),
14
+ risk: z.enum(["read", "write", "destructive"]).optional(),
15
+ critical: z.boolean().optional(),
16
+ /** false = wake a statically-unclassifiable tool (needs reasoning). */
17
+ disabled: z.boolean().optional(),
18
+ reasoning: z.string().max(500).optional(),
19
+ });
20
+ export const extractionDraftSchema = z.object({
21
+ /** One-paragraph product brief drafted from the codebase. */
22
+ brief: z.string().min(1).max(4000),
23
+ tools: z.array(draftToolSchema),
24
+ /** API surfaces the static extractor missed — surfaced to the dev, not
25
+ * written anywhere (adding tools is future work on the sync side). */
26
+ missedSurfaces: z.array(z.string().max(300)).optional(),
27
+ });
28
+ /** Walk balanced `{…}` spans (string-aware) starting at each `{`, returning
29
+ * each complete candidate — so prose containing stray braces around the real
30
+ * JSON object cannot poison the parse (Greptile P2 on #363). */
31
+ function* balancedObjectSpans(text) {
32
+ for (let start = text.indexOf("{"); start !== -1; start = text.indexOf("{", start + 1)) {
33
+ let depth = 0;
34
+ let inString = false;
35
+ for (let index = start; index < text.length; index += 1) {
36
+ const char = text[index];
37
+ if (inString) {
38
+ if (char === "\\")
39
+ index += 1;
40
+ else if (char === '"')
41
+ inString = false;
42
+ continue;
43
+ }
44
+ if (char === '"')
45
+ inString = true;
46
+ else if (char === "{")
47
+ depth += 1;
48
+ else if (char === "}") {
49
+ depth -= 1;
50
+ if (depth === 0) {
51
+ yield text.slice(start, index + 1);
52
+ break;
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ /** Extract a stage artifact from an agent's final text: prefer a fenced
59
+ * block, else try each balanced object span until one validates against the
60
+ * stage's schema. Throws on unparseable output. */
61
+ export function parseArtifact(text, schema) {
62
+ const fenced = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
63
+ if (fenced?.[1] !== undefined) {
64
+ return schema.parse(JSON.parse(fenced[1]));
65
+ }
66
+ let firstError = new Error("no JSON object found in the agent's output");
67
+ let attempts = 0;
68
+ for (const span of balancedObjectSpans(text)) {
69
+ if (attempts >= 20)
70
+ break;
71
+ attempts += 1;
72
+ try {
73
+ return schema.parse(JSON.parse(span));
74
+ }
75
+ catch (error) {
76
+ if (attempts === 1)
77
+ firstError = error;
78
+ }
79
+ }
80
+ throw firstError;
81
+ }
82
+ export function parseDraft(text) {
83
+ return parseArtifact(text, extractionDraftSchema);
84
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Public seam for the extraction pipeline pieces the corpus AI eval matrix
3
+ * consumes (install-dx lane 3). Additive re-exports only — the pipeline
4
+ * itself lives in the sibling modules and is owned by the init flow.
5
+ */
6
+ export { draftToolSchema, extractionDraftSchema, parseDraft, type DraftTool, type ExtractionDraft, type ExtractionHarness, type ExtractionRunInput, } from "./harness.js";
7
+ export { applyDraft, composeInstructions } from "./extraction.js";
8
+ export { runStagedExtraction, staticToolSchema, type StagedExtractionInput, type StagedExtractionResult, type StaticTool, } from "./stages.js";
9
+ export { claudeHarness, type ClaudeHarnessOptions } from "./claude-harness.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Public seam for the extraction pipeline pieces the corpus AI eval matrix
3
+ * consumes (install-dx lane 3). Additive re-exports only — the pipeline
4
+ * itself lives in the sibling modules and is owned by the init flow.
5
+ */
6
+ export { draftToolSchema, extractionDraftSchema, parseDraft, } from "./harness.js";
7
+ export { applyDraft, composeInstructions } from "./extraction.js";
8
+ export { runStagedExtraction, staticToolSchema, } from "./stages.js";
9
+ export { claudeHarness } from "./claude-harness.js";
@@ -0,0 +1,73 @@
1
+ import { type ExecFileException } from "node:child_process";
2
+ import type { ExtractionHarness } from "./harness.js";
3
+ /**
4
+ * Last-resort extraction harness: nothing Claude-shaped is installed on the
5
+ * dev's machine (no Agent SDK, no `claude` binary, no `codex` binary), but
6
+ * they do have a usable credential — so init fetches Claude Code itself via
7
+ * `npm exec` rather than degrading straight to the honest skip. This is the
8
+ * fourth and final rung of the ladder in extraction.ts.
9
+ *
10
+ * Cross-task contract (a parallel task builds `@vendoai/engine` to this exact
11
+ * shape): `npm exec --yes @vendoai/engine@<PINNED_VERSION> -- run`, job JSON
12
+ * `{ instructions, root }` on the child's stdin, credentials/base-url/headers
13
+ * ride the child's process env (never the job JSON), stdout is EXACTLY the
14
+ * agent's final text, stderr is progress/narration, exit 0 = success. The
15
+ * version is pinned exact (never a range) so this rung's behavior can't
16
+ * drift out from under init on a machine with no local install to pin
17
+ * instead.
18
+ *
19
+ * Availability deliberately never touches npm or the network — it is called
20
+ * eagerly for every rung on every `vendo init`, and a probe here would mean
21
+ * every init pays an npm-registry round trip just to build the "AI polish?"
22
+ * prompt. The ~250MB download surprise is disclosed instead via the
23
+ * run-time notice below, right before the first real network access.
24
+ *
25
+ * Gateway fuel: mirrors claude-cli-harness.ts — when the dev has none of
26
+ * ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, or
27
+ * ANTHROPIC_BASE_URL (the corporate-gateway/custom-endpoint path) but
28
+ * VENDO_API_KEY is set, the child runs against Vendo Cloud's model gateway
29
+ * instead of degrading to unavailable (see gateway-fuel.ts). Own credential
30
+ * always wins — availability() must label these honestly (not as "Vendo
31
+ * Cloud key") since composeGatewayFuel itself refuses to overlay onto any of
32
+ * them; a wrong label here would make the consent prompt lie about what
33
+ * run() actually does.
34
+ */
35
+ export declare const ENGINE_PACKAGE_NAME = "@vendoai/engine";
36
+ export declare const ENGINE_PACKAGE_VERSION = "0.1.0";
37
+ interface ExecResult {
38
+ stdout: string;
39
+ stderr: string;
40
+ code: number;
41
+ }
42
+ type Exec = (args: string[], options: {
43
+ cwd: string;
44
+ env: NodeJS.ProcessEnv;
45
+ input: string;
46
+ onStderrLine?: (line: string) => void;
47
+ }) => Promise<ExecResult>;
48
+ /** Pure mapping from a completed `execFile` callback into an ExecResult —
49
+ * pulled out of the real spawn so the four ways `error` shows up (a normal
50
+ * nonzero exit, a real spawn-layer failure, the RUN_TIMEOUT_MS kill, or the
51
+ * MAX_BUFFER_BYTES kill) each get direct unit coverage without actually
52
+ * spawning anything (mirrors resolveCodexExecResult in
53
+ * codex-cli-harness.ts). The four are easy to conflate — all leave
54
+ * `error.code` non-numeric — but they need different messages: a killed
55
+ * 15-minute extraction run is not npm being uninstalled, and a >10MB
56
+ * narration stream is not a timeout. */
57
+ export declare function resolveNpmExecResult(error: ExecFileException | null, stdout: string, stderr: string): ExecResult;
58
+ /** Line-buffered splitter for a live-streaming descriptor, extracted out of
59
+ * execNpmEngine so it's directly unit-testable without a real child
60
+ * process: a line split across two `data` chunks, and — the bug this
61
+ * fixes — a trailing line with no final newline, which `flush()` still
62
+ * delivers instead of silently dropping the child's last progress line
63
+ * when the stream ends without one. */
64
+ export declare function createLineSplitter(onLine: (line: string) => void): {
65
+ push(chunk: string): void;
66
+ flush(): void;
67
+ };
68
+ export interface NpxEngineHarnessOptions {
69
+ /** Test seam. */
70
+ exec?: Exec;
71
+ }
72
+ export declare function npxEngineHarness(options?: NpxEngineHarnessOptions): ExtractionHarness;
73
+ export {};
@@ -0,0 +1,225 @@
1
+ import { execFile } from "node:child_process";
2
+ import { composeGatewayFuel, hasOwnAnthropicEnvOverride } from "./gateway-fuel.js";
3
+ /**
4
+ * Last-resort extraction harness: nothing Claude-shaped is installed on the
5
+ * dev's machine (no Agent SDK, no `claude` binary, no `codex` binary), but
6
+ * they do have a usable credential — so init fetches Claude Code itself via
7
+ * `npm exec` rather than degrading straight to the honest skip. This is the
8
+ * fourth and final rung of the ladder in extraction.ts.
9
+ *
10
+ * Cross-task contract (a parallel task builds `@vendoai/engine` to this exact
11
+ * shape): `npm exec --yes @vendoai/engine@<PINNED_VERSION> -- run`, job JSON
12
+ * `{ instructions, root }` on the child's stdin, credentials/base-url/headers
13
+ * ride the child's process env (never the job JSON), stdout is EXACTLY the
14
+ * agent's final text, stderr is progress/narration, exit 0 = success. The
15
+ * version is pinned exact (never a range) so this rung's behavior can't
16
+ * drift out from under init on a machine with no local install to pin
17
+ * instead.
18
+ *
19
+ * Availability deliberately never touches npm or the network — it is called
20
+ * eagerly for every rung on every `vendo init`, and a probe here would mean
21
+ * every init pays an npm-registry round trip just to build the "AI polish?"
22
+ * prompt. The ~250MB download surprise is disclosed instead via the
23
+ * run-time notice below, right before the first real network access.
24
+ *
25
+ * Gateway fuel: mirrors claude-cli-harness.ts — when the dev has none of
26
+ * ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, or
27
+ * ANTHROPIC_BASE_URL (the corporate-gateway/custom-endpoint path) but
28
+ * VENDO_API_KEY is set, the child runs against Vendo Cloud's model gateway
29
+ * instead of degrading to unavailable (see gateway-fuel.ts). Own credential
30
+ * always wins — availability() must label these honestly (not as "Vendo
31
+ * Cloud key") since composeGatewayFuel itself refuses to overlay onto any of
32
+ * them; a wrong label here would make the consent prompt lie about what
33
+ * run() actually does.
34
+ */
35
+ export const ENGINE_PACKAGE_NAME = "@vendoai/engine";
36
+ export const ENGINE_PACKAGE_VERSION = "0.1.0";
37
+ // A one-time ~250MB package fetch plus the extraction stages themselves (which
38
+ // can already run for minutes over a real codebase on the other rungs) needs
39
+ // more headroom than the PATH-binary rungs' 10-minute budget.
40
+ const RUN_TIMEOUT_MS = 15 * 60 * 1_000;
41
+ const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
42
+ /** Pure mapping from a completed `execFile` callback into an ExecResult —
43
+ * pulled out of the real spawn so the four ways `error` shows up (a normal
44
+ * nonzero exit, a real spawn-layer failure, the RUN_TIMEOUT_MS kill, or the
45
+ * MAX_BUFFER_BYTES kill) each get direct unit coverage without actually
46
+ * spawning anything (mirrors resolveCodexExecResult in
47
+ * codex-cli-harness.ts). The four are easy to conflate — all leave
48
+ * `error.code` non-numeric — but they need different messages: a killed
49
+ * 15-minute extraction run is not npm being uninstalled, and a >10MB
50
+ * narration stream is not a timeout. */
51
+ export function resolveNpmExecResult(error, stdout, stderr) {
52
+ if (error === null)
53
+ return { stdout, stderr, code: 0 };
54
+ if (typeof error.code === "number")
55
+ return { stdout, stderr, code: error.code };
56
+ if (error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
57
+ // execFile's `maxBuffer` kill also sets error.killed, so this must be
58
+ // checked BEFORE the killed branch below — a >10MB narration stream is
59
+ // not the 15-minute timeout, and labeling it as one would send the dev
60
+ // chasing a slowness that isn't there.
61
+ const megabytes = Math.round(MAX_BUFFER_BYTES / (1024 * 1024));
62
+ return {
63
+ stdout: "",
64
+ stderr: `npm exec output exceeded the ${megabytes}MB buffer — the engine's narration stream was `
65
+ + "larger than expected, so the child was killed before finishing",
66
+ code: 1,
67
+ };
68
+ }
69
+ if (error.killed === true) {
70
+ // execFile's `timeout` option SIGTERMs a still-running child — a
71
+ // legitimate long extraction run, not a broken npm install. This must be
72
+ // checked before the spawn-failure branch below: a kill leaves
73
+ // error.code null (not a string), so it would otherwise fall into "npm
74
+ // could not be launched" and mislabel the dev's own long-running job as
75
+ // a missing npm.
76
+ const minutes = Math.round(RUN_TIMEOUT_MS / 60_000);
77
+ return {
78
+ stdout: "",
79
+ stderr: `npm exec was killed for exceeding the ${minutes}-minute timeout`
80
+ + (error.signal !== undefined ? ` (${error.signal})` : ""),
81
+ code: 1,
82
+ };
83
+ }
84
+ if (typeof error.code === "string") {
85
+ // A real spawn-layer failure (error.code is an errno string like
86
+ // "ENOENT" or "EACCES", set when the OS never launched the child at
87
+ // all) — npm itself could not be launched: not installed, not on PATH,
88
+ // or not executable. execFile's own stdout/stderr are empty in this
89
+ // case, so the actionable detail has to come from error.message.
90
+ return {
91
+ stdout: "",
92
+ stderr: `npm could not be launched (${error.message}) — is npm installed and on PATH?`,
93
+ code: 1,
94
+ };
95
+ }
96
+ // Any other shape (no numeric exit code, not killed, no string spawn
97
+ // code) — don't fabricate a diagnosis; forward whatever npm itself wrote.
98
+ // An offline registry lands here: npm still launches and exits non-zero
99
+ // with its own descriptive stderr (e.g. "npm error code ENOTFOUND"),
100
+ // which run()'s nonzero-exit handling forwards to the dev verbatim.
101
+ return { stdout, stderr, code: 1 };
102
+ }
103
+ /** Line-buffered splitter for a live-streaming descriptor, extracted out of
104
+ * execNpmEngine so it's directly unit-testable without a real child
105
+ * process: a line split across two `data` chunks, and — the bug this
106
+ * fixes — a trailing line with no final newline, which `flush()` still
107
+ * delivers instead of silently dropping the child's last progress line
108
+ * when the stream ends without one. */
109
+ export function createLineSplitter(onLine) {
110
+ let buffer = "";
111
+ return {
112
+ push(chunk) {
113
+ buffer += chunk;
114
+ let index = buffer.indexOf("\n");
115
+ while (index !== -1) {
116
+ const line = buffer.slice(0, index);
117
+ buffer = buffer.slice(index + 1);
118
+ if (line.length > 0)
119
+ onLine(line);
120
+ index = buffer.indexOf("\n");
121
+ }
122
+ },
123
+ flush() {
124
+ if (buffer.length > 0)
125
+ onLine(buffer);
126
+ buffer = "";
127
+ },
128
+ };
129
+ }
130
+ function execNpmEngine(args, options) {
131
+ return new Promise((resolve) => {
132
+ // stderr is progress/narration per the child protocol — forward it line
133
+ // by line as it streams in, not just once the process exits; flush()
134
+ // in the callback below delivers any trailing partial line too.
135
+ const splitter = createLineSplitter((line) => options.onStderrLine?.(line));
136
+ const child = execFile("npm", args, { cwd: options.cwd, env: options.env, timeout: RUN_TIMEOUT_MS, maxBuffer: MAX_BUFFER_BYTES }, (error, stdout, stderr) => {
137
+ splitter.flush();
138
+ resolve(resolveNpmExecResult(error, stdout, stderr));
139
+ });
140
+ child.stderr?.on("data", (chunk) => splitter.push(chunk.toString()));
141
+ // If the child exits (or is killed by the timeout) before the stdin
142
+ // write drains, Node emits an unhandled EPIPE 'error' on this stream —
143
+ // with no listener, that's an uncaughtException that aborts all of
144
+ // `vendo init`, bypassing extraction.ts's graceful per-rung degradation.
145
+ // The real outcome always arrives via the execFile callback's exit code
146
+ // + stderr above, so this handler is intentionally a no-op.
147
+ child.stdin?.on("error", () => { });
148
+ child.stdin?.write(options.input);
149
+ child.stdin?.end();
150
+ });
151
+ }
152
+ function isSet(value) {
153
+ return typeof value === "string" && value.trim().length > 0;
154
+ }
155
+ // Every label this rung can return carries the download disclosure, since
156
+ // (unlike the PATH-binary rungs) a fetch always happens here.
157
+ const DOWNLOAD_NOTE = "via the Vendo engine, ~250MB one-time download";
158
+ const DOWNLOAD_SUFFIX = ` (${DOWNLOAD_NOTE})`;
159
+ export function npxEngineHarness(options = {}) {
160
+ const exec = options.exec ?? execNpmEngine;
161
+ return {
162
+ id: "npx-engine",
163
+ async availability({ env }) {
164
+ // The child spawns with {...process.env, ...input.env} (see run()), so
165
+ // the label must be judged against that SAME merged view — an ambient
166
+ // (process.env) own credential is the one the child would actually
167
+ // use, even when the caller's partial env carries only VENDO_API_KEY.
168
+ const merged = { ...process.env, ...env };
169
+ if (isSet(merged["ANTHROPIC_API_KEY"]))
170
+ return `your ANTHROPIC_API_KEY${DOWNLOAD_SUFFIX}`;
171
+ // The corporate-gateway/custom-endpoint env vars are an own credential
172
+ // too (see gateway-fuel.ts's INVARIANT) — composeGatewayFuel refuses to
173
+ // overlay onto them regardless of what run() passes, so labeling this
174
+ // rung "your Vendo Cloud key" here would be a lie about what actually
175
+ // runs. Per-var labels (same priority order as claude-cli-harness.ts)
176
+ // so the consent line names the credential that's really in play.
177
+ if (hasOwnAnthropicEnvOverride(merged)) {
178
+ if (isSet(merged["ANTHROPIC_AUTH_TOKEN"]))
179
+ return `your ANTHROPIC_AUTH_TOKEN${DOWNLOAD_SUFFIX}`;
180
+ if (isSet(merged["CLAUDE_CODE_OAUTH_TOKEN"]))
181
+ return `your CLAUDE_CODE_OAUTH_TOKEN${DOWNLOAD_SUFFIX}`;
182
+ return `your ANTHROPIC_BASE_URL${DOWNLOAD_SUFFIX}`;
183
+ }
184
+ if (isSet(merged["VENDO_API_KEY"])) {
185
+ return `your Vendo Cloud key (managed inference, ${DOWNLOAD_NOTE})`;
186
+ }
187
+ return null;
188
+ },
189
+ async run(input) {
190
+ // Merge FIRST, then guard — the child is spawned with the caller's env
191
+ // over process.env, so the own-credential verdict and composeGatewayFuel
192
+ // must evaluate that same merged env. Guarding input.env alone would
193
+ // let the overlay clobber an ambient (process.env) BYO endpoint the
194
+ // child would otherwise have used. composeGatewayFuel already refuses
195
+ // to overlay onto these env vars on its own (defense in depth) —
196
+ // computed explicitly here too so this rung's own-credential verdict
197
+ // matches availability()'s exactly, the same belt-and-suspenders style
198
+ // as claude-cli-harness.ts.
199
+ const merged = { ...process.env, ...input.env };
200
+ const hasOwnKey = isSet(merged["ANTHROPIC_API_KEY"]) || hasOwnAnthropicEnvOverride(merged);
201
+ const overlay = composeGatewayFuel({ env: merged, ownCredentialAvailable: hasOwnKey });
202
+ // Visible-never-silent: the ~250MB fetch is a real surprise on a
203
+ // machine with nothing installed, so it's disclosed up front, before
204
+ // the child (and its network access) ever starts.
205
+ input.onProgress?.(`Fetching ${ENGINE_PACKAGE_NAME}@${ENGINE_PACKAGE_VERSION} via npm exec (~250MB one-time download; `
206
+ + "npm caches it locally, so later runs skip the download)…");
207
+ const job = JSON.stringify({ instructions: input.instructions, root: input.root });
208
+ const args = ["exec", "--yes", `${ENGINE_PACKAGE_NAME}@${ENGINE_PACKAGE_VERSION}`, "--", "run"];
209
+ // Forward the caller's env so a key present only in the passed map
210
+ // (not process.env) still authenticates the child; gateway fuel (if
211
+ // applicable) wins last — mirrors claude-cli-harness.ts.
212
+ const result = await exec(args, {
213
+ cwd: input.root,
214
+ env: { ...merged, ...overlay },
215
+ input: job,
216
+ onStderrLine: input.onProgress,
217
+ });
218
+ if (result.code !== 0) {
219
+ throw new Error(`npm exec ${ENGINE_PACKAGE_NAME}@${ENGINE_PACKAGE_VERSION} exited with code ${result.code}: `
220
+ + `${result.stderr.trim() || "(no stderr)"}`);
221
+ }
222
+ return result.stdout;
223
+ },
224
+ };
225
+ }
@@ -0,0 +1,149 @@
1
+ import { z } from "zod";
2
+ import { type ExtractionDraft, type ExtractionHarness } from "./harness.js";
3
+ import { modelThemeSchema, type ThemeSlotValues } from "../theme/extract-theme.js";
4
+ /**
5
+ * The staged extraction pipeline (install-dx, PostHog lesson: narrow stages
6
+ * beat one-shot). Four required passes plus one optional pass, all over the
7
+ * SAME harness seam — each is one `harness.run(instructions)` with its own
8
+ * narrow instructions and its own zod-validated artifact, written to
9
+ * `.vendo/data/extract/<stage>.json` so a failed run is diagnosable stage by
10
+ * stage:
11
+ *
12
+ * 1. survey — map the repo: frameworks, where the API surfaces live, and a
13
+ * grouping of the static tool list into surfaces (cheap/fast; respects a
14
+ * VENDO_EXTRACTION_SURVEY_MODEL override).
15
+ * 2. draft — one focused pass per surface, drafting judgment for just that
16
+ * surface's tools. Surfaces run sequentially (rate-limit friendly); a
17
+ * failed surface is SKIPPED with an honest note, never aborting the run.
18
+ * 3. cross-check — one pass over the combined draft for naming consistency,
19
+ * duplicate coverage, and risk sanity. It may only AMEND entries within
20
+ * the same schema (unknown names ignored, omitted entries stand); a
21
+ * failure degrades to the uncross-checked drafts.
22
+ * 4. brief — drafted from what the survey and drafts learned; a failure
23
+ * keeps the current brief.
24
+ * 5. theme — OPTIONAL, only when the caller supplies `theme` input and the
25
+ * allowlist left brand slots unfilled: fills those slots the same way the
26
+ * old theme model pass did, but over this harness seam (Read/Glob/Grep the
27
+ * repo instead of a fixed evidence-file set). A failure degrades to a note
28
+ * — the exact reads and neutral defaults still stand.
29
+ *
30
+ * The combined output feeds the EXISTING applyDraft guards in extraction.ts
31
+ * unchanged — deterministic verification stays the single gate. Nothing here
32
+ * assumes a vendor; the model override rides VENDO_EXTRACTION_MODEL, which
33
+ * every harness already honors.
34
+ */
35
+ export declare const BRIEF_TEMPLATE = "Describe this product, its users, and the jobs the agent should help them complete.";
36
+ /** Static facts don't clutter passes that only need names — kept small. */
37
+ export declare const staticToolSchema: z.ZodObject<{
38
+ name: z.ZodString;
39
+ description: z.ZodOptional<z.ZodString>;
40
+ risk: z.ZodOptional<z.ZodEnum<["read", "write", "destructive"]>>;
41
+ disabled: z.ZodOptional<z.ZodBoolean>;
42
+ method: z.ZodOptional<z.ZodString>;
43
+ path: z.ZodOptional<z.ZodString>;
44
+ }, "strip", z.ZodTypeAny, {
45
+ name: string;
46
+ path?: string | undefined;
47
+ method?: string | undefined;
48
+ risk?: "read" | "write" | "destructive" | undefined;
49
+ description?: string | undefined;
50
+ disabled?: boolean | undefined;
51
+ }, {
52
+ name: string;
53
+ path?: string | undefined;
54
+ method?: string | undefined;
55
+ risk?: "read" | "write" | "destructive" | undefined;
56
+ description?: string | undefined;
57
+ disabled?: boolean | undefined;
58
+ }>;
59
+ export type StaticTool = z.infer<typeof staticToolSchema>;
60
+ export declare const surveySchema: z.ZodObject<{
61
+ /** Frameworks/routers the surveyor identified (context for later stages). */
62
+ frameworks: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
63
+ surfaces: z.ZodArray<z.ZodObject<{
64
+ name: z.ZodString;
65
+ /** One line on where the surface lives in the code. */
66
+ note: z.ZodOptional<z.ZodString>;
67
+ tools: z.ZodArray<z.ZodString, "many">;
68
+ }, "strip", z.ZodTypeAny, {
69
+ tools: string[];
70
+ name: string;
71
+ note?: string | undefined;
72
+ }, {
73
+ tools: string[];
74
+ name: string;
75
+ note?: string | undefined;
76
+ }>, "many">;
77
+ }, "strip", z.ZodTypeAny, {
78
+ surfaces: {
79
+ tools: string[];
80
+ name: string;
81
+ note?: string | undefined;
82
+ }[];
83
+ frameworks?: string[] | undefined;
84
+ }, {
85
+ surfaces: {
86
+ tools: string[];
87
+ name: string;
88
+ note?: string | undefined;
89
+ }[];
90
+ frameworks?: string[] | undefined;
91
+ }>;
92
+ export type Survey = z.infer<typeof surveySchema>;
93
+ export declare function staticFacts(tools: StaticTool[]): string;
94
+ export declare function composeInstructions(tools: StaticTool[], appName: string, surface?: {
95
+ name: string;
96
+ note?: string;
97
+ }): string;
98
+ /**
99
+ * Instructions for the OPTIONAL theme stage — the SOLE copy of the theme
100
+ * judgment rules (extract-theme.ts's old model pass and its prompt were
101
+ * deleted when the deterministic split landed; that file now owns only the
102
+ * exact pass, validators, and assembly).
103
+ */
104
+ export declare function composeThemeInstructions(input: {
105
+ needed: string[];
106
+ alreadyExact: Record<string, string>;
107
+ evidencePaths: string[];
108
+ appName: string;
109
+ }): string;
110
+ interface NormalizedSurface {
111
+ name: string;
112
+ slug: string;
113
+ note?: string;
114
+ tools: StaticTool[];
115
+ }
116
+ /** Deterministic normalization of the survey grouping: unknown names are
117
+ * dropped, a tool claimed twice keeps its first surface, unassigned tools
118
+ * land in a catch-all pass, and a runaway surface count is merged down. The
119
+ * drafting stage always covers EVERY static tool exactly once. */
120
+ export declare function normalizeSurfaces(survey: Survey | null, tools: StaticTool[], notes: string[]): NormalizedSurface[];
121
+ export interface StagedExtractionInput {
122
+ root: string;
123
+ env: Record<string, string | undefined>;
124
+ harness: ExtractionHarness;
125
+ tools: StaticTool[];
126
+ appName: string;
127
+ onProgress?: (line: string) => void;
128
+ /** Optional theme stage input — omitted entirely when the caller has no
129
+ * theme extraction to do (e.g. init running without a theme pass). */
130
+ theme?: {
131
+ needed: Array<keyof ThemeSlotValues>;
132
+ alreadyExact: Record<string, string>;
133
+ evidencePaths: string[];
134
+ };
135
+ }
136
+ export interface StagedExtractionResult {
137
+ draft: ExtractionDraft;
138
+ /** Honest degradation notes (skipped surfaces, failed cross-check, …). */
139
+ notes: string[];
140
+ /** false = the brief stage failed and draft.brief is the pre-existing one. */
141
+ briefFromStage: boolean;
142
+ /** Present only when the theme stage ran and succeeded. */
143
+ theme?: z.infer<typeof modelThemeSchema>;
144
+ }
145
+ /** Orchestrate the staged pipeline. Throws only when no stage produced
146
+ * anything usable (the error names the failed stage); partial failures
147
+ * degrade with notes instead. */
148
+ export declare function runStagedExtraction(input: StagedExtractionInput): Promise<StagedExtractionResult>;
149
+ export {};