depbot-policy 0.1.0 → 0.2.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/README.md +121 -2
- package/dist/bin.js +1 -1
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +77 -5
- package/dist/describe-models.d.ts +61 -0
- package/dist/describe-models.js +66 -0
- package/dist/describe.d.ts +116 -0
- package/dist/describe.js +262 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ npx depbot-policy generate
|
|
|
39
39
|
- [Why](#why)
|
|
40
40
|
- [Quick start](#quick-start)
|
|
41
41
|
- [CLI](#cli)
|
|
42
|
+
- [Writing a policy with AI](#writing-a-policy-with-ai)
|
|
42
43
|
- [Policy reference](#policy-reference)
|
|
43
44
|
- [What gets generated](#what-gets-generated)
|
|
44
45
|
- [Repository setup](#repository-setup)
|
|
@@ -86,6 +87,9 @@ git commit -m "Manage Dependabot with depbot-policy"
|
|
|
86
87
|
|
|
87
88
|
Then do the one-time [repository setup](#repository-setup), so that auto-merge waits for CI.
|
|
88
89
|
|
|
90
|
+
Prefer to describe what you want? `npx depbot-policy init --describe "…"` has an AI model (Claude
|
|
91
|
+
or Gemini) write the policy for you. See [Writing a policy with AI](#writing-a-policy-with-ai).
|
|
92
|
+
|
|
89
93
|
To change anything later, edit `depbot.policy.yml` and run `npx depbot-policy generate` again.
|
|
90
94
|
Don't edit the generated files by hand. [`check`](#keeping-files-in-sync-in-ci) catches that.
|
|
91
95
|
|
|
@@ -95,7 +99,7 @@ Don't edit the generated files by hand. [`check`](#keeping-files-in-sync-in-ci)
|
|
|
95
99
|
depbot-policy <command> [options]
|
|
96
100
|
|
|
97
101
|
Commands:
|
|
98
|
-
init Create a starter depbot.policy.yml
|
|
102
|
+
init Create a starter depbot.policy.yml, or one written by AI with --describe
|
|
99
103
|
generate Write .github/dependabot.yml and the auto-merge workflow
|
|
100
104
|
check Fail if the policy is invalid or the generated files are out of date
|
|
101
105
|
reviewer Print this week's reviewer from review.rotation
|
|
@@ -104,6 +108,17 @@ Options:
|
|
|
104
108
|
--policy <file> Policy file (default: depbot.policy.yml)
|
|
105
109
|
--out <dir> Repository root to write to or check (default: .)
|
|
106
110
|
--dry-run generate: print the files instead of writing them
|
|
111
|
+
--describe <text> init: have an AI model write the policy from a description
|
|
112
|
+
(needs ANTHROPIC_API_KEY or GEMINI_API_KEY)
|
|
113
|
+
--model <id> init --describe: which model to use (default: claude-opus-5-5)
|
|
114
|
+
Claude models:
|
|
115
|
+
claude-opus-5-5 Recommended: best balance of quality and cost, about 3–5¢ per policy
|
|
116
|
+
claude-sonnet-5-5 Faster and cheaper, about 2¢ per policy
|
|
117
|
+
claude-haiku-5-5 Fastest and cheapest, well under 1¢ per policy
|
|
118
|
+
claude-fable-5-1 Most capable and most expensive, about 8–13¢ per policy
|
|
119
|
+
Gemini models:
|
|
120
|
+
gemini-3-flash-preview Fast and cheap, under 1¢ per policy (free tier available)
|
|
121
|
+
gemini-3.1-pro-preview More capable, about 2–3¢ per policy
|
|
107
122
|
--force init: overwrite an existing policy file
|
|
108
123
|
--date <date> reviewer: use this date instead of today (e.g. 2026-10-12)
|
|
109
124
|
-h, --help Show this help
|
|
@@ -113,7 +128,7 @@ Options:
|
|
|
113
128
|
| Command | Exit code |
|
|
114
129
|
|---|---|
|
|
115
130
|
| Success | `0` |
|
|
116
|
-
| Invalid policy,
|
|
131
|
+
| Invalid policy, `check` found missing or outdated files, or `--describe` failed | `1` |
|
|
117
132
|
| Wrong usage (unknown command or option, bad `--date`) | `2` |
|
|
118
133
|
|
|
119
134
|
Examples:
|
|
@@ -128,6 +143,73 @@ npx depbot-policy reviewer --date 2026-12-28 # ...and in the last week o
|
|
|
128
143
|
You can also install it as a dev dependency (`npm install --save-dev depbot-policy`) and call
|
|
129
144
|
`depbot-policy` from npm scripts.
|
|
130
145
|
|
|
146
|
+
## Writing a policy with AI
|
|
147
|
+
|
|
148
|
+
Describe your setup in plain words and let an AI model (Claude or Gemini) write the policy:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
export ANTHROPIC_API_KEY=sk-ant-... # or: export GEMINI_API_KEY=AIza... with a gemini model
|
|
152
|
+
npx depbot-policy init --describe "pnpm monorepo with apps in /apps/web and /apps/api, \
|
|
153
|
+
plus Dockerfiles. Auto-merge patch updates to dev dependencies. Never update react \
|
|
154
|
+
until we migrate to 19. alice and bob-smith take turns reviewing."
|
|
155
|
+
|
|
156
|
+
npx depbot-policy init --describe "..." --model gemini-3-flash-preview # use Gemini instead
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Example output (the model's notes vary):
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
Asking Claude Opus 5.5 to write the policy...
|
|
163
|
+
Note: Assumed a weekly schedule, since none was given.
|
|
164
|
+
Created depbot.policy.yml. Review it, then run: depbot-policy generate
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The [playground](#web-playground) has the same feature under **Describe with AI**.
|
|
168
|
+
|
|
169
|
+
**How it works.** The model only writes the *policy*. It never writes the workflow or
|
|
170
|
+
`dependabot.yml`.
|
|
171
|
+
|
|
172
|
+
1. The model fills in a simplified version of the policy schema and adds short notes about any
|
|
173
|
+
assumptions it made. Claude uses
|
|
174
|
+
[structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs);
|
|
175
|
+
Gemini uses JSON output with a response schema. Both are held to the same schema.
|
|
176
|
+
2. depbot-policy turns that into YAML and validates it with the same rules as a hand-written
|
|
177
|
+
policy.
|
|
178
|
+
3. If anything fails validation, the errors go back to the model once to fix. Anything still wrong
|
|
179
|
+
is reported with its line and column, like any other policy error.
|
|
180
|
+
4. You review the policy. `generate` then produces the files with the same deterministic code as
|
|
181
|
+
always.
|
|
182
|
+
|
|
183
|
+
So the safety properties of the generated workflow (pinned actions, minimal permissions, the
|
|
184
|
+
Dependabot-only checks, no major auto-merges) don't depend on the model.
|
|
185
|
+
|
|
186
|
+
**Details:**
|
|
187
|
+
|
|
188
|
+
- **Models:** pick one with `--model <id>` (or the Model menu in the playground). Claude models run
|
|
189
|
+
at low effort. Costs are rough list-price estimates per policy:
|
|
190
|
+
|
|
191
|
+
| Model | `--model` | About |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| Claude Opus 5.5 (default) | `claude-opus-5-5` | 3–5¢, best balance of quality and cost |
|
|
194
|
+
| Claude Sonnet 5.5 | `claude-sonnet-5-5` | 2¢, faster and cheaper |
|
|
195
|
+
| Claude Haiku 5.5 | `claude-haiku-5-5` | well under 1¢, fastest and cheapest |
|
|
196
|
+
| Claude Fable 5.1 | `claude-fable-5-1` | 8–13¢, most capable |
|
|
197
|
+
| Gemini 3 Flash | `gemini-3-flash-preview` | under 1¢, and Google lists it as free-tier eligible |
|
|
198
|
+
| Gemini 3.1 Pro | `gemini-3.1-pro-preview` | 2–3¢, more capable |
|
|
199
|
+
|
|
200
|
+
Try a cheaper model first. This is a short, well-specified task, and every result is validated
|
|
201
|
+
and shown to you before it's used. The Gemini models are previews, so Google may change or
|
|
202
|
+
retire them.
|
|
203
|
+
- **Credentials:** for Claude, the CLI reads `ANTHROPIC_API_KEY`, or a login from Anthropic's
|
|
204
|
+
`ant` CLI. For Gemini, it reads `GEMINI_API_KEY` (or `GOOGLE_API_KEY`). In the playground, paste
|
|
205
|
+
a key for the provider of the model you picked.
|
|
206
|
+
- **Privacy:** only your description is sent, to the provider whose model you choose (Anthropic or
|
|
207
|
+
Google). The playground sends it straight from your browser, keeps your key in memory only, and
|
|
208
|
+
forgets it when you leave the page.
|
|
209
|
+
- **Exit codes:** if the model declines, or the result still has errors after the repair attempt,
|
|
210
|
+
`init --describe` exits with `1`. If there are remaining errors, it still writes the file so you
|
|
211
|
+
can fix them by hand.
|
|
212
|
+
|
|
131
213
|
## Policy reference
|
|
132
214
|
|
|
133
215
|
A complete policy, with every option:
|
|
@@ -387,6 +469,10 @@ Rules worth knowing:
|
|
|
387
469
|
**[heyhadi.github.io/depbot-policy](https://heyhadi.github.io/depbot-policy/)** lets you write a
|
|
388
470
|
policy and see the generated files as you type.
|
|
389
471
|
|
|
472
|
+
- **Describe with AI.** Describe your setup in plain words, paste your own Anthropic API key, and
|
|
473
|
+
the model writes the policy into the editor, with notes on any assumptions it made. You can choose
|
|
474
|
+
between four Claude models and two Gemini models. See
|
|
475
|
+
[Writing a policy with AI](#writing-a-policy-with-ai).
|
|
390
476
|
- **Live validation.** Errors are underlined in the editor and listed below it in line order.
|
|
391
477
|
Click one to jump to the exact text.
|
|
392
478
|
- **Generated files in tabs**, with Copy and Download buttons. While the policy has errors, the
|
|
@@ -440,6 +526,30 @@ if (!result.ok) {
|
|
|
440
526
|
Everything is pure: no file system, network or global state. That's what lets the same code run in
|
|
441
527
|
the CLI, in tests and in the browser.
|
|
442
528
|
|
|
529
|
+
To write a policy from a description, import from `depbot-policy/describe`. It's a separate entry
|
|
530
|
+
point, so the core library never loads the Anthropic or Google SDKs:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
534
|
+
import { describePolicy } from "depbot-policy/describe";
|
|
535
|
+
|
|
536
|
+
const { source, errors, notes } = await describePolicy(
|
|
537
|
+
"npm project at the root; auto-merge patch updates",
|
|
538
|
+
new Anthropic(), // reads ANTHROPIC_API_KEY
|
|
539
|
+
{ model: "claude-sonnet-5-5" }, // optional; defaults to claude-opus-5-5
|
|
540
|
+
);
|
|
541
|
+
// source: policy YAML, errors: [] when valid, notes: the model's assumptions
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
For a Gemini model, pass a Google client instead. The client has to match the model's provider, or
|
|
545
|
+
`describePolicy` throws:
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
import { GoogleGenAI } from "@google/genai";
|
|
549
|
+
|
|
550
|
+
await describePolicy("npm project", new GoogleGenAI({ apiKey }), { model: "gemini-3-flash-preview" });
|
|
551
|
+
```
|
|
552
|
+
|
|
443
553
|
## Development
|
|
444
554
|
|
|
445
555
|
Requires Node 22 or newer (see [`.nvmrc`](.nvmrc)). Node runs the TypeScript source directly
|
|
@@ -483,6 +593,8 @@ src/
|
|
|
483
593
|
dependabot.ts dependabot.yml generator
|
|
484
594
|
workflow.ts Auto-merge workflow generator
|
|
485
595
|
rotation.ts Weekly reviewer: TypeScript formula and the workflow's shell version
|
|
596
|
+
describe.ts Write a policy from a description with Claude or Gemini (depbot-policy/describe)
|
|
597
|
+
describe-models.ts The models it can use, kept free of the SDK so help text and UI can list them
|
|
486
598
|
files.ts generateFiles: every generated file and its path
|
|
487
599
|
cli.ts, bin.ts The depbot-policy command (bin.ts is the executable entry point)
|
|
488
600
|
starter.ts The policy written by `init`
|
|
@@ -508,6 +620,10 @@ depbot.policy.yml This repository's own policy; .github/dependabot.yml and th
|
|
|
508
620
|
- **Rotation:** the workflow's shell script runs in bash, with stand-in `date` and `gh` commands,
|
|
509
621
|
and must pick the same person as `reviewerFor` on every test date.
|
|
510
622
|
- **CLI:** every command runs against a real temporary directory.
|
|
623
|
+
- **AI:** tests use real client objects with the request method stubbed, so they never call an API.
|
|
624
|
+
They cover the request (model,
|
|
625
|
+
structured output format, refusal fallback for each model), the repair loop, replies the model can't use, and
|
|
626
|
+
error messages.
|
|
511
627
|
- **Playground:** unit tests for its logic, plus tests of the whole page with React Testing Library.
|
|
512
628
|
CodeMirror can't run in jsdom, so the tests replace the two small editor components with plain
|
|
513
629
|
elements.
|
|
@@ -570,6 +686,9 @@ Short versions of the main decisions. The pull requests have the full reasoning.
|
|
|
570
686
|
browser.
|
|
571
687
|
- **Generated YAML via the `yaml` document API**, not string templates. The library handles quoting
|
|
572
688
|
(`"@types/*"` must be quoted) and comments.
|
|
689
|
+
- **AI writes the policy, never the workflow.** The model handles what needs judgment (understanding a
|
|
690
|
+
description). Validation and generation stay deterministic, so a bad model reply can't weaken
|
|
691
|
+
the security properties.
|
|
573
692
|
- **Stateless rotation.** The reviewer is a function of the week, so there's nothing to store, sync
|
|
574
693
|
or get out of date.
|
|
575
694
|
- **Pinned actions.** Actions are referenced by commit SHA with a `# vX.Y.Z` comment, both in the
|
package/dist/bin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { run } from "./cli.js";
|
|
3
|
-
process.exitCode = run(process.argv.slice(2), {
|
|
3
|
+
process.exitCode = await run(process.argv.slice(2), {
|
|
4
4
|
cwd: process.cwd(),
|
|
5
5
|
stdout: (text) => process.stdout.write(text),
|
|
6
6
|
stderr: (text) => process.stderr.write(text),
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { type DescribeModelId } from "./describe-models.ts";
|
|
2
|
+
import type { DescribeResult } from "./describe.ts";
|
|
1
3
|
import { type PolicyError } from "./parse.ts";
|
|
2
4
|
export interface CliIo {
|
|
3
5
|
cwd: string;
|
|
4
6
|
stdout: (text: string) => void;
|
|
5
7
|
stderr: (text: string) => void;
|
|
8
|
+
/** Writes a policy from a description. Defaults to calling the chosen provider; tests pass a stand-in. */
|
|
9
|
+
describe?: (description: string, model: DescribeModelId) => Promise<DescribeResult>;
|
|
6
10
|
}
|
|
7
11
|
/** Exit codes: 0 success, 1 invalid policy or outdated files, 2 wrong usage. */
|
|
8
12
|
export declare const exitCodes: {
|
|
@@ -11,6 +15,6 @@ export declare const exitCodes: {
|
|
|
11
15
|
readonly usage: 2;
|
|
12
16
|
};
|
|
13
17
|
/** Runs the CLI and returns its exit code. Side effects go through `io` and the file system. */
|
|
14
|
-
export declare function run(argv: readonly string[], io: CliIo): number
|
|
18
|
+
export declare function run(argv: readonly string[], io: CliIo): Promise<number>;
|
|
15
19
|
/** `file:line:column: path: message`, the format editors and CI annotations understand. */
|
|
16
20
|
export declare function formatError(file: string, error: PolicyError): string;
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
+
import { defaultDescribeModel, describeModel, describeModels, isDescribeModelId, providerLabel, } from "./describe-models.js";
|
|
4
5
|
import { generateFiles } from "./files.js";
|
|
5
6
|
import { parsePolicy } from "./parse.js";
|
|
6
7
|
import { reviewerFor } from "./rotation.js";
|
|
@@ -8,10 +9,23 @@ import { starterPolicy } from "./starter.js";
|
|
|
8
9
|
/** Exit codes: 0 success, 1 invalid policy or outdated files, 2 wrong usage. */
|
|
9
10
|
export const exitCodes = { ok: 0, failed: 1, usage: 2 };
|
|
10
11
|
const defaultPolicyPath = "depbot.policy.yml";
|
|
12
|
+
// The model list, grouped by provider, for the --model help. Each provider gets its own heading
|
|
13
|
+
// so the two sets of models don't blur together.
|
|
14
|
+
const modelHelp = ["anthropic", "google"]
|
|
15
|
+
.map((provider) => {
|
|
16
|
+
const heading = provider === "anthropic" ? "Claude models:" : "Gemini models:";
|
|
17
|
+
return [
|
|
18
|
+
` ${heading}`,
|
|
19
|
+
...describeModels
|
|
20
|
+
.filter((model) => model.provider === provider)
|
|
21
|
+
.map((model) => ` ${model.id.padEnd(23)} ${model.summary}`),
|
|
22
|
+
].join("\n");
|
|
23
|
+
})
|
|
24
|
+
.join("\n");
|
|
11
25
|
const usage = `Usage: depbot-policy <command> [options]
|
|
12
26
|
|
|
13
27
|
Commands:
|
|
14
|
-
init Create a starter ${defaultPolicyPath}
|
|
28
|
+
init Create a starter ${defaultPolicyPath}, or one written by AI with --describe
|
|
15
29
|
generate Write .github/dependabot.yml and the auto-merge workflow
|
|
16
30
|
check Fail if the policy is invalid or the generated files are out of date
|
|
17
31
|
reviewer Print this week's reviewer from review.rotation
|
|
@@ -20,13 +34,17 @@ Options:
|
|
|
20
34
|
--policy <file> Policy file (default: ${defaultPolicyPath})
|
|
21
35
|
--out <dir> Repository root to write to or check (default: .)
|
|
22
36
|
--dry-run generate: print the files instead of writing them
|
|
37
|
+
--describe <text> init: have an AI model write the policy from a description
|
|
38
|
+
(needs ANTHROPIC_API_KEY or GEMINI_API_KEY)
|
|
39
|
+
--model <id> init --describe: which model to use (default: ${defaultDescribeModel})
|
|
40
|
+
${modelHelp}
|
|
23
41
|
--force init: overwrite an existing policy file
|
|
24
42
|
--date <date> reviewer: use this date instead of today (e.g. 2026-10-12)
|
|
25
43
|
-h, --help Show this help
|
|
26
44
|
-v, --version Show the version
|
|
27
45
|
`;
|
|
28
46
|
/** Runs the CLI and returns its exit code. Side effects go through `io` and the file system. */
|
|
29
|
-
export function run(argv, io) {
|
|
47
|
+
export async function run(argv, io) {
|
|
30
48
|
let parsed;
|
|
31
49
|
try {
|
|
32
50
|
parsed = parseArgs({
|
|
@@ -37,6 +55,8 @@ export function run(argv, io) {
|
|
|
37
55
|
out: { type: "string", default: "." },
|
|
38
56
|
"dry-run": { type: "boolean", default: false },
|
|
39
57
|
force: { type: "boolean", default: false },
|
|
58
|
+
describe: { type: "string" },
|
|
59
|
+
model: { type: "string" },
|
|
40
60
|
date: { type: "string" },
|
|
41
61
|
help: { type: "boolean", short: "h", default: false },
|
|
42
62
|
version: { type: "boolean", short: "v", default: false },
|
|
@@ -61,6 +81,10 @@ export function run(argv, io) {
|
|
|
61
81
|
io.stderr(`Unexpected argument: ${extra[0]}\n\n${usage}`);
|
|
62
82
|
return exitCodes.usage;
|
|
63
83
|
}
|
|
84
|
+
if (values.model !== undefined && values.describe === undefined) {
|
|
85
|
+
io.stderr("--model only applies to init --describe.\n");
|
|
86
|
+
return exitCodes.usage;
|
|
87
|
+
}
|
|
64
88
|
const policyPath = values.policy;
|
|
65
89
|
const resolve = (file) => path.resolve(io.cwd, file);
|
|
66
90
|
switch (command) {
|
|
@@ -69,9 +93,12 @@ export function run(argv, io) {
|
|
|
69
93
|
io.stderr(`${policyPath} already exists. Use --force to overwrite it.\n`);
|
|
70
94
|
return exitCodes.failed;
|
|
71
95
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
96
|
+
if (values.describe === undefined) {
|
|
97
|
+
writeFileSync(resolve(policyPath), starterPolicy);
|
|
98
|
+
io.stdout(`Created ${policyPath}. Edit it, then run: depbot-policy generate\n`);
|
|
99
|
+
return exitCodes.ok;
|
|
100
|
+
}
|
|
101
|
+
return describeInto(policyPath, values.describe, values.model, resolve, io);
|
|
75
102
|
}
|
|
76
103
|
case "generate": {
|
|
77
104
|
const policy = loadPolicy(policyPath, resolve, io);
|
|
@@ -129,6 +156,51 @@ export function run(argv, io) {
|
|
|
129
156
|
return exitCodes.usage;
|
|
130
157
|
}
|
|
131
158
|
}
|
|
159
|
+
async function describeInto(policyPath, description, modelOption, resolve, io) {
|
|
160
|
+
if (description.trim() === "") {
|
|
161
|
+
io.stderr("--describe needs a description, e.g. --describe \"pnpm monorepo, auto-merge patches\"\n");
|
|
162
|
+
return exitCodes.usage;
|
|
163
|
+
}
|
|
164
|
+
// Loaded on demand, so the other commands never load the provider SDKs.
|
|
165
|
+
const describeModule = await import("./describe.js");
|
|
166
|
+
const model = modelOption ?? defaultDescribeModel;
|
|
167
|
+
if (!isDescribeModelId(model)) {
|
|
168
|
+
const ids = describeModels.map((candidate) => candidate.id).join(", ");
|
|
169
|
+
io.stderr(`Unknown model: ${model}. Use one of: ${ids}\n`);
|
|
170
|
+
return exitCodes.usage;
|
|
171
|
+
}
|
|
172
|
+
const describe = io.describe ??
|
|
173
|
+
((text, chosen) => describeModule.describePolicy(text, describeModule.environmentClient(describeModel(chosen).provider), {
|
|
174
|
+
model: chosen,
|
|
175
|
+
}));
|
|
176
|
+
const { label } = describeModels.find((candidate) => candidate.id === model);
|
|
177
|
+
io.stdout(`Asking ${label} to write the policy...\n`);
|
|
178
|
+
let result;
|
|
179
|
+
try {
|
|
180
|
+
result = await describe(description, model);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
io.stderr(`${describeModule.describeFailureMessage(error)}\n`);
|
|
184
|
+
// A missing Gemini key already says so. For Claude, the SDK's own error doesn't.
|
|
185
|
+
if (describeModel(model).provider === "anthropic" &&
|
|
186
|
+
!process.env.ANTHROPIC_API_KEY &&
|
|
187
|
+
!process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
188
|
+
io.stderr("Set ANTHROPIC_API_KEY (or log in with `ant auth login`) to use --describe.\n");
|
|
189
|
+
}
|
|
190
|
+
return exitCodes.failed;
|
|
191
|
+
}
|
|
192
|
+
writeFileSync(resolve(policyPath), result.source);
|
|
193
|
+
for (const note of result.notes)
|
|
194
|
+
io.stdout(`Note: ${note}\n`);
|
|
195
|
+
if (result.errors.length > 0) {
|
|
196
|
+
io.stderr(`Created ${policyPath}, but it still has problems to fix by hand:\n`);
|
|
197
|
+
for (const error of result.errors)
|
|
198
|
+
io.stderr(`${formatError(policyPath, error)}\n`);
|
|
199
|
+
return exitCodes.failed;
|
|
200
|
+
}
|
|
201
|
+
io.stdout(`Created ${policyPath}. Review it, then run: depbot-policy generate\n`);
|
|
202
|
+
return exitCodes.ok;
|
|
203
|
+
}
|
|
132
204
|
function loadPolicy(policyPath, resolve, io) {
|
|
133
205
|
let source;
|
|
134
206
|
try {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Which API a model runs on. Drives the SDK client, the key it reads, and the failure copy. */
|
|
2
|
+
export type DescribeProvider = "anthropic" | "google";
|
|
3
|
+
export interface DescribeModel {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** One line for pickers: what it's good for and roughly what one policy costs. */
|
|
7
|
+
summary: string;
|
|
8
|
+
/** Which provider runs the model. */
|
|
9
|
+
provider: DescribeProvider;
|
|
10
|
+
/**
|
|
11
|
+
* Whether the API may retry a refusal on another model (`fallbacks: "default"`).
|
|
12
|
+
* Claude Haiku 5.5 has no server-side fallback, so it must not send the parameter.
|
|
13
|
+
*/
|
|
14
|
+
serverSideFallback: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare const describeModels: readonly [{
|
|
17
|
+
readonly id: "claude-opus-5-5";
|
|
18
|
+
readonly label: "Claude Opus 5.5";
|
|
19
|
+
readonly summary: "Recommended: best balance of quality and cost, about 3–5¢ per policy";
|
|
20
|
+
readonly provider: "anthropic";
|
|
21
|
+
readonly serverSideFallback: true;
|
|
22
|
+
}, {
|
|
23
|
+
readonly id: "claude-sonnet-5-5";
|
|
24
|
+
readonly label: "Claude Sonnet 5.5";
|
|
25
|
+
readonly summary: "Faster and cheaper, about 2¢ per policy";
|
|
26
|
+
readonly provider: "anthropic";
|
|
27
|
+
readonly serverSideFallback: true;
|
|
28
|
+
}, {
|
|
29
|
+
readonly id: "claude-haiku-5-5";
|
|
30
|
+
readonly label: "Claude Haiku 5.5";
|
|
31
|
+
readonly summary: "Fastest and cheapest, well under 1¢ per policy";
|
|
32
|
+
readonly provider: "anthropic";
|
|
33
|
+
readonly serverSideFallback: false;
|
|
34
|
+
}, {
|
|
35
|
+
readonly id: "claude-fable-5-1";
|
|
36
|
+
readonly label: "Claude Fable 5.1";
|
|
37
|
+
readonly summary: "Most capable and most expensive, about 8–13¢ per policy";
|
|
38
|
+
readonly provider: "anthropic";
|
|
39
|
+
readonly serverSideFallback: true;
|
|
40
|
+
}, {
|
|
41
|
+
readonly id: "gemini-3-flash-preview";
|
|
42
|
+
readonly label: "Gemini 3 Flash";
|
|
43
|
+
readonly summary: "Fast and cheap, under 1¢ per policy (free tier available)";
|
|
44
|
+
readonly provider: "google";
|
|
45
|
+
readonly serverSideFallback: false;
|
|
46
|
+
}, {
|
|
47
|
+
readonly id: "gemini-3.1-pro-preview";
|
|
48
|
+
readonly label: "Gemini 3.1 Pro";
|
|
49
|
+
readonly summary: "More capable, about 2–3¢ per policy";
|
|
50
|
+
readonly provider: "google";
|
|
51
|
+
readonly serverSideFallback: false;
|
|
52
|
+
}];
|
|
53
|
+
export type DescribeModelId = (typeof describeModels)[number]["id"];
|
|
54
|
+
export declare const defaultDescribeModel: DescribeModelId;
|
|
55
|
+
/** The model with the given id, or undefined if it isn't supported. */
|
|
56
|
+
export declare function describeModel(model: DescribeModelId): DescribeModel;
|
|
57
|
+
/** A short, friendly name for a provider, used in CLI and playground copy. */
|
|
58
|
+
export declare function providerLabel(provider: DescribeProvider): string;
|
|
59
|
+
/** The provider a given model runs on. */
|
|
60
|
+
export declare function providerFor(model: DescribeModelId): DescribeProvider;
|
|
61
|
+
export declare function isDescribeModelId(value: string): value is DescribeModelId;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The models `describePolicy` can use. Kept apart from describe.ts, which loads the provider SDKs,
|
|
2
|
+
// so the CLI's help text and the playground's picker can list them without loading any SDK.
|
|
3
|
+
// Each model names its provider; everything else (clients, schemas) follows from that.
|
|
4
|
+
// Costs assume ~2.5k input and 1–2k output tokens per policy, at each model's list price.
|
|
5
|
+
// Gemini ids are exactly as listed on ai.google.dev/gemini-api/docs/models. Both are preview
|
|
6
|
+
// models, so Google may change or retire them.
|
|
7
|
+
export const describeModels = [
|
|
8
|
+
{
|
|
9
|
+
id: "claude-opus-5-5",
|
|
10
|
+
label: "Claude Opus 5.5",
|
|
11
|
+
summary: "Recommended: best balance of quality and cost, about 3–5¢ per policy",
|
|
12
|
+
provider: "anthropic",
|
|
13
|
+
serverSideFallback: true,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "claude-sonnet-5-5",
|
|
17
|
+
label: "Claude Sonnet 5.5",
|
|
18
|
+
summary: "Faster and cheaper, about 2¢ per policy",
|
|
19
|
+
provider: "anthropic",
|
|
20
|
+
serverSideFallback: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "claude-haiku-5-5",
|
|
24
|
+
label: "Claude Haiku 5.5",
|
|
25
|
+
summary: "Fastest and cheapest, well under 1¢ per policy",
|
|
26
|
+
provider: "anthropic",
|
|
27
|
+
serverSideFallback: false,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "claude-fable-5-1",
|
|
31
|
+
label: "Claude Fable 5.1",
|
|
32
|
+
summary: "Most capable and most expensive, about 8–13¢ per policy",
|
|
33
|
+
provider: "anthropic",
|
|
34
|
+
serverSideFallback: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "gemini-3-flash-preview",
|
|
38
|
+
label: "Gemini 3 Flash",
|
|
39
|
+
summary: "Fast and cheap, under 1¢ per policy (free tier available)",
|
|
40
|
+
provider: "google",
|
|
41
|
+
serverSideFallback: false,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "gemini-3.1-pro-preview",
|
|
45
|
+
label: "Gemini 3.1 Pro",
|
|
46
|
+
summary: "More capable, about 2–3¢ per policy",
|
|
47
|
+
provider: "google",
|
|
48
|
+
serverSideFallback: false,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
export const defaultDescribeModel = "claude-opus-5-5";
|
|
52
|
+
/** The model with the given id, or undefined if it isn't supported. */
|
|
53
|
+
export function describeModel(model) {
|
|
54
|
+
return describeModels.find((candidate) => candidate.id === model);
|
|
55
|
+
}
|
|
56
|
+
/** A short, friendly name for a provider, used in CLI and playground copy. */
|
|
57
|
+
export function providerLabel(provider) {
|
|
58
|
+
return provider === "anthropic" ? "Claude" : "Gemini";
|
|
59
|
+
}
|
|
60
|
+
/** The provider a given model runs on. */
|
|
61
|
+
export function providerFor(model) {
|
|
62
|
+
return describeModel(model).provider;
|
|
63
|
+
}
|
|
64
|
+
export function isDescribeModelId(value) {
|
|
65
|
+
return describeModels.some((model) => model.id === value);
|
|
66
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { GoogleGenAI } from "@google/genai";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { type DescribeModelId, type DescribeProvider } from "./describe-models.ts";
|
|
5
|
+
import { type PolicyError } from "./parse.ts";
|
|
6
|
+
export { defaultDescribeModel, describeModel, describeModels, isDescribeModelId, providerFor, providerLabel, type DescribeModel, type DescribeModelId, type DescribeProvider, } from "./describe-models.ts";
|
|
7
|
+
/**
|
|
8
|
+
* What Claude fills in. It's a simplified, refinement-free version of the policy schema, because
|
|
9
|
+
* structured outputs can't express cross-field rules or defaults. Our own code turns the draft into
|
|
10
|
+
* YAML, and `parsePolicy` still applies every rule, so the model can never bypass validation.
|
|
11
|
+
* A reply outside this schema (say, `major` as an update type) is rejected when the SDK parses it.
|
|
12
|
+
*/
|
|
13
|
+
export declare const policyDraftSchema: z.ZodObject<{
|
|
14
|
+
ecosystems: z.ZodArray<z.ZodObject<{
|
|
15
|
+
type: z.ZodEnum<{
|
|
16
|
+
bundler: "bundler";
|
|
17
|
+
cargo: "cargo";
|
|
18
|
+
composer: "composer";
|
|
19
|
+
docker: "docker";
|
|
20
|
+
"github-actions": "github-actions";
|
|
21
|
+
gomod: "gomod";
|
|
22
|
+
gradle: "gradle";
|
|
23
|
+
maven: "maven";
|
|
24
|
+
mix: "mix";
|
|
25
|
+
npm: "npm";
|
|
26
|
+
nuget: "nuget";
|
|
27
|
+
pip: "pip";
|
|
28
|
+
pub: "pub";
|
|
29
|
+
swift: "swift";
|
|
30
|
+
terraform: "terraform";
|
|
31
|
+
}>;
|
|
32
|
+
directory: z.ZodString;
|
|
33
|
+
schedule: z.ZodEnum<{
|
|
34
|
+
daily: "daily";
|
|
35
|
+
monthly: "monthly";
|
|
36
|
+
weekly: "weekly";
|
|
37
|
+
}>;
|
|
38
|
+
}, z.core.$strip>>;
|
|
39
|
+
autoMerge: z.ZodObject<{
|
|
40
|
+
updateTypes: z.ZodArray<z.ZodEnum<{
|
|
41
|
+
minor: "minor";
|
|
42
|
+
patch: "patch";
|
|
43
|
+
}>>;
|
|
44
|
+
dependencyTypes: z.ZodArray<z.ZodEnum<{
|
|
45
|
+
development: "development";
|
|
46
|
+
production: "production";
|
|
47
|
+
}>>;
|
|
48
|
+
mergeMethod: z.ZodEnum<{
|
|
49
|
+
merge: "merge";
|
|
50
|
+
rebase: "rebase";
|
|
51
|
+
squash: "squash";
|
|
52
|
+
}>;
|
|
53
|
+
}, z.core.$strip>;
|
|
54
|
+
block: z.ZodArray<z.ZodObject<{
|
|
55
|
+
name: z.ZodString;
|
|
56
|
+
reason: z.ZodString;
|
|
57
|
+
ecosystems: z.ZodArray<z.ZodEnum<{
|
|
58
|
+
bundler: "bundler";
|
|
59
|
+
cargo: "cargo";
|
|
60
|
+
composer: "composer";
|
|
61
|
+
docker: "docker";
|
|
62
|
+
"github-actions": "github-actions";
|
|
63
|
+
gomod: "gomod";
|
|
64
|
+
gradle: "gradle";
|
|
65
|
+
maven: "maven";
|
|
66
|
+
mix: "mix";
|
|
67
|
+
npm: "npm";
|
|
68
|
+
nuget: "nuget";
|
|
69
|
+
pip: "pip";
|
|
70
|
+
pub: "pub";
|
|
71
|
+
swift: "swift";
|
|
72
|
+
terraform: "terraform";
|
|
73
|
+
}>>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
reviewRotation: z.ZodArray<z.ZodString>;
|
|
76
|
+
notes: z.ZodArray<z.ZodString>;
|
|
77
|
+
}, z.core.$strip>;
|
|
78
|
+
export type PolicyDraft = z.infer<typeof policyDraftSchema>;
|
|
79
|
+
export interface DescribeResult {
|
|
80
|
+
/** The policy as YAML, ready to save as depbot.policy.yml. */
|
|
81
|
+
source: string;
|
|
82
|
+
/** Validation errors still present after one repair attempt; empty when the policy is valid. */
|
|
83
|
+
errors: PolicyError[];
|
|
84
|
+
/** Claude's notes on assumptions it made. */
|
|
85
|
+
notes: string[];
|
|
86
|
+
}
|
|
87
|
+
export declare class DescribeError extends Error {
|
|
88
|
+
name: string;
|
|
89
|
+
}
|
|
90
|
+
export interface DescribeOptions {
|
|
91
|
+
/** Which model writes the policy. Defaults to `defaultDescribeModel`. */
|
|
92
|
+
model?: DescribeModelId;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A provider-agnostic way to ask a model for a policy draft. Each provider (Claude, Gemini) has an
|
|
96
|
+
* adapter that knows how to call its SDK with structured output and how to turn its replies into
|
|
97
|
+
* a `PolicyDraft`. `describePolicy` stays the same regardless of which one it's given.
|
|
98
|
+
*/
|
|
99
|
+
export interface DescribeBackend {
|
|
100
|
+
draft(content: string): Promise<PolicyDraft>;
|
|
101
|
+
}
|
|
102
|
+
/** Either SDK's client. The adapters narrow this with `instanceof`. */
|
|
103
|
+
export type DescribeBackendClient = Anthropic | GoogleGenAI;
|
|
104
|
+
/**
|
|
105
|
+
* Asks a model to write a policy from a plain-language description, validates it with
|
|
106
|
+
* `parsePolicy`, and gives the model one chance to fix any errors.
|
|
107
|
+
*/
|
|
108
|
+
export declare function describePolicy(description: string, client: DescribeBackendClient, { model }?: DescribeOptions): Promise<DescribeResult>;
|
|
109
|
+
/** Turns a draft into policy YAML, leaving out empty optional sections. */
|
|
110
|
+
export declare function draftToYaml(draft: PolicyDraft, provider?: DescribeProvider): string;
|
|
111
|
+
/** A short, user-facing explanation for an error thrown while describing a policy. */
|
|
112
|
+
export declare function describeFailureMessage(error: unknown): string;
|
|
113
|
+
/** A client for use in a browser, with a key the user typed in. */
|
|
114
|
+
export declare function browserClient(provider: DescribeProvider, apiKey: string): DescribeBackendClient;
|
|
115
|
+
/** A backend that reads credentials from the environment for the given provider. */
|
|
116
|
+
export declare function environmentClient(provider: DescribeProvider): DescribeBackendClient;
|
package/dist/describe.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { betaZodOutputFormat } from "@anthropic-ai/sdk/helpers/beta/zod";
|
|
3
|
+
import { ApiError, GoogleGenAI } from "@google/genai";
|
|
4
|
+
import { Document, isScalar, visit } from "yaml";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { defaultDescribeModel, describeModel, describeModels, providerLabel, } from "./describe-models.js";
|
|
7
|
+
import { parsePolicy } from "./parse.js";
|
|
8
|
+
import { ecosystemTypes } from "./schema.js";
|
|
9
|
+
export { defaultDescribeModel, describeModel, describeModels, isDescribeModelId, providerFor, providerLabel, } from "./describe-models.js";
|
|
10
|
+
/**
|
|
11
|
+
* What Claude fills in. It's a simplified, refinement-free version of the policy schema, because
|
|
12
|
+
* structured outputs can't express cross-field rules or defaults. Our own code turns the draft into
|
|
13
|
+
* YAML, and `parsePolicy` still applies every rule, so the model can never bypass validation.
|
|
14
|
+
* A reply outside this schema (say, `major` as an update type) is rejected when the SDK parses it.
|
|
15
|
+
*/
|
|
16
|
+
export const policyDraftSchema = z.object({
|
|
17
|
+
ecosystems: z
|
|
18
|
+
.array(z.object({
|
|
19
|
+
type: z.enum(ecosystemTypes),
|
|
20
|
+
directory: z.string().describe('Path from the repository root, starting with "/"'),
|
|
21
|
+
schedule: z.enum(["daily", "weekly", "monthly"]),
|
|
22
|
+
}))
|
|
23
|
+
.describe("One entry per package manager and directory"),
|
|
24
|
+
autoMerge: z.object({
|
|
25
|
+
updateTypes: z.array(z.enum(["patch", "minor"])),
|
|
26
|
+
dependencyTypes: z.array(z.enum(["development", "production"])),
|
|
27
|
+
mergeMethod: z.enum(["squash", "merge", "rebase"]),
|
|
28
|
+
}),
|
|
29
|
+
block: z
|
|
30
|
+
.array(z.object({
|
|
31
|
+
name: z.string().describe('Package name or glob, e.g. "@types/*"'),
|
|
32
|
+
reason: z.string().describe("Why it's blocked, in the user's words where possible"),
|
|
33
|
+
ecosystems: z
|
|
34
|
+
.array(z.enum(ecosystemTypes))
|
|
35
|
+
.describe("Ecosystems the block applies to; empty means all of them"),
|
|
36
|
+
}))
|
|
37
|
+
.describe("Packages Dependabot must never update; empty if none were mentioned"),
|
|
38
|
+
reviewRotation: z
|
|
39
|
+
.array(z.string())
|
|
40
|
+
.describe("GitHub usernames without @, in the order given; empty if none were named"),
|
|
41
|
+
notes: z
|
|
42
|
+
.array(z.string())
|
|
43
|
+
.describe("Short notes on assumptions made or requests that couldn't be honoured"),
|
|
44
|
+
});
|
|
45
|
+
const systemPrompt = `You turn a team's plain-language description of how they want Dependabot to behave into a depbot-policy draft.
|
|
46
|
+
|
|
47
|
+
How to fill each field:
|
|
48
|
+
- ecosystems: one entry per package manager and directory. Yarn, pnpm and Bun projects use "npm"; Poetry and Pipenv use "pip"; Go modules use "gomod"; Rust uses "cargo"; Ruby uses "bundler"; Dockerfiles use "docker"; GitHub Actions workflows use "github-actions" with directory "/". Directories start with "/", and "/" is the repository root. In a monorepo, list each package directory the user mentions. Use the schedule they ask for, otherwise "weekly" ("monthly" for github-actions).
|
|
49
|
+
- autoMerge: unless the user says otherwise, use updateTypes ["patch"], both dependency types, and "squash". Include "minor" only if the user explicitly allows it. Major updates can never be auto-merged; if the user asks for that, leave majors out and say so in notes.
|
|
50
|
+
- block: only packages the user says to pin, freeze or never update. Keep their reason. Limit the block to an ecosystem when the package clearly belongs to one.
|
|
51
|
+
- reviewRotation: only GitHub usernames the user actually gives, without "@". Never invent people.
|
|
52
|
+
- notes: brief, one sentence each, for any assumption you made or anything you couldn't do. Leave it empty when there's nothing worth saying.`;
|
|
53
|
+
export class DescribeError extends Error {
|
|
54
|
+
name = "DescribeError";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Asks a model to write a policy from a plain-language description, validates it with
|
|
58
|
+
* `parsePolicy`, and gives the model one chance to fix any errors.
|
|
59
|
+
*/
|
|
60
|
+
export async function describePolicy(description, client, { model = defaultDescribeModel } = {}) {
|
|
61
|
+
const backend = backendFor(client, model);
|
|
62
|
+
const provider = providerOf(model);
|
|
63
|
+
const first = await backend.draft(description);
|
|
64
|
+
const firstSource = draftToYaml(first, provider);
|
|
65
|
+
const firstResult = parsePolicy(firstSource);
|
|
66
|
+
if (firstResult.ok)
|
|
67
|
+
return { source: firstSource, errors: [], notes: first.notes };
|
|
68
|
+
const repair = await backend.draft(`${description}
|
|
69
|
+
|
|
70
|
+
A previous attempt produced this policy, which fails validation:
|
|
71
|
+
|
|
72
|
+
${firstSource}
|
|
73
|
+
Errors:
|
|
74
|
+
${firstResult.errors.map((error) => `- ${error.path || "(file)"}: ${error.message}`).join("\n")}
|
|
75
|
+
|
|
76
|
+
Return a corrected draft.`);
|
|
77
|
+
const source = draftToYaml(repair, provider);
|
|
78
|
+
const result = parsePolicy(source);
|
|
79
|
+
return { source, errors: result.ok ? [] : result.errors, notes: repair.notes };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Picks the adapter for the client the caller passed. Claude and Gemini use different SDKs and
|
|
83
|
+
* request shapes, so each gets its own adapter; everything above it is shared.
|
|
84
|
+
*/
|
|
85
|
+
function backendFor(client, model) {
|
|
86
|
+
const provider = providerOf(model);
|
|
87
|
+
if (provider === "google" && client instanceof GoogleGenAI)
|
|
88
|
+
return geminiBackend(client, model);
|
|
89
|
+
if (provider === "anthropic" && client instanceof Anthropic)
|
|
90
|
+
return anthropicBackend(client, model);
|
|
91
|
+
throw new DescribeError(`${describeModel(model).label} needs a ${providerLabel(provider)} client, but a different one was passed.`);
|
|
92
|
+
}
|
|
93
|
+
/** Claude adapter: structured outputs via the Anthropic SDK's `messages.parse`. */
|
|
94
|
+
function anthropicBackend(client, model) {
|
|
95
|
+
return {
|
|
96
|
+
async draft(content) {
|
|
97
|
+
let response;
|
|
98
|
+
try {
|
|
99
|
+
response = await sendAnthropicRequest(client, model, content);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// The SDK reports a reply that fails policyDraftSchema as a plain AnthropicError. API errors
|
|
103
|
+
// (a subclass) and anything else, such as missing credentials, go to the caller unchanged.
|
|
104
|
+
if (error instanceof Anthropic.AnthropicError && !(error instanceof Anthropic.APIError)) {
|
|
105
|
+
throw new DescribeError("The model's reply didn't match the policy format. Try again.");
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
if (response.stop_reason === "refusal") {
|
|
110
|
+
throw new DescribeError("The model declined to write a policy for this description.");
|
|
111
|
+
}
|
|
112
|
+
if (response.stop_reason === "max_tokens") {
|
|
113
|
+
throw new DescribeError("The answer was cut off. Try a shorter description.");
|
|
114
|
+
}
|
|
115
|
+
if (response.parsed_output == null) {
|
|
116
|
+
throw new DescribeError("The model didn't return a policy. Try rephrasing the description.");
|
|
117
|
+
}
|
|
118
|
+
return response.parsed_output;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function sendAnthropicRequest(client, model, content) {
|
|
123
|
+
const { serverSideFallback } = describeModel(model);
|
|
124
|
+
return client.beta.messages.parse({
|
|
125
|
+
model,
|
|
126
|
+
max_tokens: 16000,
|
|
127
|
+
// On a refusal, the API retries on a fallback model it picks for the refusal category.
|
|
128
|
+
...(serverSideFallback && {
|
|
129
|
+
betas: ["server-side-fallback-2026-07-01"],
|
|
130
|
+
fallbacks: "default",
|
|
131
|
+
}),
|
|
132
|
+
system: systemPrompt,
|
|
133
|
+
messages: [{ role: "user", content }],
|
|
134
|
+
output_config: {
|
|
135
|
+
// A short, well-specified extraction; low effort keeps it fast.
|
|
136
|
+
effort: "low",
|
|
137
|
+
format: betaZodOutputFormat(policyDraftSchema),
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** Gemini adapter: JSON output via `generateContent` + `responseJsonSchema`. */
|
|
142
|
+
function geminiBackend(client, model) {
|
|
143
|
+
return {
|
|
144
|
+
async draft(content) {
|
|
145
|
+
// API failures, including a 400 for a bad key, are explained by describeFailureMessage.
|
|
146
|
+
// Only a reply we can't parse (below) means the model didn't follow the format.
|
|
147
|
+
const response = await client.models.generateContent({
|
|
148
|
+
model,
|
|
149
|
+
contents: content,
|
|
150
|
+
config: {
|
|
151
|
+
systemInstruction: systemPrompt,
|
|
152
|
+
responseMimeType: "application/json",
|
|
153
|
+
// The same draft schema the Claude path uses, so both providers are held to one
|
|
154
|
+
// contract. Gemini returns plain JSON, which we validate with Zod below.
|
|
155
|
+
responseJsonSchema: geminiResponseSchema,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const text = response.text;
|
|
159
|
+
if (!text) {
|
|
160
|
+
throw new DescribeError("The model didn't return a policy. Try rephrasing the description.");
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
return policyDraftSchema.parse(JSON.parse(text));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw new DescribeError("The model's reply didn't match the policy format. Try again.");
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// Gemini's `responseJsonSchema` accepts only a subset of JSON Schema properties, and `$schema`
|
|
172
|
+
// isn't one of them.
|
|
173
|
+
const { $schema: _dialect, ...geminiResponseSchema } = z.toJSONSchema(policyDraftSchema);
|
|
174
|
+
/** Turns a draft into policy YAML, leaving out empty optional sections. */
|
|
175
|
+
export function draftToYaml(draft, provider = "anthropic") {
|
|
176
|
+
const doc = new Document({
|
|
177
|
+
version: 1,
|
|
178
|
+
ecosystems: draft.ecosystems,
|
|
179
|
+
autoMerge: draft.autoMerge,
|
|
180
|
+
...(draft.block.length > 0 && {
|
|
181
|
+
block: draft.block.map(({ ecosystems, ...entry }) => ({
|
|
182
|
+
...entry,
|
|
183
|
+
...(ecosystems.length > 0 && { ecosystems }),
|
|
184
|
+
})),
|
|
185
|
+
}),
|
|
186
|
+
...(draft.reviewRotation.length > 0 && { review: { rotation: draft.reviewRotation } }),
|
|
187
|
+
});
|
|
188
|
+
doc.commentBefore = ` Written by ${providerLabel(provider)} from a description. Review it before committing.`;
|
|
189
|
+
// Short lists of plain values read better inline: updateTypes: [patch, minor]
|
|
190
|
+
visit(doc, {
|
|
191
|
+
Seq(_, node) {
|
|
192
|
+
if (node.items.every(isScalar))
|
|
193
|
+
node.flow = true;
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
return doc.toString({ lineWidth: 0, flowCollectionPadding: false });
|
|
197
|
+
}
|
|
198
|
+
/** The provider a given model runs on. */
|
|
199
|
+
function providerOf(model) {
|
|
200
|
+
return describeModel(model).provider;
|
|
201
|
+
}
|
|
202
|
+
/** A short, user-facing explanation for an error thrown while describing a policy. */
|
|
203
|
+
export function describeFailureMessage(error) {
|
|
204
|
+
if (error instanceof DescribeError)
|
|
205
|
+
return error.message;
|
|
206
|
+
if (error instanceof Anthropic.AuthenticationError)
|
|
207
|
+
return "The API key was rejected. Check it and try again.";
|
|
208
|
+
if (error instanceof Anthropic.PermissionDeniedError)
|
|
209
|
+
return "This API key isn't allowed to use the model.";
|
|
210
|
+
if (error instanceof Anthropic.RateLimitError)
|
|
211
|
+
return "Rate limited by the Anthropic API. Wait a moment and try again.";
|
|
212
|
+
if (error instanceof Anthropic.APIConnectionError)
|
|
213
|
+
return "Couldn't reach the Anthropic API. Check your connection.";
|
|
214
|
+
if (error instanceof Anthropic.APIError)
|
|
215
|
+
return `The Anthropic API returned an error (${error.status ?? "unknown"}).`;
|
|
216
|
+
if (error instanceof ApiError) {
|
|
217
|
+
// Google reports a bad key as HTTP 400 with the reason API_KEY_INVALID, not as 401.
|
|
218
|
+
if (googleErrorReason(error) === "API_KEY_INVALID" || error.status === 401 || error.status === 403) {
|
|
219
|
+
return "The API key was rejected. Check it and try again.";
|
|
220
|
+
}
|
|
221
|
+
if (error.status === 404)
|
|
222
|
+
return "The Gemini API doesn't know that model, or this key can't use it.";
|
|
223
|
+
if (error.status === 429)
|
|
224
|
+
return "Rate limited by the Gemini API. Wait a moment and try again.";
|
|
225
|
+
if (error.status === 400)
|
|
226
|
+
return "The Gemini API rejected the request (400).";
|
|
227
|
+
return `The Gemini API returned an error (${error.status}).`;
|
|
228
|
+
}
|
|
229
|
+
return "Something went wrong while generating the policy.";
|
|
230
|
+
}
|
|
231
|
+
/** The `reason` in a Gemini API error's details, such as `API_KEY_INVALID`, if there is one. */
|
|
232
|
+
function googleErrorReason(error) {
|
|
233
|
+
try {
|
|
234
|
+
const body = JSON.parse(error.message);
|
|
235
|
+
return body.error?.details?.find((detail) => detail.reason !== undefined)?.reason;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// The message isn't JSON (for example a network failure), so there's no reason to read.
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** A client for use in a browser, with a key the user typed in. */
|
|
243
|
+
export function browserClient(provider, apiKey) {
|
|
244
|
+
if (provider === "google")
|
|
245
|
+
return new GoogleGenAI({ apiKey });
|
|
246
|
+
return new Anthropic({ apiKey, dangerouslyAllowBrowser: true });
|
|
247
|
+
}
|
|
248
|
+
/** A backend that reads credentials from the environment for the given provider. */
|
|
249
|
+
export function environmentClient(provider) {
|
|
250
|
+
return providerClient(provider);
|
|
251
|
+
}
|
|
252
|
+
/** The provider's SDK client, reading its key from the environment. */
|
|
253
|
+
function providerClient(provider) {
|
|
254
|
+
if (provider === "anthropic")
|
|
255
|
+
return new Anthropic();
|
|
256
|
+
// Passing the key explicitly keeps the Gemini SDK from printing its own messages, or from
|
|
257
|
+
// probing for Google Cloud credentials, when none is set.
|
|
258
|
+
const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
259
|
+
if (!apiKey)
|
|
260
|
+
throw new DescribeError("No Gemini API key found. Set GEMINI_API_KEY.");
|
|
261
|
+
return new GoogleGenAI({ apiKey });
|
|
262
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "depbot-policy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "One policy file → dependabot.yml, a safe auto-merge workflow, a block list, and a weekly review rotation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dependabot",
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
".": {
|
|
23
23
|
"types": "./dist/index.d.ts",
|
|
24
24
|
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./describe": {
|
|
27
|
+
"types": "./dist/describe.d.ts",
|
|
28
|
+
"default": "./dist/describe.js"
|
|
25
29
|
}
|
|
26
30
|
},
|
|
27
31
|
"bin": {
|
|
@@ -41,6 +45,8 @@
|
|
|
41
45
|
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
42
46
|
},
|
|
43
47
|
"dependencies": {
|
|
48
|
+
"@anthropic-ai/sdk": "^0.131.0",
|
|
49
|
+
"@google/genai": "^2.28.0",
|
|
44
50
|
"yaml": "^2.9.1",
|
|
45
51
|
"zod": "^4.6.5"
|
|
46
52
|
},
|