auto-mode 0.6.0 → 0.7.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 CHANGED
@@ -31,14 +31,14 @@ Create the classifier configuration:
31
31
 
32
32
  ```json
33
33
  {
34
- "preset": "jev",
35
- "provider": { "apiKeyEnv": "TYPESAFE_API_KEY" }
34
+ "classifiers": { "jev": { "apiKeyEnv": "TYPESAFE_API_KEY" } },
35
+ "decision": { "classifier": "jev" }
36
36
  }
37
37
  ```
38
38
 
39
- Save it as `~/.config/auto-mode/config.json` and export your TypeSafe API key, or set
40
- `provider.apiKeyCommand` to a command that prints it.
41
- [Configuration](./docs/guides/configuration.md) covers credentials and provider overrides.
39
+ Save it as `~/.config/auto-mode/config.json` and export your TypeSafe API key, or set the entry's
40
+ `apiKeyCommand` to a command that prints it. [Configuration](./docs/guides/configuration.md) covers
41
+ the registries, credentials, and migrating an older file.
42
42
 
43
43
  ## Standing permissions
44
44
 
@@ -63,9 +63,10 @@ approval when every rule confidently allows the action, and a denial for uncerta
63
63
  denial names its rule, the harm, and what clears it, and the agent continues on another path. The
64
64
  confidence threshold is configurable and needs evaluation against your actions.
65
65
 
66
- A missing key, failed API call, malformed response, or oversized request follows `onFailure`. The
67
- default `defer` writes no verdict and keeps the prompt; `deny` fails closed. Failures write a
68
- diagnostic to stderr. auto-mode never truncates a Jev action to make it fit.
66
+ A missing key, failed API call, malformed response, or oversized request follows
67
+ `decision.onFailure`. The default `defer` writes no verdict and keeps the prompt; `deny` fails
68
+ closed. Failures write a diagnostic to stderr. auto-mode never truncates a Jev action to make it
69
+ fit.
69
70
 
70
71
  ## Commands
71
72
 
@@ -76,10 +77,11 @@ diagnostic to stderr. auto-mode never truncates a Jev action to make it fit.
76
77
  | `auto-mode run --local-only` | Skip the model tier |
77
78
  | `auto-mode run --jev-only` | Require Jev with a 5-second API timeout |
78
79
  | `auto-mode print-prompt` | Print the selected provider's base policy |
80
+ | `auto-mode config migrate` | Rewrite an older config file in place |
79
81
 
80
82
  The mod evaluates only ask decisions and preserves existing allow and deny decisions. It runs the
81
- CLI as a bounded child process with `--jev-only`. The `spark`, `claude`, and `glm` presets use the
82
- Messages API and serve library callers.
83
+ CLI as a bounded child process with `--jev-only`. The `spark`, `claude`, `glm`, and `messages`
84
+ classifier kinds use the Messages API and serve library callers.
83
85
 
84
86
  ## Library
85
87
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { n as parseActionRequest, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-9pnm4JBS.js";
3
- import { appendFile, mkdir } from "node:fs/promises";
2
+ import { d as resolveConfigPath, f as normalizeConfigFile, l as loadConfig, n as parseActionRequest, r as classifyAction, s as loadPolicy, t as renderVerdict } from "./render-verdict-CAg3AKWH.js";
3
+ import { appendFile, constants, copyFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { createHash, randomUUID } from "node:crypto";
@@ -18,6 +18,36 @@ function buildJevOnlyConfig(config) {
18
18
  };
19
19
  }
20
20
  //#endregion
21
+ //#region src/config/write-migrated-config.ts
22
+ async function writeMigratedConfig(path) {
23
+ let raw;
24
+ try {
25
+ raw = await readFile(path, "utf8");
26
+ } catch (error) {
27
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return `${path} does not exist; nothing to migrate`;
28
+ throw new Error(`${path} is unreadable`, { cause: error });
29
+ }
30
+ let json;
31
+ try {
32
+ json = JSON.parse(raw);
33
+ } catch {
34
+ throw new Error(`${path} is not valid JSON`);
35
+ }
36
+ const normalized = normalizeConfigFile(json, path);
37
+ if (!normalized.isLegacy) return `${path} already uses the current shape`;
38
+ const backup = `${path}.bak`;
39
+ const staged = `${path}.migrating`;
40
+ try {
41
+ await copyFile(path, backup, constants.COPYFILE_EXCL);
42
+ } catch (error) {
43
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") throw new Error(`${backup} already exists; move it aside and run the migration again`, { cause: error });
44
+ throw error;
45
+ }
46
+ await writeFile(staged, `${JSON.stringify(normalized.file, null, 2)}\n`, { mode: 384 });
47
+ await rename(staged, path);
48
+ return `rewrote ${path} in the current shape; the old file is at ${backup}`;
49
+ }
50
+ //#endregion
21
51
  //#region src/diagnostics/write-action-diagnostic.ts
22
52
  async function writeActionDiagnostic(payload, entry) {
23
53
  const stateHome = process.env["XDG_STATE_HOME"];
@@ -54,6 +84,7 @@ const USAGE = `auto-mode — a permission classifier for the auto-mode Claude Co
54
84
  Usage:
55
85
  auto-mode run Read an action request on stdin, write a verdict on stdout
56
86
  auto-mode print-prompt Print the system prompt the classifier receives
87
+ auto-mode config migrate Rewrite the config file from the old keys into the current shape
57
88
 
58
89
  Options:
59
90
  --classifier <path> Use this framework file instead of the shipped one
@@ -92,6 +123,7 @@ async function run(explain, localOnly, jevOnly, deadline) {
92
123
  });
93
124
  return printNote(true, "configuration unreadable; no verdict");
94
125
  }
126
+ printWarnings(loaded.warnings);
95
127
  const config = jevOnly ? buildJevOnlyConfig(loaded) : loaded;
96
128
  if (config === null) {
97
129
  await writeActionDiagnostic(request, {
@@ -140,6 +172,9 @@ async function run(explain, localOnly, jevOnly, deadline) {
140
172
  function writeVerdict(verdict) {
141
173
  process.stdout.write(renderVerdict(verdict));
142
174
  }
175
+ function printWarnings(warnings = []) {
176
+ for (const warning of warnings) process.stderr.write(`auto-mode: ${warning}\n`);
177
+ }
143
178
  function printNote(explain, message) {
144
179
  if (explain) process.stderr.write(`auto-mode: ${message}\n`);
145
180
  return 0;
@@ -174,6 +209,7 @@ async function main(argv) {
174
209
  }
175
210
  if (command === "print-prompt") {
176
211
  const config = await loadConfig();
212
+ printWarnings(config.warnings);
177
213
  const framework = config.provider.protocol === "system-one" ? "decision.md" : "classifier.md";
178
214
  const prompt = await loadPolicy({
179
215
  classifierPath: args.values.classifier ?? config.classifierPath,
@@ -182,9 +218,21 @@ async function main(argv) {
182
218
  process.stdout.write(`${prompt}\n`);
183
219
  return 0;
184
220
  }
221
+ if (command === "config" && args.positionals[1] === "migrate") return runConfigMigration();
185
222
  process.stderr.write(`auto-mode: unknown command '${command}'\n\n${USAGE}`);
186
223
  return 2;
187
224
  }
225
+ async function runConfigMigration() {
226
+ const path = resolveConfigPath();
227
+ try {
228
+ const result = await writeMigratedConfig(path);
229
+ process.stdout.write(`${result}\n`);
230
+ return 0;
231
+ } catch (error) {
232
+ process.stderr.write(`auto-mode: ${error instanceof Error ? error.message : "config migration failed"}\n`);
233
+ return 1;
234
+ }
235
+ }
188
236
  process.exitCode = await main(process.argv.slice(2));
189
237
  //#endregion
190
238
  export {};
package/dist/index.d.ts CHANGED
@@ -4,6 +4,9 @@ interface EvaluationOptions {
4
4
  readonly signal?: Readonly<AbortSignal> | undefined;
5
5
  }
6
6
  //#endregion
7
+ //#region src/config/presets.d.ts
8
+ export declare const PRESETS: Readonly<Record<string, ProviderConfig>>;
9
+ //#endregion
7
10
  //#region src/config/config.d.ts
8
11
  interface ProviderConfig {
9
12
  readonly protocol?: 'messages' | 'system-one';
@@ -15,16 +18,24 @@ interface ProviderConfig {
15
18
  readonly maxTokens: number;
16
19
  readonly timeoutMs: number;
17
20
  }
21
+ interface ScopeSource {
22
+ readonly kind: ScopeSourceKind;
23
+ readonly paths?: readonly string[];
24
+ }
25
+ type ScopeSourceKind = (typeof SCOPE_SOURCE_KINDS)[number];
26
+ declare const SCOPE_SOURCE_KINDS: readonly ['cwd', 'session', 'globs', 'atc'];
18
27
  interface Config {
19
28
  readonly provider: ProviderConfig;
29
+ readonly judge?: ProviderConfig | null;
30
+ readonly scopeSources?: Readonly<Record<string, ScopeSource>>;
20
31
  readonly classifierPath?: string | undefined;
21
32
  readonly rulesPath?: string | undefined;
22
33
  readonly onFailure: 'defer' | 'deny';
23
34
  readonly claudeSettingsPath?: string | null | undefined;
24
35
  readonly minConfidence?: number | undefined;
36
+ readonly warnings?: readonly string[];
25
37
  }
26
38
  export declare const DEFAULT_CONFIG: Config;
27
- export declare const PRESETS: Readonly<Record<string, ProviderConfig>>;
28
39
  export declare function resolveConfigPath(): string;
29
40
  export declare function loadConfig(path?: string): Promise<Config>;
30
41
  export declare function resolveApiKey(provider: ProviderConfig, options?: EvaluationOptions): Promise<string | null>;
@@ -143,4 +154,4 @@ type LocalVerdict = {
143
154
  };
144
155
  export declare function classifyLocally(payload: ActionRequest): LocalVerdict;
145
156
  //#endregion
146
- export type { ActionOutcome, ActionRequest, ClassifyOptions, Config, DecisionContext, LocalVerdict, ModelOutcome, PolicyPaths, ProviderConfig, Verdict };
157
+ export type { ActionOutcome, ActionRequest, ClassifyOptions, Config, DecisionContext, LocalVerdict, ModelOutcome, PolicyPaths, ProviderConfig, ScopeSource, Verdict };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as classifyWithModel, c as DEFAULT_CONFIG, d as resolveApiKey, f as resolveConfigPath, i as classifyLocally, l as PRESETS, n as parseActionRequest, o as parseModelVerdict, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-9pnm4JBS.js";
1
+ import { a as classifyWithModel, c as DEFAULT_CONFIG, d as resolveConfigPath, i as classifyLocally, l as loadConfig, n as parseActionRequest, o as parseModelVerdict, p as PRESETS, r as classifyAction, s as loadPolicy, t as renderVerdict, u as resolveApiKey } from "./render-verdict-CAg3AKWH.js";
2
2
  export { DEFAULT_CONFIG, PRESETS, classifyAction, classifyLocally, classifyWithModel, loadConfig, loadPolicy, parseActionRequest, parseModelVerdict, renderVerdict, resolveApiKey, resolveConfigPath };
@@ -252,7 +252,7 @@ function safeExtend(schema, shape) {
252
252
  if (!isPlainObject(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
253
253
  return clone(schema, mergeDefs(schema._zod.def, { shape: extended(schema, shape) }));
254
254
  }
255
- function merge$1(a, b) {
255
+ function merge(a, b) {
256
256
  if (!b?._zod?.def) throw new Error("Invalid input to merge: expected an object schema. To merge a plain shape, use `.extend()`.");
257
257
  if (a._zod.def.checks?.length) throw new Error(".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.");
258
258
  const newShape = {};
@@ -5218,7 +5218,7 @@ const ZodObject = /*@__PURE__*/ $constructor("ZodObject", (inst, def) => {
5218
5218
  return safeExtend(this, incoming);
5219
5219
  },
5220
5220
  merge(other) {
5221
- return merge$1(this, other);
5221
+ return merge(this, other);
5222
5222
  },
5223
5223
  pick(mask) {
5224
5224
  return pick(this, mask);
@@ -5569,6 +5569,171 @@ async function loadClaudeRules(path) {
5569
5569
  hard_deny: (rules.autoMode?.hard_deny ?? []).filter((entry) => entry !== "$defaults")
5570
5570
  };
5571
5571
  }
5572
+ const PRESETS = {
5573
+ jev: {
5574
+ protocol: "system-one",
5575
+ baseURL: "https://api.typesafe.ai",
5576
+ model: "jev-1.13.0",
5577
+ apiKeyEnv: "TYPESAFE_API_KEY",
5578
+ reasoning: false,
5579
+ maxTokens: 3e3,
5580
+ timeoutMs: 5e3
5581
+ },
5582
+ spark: {
5583
+ baseURL: "https://api.meta.ai",
5584
+ model: "muse-spark-1.3-contributor",
5585
+ apiKeyEnv: "META_API_KEY",
5586
+ reasoning: true,
5587
+ maxTokens: 3e3,
5588
+ timeoutMs: 45e3
5589
+ },
5590
+ claude: {
5591
+ baseURL: "https://api.anthropic.com",
5592
+ model: "claude-haiku-4-5-20251001",
5593
+ apiKeyEnv: "ANTHROPIC_API_KEY",
5594
+ reasoning: true,
5595
+ maxTokens: 3e3,
5596
+ timeoutMs: 45e3
5597
+ },
5598
+ glm: {
5599
+ baseURL: "https://api.z.ai/api/anthropic",
5600
+ model: "glm-5.3-flash",
5601
+ apiKeyEnv: "ZAI_API_KEY",
5602
+ reasoning: true,
5603
+ maxTokens: 3e3,
5604
+ timeoutMs: 6e4
5605
+ }
5606
+ };
5607
+ const MESSAGES_DEFAULTS = {
5608
+ protocol: "messages",
5609
+ baseURL: "https://api.anthropic.com",
5610
+ reasoning: true,
5611
+ maxTokens: 3e3,
5612
+ timeoutMs: 45e3
5613
+ };
5614
+ function findPreset(id) {
5615
+ return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
5616
+ }
5617
+ //#endregion
5618
+ //#region src/config/normalize-config-file.ts
5619
+ function normalizeConfigFile(json, path) {
5620
+ if (!isRecord(json)) return {
5621
+ file: json,
5622
+ isLegacy: false,
5623
+ warnings: []
5624
+ };
5625
+ const legacyKeys = Object.keys(json).filter((key) => LEGACY_KEYS.has(key));
5626
+ if (legacyKeys.length === 0) return {
5627
+ file: json,
5628
+ isLegacy: false,
5629
+ warnings: []
5630
+ };
5631
+ const currentKeys = Object.keys(json).filter((key) => !LEGACY_KEYS.has(key));
5632
+ if (currentKeys.length > 0) throw new Error(`${path} mixes the old keys (${legacyKeys.join(", ")}) with the new ones (${currentKeys.join(", ")}); run \`auto-mode config migrate\` on a copy without the new keys`);
5633
+ const parsed = legacyConfigSchema.safeParse(json);
5634
+ if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
5635
+ const legacy = parsed.data;
5636
+ const warnings = [`${path} uses the old config keys (${legacyKeys.join(", ")}); run \`auto-mode config migrate\` to rewrite it`, ...legacy.transcriptEntries === void 0 ? [] : [`${path} sets transcriptEntries, which has no effect; migration drops it`]];
5637
+ return {
5638
+ file: buildFileFromLegacy(legacy, path),
5639
+ isLegacy: true,
5640
+ warnings
5641
+ };
5642
+ }
5643
+ function isRecord(value) {
5644
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5645
+ }
5646
+ const LEGACY_KEYS = /* @__PURE__ */ new Set([
5647
+ "preset",
5648
+ "provider",
5649
+ "classifierPath",
5650
+ "rulesPath",
5651
+ "transcriptEntries",
5652
+ "onFailure",
5653
+ "claudeSettingsPath",
5654
+ "minConfidence"
5655
+ ]);
5656
+ const text$1 = string().min(1);
5657
+ const positive$1 = number().positive();
5658
+ const legacyConfigSchema = strictObject({
5659
+ preset: text$1.optional(),
5660
+ provider: strictObject({
5661
+ protocol: _enum(["messages", "system-one"]).optional(),
5662
+ baseURL: text$1.optional(),
5663
+ model: text$1.optional(),
5664
+ apiKeyEnv: text$1.optional(),
5665
+ apiKeyCommand: text$1.optional(),
5666
+ reasoning: boolean().optional(),
5667
+ maxTokens: positive$1.optional(),
5668
+ timeoutMs: positive$1.optional()
5669
+ }).optional(),
5670
+ classifierPath: text$1.optional(),
5671
+ rulesPath: text$1.optional(),
5672
+ transcriptEntries: number().nonnegative().optional(),
5673
+ onFailure: _enum(["defer", "deny"]).optional(),
5674
+ claudeSettingsPath: text$1.nullable().optional(),
5675
+ minConfidence: number().min(.5).max(1).optional()
5676
+ });
5677
+ function buildFileFromLegacy(legacy, path) {
5678
+ const hasClassifier = legacy.preset !== void 0 || legacy.provider !== void 0;
5679
+ const decision = {
5680
+ ...hasClassifier ? { classifier: getLegacyClassifierID(legacy) } : {},
5681
+ ...legacy.minConfidence === void 0 ? {} : { minConfidence: legacy.minConfidence },
5682
+ ...legacy.onFailure === void 0 ? {} : { onFailure: legacy.onFailure }
5683
+ };
5684
+ const policy = {
5685
+ ...legacy.rulesPath === void 0 ? {} : { rulesPath: legacy.rulesPath },
5686
+ ...legacy.classifierPath === void 0 ? {} : { frameworkPath: legacy.classifierPath },
5687
+ ...legacy.claudeSettingsPath === void 0 ? {} : { claudeSettingsPath: legacy.claudeSettingsPath }
5688
+ };
5689
+ return {
5690
+ ...hasClassifier ? { classifiers: { [getLegacyClassifierID(legacy)]: buildClassifierEntry(getLegacyClassifierID(legacy), buildLegacyProvider(legacy, path)) } } : {},
5691
+ ...Object.keys(decision).length === 0 ? {} : { decision },
5692
+ ...Object.keys(policy).length === 0 ? {} : { policy }
5693
+ };
5694
+ }
5695
+ function getLegacyClassifierID(legacy) {
5696
+ if (legacy.preset !== void 0) return legacy.preset;
5697
+ return legacy.provider !== void 0 && legacy.provider.protocol !== "system-one" ? "spark" : "jev";
5698
+ }
5699
+ function buildLegacyProvider(legacy, path) {
5700
+ const preset = findPreset(getLegacyClassifierID(legacy));
5701
+ if (preset === void 0) throw new Error(`${path} names an unknown preset '${legacy.preset}'; known presets are ${Object.keys(PRESETS).join(", ")}`);
5702
+ const override = legacy.provider ?? {};
5703
+ return {
5704
+ protocol: override.protocol ?? preset.protocol ?? "messages",
5705
+ baseURL: override.baseURL ?? preset.baseURL,
5706
+ model: override.model ?? preset.model,
5707
+ apiKeyEnv: override.apiKeyEnv ?? preset.apiKeyEnv,
5708
+ apiKeyCommand: override.apiKeyCommand ?? preset.apiKeyCommand,
5709
+ reasoning: override.reasoning ?? preset.reasoning,
5710
+ maxTokens: override.maxTokens ?? preset.maxTokens,
5711
+ timeoutMs: override.timeoutMs ?? preset.timeoutMs
5712
+ };
5713
+ }
5714
+ const PROVIDER_FIELDS = [
5715
+ "baseURL",
5716
+ "model",
5717
+ "apiKeyEnv",
5718
+ "apiKeyCommand",
5719
+ "reasoning",
5720
+ "maxTokens",
5721
+ "timeoutMs"
5722
+ ];
5723
+ function buildClassifierEntry(id, provider) {
5724
+ const kind = pickEntryKind(id, provider);
5725
+ const base = kind === "messages" ? MESSAGES_DEFAULTS : findPreset(kind) ?? {};
5726
+ const fields = Object.fromEntries(PROVIDER_FIELDS.filter((field) => provider[field] !== void 0 && provider[field] !== base[field]).map((field) => [field, provider[field]]));
5727
+ return {
5728
+ ...kind === id ? {} : { kind },
5729
+ ...fields
5730
+ };
5731
+ }
5732
+ function pickEntryKind(id, provider) {
5733
+ const preset = findPreset(id);
5734
+ if (preset !== void 0 && (preset.protocol ?? "messages") === provider.protocol) return id;
5735
+ return provider.protocol === "system-one" ? "jev" : "messages";
5736
+ }
5572
5737
  //#endregion
5573
5738
  //#region src/config/read-api-key-from-command.ts
5574
5739
  function readApiKeyFromCommand(command, options = {}) {
@@ -5624,73 +5789,24 @@ function readApiKeyFromCommand(command, options = {}) {
5624
5789
  }
5625
5790
  //#endregion
5626
5791
  //#region src/config/config.ts
5792
+ const SCOPE_SOURCE_KINDS = [
5793
+ "cwd",
5794
+ "session",
5795
+ "globs",
5796
+ "atc"
5797
+ ];
5798
+ const DEFAULT_PROVIDER = findPreset("jev");
5799
+ if (DEFAULT_PROVIDER === void 0) throw new Error("the jev preset is missing");
5627
5800
  const DEFAULT_CONFIG = {
5628
- provider: {
5629
- protocol: "system-one",
5630
- baseURL: "https://api.typesafe.ai",
5631
- model: "jev-1.13.0",
5632
- apiKeyEnv: "TYPESAFE_API_KEY",
5633
- reasoning: false,
5634
- maxTokens: 3e3,
5635
- timeoutMs: 5e3
5636
- },
5801
+ provider: DEFAULT_PROVIDER,
5637
5802
  onFailure: "defer",
5638
5803
  minConfidence: .8
5639
5804
  };
5640
- const SPARK_PROVIDER = {
5641
- baseURL: "https://api.meta.ai",
5642
- model: "muse-spark-1.3-contributor",
5643
- apiKeyEnv: "META_API_KEY",
5644
- reasoning: true,
5645
- maxTokens: 3e3,
5646
- timeoutMs: 45e3
5647
- };
5648
- const PRESETS = {
5649
- jev: DEFAULT_CONFIG.provider,
5650
- spark: SPARK_PROVIDER,
5651
- claude: {
5652
- baseURL: "https://api.anthropic.com",
5653
- model: "claude-haiku-4-5-20251001",
5654
- apiKeyEnv: "ANTHROPIC_API_KEY",
5655
- reasoning: true,
5656
- maxTokens: 3e3,
5657
- timeoutMs: 45e3
5658
- },
5659
- glm: {
5660
- baseURL: "https://api.z.ai/api/anthropic",
5661
- model: "glm-5.3-flash",
5662
- apiKeyEnv: "ZAI_API_KEY",
5663
- reasoning: true,
5664
- maxTokens: 3e3,
5665
- timeoutMs: 6e4
5666
- }
5667
- };
5668
5805
  function resolveConfigPath() {
5669
5806
  const xdg = process.env["XDG_CONFIG_HOME"];
5670
5807
  const base = xdg !== void 0 && xdg !== "" ? xdg : join(homedir(), ".config");
5671
5808
  return join(base, "auto-mode", "config.json");
5672
5809
  }
5673
- const text = string().min(1);
5674
- const positive = number().positive();
5675
- const configFileSchema = strictObject({
5676
- preset: text.optional(),
5677
- provider: strictObject({
5678
- protocol: _enum(["messages", "system-one"]).optional(),
5679
- baseURL: text.optional(),
5680
- model: text.optional(),
5681
- apiKeyEnv: text.optional(),
5682
- apiKeyCommand: text.optional(),
5683
- reasoning: boolean().optional(),
5684
- maxTokens: positive.optional(),
5685
- timeoutMs: positive.optional()
5686
- }).optional(),
5687
- classifierPath: text.optional(),
5688
- rulesPath: text.optional(),
5689
- transcriptEntries: number().nonnegative().optional(),
5690
- onFailure: _enum(["defer", "deny"]).optional(),
5691
- claudeSettingsPath: text.nullable().optional(),
5692
- minConfidence: number().min(.5).max(1).optional()
5693
- });
5694
5810
  async function loadConfig(path = resolveConfigPath()) {
5695
5811
  let raw;
5696
5812
  try {
@@ -5705,33 +5821,141 @@ async function loadConfig(path = resolveConfigPath()) {
5705
5821
  } catch {
5706
5822
  throw new Error(`${path} is not valid JSON`);
5707
5823
  }
5824
+ const normalized = normalizeConfigFile(json, path);
5825
+ return buildConfig(normalized.file, path, normalized.warnings);
5826
+ }
5827
+ const text = string().min(1);
5828
+ const positive = number().positive();
5829
+ const configFileSchema = strictObject({
5830
+ classifiers: record(text, unknown()).optional(),
5831
+ scopeSources: record(text, unknown()).optional(),
5832
+ decision: strictObject({
5833
+ classifier: text.optional(),
5834
+ judge: text.nullable().optional(),
5835
+ minConfidence: number().min(.5).max(1).optional(),
5836
+ onFailure: _enum(["defer", "deny"]).optional()
5837
+ }).optional(),
5838
+ policy: strictObject({
5839
+ rulesPath: text.nullable().optional(),
5840
+ frameworkPath: text.nullable().optional(),
5841
+ claudeSettingsPath: text.nullable().optional()
5842
+ }).optional()
5843
+ });
5844
+ function buildConfig(json, path, legacyWarnings) {
5708
5845
  const parsed = configFileSchema.safeParse(json);
5709
5846
  if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
5710
- return merge(parsed.data, path);
5847
+ const file = parsed.data;
5848
+ const warnings = [...legacyWarnings];
5849
+ const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
5850
+ const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
5851
+ warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`));
5852
+ const decision = file.decision ?? {};
5853
+ const policy = file.policy ?? {};
5854
+ const judgeID = decision.judge ?? null;
5855
+ return {
5856
+ provider: resolveRole("classifier", decision.classifier ?? "jev", classifiers.entries, classifiers.droppedIDs, path),
5857
+ judge: judgeID === null ? null : resolveRole("judge", judgeID, classifiers.entries, classifiers.droppedIDs, path),
5858
+ scopeSources: scopeSources.entries,
5859
+ classifierPath: expandHomePath(policy.frameworkPath ?? void 0),
5860
+ rulesPath: expandHomePath(policy.rulesPath ?? void 0),
5861
+ onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
5862
+ claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath),
5863
+ minConfidence: decision.minConfidence ?? DEFAULT_CONFIG.minConfidence,
5864
+ warnings
5865
+ };
5711
5866
  }
5712
- function merge(file, path) {
5713
- const implicitProvider = file.provider !== void 0 && file.provider.protocol !== "system-one" ? SPARK_PROVIDER : DEFAULT_CONFIG.provider;
5714
- const preset = file.preset === void 0 ? implicitProvider : PRESETS[file.preset];
5715
- if (preset === void 0) throw new Error(`${path} names an unknown preset '${file.preset}'; known presets are ${Object.keys(PRESETS).join(", ")}`);
5716
- const override = file.provider ?? {};
5867
+ function buildRegistry(raw, parseEntry) {
5868
+ const entries = /* @__PURE__ */ new Map();
5869
+ const dropped = [];
5870
+ const droppedIDs = [];
5871
+ for (const [id, entry] of Object.entries(raw)) {
5872
+ const result = parseEntry(id, entry);
5873
+ if (typeof result === "string") {
5874
+ dropped.push(`${id} dropped: ${result}`);
5875
+ droppedIDs.push(id);
5876
+ } else entries.set(id, result);
5877
+ }
5717
5878
  return {
5718
- provider: {
5719
- protocol: override.protocol ?? preset.protocol ?? "messages",
5720
- baseURL: override.baseURL ?? preset.baseURL,
5721
- model: override.model ?? preset.model,
5722
- apiKeyEnv: override.apiKeyEnv ?? preset.apiKeyEnv,
5723
- apiKeyCommand: override.apiKeyCommand ?? preset.apiKeyCommand,
5724
- reasoning: override.reasoning ?? preset.reasoning,
5725
- maxTokens: override.maxTokens ?? preset.maxTokens,
5726
- timeoutMs: override.timeoutMs ?? preset.timeoutMs
5727
- },
5728
- classifierPath: file.classifierPath,
5729
- rulesPath: file.rulesPath,
5730
- onFailure: file.onFailure ?? DEFAULT_CONFIG.onFailure,
5731
- claudeSettingsPath: file.claudeSettingsPath,
5732
- minConfidence: file.minConfidence ?? DEFAULT_CONFIG.minConfidence
5879
+ entries: Object.fromEntries(entries),
5880
+ dropped,
5881
+ droppedIDs
5882
+ };
5883
+ }
5884
+ const CLASSIFIER_KINDS = [...Object.keys(PRESETS), "messages"];
5885
+ const classifierEntrySchema = strictObject({
5886
+ kind: text.optional(),
5887
+ baseURL: text.optional(),
5888
+ model: text.optional(),
5889
+ apiKeyEnv: text.optional(),
5890
+ apiKeyCommand: text.optional(),
5891
+ reasoning: boolean().optional(),
5892
+ maxTokens: positive.optional(),
5893
+ timeoutMs: positive.optional()
5894
+ });
5895
+ const CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
5896
+ "apiKey",
5897
+ "key",
5898
+ "token",
5899
+ "secret"
5900
+ ]);
5901
+ function parseClassifierEntry(id, entry) {
5902
+ if (typeof entry === "object" && entry !== null) {
5903
+ const credential = Object.keys(entry).find((key) => CREDENTIAL_KEYS.has(key));
5904
+ if (credential !== void 0) return `holds a literal ${credential}; name the key with apiKeyEnv or apiKeyCommand`;
5905
+ }
5906
+ const parsed = classifierEntrySchema.safeParse(entry);
5907
+ if (!parsed.success) return formatFirstIssue(parsed.error.issues);
5908
+ const kind = parsed.data.kind ?? id;
5909
+ if (!CLASSIFIER_KINDS.includes(kind)) return `unknown kind '${kind}'; known kinds are ${CLASSIFIER_KINDS.join(", ")}`;
5910
+ const base = kind === "messages" ? MESSAGES_DEFAULTS : findPreset(kind);
5911
+ const model = parsed.data.model ?? base?.model;
5912
+ if (base === void 0 || model === void 0) return `kind '${kind}' needs a model`;
5913
+ return {
5914
+ protocol: base.protocol ?? "messages",
5915
+ baseURL: parsed.data.baseURL ?? base.baseURL,
5916
+ model,
5917
+ apiKeyEnv: parsed.data.apiKeyEnv ?? base.apiKeyEnv,
5918
+ apiKeyCommand: parsed.data.apiKeyCommand ?? base.apiKeyCommand,
5919
+ reasoning: parsed.data.reasoning ?? base.reasoning,
5920
+ maxTokens: parsed.data.maxTokens ?? base.maxTokens,
5921
+ timeoutMs: parsed.data.timeoutMs ?? base.timeoutMs
5733
5922
  };
5734
5923
  }
5924
+ const scopeSourceEntrySchema = strictObject({
5925
+ kind: text.optional(),
5926
+ paths: array(text).min(1).optional()
5927
+ });
5928
+ function parseScopeSourceEntry(id, entry) {
5929
+ const parsed = scopeSourceEntrySchema.safeParse(entry);
5930
+ if (!parsed.success) return formatFirstIssue(parsed.error.issues);
5931
+ const kind = SCOPE_SOURCE_KINDS.find((known) => known === (parsed.data.kind ?? id));
5932
+ if (kind === void 0) return `unknown kind '${parsed.data.kind ?? id}'; known kinds are ${SCOPE_SOURCE_KINDS.join(", ")}`;
5933
+ if (kind === "globs") return parsed.data.paths === void 0 ? "kind 'globs' needs paths" : {
5934
+ kind,
5935
+ paths: parsed.data.paths
5936
+ };
5937
+ return parsed.data.paths === void 0 ? { kind } : `kind '${kind}' takes no paths`;
5938
+ }
5939
+ function formatFirstIssue(issues) {
5940
+ const [issue] = issues;
5941
+ if (issue === void 0) return "invalid entry";
5942
+ return issue.path.length === 0 ? issue.message : `${issue.path.map(String).join(".")}: ${issue.message}`;
5943
+ }
5944
+ function resolveRole(role, id, entries, droppedIDs, path) {
5945
+ const entry = Object.hasOwn(entries, id) ? entries[id] : void 0;
5946
+ if (entry !== void 0) return entry;
5947
+ if (droppedIDs.includes(id)) throw new Error(`${path}: decision.${role} names '${id}', whose entry was dropped`);
5948
+ if (findPreset(id) === void 0) throw new Error(`${path}: decision.${role} names '${id}', which is neither a classifiers entry nor a built-in kind (${Object.keys(PRESETS).join(", ")})`);
5949
+ return parseBuiltIn(id);
5950
+ }
5951
+ function parseBuiltIn(id) {
5952
+ const resolved = parseClassifierEntry(id, {});
5953
+ if (typeof resolved === "string") throw new TypeError(`built-in kind ${id} does not resolve: ${resolved}`);
5954
+ return resolved;
5955
+ }
5956
+ function expandHomePath(path) {
5957
+ return path?.startsWith("~/") === true ? join(homedir(), path.slice(2)) : path;
5958
+ }
5735
5959
  function resolveApiKey(provider, options = {}) {
5736
5960
  const fromEnv = provider.apiKeyEnv === void 0 ? void 0 : process.env[provider.apiKeyEnv];
5737
5961
  if (fromEnv !== void 0 && fromEnv !== "") return Promise.resolve(fromEnv);
@@ -7076,4 +7300,4 @@ function renderVerdict(verdict) {
7076
7300
  })).exhaustive();
7077
7301
  }
7078
7302
  //#endregion
7079
- export { classifyWithModel as a, DEFAULT_CONFIG as c, resolveApiKey as d, resolveConfigPath as f, classifyLocally as i, PRESETS as l, parseActionRequest as n, parseModelVerdict as o, classifyAction as r, loadPolicy as s, renderVerdict as t, loadConfig as u };
7303
+ export { classifyWithModel as a, DEFAULT_CONFIG as c, resolveConfigPath as d, normalizeConfigFile as f, classifyLocally as i, loadConfig as l, parseActionRequest as n, parseModelVerdict as o, PRESETS as p, classifyAction as r, loadPolicy as s, renderVerdict as t, resolveApiKey as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-mode",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "A permission classifier library and Claude Code mod, so agents that are not Claude can run unattended",
5
5
  "keywords": [
6
6
  "agent",