takomi 2.1.28 → 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/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-runtime/context-panel.ts +23 -1
- package/package.json +7 -5
- package/src/pi-harness.js +53 -6
|
@@ -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 {
|
|
@@ -70,6 +70,15 @@ type BoardCounts = {
|
|
|
70
70
|
blocked: number;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
type BoardCountsCacheEntry = {
|
|
74
|
+
checkedAt: number;
|
|
75
|
+
mtimeMs: number;
|
|
76
|
+
counts?: BoardCounts;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const BOARD_COUNTS_CACHE_MS = 1_000;
|
|
80
|
+
const boardCountsCache = new Map<string, BoardCountsCacheEntry>();
|
|
81
|
+
|
|
73
82
|
function createEmptyState(sessionStart = Date.now(), runtime: ContextRuntimeState = {}): ContextPanelState {
|
|
74
83
|
return {
|
|
75
84
|
fileChanges: [],
|
|
@@ -186,8 +195,19 @@ function formatDisplayPath(filePath: string, cwd?: string): string {
|
|
|
186
195
|
|
|
187
196
|
function loadBoardCounts(cwd: string, sessionId?: string): BoardCounts | undefined {
|
|
188
197
|
if (!sessionId) return undefined;
|
|
198
|
+
const stateFile = path.join(cwd, ".pi", "takomi", "orchestrator", `${sessionId}.json`);
|
|
199
|
+
const cached = boardCountsCache.get(stateFile);
|
|
200
|
+
const checkedAt = Date.now();
|
|
201
|
+
|
|
202
|
+
if (cached && checkedAt - cached.checkedAt < BOARD_COUNTS_CACHE_MS) return cached.counts;
|
|
203
|
+
|
|
189
204
|
try {
|
|
190
|
-
const
|
|
205
|
+
const mtimeMs = statSync(stateFile).mtimeMs;
|
|
206
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
207
|
+
boardCountsCache.set(stateFile, { ...cached, checkedAt });
|
|
208
|
+
return cached.counts;
|
|
209
|
+
}
|
|
210
|
+
|
|
191
211
|
const parsed = JSON.parse(readFileSync(stateFile, "utf8")) as { tasks?: Array<{ status?: string }> };
|
|
192
212
|
const counts: BoardCounts = { completed: 0, pending: 0, inProgress: 0, blocked: 0 };
|
|
193
213
|
for (const task of parsed.tasks ?? []) {
|
|
@@ -196,8 +216,10 @@ function loadBoardCounts(cwd: string, sessionId?: string): BoardCounts | undefin
|
|
|
196
216
|
else if (task.status === "blocked") counts.blocked += 1;
|
|
197
217
|
else counts.pending += 1;
|
|
198
218
|
}
|
|
219
|
+
boardCountsCache.set(stateFile, { checkedAt, mtimeMs, counts });
|
|
199
220
|
return counts;
|
|
200
221
|
} catch {
|
|
222
|
+
boardCountsCache.set(stateFile, { checkedAt, mtimeMs: 0, counts: undefined });
|
|
201
223
|
return undefined;
|
|
202
224
|
}
|
|
203
225
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takomi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.29",
|
|
4
4
|
"description": "🎯 Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
25
|
"postinstall": "node src/postinstall.js",
|
|
26
|
-
"test": "
|
|
26
|
+
"test": "node scripts/test-skill-selection.js",
|
|
27
27
|
"test:skills": "node scripts/test-skill-selection.js"
|
|
28
28
|
},
|
|
29
29
|
"repository": {
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
"author": "J StaR Films Studios",
|
|
46
46
|
"license": "ISC",
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@earendil-works/pi-ai": "^0.78.1",
|
|
49
|
+
"@mariozechner/pi-agent-core": "^0.70.6",
|
|
50
|
+
"@mariozechner/pi-ai": "^0.70.2",
|
|
51
|
+
"@mariozechner/pi-coding-agent": "^0.70.2",
|
|
52
|
+
"@mariozechner/pi-tui": "^0.70.6",
|
|
48
53
|
"commander": "^13.1.0",
|
|
49
54
|
"figlet": "^1.8.0",
|
|
50
55
|
"fs-extra": "^11.3.0",
|
|
@@ -58,11 +63,8 @@
|
|
|
58
63
|
"homepage": "https://github.com/JStaRFilms/VibeCode-Protocol-Suite#readme",
|
|
59
64
|
"devDependencies": {
|
|
60
65
|
"@earendil-works/pi-agent-core": "^0.78.1",
|
|
61
|
-
"@earendil-works/pi-ai": "^0.78.1",
|
|
62
66
|
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
63
67
|
"@earendil-works/pi-tui": "^0.78.1",
|
|
64
|
-
"@mariozechner/pi-ai": "^0.70.2",
|
|
65
|
-
"@mariozechner/pi-coding-agent": "^0.70.2",
|
|
66
68
|
"@types/node": "^25.5.2",
|
|
67
69
|
"typebox": "^1.1.33",
|
|
68
70
|
"typescript": "^6.0.2"
|
package/src/pi-harness.js
CHANGED
|
@@ -213,6 +213,42 @@ function runCommand(command, args) {
|
|
|
213
213
|
return spawnSync(command, args, { stdio: 'pipe', encoding: 'utf8', shell: process.platform === 'win32' });
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
function runCommandWithTimeout(command, args, timeoutMs) {
|
|
217
|
+
return new Promise((resolve) => {
|
|
218
|
+
let stdout = '';
|
|
219
|
+
let stderr = '';
|
|
220
|
+
let settled = false;
|
|
221
|
+
let timedOut = false;
|
|
222
|
+
|
|
223
|
+
const child = spawn(command, args, {
|
|
224
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
225
|
+
shell: process.platform === 'win32',
|
|
226
|
+
windowsHide: true,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const finish = (result) => {
|
|
230
|
+
if (settled) return;
|
|
231
|
+
settled = true;
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
resolve({ stdout, stderr, timedOut, ...result });
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const timer = setTimeout(() => {
|
|
237
|
+
timedOut = true;
|
|
238
|
+
try { child.kill(); } catch {}
|
|
239
|
+
setTimeout(() => {
|
|
240
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
241
|
+
}, 1500).unref?.();
|
|
242
|
+
}, timeoutMs);
|
|
243
|
+
timer.unref?.();
|
|
244
|
+
|
|
245
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
246
|
+
child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
247
|
+
child.on('error', (error) => finish({ status: 1, error }));
|
|
248
|
+
child.on('close', (code) => finish({ status: timedOut ? null : code }));
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
216
252
|
export async function ensurePiInstalled() {
|
|
217
253
|
const before = await detectPiCommand();
|
|
218
254
|
if (before.installed) {
|
|
@@ -300,17 +336,28 @@ export function printPiSubagentsInstallResult(result) {
|
|
|
300
336
|
if (result.report) console.log(pc.dim(result.report.split(/\r?\n/).slice(-8).join('\n')));
|
|
301
337
|
}
|
|
302
338
|
|
|
303
|
-
export async function updatePiManagedPackages() {
|
|
339
|
+
export async function updatePiManagedPackages({ timeoutMs = 20_000 } = {}) {
|
|
340
|
+
if (process.env.TAKOMI_SKIP_PI_PACKAGE_UPDATE === '1') {
|
|
341
|
+
return { ok: true, changed: false, report: 'Skipped Pi package update because TAKOMI_SKIP_PI_PACKAGE_UPDATE=1.' };
|
|
342
|
+
}
|
|
343
|
+
|
|
304
344
|
const pi = await detectPiCommand();
|
|
305
345
|
if (!pi.installed) return { ok: false, changed: false, report: 'Pi is not installed.' };
|
|
306
346
|
|
|
307
|
-
// `pi update` reconciles
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
// know their names ahead of time.
|
|
347
|
+
// `pi update` reconciles packages from Pi settings, including user-installed
|
|
348
|
+
// npm/git/local packages. Treat this as a best-effort follow-up: third-party
|
|
349
|
+
// packages or manual extension entries must never hang Takomi installation.
|
|
311
350
|
const command = pi.path || 'pi';
|
|
312
|
-
const result =
|
|
351
|
+
const result = await runCommandWithTimeout(command, ['update'], timeoutMs);
|
|
313
352
|
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
353
|
+
if (result.timedOut) {
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
changed: false,
|
|
357
|
+
report: `Timed out after ${Math.round(timeoutMs / 1000)}s while running \`pi update\`. Takomi setup is complete; retry package updates later with \`pi update\` or skip this step with TAKOMI_SKIP_PI_PACKAGE_UPDATE=1. Last output:\n${output}`.trim(),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
314
361
|
return {
|
|
315
362
|
ok: result.status === 0,
|
|
316
363
|
changed: result.status === 0,
|