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/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
- const report = await inspectPiHarnessEnvironment(cwd);
462
-
463
- if (!report.pi.installed) {
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 (!report.installed.runtimeInstalled || !report.installed.subagentsInstalled) {
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 child = process.platform === 'win32'
480
- ? spawn('cmd.exe', ['/d', '/s', '/c', 'pi'], {
481
- cwd,
482
- stdio: 'inherit',
483
- env,
484
- shell: false,
485
- })
486
- : spawn(report.pi.path || 'pi', [], {
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));
@@ -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 fs.copy(src, dest, { overwrite: true });
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 fs.copy(src, dest, { overwrite: true });
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: !await fs.pathExists(path.join(PATHS.pi, 'settings.json')) || true,
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 fs.copy(src, dest, { overwrite: true });
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 fs.copy(src, dest, { overwrite: true });
295
+ await copyOwnedTree(src, dest);
331
296
  nextOwned[workflow] = {
332
297
  hash: await hashDirectory(dest),
333
298
  targetPath: dest,
@@ -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
- for (const line of text.split(/\r?\n/)) {
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 { exec } from 'child_process';
5
+ import { execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
7
 
8
- const execAsync = promisify(exec);
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 = 'JStaRFilms/VibeCode-Protocol-Suite';
20
+ const GITHUB_REPO = 'J-StaR-Films-Studios/VibeCode-Protocol-Suite';
21
21
  const GITHUB_BRANCH = 'main';
22
- const GITHUB_RAW_URL = `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}`;
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 rawUrl = `${GITHUB_RAW_URL}/${relativePath}`;
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 execAsync(`curl -sL --max-time ${CURL_TIMEOUT} "${rawUrl}"`);
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 apiUrl = `https://api.github.com/repos/${GITHUB_REPO}/contents/${relativePath}?ref=${GITHUB_BRANCH}`;
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
- https.get(apiUrl, {
200
- timeout: 10000,
201
- headers: {
202
- 'User-Agent': 'takomi-cli',
203
- 'Accept': 'application/vnd.github.v3+json'
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
- }).on('error', reject);
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
- await downloadDirectoryFromGitHub('.agent/workflows', destFolder);
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
- await downloadDirectoryFromGitHub('.agent/skills', destFolder);
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
- await downloadDirectoryFromGitHub('Takomi-Agents', destFolder);
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
- await downloadDirectoryFromGitHub('Legacy (Manual Method)', destFolder);
526
+ return downloadDirectoryFromGitHub('assets/Legacy', destFolder, null, 250, { throwOnError: true });
490
527
  }