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 +10 -0
- package/.pi/extensions/oauth-router/commands.ts +5 -9
- package/.pi/extensions/oauth-router/config.ts +6 -3
- package/.pi/extensions/oauth-router/oauth-flow.ts +34 -3
- package/.pi/extensions/oauth-router/types.ts +2 -0
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +1 -1
- package/.pi/extensions/takomi-context-manager/extension-conflicts.ts +14 -3
- package/.pi/extensions/takomi-runtime/context-panel.ts +605 -282
- package/.pi/extensions/takomi-runtime/index.ts +38 -1
- package/.pi/extensions/takomi-runtime/takomi-stats.js +44 -16
- package/README.md +34 -4
- package/package.json +9 -5
- package/src/cli.js +326 -33
- package/src/harness.js +132 -16
- package/src/pi-harness.js +76 -8
- package/src/pi-optional-features.js +195 -0
- package/src/postinstall.js +27 -0
- package/src/skills-catalog.js +245 -0
- package/src/skills-installer.js +244 -101
- package/src/skills-selection-tui.js +200 -0
- package/src/store.js +418 -240
- package/src/takomi-stats.js +44 -16
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
|
|
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 >
|
|
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 >
|
|
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
|
|
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
|
-
|
|
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
|
|
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}`);
|