agentcraft 0.0.1 → 0.0.2

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.
@@ -0,0 +1,667 @@
1
+ # Sound Pack System Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Implement isolated, git-installable sound packs under `~/.agentcraft/packs/<publisher>/<name>/`, with a real `agentcraft` npm CLI and seamless migration from the old flat `~/.agentcraft/sounds/` layout.
6
+
7
+ **Architecture:** Each pack is a bare directory at `~/.agentcraft/packs/<publisher>/<name>/` — no manifest required. Assignment paths gain a `publisher/name:internal/path` prefix. A shared `web/lib/packs.ts` module handles all resolution so API routes stay thin. The `agentcraft` npm CLI wraps git for install/update/remove and launches the dashboard.
8
+
9
+ **Tech Stack:** Bun, Next.js 15 App Router, TypeScript, Bash, Node.js (CLI)
10
+
11
+ ---
12
+
13
+ ## Task 1: Save current config as defaults in agentcraft-sounds
14
+
15
+ Save the user's current `assignments.json` as the starter config shipped with the official pack.
16
+
17
+ **Files:**
18
+ - Create: `~/.agentcraft/packs/rohenaz/agentcraft-sounds/defaults/assignments.json` (after Task 2 migration)
19
+ - In agentcraft-sounds repo: create `defaults/assignments.json`
20
+
21
+ **Step 1: Copy current assignments into the agentcraft-sounds repo**
22
+
23
+ ```bash
24
+ mkdir -p ~/code/claude-sounds/defaults
25
+ cp ~/.agentcraft/assignments.json ~/code/claude-sounds/defaults/assignments.json
26
+ ```
27
+
28
+ **Step 2: Strip agent-specific hooks (keep only global + settings)**
29
+
30
+ Edit `~/code/claude-sounds/defaults/assignments.json` — keep `global`, `settings`, `skills` but clear `agents` to `{}` since agent names are user-specific.
31
+
32
+ **Step 3: Commit and push to agentcraft-sounds**
33
+
34
+ ```bash
35
+ cd ~/code/claude-sounds
36
+ git add defaults/assignments.json
37
+ git commit -m "Add default assignments — starter configuration for new installs"
38
+ git push
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Task 2: Migrate local directory structure
44
+
45
+ Move the existing `~/.agentcraft/sounds/` into the packs layout. The old path disappears; everything lives under `packs/` going forward.
46
+
47
+ **Step 1: Create packs directory and move sounds**
48
+
49
+ ```bash
50
+ mkdir -p ~/.agentcraft/packs/rohenaz
51
+ mv ~/.agentcraft/sounds ~/.agentcraft/packs/rohenaz/agentcraft-sounds
52
+ ```
53
+
54
+ **Step 2: Verify structure**
55
+
56
+ ```bash
57
+ ls ~/.agentcraft/packs/rohenaz/agentcraft-sounds/
58
+ # Expected: sc2 wc3 ff7 ff9 apps classic-os phones ui README.md
59
+ ```
60
+
61
+ **Step 3: Update existing assignments.json paths to use pack prefix**
62
+
63
+ Run this one-liner to prefix all sound paths:
64
+
65
+ ```bash
66
+ cd ~/.agentcraft
67
+ python3 -c "
68
+ import json, re, sys
69
+
70
+ with open('assignments.json') as f:
71
+ data = json.load(f)
72
+
73
+ PREFIX = 'rohenaz/agentcraft-sounds:'
74
+
75
+ def prefix_path(p):
76
+ if p and ':' not in p:
77
+ return PREFIX + p
78
+ return p
79
+
80
+ def walk(obj):
81
+ if isinstance(obj, dict):
82
+ return {k: walk(v) for k, v in obj.items()}
83
+ if isinstance(obj, str) and obj.endswith(('.mp3', '.wav', '.ogg', '.m4a')):
84
+ return prefix_path(obj)
85
+ return obj
86
+
87
+ result = walk(data)
88
+ with open('assignments.json', 'w') as f:
89
+ json.dump(result, f, indent=2)
90
+ print('Done')
91
+ "
92
+ ```
93
+
94
+ **Step 4: Verify assignments look correct**
95
+
96
+ ```bash
97
+ cat ~/.agentcraft/assignments.json | grep -o '"rohenaz/agentcraft-sounds:[^"]*"' | head -5
98
+ # Each sound path should now start with rohenaz/agentcraft-sounds:
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Task 3: Create `web/lib/packs.ts` — shared pack resolution module
104
+
105
+ All path logic lives here. API routes import from this module.
106
+
107
+ **Files:**
108
+ - Create: `web/lib/packs.ts`
109
+
110
+ **Step 1: Write the module**
111
+
112
+ ```typescript
113
+ // web/lib/packs.ts
114
+ import { readdir, stat } from 'fs/promises';
115
+ import { join } from 'path';
116
+ import { homedir } from 'os';
117
+
118
+ export const PACKS_DIR = join(homedir(), '.agentcraft', 'packs');
119
+ export const ASSIGNMENTS_PATH = join(homedir(), '.agentcraft', 'assignments.json');
120
+ export const WAVEFORM_CACHE = join(homedir(), '.agentcraft', 'waveforms.json');
121
+ const AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg', '.m4a']);
122
+
123
+ export interface Pack {
124
+ publisher: string;
125
+ name: string;
126
+ path: string; // absolute path to pack root
127
+ id: string; // "publisher/name"
128
+ }
129
+
130
+ /** List all installed packs */
131
+ export async function listPacks(): Promise<Pack[]> {
132
+ const packs: Pack[] = [];
133
+ let publishers: string[];
134
+ try {
135
+ publishers = await readdir(PACKS_DIR);
136
+ } catch {
137
+ return packs;
138
+ }
139
+ for (const publisher of publishers) {
140
+ const publisherPath = join(PACKS_DIR, publisher);
141
+ const ps = await stat(publisherPath).catch(() => null);
142
+ if (!ps?.isDirectory()) continue;
143
+ const names = await readdir(publisherPath).catch(() => [] as string[]);
144
+ for (const name of names) {
145
+ const packPath = join(publisherPath, name);
146
+ const ns = await stat(packPath).catch(() => null);
147
+ if (!ns?.isDirectory()) continue;
148
+ packs.push({ publisher, name, path: packPath, id: `${publisher}/${name}` });
149
+ }
150
+ }
151
+ return packs;
152
+ }
153
+
154
+ /**
155
+ * Resolve a pack-prefixed path to an absolute filesystem path.
156
+ * Format: "publisher/name:internal/path/to/sound.mp3"
157
+ * Legacy (no prefix): resolved against first installed pack that contains the file.
158
+ */
159
+ export function resolvePackPath(soundPath: string): string | null {
160
+ if (!soundPath) return null;
161
+ const colonIdx = soundPath.indexOf(':');
162
+ if (colonIdx === -1) {
163
+ // Legacy path — assume rohenaz/agentcraft-sounds
164
+ return join(PACKS_DIR, 'rohenaz', 'agentcraft-sounds', soundPath);
165
+ }
166
+ const packId = soundPath.slice(0, colonIdx);
167
+ const internal = soundPath.slice(colonIdx + 1);
168
+ if (!packId || !internal || internal.includes('..')) return null;
169
+ const [publisher, name] = packId.split('/');
170
+ if (!publisher || !name) return null;
171
+ return join(PACKS_DIR, publisher, name, internal);
172
+ }
173
+
174
+ /**
175
+ * Walk a directory recursively, returning all audio files.
176
+ * Returns paths relative to `base`, prefixed with `packId:`.
177
+ */
178
+ export async function walkPackDir(
179
+ dir: string,
180
+ base: string,
181
+ packId: string
182
+ ): Promise<Array<{ relPath: string; absPath: string }>> {
183
+ const results: Array<{ relPath: string; absPath: string }> = [];
184
+ let entries: Awaited<ReturnType<typeof readdir>>;
185
+ try {
186
+ entries = await readdir(dir, { withFileTypes: true });
187
+ } catch {
188
+ return results;
189
+ }
190
+ for (const entry of entries) {
191
+ const abs = join(dir, entry.name);
192
+ if (entry.isDirectory()) {
193
+ results.push(...await walkPackDir(abs, base, packId));
194
+ } else {
195
+ const ext = entry.name.slice(entry.name.lastIndexOf('.')).toLowerCase();
196
+ if (AUDIO_EXTS.has(ext)) {
197
+ const rel = abs.slice(base.length + 1); // relative to pack root
198
+ results.push({ relPath: `${packId}:${rel}`, absPath: abs });
199
+ }
200
+ }
201
+ }
202
+ return results;
203
+ }
204
+ ```
205
+
206
+ **Step 2: Verify TypeScript compiles**
207
+
208
+ ```bash
209
+ cd ~/code/agentcraft/web && bunx tsc --noEmit 2>&1 | grep packs
210
+ # Should produce no errors
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Task 4: Update `/api/sounds` route
216
+
217
+ **Files:**
218
+ - Modify: `web/app/api/sounds/route.ts`
219
+
220
+ **Step 1: Rewrite to use packs.ts**
221
+
222
+ Replace the entire file:
223
+
224
+ ```typescript
225
+ import { NextResponse } from 'next/server';
226
+ import { readFile, writeFile, mkdir } from 'fs/promises';
227
+ import { join, dirname } from 'path';
228
+ import { spawnSync } from 'child_process';
229
+ import { listPacks, walkPackDir, WAVEFORM_CACHE } from '@/lib/packs';
230
+ import type { SoundAsset } from '@/lib/types';
231
+
232
+ const BARS = 16;
233
+ const FALLBACK = [5, 7, 4, 9, 6, 8, 3, 7, 5, 8, 4, 6, 7, 5, 8, 4];
234
+
235
+ function computeWaveform(filePath: string): number[] {
236
+ const result = spawnSync('ffmpeg', [
237
+ '-i', filePath, '-ac', '1', '-ar', '1000', '-f', 'f32le', '-',
238
+ ], { maxBuffer: 1024 * 1024 * 4 });
239
+ if (result.status !== 0 || !result.stdout?.length) return FALLBACK;
240
+ const buf = result.stdout as Buffer;
241
+ const samples = buf.length / 4;
242
+ const blockSize = Math.floor(samples / BARS);
243
+ const values: number[] = [];
244
+ for (let i = 0; i < BARS; i++) {
245
+ let sum = 0;
246
+ for (let j = 0; j < blockSize; j++) {
247
+ sum += Math.abs(buf.readFloatLE((i * blockSize + j) * 4));
248
+ }
249
+ values.push(sum / blockSize);
250
+ }
251
+ const max = Math.max(...values, 0.0001);
252
+ return values.map((v) => Math.max(1, Math.round((v / max) * 10)));
253
+ }
254
+
255
+ async function loadCache(): Promise<Record<string, number[]>> {
256
+ try { return JSON.parse(await readFile(WAVEFORM_CACHE, 'utf-8')); }
257
+ catch { return {}; }
258
+ }
259
+
260
+ async function saveCache(cache: Record<string, number[]>) {
261
+ await mkdir(dirname(WAVEFORM_CACHE), { recursive: true });
262
+ await writeFile(WAVEFORM_CACHE, JSON.stringify(cache), 'utf-8');
263
+ }
264
+
265
+ export async function GET() {
266
+ try {
267
+ const packs = await listPacks();
268
+ const allFiles: Array<{ relPath: string; absPath: string }> = [];
269
+ for (const pack of packs) {
270
+ const files = await walkPackDir(pack.path, pack.path, pack.id);
271
+ allFiles.push(...files);
272
+ }
273
+
274
+ const cache = await loadCache();
275
+ let dirty = false;
276
+
277
+ const results: SoundAsset[] = allFiles.map(({ relPath, absPath }) => {
278
+ // relPath: "rohenaz/agentcraft-sounds:sc2/terran/session-start/scv-ready.mp3"
279
+ const internal = relPath.slice(relPath.indexOf(':') + 1);
280
+ const parts = internal.split('/');
281
+ const packId = relPath.slice(0, relPath.indexOf(':'));
282
+ const category = parts.length > 2
283
+ ? `${packId}:${parts.slice(0, -2).join('/')}`
284
+ : `${packId}:${parts[0]}`;
285
+ const subcategory = parts.length > 2 ? parts[parts.length - 2] : '';
286
+
287
+ const waveform = cache[relPath] ?? (() => {
288
+ const wf = computeWaveform(absPath);
289
+ cache[relPath] = wf;
290
+ dirty = true;
291
+ return wf;
292
+ })();
293
+
294
+ return {
295
+ id: relPath.replace(/\.[^/.]+$/, ''),
296
+ filename: parts[parts.length - 1],
297
+ category,
298
+ subcategory,
299
+ path: relPath,
300
+ waveform,
301
+ };
302
+ });
303
+
304
+ if (dirty) await saveCache(cache);
305
+ return NextResponse.json(results);
306
+ } catch (e) {
307
+ return NextResponse.json({ error: 'Failed to read sound library' }, { status: 500 });
308
+ }
309
+ }
310
+ ```
311
+
312
+ **Step 2: Start dev server and verify sounds load**
313
+
314
+ ```bash
315
+ cd ~/code/agentcraft/web && bun dev --port 4040 &
316
+ sleep 5
317
+ curl -s http://localhost:4040/api/sounds | jq '.[0]'
318
+ # Expect: path starts with "rohenaz/agentcraft-sounds:"
319
+ ```
320
+
321
+ ---
322
+
323
+ ## Task 5: Update `/api/audio` and `/api/preview` routes
324
+
325
+ **Files:**
326
+ - Modify: `web/app/api/audio/[...path]/route.ts`
327
+ - Modify: `web/app/api/preview/route.ts`
328
+
329
+ **Step 1: Update audio route**
330
+
331
+ ```typescript
332
+ import { NextRequest, NextResponse } from 'next/server';
333
+ import { readFile } from 'fs/promises';
334
+ import { resolvePackPath } from '@/lib/packs';
335
+
336
+ export async function GET(
337
+ req: NextRequest,
338
+ { params }: { params: Promise<{ path: string[] }> }
339
+ ) {
340
+ try {
341
+ const { path: pathParts } = await params;
342
+ // Reconstruct the full pack path — URL encodes ":" as %3A
343
+ const soundPath = decodeURIComponent(pathParts.join('/'));
344
+ if (soundPath.includes('..')) {
345
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
346
+ }
347
+ const fullPath = resolvePackPath(soundPath);
348
+ if (!fullPath) return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
349
+ const data = await readFile(fullPath);
350
+ const ext = soundPath.split('.').pop()?.toLowerCase();
351
+ const contentType = ext === 'mp3' ? 'audio/mpeg' : ext === 'wav' ? 'audio/wav' : 'audio/mpeg';
352
+ return new NextResponse(data, {
353
+ headers: { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=86400' },
354
+ });
355
+ } catch {
356
+ return NextResponse.json({ error: 'Audio file not found' }, { status: 404 });
357
+ }
358
+ }
359
+ ```
360
+
361
+ **Step 2: Update preview route**
362
+
363
+ ```typescript
364
+ import { NextRequest, NextResponse } from 'next/server';
365
+ import { spawn } from 'child_process';
366
+ import { resolvePackPath } from '@/lib/packs';
367
+
368
+ export async function POST(req: NextRequest) {
369
+ try {
370
+ const { path } = await req.json();
371
+ if (!path || typeof path !== 'string') {
372
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
373
+ }
374
+ const fullPath = resolvePackPath(path);
375
+ if (!fullPath) return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
376
+ spawn('afplay', [fullPath], { detached: true, stdio: 'ignore' }).unref();
377
+ return NextResponse.json({ ok: true });
378
+ } catch {
379
+ return NextResponse.json({ error: 'Playback failed' }, { status: 500 });
380
+ }
381
+ }
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Task 6: Update `/api/ui-sounds` route
387
+
388
+ **Files:**
389
+ - Modify: `web/app/api/ui-sounds/route.ts`
390
+
391
+ **Step 1: Rewrite to scan ui/ in all packs**
392
+
393
+ ```typescript
394
+ import { NextResponse } from 'next/server';
395
+ import { listPacks, walkPackDir } from '@/lib/packs';
396
+
397
+ interface UISound {
398
+ path: string; // pack-prefixed, e.g. "rohenaz/agentcraft-sounds:ui/sc2/click.mp3"
399
+ filename: string;
400
+ group: string; // e.g. "sc2"
401
+ }
402
+
403
+ export async function GET() {
404
+ const packs = await listPacks();
405
+ const results: UISound[] = [];
406
+
407
+ for (const pack of packs) {
408
+ const uiDir = `${pack.path}/ui`;
409
+ const files = await walkPackDir(uiDir, pack.path, pack.id).catch(() => []);
410
+ for (const { relPath } of files) {
411
+ const internal = relPath.slice(relPath.indexOf(':') + 1); // "ui/sc2/click.mp3"
412
+ const parts = internal.split('/'); // ["ui", "sc2", "click.mp3"]
413
+ if (parts[0] !== 'ui') continue;
414
+ const group = parts[1] ?? ''; // "sc2"
415
+ results.push({ path: relPath, filename: parts[parts.length - 1], group });
416
+ }
417
+ }
418
+
419
+ return NextResponse.json(results);
420
+ }
421
+ ```
422
+
423
+ ---
424
+
425
+ ## Task 7: Update hook script to resolve pack paths
426
+
427
+ **Files:**
428
+ - Modify: `hooks/play-sound.sh`
429
+
430
+ **Step 1: Replace `LIBRARY` constant and resolution logic**
431
+
432
+ Replace the `LIBRARY` line and the `FULL=` line at the bottom:
433
+
434
+ ```bash
435
+ PACKS="$HOME/.agentcraft/packs"
436
+
437
+ # ...existing lookup logic unchanged...
438
+
439
+ [ -z "$SOUND" ] && exit 0
440
+
441
+ # Resolve pack-prefixed path: "publisher/name:internal/path"
442
+ if echo "$SOUND" | grep -q ':'; then
443
+ PACK_ID="${SOUND%%:*}"
444
+ INTERNAL="${SOUND#*:}"
445
+ PUBLISHER="${PACK_ID%%/*}"
446
+ PACKNAME="${PACK_ID##*/}"
447
+ FULL="$PACKS/$PUBLISHER/$PACKNAME/$INTERNAL"
448
+ else
449
+ # Legacy path — fall back to rohenaz/agentcraft-sounds
450
+ FULL="$PACKS/rohenaz/agentcraft-sounds/$SOUND"
451
+ fi
452
+
453
+ [ ! -f "$FULL" ] && exit 0
454
+ ```
455
+
456
+ **Step 2: Test the hook manually**
457
+
458
+ ```bash
459
+ echo '{"hook_event_name":"SessionStart","agent_type":""}' | bash ~/code/agentcraft/hooks/play-sound.sh
460
+ # Should play the SessionStart sound
461
+ ```
462
+
463
+ ---
464
+
465
+ ## Task 8: Update slash command
466
+
467
+ **Files:**
468
+ - Modify: `commands/agentcraft.md`
469
+
470
+ **Step 1: Update first-run check to use packs/ directory**
471
+
472
+ Replace the sounds presence check and clone command:
473
+
474
+ ```markdown
475
+ Check if the official sound pack is installed:
476
+ ```bash
477
+ ls ~/.agentcraft/packs/rohenaz/agentcraft-sounds 2>/dev/null | head -1
478
+ ```
479
+
480
+ If that returned nothing, install it:
481
+ ```bash
482
+ agentcraft pack install rohenaz/agentcraft-sounds 2>/dev/null || \
483
+ git clone https://github.com/rohenaz/agentcraft-sounds ~/.agentcraft/packs/rohenaz/agentcraft-sounds
484
+ ```
485
+ ```
486
+
487
+ Also add `Bash(agentcraft:*)` to `allowed-tools`.
488
+
489
+ ---
490
+
491
+ ## Task 9: Build real `agentcraft` CLI
492
+
493
+ **Files:**
494
+ - Modify: `bin/agentcraft.js`
495
+
496
+ **Step 1: Write the full CLI**
497
+
498
+ ```javascript
499
+ #!/usr/bin/env node
500
+ 'use strict';
501
+
502
+ const { execSync, spawnSync } = require('child_process');
503
+ const { existsSync, readdirSync, rmSync, statSync } = require('fs');
504
+ const { join } = require('path');
505
+ const { homedir } = require('os');
506
+
507
+ const PACKS_DIR = join(homedir(), '.agentcraft', 'packs');
508
+ const [,, cmd, sub, ...rest] = process.argv;
509
+
510
+ function ensurePacksDir() {
511
+ require('fs').mkdirSync(PACKS_DIR, { recursive: true });
512
+ }
513
+
514
+ function parsePackId(arg) {
515
+ if (!arg || !arg.includes('/')) {
516
+ console.error(`Error: pack must be "publisher/name", got: ${arg}`);
517
+ process.exit(1);
518
+ }
519
+ const [publisher, name] = arg.split('/');
520
+ return { publisher, name, url: `https://github.com/${publisher}/${name}` };
521
+ }
522
+
523
+ function listPacks() {
524
+ if (!existsSync(PACKS_DIR)) { console.log('No packs installed.'); return []; }
525
+ const packs = [];
526
+ for (const publisher of readdirSync(PACKS_DIR)) {
527
+ const ppath = join(PACKS_DIR, publisher);
528
+ if (!statSync(ppath).isDirectory()) continue;
529
+ for (const name of readdirSync(ppath)) {
530
+ if (statSync(join(ppath, name)).isDirectory()) packs.push(`${publisher}/${name}`);
531
+ }
532
+ }
533
+ return packs;
534
+ }
535
+
536
+ if (cmd === 'pack') {
537
+ if (sub === 'install') {
538
+ const { publisher, name, url } = parsePackId(rest[0]);
539
+ const dest = join(PACKS_DIR, publisher, name);
540
+ if (existsSync(dest)) { console.log(`Already installed: ${publisher}/${name}`); process.exit(0); }
541
+ ensurePacksDir();
542
+ require('fs').mkdirSync(join(PACKS_DIR, publisher), { recursive: true });
543
+ console.log(`Installing ${publisher}/${name} from ${url}...`);
544
+ const r = spawnSync('git', ['clone', url, dest], { stdio: 'inherit' });
545
+ if (r.status !== 0) { console.error('Install failed.'); process.exit(1); }
546
+ console.log(`Installed: ${publisher}/${name}`);
547
+
548
+ } else if (sub === 'remove') {
549
+ const { publisher, name } = parsePackId(rest[0]);
550
+ const dest = join(PACKS_DIR, publisher, name);
551
+ if (!existsSync(dest)) { console.error(`Not installed: ${publisher}/${name}`); process.exit(1); }
552
+ rmSync(dest, { recursive: true, force: true });
553
+ console.log(`Removed: ${publisher}/${name}`);
554
+
555
+ } else if (sub === 'update') {
556
+ const all = rest[0] === '--all';
557
+ const targets = all ? listPacks() : [rest[0]];
558
+ for (const packId of targets) {
559
+ const { publisher, name } = parsePackId(packId);
560
+ const dest = join(PACKS_DIR, publisher, name);
561
+ if (!existsSync(dest)) { console.error(`Not installed: ${packId}`); continue; }
562
+ console.log(`Updating ${packId}...`);
563
+ spawnSync('git', ['-C', dest, 'pull'], { stdio: 'inherit' });
564
+ }
565
+
566
+ } else if (sub === 'list') {
567
+ const packs = listPacks();
568
+ if (!packs.length) { console.log('No packs installed.'); }
569
+ else { console.log('Installed packs:\n' + packs.map(p => ` ${p}`).join('\n')); }
570
+
571
+ } else {
572
+ console.log('Usage: agentcraft pack <install|remove|update|list> [publisher/name] [--all]');
573
+ }
574
+
575
+ } else if (cmd === 'start') {
576
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
577
+ if (!pluginRoot) { console.error('CLAUDE_PLUGIN_ROOT not set. Run from the Claude Code plugin.'); process.exit(1); }
578
+ execSync(`cd "${pluginRoot}/web" && bun dev --port 4040`, { stdio: 'inherit' });
579
+
580
+ } else {
581
+ console.log(`AgentCraft CLI
582
+
583
+ Usage:
584
+ agentcraft pack install <publisher/name> Install a sound pack
585
+ agentcraft pack remove <publisher/name> Remove a sound pack
586
+ agentcraft pack update <publisher/name> Update a pack (git pull)
587
+ agentcraft pack update --all Update all packs
588
+ agentcraft pack list List installed packs
589
+ agentcraft start Launch the dashboard
590
+
591
+ Install the Claude Code plugin:
592
+ claude plugin install agentcraft@rohenaz
593
+ `);
594
+ }
595
+ ```
596
+
597
+ **Step 2: Test locally**
598
+
599
+ ```bash
600
+ node ~/code/agentcraft/bin/agentcraft.js pack list
601
+ # Should list rohenaz/agentcraft-sounds
602
+
603
+ node ~/code/agentcraft/bin/agentcraft.js
604
+ # Should show usage
605
+ ```
606
+
607
+ ---
608
+
609
+ ## Task 10: Add `pack.json` to agentcraft-sounds
610
+
611
+ **Files:**
612
+ - Create: `~/code/claude-sounds/pack.json`
613
+
614
+ ```json
615
+ {
616
+ "name": "agentcraft-sounds",
617
+ "publisher": "rohenaz",
618
+ "version": "2.0.0",
619
+ "description": "Official AgentCraft sound library — SC2, WC3, FF7, FF9, phones, apps, classic OS",
620
+ "types": ["sounds", "ui"]
621
+ }
622
+ ```
623
+
624
+ ```bash
625
+ cd ~/code/claude-sounds
626
+ git add pack.json
627
+ git commit -m "Add pack.json manifest"
628
+ git push
629
+ ```
630
+
631
+ ---
632
+
633
+ ## Task 11: Bump versions and publish
634
+
635
+ **Step 1: Bump Claude plugin to 0.0.9**
636
+
637
+ In `.claude-plugin/plugin.json`: `"version": "0.0.9"`
638
+
639
+ **Step 2: Publish npm CLI as 0.0.2**
640
+
641
+ In `package.json`: `"version": "0.0.2"`
642
+
643
+ ```bash
644
+ cd ~/code/agentcraft
645
+ git add -A
646
+ git commit -m "v0.0.9: Sound pack system — packs/, CLI pack commands, path migration"
647
+ git push
648
+ ```
649
+
650
+ Then publish with OTP.
651
+
652
+ **Step 3: Update plugin**
653
+
654
+ ```bash
655
+ CLAUDECODE=1 claude plugin update agentcraft@rohenaz
656
+ ```
657
+
658
+ ---
659
+
660
+ ## Task 12: Update README
661
+
662
+ Update `README.md` to document the pack system:
663
+
664
+ - Replace "Sound Library" section with "Sound Packs"
665
+ - Show `agentcraft pack install` as the way to get sounds
666
+ - Document `agentcraft pack list/update/remove`
667
+ - Note that manual `git clone` into `~/.agentcraft/packs/publisher/name/` works identically