takomi 2.1.34 → 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/context-panel.ts +37 -13
- package/.pi/extensions/takomi-runtime/index.ts +68 -1
- package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +35 -5
- package/.pi/extensions/takomi-runtime/routing-policy.ts +35 -7
- package/.pi/extensions/takomi-runtime/takomi-stats.js +13 -3
- package/.pi/extensions/takomi-runtime/ui.ts +3 -3
- package/.pi/extensions/takomi-subagents/index.ts +10 -4
- package/.pi/extensions/takomi-subagents/native-render.ts +127 -7
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +32 -9
- package/.pi/extensions/takomi-subagents/tool-runner.ts +75 -17
- package/README.md +11 -7
- package/assets/.agent/skills/photo-book-builder/SKILL.md +51 -7
- package/package.json +18 -2
- package/src/cli.js +137 -23
- package/src/harness.js +63 -71
- package/src/owned-tree.js +114 -0
- package/src/pi-harness.js +53 -24
- package/src/pi-installer.js +12 -36
- package/src/store.js +5 -40
- package/src/takomi-stats.js +14 -3
- package/src/utils.js +81 -44
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/pi-installer.js
CHANGED
|
@@ -1,47 +1,15 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import crypto from 'crypto';
|
|
5
4
|
import pc from 'picocolors';
|
|
6
5
|
import { PATHS } from './utils.js';
|
|
7
6
|
import { getPiGlobalTargets } from './pi-harness.js';
|
|
7
|
+
import { hashPath, copyOwnedTree } from './owned-tree.js';
|
|
8
8
|
|
|
9
9
|
const HOME = os.homedir();
|
|
10
10
|
const TAKOMI_HOME = path.join(HOME, '.takomi');
|
|
11
11
|
export const PI_MANIFEST_PATH = path.join(TAKOMI_HOME, 'pi-manifest.json');
|
|
12
12
|
|
|
13
|
-
function sha256(value) {
|
|
14
|
-
return crypto.createHash('sha256').update(value).digest('hex');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function hashPath(targetPath) {
|
|
18
|
-
if (!await fs.pathExists(targetPath)) return null;
|
|
19
|
-
const stat = await fs.stat(targetPath);
|
|
20
|
-
if (stat.isFile()) {
|
|
21
|
-
const buf = await fs.readFile(targetPath);
|
|
22
|
-
return sha256(buf);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const entries = [];
|
|
26
|
-
async function walk(dir, prefix = '') {
|
|
27
|
-
const names = (await fs.readdir(dir)).sort();
|
|
28
|
-
for (const name of names) {
|
|
29
|
-
const full = path.join(dir, name);
|
|
30
|
-
const rel = path.join(prefix, name).replace(/\\/g, '/');
|
|
31
|
-
const st = await fs.stat(full);
|
|
32
|
-
if (st.isDirectory()) {
|
|
33
|
-
entries.push(`dir:${rel}`);
|
|
34
|
-
await walk(full, rel);
|
|
35
|
-
} else {
|
|
36
|
-
const buf = await fs.readFile(full);
|
|
37
|
-
entries.push(`file:${rel}:${sha256(buf)}`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
await walk(targetPath);
|
|
42
|
-
return sha256(entries.join('\n'));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
13
|
async function pathIsSameSymlink(dest, src) {
|
|
46
14
|
try {
|
|
47
15
|
const stat = await fs.lstat(dest);
|
|
@@ -69,7 +37,7 @@ async function copyOwnedDirectory(src, dest) {
|
|
|
69
37
|
await fs.ensureDir(path.dirname(dest));
|
|
70
38
|
const mode = await prepareOwnedTarget(src, dest);
|
|
71
39
|
if (mode === 'symlink') return hashPath(src);
|
|
72
|
-
await
|
|
40
|
+
await copyOwnedTree(src, dest);
|
|
73
41
|
return hashPath(dest);
|
|
74
42
|
}
|
|
75
43
|
|
|
@@ -77,7 +45,7 @@ async function copyOwnedFile(src, dest) {
|
|
|
77
45
|
await fs.ensureDir(path.dirname(dest));
|
|
78
46
|
const mode = await prepareOwnedTarget(src, dest);
|
|
79
47
|
if (mode === 'symlink') return hashPath(src);
|
|
80
|
-
await
|
|
48
|
+
await copyOwnedTree(src, dest);
|
|
81
49
|
return hashPath(dest);
|
|
82
50
|
}
|
|
83
51
|
|
|
@@ -96,6 +64,8 @@ export async function writePiInstallManifest(manifest) {
|
|
|
96
64
|
export async function installPiHarnessAssets(version = 'unknown') {
|
|
97
65
|
const srcRoot = PATHS.pi;
|
|
98
66
|
const targets = getPiGlobalTargets();
|
|
67
|
+
const previousSettingsHash = await hashPath(targets.settings);
|
|
68
|
+
const previousRoutingPolicyHash = await hashPath(targets.routingPolicy);
|
|
99
69
|
|
|
100
70
|
const copied = {};
|
|
101
71
|
|
|
@@ -153,7 +123,9 @@ export async function installPiHarnessAssets(version = 'unknown') {
|
|
|
153
123
|
owned: copied,
|
|
154
124
|
preserved: {
|
|
155
125
|
settings: targets.settings,
|
|
126
|
+
settingsHash: previousSettingsHash,
|
|
156
127
|
routingPolicy: targets.routingPolicy,
|
|
128
|
+
routingPolicyHash: previousRoutingPolicyHash,
|
|
157
129
|
userStateDir: targets.takomi,
|
|
158
130
|
},
|
|
159
131
|
};
|
|
@@ -168,6 +140,9 @@ export async function syncPiHarnessAssets(version = 'unknown') {
|
|
|
168
140
|
|
|
169
141
|
export async function validatePiHarnessInstall() {
|
|
170
142
|
const targets = getPiGlobalTargets();
|
|
143
|
+
const manifest = await readPiInstallManifest();
|
|
144
|
+
const currentSettingsHash = await hashPath(targets.settings);
|
|
145
|
+
const currentRoutingPolicyHash = await hashPath(targets.routingPolicy);
|
|
171
146
|
return {
|
|
172
147
|
runtime: await fs.pathExists(path.join(targets.extensions, 'takomi-runtime')),
|
|
173
148
|
subagents: await fs.pathExists(path.join(targets.extensions, 'takomi-subagents')),
|
|
@@ -180,7 +155,8 @@ export async function validatePiHarnessInstall() {
|
|
|
180
155
|
readme: await fs.pathExists(path.join(targets.root, 'README.md')),
|
|
181
156
|
core: await fs.pathExists(path.join(path.dirname(targets.root), 'src', 'pi-takomi-core')),
|
|
182
157
|
piSubagentsModule: await fs.pathExists(path.join(targets.root, 'node_modules', 'pi-subagents')),
|
|
183
|
-
settingsPreserved: !
|
|
158
|
+
settingsPreserved: !manifest || manifest.preserved?.settingsHash === currentSettingsHash,
|
|
159
|
+
routingPolicyPreserved: !manifest || manifest.preserved?.routingPolicyHash === currentRoutingPolicyHash,
|
|
184
160
|
};
|
|
185
161
|
}
|
|
186
162
|
|
package/src/store.js
CHANGED
|
@@ -1,59 +1,23 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import crypto from 'crypto';
|
|
5
4
|
import pc from 'picocolors';
|
|
6
5
|
import { PATHS } from './utils.js';
|
|
7
6
|
import { CORE_SKILLS } from './skills-catalog.js';
|
|
7
|
+
import { hashPath as hashDirectory, normalizeOwnedMap, copyOwnedTree } from './owned-tree.js';
|
|
8
8
|
|
|
9
9
|
// ─── Global Store Path ───────────────────────────────────────────────────────
|
|
10
10
|
const HOME = os.homedir();
|
|
11
11
|
export const STORE_PATH = process.env.TAKOMI_STORE_PATH || process.env.TAKOMI_HOME_DIR || path.join(HOME, '.takomi');
|
|
12
12
|
export const MANIFEST_PATH = path.join(STORE_PATH, 'manifest.json');
|
|
13
13
|
|
|
14
|
-
function sha256(value) {
|
|
15
|
-
return crypto.createHash('sha256').update(value).digest('hex');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function hashDirectory(dir) {
|
|
19
|
-
if (!await fs.pathExists(dir)) return null;
|
|
20
|
-
const rootStat = await fs.stat(dir);
|
|
21
|
-
if (rootStat.isFile()) return sha256(await fs.readFile(dir));
|
|
22
|
-
const entries = [];
|
|
23
|
-
async function walk(current, prefix = '') {
|
|
24
|
-
const names = (await fs.readdir(current)).sort();
|
|
25
|
-
for (const name of names) {
|
|
26
|
-
const full = path.join(current, name);
|
|
27
|
-
const rel = path.join(prefix, name).replace(/\\/g, '/');
|
|
28
|
-
const stat = await fs.stat(full);
|
|
29
|
-
if (stat.isDirectory()) {
|
|
30
|
-
entries.push(`dir:${rel}`);
|
|
31
|
-
await walk(full, rel);
|
|
32
|
-
} else {
|
|
33
|
-
entries.push(`file:${rel}:${sha256(await fs.readFile(full))}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
await walk(dir);
|
|
38
|
-
return sha256(entries.join('\n'));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function normalizeOwnedMap(value) {
|
|
42
|
-
if (!value || typeof value !== 'object') return {};
|
|
43
|
-
const normalized = {};
|
|
44
|
-
for (const [name, entry] of Object.entries(value)) {
|
|
45
|
-
if (typeof entry === 'string') normalized[name] = { hash: entry };
|
|
46
|
-
else if (entry?.hash) normalized[name] = entry;
|
|
47
|
-
}
|
|
48
|
-
return normalized;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
14
|
function createDefaultManifest() {
|
|
52
15
|
return {
|
|
53
16
|
version: '2.0.0',
|
|
54
17
|
createdAt: new Date().toISOString(),
|
|
55
18
|
updatedAt: new Date().toISOString(),
|
|
56
19
|
linkedHarnesses: [],
|
|
20
|
+
syncMode: 'copy',
|
|
57
21
|
installed: {
|
|
58
22
|
skills: [],
|
|
59
23
|
workflows: [],
|
|
@@ -75,6 +39,7 @@ function normalizeManifest(manifest) {
|
|
|
75
39
|
workflows: normalizeOwnedMap(manifest?.bundledOwned?.workflows),
|
|
76
40
|
};
|
|
77
41
|
normalized.harnessOwned = manifest?.harnessOwned || {};
|
|
42
|
+
normalized.syncMode = manifest?.syncMode || 'copy';
|
|
78
43
|
return normalized;
|
|
79
44
|
}
|
|
80
45
|
|
|
@@ -250,7 +215,7 @@ export async function populateSkills(mode) {
|
|
|
250
215
|
await fs.remove(dest);
|
|
251
216
|
}
|
|
252
217
|
|
|
253
|
-
await
|
|
218
|
+
await copyOwnedTree(src, dest);
|
|
254
219
|
nextOwned[skill] = {
|
|
255
220
|
hash: await hashDirectory(dest),
|
|
256
221
|
targetPath: dest,
|
|
@@ -327,7 +292,7 @@ export async function populateWorkflows(mode) {
|
|
|
327
292
|
await fs.remove(dest);
|
|
328
293
|
}
|
|
329
294
|
|
|
330
|
-
await
|
|
295
|
+
await copyOwnedTree(src, dest);
|
|
331
296
|
nextOwned[workflow] = {
|
|
332
297
|
hash: await hashDirectory(dest),
|
|
333
298
|
targetPath: dest,
|
package/src/takomi-stats.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
1
|
+
import { createReadStream, promises as fs } from 'node:fs';
|
|
2
|
+
import readline from 'node:readline';
|
|
2
3
|
import os from 'os';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
|
|
@@ -167,9 +168,16 @@ async function scanPiSessions(root, source, events, sessionRows = [], taskRows =
|
|
|
167
168
|
const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map(), activeMs: 0, activityIntervals: [] };
|
|
168
169
|
const rowTracker = createActivityTracker();
|
|
169
170
|
const toolCalls = new Map();
|
|
170
|
-
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
let lines;
|
|
173
|
+
try {
|
|
174
|
+
lines = readline.createInterface({ input: createReadStream(file, { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
175
|
+
} catch {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
for await (const line of lines) {
|
|
173
181
|
const obj = safeJson(line); if (!obj) continue;
|
|
174
182
|
if (obj.timestamp) { row.start ||= obj.timestamp; row.end = obj.timestamp; }
|
|
175
183
|
if (obj.type === 'session') { session = obj.id || session; cwd = obj.cwd || cwd; row.key = session; row.session = session; row.cwd = cwd; }
|
|
@@ -254,6 +262,9 @@ async function scanPiSessions(root, source, events, sessionRows = [], taskRows =
|
|
|
254
262
|
if (currentTask) currentTask.end = ts;
|
|
255
263
|
}
|
|
256
264
|
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
continue;
|
|
257
268
|
}
|
|
258
269
|
pushTask(taskRows, currentTask);
|
|
259
270
|
if (currentTaskTracker && currentTask) {
|
package/src/utils.js
CHANGED
|
@@ -2,10 +2,10 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import https from 'https';
|
|
5
|
-
import {
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
9
|
|
|
10
10
|
// Reduced timeout to fail fast on bad connections
|
|
11
11
|
const FETCH_TIMEOUT = 10000;
|
|
@@ -17,9 +17,18 @@ const __dirname = path.dirname(__filename);
|
|
|
17
17
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
18
18
|
|
|
19
19
|
// GitHub repository configuration
|
|
20
|
-
const GITHUB_REPO = '
|
|
20
|
+
const GITHUB_REPO = 'J-StaR-Films-Studios/VibeCode-Protocol-Suite';
|
|
21
21
|
const GITHUB_BRANCH = 'main';
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
function githubRef(options = {}) {
|
|
24
|
+
const repo = options.repo || GITHUB_REPO;
|
|
25
|
+
const branch = options.branch || GITHUB_BRANCH;
|
|
26
|
+
return {
|
|
27
|
+
repo,
|
|
28
|
+
branch,
|
|
29
|
+
rawBaseUrl: `https://raw.githubusercontent.com/${repo}/${branch}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
export const PATHS = {
|
|
25
34
|
root: PACKAGE_ROOT,
|
|
@@ -134,9 +143,10 @@ function delay(ms) {
|
|
|
134
143
|
* @param {number} retries - Number of retries (default: 3)
|
|
135
144
|
* @returns {Promise<string>} File content
|
|
136
145
|
*/
|
|
137
|
-
export async function fetchFromGitHub(relativePath, retries = 3) {
|
|
146
|
+
export async function fetchFromGitHub(relativePath, retries = 3, options = {}) {
|
|
138
147
|
// Use raw GitHub content URL to avoid API rate limits
|
|
139
|
-
const
|
|
148
|
+
const { rawBaseUrl } = githubRef(options);
|
|
149
|
+
const rawUrl = `${rawBaseUrl}/${relativePath}`;
|
|
140
150
|
|
|
141
151
|
return new Promise((resolve, reject) => {
|
|
142
152
|
const attempt = (remainingRetries) => {
|
|
@@ -146,6 +156,19 @@ export async function fetchFromGitHub(relativePath, retries = 3) {
|
|
|
146
156
|
'User-Agent': 'takomi-cli'
|
|
147
157
|
}
|
|
148
158
|
}, (res) => {
|
|
159
|
+
if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
160
|
+
res.resume();
|
|
161
|
+
const redirectedUrl = new URL(res.headers.location, rawUrl).toString();
|
|
162
|
+
https.get(redirectedUrl, {
|
|
163
|
+
timeout: FETCH_TIMEOUT,
|
|
164
|
+
headers: { 'User-Agent': 'takomi-cli' }
|
|
165
|
+
}, (redirectRes) => {
|
|
166
|
+
let redirectedData = '';
|
|
167
|
+
redirectRes.on('data', chunk => redirectedData += chunk);
|
|
168
|
+
redirectRes.on('end', () => redirectRes.statusCode === 200 ? resolve(redirectedData) : reject(new Error(`HTTP ${redirectRes.statusCode}`)));
|
|
169
|
+
}).on('error', reject);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
149
172
|
let data = '';
|
|
150
173
|
res.on('data', chunk => data += chunk);
|
|
151
174
|
res.on('end', () => {
|
|
@@ -167,8 +190,8 @@ export async function fetchFromGitHub(relativePath, retries = 3) {
|
|
|
167
190
|
} else {
|
|
168
191
|
// Fallback to curl if Node https fails (e.g. proxy/network issues)
|
|
169
192
|
try {
|
|
170
|
-
// Ensure we use a timeout with curl too
|
|
171
|
-
const { stdout } = await
|
|
193
|
+
// Ensure we use a timeout with curl too, without invoking a shell.
|
|
194
|
+
const { stdout } = await execFileAsync('curl', ['-sL', '--max-time', String(CURL_TIMEOUT), rawUrl]);
|
|
172
195
|
if (stdout) resolve(stdout);
|
|
173
196
|
else reject(err);
|
|
174
197
|
} catch (curlErr) {
|
|
@@ -192,36 +215,49 @@ export async function fetchFromGitHub(relativePath, retries = 3) {
|
|
|
192
215
|
* @param {string} relativePath - Path relative to repo root
|
|
193
216
|
* @returns {Promise<Array>} Array of file objects
|
|
194
217
|
*/
|
|
195
|
-
export async function fetchDirectoryListing(relativePath) {
|
|
196
|
-
const
|
|
218
|
+
export async function fetchDirectoryListing(relativePath, options = {}) {
|
|
219
|
+
const { repo, branch } = githubRef(options);
|
|
220
|
+
const apiUrl = `https://api.github.com/repos/${repo}/contents/${relativePath}?ref=${branch}`;
|
|
197
221
|
|
|
198
222
|
return new Promise((resolve, reject) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}, (res) => {
|
|
206
|
-
let data = '';
|
|
207
|
-
res.on('data', chunk => data += chunk);
|
|
208
|
-
res.on('end', () => {
|
|
209
|
-
try {
|
|
210
|
-
if (res.statusCode === 200) {
|
|
211
|
-
const items = JSON.parse(data);
|
|
212
|
-
resolve(Array.isArray(items) ? items : []);
|
|
213
|
-
} else if (res.statusCode === 404) {
|
|
214
|
-
resolve([]);
|
|
215
|
-
} else if (res.statusCode === 403) {
|
|
216
|
-
reject(new Error('GitHub API rate limit exceeded. Please try again later.'));
|
|
217
|
-
} else {
|
|
218
|
-
reject(new Error(`GitHub API error: ${res.statusCode}`));
|
|
219
|
-
}
|
|
220
|
-
} catch (e) {
|
|
221
|
-
reject(new Error('Failed to parse GitHub API response'));
|
|
223
|
+
const requestUrl = (url, redirectsRemaining = 3) => {
|
|
224
|
+
const req = https.get(url, {
|
|
225
|
+
timeout: 10000,
|
|
226
|
+
headers: {
|
|
227
|
+
'User-Agent': 'takomi-cli',
|
|
228
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
222
229
|
}
|
|
230
|
+
}, (res) => {
|
|
231
|
+
if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location && redirectsRemaining > 0) {
|
|
232
|
+
res.resume();
|
|
233
|
+
requestUrl(new URL(res.headers.location, url).toString(), redirectsRemaining - 1);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let data = '';
|
|
238
|
+
res.on('data', chunk => data += chunk);
|
|
239
|
+
res.on('end', () => {
|
|
240
|
+
try {
|
|
241
|
+
if (res.statusCode === 200) {
|
|
242
|
+
const items = JSON.parse(data);
|
|
243
|
+
resolve(Array.isArray(items) ? items : []);
|
|
244
|
+
} else if (res.statusCode === 404) {
|
|
245
|
+
resolve([]);
|
|
246
|
+
} else if (res.statusCode === 403) {
|
|
247
|
+
reject(new Error('GitHub API rate limit exceeded. Please try again later.'));
|
|
248
|
+
} else {
|
|
249
|
+
reject(new Error(`GitHub API error: ${res.statusCode}`));
|
|
250
|
+
}
|
|
251
|
+
} catch (e) {
|
|
252
|
+
reject(new Error('Failed to parse GitHub API response'));
|
|
253
|
+
}
|
|
254
|
+
});
|
|
223
255
|
});
|
|
224
|
-
|
|
256
|
+
req.on('timeout', () => req.destroy(new Error('Request timed out')));
|
|
257
|
+
req.on('error', reject);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
requestUrl(apiUrl);
|
|
225
261
|
});
|
|
226
262
|
}
|
|
227
263
|
|
|
@@ -231,9 +267,9 @@ export async function fetchDirectoryListing(relativePath) {
|
|
|
231
267
|
* @param {string} destPath - Local destination path
|
|
232
268
|
* @returns {Promise<boolean>} Success status
|
|
233
269
|
*/
|
|
234
|
-
export async function downloadFromGitHub(relativePath, destPath) {
|
|
270
|
+
export async function downloadFromGitHub(relativePath, destPath, options = {}) {
|
|
235
271
|
try {
|
|
236
|
-
const content = await fetchFromGitHub(relativePath);
|
|
272
|
+
const content = await fetchFromGitHub(relativePath, 3, options);
|
|
237
273
|
await fs.ensureDir(path.dirname(destPath));
|
|
238
274
|
await fs.writeFile(destPath, content, 'utf8');
|
|
239
275
|
return true;
|
|
@@ -265,11 +301,11 @@ export async function downloadFromGitHub(relativePath, destPath) {
|
|
|
265
301
|
* @param {number} delayMs - Delay between file downloads in ms (default: 100)
|
|
266
302
|
* @returns {Promise<number>} Number of files downloaded
|
|
267
303
|
*/
|
|
268
|
-
export async function downloadDirectoryFromGitHub(relativePath, destPath, filter = null, delayMs = 250) {
|
|
304
|
+
export async function downloadDirectoryFromGitHub(relativePath, destPath, filter = null, delayMs = 250, options = {}) {
|
|
269
305
|
let count = 0;
|
|
270
306
|
|
|
271
307
|
try {
|
|
272
|
-
const items = await fetchDirectoryListing(relativePath);
|
|
308
|
+
const items = await fetchDirectoryListing(relativePath, options);
|
|
273
309
|
|
|
274
310
|
for (const item of items) {
|
|
275
311
|
if (!useGitHub) break; // Allow aborting if we disabled GitHub midway
|
|
@@ -277,7 +313,7 @@ export async function downloadDirectoryFromGitHub(relativePath, destPath, filter
|
|
|
277
313
|
|
|
278
314
|
if (item.type === 'file') {
|
|
279
315
|
const fileDest = path.join(destPath, item.name);
|
|
280
|
-
const success = await downloadFromGitHub(item.path, fileDest);
|
|
316
|
+
const success = await downloadFromGitHub(item.path, fileDest, options);
|
|
281
317
|
if (success) {
|
|
282
318
|
count++;
|
|
283
319
|
// Add small delay between file downloads to be nice to GitHub
|
|
@@ -287,12 +323,13 @@ export async function downloadDirectoryFromGitHub(relativePath, destPath, filter
|
|
|
287
323
|
}
|
|
288
324
|
} else if (item.type === 'dir') {
|
|
289
325
|
const subDirDest = path.join(destPath, item.name);
|
|
290
|
-
const subCount = await downloadDirectoryFromGitHub(item.path, subDirDest, filter, delayMs);
|
|
326
|
+
const subCount = await downloadDirectoryFromGitHub(item.path, subDirDest, filter, delayMs, options);
|
|
291
327
|
count += subCount;
|
|
292
328
|
}
|
|
293
329
|
}
|
|
294
330
|
} catch (error) {
|
|
295
331
|
console.error(`Error downloading directory ${relativePath}: ${error.message}`);
|
|
332
|
+
if (options.throwOnError) throw error;
|
|
296
333
|
}
|
|
297
334
|
|
|
298
335
|
return count;
|
|
@@ -459,7 +496,7 @@ export async function copyLegacyManual(destFolder) {
|
|
|
459
496
|
export async function updateWorkflows(destFolder) {
|
|
460
497
|
console.log('📡 Fetching latest workflows from GitHub...');
|
|
461
498
|
await fs.ensureDir(destFolder);
|
|
462
|
-
|
|
499
|
+
return downloadDirectoryFromGitHub('assets/.agent/workflows', destFolder, null, 250, { throwOnError: true });
|
|
463
500
|
}
|
|
464
501
|
|
|
465
502
|
/**
|
|
@@ -468,7 +505,7 @@ export async function updateWorkflows(destFolder) {
|
|
|
468
505
|
export async function updateSkills(destFolder) {
|
|
469
506
|
console.log('📡 Fetching latest skills from GitHub...');
|
|
470
507
|
await fs.ensureDir(destFolder);
|
|
471
|
-
|
|
508
|
+
return downloadDirectoryFromGitHub('assets/.agent/skills', destFolder, null, 250, { throwOnError: true });
|
|
472
509
|
}
|
|
473
510
|
|
|
474
511
|
/**
|
|
@@ -477,7 +514,7 @@ export async function updateSkills(destFolder) {
|
|
|
477
514
|
export async function updateAgentYamls(destFolder) {
|
|
478
515
|
console.log('📡 Fetching latest Agent YAMLs from GitHub...');
|
|
479
516
|
await fs.ensureDir(destFolder);
|
|
480
|
-
|
|
517
|
+
return downloadDirectoryFromGitHub('assets/Takomi-Agents', destFolder, null, 250, { throwOnError: true });
|
|
481
518
|
}
|
|
482
519
|
|
|
483
520
|
/**
|
|
@@ -486,5 +523,5 @@ export async function updateAgentYamls(destFolder) {
|
|
|
486
523
|
export async function updateLegacyManual(destFolder) {
|
|
487
524
|
console.log('📡 Fetching latest Legacy Protocols from GitHub...');
|
|
488
525
|
await fs.ensureDir(destFolder);
|
|
489
|
-
|
|
526
|
+
return downloadDirectoryFromGitHub('assets/Legacy', destFolder, null, 250, { throwOnError: true });
|
|
490
527
|
}
|