pi-keyrouter 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +9 -1
  2. package/config.ts +37 -31
  3. package/index.ts +24 -18
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -110,7 +110,15 @@ Every key switch notifies the user with a Box widget:
110
110
 
111
111
  ## 🔧 Config
112
112
 
113
- `~/.pi/keyrouter.json` (or `<cwd>/.soly/keyrouter.json`, `<cwd>/.pi/keyrouter.json`).
113
+ **`~/.pi/keyrouter.json` only** (user-level, never project-scoped).
114
+
115
+ - Windows: `%USERPROFILE%\.pi\keyrouter.json`
116
+ - macOS/Linux: `~/.pi/keyrouter.json`
117
+
118
+ Config is global because API keys are personal credentials — they do not
119
+ belong inside a project directory. Project-local `keyrouter.json` files are
120
+ **deliberately ignored** (security: prevents a malicious repo from overriding
121
+ your real keys).
114
122
 
115
123
  ```json5
116
124
  {
package/config.ts CHANGED
@@ -2,10 +2,13 @@
2
2
  // config.ts — load key router config from disk
3
3
  // =============================================================================
4
4
  //
5
- // Looks in this order (first hit wins):
6
- // 1. <cwd>/.pi/keyrouter.json — project override
7
- // 2. <cwd>/.soly/keyrouter.json — soly convention
8
- // 3. ~/.pi/keyrouter.json — user-level default
5
+ // Config is GLOBAL (user-level), never project-scoped. API keys are personal
6
+ // credentials that do not belong inside a project directory (risk of leaking
7
+ // via git, shared repos, etc.).
8
+ //
9
+ // Single location: ~/.pi/keyrouter.json
10
+ // - Windows: %USERPROFILE%\.pi\keyrouter.json
11
+ // - macOS/Linux: ~/.pi/keyrouter.json
9
12
  //
10
13
  // Schema:
11
14
  // {
@@ -28,8 +31,6 @@ import * as os from "node:os";
28
31
  import * as path from "node:path";
29
32
  import type { KeyRouterConfig } from "./types.ts";
30
33
 
31
- const CONFIG_FILENAMES = ["keyrouter.json"];
32
-
33
34
  export function defaultConfig(): KeyRouterConfig {
34
35
  return {
35
36
  providers: [],
@@ -38,33 +39,38 @@ export function defaultConfig(): KeyRouterConfig {
38
39
  };
39
40
  }
40
41
 
41
- export function loadConfig(cwd: string, home?: string): KeyRouterConfig {
42
+ /**
43
+ * Resolve the config path. Always under the user profile (~/.pi/), never
44
+ * project-scoped. The `cwd` argument is accepted for API symmetry but
45
+ * ignored — keys are global.
46
+ *
47
+ * @param _cwd ignored — config is always user-level
48
+ * @param home override home dir (for testing)
49
+ */
50
+ export function configPath(_cwd?: string, home?: string): string {
42
51
  const homeDir = home ?? os.homedir();
43
- const candidates: string[] = [];
44
- for (const dir of [
45
- path.join(cwd, ".soly"),
46
- path.join(cwd, ".pi"),
47
- cwd,
48
- path.join(homeDir, ".soly"),
49
- path.join(homeDir, ".pi"),
50
- homeDir,
51
- ]) {
52
- for (const name of CONFIG_FILENAMES) {
53
- candidates.push(path.join(dir, name));
54
- }
55
- }
56
- for (const file of candidates) {
57
- if (fs.existsSync(file)) {
58
- try {
59
- const raw = fs.readFileSync(file, "utf-8");
60
- const parsed = JSON.parse(raw) as Partial<KeyRouterConfig>;
61
- return normalize(parsed);
62
- } catch {
63
- // bad config — fall through to default
64
- }
65
- }
52
+ return path.join(homeDir, ".pi", "keyrouter.json");
53
+ }
54
+
55
+ /**
56
+ * Path displayed in error messages / /keyrouter status so the user can see
57
+ * exactly where we're looking.
58
+ */
59
+ export function configSearchPaths(): string[] {
60
+ return [configPath()];
61
+ }
62
+
63
+ export function loadConfig(_cwd?: string, home?: string): KeyRouterConfig {
64
+ const file = configPath(undefined, home);
65
+ if (!fs.existsSync(file)) return defaultConfig();
66
+ try {
67
+ const raw = fs.readFileSync(file, "utf-8");
68
+ const parsed = JSON.parse(raw) as Partial<KeyRouterConfig>;
69
+ return normalize(parsed);
70
+ } catch {
71
+ // bad config — fall through to default
72
+ return defaultConfig();
66
73
  }
67
- return defaultConfig();
68
74
  }
69
75
 
70
76
  function normalize(input: Partial<KeyRouterConfig>): KeyRouterConfig {
package/index.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  // /reload
29
29
 
30
30
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
31
- import { loadConfig } from "./config.ts";
31
+ import { loadConfig, configPath } from "./config.ts";
32
32
  import {
33
33
  initKeyStates,
34
34
  isAvailable,
@@ -49,16 +49,22 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
49
49
  let notify: ((text: string, level: "info" | "warning" | "error") => void) | undefined;
50
50
  let activationNotified = false;
51
51
 
52
- function ensureRuntime(providerName: string, cfg: KeyRouterConfig): ProviderRuntime | undefined {
53
- let rt = runtimes.get(providerName);
52
+ /**
53
+ * Get-or-create the runtime for a provider. Keyed by the RESOLVED
54
+ * name (the authStorage id, e.g. "zai"), but populated from the
55
+ * provider config passed in directly (avoids name-mismatch bugs).
56
+ */
57
+ function ensureRuntime(
58
+ resolvedName: string,
59
+ providerCfg: { keys: ReadonlyArray<{ name: string; value: string }> },
60
+ ): ProviderRuntime {
61
+ let rt = runtimes.get(resolvedName);
54
62
  if (rt) return rt;
55
- const providerCfg = cfg.providers.find((p) => p.name === providerName);
56
- if (!providerCfg) return undefined;
57
63
  rt = {
58
64
  keys: initKeyStates(providerCfg.keys),
59
65
  currentIndex: -1,
60
66
  };
61
- runtimes.set(providerName, rt);
67
+ runtimes.set(resolvedName, rt);
62
68
  return rt;
63
69
  }
64
70
 
@@ -82,15 +88,15 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
82
88
  const authStorage = ctx.modelRegistry.authStorage;
83
89
  let newlyBootstrapped = 0;
84
90
  for (const p of config.providers) {
85
- const providerName = resolveProviderName(p.name);
91
+ const resolvedName = resolveProviderName(p.name);
86
92
  // Skip providers we've already bootstrapped
87
- if (runtimes.has(providerName)) continue;
88
- if (bootstrap(providerName)) {
89
- const rt = runtimes.get(providerName);
93
+ if (runtimes.has(resolvedName)) continue;
94
+ if (bootstrap(resolvedName, p)) {
95
+ const rt = runtimes.get(resolvedName);
90
96
  if (rt && rt.currentIndex >= 0) {
91
97
  const key = rt.keys[rt.currentIndex];
92
98
  if (key) {
93
- authStorage.setRuntimeApiKey(providerName, key.value);
99
+ authStorage.setRuntimeApiKey(resolvedName, key.value);
94
100
  newlyBootstrapped++;
95
101
  }
96
102
  }
@@ -107,11 +113,11 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
107
113
  }
108
114
 
109
115
  /** Set the initial key for a provider on first use. */
110
- function bootstrap(providerName: string): boolean {
111
- const cfg = config;
112
- if (!cfg) return false;
113
- const rt = ensureRuntime(providerName, cfg);
114
- if (!rt) return false;
116
+ function bootstrap(
117
+ resolvedName: string,
118
+ providerCfg: { keys: ReadonlyArray<{ name: string; value: string }> },
119
+ ): boolean {
120
+ const rt = ensureRuntime(resolvedName, providerCfg);
115
121
  if (rt.currentIndex >= 0) return true; // already bootstrapped
116
122
  const idx = pickNextKey(rt.keys, 0, Date.now());
117
123
  if (idx < 0) return false;
@@ -237,8 +243,8 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
237
243
  }
238
244
  if (!config || runtimes.size === 0) {
239
245
  ctx.ui.notify(
240
- "🔑 keyrouter: not active — no providers in ~/.pi/keyrouter.json " +
241
- "(checked cwd/.soly, cwd/.pi, cwd, ~/.soly, ~/.pi, ~)",
246
+ `🔑 keyrouter: not active — no ~/.pi/keyrouter.json found ` +
247
+ `(expected at ${configPath()}). Config is user-level only, never project-scoped.`,
242
248
  "warning",
243
249
  );
244
250
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-keyrouter",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "API key rotation for pi-coding-agent. Multiple keys per provider, automatic 429/401 fallback, max-retries guard.",
5
5
  "type": "module",
6
6
  "main": "index.ts",