takomi 2.1.3 → 2.1.5
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/takomi-runtime/index.ts +0 -42
- package/.pi/extensions/takomi-runtime/shared.ts +28 -0
- package/.pi/extensions/takomi-runtime/subagent-types.ts +7 -0
- package/.pi/extensions/takomi-subagents/dispatch.ts +104 -1
- package/.pi/extensions/takomi-subagents/index.ts +4 -3
- package/.pi/extensions/takomi-subagents/live-updates.ts +52 -0
- package/.pi/extensions/takomi-subagents/native-render.ts +5 -143
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +239 -0
- package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +18 -0
- package/package.json +2 -2
- package/src/cli.js +14 -0
- package/src/update-check.js +144 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
4
3
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
4
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
6
5
|
import { Type } from "typebox";
|
|
@@ -47,9 +46,6 @@ import {
|
|
|
47
46
|
type TakomiSubagentRuntimeEvent,
|
|
48
47
|
} from "./subagent-types";
|
|
49
48
|
import {
|
|
50
|
-
visibleWidth,
|
|
51
|
-
truncateToWidth,
|
|
52
|
-
formatFooterNumber,
|
|
53
49
|
buildTaskPrompt,
|
|
54
50
|
resolvePreferredModel,
|
|
55
51
|
} from "./shared";
|
|
@@ -438,46 +434,8 @@ async function applyProfileDefaultsToTasks(ctx: ExtensionContext, tasks: Orchest
|
|
|
438
434
|
return nextTasks;
|
|
439
435
|
}
|
|
440
436
|
|
|
441
|
-
// stripAnsi, visibleWidth, truncateToWidth, formatFooterNumber
|
|
442
|
-
// are imported from "./shared"
|
|
443
|
-
|
|
444
437
|
function installTakomiFooter(ctx: ExtensionContext, stateRef: { current: TakomiState }): void {
|
|
445
438
|
ctx.ui.setFooter((tui, theme, footerData) => new TakomiFooterComponent(tui, theme, footerData, ctx, () => stateRef.current));
|
|
446
|
-
return;
|
|
447
|
-
ctx.ui.setFooter((_tui, theme, footerData) => ({
|
|
448
|
-
invalidate() {},
|
|
449
|
-
render(width: number): string[] {
|
|
450
|
-
const state = stateRef.current;
|
|
451
|
-
let input = 0;
|
|
452
|
-
let output = 0;
|
|
453
|
-
let cost = 0;
|
|
454
|
-
for (const entry of ctx.sessionManager.getBranch()) {
|
|
455
|
-
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
456
|
-
const message = entry.message as AssistantMessage;
|
|
457
|
-
input += message.usage.input;
|
|
458
|
-
output += message.usage.output;
|
|
459
|
-
cost += message.usage.cost.total;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const cwd = theme.fg("dim", ctx.cwd);
|
|
464
|
-
const stats = theme.fg("dim", `↑${formatFooterNumber(input)} ↓${formatFooterNumber(output)} $${cost.toFixed(3)}`);
|
|
465
|
-
const leftPad = " ".repeat(Math.max(1, width - visibleWidth(cwd) - visibleWidth(stats)));
|
|
466
|
-
const topLine = truncateToWidth(cwd + leftPad + stats, width);
|
|
467
|
-
|
|
468
|
-
const extensionStatuses = [...footerData.getExtensionStatuses().entries()]
|
|
469
|
-
.filter(([key]) => key !== "takomi-runtime")
|
|
470
|
-
.map(([, value]) => value)
|
|
471
|
-
.filter(Boolean);
|
|
472
|
-
const runtimeStatus = renderRuntimeStatus(theme, state);
|
|
473
|
-
const left = [runtimeStatus, ...extensionStatuses].join(theme.fg("dim", " · "));
|
|
474
|
-
const right = theme.fg("dim", ctx.model?.id || "no-model");
|
|
475
|
-
const rightPad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
|
|
476
|
-
const bottomLine = truncateToWidth(left + rightPad + right, width);
|
|
477
|
-
|
|
478
|
-
return [topLine, bottomLine];
|
|
479
|
-
},
|
|
480
|
-
}));
|
|
481
439
|
}
|
|
482
440
|
|
|
483
441
|
// Mutable state ref so the footer closure always reads the latest state
|
|
@@ -194,6 +194,14 @@ export type RunHooks = {
|
|
|
194
194
|
export type JsonRunHooks = {
|
|
195
195
|
onAssistantText?: (text: string) => void;
|
|
196
196
|
onEventText?: (line: string) => void;
|
|
197
|
+
onToolEvent?: (event: {
|
|
198
|
+
type: "start" | "update" | "end";
|
|
199
|
+
toolName: string;
|
|
200
|
+
args?: string;
|
|
201
|
+
isError?: boolean;
|
|
202
|
+
summary?: string;
|
|
203
|
+
invocationId?: string;
|
|
204
|
+
}) => void;
|
|
197
205
|
onStderr?: (chunk: string) => void;
|
|
198
206
|
};
|
|
199
207
|
|
|
@@ -267,6 +275,14 @@ function extractAssistantSnapshot(event: Record<string, unknown>, currentText: s
|
|
|
267
275
|
return undefined;
|
|
268
276
|
}
|
|
269
277
|
|
|
278
|
+
function extractToolInvocationId(event: Record<string, unknown>, toolName: string): string {
|
|
279
|
+
for (const key of ["invocationId", "toolCallId", "toolUseId", "callId", "id"]) {
|
|
280
|
+
const value = event[key];
|
|
281
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
282
|
+
}
|
|
283
|
+
return toolName;
|
|
284
|
+
}
|
|
285
|
+
|
|
270
286
|
export function summarizeJsonEvent(event: Record<string, unknown>): string | undefined {
|
|
271
287
|
const type = typeof event.type === "string" ? event.type : "";
|
|
272
288
|
if (type === "tool_execution_start") {
|
|
@@ -346,6 +362,18 @@ export async function runPiAgentJson(cwd: string, args: string[], signal?: Abort
|
|
|
346
362
|
const messageText = summarizeJsonEvent(event);
|
|
347
363
|
if (messageText) hooks?.onEventText?.(messageText);
|
|
348
364
|
|
|
365
|
+
if (event.type === "tool_execution_start") {
|
|
366
|
+
const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
|
|
367
|
+
const args = typeof event.args === "string" ? event.args : event.args ? JSON.stringify(event.args) : undefined;
|
|
368
|
+
hooks?.onToolEvent?.({ type: "start", toolName, args, summary: messageText, invocationId: extractToolInvocationId(event, toolName) });
|
|
369
|
+
} else if (event.type === "tool_execution_update") {
|
|
370
|
+
const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
|
|
371
|
+
hooks?.onToolEvent?.({ type: "update", toolName, summary: messageText, invocationId: extractToolInvocationId(event, toolName) });
|
|
372
|
+
} else if (event.type === "tool_execution_end") {
|
|
373
|
+
const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
|
|
374
|
+
hooks?.onToolEvent?.({ type: "end", toolName, isError: event.isError === true, summary: messageText, invocationId: extractToolInvocationId(event, toolName) });
|
|
375
|
+
}
|
|
376
|
+
|
|
349
377
|
if (event.type === "message_end") {
|
|
350
378
|
const message = event.message;
|
|
351
379
|
if (message && typeof message === "object" && (message as { role?: string }).role === "assistant") {
|
|
@@ -25,6 +25,13 @@ export type TakomiSubagentRun = {
|
|
|
25
25
|
boardTaskStatus?: TakomiBoardTaskStatus;
|
|
26
26
|
summary?: string;
|
|
27
27
|
outputText?: string;
|
|
28
|
+
currentTool?: string;
|
|
29
|
+
currentToolArgs?: string;
|
|
30
|
+
currentToolStartedAt?: number;
|
|
31
|
+
recentTools?: Array<{ tool: string; args: string; endMs: number }>;
|
|
32
|
+
recentOutput?: string[];
|
|
33
|
+
toolCount?: number;
|
|
34
|
+
sessionFile?: string;
|
|
28
35
|
logs: string[];
|
|
29
36
|
startedAt: number;
|
|
30
37
|
updatedAt: number;
|
|
@@ -53,6 +53,16 @@ export type TakomiDispatchResult = {
|
|
|
53
53
|
output: string;
|
|
54
54
|
stderr: string;
|
|
55
55
|
preflight: string;
|
|
56
|
+
startedAt?: number;
|
|
57
|
+
endedAt?: number;
|
|
58
|
+
lastActivityAt?: number;
|
|
59
|
+
currentTool?: string;
|
|
60
|
+
currentToolArgs?: string;
|
|
61
|
+
currentToolStartedAt?: number;
|
|
62
|
+
recentTools?: Array<{ tool: string; args: string; endMs: number }>;
|
|
63
|
+
recentOutput?: string[];
|
|
64
|
+
toolCount?: number;
|
|
65
|
+
sessionFile?: string;
|
|
56
66
|
};
|
|
57
67
|
|
|
58
68
|
export type TakomiDispatchHooks = {
|
|
@@ -72,6 +82,30 @@ export async function dispatchTakomiSubagent(
|
|
|
72
82
|
const sessionDir = path.join(input.rootCwd, ".pi", "takomi", "subagents");
|
|
73
83
|
const sessionPath = path.join(sessionDir, `${conversationId}.jsonl`);
|
|
74
84
|
await mkdir(sessionDir, { recursive: true });
|
|
85
|
+
const startedAt = Date.now();
|
|
86
|
+
let lastActivityAt = startedAt;
|
|
87
|
+
let currentTool: string | undefined;
|
|
88
|
+
let currentToolArgs: string | undefined;
|
|
89
|
+
let currentToolStartedAt: number | undefined;
|
|
90
|
+
let toolCount = 0;
|
|
91
|
+
let recentTools: Array<{ tool: string; args: string; endMs: number }> = [];
|
|
92
|
+
const activeToolInvocations = new Map<string, { tool: string; args: string; startedAt: number }>();
|
|
93
|
+
let recentOutput: string[] = [];
|
|
94
|
+
|
|
95
|
+
const setCurrentToolFromActive = () => {
|
|
96
|
+
const latest = [...activeToolInvocations.values()].at(-1);
|
|
97
|
+
currentTool = latest?.tool;
|
|
98
|
+
currentToolArgs = latest?.args;
|
|
99
|
+
currentToolStartedAt = latest?.startedAt;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const appendRecentOutput = (text: string) => {
|
|
103
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
104
|
+
if (!lines.length) return;
|
|
105
|
+
recentOutput.push(...lines);
|
|
106
|
+
if (recentOutput.length > 8) recentOutput.splice(0, recentOutput.length - 8);
|
|
107
|
+
lastActivityAt = Date.now();
|
|
108
|
+
};
|
|
75
109
|
|
|
76
110
|
hooks?.emit?.({
|
|
77
111
|
type: "start",
|
|
@@ -123,6 +157,13 @@ export async function dispatchTakomiSubagent(
|
|
|
123
157
|
output: "",
|
|
124
158
|
stderr: preflight.report,
|
|
125
159
|
preflight: preflight.report,
|
|
160
|
+
startedAt,
|
|
161
|
+
endedAt: Date.now(),
|
|
162
|
+
lastActivityAt,
|
|
163
|
+
recentTools,
|
|
164
|
+
recentOutput,
|
|
165
|
+
toolCount,
|
|
166
|
+
sessionFile: sessionPath,
|
|
126
167
|
};
|
|
127
168
|
hooks?.emit?.({
|
|
128
169
|
type: "block",
|
|
@@ -154,13 +195,65 @@ export async function dispatchTakomiSubagent(
|
|
|
154
195
|
|
|
155
196
|
const result = await runPiAgentJson(subagentCwd, args, signal, {
|
|
156
197
|
onAssistantText: (text) => {
|
|
157
|
-
|
|
198
|
+
appendRecentOutput(text);
|
|
199
|
+
hooks?.emit?.({
|
|
200
|
+
type: "update",
|
|
201
|
+
runKey,
|
|
202
|
+
patch: {
|
|
203
|
+
outputText: text,
|
|
204
|
+
recentOutput,
|
|
205
|
+
currentTool,
|
|
206
|
+
currentToolArgs,
|
|
207
|
+
currentToolStartedAt,
|
|
208
|
+
recentTools,
|
|
209
|
+
toolCount,
|
|
210
|
+
boardTaskStatus: input.boardTaskStatus,
|
|
211
|
+
checklist: input.checklist,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
158
214
|
},
|
|
159
215
|
onEventText: (line) => {
|
|
216
|
+
appendRecentOutput(line);
|
|
160
217
|
hooks?.emit?.({ type: "appendLog", runKey, chunk: line });
|
|
218
|
+
hooks?.emit?.({ type: "update", runKey, patch: { recentOutput, boardTaskStatus: input.boardTaskStatus, checklist: input.checklist } });
|
|
219
|
+
},
|
|
220
|
+
onToolEvent: (event) => {
|
|
221
|
+
lastActivityAt = Date.now();
|
|
222
|
+
const invocationId = event.invocationId ?? event.toolName;
|
|
223
|
+
if (event.type === "start") {
|
|
224
|
+
activeToolInvocations.set(invocationId, {
|
|
225
|
+
tool: event.toolName,
|
|
226
|
+
args: event.args ?? "",
|
|
227
|
+
startedAt: Date.now(),
|
|
228
|
+
});
|
|
229
|
+
toolCount += 1;
|
|
230
|
+
setCurrentToolFromActive();
|
|
231
|
+
} else if (event.type === "end") {
|
|
232
|
+
const active = activeToolInvocations.get(invocationId);
|
|
233
|
+
recentTools.push({ tool: event.toolName, args: active?.args ?? "", endMs: Date.now() });
|
|
234
|
+
if (recentTools.length > 8) recentTools.splice(0, recentTools.length - 8);
|
|
235
|
+
activeToolInvocations.delete(invocationId);
|
|
236
|
+
setCurrentToolFromActive();
|
|
237
|
+
}
|
|
238
|
+
hooks?.emit?.({
|
|
239
|
+
type: "update",
|
|
240
|
+
runKey,
|
|
241
|
+
patch: {
|
|
242
|
+
currentTool,
|
|
243
|
+
currentToolArgs,
|
|
244
|
+
currentToolStartedAt,
|
|
245
|
+
recentTools,
|
|
246
|
+
recentOutput,
|
|
247
|
+
toolCount,
|
|
248
|
+
boardTaskStatus: input.boardTaskStatus,
|
|
249
|
+
checklist: input.checklist,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
161
252
|
},
|
|
162
253
|
onStderr: (chunk) => {
|
|
254
|
+
appendRecentOutput(chunk);
|
|
163
255
|
hooks?.emit?.({ type: "appendLog", runKey, chunk });
|
|
256
|
+
hooks?.emit?.({ type: "update", runKey, patch: { recentOutput, boardTaskStatus: input.boardTaskStatus, checklist: input.checklist } });
|
|
164
257
|
},
|
|
165
258
|
});
|
|
166
259
|
|
|
@@ -177,6 +270,16 @@ export async function dispatchTakomiSubagent(
|
|
|
177
270
|
output,
|
|
178
271
|
stderr: result.stderr.trim(),
|
|
179
272
|
preflight: preflight.report,
|
|
273
|
+
startedAt,
|
|
274
|
+
endedAt: Date.now(),
|
|
275
|
+
lastActivityAt,
|
|
276
|
+
currentTool,
|
|
277
|
+
currentToolArgs,
|
|
278
|
+
currentToolStartedAt,
|
|
279
|
+
recentTools,
|
|
280
|
+
recentOutput,
|
|
281
|
+
toolCount,
|
|
282
|
+
sessionFile: sessionPath,
|
|
180
283
|
};
|
|
181
284
|
|
|
182
285
|
if (result.code !== 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { renderTakomiSubagentCall, renderTakomiSubagentResult } from "./native-render";
|
|
4
|
-
import {
|
|
4
|
+
import { createTakomiPiSubagentsEngine } from "./pi-subagents-engine";
|
|
5
5
|
|
|
6
6
|
const ChecklistItemSchema = Type.Object({
|
|
7
7
|
text: Type.String(),
|
|
@@ -50,6 +50,7 @@ const SubagentParameters = Type.Object({
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
function registerSubagentTool(pi: ExtensionAPI): void {
|
|
53
|
+
const engine = createTakomiPiSubagentsEngine(pi);
|
|
53
54
|
pi.registerTool({
|
|
54
55
|
name: "takomi_subagent",
|
|
55
56
|
label: "Takomi",
|
|
@@ -62,8 +63,8 @@ function registerSubagentTool(pi: ExtensionAPI): void {
|
|
|
62
63
|
"If review sends work back to the same agent, reuse the same conversationId for continuity.",
|
|
63
64
|
],
|
|
64
65
|
parameters: SubagentParameters,
|
|
65
|
-
async execute(
|
|
66
|
-
return
|
|
66
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
67
|
+
return engine.execute(toolCallId, params, signal, onUpdate as any, ctx);
|
|
67
68
|
},
|
|
68
69
|
renderCall: renderTakomiSubagentCall,
|
|
69
70
|
renderResult: renderTakomiSubagentResult,
|
|
@@ -16,11 +16,30 @@ type LiveTask = {
|
|
|
16
16
|
conversationId?: string;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
function mergeRecentTools(
|
|
20
|
+
existing: Array<{ tool: string; args: string; endMs: number }> | undefined,
|
|
21
|
+
incoming: Array<{ tool: string; args: string; endMs: number }> | undefined,
|
|
22
|
+
): Array<{ tool: string; args: string; endMs: number }> | undefined {
|
|
23
|
+
if (!incoming?.length) return existing;
|
|
24
|
+
return incoming.slice(-8);
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
function appendLine(value: string, line: string): string {
|
|
20
28
|
const next = [value, line.trim()].filter(Boolean).join("\n");
|
|
21
29
|
return next.split(/\r?\n/).slice(-12).join("\n");
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
function compactOutputPreview(text: string): string[] {
|
|
33
|
+
const lines = text
|
|
34
|
+
.split(/\r?\n/)
|
|
35
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.filter((line) => !/^\*\*[^*]+\*\*\s+I\s/i.test(line))
|
|
38
|
+
.filter((line) => !/\bI need to make sure\b/i.test(line))
|
|
39
|
+
.filter((line) => !/\bI want the user to\b/i.test(line));
|
|
40
|
+
return lines.slice(-3);
|
|
41
|
+
}
|
|
42
|
+
|
|
24
43
|
export function createTakomiLiveUpdateBridge(
|
|
25
44
|
tasks: LiveTask[],
|
|
26
45
|
mode: "single" | "parallel" | "chain",
|
|
@@ -38,6 +57,10 @@ export function createTakomiLiveUpdateBridge(
|
|
|
38
57
|
output: "Queued.",
|
|
39
58
|
stderr: "",
|
|
40
59
|
preflight: "",
|
|
60
|
+
lastActivityAt: Date.now(),
|
|
61
|
+
recentTools: [],
|
|
62
|
+
recentOutput: ["Queued."],
|
|
63
|
+
toolCount: 0,
|
|
41
64
|
}));
|
|
42
65
|
|
|
43
66
|
const emit = () => {
|
|
@@ -54,23 +77,52 @@ export function createTakomiLiveUpdateBridge(
|
|
|
54
77
|
event(index: number, event: TakomiSubagentRuntimeEvent): void {
|
|
55
78
|
const current = results[index];
|
|
56
79
|
if (!current) return;
|
|
80
|
+
current.lastActivityAt = Date.now();
|
|
57
81
|
if (event.type === "start") {
|
|
82
|
+
current.startedAt = Date.now();
|
|
58
83
|
current.conversationId = event.state.conversationId ?? current.conversationId;
|
|
59
84
|
current.thinking = event.state.thinking ?? current.thinking;
|
|
85
|
+
current.sessionFile = event.state.sessionFile ?? current.sessionFile;
|
|
60
86
|
current.output = event.state.summary ?? "Starting.";
|
|
87
|
+
current.recentOutput = [current.output].filter(Boolean).slice(-8);
|
|
61
88
|
} else if (event.type === "update") {
|
|
62
89
|
current.model = event.patch.model ?? current.model;
|
|
63
90
|
current.thinking = event.patch.thinking ?? current.thinking;
|
|
91
|
+
current.sessionFile = event.patch.sessionFile ?? current.sessionFile;
|
|
92
|
+
current.currentTool = event.patch.currentTool;
|
|
93
|
+
current.currentToolArgs = event.patch.currentToolArgs;
|
|
94
|
+
current.currentToolStartedAt = event.patch.currentToolStartedAt;
|
|
95
|
+
current.recentTools = mergeRecentTools(current.recentTools, event.patch.recentTools);
|
|
96
|
+
current.toolCount = event.patch.toolCount ?? current.toolCount;
|
|
64
97
|
current.output = event.patch.outputText ?? event.patch.summary ?? current.output;
|
|
98
|
+
current.recentOutput = event.patch.recentOutput
|
|
99
|
+
?? (event.patch.outputText ? compactOutputPreview(event.patch.outputText) : current.recentOutput);
|
|
65
100
|
if (event.patch.logs?.length) current.output = appendLine(current.output, event.patch.logs.join("\n"));
|
|
66
101
|
} else if (event.type === "appendLog") {
|
|
67
102
|
current.output = appendLine(current.output, event.chunk);
|
|
103
|
+
current.recentOutput = [...(current.recentOutput ?? []), event.chunk.trim()].filter(Boolean).slice(-8);
|
|
68
104
|
} else if (event.type === "complete") {
|
|
69
105
|
current.code = 0;
|
|
106
|
+
current.endedAt = Date.now();
|
|
107
|
+
current.currentTool = undefined;
|
|
108
|
+
current.currentToolArgs = undefined;
|
|
109
|
+
current.currentToolStartedAt = undefined;
|
|
70
110
|
current.output = event.patch?.outputText ?? event.patch?.summary ?? current.output;
|
|
111
|
+
current.recentOutput = event.patch?.recentOutput ?? current.recentOutput;
|
|
112
|
+
current.recentTools = mergeRecentTools(current.recentTools, event.patch?.recentTools);
|
|
113
|
+
current.toolCount = event.patch?.toolCount ?? current.toolCount;
|
|
114
|
+
current.sessionFile = event.patch?.sessionFile ?? current.sessionFile;
|
|
71
115
|
} else if (event.type === "block") {
|
|
72
116
|
current.code = 1;
|
|
117
|
+
current.endedAt = Date.now();
|
|
118
|
+
current.currentTool = undefined;
|
|
119
|
+
current.currentToolArgs = undefined;
|
|
120
|
+
current.currentToolStartedAt = undefined;
|
|
73
121
|
current.output = event.patch?.outputText ?? event.patch?.summary ?? current.output;
|
|
122
|
+
current.recentOutput = event.patch?.recentOutput ?? current.recentOutput;
|
|
123
|
+
current.recentTools = mergeRecentTools(current.recentTools, event.patch?.recentTools);
|
|
124
|
+
current.toolCount = event.patch?.toolCount ?? current.toolCount;
|
|
125
|
+
current.sessionFile = event.patch?.sessionFile ?? current.sessionFile;
|
|
74
126
|
if (event.patch?.logs?.length) current.stderr = event.patch.logs.join("\n");
|
|
75
127
|
}
|
|
76
128
|
emit();
|
|
@@ -1,21 +1,10 @@
|
|
|
1
|
-
import { renderSubagentResult, syncResultAnimation } from "pi-subagents/src/tui/render";
|
|
2
1
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
3
|
-
import
|
|
2
|
+
import { renderSubagentResult, syncResultAnimation, type Details } from "./pi-subagents-internal";
|
|
4
3
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
import type { TakomiDispatchResult } from "./dispatch";
|
|
4
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
7
5
|
import type { TakomiSubagentToolParams } from "./tool-runner";
|
|
8
6
|
|
|
9
|
-
type ToolResult =
|
|
10
|
-
content?: Array<{ type: string; text?: string }>;
|
|
11
|
-
details?: {
|
|
12
|
-
results?: TakomiDispatchResult[];
|
|
13
|
-
mode?: "single" | "parallel" | "chain";
|
|
14
|
-
agentScope?: string;
|
|
15
|
-
plan?: unknown;
|
|
16
|
-
};
|
|
17
|
-
isError?: boolean;
|
|
18
|
-
};
|
|
7
|
+
type ToolResult = AgentToolResult<Details>;
|
|
19
8
|
|
|
20
9
|
function taskList(params: TakomiSubagentToolParams): Array<{ agent: string; task: string }> {
|
|
21
10
|
if (params.chain?.length) return params.chain;
|
|
@@ -41,134 +30,7 @@ export function renderTakomiSubagentCall(params: TakomiSubagentToolParams, theme
|
|
|
41
30
|
);
|
|
42
31
|
}
|
|
43
32
|
|
|
44
|
-
function parseTakomiOutput(outputText: string) {
|
|
45
|
-
const rawLines = outputText.split(/\r?\n/);
|
|
46
|
-
const textLines: string[] = [];
|
|
47
|
-
const recentTools: Array<{ tool: string; args: string; endMs: number }> = [];
|
|
48
|
-
let currentTool: string | undefined;
|
|
49
|
-
let currentToolArgs: string | undefined;
|
|
50
|
-
|
|
51
|
-
for (const line of rawLines) {
|
|
52
|
-
if (!line.trim()) continue;
|
|
53
|
-
|
|
54
|
-
// 1. Check for tool lifecycle markers
|
|
55
|
-
if (line.startsWith("Tool start: ")) {
|
|
56
|
-
currentTool = line.replace("Tool start: ", "").trim();
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (line.startsWith("Tool complete: ") || line.startsWith("Tool failed: ")) {
|
|
60
|
-
const toolName = line.replace(/Tool (complete|failed): /, "").trim();
|
|
61
|
-
recentTools.push({ tool: toolName, args: "", endMs: Date.now() });
|
|
62
|
-
if (currentTool === toolName) {
|
|
63
|
-
currentTool = undefined;
|
|
64
|
-
currentToolArgs = undefined;
|
|
65
|
-
}
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 2. Handle JSON blobs (tool calls)
|
|
70
|
-
const jsonStartIdx = line.indexOf('{"');
|
|
71
|
-
if (jsonStartIdx !== -1) {
|
|
72
|
-
const beforeJson = line.substring(0, jsonStartIdx).trim();
|
|
73
|
-
if (beforeJson) textLines.push(beforeJson);
|
|
74
|
-
|
|
75
|
-
const jsonPart = line.substring(jsonStartIdx);
|
|
76
|
-
try {
|
|
77
|
-
const parsed = JSON.parse(jsonPart);
|
|
78
|
-
const toolName = parsed.tool || (parsed.command ? parsed.command.split(" ")[0] : undefined);
|
|
79
|
-
if (toolName) {
|
|
80
|
-
const args = parsed.args ? JSON.stringify(parsed.args) : (parsed.command || "");
|
|
81
|
-
currentTool = toolName;
|
|
82
|
-
currentToolArgs = args;
|
|
83
|
-
}
|
|
84
|
-
} catch (e) {
|
|
85
|
-
if (jsonPart.trim().length > 1) {
|
|
86
|
-
textLines.push(jsonPart);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// 3. Skip lone brackets
|
|
93
|
-
const trimmed = line.trim();
|
|
94
|
-
if (trimmed === "{" || trimmed === "}") continue;
|
|
95
|
-
|
|
96
|
-
// 4. Everything else is text (thoughts)
|
|
97
|
-
textLines.push(line);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return { textLines, recentTools, currentTool, currentToolArgs };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
33
|
export function renderTakomiSubagentResult(result: ToolResult, options: { expanded?: boolean; isPartial?: boolean }, theme: Theme, context: any) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (results.length === 0) {
|
|
108
|
-
const text = result.content?.find((item) => item.type === "text")?.text ?? "(no output)";
|
|
109
|
-
return new Text(text, 0, 0);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const mappedResults: SingleResult[] = results.map((r, i) => {
|
|
113
|
-
const isRunning = r.code === -1;
|
|
114
|
-
const outputText = r.output || r.stderr || "";
|
|
115
|
-
const { textLines, recentTools, currentTool, currentToolArgs } = parseTakomiOutput(outputText);
|
|
116
|
-
|
|
117
|
-
const recentOutput = textLines.slice(-5);
|
|
118
|
-
|
|
119
|
-
const progress: AgentProgress | undefined = isRunning ? {
|
|
120
|
-
index: i,
|
|
121
|
-
agent: r.agent,
|
|
122
|
-
status: "running",
|
|
123
|
-
task: r.task || "",
|
|
124
|
-
lastActivityAt: Date.now(),
|
|
125
|
-
currentTool,
|
|
126
|
-
currentToolArgs,
|
|
127
|
-
currentToolStartedAt: currentTool ? Date.now() : undefined,
|
|
128
|
-
recentTools,
|
|
129
|
-
recentOutput,
|
|
130
|
-
toolCount: recentTools.length + (currentTool ? 1 : 0),
|
|
131
|
-
tokens: 0,
|
|
132
|
-
durationMs: 0,
|
|
133
|
-
} : {
|
|
134
|
-
index: i,
|
|
135
|
-
agent: r.agent,
|
|
136
|
-
status: "completed", // Takomi results in mappedResults are usually final if code !== -1
|
|
137
|
-
task: r.task || "",
|
|
138
|
-
lastActivityAt: Date.now(),
|
|
139
|
-
recentTools,
|
|
140
|
-
recentOutput: [],
|
|
141
|
-
toolCount: recentTools.length,
|
|
142
|
-
tokens: 0,
|
|
143
|
-
durationMs: 0,
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
agent: r.agent,
|
|
148
|
-
task: r.task || "",
|
|
149
|
-
exitCode: isRunning ? 0 : r.code,
|
|
150
|
-
model: r.model,
|
|
151
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
152
|
-
progress,
|
|
153
|
-
truncation: isRunning ? undefined : {
|
|
154
|
-
text: textLines.join("\n").replace(/\n/g, " \n"),
|
|
155
|
-
truncated: false
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const mappedDetails: Details = {
|
|
161
|
-
mode: details?.mode ?? "single",
|
|
162
|
-
results: mappedResults,
|
|
163
|
-
chainAgents: details?.mode === "chain" ? results.map(r => r.agent) : undefined,
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const agentToolResult: AgentToolResult<Details> = {
|
|
167
|
-
content: result.content as any || [],
|
|
168
|
-
details: mappedDetails,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
syncResultAnimation(agentToolResult, context);
|
|
172
|
-
return renderSubagentResult(agentToolResult, { expanded: options.expanded ?? false }, theme);
|
|
34
|
+
syncResultAnimation(result, context);
|
|
35
|
+
return renderSubagentResult(result, { expanded: options.expanded ?? false }, theme);
|
|
173
36
|
}
|
|
174
|
-
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
createSubagentExecutor,
|
|
8
|
+
discoverPiAgents,
|
|
9
|
+
TEMP_ARTIFACTS_DIR,
|
|
10
|
+
type AgentConfig,
|
|
11
|
+
type AgentScope,
|
|
12
|
+
type Details,
|
|
13
|
+
type ExtensionConfig,
|
|
14
|
+
type SubagentParamsLike,
|
|
15
|
+
type SubagentState,
|
|
16
|
+
} from "./pi-subagents-internal";
|
|
17
|
+
import { resolveAgentName } from "./agent-aliases";
|
|
18
|
+
import type { TakomiSubagentToolParams, TakomiSubagentToolTask } from "./tool-runner";
|
|
19
|
+
|
|
20
|
+
type ToolUpdate = (partial: AgentToolResult<Details>) => void;
|
|
21
|
+
|
|
22
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
23
|
+
const NEVER_ABORT: AbortSignal = new AbortController().signal;
|
|
24
|
+
|
|
25
|
+
function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
26
|
+
if (parentSessionFile) {
|
|
27
|
+
const baseName = path.basename(parentSessionFile, ".jsonl");
|
|
28
|
+
return path.join(path.dirname(parentSessionFile), baseName);
|
|
29
|
+
}
|
|
30
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "takomi-subagent-session-"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function expandTilde(value: string): string {
|
|
34
|
+
return value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createState(): SubagentState {
|
|
38
|
+
return {
|
|
39
|
+
baseCwd: process.cwd(),
|
|
40
|
+
currentSessionId: null,
|
|
41
|
+
asyncJobs: new Map(),
|
|
42
|
+
foregroundRuns: new Map(),
|
|
43
|
+
foregroundControls: new Map(),
|
|
44
|
+
lastForegroundControlId: null,
|
|
45
|
+
pendingForegroundControlNotices: new Map(),
|
|
46
|
+
cleanupTimers: new Map(),
|
|
47
|
+
lastUiContext: null,
|
|
48
|
+
poller: null,
|
|
49
|
+
completionSeen: new Map(),
|
|
50
|
+
watcher: null,
|
|
51
|
+
watcherRestartTimer: null,
|
|
52
|
+
resultFileCoalescer: {
|
|
53
|
+
schedule: () => false,
|
|
54
|
+
clear: () => {},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveMode(params: TakomiSubagentToolParams): "single" | "parallel" | "chain" | undefined {
|
|
60
|
+
const hasChain = Boolean(params.chain?.length);
|
|
61
|
+
const hasParallel = Boolean(params.tasks?.length);
|
|
62
|
+
const hasSingle = Boolean(params.agent && params.task);
|
|
63
|
+
if (Number(hasChain) + Number(hasParallel) + Number(hasSingle) !== 1) return undefined;
|
|
64
|
+
return hasChain ? "chain" : hasParallel ? "parallel" : "single";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveTasks(params: TakomiSubagentToolParams): TakomiSubagentToolTask[] {
|
|
68
|
+
if (params.chain?.length) return params.chain;
|
|
69
|
+
if (params.tasks?.length) return params.tasks;
|
|
70
|
+
if (params.agent && params.task) {
|
|
71
|
+
return [{
|
|
72
|
+
agent: params.agent,
|
|
73
|
+
task: params.task,
|
|
74
|
+
workflow: params.workflow,
|
|
75
|
+
skills: params.skills,
|
|
76
|
+
model: params.model,
|
|
77
|
+
fallbackModels: params.fallbackModels,
|
|
78
|
+
thinking: params.thinking,
|
|
79
|
+
conversationId: params.conversationId,
|
|
80
|
+
cwd: params.cwd,
|
|
81
|
+
checklist: params.checklist,
|
|
82
|
+
}];
|
|
83
|
+
}
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeThinking(value: unknown): string | undefined {
|
|
88
|
+
return typeof value === "string" && (THINKING_LEVELS as readonly string[]).includes(value) ? value : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildTakomiTaskPrompt(task: TakomiSubagentToolTask): string {
|
|
92
|
+
const checklist = task.checklist?.length
|
|
93
|
+
? [
|
|
94
|
+
"Checklist:",
|
|
95
|
+
...task.checklist.map((item) => typeof item === "string" ? `- [ ] ${item}` : `- [${item.done ? "x" : " "}] ${item.text}`),
|
|
96
|
+
].join("\n")
|
|
97
|
+
: "";
|
|
98
|
+
const takomiContext = [
|
|
99
|
+
task.workflow ? `Takomi workflow: ${task.workflow}` : "",
|
|
100
|
+
task.skills?.length ? `Takomi skills/context overlays: ${task.skills.join(", ")}` : "",
|
|
101
|
+
checklist,
|
|
102
|
+
].filter(Boolean).join("\n\n");
|
|
103
|
+
|
|
104
|
+
return takomiContext ? `${takomiContext}\n\n${task.task}` : task.task;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function modelWithThinking(model: string | undefined, thinking: string | undefined): string | undefined {
|
|
108
|
+
const level = normalizeThinking(thinking);
|
|
109
|
+
if (!model || !level || level === "off") return model;
|
|
110
|
+
if (new RegExp(`:(${THINKING_LEVELS.join("|")})$`, "i").test(model)) return model;
|
|
111
|
+
return `${model}:${level}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function defaultChildExtensions(): string[] {
|
|
115
|
+
// Child runs must not auto-load every user/project extension because this repo
|
|
116
|
+
// currently has both global and project Takomi extensions, which causes tool
|
|
117
|
+
// name conflicts in children. But model providers such as oauth-router are
|
|
118
|
+
// extensions too, so we explicitly allow the provider extension through.
|
|
119
|
+
const roots = [
|
|
120
|
+
process.env.PI_AGENT_ROOT,
|
|
121
|
+
path.join(os.homedir(), ".pi", "agent"),
|
|
122
|
+
path.join(process.cwd(), ".pi"),
|
|
123
|
+
].filter((root): root is string => Boolean(root));
|
|
124
|
+
const candidates = roots.flatMap((root) => [
|
|
125
|
+
path.join(root, "extensions", "oauth-router", "index.ts"),
|
|
126
|
+
path.join(root, "extensions", "oauth-router", "index.js"),
|
|
127
|
+
]);
|
|
128
|
+
return candidates.filter((candidate) => fs.existsSync(candidate));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function withTakomiAgentDefaults(agent: AgentConfig): AgentConfig {
|
|
132
|
+
return {
|
|
133
|
+
...agent,
|
|
134
|
+
systemPromptMode: agent.systemPromptMode ?? "replace",
|
|
135
|
+
inheritProjectContext: agent.inheritProjectContext ?? true,
|
|
136
|
+
inheritSkills: agent.inheritSkills ?? false,
|
|
137
|
+
defaultContext: agent.defaultContext ?? "fresh",
|
|
138
|
+
extensions: [...new Set([...(agent.extensions ?? []), ...defaultChildExtensions()])],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function discoverUnifiedAgents(cwd: string, scope: AgentScope): { agents: AgentConfig[] } {
|
|
143
|
+
return { agents: discoverPiAgents(cwd, scope).agents.map(withTakomiAgentDefaults) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function agentNameSet(cwd: string): Set<string> {
|
|
147
|
+
return new Set(discoverUnifiedAgents(cwd, "both").agents.map((agent) => agent.name));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function mapSingleTask(task: TakomiSubagentToolTask, names: Set<string>) {
|
|
151
|
+
const resolvedAgent = resolveAgentName(task.agent, new Map([...names].map((name) => [name, { name } as any])));
|
|
152
|
+
return {
|
|
153
|
+
agent: resolvedAgent,
|
|
154
|
+
task: buildTakomiTaskPrompt({ ...task, agent: resolvedAgent }),
|
|
155
|
+
cwd: task.cwd,
|
|
156
|
+
model: modelWithThinking(task.model, task.thinking),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toSubagentParams(params: TakomiSubagentToolParams, rootCwd: string): SubagentParamsLike {
|
|
161
|
+
const mode = resolveMode(params);
|
|
162
|
+
const tasks = resolveTasks(params);
|
|
163
|
+
const names = agentNameSet(rootCwd);
|
|
164
|
+
if (!mode) throw new Error("Provide exactly one mode: agent/task, tasks, or chain.");
|
|
165
|
+
|
|
166
|
+
const base = {
|
|
167
|
+
agentScope: params.agentScope ?? "both",
|
|
168
|
+
cwd: rootCwd,
|
|
169
|
+
context: "fresh" as const,
|
|
170
|
+
async: false,
|
|
171
|
+
clarify: false,
|
|
172
|
+
includeProgress: true,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (mode === "single") {
|
|
176
|
+
const task = tasks[0]!;
|
|
177
|
+
const mapped = mapSingleTask(task, names);
|
|
178
|
+
return {
|
|
179
|
+
...base,
|
|
180
|
+
agent: mapped.agent,
|
|
181
|
+
task: mapped.task,
|
|
182
|
+
cwd: task.cwd ? path.resolve(rootCwd, task.cwd) : rootCwd,
|
|
183
|
+
model: mapped.model,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (mode === "parallel") {
|
|
188
|
+
return {
|
|
189
|
+
...base,
|
|
190
|
+
tasks: tasks.map((task) => mapSingleTask(task, names)),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...base,
|
|
196
|
+
chain: tasks.map((task) => {
|
|
197
|
+
const mapped = mapSingleTask(task, names);
|
|
198
|
+
return {
|
|
199
|
+
agent: mapped.agent,
|
|
200
|
+
task: mapped.task,
|
|
201
|
+
cwd: task.cwd,
|
|
202
|
+
model: mapped.model,
|
|
203
|
+
};
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createTakomiPiSubagentsEngine(pi: ExtensionAPI) {
|
|
209
|
+
const state = createState();
|
|
210
|
+
const config: ExtensionConfig = {
|
|
211
|
+
maxSubagentDepth: 2,
|
|
212
|
+
asyncByDefault: false,
|
|
213
|
+
forceTopLevelAsync: false,
|
|
214
|
+
};
|
|
215
|
+
const executor = createSubagentExecutor({
|
|
216
|
+
pi,
|
|
217
|
+
state,
|
|
218
|
+
config,
|
|
219
|
+
asyncByDefault: false,
|
|
220
|
+
tempArtifactsDir: TEMP_ARTIFACTS_DIR,
|
|
221
|
+
getSubagentSessionRoot,
|
|
222
|
+
expandTilde,
|
|
223
|
+
discoverAgents: discoverUnifiedAgents,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
async execute(
|
|
228
|
+
id: string,
|
|
229
|
+
params: TakomiSubagentToolParams,
|
|
230
|
+
signal: AbortSignal | undefined,
|
|
231
|
+
onUpdate: ToolUpdate | undefined,
|
|
232
|
+
ctx: ExtensionContext,
|
|
233
|
+
): Promise<AgentToolResult<Details>> {
|
|
234
|
+
const rootCwd = params.cwd ? path.resolve(ctx.cwd, params.cwd) : ctx.cwd;
|
|
235
|
+
const subagentParams = toSubagentParams(params, rootCwd);
|
|
236
|
+
return executor.execute(id, subagentParams, signal ?? NEVER_ABORT, onUpdate, ctx);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Centralizes Takomi's current pi-subagents internal imports.
|
|
2
|
+
// pi-subagents does not yet expose all of these helpers through a stable public API,
|
|
3
|
+
// so package.json pins the dependency exactly and this adapter localizes future
|
|
4
|
+
// upstream path changes to one file.
|
|
5
|
+
export { createSubagentExecutor } from "pi-subagents/src/runs/foreground/subagent-executor";
|
|
6
|
+
export type { SubagentParamsLike } from "pi-subagents/src/runs/foreground/subagent-executor";
|
|
7
|
+
export { discoverAgents as discoverPiAgents } from "pi-subagents/src/agents/agents";
|
|
8
|
+
export type { AgentConfig, AgentScope } from "pi-subagents/src/agents/agents";
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_ARTIFACT_CONFIG,
|
|
11
|
+
TEMP_ARTIFACTS_DIR,
|
|
12
|
+
} from "pi-subagents/src/shared/types";
|
|
13
|
+
export type {
|
|
14
|
+
Details,
|
|
15
|
+
ExtensionConfig,
|
|
16
|
+
SubagentState,
|
|
17
|
+
} from "pi-subagents/src/shared/types";
|
|
18
|
+
export { renderSubagentResult, syncResultAnimation } from "pi-subagents/src/tui/render";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takomi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
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": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"commander": "^13.1.0",
|
|
42
42
|
"figlet": "^1.8.0",
|
|
43
43
|
"fs-extra": "^11.3.0",
|
|
44
|
-
"pi-subagents": "
|
|
44
|
+
"pi-subagents": "0.24.0",
|
|
45
45
|
"picocolors": "^1.1.1",
|
|
46
46
|
"prompts": "^2.4.2"
|
|
47
47
|
},
|
package/src/cli.js
CHANGED
|
@@ -44,6 +44,7 @@ import { runDoctor } from './doctor.js';
|
|
|
44
44
|
import { ensurePiInstalled, ensurePiSubagentsInstalled, launchTakomiHarness, printPiInstallResult, printPiSubagentsInstallResult } from './pi-harness.js';
|
|
45
45
|
import { installPiHarnessAssets, printPiInstallSummary, syncPiHarnessAssets, validatePiHarnessInstall } from './pi-installer.js';
|
|
46
46
|
import { installBundledSkills, printSkillsInstallSummary, validateSkillsInstall } from './skills-installer.js';
|
|
47
|
+
import { notifyIfTakomiUpdateAvailable, printTakomiUpdateStatus, upgradeTakomiPackage } from './update-check.js';
|
|
47
48
|
|
|
48
49
|
const packageJson = await fs.readJson(PATHS.packageJson);
|
|
49
50
|
const program = new Command();
|
|
@@ -713,6 +714,18 @@ program
|
|
|
713
714
|
.description('Run Pi/Takomi installation diagnostics')
|
|
714
715
|
.action(() => runDoctor({ version: program.version() }));
|
|
715
716
|
|
|
717
|
+
program
|
|
718
|
+
.command('check-update')
|
|
719
|
+
.description('Check whether a newer Takomi package is available')
|
|
720
|
+
.action(() => printTakomiUpdateStatus(program.version()));
|
|
721
|
+
|
|
722
|
+
program
|
|
723
|
+
.command('upgrade')
|
|
724
|
+
.description('Manually update the global Takomi CLI package from npm')
|
|
725
|
+
.action(() => {
|
|
726
|
+
process.exitCode = upgradeTakomiPackage();
|
|
727
|
+
});
|
|
728
|
+
|
|
716
729
|
// Update from GitHub (EXISTING — enhanced)
|
|
717
730
|
program
|
|
718
731
|
.command('update')
|
|
@@ -783,6 +796,7 @@ program
|
|
|
783
796
|
});
|
|
784
797
|
|
|
785
798
|
if (process.argv.length <= 2) {
|
|
799
|
+
notifyIfTakomiUpdateAvailable(program.version());
|
|
786
800
|
const exitCode = await launchTakomiHarness(process.cwd());
|
|
787
801
|
process.exitCode = exitCode;
|
|
788
802
|
} else {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
|
|
8
|
+
const CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
9
|
+
const CACHE_PATH = path.join(os.homedir(), '.takomi', 'update-check.json');
|
|
10
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/takomi/latest';
|
|
11
|
+
|
|
12
|
+
function parseVersion(version = '') {
|
|
13
|
+
const [core] = String(version).replace(/^v/, '').split('-');
|
|
14
|
+
return core.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isNewerVersion(latest, current) {
|
|
18
|
+
const a = parseVersion(latest);
|
|
19
|
+
const b = parseVersion(current);
|
|
20
|
+
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
21
|
+
const left = a[i] || 0;
|
|
22
|
+
const right = b[i] || 0;
|
|
23
|
+
if (left > right) return true;
|
|
24
|
+
if (left < right) return false;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fetchLatestPackageInfo(timeoutMs = 2500) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const req = https.get(REGISTRY_URL, {
|
|
32
|
+
headers: {
|
|
33
|
+
'accept': 'application/json',
|
|
34
|
+
'user-agent': 'takomi-update-check',
|
|
35
|
+
},
|
|
36
|
+
timeout: timeoutMs,
|
|
37
|
+
}, (res) => {
|
|
38
|
+
let body = '';
|
|
39
|
+
res.setEncoding('utf8');
|
|
40
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
41
|
+
res.on('end', () => {
|
|
42
|
+
if (res.statusCode && res.statusCode >= 400) return resolve(null);
|
|
43
|
+
try {
|
|
44
|
+
resolve(JSON.parse(body));
|
|
45
|
+
} catch {
|
|
46
|
+
resolve(null);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
req.on('timeout', () => {
|
|
51
|
+
req.destroy();
|
|
52
|
+
resolve(null);
|
|
53
|
+
});
|
|
54
|
+
req.on('error', () => resolve(null));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readCache() {
|
|
59
|
+
try {
|
|
60
|
+
return await fs.readJson(CACHE_PATH);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function writeCache(cache) {
|
|
67
|
+
try {
|
|
68
|
+
await fs.ensureDir(path.dirname(CACHE_PATH));
|
|
69
|
+
await fs.writeJson(CACHE_PATH, cache, { spaces: 2 });
|
|
70
|
+
} catch {
|
|
71
|
+
// Update checks must never block normal Takomi startup.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getLatestTakomiVersion({ currentVersion, force = false } = {}) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const cache = await readCache();
|
|
78
|
+
if (!force && cache?.checkedAt && cache.latestVersion && now - cache.checkedAt < CHECK_INTERVAL_MS) {
|
|
79
|
+
return {
|
|
80
|
+
...cache,
|
|
81
|
+
currentVersion,
|
|
82
|
+
updateAvailable: Boolean(currentVersion && isNewerVersion(cache.latestVersion, currentVersion)),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const info = await fetchLatestPackageInfo();
|
|
87
|
+
const latestVersion = typeof info?.version === 'string' ? info.version : null;
|
|
88
|
+
const next = {
|
|
89
|
+
checkedAt: now,
|
|
90
|
+
currentVersion,
|
|
91
|
+
latestVersion,
|
|
92
|
+
updateAvailable: Boolean(latestVersion && currentVersion && isNewerVersion(latestVersion, currentVersion)),
|
|
93
|
+
};
|
|
94
|
+
await writeCache(next);
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function notifyIfTakomiUpdateAvailable(currentVersion) {
|
|
99
|
+
if (process.env.TAKOMI_NO_UPDATE_CHECK === '1') return;
|
|
100
|
+
|
|
101
|
+
// Fire-and-forget by design: launching the Takomi harness must never wait on
|
|
102
|
+
// network, DNS, npm registry latency, or cache file IO.
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
getLatestTakomiVersion({ currentVersion })
|
|
105
|
+
.then((result) => {
|
|
106
|
+
if (!result?.updateAvailable) return;
|
|
107
|
+
console.log(pc.yellow(`\n⬆ Takomi ${result.latestVersion} is available (installed: ${currentVersion}).`));
|
|
108
|
+
console.log(pc.dim(' Run: takomi upgrade'));
|
|
109
|
+
console.log(pc.dim(' Disable this check with TAKOMI_NO_UPDATE_CHECK=1.\n'));
|
|
110
|
+
})
|
|
111
|
+
.catch(() => {
|
|
112
|
+
// Silent: update checks must never affect harness startup or usage.
|
|
113
|
+
});
|
|
114
|
+
}, 0).unref?.();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function printTakomiUpdateStatus(currentVersion) {
|
|
118
|
+
const result = await getLatestTakomiVersion({ currentVersion, force: true });
|
|
119
|
+
if (!result?.latestVersion) {
|
|
120
|
+
console.log(pc.yellow('Could not check the npm registry for Takomi updates.'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (result.updateAvailable) {
|
|
124
|
+
console.log(pc.yellow(`Takomi ${result.latestVersion} is available (installed: ${currentVersion}).`));
|
|
125
|
+
console.log(pc.dim('Run: takomi upgrade'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log(pc.green(`Takomi is up to date (${currentVersion}).`));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function upgradeTakomiPackage() {
|
|
132
|
+
console.log(pc.cyan('Updating Takomi from npm...\n'));
|
|
133
|
+
const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
134
|
+
const result = spawnSync(command, ['install', '-g', 'takomi@latest'], {
|
|
135
|
+
stdio: 'inherit',
|
|
136
|
+
shell: process.platform === 'win32',
|
|
137
|
+
});
|
|
138
|
+
if (result.status === 0) {
|
|
139
|
+
console.log(pc.green('\nTakomi updated. Run `takomi --version` to confirm.'));
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
console.log(pc.red('\nTakomi update failed. Try manually: npm install -g takomi@latest'));
|
|
143
|
+
return result.status || 1;
|
|
144
|
+
}
|