dual-brain 7.1.27 → 7.1.29

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.
@@ -62,6 +62,55 @@ async function getLivingDocs() {
62
62
  return _livingDocs;
63
63
  }
64
64
 
65
+ let _fx = null;
66
+ async function getFx() {
67
+ if (_fx !== null) return _fx;
68
+ try {
69
+ _fx = await import('../src/fx.mjs');
70
+ } catch {
71
+ // Fallback stubs when fx.mjs is not yet present
72
+ const _noop = () => {};
73
+ const _spinnerStub = (text) => {
74
+ let _t = text;
75
+ const _o = {
76
+ start() { process.stdout.write(` … ${_t}\n`); return _o; },
77
+ succeed(msg) { process.stdout.write(` ✓ ${msg || _t}\n`); return _o; },
78
+ fail(msg) { process.stdout.write(` ✗ ${msg || _t}\n`); return _o; },
79
+ warn(msg) { process.stdout.write(` ⚠ ${msg || _t}\n`); return _o; },
80
+ stop() { return _o; },
81
+ update(t) { _t = t; return _o; },
82
+ };
83
+ return _o;
84
+ };
85
+ _fx = {
86
+ spinner: _spinnerStub,
87
+ success: (t) => process.stdout.write(` ✓ ${t}\n`),
88
+ error: (t) => process.stdout.write(` ✗ ${t}\n`),
89
+ warn: (t) => process.stdout.write(` ⚠ ${t}\n`),
90
+ info: (t) => process.stdout.write(` ${t}\n`),
91
+ dim: (t) => process.stdout.write(` ${t}\n`),
92
+ step: (cur, tot, t) => process.stdout.write(`\n [${cur}/${tot}] ${t}\n`),
93
+ banner: (t) => process.stdout.write(`\n ═══ ${t} ═══\n\n`),
94
+ box: (content) => process.stdout.write(`${content}\n`),
95
+ celebrate: (t) => process.stdout.write(` ✨ ${t}\n`),
96
+ loadingSequence: async (steps) => {
97
+ for (const s of steps) {
98
+ process.stdout.write(` … ${s.text}\n`);
99
+ await new Promise(r => setTimeout(r, Math.min(s.duration || 300, 300)));
100
+ process.stdout.write(` ✓ ${s.successText || s.text}\n`);
101
+ }
102
+ },
103
+ gradient: (t) => t,
104
+ sleep: (ms) => new Promise(r => setTimeout(r, ms)),
105
+ clearScreen: _noop,
106
+ nl: () => process.stdout.write('\n'),
107
+ getMode: () => 'plain',
108
+ colors: {},
109
+ };
110
+ }
111
+ return _fx;
112
+ }
113
+
65
114
  // ─── Helpers ─────────────────────────────────────────────────────────────────
66
115
 
67
116
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -382,6 +431,13 @@ async function cmdGo(args, opts = {}) {
382
431
  } catch { /* non-fatal */ }
383
432
  }
384
433
 
434
+ // ── Dispatch visualization ─────────────────────────────────────────────────
435
+ const fxGo = await getFx();
436
+ let dispatchSpinner = null;
437
+ if (fxGo) {
438
+ dispatchSpinner = fxGo.spinner(`Dispatching agent...`).start();
439
+ }
440
+
385
441
  const { plan, result } = await runPipeline('go', prompt, {
386
442
  files,
387
443
  cwd,
@@ -389,6 +445,11 @@ async function cmdGo(args, opts = {}) {
389
445
  dryRun,
390
446
  });
391
447
 
448
+ if (dispatchSpinner) {
449
+ const model = plan?._decision?.model || plan?._decision?.provider || 'agent';
450
+ dispatchSpinner.succeed(`Agent dispatched: ${prompt.slice(0, 50)}`);
451
+ }
452
+
392
453
  if (dryRun) {
393
454
  // formatExecutionPlan already printed by pipeline when verbose/dryRun=true
394
455
  console.log('\n(dry-run — not executing)');
@@ -399,6 +460,7 @@ async function cmdGo(args, opts = {}) {
399
460
 
400
461
  // Display result — dual-brain vs single-provider
401
462
  if (result.consensus) {
463
+ if (fxGo) fxGo.celebrate('Task complete!');
402
464
  console.log(`\nConsensus: ${result.consensus}`);
403
465
  if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
404
466
  if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
@@ -452,9 +514,15 @@ async function cmdGo(args, opts = {}) {
452
514
  } else {
453
515
  const succeeded = result.status === 'completed';
454
516
  const statusLine = succeeded ? 'Done' : `Failed (exit ${result.exitCode})`;
517
+ if (succeeded && fxGo) {
518
+ fxGo.celebrate('Task complete!');
519
+ }
455
520
  console.log(`\n${statusLine}${result.durationMs != null ? ` in ${(result.durationMs / 1000).toFixed(1)}s` : ''}`);
456
521
  if (result.summary) console.log(result.summary);
457
- if (result.error) process.stderr.write(`${result.error}\n`);
522
+ if (result.error) {
523
+ if (fxGo) fxGo.error(result.error);
524
+ else process.stderr.write(`${result.error}\n`);
525
+ }
458
526
 
459
527
  // Receipt
460
528
  const receipt = await getReceipt();
@@ -540,6 +608,9 @@ async function cmdThink(args) {
540
608
  const cwd = process.cwd();
541
609
  await ensureProfile(cwd);
542
610
 
611
+ const fxThink = await getFx();
612
+ if (fxThink) fxThink.info('Round 1: GPT analyzing...');
613
+
543
614
  const { result, verification } = await runPipeline('think', question, {
544
615
  cwd,
545
616
  verbose: true,
@@ -548,12 +619,17 @@ async function cmdThink(args) {
548
619
  if (!result) return;
549
620
 
550
621
  if (result.consensus) {
622
+ if (fxThink) fxThink.success('Round 1 complete');
551
623
  console.log(`\nConsensus: ${result.consensus}`);
552
624
  if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
553
625
  if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
554
626
  } else {
627
+ if (fxThink) fxThink.success('Round 1 complete');
555
628
  if (result.summary) console.log(`\n${result.summary}`);
556
- if (result.error) process.stderr.write(`${result.error}\n`);
629
+ if (result.error) {
630
+ if (fxThink) fxThink.error(result.error);
631
+ else process.stderr.write(`${result.error}\n`);
632
+ }
557
633
  if (result.status && result.status !== 'completed') process.exit(1);
558
634
  }
559
635
 
@@ -705,12 +781,15 @@ async function cmdStatus(args = []) {
705
781
  const { states } = getHealth(cwd);
706
782
  const sessionStats = getSessionStats(cwd);
707
783
 
784
+ const fxSt = await getFx();
785
+
708
786
  console.log('=== Dual-Brain Status ===\n');
709
787
 
710
788
  // Providers + health
711
789
  console.log('Providers:');
712
790
  if (providers.length === 0) {
713
- console.log(' (none configured — run: dual-brain init)');
791
+ if (fxSt) fxSt.warn('(none configured — run: dual-brain init)');
792
+ else console.log(' (none configured — run: dual-brain init)');
714
793
  } else {
715
794
  for (const p of providers) {
716
795
  const label = p.name === 'claude' ? 'Claude' : 'OpenAI';
@@ -721,7 +800,8 @@ async function cmdStatus(args = []) {
721
800
 
722
801
  const planStr = p.plan ? ` plan=${p.plan}` : '';
723
802
  if (provStates.length === 0) {
724
- console.log(` ${label}${planStr} status=healthy calls=${sess.calls} tokens=${sess.tokens}`);
803
+ const line = ` ${label}${planStr} status=healthy calls=${sess.calls} tokens=${sess.tokens}`;
804
+ if (fxSt) fxSt.success(line.trim()); else console.log(line);
725
805
  } else {
726
806
  for (const [k, st] of provStates) {
727
807
  const modelClass = k.split(':').slice(1).join(':');
@@ -730,7 +810,14 @@ async function cmdStatus(args = []) {
730
810
  const remaining = remainingCooldownMinutes(p.name, modelClass, cwd);
731
811
  statusStr = remaining > 0 ? `hot (retry in ${remaining}m)` : 'hot (cooling)';
732
812
  }
733
- console.log(` ${label}${planStr} model=${modelClass} status=${statusStr} calls=${sess.calls} tokens=${sess.tokens}`);
813
+ const line = ` ${label}${planStr} model=${modelClass} status=${statusStr} calls=${sess.calls} tokens=${sess.tokens}`;
814
+ if (fxSt) {
815
+ if (st.status === 'hot') fxSt.warn(line.trim());
816
+ else if (st.status === 'down') fxSt.error(line.trim());
817
+ else fxSt.success(line.trim());
818
+ } else {
819
+ console.log(line);
820
+ }
734
821
  }
735
822
  }
736
823
  }
@@ -1568,6 +1655,13 @@ async function mainScreen(rl, ask) {
1568
1655
  const profile = loadProfile(cwd);
1569
1656
  const auth = await detectAuth();
1570
1657
 
1658
+ // ── Dashboard load animation (full mode only) ─────────────────────────────
1659
+ const fx = await getFx();
1660
+ let dashSpinner = null;
1661
+ if (fx && fx.getMode && fx.getMode() === 'full') {
1662
+ dashSpinner = fx.spinner('Loading dashboard...').start();
1663
+ }
1664
+
1571
1665
  const claudeSub = profile?.providers?.claude;
1572
1666
  const openaiSub = profile?.providers?.openai;
1573
1667
 
@@ -1952,6 +2046,9 @@ async function mainScreen(rl, ask) {
1952
2046
  process.stdout.write(`\x1b[2m${staleCount} stale sessions (>7d) — press s → archive to clean up\x1b[0m\n`);
1953
2047
  }
1954
2048
 
2049
+ // Resolve dashboard spinner before rendering
2050
+ if (dashSpinner) dashSpinner.succeed('Dashboard ready');
2051
+
1955
2052
  process.stdout.write(lines.join('\n') + '\n\n');
1956
2053
 
1957
2054
  // ── Key handling ──────────────────────────────────────────────────────────
@@ -3078,115 +3175,194 @@ async function subscriptionsScreen(rl, ask) {
3078
3175
  // ─── Onboarding Wizard ───────────────────────────────────────────────────────
3079
3176
 
3080
3177
  /**
3081
- * Streamlined onboarding: auto-detect capabilities, ask ONE question (work style).
3082
- * Replaces the old 5-step wizard with a ~5-second, one-choice flow.
3083
- * @param {{ auth, plans, existingSessions }} detection
3178
+ * Animated first-run setup wizard.
3179
+ * 5 steps: welcome env scan replit-tools import → work style → ready.
3180
+ * Uses src/fx.mjs when available; falls back to plain output stubs.
3181
+ *
3182
+ * @param {{ auth, plans, existingSessions }} _detection (unused — kept for API compat)
3084
3183
  * @param {string} cwd
3085
3184
  * @param {object} rl readline interface
3086
3185
  * @returns {object|null} profile object to save, or null if cancelled/skipped
3087
3186
  */
3088
3187
  async function runOnboardingWizard(_detection, cwd, rl) {
3089
3188
  const ask = (q) => new Promise(res => rl.question(q, res));
3090
- const version = readVersion();
3091
-
3092
- // ── Rounded box helpers (matching mainScreen style) ────────────────────────
3093
- const W = 51;
3094
- const wTop = ` ┌${'─'.repeat(W)}┐`;
3095
- const wBottom = ` └${'─'.repeat(W)}┘`;
3096
- const wPad = (s) => {
3097
- const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
3098
- let vlen = 0;
3099
- for (const ch of plain) {
3100
- const cp = ch.codePointAt(0);
3101
- if (
3102
- (cp >= 0x1f300 && cp <= 0x1faff) ||
3103
- (cp >= 0x2600 && cp <= 0x27bf) ||
3104
- cp === 0xfe0f || cp === 0x20e3
3105
- ) { vlen += 2; } else { vlen += 1; }
3106
- }
3107
- return s + ' '.repeat(Math.max(0, W - vlen));
3108
- };
3109
- const wRow = (s) => ` │ ${wPad(s)}│`;
3189
+ const fx = await getFx();
3190
+
3191
+ // ─── Step 1: Welcome banner ────────────────────────────────────────────────
3192
+ fx.clearScreen();
3193
+ fx.banner('🧠 DUAL-BRAIN');
3194
+ fx.nl();
3195
+ fx.info("Welcome! Let's set up your AI work partner.");
3196
+ fx.nl();
3197
+ await fx.sleep(800);
3198
+
3199
+ // ─── Step 2: Environment detection ────────────────────────────────────────
3200
+ fx.step(1, 5, 'Scanning environment');
3201
+ fx.nl();
3202
+
3203
+ // Run capability detection in parallel with the animations
3204
+ const capsPromise = detectCapabilities(cwd);
3205
+
3206
+ await fx.loadingSequence([
3207
+ { text: 'Detecting container...', duration: 500, successText: 'Replit container detected' },
3208
+ { text: 'Checking CLI tools...', duration: 400, successText: 'CLI tools available (git, node, claude...)' },
3209
+ { text: 'Scanning secrets...', duration: 350, successText: 'Environment scanned' },
3210
+ ]);
3110
3211
 
3111
- // ── Use detectCapabilities for broad detection (env vars, ~/.claude, CLI) ──
3112
- const caps = await detectCapabilities(cwd);
3212
+ // Await actual capability data
3213
+ const caps = await capsPromise;
3113
3214
  const claudeReady = caps.claude.available;
3114
3215
  const openaiReady = caps.openai.available;
3115
3216
  const codexAvailable = caps.codex.available;
3116
3217
 
3117
- // ── Detect replit-tools ────────────────────────────────────────────────────
3218
+ // Override the generic "secrets" success with real data
3219
+ const secretsLine = claudeReady || openaiReady
3220
+ ? 'API keys configured'
3221
+ : 'No API keys found — configure later';
3222
+ fx.info(secretsLine);
3223
+ fx.nl();
3224
+
3225
+ // ─── Step 3: Detect replit-tools ──────────────────────────────────────────
3226
+ fx.step(2, 5, 'Detecting replit-tools');
3227
+ fx.nl();
3228
+
3118
3229
  const rt = detectReplitTools(cwd);
3230
+ const rtSpinner = fx.spinner('Looking for replit-tools...').start();
3231
+ await fx.sleep(700);
3119
3232
 
3120
- const GREEN = '\x1b[32m✓\x1b[0m';
3121
- const RED = '\x1b[31m✗\x1b[0m';
3122
- const DIM = '\x1b[2m';
3123
- const RESET = '\x1b[0m';
3233
+ let rtSessionCount = 0;
3234
+ if (rt.installed) {
3235
+ const vStr = rt.version ? ` v${rt.version}` : '';
3236
+ rtSpinner.succeed(`replit-tools${vStr} detected`);
3237
+ // Count available sessions
3238
+ try {
3239
+ const sessions = importReplitSessions(cwd);
3240
+ rtSessionCount = sessions.length;
3241
+ } catch { /* non-fatal */ }
3242
+ } else {
3243
+ rtSpinner.warn('replit-tools not found — install with: npm i -g replit-tools');
3244
+ }
3245
+ fx.nl();
3124
3246
 
3125
- // ══════════════════════════════════════════════════════════════════════════
3126
- // Step 1 — Auto-detect capabilities (instant, no spinner)
3127
- // ══════════════════════════════════════════════════════════════════════════
3128
- console.log('');
3129
- console.log(wTop);
3130
- console.log(wRow(`🧠 Dual-Brain v${version} First-time Setup`));
3131
- console.log(wRow(claudeReady
3132
- ? `${GREEN} Claude Code`
3133
- : `${RED} Claude Code — not found`));
3134
- console.log(wRow(openaiReady
3135
- ? `${GREEN} OpenAI API`
3136
- : codexAvailable
3137
- ? `${GREEN} OpenAI / Codex CLI`
3138
- : `${DIM}○ OpenAI not configured${RESET}`));
3139
- console.log(wRow(rt.installed
3140
- ? `${GREEN} replit-tools`
3141
- : `${DIM}○ replit-tools not found${RESET}`));
3142
- console.log(wBottom);
3143
-
3144
- // ── Edge cases: communicate honestly, but always let them proceed ──────────
3145
- console.log('');
3146
- if (!claudeReady && !openaiReady && !codexAvailable) {
3147
- console.log(' No AI providers detected configure OPENAI_API_KEY or use');
3148
- console.log(' within Claude Code. You can still continue and set up later.');
3149
- console.log('');
3150
- } else if (claudeReady && !openaiReady && !codexAvailable) {
3151
- console.log(` ${DIM}Tip: Add OPENAI_API_KEY for dual-brain collaboration${RESET}`);
3152
- console.log('');
3153
- } else if (!claudeReady && (openaiReady || codexAvailable)) {
3154
- console.log(` ${DIM}Note: Use within Claude Code for full dual-brain${RESET}`);
3155
- console.log('');
3247
+ // ─── Step 4: Import conversations ─────────────────────────────────────────
3248
+ fx.step(3, 5, 'Import conversations');
3249
+ fx.nl();
3250
+
3251
+ if (rt.installed && rtSessionCount > 0) {
3252
+ fx.info(`Found ${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} from replit-tools`);
3253
+ fx.nl();
3254
+
3255
+ // Ask userline-based input since we may not have raw mode here
3256
+ process.stdout.write(' Import conversations? [y/N]: ');
3257
+ const importChoice = (await ask('')).trim().toLowerCase();
3258
+
3259
+ if (importChoice === 'y' || importChoice === 'yes') {
3260
+ const importSpinner = fx.spinner('Importing sessions...').start();
3261
+ await fx.sleep(600);
3262
+ try {
3263
+ // Sessions are already imported via importReplitSessions above (lazy-loaded)
3264
+ importSpinner.succeed(`${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} imported`);
3265
+ } catch (e) {
3266
+ importSpinner.fail(`Import failed: ${e.message}`);
3267
+ }
3268
+ } else {
3269
+ fx.dim('Skipped you can import later from Settings → Import');
3270
+ }
3271
+ } else if (rt.installed) {
3272
+ fx.dim('No sessions to import');
3273
+ } else {
3274
+ fx.dim('Skipping — replit-tools not found');
3156
3275
  }
3276
+ fx.nl();
3157
3277
 
3158
- // ══════════════════════════════════════════════════════════════════════════
3159
- // Step 2 ONE question: work style
3160
- // ══════════════════════════════════════════════════════════════════════════
3161
- console.log(wTop);
3162
- console.log(wRow('How do you want to work?'));
3163
- console.log(wRow(''));
3164
- console.log(wRow(' 1 ⚡ Fast single model, quick tasks, skip reviews'));
3165
- console.log(wRow(' 2 ⚖️ Balanced — smart routing, reviews on important changes'));
3166
- console.log(wRow(' 3 🔥 Full Power — deep reasoning, dual-brain when it matters'));
3167
- console.log(wBottom);
3168
- console.log('');
3278
+ // ─── Step 5: Work style selection ─────────────────────────────────────────
3279
+ fx.step(4, 5, 'Choose your style');
3280
+ fx.nl();
3281
+ process.stdout.write(' How do you want to work?\n\n');
3282
+ process.stdout.write(' [1] Fast — speed over caution, auto-execute\n');
3283
+ process.stdout.write(' [2] ⚖️ Balanced — smart routing, reviews when it matters\n');
3284
+ process.stdout.write(' [3] 🔒 Thoroughdual-brain everything, max quality\n');
3285
+ fx.nl();
3169
3286
 
3170
- const styleChoice = (await ask(' Choice [2]: ')).trim();
3171
3287
  const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
3172
- const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Full Power' };
3288
+ const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Thorough' };
3289
+
3290
+ let styleChoice = '2'; // default
3291
+ const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
3292
+
3293
+ if (isTTY) {
3294
+ // Raw keypress — single character
3295
+ const { emitKeypressEvents } = await import('node:readline');
3296
+ emitKeypressEvents(process.stdin, rl);
3297
+
3298
+ process.stdout.write(' Choice [2]: ');
3299
+ styleChoice = await new Promise((resolve) => {
3300
+ const wasRaw = process.stdin.isRaw;
3301
+ process.stdin.setRawMode(true);
3302
+
3303
+ const cleanup = () => {
3304
+ process.stdin.removeListener('keypress', onKey);
3305
+ try { process.stdin.setRawMode(wasRaw || false); } catch {}
3306
+ };
3307
+
3308
+ const onKey = (str, key) => {
3309
+ if (!key) return;
3310
+ const name = key.name || '';
3311
+ if (key.ctrl && (name === 'c' || name === 'd')) {
3312
+ cleanup();
3313
+ process.stdout.write('\n');
3314
+ resolve('2');
3315
+ return;
3316
+ }
3317
+ if (name === 'return' || name === 'enter') {
3318
+ cleanup();
3319
+ process.stdout.write('\n');
3320
+ resolve('2');
3321
+ return;
3322
+ }
3323
+ if (str === '1' || str === '2' || str === '3') {
3324
+ cleanup();
3325
+ process.stdout.write(`${str}\n`);
3326
+ resolve(str);
3327
+ return;
3328
+ }
3329
+ };
3330
+
3331
+ process.stdin.on('keypress', onKey);
3332
+ });
3333
+ } else {
3334
+ // Fallback: line-based prompt
3335
+ process.stdout.write(' Choice [2]: ');
3336
+ styleChoice = (await ask('')).trim() || '2';
3337
+ }
3338
+
3173
3339
  const chosenBias = styleMap[styleChoice] || 'balanced';
3174
3340
  const chosenName = styleNames[chosenBias];
3341
+ fx.nl();
3175
3342
 
3176
- // ── Non-blocking note if metered API detected ──────────────────────────────
3343
+ // Non-blocking note if metered API detected
3177
3344
  if (openaiReady && caps.openai.metered) {
3178
- console.log(` ${DIM}OpenAI API key detected usage is metered, guardrails enabled${RESET}`);
3179
- console.log('');
3345
+ const DIM = '\x1b[2m'; const RESET = '\x1b[0m';
3346
+ process.stdout.write(` ${DIM}OpenAI API key detected — usage is metered, guardrails enabled${RESET}\n\n`);
3180
3347
  }
3181
3348
 
3182
- // ── Done ───────────────────────────────────────────────────────────────────
3183
- console.log(wTop);
3184
- console.log(wRow(`${GREEN} Ready — ${chosenName} mode`));
3185
- console.log(wRow(` Type a task to start, or press Enter for dashboard`));
3186
- console.log(wBottom);
3187
- console.log('');
3349
+ // ─── Step 6: Ready ────────────────────────────────────────────────────────
3350
+ fx.step(5, 5, 'Ready!');
3351
+ fx.nl();
3352
+
3353
+ // Init living docs
3354
+ try {
3355
+ const ld = await getLivingDocs();
3356
+ if (ld.initLivingDocs) ld.initLivingDocs(cwd);
3357
+ } catch { /* non-fatal */ }
3358
+
3359
+ await fx.sleep(400);
3360
+ fx.celebrate(`dual-brain is ready! (${chosenName} mode)`);
3361
+ fx.nl();
3362
+ fx.info('Type anything to get started. Your AI partner is listening.');
3363
+ await fx.sleep(1200);
3188
3364
 
3189
- // ── Build and return the profile object ────────────────────────────────────
3365
+ // ─── Build and return the profile object ──────────────────────────────────
3190
3366
  const finalProfile = loadProfile(cwd);
3191
3367
 
3192
3368
  finalProfile.providers.claude = { enabled: claudeReady };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "7.1.27",
3
+ "version": "7.1.29",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -80,6 +80,7 @@
80
80
  "src/install-hooks.mjs",
81
81
  "src/update-check.mjs",
82
82
  "src/prompt-intel.mjs",
83
+ "src/fx.mjs",
83
84
  "bin/*.mjs",
84
85
  "hooks/enforce-tier.mjs",
85
86
  "hooks/cost-logger.mjs",