takomi 2.1.35 → 2.1.36
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 +68 -1
- package/.pi/extensions/takomi-runtime/ui.ts +3 -3
- package/.pi/extensions/takomi-subagents/index.ts +7 -1
- package/.pi/extensions/takomi-subagents/native-render.ts +81 -6
- package/.pi/extensions/takomi-subagents/tool-runner.ts +14 -2
- package/README.md +11 -7
- package/package.json +1 -1
- package/src/cli.js +96 -12
- package/src/harness.js +54 -32
- package/src/owned-tree.js +28 -0
- package/src/pi-harness.js +53 -24
- package/src/store.js +2 -0
|
@@ -92,6 +92,73 @@ let activeSubagentLabel: string | undefined;
|
|
|
92
92
|
let activeSubagentAgent: string | undefined;
|
|
93
93
|
let activeSubagentTask: string | undefined;
|
|
94
94
|
let activeSubagentStatus: string | undefined;
|
|
95
|
+
let delayedPiVersionCheckStarted = false;
|
|
96
|
+
|
|
97
|
+
const PI_LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
|
|
98
|
+
|
|
99
|
+
function parseVersionParts(version: string): number[] | undefined {
|
|
100
|
+
const core = version.trim().replace(/^v/, "").split("-")[0];
|
|
101
|
+
if (!/^\d+(?:\.\d+)*$/.test(core)) return undefined;
|
|
102
|
+
return core.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isNewerVersion(candidate: string, current: string): boolean {
|
|
106
|
+
const left = parseVersionParts(candidate);
|
|
107
|
+
const right = parseVersionParts(current);
|
|
108
|
+
if (!left || !right) return candidate.trim() !== current.trim();
|
|
109
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
110
|
+
const a = left[i] ?? 0;
|
|
111
|
+
const b = right[i] ?? 0;
|
|
112
|
+
if (a > b) return true;
|
|
113
|
+
if (a < b) return false;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function readCurrentPiVersion(): Promise<string | undefined> {
|
|
119
|
+
const candidates = [
|
|
120
|
+
path.resolve(path.dirname(process.argv[1] ?? ""), "..", "package.json"),
|
|
121
|
+
path.resolve(path.dirname(process.argv[1] ?? ""), "package.json"),
|
|
122
|
+
];
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(await readFile(candidate, "utf8")) as { name?: string; version?: string };
|
|
126
|
+
if (parsed.name === "@earendil-works/pi-coding-agent" && typeof parsed.version === "string") return parsed.version;
|
|
127
|
+
} catch { }
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function scheduleDelayedPiVersionCheck(ctx: ExtensionContext): void {
|
|
133
|
+
if (delayedPiVersionCheckStarted) return;
|
|
134
|
+
if (process.env.TAKOMI_DELAYED_PI_VERSION_CHECK !== "1") return;
|
|
135
|
+
if (process.env.TAKOMI_SKIP_DELAYED_PI_VERSION_CHECK === "1" || process.env.PI_OFFLINE === "1") return;
|
|
136
|
+
delayedPiVersionCheckStarted = true;
|
|
137
|
+
|
|
138
|
+
const delayMs = Math.max(0, Number(process.env.TAKOMI_PI_VERSION_CHECK_DELAY_MS || 3000) || 3000);
|
|
139
|
+
const timeoutMs = Math.max(500, Number(process.env.TAKOMI_PI_VERSION_CHECK_TIMEOUT_MS || 2500) || 2500);
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
void (async () => {
|
|
142
|
+
try {
|
|
143
|
+
const currentVersion = await readCurrentPiVersion();
|
|
144
|
+
if (!currentVersion) return;
|
|
145
|
+
const response = await fetch(PI_LATEST_VERSION_URL, {
|
|
146
|
+
headers: { accept: "application/json", "user-agent": `takomi-delayed-pi-version-check/${currentVersion}` },
|
|
147
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok) return;
|
|
150
|
+
const data = await response.json() as { version?: unknown; note?: unknown };
|
|
151
|
+
if (typeof data.version !== "string" || !isNewerVersion(data.version, currentVersion)) return;
|
|
152
|
+
if (ctx.hasUI) {
|
|
153
|
+
const note = typeof data.note === "string" && data.note.trim() ? ` ${data.note.trim()}` : "";
|
|
154
|
+
ctx.ui.notify(`Pi ${data.version} is available (installed: ${currentVersion}). Run: takomi refresh pi.${note}`, "info");
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Best-effort only. Delayed version checks must never affect startup or usage.
|
|
158
|
+
}
|
|
159
|
+
})();
|
|
160
|
+
}, delayMs).unref?.();
|
|
161
|
+
}
|
|
95
162
|
|
|
96
163
|
const ThinkingSchema = Type.Union([
|
|
97
164
|
Type.Literal("off"),
|
|
@@ -821,7 +888,6 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
821
888
|
case "review":
|
|
822
889
|
state.autoOrch = false;
|
|
823
890
|
state.planMode = true;
|
|
824
|
-
state.launchMode = "manual";
|
|
825
891
|
state.role = "review";
|
|
826
892
|
state.stage = undefined;
|
|
827
893
|
state.workflow = undefined;
|
|
@@ -1365,6 +1431,7 @@ ${stateJson}`
|
|
|
1365
1431
|
|
|
1366
1432
|
pi.on("session_start", async (_event, ctx) => {
|
|
1367
1433
|
runtimeCtx = ctx;
|
|
1434
|
+
scheduleDelayedPiVersionCheck(ctx);
|
|
1368
1435
|
activeProfile = await loadTakomiProfile(ctx.cwd);
|
|
1369
1436
|
activeSubagentLabel = undefined;
|
|
1370
1437
|
activeSubagentAgent = undefined;
|
|
@@ -81,9 +81,9 @@ export function renderRuntimeStatus(theme: Theme, state: RuntimeHudState): strin
|
|
|
81
81
|
const sourceLabel = source === "manual" ? theme.fg("warning", "manual") : theme.fg("success", source);
|
|
82
82
|
const role = state.stage && state.role !== state.stage ? theme.fg("dim", state.role) : "";
|
|
83
83
|
const plan = state.planMode ? theme.fg("warning", "plan") : theme.fg("dim", "direct");
|
|
84
|
-
const gate = state.launchMode === "manual" ? theme.fg("warning", "review-gate") : "";
|
|
84
|
+
const gate = state.launchMode === "manual" ? theme.fg("warning", "review-gate") : theme.fg("success", "auto-gate");
|
|
85
85
|
const subagents = state.subagentsEnabled === false ? theme.fg("error", "subagents:off") : "";
|
|
86
|
-
return [theme.fg("accent", "Takomi"), sourceLabel, stageBadge, role, gate
|
|
86
|
+
return [theme.fg("accent", "Takomi"), sourceLabel, stageBadge, role, gate, plan, subagents].filter(Boolean).join(" ");
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export function renderRuntimeWidget(theme: Theme, state: RuntimeHudState): string[] {
|
|
@@ -95,7 +95,7 @@ export function renderRuntimeWidget(theme: Theme, state: RuntimeHudState): strin
|
|
|
95
95
|
sourceLabel,
|
|
96
96
|
badge(theme, primary, stageTone(state.stage)),
|
|
97
97
|
state.stage && state.role !== state.stage ? theme.fg("dim", `role:${state.role}`) : "",
|
|
98
|
-
state.launchMode === "manual" ? theme.fg("warning", "review-gate") : "",
|
|
98
|
+
state.launchMode === "manual" ? theme.fg("warning", "review-gate") : theme.fg("success", "auto-gate"),
|
|
99
99
|
state.planMode ? theme.fg("warning", "plan") : theme.fg("dim", "direct"),
|
|
100
100
|
state.subagentsEnabled === false ? theme.fg("error", "subagents:off") : theme.fg("dim", "subagents:on"),
|
|
101
101
|
state.workflow ? theme.fg("dim", `wf:${state.workflow}`) : "",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { renderTakomiSubagentCall, renderTakomiSubagentResult } from "./native-render";
|
|
4
|
+
import { loadPiSubagentsInternals } from "./pi-subagents-internal";
|
|
4
5
|
import { executeTakomiSubagentTool } from "./tool-runner";
|
|
5
6
|
|
|
6
7
|
const ChecklistItemSchema = Type.Object({
|
|
@@ -74,6 +75,11 @@ function registerSubagentTool(pi: ExtensionAPI): void {
|
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
export default function takomiSubagents(pi: ExtensionAPI) {
|
|
78
|
+
export default async function takomiSubagents(pi: ExtensionAPI) {
|
|
79
|
+
// Preload the native pi-subagents renderer before tool registration. Rendering
|
|
80
|
+
// callbacks are synchronous, so if we only lazy-load internals during execution
|
|
81
|
+
// a completed takomi_subagent result can fall back to Takomi's plain text
|
|
82
|
+
// renderer and lose the native compact/expanded details shown by `subagent`.
|
|
83
|
+
await loadPiSubagentsInternals();
|
|
78
84
|
registerSubagentTool(pi);
|
|
79
85
|
}
|
|
@@ -38,12 +38,16 @@ function resultText(result: ToolResult): string {
|
|
|
38
38
|
: JSON.stringify((result as any)?.details ?? {}, null, 2);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function
|
|
41
|
+
function extractPolicyNames(text: string): string[] {
|
|
42
42
|
const policyMatch = text.match(/Required policies:\n((?:- .+\n?)+)/);
|
|
43
|
-
|
|
43
|
+
return policyMatch?.[1]
|
|
44
44
|
?.split("\n")
|
|
45
45
|
.map((line) => line.replace(/^[-\s]+/, "").trim())
|
|
46
46
|
.filter(Boolean) ?? [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function summarizeCollapsedResult(text: string, status: string, theme: Theme): string {
|
|
50
|
+
const policies = extractPolicyNames(text);
|
|
47
51
|
const lineCount = text ? text.split(/\r?\n/).length : 0;
|
|
48
52
|
const label = policies.length > 0
|
|
49
53
|
? `policy context loaded: ${policies.join(", ")}`
|
|
@@ -53,22 +57,89 @@ function summarizeCollapsedResult(text: string, status: string, theme: Theme): s
|
|
|
53
57
|
return `${theme.fg(color, `${icon} takomi_subagent ${status}`)}${theme.fg("dim", ` · ${label} (ctrl+o to expand)`)}`;
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
function isPolicyGateBlock(text: string): boolean {
|
|
61
|
+
return /^Blocked\s+takomi_subagent:\s+required policy context had not been loaded yet\./m.test(text)
|
|
62
|
+
&& /\nRequired policies:\n/.test(text)
|
|
63
|
+
&& /\nLoaded policy context:\n/.test(text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderPolicyGateBlock(text: string, expanded: boolean | undefined, theme: Theme): string {
|
|
67
|
+
const policies = extractPolicyNames(text);
|
|
68
|
+
const lineCount = text ? text.split(/\r?\n/).length : 0;
|
|
69
|
+
const policyLabel = policies.length ? policies.join(", ") : "required policy";
|
|
70
|
+
if (!expanded) {
|
|
71
|
+
return [
|
|
72
|
+
theme.fg("warning", "⚠ takomi_subagent blocked"),
|
|
73
|
+
theme.fg("dim", `Required policy context loaded for this session: ${policyLabel}.`),
|
|
74
|
+
theme.fg("dim", `Retry the original tool call. ${lineCount} policy lines hidden (ctrl+o to expand).`),
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
return [
|
|
78
|
+
theme.fg("warning", "⚠ takomi_subagent blocked"),
|
|
79
|
+
theme.fg("dim", "Policy context was loaded and passed back to the model; retry the original call."),
|
|
80
|
+
theme.fg("dim", "Press ctrl+o again to collapse."),
|
|
81
|
+
"",
|
|
82
|
+
text,
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
function recentOutputLines(value: unknown): string[] {
|
|
57
87
|
if (Array.isArray(value)) return value.map(String).filter(Boolean).slice(-3);
|
|
58
88
|
if (typeof value === "string") return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-3);
|
|
59
89
|
return [];
|
|
60
90
|
}
|
|
61
91
|
|
|
92
|
+
function normalizeLiveStatus(value: string | undefined): string | undefined {
|
|
93
|
+
if (!value) return undefined;
|
|
94
|
+
if (value === "complete") return "completed";
|
|
95
|
+
if (value === "in-progress") return "running";
|
|
96
|
+
if (value === "timed-out") return "failed";
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function rawLiveStatus(row: any): string {
|
|
101
|
+
const explicit = normalizeLiveStatus(
|
|
102
|
+
typeof row?.progress?.status === "string" ? row.progress.status
|
|
103
|
+
: typeof row?.status === "string" ? row.status
|
|
104
|
+
: undefined,
|
|
105
|
+
);
|
|
106
|
+
if (explicit) return explicit;
|
|
107
|
+
|
|
108
|
+
const exitCode = typeof row?.exitCode === "number" ? row.exitCode
|
|
109
|
+
: typeof row?.code === "number" ? row.code
|
|
110
|
+
: undefined;
|
|
111
|
+
if (exitCode === 0) return "completed";
|
|
112
|
+
if (exitCode === undefined || exitCode === null || exitCode === -1) return "running";
|
|
113
|
+
return "failed";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isLiveTerminalStatus(status: string): boolean {
|
|
117
|
+
return ["completed", "failed", "blocked", "detached", "paused"].includes(status);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function displayLiveStatus(status: string, allRowsTerminal: boolean): string {
|
|
121
|
+
// Native pi-subagents can emit one last partial update after the child process
|
|
122
|
+
// reaches exitCode 0 but before the takomi_subagent tool result has actually
|
|
123
|
+
// settled. In that window the tool card is still partial/running, so showing a
|
|
124
|
+
// child as "completed" is misleading. Call it finalizing until Pi renders the
|
|
125
|
+
// real final result.
|
|
126
|
+
if (allRowsTerminal && status === "completed") return "finalizing";
|
|
127
|
+
return status;
|
|
128
|
+
}
|
|
129
|
+
|
|
62
130
|
function livePartialText(result: ToolResult, theme: Theme): string {
|
|
63
131
|
const details = (result as any)?.details ?? {};
|
|
64
132
|
const results = Array.isArray(details.results) ? details.results : [];
|
|
65
133
|
const progress = Array.isArray(details.progress) ? details.progress : [];
|
|
134
|
+
const rows = results.length ? results : progress;
|
|
135
|
+
const rawStatuses = rows.map(rawLiveStatus);
|
|
136
|
+
const allRowsTerminal = rows.length > 0 && rawStatuses.every(isLiveTerminalStatus);
|
|
137
|
+
const titleStatus = allRowsTerminal ? "finalizing" : "running";
|
|
66
138
|
const lines = [
|
|
67
|
-
theme.fg("toolTitle", theme.bold(
|
|
139
|
+
theme.fg("toolTitle", theme.bold(`takomi_subagent ${titleStatus}`)),
|
|
68
140
|
theme.fg("dim", "Live detail is bounded while streaming so manual scroll/ctrl+o does not jump on every token."),
|
|
69
141
|
];
|
|
70
142
|
|
|
71
|
-
const rows = results.length ? results : progress;
|
|
72
143
|
if (!rows.length) {
|
|
73
144
|
const text = resultText(result).split(/\r?\n/).find((line) => line.trim())?.trim();
|
|
74
145
|
lines.push(theme.fg("dim", text || "Waiting for subagent progress…"));
|
|
@@ -76,7 +147,7 @@ function livePartialText(result: ToolResult, theme: Theme): string {
|
|
|
76
147
|
}
|
|
77
148
|
|
|
78
149
|
rows.slice(0, 6).forEach((row: any, index: number) => {
|
|
79
|
-
const status =
|
|
150
|
+
const status = displayLiveStatus(rawStatuses[index] ?? rawLiveStatus(row), allRowsTerminal);
|
|
80
151
|
const agent = row.agent ?? `task ${index + 1}`;
|
|
81
152
|
const task = String(row.task ?? "").replace(/\s+/g, " ").trim();
|
|
82
153
|
const currentTool = row.currentTool ?? row.progress?.currentTool;
|
|
@@ -93,9 +164,13 @@ function livePartialText(result: ToolResult, theme: Theme): string {
|
|
|
93
164
|
}
|
|
94
165
|
|
|
95
166
|
export function renderTakomiSubagentResult(result: ToolResult, options: { expanded?: boolean; isPartial?: boolean }, theme: Theme, context: any): any {
|
|
96
|
-
const status = (result as any)?.isError ? "failed" : options.isPartial ? "running" : "completed";
|
|
167
|
+
const status = ((result as any)?.isError || context?.isError) ? "failed" : options.isPartial ? "running" : "completed";
|
|
97
168
|
const text = resultText(result);
|
|
98
169
|
|
|
170
|
+
if (isPolicyGateBlock(text)) {
|
|
171
|
+
return new Text(renderPolicyGateBlock(text, options.expanded, theme), 0, 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
99
174
|
if (options.isPartial) {
|
|
100
175
|
if (options.expanded) return new Text(livePartialText(result, theme), 0, 0);
|
|
101
176
|
return new Text(summarizeCollapsedResult(text, status, theme), 0, 0);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import type { TakomiThinkingLevel } from "../../../src/pi-takomi-core";
|
|
4
|
+
import type { TakomiLaunchMode, TakomiThinkingLevel } from "../../../src/pi-takomi-core";
|
|
5
5
|
import { loadTakomiProfile } from "../takomi-runtime/profile";
|
|
6
6
|
import { applyTakomiRoutingDefaults, loadTakomiModelRoutingSnapshot } from "../takomi-runtime/model-routing-defaults";
|
|
7
7
|
import { resolveAgentName } from "./agent-aliases";
|
|
@@ -192,6 +192,17 @@ function withNativeHardStop(result: any, hardStop: { reason: string; message: st
|
|
|
192
192
|
},
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
+
|
|
196
|
+
function readRuntimeLaunchMode(ctx: ExtensionContext): TakomiLaunchMode | undefined {
|
|
197
|
+
const entries = ctx.sessionManager.getEntries();
|
|
198
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
199
|
+
const entry = entries[i] as { type?: string; customType?: string; data?: { launchMode?: unknown } };
|
|
200
|
+
if (entry.type !== "custom" || entry.customType !== "takomi-runtime-state") continue;
|
|
201
|
+
if (entry.data?.launchMode === "manual" || entry.data?.launchMode === "auto") return entry.data.launchMode;
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
195
206
|
function resolveMode(params: TakomiSubagentToolParams): "single" | "parallel" | "chain" | undefined {
|
|
196
207
|
const hasChain = Boolean(params.chain?.length);
|
|
197
208
|
const hasParallel = Boolean(params.tasks?.length);
|
|
@@ -234,6 +245,7 @@ export async function executeTakomiSubagentTool(
|
|
|
234
245
|
return textResult(message, { results: [], agentScope: params.agentScope ?? "both" }, true);
|
|
235
246
|
}
|
|
236
247
|
const profile = await loadTakomiProfile(rootCwd);
|
|
248
|
+
const runtimeLaunchMode = readRuntimeLaunchMode(ctx);
|
|
237
249
|
const agentScope = params.agentScope ?? "both";
|
|
238
250
|
const agents = discoverTakomiAgents(rootCwd, agentScope);
|
|
239
251
|
const byName = new Map<string, TakomiAgentConfig>(agents.map((agent) => [agent.name, agent]));
|
|
@@ -290,7 +302,7 @@ export async function executeTakomiSubagentTool(
|
|
|
290
302
|
}
|
|
291
303
|
const plan = createTakomiDelegationPlan({
|
|
292
304
|
source: "takomi-tool",
|
|
293
|
-
launchMode: profile.launchMode ?? "auto",
|
|
305
|
+
launchMode: runtimeLaunchMode ?? profile.launchMode ?? "auto",
|
|
294
306
|
profile,
|
|
295
307
|
tasks: tasks.map((task, index) => ({
|
|
296
308
|
id: task.conversationId ?? `direct-${index + 1}`,
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Takomi now ships a Pi-native `takomi-context-manager` extension. It reduces prom
|
|
|
63
63
|
|
|
64
64
|
### Option A: Global Install (Best for Multi-IDE Users) ⭐
|
|
65
65
|
|
|
66
|
-
Install once. Use everywhere. Your skills follow you across
|
|
66
|
+
Install once. Use everywhere. Your skills follow you across detected AI harnesses (Antigravity, Claude Code, Codex, Cursor, Kilo Code, Pi/shared Agent Skills, Windsurf):
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
69
|
# Using pnpm
|
|
@@ -184,17 +184,21 @@ npx -y skills add https://github.com/JStaRFilms/VibeCode-Protocol-Suite --skill
|
|
|
184
184
|
|
|
185
185
|
**One install. Every IDE. Zero friction.**
|
|
186
186
|
|
|
187
|
-
Takomi v2.0 introduces the **Global Skills Router** — install skills once
|
|
187
|
+
Takomi v2.0 introduces the **Global Skills Router** — install skills once into `~/.takomi/`, and Takomi syncs them into each harness' current global skills directory. You can choose symlink/junction mode for one canonical copy, auto fallback, or plain copy mode. Works on Mac & Windows.
|
|
188
|
+
|
|
189
|
+
Note: `SKILL.md` is portable, but `~/.agents/skills` is **not** a universal global skills directory. Pi does load `~/.agents/skills`, and Takomi keeps that as the Pi/shared Agent Skills target. Other harnesses use their own global paths. See `docs/skills-harness-targets.md` for the current checked path table.
|
|
188
190
|
|
|
189
191
|
### Supported Harnesses
|
|
190
192
|
|
|
191
193
|
| Harness | Global Skills Path | Global Workflows Path |
|
|
192
194
|
|---|---|---|
|
|
193
|
-
| **Antigravity** | `~/.gemini/
|
|
194
|
-
| **
|
|
195
|
+
| **Antigravity** | `~/.gemini/config/skills/` | `~/.gemini/config/global_workflows/` |
|
|
196
|
+
| **Claude Code** | `~/.claude/skills/` | _(skills only)_ |
|
|
197
|
+
| **Codex** | `~/.codex/skills/` | _(skills only)_ |
|
|
198
|
+
| **Cursor** | `~/.cursor/skills/` | _(skills only)_ |
|
|
199
|
+
| **Kilo Code** | `~/.kilocode/skills/` | `~/.kilocode/workflows/` |
|
|
200
|
+
| **Pi / shared Agent Skills** | `~/.agents/skills/` | _(skills only)_ |
|
|
195
201
|
| **Windsurf** | `~/.codeium/windsurf/skills/` | `~/.codeium/windsurf/global_workflows/` |
|
|
196
|
-
| **Global agents-compatible CLIs** _(e.g., Codex, Gemini CLI)_ | `~/.agents/skills/` | _(skills only)_ |
|
|
197
|
-
| **Cursor** | `~/.cursor/skills/` | _(uses rules)_ |
|
|
198
202
|
|
|
199
203
|
### CLI Commands
|
|
200
204
|
|
|
@@ -505,7 +509,7 @@ Externally sourced skills and optional Pi packages retain credit to their upstre
|
|
|
505
509
|
- **Context7**: From [upstash/context7](https://github.com/upstash/context7) — fresh library documentation fetcher.
|
|
506
510
|
- **Audit Website**: From [squirrelscan/skills](https://github.com/squirrelscan/skills) — professional website auditor.
|
|
507
511
|
- **Convex Skills**: From [waynesutton/convexskills](https://github.com/waynesutton/convexskills) — complete Convex development suite including **Functions**, **Schema Validation**, **Realtime**, **Agents**, **File Storage**, **Migrations**, **HTTP Actions**, **Cron Jobs**, **Component Authoring**, **Best Practices**, **Security Audit**, **Security Check**, **Avoid Feature Creep**, and **Optimize Agent Context**.
|
|
508
|
-
- **
|
|
512
|
+
- **Anti-Gravity**: Custom skill for running Google's current Anti-Gravity CLI workflow for large-context review and analysis.
|
|
509
513
|
- **Google Stitch Skills**: From [google-labs-code/stitch-skills](https://github.com/google-labs-code/stitch-skills) — Design-to-code suite including **design-md**, **enhance-prompt**, **stitch-loop**, **react-components**, and **shadcn-ui**.
|
|
510
514
|
- **Jules**: From [sanjay3290/ai-skills](https://github.com/sanjay3290/ai-skills) — delegate coding tasks to Google Jules AI agent.
|
|
511
515
|
- **Subagent Execution**: Built on **[`pi-subagents`](https://github.com/nicobailon/pi-subagents)** by **Nico Bailon** — providing the underlying Pi extension for delegated subagent runs (result rendering, live progress, single/parallel/chain execution, session/artifact handling, and related subagent tooling), upon which Takomi adds its own lifecycle orchestration, model-routing policy, and workflow metadata.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takomi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.36",
|
|
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": {
|
package/src/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
downloadDirectoryFromGitHub,
|
|
24
24
|
} from './utils.js';
|
|
25
25
|
import {
|
|
26
|
+
HARNESS_MAP,
|
|
26
27
|
detectHarnesses,
|
|
27
28
|
printHarnessStatus,
|
|
28
29
|
syncToAllHarnesses,
|
|
@@ -393,6 +394,43 @@ async function promptSkillsInstallSelection() {
|
|
|
393
394
|
return { mode: response.mode, selectedSkills };
|
|
394
395
|
}
|
|
395
396
|
|
|
397
|
+
function normalizeHarnessSyncMode(value) {
|
|
398
|
+
return ['copy', 'symlink', 'auto'].includes(value) ? value : 'copy';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function promptHarnessSyncMode(initial = 'auto') {
|
|
402
|
+
const normalizedInitial = normalizeHarnessSyncMode(initial);
|
|
403
|
+
const choices = [
|
|
404
|
+
{ title: 'Auto (try symlink, copy if blocked)', value: 'auto', description: 'Best fallback when Windows permissions or filesystems block symlinks.' },
|
|
405
|
+
{ title: 'Symlink / Junction (single source of truth)', value: 'symlink', description: 'Each managed skill/workflow points back to ~/.takomi; edits update one canonical copy.' },
|
|
406
|
+
{ title: 'Copy (independent files)', value: 'copy', description: 'Most compatible; changes do not flow back to ~/.takomi.' },
|
|
407
|
+
];
|
|
408
|
+
const response = await prompts({
|
|
409
|
+
type: 'select',
|
|
410
|
+
name: 'syncMode',
|
|
411
|
+
message: 'How should Takomi sync store resources into harness folders?',
|
|
412
|
+
choices,
|
|
413
|
+
initial: Math.max(0, choices.findIndex((choice) => choice.value === normalizedInitial)),
|
|
414
|
+
});
|
|
415
|
+
if (!response.syncMode) return null;
|
|
416
|
+
return normalizeHarnessSyncMode(response.syncMode);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function collectSyncFailures(syncSummary) {
|
|
420
|
+
return Object.entries(syncSummary || {})
|
|
421
|
+
.filter(([, result]) => result?.ok === false)
|
|
422
|
+
.map(([id, result]) => ({ id, errors: result.errors?.length ? result.errors : ['unknown sync error'] }));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function printSyncFailures(failures) {
|
|
426
|
+
if (!failures.length) return;
|
|
427
|
+
console.log(pc.yellow('\nSome harnesses did not fully sync:'));
|
|
428
|
+
for (const failure of failures) {
|
|
429
|
+
console.log(pc.dim(` - ${failure.id}: ${failure.errors.join('; ')}`));
|
|
430
|
+
}
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
}
|
|
433
|
+
|
|
396
434
|
async function installSkillsTarget() {
|
|
397
435
|
console.log(pc.magenta('🧰 Takomi Skills Install\n'));
|
|
398
436
|
try {
|
|
@@ -411,7 +449,7 @@ async function installSkillsTarget() {
|
|
|
411
449
|
const result = await installBundledSkills(program.version(), selection);
|
|
412
450
|
const validation = await validateSkillsInstall(selection.selectedSkills);
|
|
413
451
|
printSkillsInstallSummary(result, validation);
|
|
414
|
-
console.log(pc.dim('\
|
|
452
|
+
console.log(pc.dim('\nShared skills target is ready. For per-harness global paths, run "takomi setup" or "takomi sync".\n'));
|
|
415
453
|
} catch (error) {
|
|
416
454
|
console.log(pc.red('\nSkills install failed.'));
|
|
417
455
|
console.log(pc.dim(String(error?.message || error)));
|
|
@@ -622,7 +660,7 @@ async function install(target) {
|
|
|
622
660
|
|
|
623
661
|
if (detected.length === 0) {
|
|
624
662
|
console.log(pc.yellow(' No AI harnesses detected on this machine.'));
|
|
625
|
-
console.log(pc.dim(
|
|
663
|
+
console.log(pc.dim(` We support: ${Object.values(HARNESS_MAP).map(h => h.name).join(', ')}`));
|
|
626
664
|
console.log(pc.dim(' Run "takomi init" instead for per-project setup.\n'));
|
|
627
665
|
return;
|
|
628
666
|
}
|
|
@@ -715,6 +753,11 @@ async function install(target) {
|
|
|
715
753
|
|
|
716
754
|
if (!contentResponse.content) return;
|
|
717
755
|
|
|
756
|
+
const syncMode = await promptHarnessSyncMode(
|
|
757
|
+
(hasExistingStoreSkills || hasExistingStoreWorkflows) ? existingStoreManifest.syncMode : 'auto'
|
|
758
|
+
);
|
|
759
|
+
if (!syncMode) return;
|
|
760
|
+
|
|
718
761
|
try {
|
|
719
762
|
// 4. Initialize global store
|
|
720
763
|
console.log(pc.cyan(`\n📦 Creating global store (${STORE_PATH})...\n`));
|
|
@@ -802,11 +845,15 @@ async function install(target) {
|
|
|
802
845
|
const syncSummary = await syncToAllHarnesses(selectedHarnesses, STORE_PATH, {
|
|
803
846
|
useOwnership: true,
|
|
804
847
|
owned: manifest.harnessOwned,
|
|
848
|
+
linkMode: syncMode,
|
|
805
849
|
});
|
|
806
850
|
|
|
807
851
|
// 7. Update manifest
|
|
852
|
+
const syncFailures = collectSyncFailures(syncSummary);
|
|
853
|
+
const successfulHarnesses = selectedHarnesses.filter(h => !syncFailures.some(failure => failure.id === h.id));
|
|
808
854
|
manifest = await getManifest();
|
|
809
|
-
manifest.linkedHarnesses =
|
|
855
|
+
manifest.linkedHarnesses = successfulHarnesses.map(h => h.id);
|
|
856
|
+
manifest.syncMode = syncMode;
|
|
810
857
|
manifest.installed.skills = await getStoreSkills();
|
|
811
858
|
manifest.installed.workflows = await getStoreWorkflows();
|
|
812
859
|
manifest.harnessOwned = manifest.harnessOwned || {};
|
|
@@ -814,11 +861,13 @@ async function install(target) {
|
|
|
814
861
|
manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
|
|
815
862
|
}
|
|
816
863
|
await writeManifest(manifest);
|
|
864
|
+
printSyncFailures(syncFailures);
|
|
817
865
|
|
|
818
866
|
// 8. Summary
|
|
819
|
-
console.log(pc.magenta('\n✨ Your command center is live. ✨'));
|
|
867
|
+
console.log(pc.magenta(syncFailures.length ? '\n⚠ Your command center is partially synced. ⚠' : '\n✨ Your command center is live. ✨'));
|
|
820
868
|
console.log(pc.white(`\n Store: ${STORE_PATH}`));
|
|
821
|
-
console.log(pc.white(`
|
|
869
|
+
console.log(pc.white(` Sync mode: ${syncMode}`));
|
|
870
|
+
console.log(pc.white(` Connected: ${successfulHarnesses.map(h => h.name).join(', ') || 'none'}`));
|
|
822
871
|
console.log(pc.dim(`\n Add skills: takomi add <github-url>`));
|
|
823
872
|
console.log(pc.dim(` Sync updates: takomi sync\n`));
|
|
824
873
|
|
|
@@ -882,23 +931,33 @@ async function sync(target) {
|
|
|
882
931
|
|
|
883
932
|
const selectedHarnesses = detected.filter(h => response.harnesses.includes(h.id));
|
|
884
933
|
|
|
885
|
-
|
|
934
|
+
const envSyncMode = process.env.TAKOMI_HARNESS_SYNC_MODE;
|
|
935
|
+
const syncMode = normalizeHarnessSyncMode(envSyncMode || manifest.syncMode || 'copy');
|
|
936
|
+
console.log(pc.cyan(`\n📡 Syncing from global store (${syncMode})...\n`));
|
|
886
937
|
const syncSummary = await syncToAllHarnesses(selectedHarnesses, STORE_PATH, {
|
|
887
938
|
useOwnership: true,
|
|
888
939
|
owned: manifest.harnessOwned,
|
|
940
|
+
linkMode: syncMode,
|
|
889
941
|
});
|
|
890
942
|
|
|
891
943
|
// Update manifest with current harnesses
|
|
892
|
-
|
|
944
|
+
const syncFailures = collectSyncFailures(syncSummary);
|
|
945
|
+
const successfulHarnessIds = response.harnesses.filter(id => !syncFailures.some(failure => failure.id === id));
|
|
946
|
+
manifest.linkedHarnesses = [...new Set([...manifest.linkedHarnesses, ...successfulHarnessIds])];
|
|
947
|
+
if (!envSyncMode) manifest.syncMode = syncMode;
|
|
893
948
|
manifest.harnessOwned = manifest.harnessOwned || {};
|
|
894
949
|
for (const harness of selectedHarnesses) {
|
|
895
950
|
manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
|
|
896
951
|
}
|
|
897
952
|
await writeManifest(manifest);
|
|
953
|
+
printSyncFailures(syncFailures);
|
|
898
954
|
|
|
899
955
|
const skills = await getStoreSkills();
|
|
900
956
|
const workflows = await getStoreWorkflows();
|
|
901
|
-
|
|
957
|
+
const syncedCount = successfulHarnessIds.length;
|
|
958
|
+
console.log(pc.magenta(syncFailures.length
|
|
959
|
+
? `\n⚠ ${skills.length} skills and ${workflows.length} workflows partially synced to ${syncedCount}/${selectedHarnesses.length} IDE(s).\n`
|
|
960
|
+
: `\n✨ ${skills.length} skills and ${workflows.length} workflows synced to ${syncedCount} IDE(s). Ready to build.\n`));
|
|
902
961
|
}
|
|
903
962
|
|
|
904
963
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -994,8 +1053,21 @@ async function add(url) {
|
|
|
994
1053
|
if (syncResponse.doSync) {
|
|
995
1054
|
const detected = detectHarnesses();
|
|
996
1055
|
const linked = detected.filter(h => manifest.linkedHarnesses.includes(h.id));
|
|
997
|
-
await syncToAllHarnesses(linked, STORE_PATH
|
|
998
|
-
|
|
1056
|
+
const syncSummary = await syncToAllHarnesses(linked, STORE_PATH, {
|
|
1057
|
+
useOwnership: true,
|
|
1058
|
+
owned: manifest.harnessOwned,
|
|
1059
|
+
linkMode: normalizeHarnessSyncMode(process.env.TAKOMI_HARNESS_SYNC_MODE || manifest.syncMode || 'copy'),
|
|
1060
|
+
});
|
|
1061
|
+
const syncFailures = collectSyncFailures(syncSummary);
|
|
1062
|
+
manifest.harnessOwned = manifest.harnessOwned || {};
|
|
1063
|
+
for (const harness of linked) {
|
|
1064
|
+
manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
|
|
1065
|
+
}
|
|
1066
|
+
await writeManifest(manifest);
|
|
1067
|
+
printSyncFailures(syncFailures);
|
|
1068
|
+
console.log(pc.magenta(syncFailures.length
|
|
1069
|
+
? `\n⚠ Live on ${linked.length - syncFailures.length}/${linked.length} IDE(s); some harnesses need attention.\n`
|
|
1070
|
+
: `\n✨ Live on ${linked.length} IDE(s). You're armed and ready.\n`));
|
|
999
1071
|
}
|
|
1000
1072
|
} else {
|
|
1001
1073
|
console.log(pc.dim('\n Run "takomi sync" when you want to push these to your IDEs.\n'));
|
|
@@ -1022,6 +1094,7 @@ async function harnesses() {
|
|
|
1022
1094
|
console.log(pc.white(` Location: ${STORE_PATH}`));
|
|
1023
1095
|
console.log(pc.white(` Skills: ${skills.length} ready to deploy`));
|
|
1024
1096
|
console.log(pc.white(` Workflows: ${workflows.length} available`));
|
|
1097
|
+
console.log(pc.white(` Sync mode: ${manifest.syncMode || 'copy'}`));
|
|
1025
1098
|
console.log(pc.white(` Connected: ${manifest.linkedHarnesses.join(', ') || 'none'}`));
|
|
1026
1099
|
console.log(pc.dim(` Updated: ${manifest.updatedAt}\n`));
|
|
1027
1100
|
} else {
|
|
@@ -1096,8 +1169,19 @@ async function updateProjectResources() {
|
|
|
1096
1169
|
const detected = detectHarnesses();
|
|
1097
1170
|
const linked = detected.filter(h => manifest.linkedHarnesses.includes(h.id));
|
|
1098
1171
|
if (linked.length > 0) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1172
|
+
const syncMode = normalizeHarnessSyncMode(process.env.TAKOMI_HARNESS_SYNC_MODE || manifest.syncMode || 'copy');
|
|
1173
|
+
console.log(pc.cyan(`\n📡 Auto-syncing to linked harnesses (${syncMode})...\n`));
|
|
1174
|
+
const syncSummary = await syncToAllHarnesses(linked, STORE_PATH, {
|
|
1175
|
+
useOwnership: true,
|
|
1176
|
+
owned: manifest.harnessOwned,
|
|
1177
|
+
linkMode: syncMode,
|
|
1178
|
+
});
|
|
1179
|
+
const syncFailures = collectSyncFailures(syncSummary);
|
|
1180
|
+
manifest.harnessOwned = manifest.harnessOwned || {};
|
|
1181
|
+
for (const harness of linked) {
|
|
1182
|
+
manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
|
|
1183
|
+
}
|
|
1184
|
+
printSyncFailures(syncFailures);
|
|
1101
1185
|
}
|
|
1102
1186
|
}
|
|
1103
1187
|
|
package/src/harness.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { hashPath, normalizeOwnedMap,
|
|
5
|
+
import { hashPath, normalizeOwnedMap, materializeOwnedTree } from './owned-tree.js';
|
|
6
6
|
|
|
7
7
|
// ─── Cross-Platform Home Directory ───────────────────────────────────────────
|
|
8
8
|
const HOME = os.homedir();
|
|
@@ -34,13 +34,26 @@ function getAppData() {
|
|
|
34
34
|
export const HARNESS_MAP = {
|
|
35
35
|
antigravity: {
|
|
36
36
|
name: 'Antigravity',
|
|
37
|
-
rootPath: path.join(HOME, '.gemini', '
|
|
37
|
+
rootPath: path.join(HOME, '.gemini', 'config'),
|
|
38
38
|
detect() {
|
|
39
39
|
return fs.existsSync(this.rootPath);
|
|
40
40
|
},
|
|
41
41
|
targets: {
|
|
42
|
-
skills: path.join(HOME, '.gemini', '
|
|
43
|
-
workflows: path.join(HOME, '.gemini', '
|
|
42
|
+
skills: path.join(HOME, '.gemini', 'config', 'skills'),
|
|
43
|
+
workflows: path.join(HOME, '.gemini', 'config', 'global_workflows'),
|
|
44
|
+
yamls: null,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
claude_code: {
|
|
49
|
+
name: 'Claude Code',
|
|
50
|
+
rootPath: path.join(HOME, '.claude'),
|
|
51
|
+
detect() {
|
|
52
|
+
return fs.existsSync(this.rootPath);
|
|
53
|
+
},
|
|
54
|
+
targets: {
|
|
55
|
+
skills: path.join(HOME, '.claude', 'skills'),
|
|
56
|
+
workflows: null,
|
|
44
57
|
yamls: null,
|
|
45
58
|
},
|
|
46
59
|
},
|
|
@@ -78,7 +91,20 @@ export const HARNESS_MAP = {
|
|
|
78
91
|
name: 'Codex',
|
|
79
92
|
rootPath: path.join(HOME, '.codex'),
|
|
80
93
|
detect() {
|
|
81
|
-
return fs.existsSync(this.rootPath);
|
|
94
|
+
return fs.existsSync(this.rootPath) || fs.existsSync('/etc/codex');
|
|
95
|
+
},
|
|
96
|
+
targets: {
|
|
97
|
+
skills: path.join(HOME, '.codex', 'skills'),
|
|
98
|
+
workflows: null,
|
|
99
|
+
yamls: null,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
pi: {
|
|
104
|
+
name: 'Pi / shared Agent Skills',
|
|
105
|
+
rootPath: path.join(HOME, '.pi'),
|
|
106
|
+
detect() {
|
|
107
|
+
return fs.existsSync(this.rootPath) || fs.existsSync(path.join(HOME, '.agents'));
|
|
82
108
|
},
|
|
83
109
|
targets: {
|
|
84
110
|
skills: path.join(HOME, '.agents', 'skills'),
|
|
@@ -100,19 +126,7 @@ export const HARNESS_MAP = {
|
|
|
100
126
|
},
|
|
101
127
|
},
|
|
102
128
|
|
|
103
|
-
|
|
104
|
-
name: 'Gemini CLI',
|
|
105
|
-
rootPath: path.join(HOME, '.gemini'),
|
|
106
|
-
detect() {
|
|
107
|
-
// Only match bare Gemini CLI — not Antigravity (which also lives under .gemini)
|
|
108
|
-
return fs.existsSync(this.rootPath) && !fs.existsSync(path.join(HOME, '.gemini', 'antigravity'));
|
|
109
|
-
},
|
|
110
|
-
targets: {
|
|
111
|
-
skills: path.join(HOME, '.agents', 'skills'),
|
|
112
|
-
workflows: null,
|
|
113
|
-
yamls: null,
|
|
114
|
-
},
|
|
115
|
-
},
|
|
129
|
+
|
|
116
130
|
};
|
|
117
131
|
|
|
118
132
|
// ─── Detection ───────────────────────────────────────────────────────────────
|
|
@@ -163,17 +177,14 @@ export function printHarnessStatus(detected) {
|
|
|
163
177
|
|
|
164
178
|
/**
|
|
165
179
|
* Syncs a source directory to a harness target path.
|
|
166
|
-
*
|
|
180
|
+
* Supports copy, symlink/junction, or auto-fallback materialization.
|
|
181
|
+
* When ownership is supplied, it also prunes previous Takomi-owned items
|
|
182
|
+
* that are no longer in the source while preserving manual or modified files.
|
|
167
183
|
*
|
|
168
184
|
* @param {string} sourcePath - Source directory (e.g., ~/.takomi/skills/)
|
|
169
|
-
* @param {string} targetPath - Destination directory (e.g., ~/.gemini/
|
|
185
|
+
* @param {string} targetPath - Destination directory (e.g., ~/.gemini/config/skills/)
|
|
170
186
|
* @param {string} label - Human-readable label for logging
|
|
171
|
-
* @returns {Promise<number>} - Number of items
|
|
172
|
-
*/
|
|
173
|
-
/**
|
|
174
|
-
* Syncs a directory to a target path. When ownership is supplied, it also
|
|
175
|
-
* prunes previous Takomi-owned items that are no longer in the source while
|
|
176
|
-
* preserving manual or modified files.
|
|
187
|
+
* @returns {Promise<number|object>} - Number of items materialized, or details
|
|
177
188
|
*/
|
|
178
189
|
export async function syncDirectory(sourcePath, targetPath, label = '', options = {}) {
|
|
179
190
|
if (!targetPath) return options.returnDetails ? { copied: 0, pruned: 0, preservedManual: [], preservedModified: [], owned: {} } : 0;
|
|
@@ -187,7 +198,10 @@ export async function syncDirectory(sourcePath, targetPath, label = '', options
|
|
|
187
198
|
const nextOwned = {};
|
|
188
199
|
const preservedManual = [];
|
|
189
200
|
const preservedModified = [];
|
|
201
|
+
const symlinkFallbacks = [];
|
|
202
|
+
const linkMode = options.linkMode || process.env.TAKOMI_HARNESS_SYNC_MODE || 'copy';
|
|
190
203
|
let copied = 0;
|
|
204
|
+
let linked = 0;
|
|
191
205
|
let pruned = 0;
|
|
192
206
|
|
|
193
207
|
if (options.prune && Object.keys(previousOwned).length > 0) {
|
|
@@ -225,29 +239,35 @@ export async function syncDirectory(sourcePath, targetPath, label = '', options
|
|
|
225
239
|
await fs.remove(dest);
|
|
226
240
|
}
|
|
227
241
|
|
|
228
|
-
await
|
|
242
|
+
const materialized = await materializeOwnedTree(src, dest, { linkMode });
|
|
229
243
|
nextOwned[item] = {
|
|
230
244
|
hash: await hashPath(dest),
|
|
231
245
|
targetPath: dest,
|
|
246
|
+
sourcePath: path.resolve(src),
|
|
247
|
+
syncMethod: materialized.method,
|
|
232
248
|
syncedAt: new Date().toISOString(),
|
|
233
249
|
};
|
|
234
|
-
|
|
250
|
+
if (materialized.method === 'symlink') linked++;
|
|
251
|
+
else copied++;
|
|
252
|
+
if (materialized.fallbackError) symlinkFallbacks.push(`${item}: ${materialized.fallbackError}`);
|
|
235
253
|
}
|
|
236
254
|
|
|
237
255
|
if (label) {
|
|
238
256
|
const suffix = pruned ? `, removed ${pruned}` : '';
|
|
239
|
-
|
|
257
|
+
const methodSummary = linked ? `${linked} linked, ${copied} copied` : `${copied} copied`;
|
|
258
|
+
console.log(pc.green(` ✔ ${label} (${methodSummary}${suffix})`));
|
|
240
259
|
if (preservedManual.length) console.log(pc.yellow(` Preserved manual items: ${preservedManual.join(', ')}`));
|
|
241
260
|
if (preservedModified.length) console.log(pc.yellow(` Preserved modified Takomi-owned items: ${preservedModified.join(', ')}`));
|
|
261
|
+
if (symlinkFallbacks.length) console.log(pc.yellow(` Symlink fallback copies: ${symlinkFallbacks.join('; ')}`));
|
|
242
262
|
}
|
|
243
263
|
|
|
244
|
-
const details = { copied, pruned, preservedManual, preservedModified, owned: Object.fromEntries(Object.entries(nextOwned).sort(([a], [b]) => a.localeCompare(b))) };
|
|
245
|
-
return options.returnDetails ? details : copied;
|
|
264
|
+
const details = { copied: copied + linked, linked, copyCount: copied, pruned, preservedManual, preservedModified, symlinkFallbacks, owned: Object.fromEntries(Object.entries(nextOwned).sort(([a], [b]) => a.localeCompare(b))) };
|
|
265
|
+
return options.returnDetails ? details : copied + linked;
|
|
246
266
|
} catch (error) {
|
|
247
267
|
if (label) {
|
|
248
268
|
console.log(pc.red(` ✗ ${label}: ${error.message}`));
|
|
249
269
|
}
|
|
250
|
-
return options.returnDetails ? { copied: 0, pruned: 0, preservedManual: [], preservedModified: [], owned: normalizeOwnedMap(options.owned), ok: false, error: error.message } : 0;
|
|
270
|
+
return options.returnDetails ? { copied: 0, linked: 0, copyCount: 0, pruned: 0, preservedManual: [], preservedModified: [], symlinkFallbacks: [], owned: normalizeOwnedMap(options.owned), ok: false, error: error.message } : 0;
|
|
251
271
|
}
|
|
252
272
|
}
|
|
253
273
|
|
|
@@ -309,6 +329,7 @@ export async function syncToHarness(harness, storePath, options = {}) {
|
|
|
309
329
|
preserveManual: useOwnership,
|
|
310
330
|
prune: useOwnership,
|
|
311
331
|
returnDetails: useOwnership,
|
|
332
|
+
linkMode: options.linkMode,
|
|
312
333
|
},
|
|
313
334
|
);
|
|
314
335
|
if (useOwnership) {
|
|
@@ -335,6 +356,7 @@ export async function syncToHarness(harness, storePath, options = {}) {
|
|
|
335
356
|
preserveManual: useOwnership,
|
|
336
357
|
prune: useOwnership,
|
|
337
358
|
returnDetails: useOwnership,
|
|
359
|
+
linkMode: options.linkMode,
|
|
338
360
|
},
|
|
339
361
|
);
|
|
340
362
|
if (useOwnership) {
|
package/src/owned-tree.js
CHANGED
|
@@ -84,3 +84,31 @@ export async function copyOwnedTree(src, dest, options = {}) {
|
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
export async function linkOwnedTree(src, dest) {
|
|
89
|
+
const resolvedSrc = path.resolve(src);
|
|
90
|
+
const stat = await fs.lstat(resolvedSrc);
|
|
91
|
+
const type = stat.isDirectory()
|
|
92
|
+
? (process.platform === 'win32' ? 'junction' : 'dir')
|
|
93
|
+
: 'file';
|
|
94
|
+
await fs.ensureDir(path.dirname(dest));
|
|
95
|
+
await fs.symlink(resolvedSrc, dest, type);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function materializeOwnedTree(src, dest, options = {}) {
|
|
99
|
+
const linkMode = options.linkMode || 'copy';
|
|
100
|
+
if (linkMode === 'symlink' || linkMode === 'auto') {
|
|
101
|
+
try {
|
|
102
|
+
await linkOwnedTree(src, dest);
|
|
103
|
+
return { method: 'symlink' };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (linkMode === 'symlink') throw error;
|
|
106
|
+
await fs.remove(dest).catch(() => {});
|
|
107
|
+
await copyOwnedTree(src, dest, options.copyOptions || {});
|
|
108
|
+
return { method: 'copy', fallbackError: error.message };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await copyOwnedTree(src, dest, options.copyOptions || {});
|
|
113
|
+
return { method: 'copy' };
|
|
114
|
+
}
|
package/src/pi-harness.js
CHANGED
|
@@ -15,6 +15,28 @@ function getPathEntries() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function commandExists(command) {
|
|
18
|
+
const pathEntries = getPathEntries();
|
|
19
|
+
|
|
20
|
+
// Avoid shelling out on the hot launch path. On Windows, `where pi` is often
|
|
21
|
+
// ~100ms by itself; a direct PATH scan is much cheaper and good enough for
|
|
22
|
+
// npm/cmd shims. Keep `where`/`which` as a fallback for edge cases.
|
|
23
|
+
const manualCandidates = process.platform === 'win32'
|
|
24
|
+
? (() => {
|
|
25
|
+
const hasExtension = /\.(cmd|exe|bat|com)$/i.test(command);
|
|
26
|
+
const extensions = hasExtension
|
|
27
|
+
? ['']
|
|
28
|
+
: (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
29
|
+
.split(';')
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.sort((a, b) => (a.toLowerCase() === '.cmd' ? -1 : b.toLowerCase() === '.cmd' ? 1 : 0));
|
|
32
|
+
return pathEntries.flatMap((entry) => extensions.map((extension) => path.join(entry, `${command}${extension.toLowerCase()}`)));
|
|
33
|
+
})()
|
|
34
|
+
: pathEntries.map((entry) => path.join(entry, command));
|
|
35
|
+
|
|
36
|
+
for (const candidate of manualCandidates) {
|
|
37
|
+
if (fs.existsSync(candidate)) return { found: true, path: candidate };
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
const probe = process.platform === 'win32'
|
|
19
41
|
? spawnSync('where', [command], { stdio: 'pipe', encoding: 'utf8' })
|
|
20
42
|
: spawnSync('which', [command], { stdio: 'pipe', encoding: 'utf8' });
|
|
@@ -27,11 +49,6 @@ function commandExists(command) {
|
|
|
27
49
|
return { found: true, path: preferred || matches[0] || null };
|
|
28
50
|
}
|
|
29
51
|
|
|
30
|
-
for (const entry of getPathEntries()) {
|
|
31
|
-
const candidate = path.join(entry, process.platform === 'win32' ? `${command}.cmd` : command);
|
|
32
|
-
if (fs.existsSync(candidate)) return { found: true, path: candidate };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
52
|
return { found: false, path: null };
|
|
36
53
|
}
|
|
37
54
|
|
|
@@ -140,12 +157,17 @@ export async function inspectPiSubagentsDependency(home = HOME) {
|
|
|
140
157
|
};
|
|
141
158
|
}
|
|
142
159
|
|
|
143
|
-
export async function detectPiCommand() {
|
|
160
|
+
export async function detectPiCommand(options = {}) {
|
|
161
|
+
const { includeVersion = true } = options;
|
|
144
162
|
const binary = commandExists('pi');
|
|
145
163
|
if (!binary.found) {
|
|
146
164
|
return { installed: false, path: null, version: null };
|
|
147
165
|
}
|
|
148
166
|
|
|
167
|
+
if (!includeVersion) {
|
|
168
|
+
return { installed: true, path: binary.path, version: null };
|
|
169
|
+
}
|
|
170
|
+
|
|
149
171
|
const versionProbe = process.platform === 'win32'
|
|
150
172
|
? spawnSync('powershell', ['-NoProfile', '-Command', `& '${(binary.path || 'pi').replace(/'/g, "''")}' --version`], { stdio: 'pipe', encoding: 'utf8' })
|
|
151
173
|
: spawnSync(binary.path || 'pi', ['--version'], { stdio: 'pipe', encoding: 'utf8' });
|
|
@@ -381,7 +403,7 @@ export async function updatePiManagedPackages({ timeoutMs } = {}) {
|
|
|
381
403
|
return { ok: true, changed: false, report: 'Skipped Pi extension package update because TAKOMI_SKIP_PI_PACKAGE_UPDATE=1.' };
|
|
382
404
|
}
|
|
383
405
|
|
|
384
|
-
const pi = await detectPiCommand();
|
|
406
|
+
const pi = await detectPiCommand({ includeVersion: false });
|
|
385
407
|
if (!pi.installed) return { ok: false, changed: false, report: 'Pi is not installed.' };
|
|
386
408
|
|
|
387
409
|
// Only reconcile Pi-managed extension/package entries here. Plain `pi update`
|
|
@@ -458,14 +480,22 @@ function printFirstRunGuidance(reason) {
|
|
|
458
480
|
}
|
|
459
481
|
|
|
460
482
|
export async function launchTakomiHarness(cwd = process.cwd()) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
483
|
+
// Keep the common `takomi` path lean. A full environment inspection shells out
|
|
484
|
+
// to `pi --version`, which costs multiple seconds on Windows before Pi even
|
|
485
|
+
// starts. For launch we only need to know the command exists and the required
|
|
486
|
+
// global Takomi extensions are present; `takomi doctor` still performs the
|
|
487
|
+
// complete diagnostic check.
|
|
488
|
+
const [pi, installed] = await Promise.all([
|
|
489
|
+
detectPiCommand({ includeVersion: false }),
|
|
490
|
+
inspectInstalledTakomiPiHarness(),
|
|
491
|
+
]);
|
|
492
|
+
|
|
493
|
+
if (!pi.installed) {
|
|
464
494
|
printFirstRunGuidance('Pi is not installed yet.');
|
|
465
495
|
return 1;
|
|
466
496
|
}
|
|
467
497
|
|
|
468
|
-
if (!
|
|
498
|
+
if (!installed.runtimeInstalled || !installed.subagentsInstalled) {
|
|
469
499
|
printFirstRunGuidance('Takomi Pi harness is not fully installed yet.');
|
|
470
500
|
return 1;
|
|
471
501
|
}
|
|
@@ -473,22 +503,21 @@ export async function launchTakomiHarness(cwd = process.cwd()) {
|
|
|
473
503
|
const env = {
|
|
474
504
|
...process.env,
|
|
475
505
|
TAKOMI_HARNESS: '1',
|
|
506
|
+
// Avoid Pi's blocking startup version check on this wrapped launch path.
|
|
507
|
+
// Takomi runtime schedules its own UI-safe delayed check after session_start.
|
|
508
|
+
PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? '1',
|
|
509
|
+
TAKOMI_DELAYED_PI_VERSION_CHECK: process.env.TAKOMI_DELAYED_PI_VERSION_CHECK ?? '1',
|
|
476
510
|
};
|
|
477
511
|
|
|
478
512
|
return await new Promise((resolve) => {
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
cwd,
|
|
488
|
-
stdio: 'inherit',
|
|
489
|
-
env,
|
|
490
|
-
shell: false,
|
|
491
|
-
});
|
|
513
|
+
const resolved = resolveCommandForSpawn(pi.path || 'pi', []);
|
|
514
|
+
const child = spawn(resolved.command, resolved.args, {
|
|
515
|
+
cwd,
|
|
516
|
+
stdio: 'inherit',
|
|
517
|
+
env,
|
|
518
|
+
shell: false,
|
|
519
|
+
windowsHide: false,
|
|
520
|
+
});
|
|
492
521
|
|
|
493
522
|
child.on('close', (code) => resolve(code ?? 0));
|
|
494
523
|
child.on('error', () => resolve(1));
|
package/src/store.js
CHANGED
|
@@ -17,6 +17,7 @@ function createDefaultManifest() {
|
|
|
17
17
|
createdAt: new Date().toISOString(),
|
|
18
18
|
updatedAt: new Date().toISOString(),
|
|
19
19
|
linkedHarnesses: [],
|
|
20
|
+
syncMode: 'copy',
|
|
20
21
|
installed: {
|
|
21
22
|
skills: [],
|
|
22
23
|
workflows: [],
|
|
@@ -38,6 +39,7 @@ function normalizeManifest(manifest) {
|
|
|
38
39
|
workflows: normalizeOwnedMap(manifest?.bundledOwned?.workflows),
|
|
39
40
|
};
|
|
40
41
|
normalized.harnessOwned = manifest?.harnessOwned || {};
|
|
42
|
+
normalized.syncMode = manifest?.syncMode || 'copy';
|
|
41
43
|
return normalized;
|
|
42
44
|
}
|
|
43
45
|
|