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.
- package/README.md +9 -1
- package/config.ts +37 -31
- package/index.ts +24 -18
- 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
|
-
|
|
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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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(
|
|
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
|
|
91
|
+
const resolvedName = resolveProviderName(p.name);
|
|
86
92
|
// Skip providers we've already bootstrapped
|
|
87
|
-
if (runtimes.has(
|
|
88
|
-
if (bootstrap(
|
|
89
|
-
const rt = runtimes.get(
|
|
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(
|
|
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(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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