pi-keyrouter 0.2.0 → 0.2.2
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 +66 -29
- 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,
|
|
@@ -47,6 +47,7 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
47
47
|
let config: KeyRouterConfig | undefined;
|
|
48
48
|
const runtimes = new Map<string, ProviderRuntime>();
|
|
49
49
|
let notify: ((text: string, level: "info" | "warning" | "error") => void) | undefined;
|
|
50
|
+
let activationNotified = false;
|
|
50
51
|
|
|
51
52
|
function ensureRuntime(providerName: string, cfg: KeyRouterConfig): ProviderRuntime | undefined {
|
|
52
53
|
let rt = runtimes.get(providerName);
|
|
@@ -61,6 +62,50 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
61
62
|
return rt;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Activate the router: load config (once), bootstrap all providers.
|
|
67
|
+
* Idempotent — safe to call on every before_agent_start. Only runs
|
|
68
|
+
* the bootstrap the FIRST time for each provider.
|
|
69
|
+
*/
|
|
70
|
+
async function activate(ctx: {
|
|
71
|
+
cwd: string;
|
|
72
|
+
ui: { notify: (t: string, l?: "info" | "warning" | "error") => void };
|
|
73
|
+
modelRegistry: { authStorage: { setRuntimeApiKey: (p: string, k: string) => void } };
|
|
74
|
+
}): Promise<void> {
|
|
75
|
+
// Load config once (reload clears it)
|
|
76
|
+
if (!config) {
|
|
77
|
+
config = loadConfig(ctx.cwd);
|
|
78
|
+
}
|
|
79
|
+
if (config.providers.length === 0) return;
|
|
80
|
+
notify = (text, level) => ctx.ui.notify(text, level);
|
|
81
|
+
|
|
82
|
+
const authStorage = ctx.modelRegistry.authStorage;
|
|
83
|
+
let newlyBootstrapped = 0;
|
|
84
|
+
for (const p of config.providers) {
|
|
85
|
+
const providerName = resolveProviderName(p.name);
|
|
86
|
+
// Skip providers we've already bootstrapped
|
|
87
|
+
if (runtimes.has(providerName)) continue;
|
|
88
|
+
if (bootstrap(providerName)) {
|
|
89
|
+
const rt = runtimes.get(providerName);
|
|
90
|
+
if (rt && rt.currentIndex >= 0) {
|
|
91
|
+
const key = rt.keys[rt.currentIndex];
|
|
92
|
+
if (key) {
|
|
93
|
+
authStorage.setRuntimeApiKey(providerName, key.value);
|
|
94
|
+
newlyBootstrapped++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Only notify on first activation (when we bootstrapped at least one)
|
|
100
|
+
if (newlyBootstrapped > 0 && !activationNotified) {
|
|
101
|
+
activationNotified = true;
|
|
102
|
+
ctx.ui.notify(
|
|
103
|
+
`🔑 keyrouter: active (${config.providers.length} provider(s), ${config.providers.reduce((a, p) => a + p.keys.length, 0)} keys)`,
|
|
104
|
+
"info",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
64
109
|
/** Set the initial key for a provider on first use. */
|
|
65
110
|
function bootstrap(providerName: string): boolean {
|
|
66
111
|
const cfg = config;
|
|
@@ -129,34 +174,14 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
129
174
|
}
|
|
130
175
|
|
|
131
176
|
pi.on("session_start", async (_event, ctx) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return; // nothing to do
|
|
135
|
-
}
|
|
136
|
-
notify = (text, level) => ctx.ui.notify(text, level);
|
|
177
|
+
await activate(ctx);
|
|
178
|
+
});
|
|
137
179
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (bootstrap(providerName)) {
|
|
144
|
-
const rt = runtimes.get(providerName);
|
|
145
|
-
if (rt && rt.currentIndex >= 0) {
|
|
146
|
-
const key = rt.keys[rt.currentIndex];
|
|
147
|
-
if (key) {
|
|
148
|
-
authStorage.setRuntimeApiKey(providerName, key.value);
|
|
149
|
-
bootstrapped++;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (bootstrapped > 0) {
|
|
155
|
-
ctx.ui.notify(
|
|
156
|
-
`🔑 keyrouter: active (${bootstrapped} provider(s), ${config.providers.reduce((a, p) => a + p.keys.length, 0)} keys)`,
|
|
157
|
-
"info",
|
|
158
|
-
);
|
|
159
|
-
}
|
|
180
|
+
// Lazy bootstrap: also fire on every turn. This handles /reload (which
|
|
181
|
+
// does NOT re-fire session_start) and config changes mid-session.
|
|
182
|
+
// activate() is idempotent — only bootstraps once per provider.
|
|
183
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
184
|
+
await activate(ctx);
|
|
160
185
|
});
|
|
161
186
|
|
|
162
187
|
pi.on("after_provider_response", async (event, ctx) => {
|
|
@@ -196,6 +221,7 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
196
221
|
runtimes.clear();
|
|
197
222
|
config = undefined;
|
|
198
223
|
notify = undefined;
|
|
224
|
+
activationNotified = false;
|
|
199
225
|
});
|
|
200
226
|
|
|
201
227
|
pi.registerCommand("keyrouter", {
|
|
@@ -203,8 +229,18 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
203
229
|
handler: async (args, ctx) => {
|
|
204
230
|
const sub = args.trim().split(/\s+/)[0] ?? "status";
|
|
205
231
|
if (sub === "status") {
|
|
232
|
+
// On-demand activation in case session_start/before_agent_start
|
|
233
|
+
// haven't fired yet (e.g. user ran /keyrouter status right after
|
|
234
|
+
// /reload without sending a prompt).
|
|
235
|
+
if (!config || runtimes.size === 0) {
|
|
236
|
+
await activate(ctx);
|
|
237
|
+
}
|
|
206
238
|
if (!config || runtimes.size === 0) {
|
|
207
|
-
ctx.ui.notify(
|
|
239
|
+
ctx.ui.notify(
|
|
240
|
+
`🔑 keyrouter: not active — no ~/.pi/keyrouter.json found ` +
|
|
241
|
+
`(expected at ${configPath()}). Config is user-level only, never project-scoped.`,
|
|
242
|
+
"warning",
|
|
243
|
+
);
|
|
208
244
|
return;
|
|
209
245
|
}
|
|
210
246
|
const lines: string[] = [`🔑 keyrouter: active`];
|
|
@@ -226,6 +262,7 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
226
262
|
if (sub === "reload") {
|
|
227
263
|
config = loadConfig(ctx.cwd);
|
|
228
264
|
runtimes.clear();
|
|
265
|
+
activationNotified = false;
|
|
229
266
|
ctx.ui.notify(
|
|
230
267
|
`🔑 keyrouter: reloaded (${config.providers.length} provider(s))`,
|
|
231
268
|
"info",
|
package/package.json
CHANGED