takomi 2.1.27 → 2.1.29

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/.pi/README.md CHANGED
@@ -45,6 +45,7 @@ Inside Pi, use:
45
45
  - The old standalone commands (`/takomi-genesis`, `/takomi-design`, `/takomi-build`, `/takomi-kickoff`, `/autoorch`, `/orch`, `/architect`, `/code`, `/review`, and the `/takomi-subagent*` variants) are folded into `/takomi` subcommands so slash autocomplete stays small.
46
46
  - prompt shortcuts are suffixed with `-prompt` to avoid collisions with runtime commands, e.g. `/orch-prompt`, `/build-prompt`, `/design-prompt`, `/genesis-prompt`, `/takomi-prompt`, `/prime-prompt`
47
47
  - a project-local theme is available at `.pi/themes/takomi-noir.json`; select `takomi-noir` in `/settings` to use the Takomi UI palette
48
+ - optional Pi feature packs are managed by `takomi setup pi` or `takomi setup pi-features` and refreshed by `takomi refresh`; current defaults install Takomi Interview (`npm:@juicesharp/rpiv-ask-user-question`) while Takomi Todo (`npm:@juicesharp/rpiv-todo`), Browser QA (`npm:pi-chrome`), and Doc Preview (`npm:pi-markdown-preview`) stay opt-in
48
49
  - additional workflow prompts are available as direct slash commands:
49
50
  - `/vibe-primeAgent`
50
51
  - `/vibe-spawnTask`
@@ -128,3 +129,12 @@ So when working on packaging, agents should distinguish between:
128
129
  - rewrite JSON machine state
129
130
  - keep task docs organized in `pending/`, `in-progress/`, `completed/`, and `blocked/`
130
131
  - `takomi_board` intentionally cannot dispatch or redispatch agents. Use `takomi_subagent` for single, parallel, or chain execution, then call `takomi_board update_task` with the result.
132
+
133
+ ## Attribution Notes
134
+
135
+ - Pi-native Takomi runs on [Pi](https://github.com/earendil-works/pi) / `@earendil-works/pi-coding-agent`, authored by Mario Zechner and Earendil Works.
136
+ - Takomi subagent execution builds on [`pi-subagents`](https://github.com/nicobailon/pi-subagents) by Nico Bailon, with Takomi adding lifecycle orchestration, board/state artifacts, model-routing policy, and review-loop conventions.
137
+ - Optional Takomi Interview and Todo feature packs integrate [`@juicesharp/rpiv-ask-user-question`](https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-ask-user-question) and [`@juicesharp/rpiv-todo`](https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-todo) by juicesharp.
138
+ - Optional Browser QA can use [`pi-chrome`](https://github.com/tianrendong/pi-chrome) by tianrendong.
139
+ - Optional Doc Preview can use [`pi-markdown-preview`](https://github.com/omaclaren/pi-markdown-preview) by omaclaren.
140
+ - [`context-mode`](https://github.com/mksglu/context-mode) by Mert Koseoğlu is credited as external research/power-user context tooling; it is not a Takomi default bundle.
@@ -404,15 +404,13 @@ async function handleRouterLogin(pi: ExtensionAPI, runtime: RouterRuntime, args:
404
404
  updatedAt: Date.now(),
405
405
  });
406
406
  runtime.clearAccountHealth(duplicate.id);
407
- await runtime.refreshUsageSnapshot(duplicate.id);
408
407
  setFooterStatus(ctx, runtime);
409
- emitReport(pi, `Updated existing account ${duplicate.id} (${duplicate.label}) with fresh credentials instead of adding a duplicate.`);
408
+ emitReport(pi, `Updated existing account ${duplicate.id} (${duplicate.label}) with fresh credentials instead of adding a duplicate. Run /router-refresh-usage ${duplicate.id} to refresh provider quota metadata.`);
410
409
  return;
411
410
  }
412
411
  runtime.addAccount(account);
413
- await runtime.refreshUsageSnapshot(account.id);
414
412
  setFooterStatus(ctx, runtime);
415
- emitReport(pi, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}.`);
413
+ emitReport(pi, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}. Run /router-refresh-usage ${account.id} to refresh provider quota metadata.`);
416
414
  ctx.ui.notify(`Added ${account.id}`, "info");
417
415
  return;
418
416
  }
@@ -461,17 +459,15 @@ async function handleRouterLogin(pi: ExtensionAPI, runtime: RouterRuntime, args:
461
459
  updatedAt: Date.now(),
462
460
  });
463
461
  runtime.clearAccountHealth(existing.id);
464
- await runtime.refreshUsageSnapshot(existing.id);
465
462
  setFooterStatus(ctx, runtime);
466
- emitReport(pi, `Re-logged account ${existing.id} (${existing.label}) and cleared auth state.`);
463
+ emitReport(pi, `Re-logged account ${existing.id} (${existing.label}) and cleared auth state. Run /router-refresh-usage ${existing.id} to refresh provider quota metadata.`);
467
464
  return;
468
465
  }
469
466
  case "refresh": {
470
467
  const account = await pickAccount(runtime, ctx, first, "Choose account to refresh");
471
468
  const refreshed = await runtime.refreshAccount(account.id);
472
- await runtime.refreshUsageSnapshot(refreshed.id);
473
469
  setFooterStatus(ctx, runtime);
474
- emitReport(pi, `Refreshed account ${refreshed.id} (${refreshed.label}).`);
470
+ emitReport(pi, `Refreshed account ${refreshed.id} (${refreshed.label}). Run /router-refresh-usage ${refreshed.id} to refresh provider quota metadata.`);
475
471
  return;
476
472
  }
477
473
  case "help":
@@ -554,7 +550,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
554
550
  });
555
551
 
556
552
  pi.registerCommand("router-refresh-usage", {
557
- description: "Refresh oauth-router account metadata from token claims",
553
+ description: "Refresh oauth-router account metadata and best-effort provider quota",
558
554
  handler: async (args, ctx) => {
559
555
  const [requestedId] = parseArgs(args || "");
560
556
  const target = await pickUsageTarget(runtime, ctx, requestedId);
@@ -98,6 +98,7 @@ const DEFAULT_UPSTREAMS: RouterUpstreamConfig[] = [
98
98
  modelIds: ["gpt-5.1", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5"],
99
99
  usageProbe: {
100
100
  enabled: true,
101
+ timeoutMs: 2_500,
101
102
  endpoints: [
102
103
  "/wham/usage",
103
104
  "/codex/usage",
@@ -138,6 +139,8 @@ function sleepSync(ms: number): void {
138
139
  }
139
140
 
140
141
  const HELD_JSON_LOCKS = new Set<string>();
142
+ const JSON_LOCK_WAIT_TIMEOUT_MS = 2_000;
143
+ const JSON_LOCK_STALE_AFTER_MS = 5_000;
141
144
 
142
145
  export function withJsonFileLock<T>(filePath: string, fn: () => T): T {
143
146
  const lockPath = `${filePath}.lock`;
@@ -150,12 +153,12 @@ export function withJsonFileLock<T>(filePath: string, fn: () => T): T {
150
153
  } catch {
151
154
  try {
152
155
  const ageMs = Date.now() - statSync(lockPath).mtimeMs;
153
- if (ageMs > 30_000) rmSync(lockPath, { recursive: true, force: true });
156
+ if (ageMs > JSON_LOCK_STALE_AFTER_MS) rmSync(lockPath, { recursive: true, force: true });
154
157
  } catch {
155
158
  // The lock disappeared between mkdir attempts.
156
159
  }
157
- if (Date.now() - started > 10_000) {
158
- throw new Error(`Timed out waiting for oauth-router JSON lock: ${lockPath}`);
160
+ if (Date.now() - started > JSON_LOCK_WAIT_TIMEOUT_MS) {
161
+ throw new Error(`Timed out after ${JSON_LOCK_WAIT_TIMEOUT_MS}ms waiting for oauth-router JSON lock: ${lockPath}`);
159
162
  }
160
163
  sleepSync(50);
161
164
  }
@@ -331,6 +331,30 @@ function collectRateLimitHeaders(headers: Headers): Record<string, string> {
331
331
  return result;
332
332
  }
333
333
 
334
+ const DEFAULT_USAGE_PROBE_TIMEOUT_MS = 2_500;
335
+
336
+ function getUsageProbeTimeoutMs(upstream: RouterUpstreamConfig): number {
337
+ const configured = upstream.usageProbe?.timeoutMs;
338
+ return typeof configured === "number" && Number.isFinite(configured) && configured > 0
339
+ ? Math.min(configured, 10_000)
340
+ : DEFAULT_USAGE_PROBE_TIMEOUT_MS;
341
+ }
342
+
343
+ async function fetchJsonWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<{ response: Response; text: string }> {
344
+ const controller = new AbortController();
345
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
346
+ try {
347
+ const response = await fetch(url, { ...init, signal: controller.signal });
348
+ const text = await response.text();
349
+ return { response, text };
350
+ } catch (error) {
351
+ if (controller.signal.aborted) throw new Error(`timed out after ${Math.round(timeoutMs)}ms`);
352
+ throw error;
353
+ } finally {
354
+ clearTimeout(timer);
355
+ }
356
+ }
357
+
334
358
  export async function refreshProviderUsageSnapshot(account: StoredRouterAccount, upstream: RouterUpstreamConfig): Promise<RouterProviderUsageSnapshot> {
335
359
  const base = inspectAccountToken(account);
336
360
  if (account.provider !== "openai-codex" || upstream.usageProbe?.enabled === false) return base;
@@ -345,12 +369,20 @@ export async function refreshProviderUsageSnapshot(account: StoredRouterAccount,
345
369
  let lastEndpoint: string | undefined;
346
370
  let lastHeaders: Record<string, string> | undefined;
347
371
  const errors: string[] = [];
372
+ const startedAt = now();
373
+ const totalTimeoutMs = getUsageProbeTimeoutMs(upstream);
348
374
 
349
375
  for (const endpoint of endpoints) {
376
+ const remainingMs = totalTimeoutMs - (now() - startedAt);
377
+ if (remainingMs <= 0) {
378
+ errors.push(`probe timed out after ${Math.round(totalTimeoutMs)}ms`);
379
+ break;
380
+ }
381
+
350
382
  const url = resolveProbeUrl(upstream.baseUrl, endpoint);
351
383
  lastEndpoint = url;
352
384
  try {
353
- const response = await fetch(url, {
385
+ const { response, text } = await fetchJsonWithTimeout(url, {
354
386
  method: "GET",
355
387
  headers: {
356
388
  Authorization: `Bearer ${account.access}`,
@@ -361,10 +393,9 @@ export async function refreshProviderUsageSnapshot(account: StoredRouterAccount,
361
393
  "User-Agent": "codex_cli_rs/0.133.0 (Windows; x86_64) pi/oauth-router",
362
394
  accept: "application/json",
363
395
  },
364
- });
396
+ }, Math.max(1, remainingMs));
365
397
  lastStatus = response.status;
366
398
  lastHeaders = collectRateLimitHeaders(response.headers);
367
- const text = await response.text();
368
399
  const json = text ? JSON.parse(text) : undefined;
369
400
  const extracted = extractProviderUsage(json);
370
401
  const hasQuota = Boolean(extracted.fiveHour || extracted.weekly || extracted.planType);
@@ -27,6 +27,8 @@ export interface RouterModelConfig {
27
27
  export interface RouterProviderUsageProbeConfig {
28
28
  enabled?: boolean;
29
29
  endpoints?: string[];
30
+ /** Total best-effort probe budget per account. Provider usage checks must not block login/refresh UX. */
31
+ timeoutMs?: number;
30
32
  }
31
33
 
32
34
  export interface RouterUpstreamConfig {
@@ -41,7 +41,7 @@ export function renderReport(state: ContextManagerState, verbose = false): strin
41
41
  lines.push("- Candidates: none");
42
42
  }
43
43
  if (report.duplicateExtensionWarnings.length > 0 || verbose) {
44
- lines.push("", "Extension Conflict Diagnostics:", ...renderDuplicateExtensionGuidance(report.duplicateExtensionWarnings));
44
+ lines.push("", "Extension Conflict Diagnostics:", ...renderDuplicateExtensionGuidance(report.duplicateExtensionWarnings, report.cwd));
45
45
  }
46
46
  if (verbose) {
47
47
  lines.push("", "Skill Index:", ...sortedSkills(state.skills).map((skill) => `- ${skill.name}`));
@@ -1,7 +1,7 @@
1
1
  import { access } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
- import type { ContextManagerState } from "./state";
5
5
 
6
6
  type KnownTakomiExtension = {
7
7
  toolName: string;
@@ -43,9 +43,20 @@ export async function detectDuplicateTakomiExtensions(cwd: string): Promise<Arra
43
43
  return warnings;
44
44
  }
45
45
 
46
- export function renderDuplicateExtensionGuidance(warnings: Array<{ toolName: string; paths: string[] }>): string[] {
46
+ function isTakomiSourceRepo(cwd: string): boolean {
47
+ return [
48
+ path.join(cwd, "package.json"),
49
+ path.join(cwd, "scripts", "pi-dev.ps1"),
50
+ path.join(cwd, ".pi", "extensions", "takomi-runtime", "index.ts"),
51
+ ].every((filePath) => existsSync(filePath));
52
+ }
53
+
54
+ export function renderDuplicateExtensionGuidance(warnings: Array<{ toolName: string; paths: string[] }>, cwd?: string): string[] {
47
55
  if (warnings.length === 0) return ["- Duplicate Takomi extensions: none detected"];
48
- const lines = ["- Duplicate Takomi extensions detected:"];
56
+ const sourceRepo = cwd ? isTakomiSourceRepo(cwd) : false;
57
+ const lines = [sourceRepo
58
+ ? "- Duplicate Takomi extensions detected (expected when running normal Pi inside the Takomi source repo):"
59
+ : "- Duplicate Takomi extensions detected:"];
49
60
  for (const warning of warnings) {
50
61
  lines.push(` - ${warning.toolName}`);
51
62
  for (const filePath of warning.paths) lines.push(` - ${filePath}`);