@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.
- package/LICENSE +202 -0
- package/README.md +28 -0
- package/bin/vendo.mjs +4 -0
- package/dist/ai-sdk.d.ts +35 -0
- package/dist/ai-sdk.js +62 -0
- package/dist/auth-presets/auth-js.d.ts +14 -0
- package/dist/auth-presets/auth-js.js +91 -0
- package/dist/auth-presets/auth0.d.ts +23 -0
- package/dist/auth-presets/auth0.js +98 -0
- package/dist/auth-presets/clerk.d.ts +23 -0
- package/dist/auth-presets/clerk.js +65 -0
- package/dist/auth-presets/conformance.d.ts +42 -0
- package/dist/auth-presets/conformance.js +139 -0
- package/dist/auth-presets/identity.d.ts +56 -0
- package/dist/auth-presets/identity.js +152 -0
- package/dist/auth-presets/index.d.ts +14 -0
- package/dist/auth-presets/index.js +12 -0
- package/dist/auth-presets/jwt.d.ts +16 -0
- package/dist/auth-presets/jwt.js +54 -0
- package/dist/auth-presets/shared.d.ts +35 -0
- package/dist/auth-presets/shared.js +1 -0
- package/dist/auth-presets/supabase.d.ts +42 -0
- package/dist/auth-presets/supabase.js +229 -0
- package/dist/byo-approvals.d.ts +37 -0
- package/dist/byo-approvals.js +189 -0
- package/dist/capability-misses.d.ts +38 -0
- package/dist/capability-misses.js +172 -0
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.js +152 -0
- package/dist/cli/cloud/args.d.ts +3 -0
- package/dist/cli/cloud/args.js +32 -0
- package/dist/cli/cloud/auth.d.ts +17 -0
- package/dist/cli/cloud/auth.js +123 -0
- package/dist/cli/cloud/client.d.ts +28 -0
- package/dist/cli/cloud/client.js +133 -0
- package/dist/cli/cloud/command.d.ts +22 -0
- package/dist/cli/cloud/command.js +73 -0
- package/dist/cli/cloud/deploy.d.ts +26 -0
- package/dist/cli/cloud/deploy.js +187 -0
- package/dist/cli/cloud/device-login.d.ts +32 -0
- package/dist/cli/cloud/device-login.js +259 -0
- package/dist/cli/cloud/index.d.ts +5 -0
- package/dist/cli/cloud/index.js +59 -0
- package/dist/cli/cloud/keys.d.ts +2 -0
- package/dist/cli/cloud/keys.js +31 -0
- package/dist/cli/cloud/members.d.ts +3 -0
- package/dist/cli/cloud/members.js +25 -0
- package/dist/cli/cloud/output.d.ts +4 -0
- package/dist/cli/cloud/output.js +12 -0
- package/dist/cli/cloud/pending-claim.d.ts +29 -0
- package/dist/cli/cloud/pending-claim.js +38 -0
- package/dist/cli/cloud/read.d.ts +3 -0
- package/dist/cli/cloud/read.js +12 -0
- package/dist/cli/cloud/services.d.ts +2 -0
- package/dist/cli/cloud/services.js +9 -0
- package/dist/cli/cloud/session.d.ts +13 -0
- package/dist/cli/cloud/session.js +41 -0
- package/dist/cli/cloud-init.d.ts +60 -0
- package/dist/cli/cloud-init.js +149 -0
- package/dist/cli/dep-versions.d.ts +26 -0
- package/dist/cli/dep-versions.js +77 -0
- package/dist/cli/doctor-codes.d.ts +58 -0
- package/dist/cli/doctor-codes.js +60 -0
- package/dist/cli/doctor-live.d.ts +67 -0
- package/dist/cli/doctor-live.js +169 -0
- package/dist/cli/doctor.d.ts +52 -0
- package/dist/cli/doctor.js +467 -0
- package/dist/cli/eject.d.ts +29 -0
- package/dist/cli/eject.js +141 -0
- package/dist/cli/extract/apply.d.ts +26 -0
- package/dist/cli/extract/apply.js +123 -0
- package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
- package/dist/cli/extract/claude-cli-harness.js +132 -0
- package/dist/cli/extract/claude-harness.d.ts +14 -0
- package/dist/cli/extract/claude-harness.js +127 -0
- package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
- package/dist/cli/extract/codex-cli-harness.js +154 -0
- package/dist/cli/extract/delegate.d.ts +21 -0
- package/dist/cli/extract/delegate.js +77 -0
- package/dist/cli/extract/extraction.d.ts +65 -0
- package/dist/cli/extract/extraction.js +300 -0
- package/dist/cli/extract/gateway-fuel.d.ts +69 -0
- package/dist/cli/extract/gateway-fuel.js +69 -0
- package/dist/cli/extract/harness.d.ts +114 -0
- package/dist/cli/extract/harness.js +84 -0
- package/dist/cli/extract/index.d.ts +9 -0
- package/dist/cli/extract/index.js +9 -0
- package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
- package/dist/cli/extract/npx-engine-harness.js +225 -0
- package/dist/cli/extract/stages.d.ts +149 -0
- package/dist/cli/extract/stages.js +398 -0
- package/dist/cli/framework.d.ts +8 -0
- package/dist/cli/framework.js +36 -0
- package/dist/cli/init-auth.d.ts +67 -0
- package/dist/cli/init-auth.js +142 -0
- package/dist/cli/init-scaffolds.d.ts +28 -0
- package/dist/cli/init-scaffolds.js +235 -0
- package/dist/cli/init.d.ts +115 -0
- package/dist/cli/init.js +1008 -0
- package/dist/cli/mcp/index.d.ts +7 -0
- package/dist/cli/mcp/index.js +59 -0
- package/dist/cli/mcp/registry.d.ts +9 -0
- package/dist/cli/mcp/registry.js +117 -0
- package/dist/cli/mcp/server-json.d.ts +12 -0
- package/dist/cli/mcp/server-json.js +65 -0
- package/dist/cli/mcp/server.schema.json +574 -0
- package/dist/cli/mcp/verify-domain.d.ts +11 -0
- package/dist/cli/mcp/verify-domain.js +48 -0
- package/dist/cli/playground/app/embed-entry.d.ts +20 -0
- package/dist/cli/playground/app/embed-entry.js +46 -0
- package/dist/cli/playground/app/fake-client.d.ts +3 -0
- package/dist/cli/playground/app/fake-client.js +215 -0
- package/dist/cli/playground/app/fixtures.d.ts +47 -0
- package/dist/cli/playground/app/fixtures.js +472 -0
- package/dist/cli/playground/app/main.d.ts +1 -0
- package/dist/cli/playground/app/main.js +108 -0
- package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
- package/dist/cli/playground/app/scenario-mount.js +48 -0
- package/dist/cli/playground/app/scenarios.d.ts +25 -0
- package/dist/cli/playground/app/scenarios.js +152 -0
- package/dist/cli/playground/app/theme-editor.d.ts +13 -0
- package/dist/cli/playground/app/theme-editor.js +149 -0
- package/dist/cli/playground/app/theme-state.d.ts +29 -0
- package/dist/cli/playground/app/theme-state.js +151 -0
- package/dist/cli/playground/bundle.gen.d.ts +2 -0
- package/dist/cli/playground/bundle.gen.js +2 -0
- package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
- package/dist/cli/playground/embed-bundle.gen.js +2 -0
- package/dist/cli/playground.d.ts +33 -0
- package/dist/cli/playground.js +120 -0
- package/dist/cli/pretty.d.ts +48 -0
- package/dist/cli/pretty.js +330 -0
- package/dist/cli/refine.d.ts +43 -0
- package/dist/cli/refine.js +181 -0
- package/dist/cli/semantics.d.ts +31 -0
- package/dist/cli/semantics.js +122 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +184 -0
- package/dist/cli/sync.d.ts +36 -0
- package/dist/cli/sync.js +202 -0
- package/dist/cli/theme/color.d.ts +15 -0
- package/dist/cli/theme/color.js +188 -0
- package/dist/cli/theme/css-vars.d.ts +13 -0
- package/dist/cli/theme/css-vars.js +40 -0
- package/dist/cli/theme/entry-candidates.d.ts +5 -0
- package/dist/cli/theme/entry-candidates.js +22 -0
- package/dist/cli/theme/extract-theme.d.ts +184 -0
- package/dist/cli/theme/extract-theme.js +358 -0
- package/dist/cli/theme/walk.d.ts +2 -0
- package/dist/cli/theme/walk.js +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +333 -0
- package/dist/cloud-apps.d.ts +17 -0
- package/dist/cloud-apps.js +46 -0
- package/dist/cloud-console.d.ts +32 -0
- package/dist/cloud-console.js +72 -0
- package/dist/cloud-tools.d.ts +23 -0
- package/dist/cloud-tools.js +196 -0
- package/dist/connections.d.ts +71 -0
- package/dist/connections.js +192 -0
- package/dist/deployment-identity.d.ts +12 -0
- package/dist/deployment-identity.js +69 -0
- package/dist/dev-creds/model.d.ts +65 -0
- package/dist/dev-creds/model.js +159 -0
- package/dist/dev-creds/resolve.d.ts +36 -0
- package/dist/dev-creds/resolve.js +57 -0
- package/dist/hosted-store.d.ts +52 -0
- package/dist/hosted-store.js +322 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -0
- package/dist/mastra.d.ts +46 -0
- package/dist/mastra.js +88 -0
- package/dist/react.d.ts +10 -0
- package/dist/react.js +32 -0
- package/dist/refine.d.ts +244 -0
- package/dist/refine.js +599 -0
- package/dist/remixable.d.ts +18 -0
- package/dist/remixable.js +42 -0
- package/dist/runtime-capture.d.ts +18 -0
- package/dist/runtime-capture.js +111 -0
- package/dist/sandbox-wire.d.ts +101 -0
- package/dist/sandbox-wire.js +101 -0
- package/dist/sandbox.d.ts +38 -0
- package/dist/sandbox.js +389 -0
- package/dist/server.d.ts +216 -0
- package/dist/server.js +1320 -0
- package/dist/sync-impact.d.ts +14 -0
- package/dist/sync-impact.js +75 -0
- package/dist/turn-liveness.d.ts +33 -0
- package/dist/turn-liveness.js +105 -0
- package/dist/wire/approvals.d.ts +10 -0
- package/dist/wire/approvals.js +59 -0
- package/dist/wire/apps.d.ts +5 -0
- package/dist/wire/apps.js +155 -0
- package/dist/wire/automations.d.ts +5 -0
- package/dist/wire/automations.js +57 -0
- package/dist/wire/box.d.ts +3 -0
- package/dist/wire/box.js +268 -0
- package/dist/wire/connections.d.ts +5 -0
- package/dist/wire/connections.js +47 -0
- package/dist/wire/context.d.ts +22 -0
- package/dist/wire/context.js +190 -0
- package/dist/wire/doctor.d.ts +19 -0
- package/dist/wire/doctor.js +116 -0
- package/dist/wire/misc.d.ts +18 -0
- package/dist/wire/misc.js +227 -0
- package/dist/wire/shared.d.ts +158 -0
- package/dist/wire/shared.js +134 -0
- package/dist/wire/threads.d.ts +3 -0
- package/dist/wire/threads.js +68 -0
- package/package.json +151 -0
- 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
|
+
}
|