takomi 2.0.6 → 2.1.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.
Files changed (76) hide show
  1. package/.pi/README.md +124 -0
  2. package/.pi/agents/architect.md +16 -0
  3. package/.pi/agents/coder.md +15 -0
  4. package/.pi/agents/designer.md +18 -0
  5. package/.pi/agents/orchestrator.md +23 -0
  6. package/.pi/agents/reviewer.md +17 -0
  7. package/.pi/extensions/oauth-router/README.md +125 -0
  8. package/.pi/extensions/oauth-router/commands.ts +380 -0
  9. package/.pi/extensions/oauth-router/config.ts +200 -0
  10. package/.pi/extensions/oauth-router/index.ts +41 -0
  11. package/.pi/extensions/oauth-router/oauth-flow.ts +154 -0
  12. package/.pi/extensions/oauth-router/oauth-store.ts +121 -0
  13. package/.pi/extensions/oauth-router/package.json +14 -0
  14. package/.pi/extensions/oauth-router/policies.ts +27 -0
  15. package/.pi/extensions/oauth-router/provider.ts +492 -0
  16. package/.pi/extensions/oauth-router/scripts/vibe-verify.py +98 -0
  17. package/.pi/extensions/oauth-router/state.ts +174 -0
  18. package/.pi/extensions/oauth-router/types.ts +153 -0
  19. package/.pi/extensions/takomi-runtime/command-text.ts +130 -0
  20. package/.pi/extensions/takomi-runtime/commands.ts +179 -0
  21. package/.pi/extensions/takomi-runtime/context-panel.ts +282 -0
  22. package/.pi/extensions/takomi-runtime/index.ts +1288 -0
  23. package/.pi/extensions/takomi-runtime/profile.ts +114 -0
  24. package/.pi/extensions/takomi-runtime/routing-policy.ts +105 -0
  25. package/.pi/extensions/takomi-runtime/shared.ts +492 -0
  26. package/.pi/extensions/takomi-runtime/subagent-controller.ts +364 -0
  27. package/.pi/extensions/takomi-runtime/subagent-render.ts +501 -0
  28. package/.pi/extensions/takomi-runtime/subagent-types.ts +83 -0
  29. package/.pi/extensions/takomi-runtime/ui.ts +133 -0
  30. package/.pi/extensions/takomi-subagents/agent-aliases.ts +18 -0
  31. package/.pi/extensions/takomi-subagents/agents.ts +113 -0
  32. package/.pi/extensions/takomi-subagents/delegation-plan.ts +95 -0
  33. package/.pi/extensions/takomi-subagents/dispatch-helpers.ts +26 -0
  34. package/.pi/extensions/takomi-subagents/dispatch.ts +215 -0
  35. package/.pi/extensions/takomi-subagents/index.ts +75 -0
  36. package/.pi/extensions/takomi-subagents/live-updates.ts +83 -0
  37. package/.pi/extensions/takomi-subagents/native-render.ts +174 -0
  38. package/.pi/extensions/takomi-subagents/tool-runner.ts +209 -0
  39. package/.pi/prompts/build-prompt.md +199 -0
  40. package/.pi/prompts/design-prompt.md +134 -0
  41. package/.pi/prompts/genesis-prompt.md +133 -0
  42. package/.pi/prompts/orch-prompt.md +144 -0
  43. package/.pi/prompts/prime-prompt.md +80 -0
  44. package/.pi/prompts/takomi-prompt.md +96 -0
  45. package/.pi/prompts/vibe-primeAgent.md +97 -0
  46. package/.pi/prompts/vibe-spawnTask.md +133 -0
  47. package/.pi/prompts/vibe-syncDocs.md +100 -0
  48. package/.pi/themes/takomi-noir.json +81 -0
  49. package/README.md +61 -27
  50. package/assets/.agent/skills/21st-dev-components/21st-handoff.md +146 -0
  51. package/assets/.agent/skills/21st-dev-components/SKILL.md +198 -0
  52. package/assets/.agent/skills/21st-dev-components/references/categories.md +91 -0
  53. package/assets/.agent/skills/21st-dev-components/references/manual-handoff-template.md +79 -0
  54. package/assets/.agent/skills/21st-dev-components/references/section-detection-rubric.md +59 -0
  55. package/assets/.agent/skills/21st-dev-components/scripts/_shared.mjs +304 -0
  56. package/assets/.agent/skills/21st-dev-components/scripts/build-manual-handoff-template.mjs +115 -0
  57. package/assets/.agent/skills/21st-dev-components/scripts/fetch-21st-source.mjs +65 -0
  58. package/assets/.agent/skills/21st-dev-components/scripts/resolve-21st-component.mjs +115 -0
  59. package/assets/.agent/skills/pr-comment-fix/SKILL.md +182 -0
  60. package/assets/.agent/skills/takomi/SKILL.md +59 -59
  61. package/package.json +58 -41
  62. package/src/cli.js +165 -15
  63. package/src/doctor.js +84 -0
  64. package/src/harness.js +13 -0
  65. package/src/pi-harness.js +351 -0
  66. package/src/pi-installer.js +171 -0
  67. package/src/pi-takomi-core/index.ts +4 -0
  68. package/src/pi-takomi-core/orchestration.ts +402 -0
  69. package/src/pi-takomi-core/routing.ts +93 -0
  70. package/src/pi-takomi-core/types.ts +173 -0
  71. package/src/pi-takomi-core/workflows.ts +299 -0
  72. package/src/skills-installer.js +101 -0
  73. package/src/utils.js +479 -447
  74. package/assets/.agent/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-311.pyc +0 -0
  75. package/assets/.agent/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
  76. package/assets/.agent/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-311.pyc +0 -0
@@ -0,0 +1,492 @@
1
+ import {
2
+ createAssistantMessageEventStream,
3
+ type Api,
4
+ type AssistantMessage,
5
+ type AssistantMessageEvent,
6
+ type AssistantMessageEventStream,
7
+ type Context,
8
+ type Model,
9
+ type SimpleStreamOptions,
10
+ streamSimpleOpenAICodexResponses,
11
+ streamSimpleOpenAICompletions,
12
+ streamSimpleOpenAIResponses,
13
+ } from "@mariozechner/pi-ai";
14
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
+ import { loadRouterConfig } from "./config.ts";
16
+ import { refreshAccountCredentials, getApiKeyForAccount } from "./oauth-flow.ts";
17
+ import { RouterAccountStore } from "./oauth-store.ts";
18
+ import { chooseEligibleAccount } from "./policies.ts";
19
+ import { RouterStateStore } from "./state.ts";
20
+ import type {
21
+ DelegateSelection,
22
+ EligibleAccount,
23
+ FailureClassification,
24
+ RouterConfig,
25
+ RouterErrorMessageInput,
26
+ RouterModelConfig,
27
+ RouterStatusRow,
28
+ RouterUpstreamConfig,
29
+ RoutingPolicyName,
30
+ StoredRouterAccount,
31
+ } from "./types.ts";
32
+
33
+ function now() {
34
+ return Date.now();
35
+ }
36
+
37
+ function parseRetryAfterMs(value: string | undefined): number | undefined {
38
+ if (!value) return undefined;
39
+ const seconds = Number(value);
40
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
41
+ const timestamp = Date.parse(value);
42
+ if (Number.isFinite(timestamp)) return Math.max(0, timestamp - now());
43
+ return undefined;
44
+ }
45
+
46
+ function isMeaningfulEvent(event: AssistantMessageEvent): boolean {
47
+ switch (event.type) {
48
+ case "text_delta":
49
+ case "thinking_delta":
50
+ case "toolcall_delta":
51
+ return event.delta.length > 0;
52
+ case "text_end":
53
+ case "thinking_end":
54
+ return event.content.length > 0;
55
+ case "toolcall_end":
56
+ return true;
57
+ case "done":
58
+ return event.message.content.length > 0;
59
+ default:
60
+ return false;
61
+ }
62
+ }
63
+
64
+ function createErrorMessage({ model, message, stopReason = "error" }: RouterErrorMessageInput): AssistantMessage {
65
+ return {
66
+ role: "assistant",
67
+ content: [],
68
+ api: model.api,
69
+ provider: model.provider,
70
+ model: model.id,
71
+ usage: {
72
+ input: 0,
73
+ output: 0,
74
+ cacheRead: 0,
75
+ cacheWrite: 0,
76
+ totalTokens: 0,
77
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
78
+ },
79
+ stopReason,
80
+ errorMessage: message,
81
+ timestamp: Date.now(),
82
+ };
83
+ }
84
+
85
+ function classifyFailure(
86
+ status: number | undefined,
87
+ headers: Record<string, string>,
88
+ message: string,
89
+ config: RouterConfig,
90
+ ): FailureClassification {
91
+ const retryAfterMs = parseRetryAfterMs(headers["retry-after"]) ?? config.rateLimitCooldownMs;
92
+ const lower = message.toLowerCase();
93
+
94
+ if (status === 429 || lower.includes("rate limit") || lower.includes("too many requests")) {
95
+ return { kind: "rate-limit", status: status ?? 429, retryAfterMs, message };
96
+ }
97
+
98
+ if (status === 401 || status === 403 || lower.includes("unauthorized") || lower.includes("forbidden")) {
99
+ return { kind: "auth", status: status ?? 401, message };
100
+ }
101
+
102
+ if (
103
+ (status !== undefined && status >= 500) ||
104
+ lower.includes("network") ||
105
+ lower.includes("fetch failed") ||
106
+ lower.includes("connection") ||
107
+ lower.includes("timeout")
108
+ ) {
109
+ return { kind: "transient", status: status ?? 500, message };
110
+ }
111
+
112
+ return { kind: "fatal", status, message };
113
+ }
114
+
115
+ function delegateStream(
116
+ upstream: RouterUpstreamConfig,
117
+ model: Model<Api>,
118
+ context: Context,
119
+ options: SimpleStreamOptions | undefined,
120
+ ): AssistantMessageEventStream {
121
+ switch (upstream.api) {
122
+ case "openai-completions":
123
+ return streamSimpleOpenAICompletions(model as Model<"openai-completions">, context, options);
124
+ case "openai-responses":
125
+ return streamSimpleOpenAIResponses(model as Model<"openai-responses">, context, options);
126
+ case "openai-codex-responses":
127
+ return streamSimpleOpenAICodexResponses(model as Model<"openai-codex-responses">, context, options);
128
+ default:
129
+ throw new Error(`Unsupported upstream api: ${upstream.api}`);
130
+ }
131
+ }
132
+
133
+ export class RouterRuntime {
134
+ private config: RouterConfig;
135
+ private accounts: RouterAccountStore;
136
+ private state: RouterStateStore;
137
+
138
+ constructor() {
139
+ this.config = loadRouterConfig();
140
+ this.accounts = new RouterAccountStore();
141
+ this.state = new RouterStateStore(this.config.policy);
142
+ this.state.pruneAccountIds(this.accounts.list().map((account) => account.id));
143
+ }
144
+
145
+ reloadConfig() {
146
+ this.config = loadRouterConfig();
147
+ this.accounts.reload();
148
+ this.state.pruneAccountIds(this.accounts.list().map((account) => account.id));
149
+ }
150
+
151
+ getConfig(): RouterConfig {
152
+ this.reloadConfig();
153
+ return this.config;
154
+ }
155
+
156
+ listUpstreams(): RouterUpstreamConfig[] {
157
+ return this.getConfig().upstreams;
158
+ }
159
+
160
+ listAccounts(): StoredRouterAccount[] {
161
+ this.reloadConfig();
162
+ return this.accounts.list();
163
+ }
164
+
165
+ getAccount(id: string): StoredRouterAccount | undefined {
166
+ this.reloadConfig();
167
+ return this.accounts.get(id);
168
+ }
169
+
170
+ addAccount(account: StoredRouterAccount) {
171
+ this.accounts.add(account);
172
+ this.state.ensureAccount(account.id);
173
+ }
174
+
175
+ updateAccount(account: StoredRouterAccount) {
176
+ this.accounts.update(account);
177
+ this.state.ensureAccount(account.id);
178
+ }
179
+
180
+ removeAccount(id: string) {
181
+ this.accounts.remove(id);
182
+ this.state.pruneAccountIds(this.accounts.list().map((account) => account.id));
183
+ }
184
+
185
+ setEnabled(id: string, enabled: boolean) {
186
+ this.accounts.setEnabled(id, enabled);
187
+ if (enabled) this.state.clearHealth(id);
188
+ }
189
+
190
+ setWeight(id: string, weight: number) {
191
+ this.accounts.setWeight(id, weight);
192
+ }
193
+
194
+ setPolicy(policy: RoutingPolicyName) {
195
+ this.state.setPolicy(policy);
196
+ }
197
+
198
+ getPolicy(): RoutingPolicyName {
199
+ return this.state.getPolicy(this.config.policy);
200
+ }
201
+
202
+ async refreshAccount(id: string): Promise<StoredRouterAccount> {
203
+ const account = this.accounts.get(id);
204
+ if (!account) throw new Error(`Unknown account: ${id}`);
205
+ const refreshed = await refreshAccountCredentials(account);
206
+ this.accounts.update(refreshed);
207
+ this.state.clearHealth(id);
208
+ return refreshed;
209
+ }
210
+
211
+ getProviderModels() {
212
+ return this.getConfig().models.map((model) => ({
213
+ id: model.id,
214
+ name: model.name,
215
+ reasoning: model.reasoning,
216
+ input: model.input,
217
+ cost: model.cost,
218
+ contextWindow: model.contextWindow,
219
+ maxTokens: model.maxTokens,
220
+ compat: model.compat,
221
+ }));
222
+ }
223
+
224
+ getStatusRows(): RouterStatusRow[] {
225
+ this.reloadConfig();
226
+ return this.accounts.list().map((account) => {
227
+ const state = this.state.ensureAccount(account.id);
228
+ return {
229
+ id: account.id,
230
+ label: account.label,
231
+ upstream: account.upstreamId,
232
+ provider: account.provider,
233
+ enabled: account.enabled,
234
+ weight: account.weight,
235
+ authHealth: state.authHealth,
236
+ cooldownUntil: state.cooldownUntil,
237
+ penaltyUntil: state.penaltyUntil,
238
+ lastUsedAt: state.lastUsedAt,
239
+ lastStatus: state.lastStatus,
240
+ failures: state.failures,
241
+ rateLimitCount: state.rateLimitCount,
242
+ authFailureCount: state.authFailureCount,
243
+ successCount: state.successCount,
244
+ expires: account.expires,
245
+ };
246
+ });
247
+ }
248
+
249
+ private getModelConfig(modelId: string): RouterModelConfig {
250
+ const model = this.config.models.find((entry) => entry.id === modelId);
251
+ if (!model) throw new Error(`Model is not configured for oauth-router: ${modelId}`);
252
+ return model;
253
+ }
254
+
255
+ private getUpstream(upstreamId: string): RouterUpstreamConfig | undefined {
256
+ return this.config.upstreams.find((upstream) => upstream.id === upstreamId);
257
+ }
258
+
259
+ private getEligibleAccounts(modelId: string, excludedIds: Set<string>): EligibleAccount[] {
260
+ const modelConfig = this.getModelConfig(modelId);
261
+ const allowedUpstreams = new Set(modelConfig.route?.upstreamIds ?? []);
262
+ const hasRouteFilter = allowedUpstreams.size > 0;
263
+ const currentTime = now();
264
+
265
+ return this.accounts
266
+ .list()
267
+ .filter((account) => !excludedIds.has(account.id))
268
+ .map((account) => ({ account, upstream: this.getUpstream(account.upstreamId), state: this.state.ensureAccount(account.id) }))
269
+ .filter((entry): entry is { account: StoredRouterAccount; upstream: RouterUpstreamConfig; state: ReturnType<RouterStateStore["ensureAccount"]> } => Boolean(entry.upstream))
270
+ .filter(({ account, upstream, state }) => {
271
+ if (!account.enabled) return false;
272
+ if (!upstream.enabled) return false;
273
+ if (!upstream.modelIds.includes(modelId)) return false;
274
+ if (hasRouteFilter && !allowedUpstreams.has(upstream.id)) return false;
275
+ if (state.authHealth === "invalid") return false;
276
+ if (state.cooldownUntil && state.cooldownUntil > currentTime) return false;
277
+ if (state.penaltyUntil && state.penaltyUntil > currentTime) return false;
278
+ return true;
279
+ })
280
+ .map(({ account, upstream, state }) => ({ account, upstream, modelConfig, state }));
281
+ }
282
+
283
+ private async prepareSelection(
284
+ eligible: EligibleAccount,
285
+ model: Model<Api>,
286
+ options?: SimpleStreamOptions,
287
+ ): Promise<DelegateSelection> {
288
+ let account = eligible.account;
289
+
290
+ if (account.provider !== "api-key" && account.expires - this.config.tokenRefreshSkewMs <= now()) {
291
+ account = await refreshAccountCredentials(account);
292
+ this.accounts.update(account);
293
+ this.state.clearHealth(account.id);
294
+ }
295
+
296
+ const apiKey = await getApiKeyForAccount(account);
297
+ const headers = { ...(eligible.upstream.headers ?? {}) };
298
+ const delegatedModel = {
299
+ ...model,
300
+ id: eligible.modelConfig.id,
301
+ name: eligible.modelConfig.name,
302
+ reasoning: eligible.modelConfig.reasoning,
303
+ input: eligible.modelConfig.input,
304
+ cost: eligible.modelConfig.cost,
305
+ contextWindow: eligible.modelConfig.contextWindow,
306
+ maxTokens: eligible.modelConfig.maxTokens,
307
+ compat: eligible.modelConfig.compat,
308
+ api: eligible.upstream.api,
309
+ baseUrl: eligible.upstream.baseUrl,
310
+ headers,
311
+ } as Model<Api>;
312
+
313
+ return {
314
+ account,
315
+ upstream: eligible.upstream,
316
+ modelConfig: eligible.modelConfig,
317
+ apiKey,
318
+ headers,
319
+ delegatedModel,
320
+ };
321
+ }
322
+
323
+ private markFailure(accountId: string, failure: FailureClassification) {
324
+ switch (failure.kind) {
325
+ case "rate-limit":
326
+ this.state.markRateLimit(accountId, failure.retryAfterMs ?? this.config.rateLimitCooldownMs, failure.status, failure.message);
327
+ break;
328
+ case "auth":
329
+ this.state.markAuthFailure(accountId, failure.status, failure.message);
330
+ break;
331
+ case "transient":
332
+ this.state.markTransientFailure(
333
+ accountId,
334
+ this.config.transientPenaltyMs,
335
+ failure.status,
336
+ failure.message,
337
+ );
338
+ break;
339
+ case "fatal":
340
+ this.state.markTransientFailure(accountId, this.config.transientPenaltyMs, failure.status ?? 500, failure.message);
341
+ break;
342
+ }
343
+ }
344
+
345
+ private async trySingleAccount(
346
+ selection: DelegateSelection,
347
+ outerStream: ReturnType<typeof createAssistantMessageEventStream>,
348
+ context: Context,
349
+ options?: SimpleStreamOptions,
350
+ ): Promise<{ completed: boolean; emittedMeaningfulOutput: boolean; failure?: FailureClassification }> {
351
+ let responseStatus: number | undefined;
352
+ let responseHeaders: Record<string, string> = {};
353
+ const buffered: AssistantMessageEvent[] = [];
354
+ let emittedMeaningfulOutput = false;
355
+
356
+ const streamOptions: SimpleStreamOptions = {
357
+ ...options,
358
+ apiKey: selection.apiKey,
359
+ headers: {
360
+ ...(options?.headers ?? {}),
361
+ ...selection.headers,
362
+ },
363
+ onResponse: async (response, responseModel) => {
364
+ responseStatus = response.status;
365
+ responseHeaders = response.headers;
366
+ await options?.onResponse?.(response, responseModel);
367
+ },
368
+ onPayload: options?.onPayload,
369
+ };
370
+
371
+ const inner = delegateStream(selection.upstream, selection.delegatedModel, context, streamOptions);
372
+
373
+ for await (const event of inner) {
374
+ if (event.type === "error") {
375
+ const failure = classifyFailure(responseStatus, responseHeaders, event.error.errorMessage || "Upstream request failed", this.config);
376
+ this.markFailure(selection.account.id, failure);
377
+
378
+ if (!emittedMeaningfulOutput) {
379
+ return { completed: false, emittedMeaningfulOutput: false, failure };
380
+ }
381
+
382
+ outerStream.push(event);
383
+ return { completed: false, emittedMeaningfulOutput: true, failure };
384
+ }
385
+
386
+ if (!emittedMeaningfulOutput && isMeaningfulEvent(event)) {
387
+ for (const pending of buffered) outerStream.push(pending);
388
+ buffered.length = 0;
389
+ emittedMeaningfulOutput = true;
390
+ }
391
+
392
+ if (emittedMeaningfulOutput) {
393
+ outerStream.push(event);
394
+ } else {
395
+ buffered.push(event);
396
+ }
397
+
398
+ if (event.type === "done") {
399
+ if (!emittedMeaningfulOutput) {
400
+ for (const pending of buffered) outerStream.push(pending);
401
+ buffered.length = 0;
402
+ }
403
+ this.state.markSuccess(selection.account.id, responseStatus ?? 200);
404
+ return { completed: true, emittedMeaningfulOutput };
405
+ }
406
+ }
407
+
408
+ return {
409
+ completed: false,
410
+ emittedMeaningfulOutput,
411
+ failure: {
412
+ kind: "transient",
413
+ status: responseStatus,
414
+ message: "Upstream stream ended unexpectedly",
415
+ },
416
+ };
417
+ }
418
+
419
+ stream(model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
420
+ this.reloadConfig();
421
+ const outer = createAssistantMessageEventStream();
422
+
423
+ (async () => {
424
+ const tried = new Set<string>();
425
+ let lastFailure: FailureClassification | undefined;
426
+ let emittedMeaningfulOutput = false;
427
+
428
+ try {
429
+ while (true) {
430
+ const eligible = this.getEligibleAccounts(model.id, tried);
431
+ const policy = this.getPolicy();
432
+ const cursor = this.state.getCursor(policy);
433
+ const picked = chooseEligibleAccount(policy, eligible, cursor);
434
+
435
+ if (!picked.selected) {
436
+ const message = lastFailure
437
+ ? `No healthy oauth-router accounts remaining after failover. Last error: ${lastFailure.message}`
438
+ : `No healthy oauth-router accounts are configured for model ${model.id}. Add accounts with /router-login add.`;
439
+ outer.push({ type: "error", reason: "error", error: createErrorMessage({ model, message }) });
440
+ outer.end();
441
+ return;
442
+ }
443
+
444
+ this.state.advanceCursor(policy, picked.nextCursor);
445
+ tried.add(picked.selected.account.id);
446
+ this.state.markAttempt(picked.selected.account.id, model.id);
447
+
448
+ let selection: DelegateSelection;
449
+ try {
450
+ selection = await this.prepareSelection(picked.selected, model, options);
451
+ } catch (error) {
452
+ const message = error instanceof Error ? error.message : String(error);
453
+ lastFailure = { kind: "auth", status: 401, message };
454
+ this.markFailure(picked.selected.account.id, lastFailure);
455
+ continue;
456
+ }
457
+
458
+ const result = await this.trySingleAccount(selection, outer, context, options);
459
+ if (result.completed) {
460
+ outer.end();
461
+ return;
462
+ }
463
+
464
+ emittedMeaningfulOutput ||= result.emittedMeaningfulOutput;
465
+ lastFailure = result.failure;
466
+
467
+ if (emittedMeaningfulOutput) {
468
+ outer.end();
469
+ return;
470
+ }
471
+ }
472
+ } catch (error) {
473
+ const message = error instanceof Error ? error.message : String(error);
474
+ outer.push({ type: "error", reason: "error", error: createErrorMessage({ model, message }) });
475
+ outer.end();
476
+ }
477
+ })();
478
+
479
+ return outer;
480
+ }
481
+ }
482
+
483
+ export function registerRouterProvider(pi: ExtensionAPI, runtime: RouterRuntime) {
484
+ const config = runtime.getConfig();
485
+ pi.registerProvider(config.providerName, {
486
+ baseUrl: "https://oauth-router.local",
487
+ apiKey: "OAUTH_ROUTER_DISABLED",
488
+ api: "oauth-router-api",
489
+ models: runtime.getProviderModels(),
490
+ streamSimple: (model, context, options) => runtime.stream(model, context, options),
491
+ });
492
+ }
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ ROOT = Path(__file__).resolve().parent.parent
12
+ REQUIRED = [
13
+ ROOT / "index.ts",
14
+ ROOT / "provider.ts",
15
+ ROOT / "commands.ts",
16
+ ROOT / "oauth-flow.ts",
17
+ ROOT / "oauth-store.ts",
18
+ ROOT / "state.ts",
19
+ ROOT / "policies.ts",
20
+ ROOT / "config.ts",
21
+ ROOT / "types.ts",
22
+ ROOT / "README.md",
23
+ ROOT / "docs" / "Project_Requirements.md",
24
+ ROOT / "docs" / "Coding_Guidelines.md",
25
+ ROOT / "docs" / "Builder_Prompt.md",
26
+ ROOT / "docs" / "issues" / "FR-001.md",
27
+ ]
28
+ EXPECTED_MODELS = ["oauth-router", "gpt-4o", "gpt-4.1", "o4-mini", "gpt-5.4"]
29
+ PI_CANDIDATES = [
30
+ os.environ.get("PI_BIN"),
31
+ shutil.which("pi"),
32
+ shutil.which("pi.cmd"),
33
+ str(Path.home() / "AppData" / "Roaming" / "npm" / "pi"),
34
+ str(Path.home() / "AppData" / "Roaming" / "npm" / "pi.cmd"),
35
+ ]
36
+
37
+
38
+ def resolve_pi() -> str | None:
39
+ for candidate in PI_CANDIDATES:
40
+ if not candidate:
41
+ continue
42
+ path = Path(candidate)
43
+ if path.exists():
44
+ return str(path)
45
+ return None
46
+
47
+
48
+ def run(cmd: list[str], timeout: int = 90) -> tuple[int, str, str]:
49
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
50
+ return proc.returncode, proc.stdout, proc.stderr
51
+
52
+
53
+ def main() -> int:
54
+ parser = argparse.ArgumentParser()
55
+ parser.add_argument("--quick", action="store_true")
56
+ args = parser.parse_args()
57
+
58
+ failed = False
59
+
60
+ for path in REQUIRED:
61
+ if not path.exists():
62
+ print(f"[FAIL] missing: {path}")
63
+ failed = True
64
+ else:
65
+ print(f"[ OK ] found: {path.relative_to(ROOT)}")
66
+
67
+ if args.quick:
68
+ return 1 if failed else 0
69
+
70
+ pi_bin = resolve_pi()
71
+ if not pi_bin:
72
+ print("[FAIL] unable to locate `pi` binary")
73
+ return 1
74
+
75
+ try:
76
+ code, stdout, stderr = run([pi_bin, "--list-models"])
77
+ output = f"{stdout}\n{stderr}"
78
+ if code != 0:
79
+ print("[FAIL] `pi --list-models` failed")
80
+ print(output.strip())
81
+ failed = True
82
+ else:
83
+ print("[ OK ] `pi --list-models` executed")
84
+ for token in EXPECTED_MODELS:
85
+ if token not in output:
86
+ print(f"[FAIL] model token not found in list output: {token}")
87
+ failed = True
88
+ else:
89
+ print(f"[ OK ] model token present: {token}")
90
+ except Exception as exc:
91
+ print(f"[FAIL] unable to execute `pi --list-models`: {exc}")
92
+ failed = True
93
+
94
+ return 1 if failed else 0
95
+
96
+
97
+ if __name__ == "__main__":
98
+ sys.exit(main())