cadet-agent 0.9.0 → 0.11.0

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/README.md CHANGED
@@ -5,8 +5,8 @@ Cadet-Agent is a cross-IDE agent framework for game-development workflows, with
5
5
  ## Repository Layout
6
6
  - `.cadet/agent/core/` contains the shared Cadet-Agent framework documents.
7
7
  - `.cadet/agent/docs/` contains setup guides for each supported IDE.
8
- - `AGENTS.md` contains shared top-level agent instructions.
9
- - `.github/` contains GitHub Copilot-specific authored files.
8
+ - `.github/agents/` contains the Copilot custom agent definition (agent mode).
9
+ - `.github/prompts/` contains the Copilot slash-command prompt (fallback).
10
10
  - `.cursor/` contains Cursor-specific authored files.
11
11
  - `.continue/` contains Continue-specific authored files.
12
12
  - `.claude/` contains Claude Code-specific authored files.
@@ -43,14 +43,18 @@ Expand-Archive .\cadet-agent.zip -DestinationPath . -Force
43
43
 
44
44
  ## Examples
45
45
 
46
- ### GitHub Copilot kickoff
47
- Run `npx cadet-agent@latest init` in your Unity project root, then open the repo in VS Code and start a kickoff chat using the Cadet prompt.
46
+ ### GitHub Copilot
47
+ Run `npx cadet-agent@latest init` in your Unity project root, then open the repo in VS Code.
48
+
49
+ **Agent mode (recommended):** Select the **Cadet** agent from the agent picker in Copilot Chat. The agent definition at `.github/agents/cadet.agent.md` provides focused instructions and tool configuration.
50
+
51
+ **Slash command (fallback):** Use `/cadet` in Copilot Chat to invoke the prompt-based path:
48
52
 
49
53
  ```text
50
54
  /cadet Help me bootstrap a beginner-friendly 2D racing prototype in Unity with AI opponents and a career progression loop.
51
55
  ```
52
56
 
53
- Cadet-Agent will use `.github/prompts/cadet.prompt.md` plus the shared framework in `.cadet/agent/core` to route the conversation through learner calibration, bootstrap checks, and planning.
57
+ Cadet will use the shared framework in `.cadet/agent/core` to route the conversation through learner calibration, bootstrap checks, and planning.
54
58
 
55
59
  ### Cursor feature request
56
60
  After opening the repository in Cursor, the always-apply rule in `.cursor/rules/cadet-agent.md` should load automatically. A typical request looks like this:
@@ -79,7 +83,7 @@ If a specific game repository needs local conventions, add a policy file under `
79
83
  ## Package Output
80
84
  Running `./package-agent.ps1` produces `cadet-agent.zip` with this layout:
81
85
  - `.cadet/agent/core/`
82
- - `AGENTS.md`
86
+ - `.github/agents/cadet.agent.md`
83
87
  - `.github/prompts/cadet.prompt.md`
84
88
  - `.cursor/rules/cadet-agent.md`
85
89
  - `.continue/rules/cadet-agent.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cadet-agent",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Cross-IDE agent framework for Unity/C# game-development — one-command install",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -1,78 +1,78 @@
1
- import { readFileSync } from 'node:fs';
2
- import { fileURLToPath } from 'node:url';
3
- import { dirname, join } from 'node:path';
4
- import { install, sync } from './install.mjs';
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
8
-
9
- function getVersion() {
10
- const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
11
- return pkg.version;
12
- }
13
-
14
- function showHelp() {
15
- console.log(`
16
- ██████╗ █████╗ ██████╗ ███████╗████████╗
17
- ██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝
18
- ██║ ███████║██║ ██║█████╗ ██║
19
- ██║ ██╔══██║██║ ██║██╔══╝ ██║
20
- ╚██████╗██║ ██║██████╔╝███████╗ ██║
21
- ╚═════╝╚═╝ ╚═╝╚═════╝ ╚══════╝ ╚═╝
22
-
23
- Cross-IDE agent framework for Unity/C# game-development
24
-
25
- Usage:
26
- npx cadet-agent@latest init Install Cadet-Agent into the current directory
27
- npx cadet-agent@latest init --target <dir> Install into a specific directory
28
- npx cadet-agent@latest sync Update framework, preserving local policies/plans
29
- npx cadet-agent@latest sync --target <dir> Sync a specific directory
30
-
31
- Options:
32
- --target, -t Target directory (default: current working directory)
33
- --source Release API URL override (for forked deployments)
34
- --help, -h Show this help
35
- --version, -v Show version number
36
- `);
37
- }
38
-
39
- export async function run(argv) {
40
- const command = argv[2];
41
-
42
- // Parse --target <dir> or -t <dir>
43
- let targetDir = process.cwd();
44
- let sourceUrl = null;
45
- const targetIdx = argv.indexOf('--target');
46
- const tIdx = argv.indexOf('-t');
47
- const sourceIdx = argv.indexOf('--source');
48
- if (targetIdx !== -1 && argv[targetIdx + 1]) {
49
- targetDir = argv[targetIdx + 1];
50
- } else if (tIdx !== -1 && argv[tIdx + 1]) {
51
- targetDir = argv[tIdx + 1];
52
- }
53
- if (sourceIdx !== -1 && argv[sourceIdx + 1]) {
54
- sourceUrl = argv[sourceIdx + 1];
55
- }
56
-
57
- switch (command) {
58
- case 'init':
59
- await install(targetDir, { sourceUrl });
60
- break;
61
- case 'sync':
62
- await sync(targetDir, { sourceUrl });
63
- break;
64
- case '--version':
65
- case '-v':
66
- console.log(`cadet-agent v${getVersion()}`);
67
- break;
68
- case '--help':
69
- case '-h':
70
- case undefined:
71
- showHelp();
72
- break;
73
- default:
74
- console.error(`Unknown command: ${command}`);
75
- console.error('Run cadet-agent --help for usage.');
76
- process.exit(1);
77
- }
78
- }
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, join } from 'node:path';
4
+ import { install, sync } from './install.mjs';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ function getVersion() {
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
11
+ return pkg.version;
12
+ }
13
+
14
+ function showHelp() {
15
+ console.log(`
16
+ ██████╗ █████╗ ██████╗ ███████╗████████╗
17
+ ██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝
18
+ ██║ ███████║██║ ██║█████╗ ██║
19
+ ██║ ██╔══██║██║ ██║██╔══╝ ██║
20
+ ╚██████╗██║ ██║██████╔╝███████╗ ██║
21
+ ╚═════╝╚═╝ ╚═╝╚═════╝ ╚══════╝ ╚═╝
22
+
23
+ Cross-IDE agent framework for Unity/C# game-development
24
+
25
+ Usage:
26
+ npx cadet-agent@latest init Install Cadet-Agent into the current directory
27
+ npx cadet-agent@latest init --target <dir> Install into a specific directory
28
+ npx cadet-agent@latest sync Update framework, preserving local policies/plans
29
+ npx cadet-agent@latest sync --target <dir> Sync a specific directory
30
+
31
+ Options:
32
+ --target, -t Target directory (default: current working directory)
33
+ --source Release API URL override (for forked deployments)
34
+ --help, -h Show this help
35
+ --version, -v Show version number
36
+ `);
37
+ }
38
+
39
+ export async function run(argv) {
40
+ const command = argv[2];
41
+
42
+ // Parse --target <dir> or -t <dir>
43
+ let targetDir = process.cwd();
44
+ let sourceUrl = null;
45
+ const targetIdx = argv.indexOf('--target');
46
+ const tIdx = argv.indexOf('-t');
47
+ const sourceIdx = argv.indexOf('--source');
48
+ if (targetIdx !== -1 && argv[targetIdx + 1]) {
49
+ targetDir = argv[targetIdx + 1];
50
+ } else if (tIdx !== -1 && argv[tIdx + 1]) {
51
+ targetDir = argv[tIdx + 1];
52
+ }
53
+ if (sourceIdx !== -1 && argv[sourceIdx + 1]) {
54
+ sourceUrl = argv[sourceIdx + 1];
55
+ }
56
+
57
+ switch (command) {
58
+ case 'init':
59
+ await install(targetDir, { sourceUrl });
60
+ break;
61
+ case 'sync':
62
+ await sync(targetDir, { sourceUrl });
63
+ break;
64
+ case '--version':
65
+ case '-v':
66
+ console.log(`cadet-agent v${getVersion()}`);
67
+ break;
68
+ case '--help':
69
+ case '-h':
70
+ case undefined:
71
+ showHelp();
72
+ break;
73
+ default:
74
+ console.error(`Unknown command: ${command}`);
75
+ console.error('Run cadet-agent --help for usage.');
76
+ process.exit(1);
77
+ }
78
+ }
package/src/install.mjs CHANGED
@@ -1,337 +1,337 @@
1
- import { createWriteStream, mkdirSync, existsSync, readFileSync } from 'node:fs';
2
- import { join, dirname } from 'node:path';
3
- import { pipeline } from 'node:stream/promises';
4
- import { createGunzip } from 'node:zlib';
5
- import { inflateRawSync } from 'node:zlib';
6
- import { tmpdir } from 'node:os';
7
- import { Readable } from 'node:stream';
8
-
9
- // ── Constants ────────────────────────────────────────────────────────────────
10
-
11
- const DEFAULT_API = 'https://api.github.com/repos/naishtech/cadet-agent/releases/latest';
12
- const USER_AGENT = 'cadet-agent-cli';
13
-
14
- function resolveApiUrl(override) {
15
- return override || process.env.CADET_AGENT_RELEASE_URL || DEFAULT_API;
16
- }
17
-
18
- // ── ZIP parser (zero-dependency, handles store + deflate) ───────────────────
19
-
20
- const SIG_EOCD = 0x06054b50;
21
- const SIG_CD = 0x02014b50;
22
- const SIG_LFH = 0x04034b50;
23
-
24
- function read32(buf, off) { return buf.readUInt32LE(off); }
25
- function read16(buf, off) { return buf.readUInt16LE(off); }
26
-
27
- function findEocd(buf) {
28
- // Search backwards from end for EOCD signature (comment is max 65535 bytes)
29
- const maxStart = Math.max(0, buf.length - 65535 - 22);
30
- for (let i = buf.length - 22; i >= maxStart; i--) {
31
- if (read32(buf, i) === SIG_EOCD) return i;
32
- }
33
- throw new Error('Not a valid ZIP file: EOCD signature not found');
34
- }
35
-
36
- function* centralDirectoryEntries(buf, cdOffset, cdSize) {
37
- let off = cdOffset;
38
- const end = cdOffset + cdSize;
39
- while (off < end) {
40
- if (read32(buf, off) !== SIG_CD) break;
41
- const method = read16(buf, off + 10);
42
- const compressedSize = read32(buf, off + 20);
43
- const uncompressedSize = read32(buf, off + 24);
44
- const filenameLen = read16(buf, off + 28);
45
- const extraLen = read16(buf, off + 30);
46
- const commentLen = read16(buf, off + 32);
47
- const localHeaderOff = read32(buf, off + 42);
48
- const filename = buf.toString('utf-8', off + 46, off + 46 + filenameLen);
49
-
50
- // Skip directory entries (trailing / in filename, or uncompressedSize == 0 with no method)
51
- if (!filename.endsWith('/')) {
52
- yield { filename, method, compressedSize, uncompressedSize, localHeaderOff };
53
- }
54
-
55
- off += 46 + filenameLen + extraLen + commentLen;
56
- }
57
- }
58
-
59
- function extractFile(buf, entry, targetDir) {
60
- const { filename, method, compressedSize, localHeaderOff } = entry;
61
-
62
- // Read local file header to get filename + extra lengths (they may differ from CD)
63
- const lfhFilenameLen = read16(buf, localHeaderOff + 26);
64
- const lfhExtraLen = read16(buf, localHeaderOff + 28);
65
-
66
- const dataStart = localHeaderOff + 30 + lfhFilenameLen + lfhExtraLen;
67
- const compressed = buf.subarray(dataStart, dataStart + compressedSize);
68
-
69
- let data;
70
- if (method === 0) {
71
- // Stored — no compression
72
- data = compressed;
73
- } else if (method === 8) {
74
- // Deflate
75
- data = inflateRawSync(compressed);
76
- } else {
77
- throw new Error(
78
- `Unsupported compression method ${method} for ${filename}.\n` +
79
- `This zip uses a compression format this CLI doesn't support.\n` +
80
- `Try downloading cadet-agent.zip manually from:\n` +
81
- ` https://github.com/naishtech/cadet-agent/releases/latest`
82
- );
83
- }
84
-
85
- const outPath = join(targetDir, filename);
86
- mkdirSync(dirname(outPath), { recursive: true });
87
- const ws = createWriteStream(outPath);
88
- Readable.from(data).pipe(ws);
89
-
90
- return outPath;
91
- }
92
-
93
- function extractZip(buf, targetDir) {
94
- const eocdOff = findEocd(buf);
95
- const cdSize = read32(buf, eocdOff + 12);
96
- const cdOff = read32(buf, eocdOff + 16);
97
-
98
- // ZIP64 detection — 0xFFFFFFFF in 32-bit fields means real values are in ZIP64 extra records
99
- if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
100
- throw new Error(
101
- 'This zip uses ZIP64 format, which is not supported.\n' +
102
- 'Try downloading cadet-agent.zip manually from:\n' +
103
- ' https://github.com/naishtech/cadet-agent/releases/latest'
104
- );
105
- }
106
-
107
- const paths = [];
108
- for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
109
- const outPath = extractFile(buf, entry, targetDir);
110
- paths.push(outPath);
111
- }
112
- return paths;
113
- }
114
-
115
- // ── GitHub release download ─────────────────────────────────────────────────
116
-
117
- async function fetchLatestRelease(apiUrl) {
118
- const url = resolveApiUrl(apiUrl);
119
- console.log('🔍 Fetching latest Cadet-Agent release...');
120
-
121
- const res = await fetch(url, {
122
- headers: {
123
- 'User-Agent': USER_AGENT,
124
- 'Accept': 'application/vnd.github+json',
125
- },
126
- });
127
-
128
- if (!res.ok) {
129
- if (res.status === 403 || res.status === 429) {
130
- throw new Error(
131
- `GitHub API rate-limited (${res.status}). ` +
132
- 'Set GITHUB_TOKEN env var for higher limits, or try again later.'
133
- );
134
- }
135
- throw new Error(`GitHub API returned ${res.status}: ${res.statusText}`);
136
- }
137
-
138
- return res.json();
139
- }
140
-
141
- function findZipAsset(release) {
142
- const asset = release.assets?.find(a => a.name === 'cadet-agent.zip');
143
- if (!asset) {
144
- throw new Error(
145
- `Release ${release.tag_name} does not contain cadet-agent.zip.\n` +
146
- `Available assets: ${(release.assets || []).map(a => a.name).join(', ') || 'none'}`
147
- );
148
- }
149
- return asset;
150
- }
151
-
152
- async function downloadZip(url) {
153
- console.log('⬇️ Downloading cadet-agent.zip...');
154
-
155
- const res = await fetch(url, {
156
- headers: {
157
- 'User-Agent': USER_AGENT,
158
- 'Accept': 'application/octet-stream',
159
- },
160
- });
161
-
162
- if (!res.ok) {
163
- throw new Error(`Download failed: ${res.status} ${res.statusText}`);
164
- }
165
-
166
- const contentLength = res.headers.get('content-length');
167
- const total = contentLength ? parseInt(contentLength, 10) : 0;
168
-
169
- // Stream to buffer with progress
170
- const chunks = [];
171
- let downloaded = 0;
172
- const reader = res.body.getReader();
173
-
174
- while (true) {
175
- const { done, value } = await reader.read();
176
- if (done) break;
177
- chunks.push(value);
178
- downloaded += value.length;
179
- if (total > 0) {
180
- const pct = Math.round((downloaded / total) * 100);
181
- process.stdout.write(`\r ${pct}% (${(downloaded / 1024).toFixed(0)} KB / ${(total / 1024).toFixed(0)} KB)`);
182
- }
183
- }
184
- if (total > 0) process.stdout.write('\n');
185
-
186
- return Buffer.concat(chunks);
187
- }
188
-
189
- // ── Public install entry ────────────────────────────────────────────────────
190
-
191
- export async function install(targetDir, opts = {}) {
192
- console.log(`📦 Cadet-Agent — installing to ${targetDir}\n`);
193
-
194
- // 1. Fetch release metadata
195
- const release = await fetchLatestRelease(opts.sourceUrl);
196
- console.log(` Latest: ${release.tag_name} (published ${release.published_at})\n`);
197
-
198
- // 2. Find zip asset
199
- const asset = findZipAsset(release);
200
-
201
- // 3. Download
202
- const zipBuf = await downloadZip(asset.browser_download_url);
203
- console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
204
-
205
- // 4. Extract
206
- console.log('📂 Extracting...');
207
- const extracted = extractZip(zipBuf, targetDir);
208
-
209
- // 5. Report
210
- console.log(`\n✅ Cadet-Agent ${release.tag_name} installed! Extracted ${extracted.length} files.\n`);
211
-
212
- // Print per-IDE next steps
213
- console.log('── Next steps ──');
214
- console.log(' GitHub Copilot:');
215
- console.log(' Start a chat: /cadet');
216
- console.log(' Cursor:');
217
- console.log(' Already active — .cursor\\rules\\cadet-agent.md loads automatically');
218
- console.log(' Continue:');
219
- console.log(' Already active — .continue\\rules\\cadet-agent.md loads automatically');
220
- console.log(' Claude Code:');
221
- console.log(' Already active — .claude\\skills\\cadet-agent.md loads as a project skill');
222
- console.log('');
223
- }
224
-
225
- // ── Manifest-aware extraction ───────────────────────────────────────────────
226
-
227
- function matchesPreservedPath(filename, preservedPaths) {
228
- // Normalize: strip leading dot (zip paths like ".cadet/agent/policies/...")
229
- const normalized = filename.replace(/^\.?\/?/, '');
230
- for (const preserved of preservedPaths) {
231
- const p = preserved.replace(/^\.?\/?/, '');
232
- if (normalized === p || normalized.startsWith(p + '/') || normalized.startsWith(p + '\\')) {
233
- return true;
234
- }
235
- }
236
- return false;
237
- }
238
-
239
- function extractZipWithManifest(buf, targetDir, { preserved, managed }) {
240
- const eocdOff = findEocd(buf);
241
- const cdSize = read32(buf, eocdOff + 12);
242
- const cdOff = read32(buf, eocdOff + 16);
243
-
244
- if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
245
- throw new Error(
246
- 'This zip uses ZIP64 format, which is not supported.\n' +
247
- 'Try downloading cadet-agent.zip manually from:\n' +
248
- ' https://github.com/naishtech/cadet-agent/releases/latest'
249
- );
250
- }
251
-
252
- const updated = [];
253
- const preserved_list = [];
254
- const added = [];
255
-
256
- for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
257
- if (matchesPreservedPath(entry.filename, preserved)) {
258
- preserved_list.push(entry.filename);
259
- continue;
260
- }
261
- const outPath = extractFile(buf, entry, targetDir);
262
- // Check if this file already existed (updated vs added)
263
- // We can't easily tell without checking before extraction, so approximate:
264
- // If it's under a managed path, call it updated; otherwise added
265
- const isManaged = managed.some(m => {
266
- const mn = m.replace(/^\.?\/?/, '');
267
- const fn = entry.filename.replace(/^\.?\/?/, '');
268
- return fn === mn || fn.startsWith(mn + '/') || fn.startsWith(mn + '\\');
269
- });
270
- if (isManaged) {
271
- updated.push(outPath);
272
- } else {
273
- added.push(outPath);
274
- }
275
- }
276
-
277
- return { updated, preserved: preserved_list, added };
278
- }
279
-
280
- // ── Public sync entry ───────────────────────────────────────────────────────
281
-
282
- export async function sync(targetDir, opts = {}) {
283
- console.log(`🔄 Cadet-Agent — syncing ${targetDir}\n`);
284
-
285
- // 1. Read existing manifest
286
- const manifestPath = join(targetDir, '.cadet', 'agent', 'core', 'FrameworkManifest.json');
287
- let existingManifest = null;
288
- let oldVersion = 'none';
289
- try {
290
- existingManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
291
- oldVersion = existingManifest.frameworkVersion || 'unknown';
292
- console.log(` Existing install: v${oldVersion}`);
293
- } catch {
294
- console.log(' No existing install found — performing full install.\n');
295
- return install(targetDir, opts);
296
- }
297
-
298
- // 2. Fetch release metadata
299
- const release = await fetchLatestRelease(opts.sourceUrl);
300
- const newVersion = release.tag_name;
301
- console.log(` Latest: ${newVersion} (published ${release.published_at})\n`);
302
-
303
- if (oldVersion === newVersion) {
304
- console.log(`✅ Already up to date (v${oldVersion}). Nothing to sync.\n`);
305
- return;
306
- }
307
-
308
- // 3. Download
309
- const asset = findZipAsset(release);
310
- const zipBuf = await downloadZip(asset.browser_download_url);
311
- console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
312
-
313
- // 4. Extract with manifest awareness
314
- console.log('📂 Extracting (preserving local policies and plans)...');
315
- const result = extractZipWithManifest(zipBuf, targetDir, {
316
- preserved: existingManifest.preservedPaths || [],
317
- managed: existingManifest.managedPaths || [],
318
- });
319
-
320
- // 5. Report
321
- console.log('');
322
- console.log(`✅ Cadet-Agent synced: v${oldVersion} → ${newVersion}`);
323
- console.log(` Updated: ${result.updated.length} files`);
324
- if (result.preserved.length > 0) {
325
- console.log(` Preserved: ${result.preserved.length} files (local policies/plans)`);
326
- }
327
- if (result.added.length > 0) {
328
- console.log(` New: ${result.added.length} files`);
329
- }
330
- console.log('');
331
-
332
- // Print per-IDE next steps
333
- console.log('── Next steps ──');
334
- console.log(' Framework files updated. Start a fresh chat for changes to take effect:');
335
- console.log(' /cadet');
336
- console.log('');
337
- }
1
+ import { createWriteStream, mkdirSync, existsSync, readFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { createGunzip } from 'node:zlib';
5
+ import { inflateRawSync } from 'node:zlib';
6
+ import { tmpdir } from 'node:os';
7
+ import { Readable } from 'node:stream';
8
+
9
+ // ── Constants ────────────────────────────────────────────────────────────────
10
+
11
+ const DEFAULT_API = 'https://api.github.com/repos/naishtech/cadet-agent/releases/latest';
12
+ const USER_AGENT = 'cadet-agent-cli';
13
+
14
+ function resolveApiUrl(override) {
15
+ return override || process.env.CADET_AGENT_RELEASE_URL || DEFAULT_API;
16
+ }
17
+
18
+ // ── ZIP parser (zero-dependency, handles store + deflate) ───────────────────
19
+
20
+ const SIG_EOCD = 0x06054b50;
21
+ const SIG_CD = 0x02014b50;
22
+ const SIG_LFH = 0x04034b50;
23
+
24
+ function read32(buf, off) { return buf.readUInt32LE(off); }
25
+ function read16(buf, off) { return buf.readUInt16LE(off); }
26
+
27
+ function findEocd(buf) {
28
+ // Search backwards from end for EOCD signature (comment is max 65535 bytes)
29
+ const maxStart = Math.max(0, buf.length - 65535 - 22);
30
+ for (let i = buf.length - 22; i >= maxStart; i--) {
31
+ if (read32(buf, i) === SIG_EOCD) return i;
32
+ }
33
+ throw new Error('Not a valid ZIP file: EOCD signature not found');
34
+ }
35
+
36
+ function* centralDirectoryEntries(buf, cdOffset, cdSize) {
37
+ let off = cdOffset;
38
+ const end = cdOffset + cdSize;
39
+ while (off < end) {
40
+ if (read32(buf, off) !== SIG_CD) break;
41
+ const method = read16(buf, off + 10);
42
+ const compressedSize = read32(buf, off + 20);
43
+ const uncompressedSize = read32(buf, off + 24);
44
+ const filenameLen = read16(buf, off + 28);
45
+ const extraLen = read16(buf, off + 30);
46
+ const commentLen = read16(buf, off + 32);
47
+ const localHeaderOff = read32(buf, off + 42);
48
+ const filename = buf.toString('utf-8', off + 46, off + 46 + filenameLen);
49
+
50
+ // Skip directory entries (trailing / in filename, or uncompressedSize == 0 with no method)
51
+ if (!filename.endsWith('/')) {
52
+ yield { filename, method, compressedSize, uncompressedSize, localHeaderOff };
53
+ }
54
+
55
+ off += 46 + filenameLen + extraLen + commentLen;
56
+ }
57
+ }
58
+
59
+ function extractFile(buf, entry, targetDir) {
60
+ const { filename, method, compressedSize, localHeaderOff } = entry;
61
+
62
+ // Read local file header to get filename + extra lengths (they may differ from CD)
63
+ const lfhFilenameLen = read16(buf, localHeaderOff + 26);
64
+ const lfhExtraLen = read16(buf, localHeaderOff + 28);
65
+
66
+ const dataStart = localHeaderOff + 30 + lfhFilenameLen + lfhExtraLen;
67
+ const compressed = buf.subarray(dataStart, dataStart + compressedSize);
68
+
69
+ let data;
70
+ if (method === 0) {
71
+ // Stored — no compression
72
+ data = compressed;
73
+ } else if (method === 8) {
74
+ // Deflate
75
+ data = inflateRawSync(compressed);
76
+ } else {
77
+ throw new Error(
78
+ `Unsupported compression method ${method} for ${filename}.\n` +
79
+ `This zip uses a compression format this CLI doesn't support.\n` +
80
+ `Try downloading cadet-agent.zip manually from:\n` +
81
+ ` https://github.com/naishtech/cadet-agent/releases/latest`
82
+ );
83
+ }
84
+
85
+ const outPath = join(targetDir, filename);
86
+ mkdirSync(dirname(outPath), { recursive: true });
87
+ const ws = createWriteStream(outPath);
88
+ Readable.from(data).pipe(ws);
89
+
90
+ return outPath;
91
+ }
92
+
93
+ function extractZip(buf, targetDir) {
94
+ const eocdOff = findEocd(buf);
95
+ const cdSize = read32(buf, eocdOff + 12);
96
+ const cdOff = read32(buf, eocdOff + 16);
97
+
98
+ // ZIP64 detection — 0xFFFFFFFF in 32-bit fields means real values are in ZIP64 extra records
99
+ if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
100
+ throw new Error(
101
+ 'This zip uses ZIP64 format, which is not supported.\n' +
102
+ 'Try downloading cadet-agent.zip manually from:\n' +
103
+ ' https://github.com/naishtech/cadet-agent/releases/latest'
104
+ );
105
+ }
106
+
107
+ const paths = [];
108
+ for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
109
+ const outPath = extractFile(buf, entry, targetDir);
110
+ paths.push(outPath);
111
+ }
112
+ return paths;
113
+ }
114
+
115
+ // ── GitHub release download ─────────────────────────────────────────────────
116
+
117
+ async function fetchLatestRelease(apiUrl) {
118
+ const url = resolveApiUrl(apiUrl);
119
+ console.log('🔍 Fetching latest Cadet-Agent release...');
120
+
121
+ const res = await fetch(url, {
122
+ headers: {
123
+ 'User-Agent': USER_AGENT,
124
+ 'Accept': 'application/vnd.github+json',
125
+ },
126
+ });
127
+
128
+ if (!res.ok) {
129
+ if (res.status === 403 || res.status === 429) {
130
+ throw new Error(
131
+ `GitHub API rate-limited (${res.status}). ` +
132
+ 'Set GITHUB_TOKEN env var for higher limits, or try again later.'
133
+ );
134
+ }
135
+ throw new Error(`GitHub API returned ${res.status}: ${res.statusText}`);
136
+ }
137
+
138
+ return res.json();
139
+ }
140
+
141
+ function findZipAsset(release) {
142
+ const asset = release.assets?.find(a => a.name === 'cadet-agent.zip');
143
+ if (!asset) {
144
+ throw new Error(
145
+ `Release ${release.tag_name} does not contain cadet-agent.zip.\n` +
146
+ `Available assets: ${(release.assets || []).map(a => a.name).join(', ') || 'none'}`
147
+ );
148
+ }
149
+ return asset;
150
+ }
151
+
152
+ async function downloadZip(url) {
153
+ console.log('⬇️ Downloading cadet-agent.zip...');
154
+
155
+ const res = await fetch(url, {
156
+ headers: {
157
+ 'User-Agent': USER_AGENT,
158
+ 'Accept': 'application/octet-stream',
159
+ },
160
+ });
161
+
162
+ if (!res.ok) {
163
+ throw new Error(`Download failed: ${res.status} ${res.statusText}`);
164
+ }
165
+
166
+ const contentLength = res.headers.get('content-length');
167
+ const total = contentLength ? parseInt(contentLength, 10) : 0;
168
+
169
+ // Stream to buffer with progress
170
+ const chunks = [];
171
+ let downloaded = 0;
172
+ const reader = res.body.getReader();
173
+
174
+ while (true) {
175
+ const { done, value } = await reader.read();
176
+ if (done) break;
177
+ chunks.push(value);
178
+ downloaded += value.length;
179
+ if (total > 0) {
180
+ const pct = Math.round((downloaded / total) * 100);
181
+ process.stdout.write(`\r ${pct}% (${(downloaded / 1024).toFixed(0)} KB / ${(total / 1024).toFixed(0)} KB)`);
182
+ }
183
+ }
184
+ if (total > 0) process.stdout.write('\n');
185
+
186
+ return Buffer.concat(chunks);
187
+ }
188
+
189
+ // ── Public install entry ────────────────────────────────────────────────────
190
+
191
+ export async function install(targetDir, opts = {}) {
192
+ console.log(`📦 Cadet-Agent — installing to ${targetDir}\n`);
193
+
194
+ // 1. Fetch release metadata
195
+ const release = await fetchLatestRelease(opts.sourceUrl);
196
+ console.log(` Latest: ${release.tag_name} (published ${release.published_at})\n`);
197
+
198
+ // 2. Find zip asset
199
+ const asset = findZipAsset(release);
200
+
201
+ // 3. Download
202
+ const zipBuf = await downloadZip(asset.browser_download_url);
203
+ console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
204
+
205
+ // 4. Extract
206
+ console.log('📂 Extracting...');
207
+ const extracted = extractZip(zipBuf, targetDir);
208
+
209
+ // 5. Report
210
+ console.log(`\n✅ Cadet-Agent ${release.tag_name} installed! Extracted ${extracted.length} files.\n`);
211
+
212
+ // Print per-IDE next steps
213
+ console.log('── Next steps ──');
214
+ console.log(' GitHub Copilot:');
215
+ console.log(' Start a chat: /cadet');
216
+ console.log(' Cursor:');
217
+ console.log(' Already active — .cursor\\rules\\cadet-agent.md loads automatically');
218
+ console.log(' Continue:');
219
+ console.log(' Already active — .continue\\rules\\cadet-agent.md loads automatically');
220
+ console.log(' Claude Code:');
221
+ console.log(' Already active — .claude\\skills\\cadet-agent.md loads as a project skill');
222
+ console.log('');
223
+ }
224
+
225
+ // ── Manifest-aware extraction ───────────────────────────────────────────────
226
+
227
+ function matchesPreservedPath(filename, preservedPaths) {
228
+ // Normalize: strip leading dot (zip paths like ".cadet/agent/policies/...")
229
+ const normalized = filename.replace(/^\.?\/?/, '');
230
+ for (const preserved of preservedPaths) {
231
+ const p = preserved.replace(/^\.?\/?/, '');
232
+ if (normalized === p || normalized.startsWith(p + '/') || normalized.startsWith(p + '\\')) {
233
+ return true;
234
+ }
235
+ }
236
+ return false;
237
+ }
238
+
239
+ function extractZipWithManifest(buf, targetDir, { preserved, managed }) {
240
+ const eocdOff = findEocd(buf);
241
+ const cdSize = read32(buf, eocdOff + 12);
242
+ const cdOff = read32(buf, eocdOff + 16);
243
+
244
+ if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
245
+ throw new Error(
246
+ 'This zip uses ZIP64 format, which is not supported.\n' +
247
+ 'Try downloading cadet-agent.zip manually from:\n' +
248
+ ' https://github.com/naishtech/cadet-agent/releases/latest'
249
+ );
250
+ }
251
+
252
+ const updated = [];
253
+ const preserved_list = [];
254
+ const added = [];
255
+
256
+ for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
257
+ if (matchesPreservedPath(entry.filename, preserved)) {
258
+ preserved_list.push(entry.filename);
259
+ continue;
260
+ }
261
+ const outPath = extractFile(buf, entry, targetDir);
262
+ // Check if this file already existed (updated vs added)
263
+ // We can't easily tell without checking before extraction, so approximate:
264
+ // If it's under a managed path, call it updated; otherwise added
265
+ const isManaged = managed.some(m => {
266
+ const mn = m.replace(/^\.?\/?/, '');
267
+ const fn = entry.filename.replace(/^\.?\/?/, '');
268
+ return fn === mn || fn.startsWith(mn + '/') || fn.startsWith(mn + '\\');
269
+ });
270
+ if (isManaged) {
271
+ updated.push(outPath);
272
+ } else {
273
+ added.push(outPath);
274
+ }
275
+ }
276
+
277
+ return { updated, preserved: preserved_list, added };
278
+ }
279
+
280
+ // ── Public sync entry ───────────────────────────────────────────────────────
281
+
282
+ export async function sync(targetDir, opts = {}) {
283
+ console.log(`🔄 Cadet-Agent — syncing ${targetDir}\n`);
284
+
285
+ // 1. Read existing manifest
286
+ const manifestPath = join(targetDir, '.cadet', 'agent', 'core', 'FrameworkManifest.json');
287
+ let existingManifest = null;
288
+ let oldVersion = 'none';
289
+ try {
290
+ existingManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
291
+ oldVersion = existingManifest.frameworkVersion || 'unknown';
292
+ console.log(` Existing install: v${oldVersion}`);
293
+ } catch {
294
+ console.log(' No existing install found — performing full install.\n');
295
+ return install(targetDir, opts);
296
+ }
297
+
298
+ // 2. Fetch release metadata
299
+ const release = await fetchLatestRelease(opts.sourceUrl);
300
+ const newVersion = release.tag_name;
301
+ console.log(` Latest: ${newVersion} (published ${release.published_at})\n`);
302
+
303
+ if (oldVersion === newVersion) {
304
+ console.log(`✅ Already up to date (v${oldVersion}). Nothing to sync.\n`);
305
+ return;
306
+ }
307
+
308
+ // 3. Download
309
+ const asset = findZipAsset(release);
310
+ const zipBuf = await downloadZip(asset.browser_download_url);
311
+ console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
312
+
313
+ // 4. Extract with manifest awareness
314
+ console.log('📂 Extracting (preserving local policies and plans)...');
315
+ const result = extractZipWithManifest(zipBuf, targetDir, {
316
+ preserved: existingManifest.preservedPaths || [],
317
+ managed: existingManifest.managedPaths || [],
318
+ });
319
+
320
+ // 5. Report
321
+ console.log('');
322
+ console.log(`✅ Cadet-Agent synced: v${oldVersion} → ${newVersion}`);
323
+ console.log(` Updated: ${result.updated.length} files`);
324
+ if (result.preserved.length > 0) {
325
+ console.log(` Preserved: ${result.preserved.length} files (local policies/plans)`);
326
+ }
327
+ if (result.added.length > 0) {
328
+ console.log(` New: ${result.added.length} files`);
329
+ }
330
+ console.log('');
331
+
332
+ // Print per-IDE next steps
333
+ console.log('── Next steps ──');
334
+ console.log(' Framework files updated. Start a fresh chat for changes to take effect:');
335
+ console.log(' /cadet');
336
+ console.log('');
337
+ }