takomi 2.1.1 → 2.1.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 (50) hide show
  1. package/.pi/README.md +124 -124
  2. package/.pi/agents/architect.md +15 -15
  3. package/.pi/agents/coder.md +14 -14
  4. package/.pi/agents/designer.md +17 -17
  5. package/.pi/agents/orchestrator.md +22 -22
  6. package/.pi/agents/reviewer.md +16 -16
  7. package/.pi/extensions/oauth-router/README.md +125 -125
  8. package/.pi/extensions/oauth-router/commands.ts +380 -380
  9. package/.pi/extensions/oauth-router/config.ts +200 -200
  10. package/.pi/extensions/oauth-router/index.ts +41 -41
  11. package/.pi/extensions/oauth-router/oauth-flow.ts +154 -154
  12. package/.pi/extensions/oauth-router/oauth-store.ts +121 -121
  13. package/.pi/extensions/oauth-router/package.json +14 -14
  14. package/.pi/extensions/oauth-router/policies.ts +27 -27
  15. package/.pi/extensions/oauth-router/provider.ts +492 -492
  16. package/.pi/extensions/oauth-router/scripts/vibe-verify.py +98 -98
  17. package/.pi/extensions/oauth-router/state.ts +174 -174
  18. package/.pi/extensions/oauth-router/types.ts +153 -153
  19. package/.pi/extensions/takomi-runtime/command-text.ts +130 -130
  20. package/.pi/extensions/takomi-runtime/commands.ts +179 -179
  21. package/.pi/extensions/takomi-runtime/context-panel.ts +282 -282
  22. package/.pi/extensions/takomi-runtime/index.ts +1288 -1288
  23. package/.pi/extensions/takomi-runtime/profile.ts +114 -114
  24. package/.pi/extensions/takomi-runtime/routing-policy.ts +105 -105
  25. package/.pi/extensions/takomi-runtime/shared.ts +492 -492
  26. package/.pi/extensions/takomi-runtime/subagent-controller.ts +364 -364
  27. package/.pi/extensions/takomi-runtime/subagent-render.ts +501 -501
  28. package/.pi/extensions/takomi-runtime/subagent-types.ts +83 -83
  29. package/.pi/extensions/takomi-runtime/ui.ts +133 -133
  30. package/.pi/extensions/takomi-subagents/agent-aliases.ts +18 -18
  31. package/.pi/extensions/takomi-subagents/agents.ts +113 -113
  32. package/.pi/extensions/takomi-subagents/delegation-plan.ts +95 -95
  33. package/.pi/extensions/takomi-subagents/dispatch-helpers.ts +26 -26
  34. package/.pi/extensions/takomi-subagents/dispatch.ts +215 -215
  35. package/.pi/extensions/takomi-subagents/index.ts +75 -75
  36. package/.pi/extensions/takomi-subagents/live-updates.ts +83 -83
  37. package/.pi/extensions/takomi-subagents/native-render.ts +174 -174
  38. package/.pi/extensions/takomi-subagents/tool-runner.ts +209 -209
  39. package/.pi/themes/takomi-noir.json +81 -81
  40. package/package.json +59 -59
  41. package/src/cli.js +2 -1
  42. package/src/doctor.js +87 -84
  43. package/src/pi-harness.js +355 -351
  44. package/src/pi-installer.js +193 -171
  45. package/src/pi-takomi-core/index.ts +4 -4
  46. package/src/pi-takomi-core/orchestration.ts +402 -402
  47. package/src/pi-takomi-core/routing.ts +93 -93
  48. package/src/pi-takomi-core/types.ts +173 -173
  49. package/src/pi-takomi-core/workflows.ts +299 -299
  50. package/src/skills-installer.js +101 -101
@@ -1,380 +1,380 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
- import { createAccountFromUpstream } from "./oauth-flow.ts";
3
- import type { RouterStatusRow, RoutingPolicyName } from "./types.ts";
4
- import { RouterRuntime } from "./provider.ts";
5
-
6
- function formatWhen(timestamp?: number): string {
7
- if (!timestamp) return "never";
8
- return new Date(timestamp).toLocaleString();
9
- }
10
-
11
- function formatRelative(target?: number): string {
12
- if (!target) return "-";
13
- const delta = target - Date.now();
14
- if (delta <= 0) return "expired";
15
- const seconds = Math.ceil(delta / 1000);
16
- if (seconds < 60) return `${seconds}s`;
17
- const minutes = Math.ceil(seconds / 60);
18
- if (minutes < 60) return `${minutes}m`;
19
- const hours = Math.ceil(minutes / 60);
20
- return `${hours}h`;
21
- }
22
-
23
- function formatAgo(timestamp?: number): string {
24
- if (!timestamp) return "never";
25
- const delta = Date.now() - timestamp;
26
- if (delta < 60_000) return `${Math.max(1, Math.ceil(delta / 1000))}s ago`;
27
- if (delta < 3_600_000) return `${Math.ceil(delta / 60_000)}m ago`;
28
- if (delta < 86_400_000) return `${Math.ceil(delta / 3_600_000)}h ago`;
29
- return `${Math.ceil(delta / 86_400_000)}d ago`;
30
- }
31
-
32
- function formatLastUsedSummary(timestamp?: number): string {
33
- if (!timestamp) return "never";
34
- return `${formatWhen(timestamp)} (${formatAgo(timestamp)})`;
35
- }
36
-
37
- function isHealthy(row: RouterStatusRow): boolean {
38
- if (!row.enabled) return false;
39
- if (row.authHealth !== "ok") return false;
40
- if (row.cooldownUntil && row.cooldownUntil > Date.now()) return false;
41
- if (row.penaltyUntil && row.penaltyUntil > Date.now()) return false;
42
- return true;
43
- }
44
-
45
- export function formatStatusReport(runtime: RouterRuntime): string {
46
- const config = runtime.getConfig();
47
- const rows = runtime.getStatusRows();
48
- const accountsById = new Map(runtime.listAccounts().map((account) => [account.id, account]));
49
- const healthyRows = rows.filter((row) => isHealthy(row));
50
- const healthy = healthyRows.length;
51
- const enabled = rows.filter((row) => row.enabled).length;
52
- const usableModels = config.models
53
- .filter((model) => {
54
- const routeFilter = new Set(model.route?.upstreamIds ?? []);
55
- return healthyRows.some((row) => {
56
- const upstream = config.upstreams.find((entry) => entry.id === row.upstream);
57
- if (!upstream?.modelIds.includes(model.id)) return false;
58
- if (routeFilter.size > 0 && !routeFilter.has(row.upstream)) return false;
59
- return true;
60
- });
61
- })
62
- .map((model) => model.id);
63
- const lastUsedRow = [...rows]
64
- .filter((row) => row.lastUsedAt)
65
- .sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0))[0];
66
- const lastUsedLine = lastUsedRow
67
- ? `${lastUsedRow.id} | ${lastUsedRow.label} | ${lastUsedRow.upstream} | ${formatLastUsedSummary(lastUsedRow.lastUsedAt)}`
68
- : "none yet";
69
- const upstreams = config.upstreams.map((upstream) => {
70
- const auth = upstream.authMode === "oauth" ? `oauth:${upstream.oauthProviderId}` : "api-key";
71
- const activeCount = rows.filter((row) => row.upstream === upstream.id && row.enabled).length;
72
- return `- ${upstream.id} | ${upstream.label} | ${auth} | ${upstream.api} | active=${activeCount} | models=${upstream.modelIds.join(", ")}`;
73
- });
74
-
75
- const accounts = rows.length
76
- ? [...rows]
77
- .sort((a, b) => {
78
- const aTime = a.lastUsedAt ?? 0;
79
- const bTime = b.lastUsedAt ?? 0;
80
- if (aTime !== bTime) return bTime - aTime;
81
- return a.label.localeCompare(b.label);
82
- })
83
- .map((row) => {
84
- const account = accountsById.get(row.id);
85
- const plan = typeof account?.meta?.planType === "string" ? account.meta.planType : "unknown";
86
- const health = isHealthy(row)
87
- ? "healthy"
88
- : row.authHealth !== "ok"
89
- ? `auth=${row.authHealth}`
90
- : row.cooldownUntil && row.cooldownUntil > Date.now()
91
- ? `cooldown ${formatRelative(row.cooldownUntil)}`
92
- : row.penaltyUntil && row.penaltyUntil > Date.now()
93
- ? `penalty ${formatRelative(row.penaltyUntil)}`
94
- : "inactive";
95
-
96
- return [
97
- `- ${row.id} | ${row.label} | plan=${plan}`,
98
- ` upstream=${row.upstream} enabled=${row.enabled} weight=${row.weight} state=${health}`,
99
- ` lastUsed=${formatLastUsedSummary(row.lastUsedAt)} | lastStatus=${row.lastStatus ?? "-"} | expires=${formatWhen(row.expires)}`,
100
- ` successes=${row.successCount} failures=${row.failures} 429s=${row.rateLimitCount} authFailures=${row.authFailureCount}`,
101
- ].join("\n");
102
- })
103
- : ["- No accounts configured yet. Use /router-login add."];
104
-
105
- return [
106
- "# oauth-router status",
107
- "",
108
- `Policy: ${runtime.getPolicy()}`,
109
- `Accounts: ${rows.length} total | ${enabled} enabled | ${healthy} healthy`,
110
- `Usable models now: ${usableModels.length > 0 ? usableModels.join(", ") : "none"}`,
111
- `Last used account: ${lastUsedLine}`,
112
- "",
113
- "## Upstreams",
114
- ...upstreams,
115
- "",
116
- "## Accounts",
117
- ...accounts,
118
- ].join("\n");
119
- }
120
-
121
- function emitReport(pi: ExtensionAPI, text: string) {
122
- pi.sendMessage({
123
- customType: "oauth-router",
124
- content: text,
125
- display: true,
126
- details: { source: "oauth-router" },
127
- });
128
- }
129
-
130
- async function pickUpstream(runtime: RouterRuntime, ctx: ExtensionCommandContext, requestedId?: string) {
131
- const upstreams = runtime.listUpstreams().filter((upstream) => upstream.enabled);
132
- if (upstreams.length === 0) throw new Error("No enabled upstreams are configured");
133
-
134
- if (requestedId) {
135
- const matched = upstreams.find((upstream) => upstream.id === requestedId);
136
- if (!matched) throw new Error(`Unknown upstream: ${requestedId}`);
137
- return matched;
138
- }
139
-
140
- if (!ctx.hasUI) return upstreams[0]!;
141
-
142
- const choice = await ctx.ui.select(
143
- "Choose an upstream",
144
- upstreams.map((upstream) => `${upstream.id} — ${upstream.label}`),
145
- );
146
- if (!choice) throw new Error("Cancelled by user");
147
- const id = choice.split(" — ")[0]?.trim();
148
- const selected = upstreams.find((upstream) => upstream.id === id);
149
- if (!selected) throw new Error(`Unknown upstream: ${id}`);
150
- return selected;
151
- }
152
-
153
- async function getLabel(ctx: ExtensionCommandContext, fallback: string, provided?: string) {
154
- if (provided && provided.trim()) return provided.trim();
155
- if (!ctx.hasUI) return fallback;
156
- const response = await ctx.ui.input("Account label:", fallback);
157
- return response?.trim() || fallback;
158
- }
159
-
160
- function normalizePolicy(input?: string): RoutingPolicyName | undefined {
161
- if (!input) return undefined;
162
- const value = input.trim().toLowerCase();
163
- if (value === "round-robin" || value === "rr") return "round-robin";
164
- if (value === "weighted-round-robin" || value === "wrr" || value === "weighted") return "weighted-round-robin";
165
- return undefined;
166
- }
167
-
168
- function parseArgs(args: string): string[] {
169
- return args
170
- .trim()
171
- .split(/\s+/)
172
- .map((value) => value.trim())
173
- .filter(Boolean);
174
- }
175
-
176
- function setFooterStatus(ctx: ExtensionCommandContext, runtime: RouterRuntime) {
177
- const rows = runtime.getStatusRows();
178
- const healthy = rows.filter((row) => isHealthy(row)).length;
179
- ctx.ui.setStatus("oauth-router", `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`);
180
- }
181
-
182
- function getAccountUsageLines(runtime: RouterRuntime): string[] {
183
- const rows = runtime.getStatusRows();
184
- if (rows.length === 0) {
185
- return ["No accounts configured yet. Use /router-login add."];
186
- }
187
-
188
- return rows.map((row) => `- ${row.id} | ${row.label} | enabled=${row.enabled} | weight=${row.weight}`);
189
- }
190
-
191
- function showCommandHint(pi: ExtensionAPI, ctx: ExtensionCommandContext, title: string, lines: string[]) {
192
- emitReport(pi, [`# ${title}`, "", ...lines].join("\n"));
193
- ctx.ui.notify(title, "info");
194
- }
195
-
196
- async function handleRouterLogin(pi: ExtensionAPI, runtime: RouterRuntime, args: string, ctx: ExtensionCommandContext) {
197
- const [command = "help", first, ...rest] = parseArgs(args);
198
-
199
- switch (command) {
200
- case "add": {
201
- const upstream = await pickUpstream(runtime, ctx, first);
202
- const label = await getLabel(ctx, `${upstream.label} ${runtime.listAccounts().length + 1}`, rest.join(" "));
203
- const account = await createAccountFromUpstream(upstream, label, ctx);
204
- runtime.addAccount(account);
205
- setFooterStatus(ctx, runtime);
206
- emitReport(pi, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}.`);
207
- ctx.ui.notify(`Added ${account.id}`, "info");
208
- return;
209
- }
210
- case "list": {
211
- const report = formatStatusReport(runtime);
212
- emitReport(pi, report);
213
- setFooterStatus(ctx, runtime);
214
- return;
215
- }
216
- case "remove": {
217
- if (!first) throw new Error("Usage: /router-login remove <id>");
218
- if (ctx.hasUI) {
219
- const ok = await ctx.ui.confirm("Remove account?", `Delete router account ${first}?`);
220
- if (!ok) throw new Error("Cancelled by user");
221
- }
222
- runtime.removeAccount(first);
223
- setFooterStatus(ctx, runtime);
224
- emitReport(pi, `Removed account ${first}.`);
225
- return;
226
- }
227
- case "refresh": {
228
- if (!first) throw new Error("Usage: /router-login refresh <id>");
229
- const refreshed = await runtime.refreshAccount(first);
230
- setFooterStatus(ctx, runtime);
231
- emitReport(pi, `Refreshed account ${refreshed.id} (${refreshed.label}).`);
232
- return;
233
- }
234
- case "help":
235
- default: {
236
- emitReport(
237
- pi,
238
- [
239
- "# oauth-router commands",
240
- "",
241
- "- /router-login add [upstreamId] [label]",
242
- "- /router-login list",
243
- "- /router-login remove <id>",
244
- "- /router-login refresh <id>",
245
- "- /router-status",
246
- "- /router-accounts",
247
- "- /router-enable <id>",
248
- "- /router-disable <id>",
249
- "- /router-policy <round-robin|weighted-round-robin>",
250
- "- /router-weight <id> <n>",
251
- ].join("\n"),
252
- );
253
- return;
254
- }
255
- }
256
- }
257
-
258
- export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime) {
259
- pi.registerCommand("router-login", {
260
- description: "Manage oauth-router accounts",
261
- handler: async (args, ctx) => handleRouterLogin(pi, runtime, args || "", ctx),
262
- });
263
-
264
- pi.registerCommand("router-status", {
265
- description: "Show oauth-router health and routing state",
266
- handler: async (_args, ctx) => {
267
- emitReport(pi, formatStatusReport(runtime));
268
- setFooterStatus(ctx, runtime);
269
- },
270
- });
271
-
272
- pi.registerCommand("router-accounts", {
273
- description: "Show oauth-router accounts",
274
- handler: async (_args, ctx) => {
275
- emitReport(pi, formatStatusReport(runtime));
276
- setFooterStatus(ctx, runtime);
277
- },
278
- });
279
-
280
- pi.registerCommand("router-enable", {
281
- description: "Enable an oauth-router account",
282
- handler: async (args, ctx) => {
283
- const [id] = parseArgs(args || "");
284
- if (!id) {
285
- showCommandHint(pi, ctx, "router-enable", ["Usage: /router-enable <id>", "", ...getAccountUsageLines(runtime)]);
286
- return;
287
- }
288
- if (!runtime.getAccount(id)) {
289
- showCommandHint(pi, ctx, "router-enable", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
290
- return;
291
- }
292
- runtime.setEnabled(id, true);
293
- setFooterStatus(ctx, runtime);
294
- emitReport(pi, `Enabled account ${id}.`);
295
- },
296
- });
297
-
298
- pi.registerCommand("router-disable", {
299
- description: "Disable an oauth-router account",
300
- handler: async (args, ctx) => {
301
- const [id] = parseArgs(args || "");
302
- if (!id) {
303
- showCommandHint(pi, ctx, "router-disable", ["Usage: /router-disable <id>", "", ...getAccountUsageLines(runtime)]);
304
- return;
305
- }
306
- if (!runtime.getAccount(id)) {
307
- showCommandHint(pi, ctx, "router-disable", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
308
- return;
309
- }
310
- runtime.setEnabled(id, false);
311
- setFooterStatus(ctx, runtime);
312
- emitReport(pi, `Disabled account ${id}.`);
313
- },
314
- });
315
-
316
- pi.registerCommand("router-policy", {
317
- description: "Set oauth-router routing policy",
318
- handler: async (args, ctx) => {
319
- const raw = (args || "").trim();
320
- const policy = normalizePolicy(raw);
321
- if (!raw) {
322
- showCommandHint(pi, ctx, "router-policy", [
323
- `Current policy: ${runtime.getPolicy()}`,
324
- "",
325
- "Usage: /router-policy <round-robin|weighted-round-robin>",
326
- "Aliases: rr, wrr, weighted",
327
- ]);
328
- return;
329
- }
330
- if (!policy) {
331
- showCommandHint(pi, ctx, "router-policy", [
332
- `Unrecognized policy: ${raw}`,
333
- "",
334
- `Current policy: ${runtime.getPolicy()}`,
335
- "Valid values: round-robin, weighted-round-robin",
336
- ]);
337
- return;
338
- }
339
- runtime.setPolicy(policy);
340
- setFooterStatus(ctx, runtime);
341
- emitReport(pi, `Routing policy set to ${policy}.`);
342
- },
343
- });
344
-
345
- pi.registerCommand("router-weight", {
346
- description: "Set oauth-router account weight for weighted round robin",
347
- handler: async (args, ctx) => {
348
- const [id, rawWeight] = parseArgs(args || "");
349
- const hasAccounts = runtime.getStatusRows().length > 0;
350
- if (!id && !rawWeight) {
351
- showCommandHint(pi, ctx, "router-weight", [
352
- "Usage: /router-weight <id> <n>",
353
- "",
354
- "Example: /router-weight acct_ab12cd34 3",
355
- "",
356
- ...getAccountUsageLines(runtime),
357
- ]);
358
- return;
359
- }
360
-
361
- const weight = Number(rawWeight);
362
- if (!id || !Number.isFinite(weight)) {
363
- showCommandHint(pi, ctx, "router-weight", [
364
- "Usage: /router-weight <id> <n>",
365
- hasAccounts ? "Provide both an account id and a numeric weight." : "Add an account first with /router-login add.",
366
- "",
367
- ...getAccountUsageLines(runtime),
368
- ]);
369
- return;
370
- }
371
- if (!runtime.getAccount(id)) {
372
- showCommandHint(pi, ctx, "router-weight", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
373
- return;
374
- }
375
- runtime.setWeight(id, weight);
376
- setFooterStatus(ctx, runtime);
377
- emitReport(pi, `Updated weight for ${id} to ${Math.max(1, Math.floor(weight))}.`);
378
- },
379
- });
380
- }
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import { createAccountFromUpstream } from "./oauth-flow.ts";
3
+ import type { RouterStatusRow, RoutingPolicyName } from "./types.ts";
4
+ import { RouterRuntime } from "./provider.ts";
5
+
6
+ function formatWhen(timestamp?: number): string {
7
+ if (!timestamp) return "never";
8
+ return new Date(timestamp).toLocaleString();
9
+ }
10
+
11
+ function formatRelative(target?: number): string {
12
+ if (!target) return "-";
13
+ const delta = target - Date.now();
14
+ if (delta <= 0) return "expired";
15
+ const seconds = Math.ceil(delta / 1000);
16
+ if (seconds < 60) return `${seconds}s`;
17
+ const minutes = Math.ceil(seconds / 60);
18
+ if (minutes < 60) return `${minutes}m`;
19
+ const hours = Math.ceil(minutes / 60);
20
+ return `${hours}h`;
21
+ }
22
+
23
+ function formatAgo(timestamp?: number): string {
24
+ if (!timestamp) return "never";
25
+ const delta = Date.now() - timestamp;
26
+ if (delta < 60_000) return `${Math.max(1, Math.ceil(delta / 1000))}s ago`;
27
+ if (delta < 3_600_000) return `${Math.ceil(delta / 60_000)}m ago`;
28
+ if (delta < 86_400_000) return `${Math.ceil(delta / 3_600_000)}h ago`;
29
+ return `${Math.ceil(delta / 86_400_000)}d ago`;
30
+ }
31
+
32
+ function formatLastUsedSummary(timestamp?: number): string {
33
+ if (!timestamp) return "never";
34
+ return `${formatWhen(timestamp)} (${formatAgo(timestamp)})`;
35
+ }
36
+
37
+ function isHealthy(row: RouterStatusRow): boolean {
38
+ if (!row.enabled) return false;
39
+ if (row.authHealth !== "ok") return false;
40
+ if (row.cooldownUntil && row.cooldownUntil > Date.now()) return false;
41
+ if (row.penaltyUntil && row.penaltyUntil > Date.now()) return false;
42
+ return true;
43
+ }
44
+
45
+ export function formatStatusReport(runtime: RouterRuntime): string {
46
+ const config = runtime.getConfig();
47
+ const rows = runtime.getStatusRows();
48
+ const accountsById = new Map(runtime.listAccounts().map((account) => [account.id, account]));
49
+ const healthyRows = rows.filter((row) => isHealthy(row));
50
+ const healthy = healthyRows.length;
51
+ const enabled = rows.filter((row) => row.enabled).length;
52
+ const usableModels = config.models
53
+ .filter((model) => {
54
+ const routeFilter = new Set(model.route?.upstreamIds ?? []);
55
+ return healthyRows.some((row) => {
56
+ const upstream = config.upstreams.find((entry) => entry.id === row.upstream);
57
+ if (!upstream?.modelIds.includes(model.id)) return false;
58
+ if (routeFilter.size > 0 && !routeFilter.has(row.upstream)) return false;
59
+ return true;
60
+ });
61
+ })
62
+ .map((model) => model.id);
63
+ const lastUsedRow = [...rows]
64
+ .filter((row) => row.lastUsedAt)
65
+ .sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0))[0];
66
+ const lastUsedLine = lastUsedRow
67
+ ? `${lastUsedRow.id} | ${lastUsedRow.label} | ${lastUsedRow.upstream} | ${formatLastUsedSummary(lastUsedRow.lastUsedAt)}`
68
+ : "none yet";
69
+ const upstreams = config.upstreams.map((upstream) => {
70
+ const auth = upstream.authMode === "oauth" ? `oauth:${upstream.oauthProviderId}` : "api-key";
71
+ const activeCount = rows.filter((row) => row.upstream === upstream.id && row.enabled).length;
72
+ return `- ${upstream.id} | ${upstream.label} | ${auth} | ${upstream.api} | active=${activeCount} | models=${upstream.modelIds.join(", ")}`;
73
+ });
74
+
75
+ const accounts = rows.length
76
+ ? [...rows]
77
+ .sort((a, b) => {
78
+ const aTime = a.lastUsedAt ?? 0;
79
+ const bTime = b.lastUsedAt ?? 0;
80
+ if (aTime !== bTime) return bTime - aTime;
81
+ return a.label.localeCompare(b.label);
82
+ })
83
+ .map((row) => {
84
+ const account = accountsById.get(row.id);
85
+ const plan = typeof account?.meta?.planType === "string" ? account.meta.planType : "unknown";
86
+ const health = isHealthy(row)
87
+ ? "healthy"
88
+ : row.authHealth !== "ok"
89
+ ? `auth=${row.authHealth}`
90
+ : row.cooldownUntil && row.cooldownUntil > Date.now()
91
+ ? `cooldown ${formatRelative(row.cooldownUntil)}`
92
+ : row.penaltyUntil && row.penaltyUntil > Date.now()
93
+ ? `penalty ${formatRelative(row.penaltyUntil)}`
94
+ : "inactive";
95
+
96
+ return [
97
+ `- ${row.id} | ${row.label} | plan=${plan}`,
98
+ ` upstream=${row.upstream} enabled=${row.enabled} weight=${row.weight} state=${health}`,
99
+ ` lastUsed=${formatLastUsedSummary(row.lastUsedAt)} | lastStatus=${row.lastStatus ?? "-"} | expires=${formatWhen(row.expires)}`,
100
+ ` successes=${row.successCount} failures=${row.failures} 429s=${row.rateLimitCount} authFailures=${row.authFailureCount}`,
101
+ ].join("\n");
102
+ })
103
+ : ["- No accounts configured yet. Use /router-login add."];
104
+
105
+ return [
106
+ "# oauth-router status",
107
+ "",
108
+ `Policy: ${runtime.getPolicy()}`,
109
+ `Accounts: ${rows.length} total | ${enabled} enabled | ${healthy} healthy`,
110
+ `Usable models now: ${usableModels.length > 0 ? usableModels.join(", ") : "none"}`,
111
+ `Last used account: ${lastUsedLine}`,
112
+ "",
113
+ "## Upstreams",
114
+ ...upstreams,
115
+ "",
116
+ "## Accounts",
117
+ ...accounts,
118
+ ].join("\n");
119
+ }
120
+
121
+ function emitReport(pi: ExtensionAPI, text: string) {
122
+ pi.sendMessage({
123
+ customType: "oauth-router",
124
+ content: text,
125
+ display: true,
126
+ details: { source: "oauth-router" },
127
+ });
128
+ }
129
+
130
+ async function pickUpstream(runtime: RouterRuntime, ctx: ExtensionCommandContext, requestedId?: string) {
131
+ const upstreams = runtime.listUpstreams().filter((upstream) => upstream.enabled);
132
+ if (upstreams.length === 0) throw new Error("No enabled upstreams are configured");
133
+
134
+ if (requestedId) {
135
+ const matched = upstreams.find((upstream) => upstream.id === requestedId);
136
+ if (!matched) throw new Error(`Unknown upstream: ${requestedId}`);
137
+ return matched;
138
+ }
139
+
140
+ if (!ctx.hasUI) return upstreams[0]!;
141
+
142
+ const choice = await ctx.ui.select(
143
+ "Choose an upstream",
144
+ upstreams.map((upstream) => `${upstream.id} — ${upstream.label}`),
145
+ );
146
+ if (!choice) throw new Error("Cancelled by user");
147
+ const id = choice.split(" — ")[0]?.trim();
148
+ const selected = upstreams.find((upstream) => upstream.id === id);
149
+ if (!selected) throw new Error(`Unknown upstream: ${id}`);
150
+ return selected;
151
+ }
152
+
153
+ async function getLabel(ctx: ExtensionCommandContext, fallback: string, provided?: string) {
154
+ if (provided && provided.trim()) return provided.trim();
155
+ if (!ctx.hasUI) return fallback;
156
+ const response = await ctx.ui.input("Account label:", fallback);
157
+ return response?.trim() || fallback;
158
+ }
159
+
160
+ function normalizePolicy(input?: string): RoutingPolicyName | undefined {
161
+ if (!input) return undefined;
162
+ const value = input.trim().toLowerCase();
163
+ if (value === "round-robin" || value === "rr") return "round-robin";
164
+ if (value === "weighted-round-robin" || value === "wrr" || value === "weighted") return "weighted-round-robin";
165
+ return undefined;
166
+ }
167
+
168
+ function parseArgs(args: string): string[] {
169
+ return args
170
+ .trim()
171
+ .split(/\s+/)
172
+ .map((value) => value.trim())
173
+ .filter(Boolean);
174
+ }
175
+
176
+ function setFooterStatus(ctx: ExtensionCommandContext, runtime: RouterRuntime) {
177
+ const rows = runtime.getStatusRows();
178
+ const healthy = rows.filter((row) => isHealthy(row)).length;
179
+ ctx.ui.setStatus("oauth-router", `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`);
180
+ }
181
+
182
+ function getAccountUsageLines(runtime: RouterRuntime): string[] {
183
+ const rows = runtime.getStatusRows();
184
+ if (rows.length === 0) {
185
+ return ["No accounts configured yet. Use /router-login add."];
186
+ }
187
+
188
+ return rows.map((row) => `- ${row.id} | ${row.label} | enabled=${row.enabled} | weight=${row.weight}`);
189
+ }
190
+
191
+ function showCommandHint(pi: ExtensionAPI, ctx: ExtensionCommandContext, title: string, lines: string[]) {
192
+ emitReport(pi, [`# ${title}`, "", ...lines].join("\n"));
193
+ ctx.ui.notify(title, "info");
194
+ }
195
+
196
+ async function handleRouterLogin(pi: ExtensionAPI, runtime: RouterRuntime, args: string, ctx: ExtensionCommandContext) {
197
+ const [command = "help", first, ...rest] = parseArgs(args);
198
+
199
+ switch (command) {
200
+ case "add": {
201
+ const upstream = await pickUpstream(runtime, ctx, first);
202
+ const label = await getLabel(ctx, `${upstream.label} ${runtime.listAccounts().length + 1}`, rest.join(" "));
203
+ const account = await createAccountFromUpstream(upstream, label, ctx);
204
+ runtime.addAccount(account);
205
+ setFooterStatus(ctx, runtime);
206
+ emitReport(pi, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}.`);
207
+ ctx.ui.notify(`Added ${account.id}`, "info");
208
+ return;
209
+ }
210
+ case "list": {
211
+ const report = formatStatusReport(runtime);
212
+ emitReport(pi, report);
213
+ setFooterStatus(ctx, runtime);
214
+ return;
215
+ }
216
+ case "remove": {
217
+ if (!first) throw new Error("Usage: /router-login remove <id>");
218
+ if (ctx.hasUI) {
219
+ const ok = await ctx.ui.confirm("Remove account?", `Delete router account ${first}?`);
220
+ if (!ok) throw new Error("Cancelled by user");
221
+ }
222
+ runtime.removeAccount(first);
223
+ setFooterStatus(ctx, runtime);
224
+ emitReport(pi, `Removed account ${first}.`);
225
+ return;
226
+ }
227
+ case "refresh": {
228
+ if (!first) throw new Error("Usage: /router-login refresh <id>");
229
+ const refreshed = await runtime.refreshAccount(first);
230
+ setFooterStatus(ctx, runtime);
231
+ emitReport(pi, `Refreshed account ${refreshed.id} (${refreshed.label}).`);
232
+ return;
233
+ }
234
+ case "help":
235
+ default: {
236
+ emitReport(
237
+ pi,
238
+ [
239
+ "# oauth-router commands",
240
+ "",
241
+ "- /router-login add [upstreamId] [label]",
242
+ "- /router-login list",
243
+ "- /router-login remove <id>",
244
+ "- /router-login refresh <id>",
245
+ "- /router-status",
246
+ "- /router-accounts",
247
+ "- /router-enable <id>",
248
+ "- /router-disable <id>",
249
+ "- /router-policy <round-robin|weighted-round-robin>",
250
+ "- /router-weight <id> <n>",
251
+ ].join("\n"),
252
+ );
253
+ return;
254
+ }
255
+ }
256
+ }
257
+
258
+ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime) {
259
+ pi.registerCommand("router-login", {
260
+ description: "Manage oauth-router accounts",
261
+ handler: async (args, ctx) => handleRouterLogin(pi, runtime, args || "", ctx),
262
+ });
263
+
264
+ pi.registerCommand("router-status", {
265
+ description: "Show oauth-router health and routing state",
266
+ handler: async (_args, ctx) => {
267
+ emitReport(pi, formatStatusReport(runtime));
268
+ setFooterStatus(ctx, runtime);
269
+ },
270
+ });
271
+
272
+ pi.registerCommand("router-accounts", {
273
+ description: "Show oauth-router accounts",
274
+ handler: async (_args, ctx) => {
275
+ emitReport(pi, formatStatusReport(runtime));
276
+ setFooterStatus(ctx, runtime);
277
+ },
278
+ });
279
+
280
+ pi.registerCommand("router-enable", {
281
+ description: "Enable an oauth-router account",
282
+ handler: async (args, ctx) => {
283
+ const [id] = parseArgs(args || "");
284
+ if (!id) {
285
+ showCommandHint(pi, ctx, "router-enable", ["Usage: /router-enable <id>", "", ...getAccountUsageLines(runtime)]);
286
+ return;
287
+ }
288
+ if (!runtime.getAccount(id)) {
289
+ showCommandHint(pi, ctx, "router-enable", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
290
+ return;
291
+ }
292
+ runtime.setEnabled(id, true);
293
+ setFooterStatus(ctx, runtime);
294
+ emitReport(pi, `Enabled account ${id}.`);
295
+ },
296
+ });
297
+
298
+ pi.registerCommand("router-disable", {
299
+ description: "Disable an oauth-router account",
300
+ handler: async (args, ctx) => {
301
+ const [id] = parseArgs(args || "");
302
+ if (!id) {
303
+ showCommandHint(pi, ctx, "router-disable", ["Usage: /router-disable <id>", "", ...getAccountUsageLines(runtime)]);
304
+ return;
305
+ }
306
+ if (!runtime.getAccount(id)) {
307
+ showCommandHint(pi, ctx, "router-disable", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
308
+ return;
309
+ }
310
+ runtime.setEnabled(id, false);
311
+ setFooterStatus(ctx, runtime);
312
+ emitReport(pi, `Disabled account ${id}.`);
313
+ },
314
+ });
315
+
316
+ pi.registerCommand("router-policy", {
317
+ description: "Set oauth-router routing policy",
318
+ handler: async (args, ctx) => {
319
+ const raw = (args || "").trim();
320
+ const policy = normalizePolicy(raw);
321
+ if (!raw) {
322
+ showCommandHint(pi, ctx, "router-policy", [
323
+ `Current policy: ${runtime.getPolicy()}`,
324
+ "",
325
+ "Usage: /router-policy <round-robin|weighted-round-robin>",
326
+ "Aliases: rr, wrr, weighted",
327
+ ]);
328
+ return;
329
+ }
330
+ if (!policy) {
331
+ showCommandHint(pi, ctx, "router-policy", [
332
+ `Unrecognized policy: ${raw}`,
333
+ "",
334
+ `Current policy: ${runtime.getPolicy()}`,
335
+ "Valid values: round-robin, weighted-round-robin",
336
+ ]);
337
+ return;
338
+ }
339
+ runtime.setPolicy(policy);
340
+ setFooterStatus(ctx, runtime);
341
+ emitReport(pi, `Routing policy set to ${policy}.`);
342
+ },
343
+ });
344
+
345
+ pi.registerCommand("router-weight", {
346
+ description: "Set oauth-router account weight for weighted round robin",
347
+ handler: async (args, ctx) => {
348
+ const [id, rawWeight] = parseArgs(args || "");
349
+ const hasAccounts = runtime.getStatusRows().length > 0;
350
+ if (!id && !rawWeight) {
351
+ showCommandHint(pi, ctx, "router-weight", [
352
+ "Usage: /router-weight <id> <n>",
353
+ "",
354
+ "Example: /router-weight acct_ab12cd34 3",
355
+ "",
356
+ ...getAccountUsageLines(runtime),
357
+ ]);
358
+ return;
359
+ }
360
+
361
+ const weight = Number(rawWeight);
362
+ if (!id || !Number.isFinite(weight)) {
363
+ showCommandHint(pi, ctx, "router-weight", [
364
+ "Usage: /router-weight <id> <n>",
365
+ hasAccounts ? "Provide both an account id and a numeric weight." : "Add an account first with /router-login add.",
366
+ "",
367
+ ...getAccountUsageLines(runtime),
368
+ ]);
369
+ return;
370
+ }
371
+ if (!runtime.getAccount(id)) {
372
+ showCommandHint(pi, ctx, "router-weight", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
373
+ return;
374
+ }
375
+ runtime.setWeight(id, weight);
376
+ setFooterStatus(ctx, runtime);
377
+ emitReport(pi, `Updated weight for ${id} to ${Math.max(1, Math.floor(weight))}.`);
378
+ },
379
+ });
380
+ }