termify-agent 1.0.39 → 1.0.41

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.
Files changed (37) hide show
  1. package/dist/agent.d.ts.map +1 -1
  2. package/dist/agent.js +2 -0
  3. package/dist/agent.js.map +1 -1
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +55 -0
  6. package/dist/auth.js.map +1 -1
  7. package/dist/dashboard.d.ts.map +1 -1
  8. package/dist/dashboard.js +26 -2
  9. package/dist/dashboard.js.map +1 -1
  10. package/dist/index.js +5 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/pty-manager.d.ts +19 -0
  13. package/dist/pty-manager.d.ts.map +1 -1
  14. package/dist/pty-manager.js +111 -0
  15. package/dist/pty-manager.js.map +1 -1
  16. package/dist/setup.d.ts +15 -0
  17. package/dist/setup.d.ts.map +1 -0
  18. package/dist/setup.js +603 -0
  19. package/dist/setup.js.map +1 -0
  20. package/hooks/termify-response.js +151 -124
  21. package/hooks/termify-sync.js +165 -116
  22. package/mcp/memsearch-mcp-server.mjs +149 -0
  23. package/package.json +3 -2
  24. package/plugins/context7/.claude-plugin/plugin.json +7 -0
  25. package/plugins/context7/.mcp.json +6 -0
  26. package/plugins/memsearch/.claude-plugin/plugin.json +5 -0
  27. package/plugins/memsearch/README.md +762 -0
  28. package/plugins/memsearch/hooks/common.sh +151 -0
  29. package/plugins/memsearch/hooks/hooks.json +50 -0
  30. package/plugins/memsearch/hooks/parse-transcript.sh +117 -0
  31. package/plugins/memsearch/hooks/session-end.sh +9 -0
  32. package/plugins/memsearch/hooks/session-start.sh +119 -0
  33. package/plugins/memsearch/hooks/stop.sh +117 -0
  34. package/plugins/memsearch/hooks/user-prompt-submit.sh +21 -0
  35. package/plugins/memsearch/scripts/derive-collection.sh +50 -0
  36. package/plugins/memsearch/skills/memory-recall/SKILL.md +42 -0
  37. package/scripts/postinstall.js +21 -483
@@ -7,7 +7,11 @@
7
7
  * 1. Rebuild node-pty native module for the current platform
8
8
  * 2. Install termify-daemon binary (bundled first, download as fallback)
9
9
  * 3. Install stats-agent binary (bundled first, download as fallback)
10
- * 4. Install termify-mcp and configure Claude Code / Codex MCP settings
10
+ * 4. Install wireguard-go binary (bundled first, download as fallback)
11
+ * 5. Create global symlink for sudo access
12
+ *
13
+ * CLI tool installation and MCP/hooks configuration is deferred to
14
+ * `termify-agent start` for faster npm install times. See src/setup.ts.
11
15
  */
12
16
 
13
17
  import { execSync, execFileSync } from 'child_process';
@@ -29,8 +33,6 @@ const TERMIFY_DIR = join(homedir(), '.termify');
29
33
  const DAEMON_PATH = join(TERMIFY_DIR, `termify-daemon${EXE}`);
30
34
  const STATS_AGENT_PATH = join(TERMIFY_DIR, `stats-agent${EXE}`);
31
35
  const WG_GO_PATH = join(TERMIFY_DIR, 'wireguard-go');
32
- const MCP_DIR = join(TERMIFY_DIR, 'termify-mcp');
33
- const MCP_BUNDLE_PATH = join(MCP_DIR, 'index.mjs');
34
36
 
35
37
  // Marker: skip node-pty test on repeated installs if it already works for this ABI
36
38
  const NODE_PTY_OK_MARKER = join(TERMIFY_DIR, `.node-pty-ok-${process.versions.modules}`);
@@ -44,6 +46,7 @@ function testNodePty() {
44
46
  if (existsSync(NODE_PTY_OK_MARKER)) return true;
45
47
 
46
48
  try {
49
+ // Safe: hardcoded command, no user input — needs shell for quoting
47
50
  execSync('node -e "require(\'node-pty\').spawn(\'/bin/sh\',[],{cols:80,rows:24}).kill()"', {
48
51
  stdio: 'pipe',
49
52
  timeout: 5000,
@@ -95,7 +98,7 @@ function rebuildNodePty() {
95
98
  console.log(`[termify-agent] node-pty resolved at: ${nodePtyDir}`);
96
99
  console.log('[termify-agent] node-pty prebuilt not working, attempting source rebuild...');
97
100
 
98
- // Try rebuild from package root first
101
+ // Safe: hardcoded npm commands, no user input — needs shell for npm
99
102
  try {
100
103
  execSync('npm rebuild node-pty', {
101
104
  stdio: 'pipe',
@@ -205,7 +208,6 @@ async function installDaemon() {
205
208
  const plat = platform();
206
209
  const ar = arch();
207
210
 
208
- // Map Node.js platform/arch to our binary names
209
211
  const platformMap = {
210
212
  'darwin-arm64': 'darwin-arm64',
211
213
  'darwin-x64': 'darwin-x64',
@@ -223,16 +225,13 @@ async function installDaemon() {
223
225
  return false;
224
226
  }
225
227
 
226
- // Skip if already exists (user can delete to force re-download)
227
228
  if (existsSync(DAEMON_PATH)) {
228
229
  console.log('[termify-agent] termify-daemon binary already exists, skipping.');
229
230
  return true;
230
231
  }
231
232
 
232
- // Ensure ~/.termify directory exists
233
233
  mkdirSync(TERMIFY_DIR, { recursive: true });
234
234
 
235
- // Try bundled binary first
236
235
  const bundledPath = join(__dirname, '..', 'binaries', `termify-daemon-${binaryName}${EXE}`);
237
236
  if (existsSync(bundledPath)) {
238
237
  try {
@@ -245,7 +244,6 @@ async function installDaemon() {
245
244
  }
246
245
  }
247
246
 
248
- // Fallback: download from server
249
247
  console.log(`[termify-agent] Downloading termify-daemon for ${binaryName}...`);
250
248
  const url = `${DOWNLOAD_BASE_URL}/termify-daemon/${binaryName}`;
251
249
 
@@ -268,7 +266,6 @@ async function installStatsAgent() {
268
266
  const plat = platform();
269
267
  const ar = arch();
270
268
 
271
- // Map Node.js platform/arch to our binary names
272
269
  const platformMap = {
273
270
  'darwin-arm64': 'darwin-arm64',
274
271
  'darwin-x64': 'darwin-x64',
@@ -286,16 +283,13 @@ async function installStatsAgent() {
286
283
  return;
287
284
  }
288
285
 
289
- // Skip if already exists
290
286
  if (existsSync(STATS_AGENT_PATH)) {
291
287
  console.log('[termify-agent] stats-agent binary already exists, skipping.');
292
288
  return;
293
289
  }
294
290
 
295
- // Ensure ~/.termify directory exists
296
291
  mkdirSync(TERMIFY_DIR, { recursive: true });
297
292
 
298
- // Try bundled binary first
299
293
  const bundledPath = join(__dirname, '..', 'binaries', `stats-agent-${binaryName}${EXE}`);
300
294
  if (existsSync(bundledPath)) {
301
295
  try {
@@ -308,7 +302,6 @@ async function installStatsAgent() {
308
302
  }
309
303
  }
310
304
 
311
- // Fallback: download from server
312
305
  console.log(`[termify-agent] Downloading stats-agent for ${binaryName}...`);
313
306
  const url = `${DOWNLOAD_BASE_URL}/stats-agent/${binaryName}`;
314
307
 
@@ -325,18 +318,10 @@ async function installStatsAgent() {
325
318
  /**
326
319
  * Step 4: Install wireguard-go binary (bundled first, download as fallback)
327
320
  *
328
- * Bundling wireguard-go eliminates the need for `brew install wireguard-tools`
329
- * on macOS and provides a userspace WireGuard implementation for all platforms.
330
- * Combined with the UAPI socket client, this means the agent has zero external
331
- * WireGuard dependencies (same model as Tailscale).
332
- *
333
321
  * Not installed on Windows (uses native WireGuard service).
334
322
  */
335
323
  async function installWireguardGo() {
336
- if (IS_WIN) {
337
- // Windows uses native WireGuard service, not wireguard-go
338
- return;
339
- }
324
+ if (IS_WIN) return;
340
325
 
341
326
  const plat = platform();
342
327
  const ar = arch();
@@ -357,16 +342,13 @@ async function installWireguardGo() {
357
342
  return;
358
343
  }
359
344
 
360
- // Skip if already exists
361
345
  if (existsSync(WG_GO_PATH)) {
362
346
  console.log('[termify-agent] wireguard-go binary already exists, skipping.');
363
347
  return;
364
348
  }
365
349
 
366
- // Ensure ~/.termify directory exists
367
350
  mkdirSync(TERMIFY_DIR, { recursive: true });
368
351
 
369
- // Try bundled binary first
370
352
  const bundledPath = join(__dirname, '..', 'binaries', `wireguard-go-${binaryName}`);
371
353
  if (existsSync(bundledPath)) {
372
354
  try {
@@ -379,7 +361,6 @@ async function installWireguardGo() {
379
361
  }
380
362
  }
381
363
 
382
- // Fallback: download from server
383
364
  console.log(`[termify-agent] Downloading wireguard-go for ${binaryName}...`);
384
365
  const url = `${DOWNLOAD_BASE_URL}/wireguard-go/${binaryName}`;
385
366
 
@@ -403,7 +384,6 @@ function downloadFile(url, dest, redirects = 0) {
403
384
  }
404
385
 
405
386
  const request = get(url, (response) => {
406
- // Handle redirects
407
387
  if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
408
388
  return downloadFile(response.headers.location, dest, redirects + 1)
409
389
  .then(resolve)
@@ -422,7 +402,6 @@ function downloadFile(url, dest, redirects = 0) {
422
402
  });
423
403
  file.on('error', (err) => {
424
404
  file.close();
425
- // Clean up partial download
426
405
  try { unlinkSync(dest); } catch {}
427
406
  reject(err);
428
407
  });
@@ -436,436 +415,18 @@ function downloadFile(url, dest, redirects = 0) {
436
415
  });
437
416
  }
438
417
 
439
- /**
440
- * Step 4: Install termify-mcp bundle and configure Claude Code / Codex / Gemini
441
- *
442
- * The MCP server allows AI tools to report their working state to Termify.
443
- * Hooks sync conversations (prompts, responses, tool use, thinking) to Termify chat.
444
- */
445
- function installTermifyMcp() {
446
- // Find the bundled MCP file
447
- const bundledMcp = join(__dirname, '..', 'mcp', 'termify-mcp-bundle.mjs');
448
- if (!existsSync(bundledMcp)) {
449
- console.warn('[termify-agent] Warning: termify-mcp bundle not found, skipping MCP setup.');
450
- return;
451
- }
452
-
453
- // Copy bundle to ~/.termify/termify-mcp/index.mjs
454
- mkdirSync(MCP_DIR, { recursive: true });
455
- try {
456
- copyFileSync(bundledMcp, MCP_BUNDLE_PATH);
457
- console.log(`[termify-agent] termify-mcp installed to ${MCP_BUNDLE_PATH}`);
458
- } catch (err) {
459
- console.warn(`[termify-agent] Warning: Failed to copy termify-mcp bundle: ${err.message}`);
460
- return;
461
- }
462
-
463
- // Configure Claude Code
464
- configureClaudeCode(MCP_BUNDLE_PATH);
465
-
466
- // Configure Codex
467
- configureCodex(MCP_BUNDLE_PATH);
468
-
469
- // Configure Gemini
470
- configureGemini();
471
- }
472
-
473
- // ---------------------------------------------------------------------------
474
- // Hook definitions: which hooks go to which CLI event
475
- // ---------------------------------------------------------------------------
476
-
477
- const HOOKS_DIR_NAME = 'hooks';
478
-
479
- /**
480
- * All Termify hooks and their CLI event mappings.
481
- * Each hook file is copied to ~/.termify/hooks/ and registered in the CLI config.
482
- */
483
- const HOOK_DEFINITIONS = {
484
- // Claude Code hooks
485
- claude: {
486
- UserPromptSubmit: ['termify-sync.js'],
487
- Stop: ['termify-response.js'],
488
- PreToolUse: [
489
- { file: 'termify-tool-hook.js' },
490
- { file: 'termify-question-hook.js', matcher: 'AskUserQuestion' },
491
- { file: 'termify-needs-input-hook.js', matcher: 'ExitPlanMode' },
492
- { file: 'termify-needs-input-hook.js', matcher: 'EnterPlanMode' },
493
- ],
494
- PostToolUse: ['termify-tool-hook.js'],
495
- },
496
- // Gemini CLI hooks
497
- gemini: {
498
- BeforeAgent: ['termify-sync.js'],
499
- AfterAgent: ['termify-response.js'],
500
- },
501
- // Codex CLI hooks (notify, not event-based)
502
- codex: {
503
- notify: ['termify-codex-notify.js'],
504
- },
505
- };
506
-
507
- /**
508
- * Copy all hook files to ~/.termify/hooks/
509
- */
510
- function installHookFiles() {
511
- const srcDir = join(__dirname, '..', HOOKS_DIR_NAME);
512
- const destDir = join(TERMIFY_DIR, 'hooks');
513
- mkdirSync(destDir, { recursive: true });
514
-
515
- const hookFiles = [
516
- 'termify-sync.js',
517
- 'termify-response.js',
518
- 'termify-tool-hook.js',
519
- 'termify-question-hook.js',
520
- 'termify-needs-input-hook.js',
521
- 'termify-codex-notify.js',
522
- ];
523
-
524
- for (const file of hookFiles) {
525
- const src = join(srcDir, file);
526
- const dest = join(destDir, file);
527
- if (existsSync(src)) {
528
- copyFileSync(src, dest);
529
- } else {
530
- console.warn(`[termify-agent] Warning: Hook file ${file} not found in package.`);
531
- }
532
- }
533
-
534
- // Clean up old hook from previous versions
535
- const oldHook = join(destDir, 'termify-auto-working.js');
536
- if (existsSync(oldHook)) {
537
- try { unlinkSync(oldHook); } catch {}
538
- }
539
-
540
- console.log(`[termify-agent] Hook files installed to ${destDir}`);
541
- return destDir;
542
- }
543
-
544
- /**
545
- * Register a hook command in a Claude Code settings.json hooks section.
546
- * Avoids duplicates by checking if the hook filename (basename) is already
547
- * registered under ANY path — prevents the same hook from being registered
548
- * from both ~/.claude/hooks/ and ~/.termify/hooks/.
549
- */
550
- function registerClaudeHook(settings, eventName, hookCommand, identifierStr) {
551
- if (!settings.hooks) settings.hooks = {};
552
-
553
- // Extract the hook filename from the command (e.g., "termify-sync.js" from "node /path/to/termify-sync.js")
554
- const hookFilename = hookCommand.split('/').pop()?.split('\\').pop() || hookCommand;
555
-
556
- const hookEntry = {
557
- hooks: [{ type: 'command', command: hookCommand }],
558
- };
559
-
560
- if (!settings.hooks[eventName]) {
561
- settings.hooks[eventName] = [hookEntry];
562
- } else {
563
- // Remove any existing entries with the same hook filename (from any path)
564
- settings.hooks[eventName] = settings.hooks[eventName].filter(
565
- (h) => !h.hooks || !h.hooks.some((hh) => hh.command && hh.command.includes(hookFilename))
566
- );
567
- settings.hooks[eventName].push(hookEntry);
568
- }
569
- }
570
-
571
- /**
572
- * Configure Claude Code: MCP + all hooks in ~/.claude/settings.json
573
- */
574
- function configureClaudeCode(mcpPath) {
575
- try {
576
- const claudeDir = join(homedir(), '.claude');
577
- const settingsPath = join(claudeDir, 'settings.json');
578
- mkdirSync(claudeDir, { recursive: true });
579
-
580
- let settings = {};
581
- if (existsSync(settingsPath)) {
582
- settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
583
- }
584
-
585
- // Configure MCP server (clean up old 'termify' key if it exists)
586
- if (!settings.mcpServers) settings.mcpServers = {};
587
- delete settings.mcpServers['termify'];
588
- settings.mcpServers['termify-status'] = {
589
- command: 'node',
590
- args: [mcpPath],
591
- };
592
-
593
- // Install hook files
594
- const hooksDestDir = installHookFiles();
595
-
596
- // Clean up ALL existing termify hooks from any path (prevents duplicates
597
- // when hooks were previously installed to ~/.claude/hooks/ or ~/.termify/hooks/)
598
- if (settings.hooks) {
599
- for (const eventName of Object.keys(settings.hooks)) {
600
- if (Array.isArray(settings.hooks[eventName])) {
601
- settings.hooks[eventName] = settings.hooks[eventName].filter(
602
- (h) => !h.hooks || !h.hooks.some((hh) => hh.command && /termify-/.test(hh.command))
603
- );
604
- if (settings.hooks[eventName].length === 0) {
605
- delete settings.hooks[eventName];
606
- }
607
- }
608
- }
609
- }
610
-
611
- // Register Claude Code hooks (fresh, no duplicates possible after cleanup above)
612
- if (!settings.hooks) settings.hooks = {};
613
- const claudeHooks = HOOK_DEFINITIONS.claude;
614
- for (const [eventName, hooks] of Object.entries(claudeHooks)) {
615
- if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
616
-
617
- for (const hook of hooks) {
618
- const file = typeof hook === 'string' ? hook : hook.file;
619
- const matcher = typeof hook === 'object' ? hook.matcher : null;
620
- const hookPath = join(hooksDestDir, file);
621
- const hookCommand = `node ${hookPath}`;
622
-
623
- const entry = {
624
- hooks: [{ type: 'command', command: hookCommand }],
625
- };
626
- if (matcher) {
627
- entry.matcher = matcher;
628
- }
629
-
630
- settings.hooks[eventName].push(entry);
631
- }
632
- }
633
-
634
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
635
- console.log('[termify-agent] Claude Code MCP + hooks configured in ~/.claude/settings.json');
636
- } catch (err) {
637
- console.warn(`[termify-agent] Warning: Failed to configure Claude Code: ${err.message}`);
638
- }
639
- }
640
-
641
- /**
642
- * Configure Gemini CLI: hooks in ~/.gemini/settings.json
643
- */
644
- function configureGemini() {
645
- try {
646
- const geminiDir = join(homedir(), '.gemini');
647
- if (!existsSync(geminiDir)) {
648
- console.log('[termify-agent] Gemini CLI not found, skipping Gemini hooks.');
649
- return;
650
- }
651
-
652
- const settingsPath = join(geminiDir, 'settings.json');
653
- let settings = {};
654
- if (existsSync(settingsPath)) {
655
- settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
656
- }
657
-
658
- const hooksDir = join(TERMIFY_DIR, 'hooks');
659
-
660
- // Register Gemini hooks
661
- if (!settings.hooks) settings.hooks = {};
662
-
663
- const geminiHooks = HOOK_DEFINITIONS.gemini;
664
- for (const [eventName, hooks] of Object.entries(geminiHooks)) {
665
- for (const file of hooks) {
666
- const hookPath = join(hooksDir, file);
667
- const hookCommand = `node ${hookPath}`;
668
-
669
- const hookEntry = {
670
- hooks: [{ type: 'command', command: hookCommand }],
671
- };
672
-
673
- if (!settings.hooks[eventName]) {
674
- settings.hooks[eventName] = [hookEntry];
675
- } else {
676
- const already = settings.hooks[eventName].some(
677
- (h) => h.hooks && h.hooks.some((hh) => hh.command && hh.command.includes(file))
678
- );
679
- if (!already) {
680
- settings.hooks[eventName].push(hookEntry);
681
- }
682
- }
683
- }
684
- }
685
-
686
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
687
- console.log('[termify-agent] Gemini CLI hooks configured in ~/.gemini/settings.json');
688
- } catch (err) {
689
- console.warn(`[termify-agent] Warning: Failed to configure Gemini CLI: ${err.message}`);
690
- }
691
- }
692
-
693
- /**
694
- * Configure Codex CLI: MCP + notify hook in ~/.codex/config.toml
695
- */
696
- function configureCodex(mcpPath) {
697
- try {
698
- const codexDir = join(homedir(), '.codex');
699
- const configPath = join(codexDir, 'config.toml');
700
-
701
- let content = '';
702
- if (existsSync(configPath)) {
703
- content = readFileSync(configPath, 'utf8');
704
- }
705
-
706
- const hooksDir = join(TERMIFY_DIR, 'hooks');
707
- const notifyHookPath = join(hooksDir, 'termify-codex-notify.js');
708
-
709
- // Escape backslashes in paths for TOML (Windows)
710
- const escapedMcpPath = mcpPath.replace(/\\/g, '\\\\');
711
- const escapedNotifyPath = notifyHookPath.replace(/\\/g, '\\\\');
712
-
713
- // Remove existing termify-status MCP section
714
- content = content.replace(/\[mcp_servers\.termify-status\]\n(?:(?!\[)[^\n]*\n?)*/g, '');
715
-
716
- // Update or add notify line
717
- const notifyValue = `["node", "${escapedNotifyPath}"]`;
718
- if (content.includes('notify =')) {
719
- // Replace existing notify line
720
- content = content.replace(/notify\s*=\s*\[.*\]/, `notify = ${notifyValue}`);
721
- } else {
722
- // Add notify at the top (after any existing first line)
723
- const lines = content.split('\n');
724
- // Insert after first non-empty line or at top
725
- let insertIdx = 0;
726
- for (let i = 0; i < lines.length; i++) {
727
- if (lines[i].trim() && !lines[i].startsWith('#') && !lines[i].startsWith('[')) {
728
- insertIdx = i + 1;
729
- break;
730
- }
731
- }
732
- lines.splice(insertIdx, 0, `notify = ${notifyValue}`);
733
- content = lines.join('\n');
734
- }
735
-
736
- content = content.trimEnd();
737
-
738
- // Append MCP section
739
- const section = `\n\n[mcp_servers.termify-status]\ncommand = "node"\nargs = ["${escapedMcpPath}"]\n`;
740
- content = content + section;
741
-
742
- mkdirSync(codexDir, { recursive: true });
743
- writeFileSync(configPath, content);
744
- console.log('[termify-agent] Codex MCP + notify hook configured in ~/.codex/config.toml');
745
- } catch (err) {
746
- console.warn(`[termify-agent] Warning: Failed to configure Codex: ${err.message}`);
747
- }
748
- }
749
-
750
- /**
751
- * Ensure ~/.claude/CLAUDE.md contains termify_status instructions.
752
- * Adds the section if missing, updates it if outdated.
753
- */
754
- function ensureClaudeInstructions() {
755
- try {
756
- const claudeMdPath = join(homedir(), '.claude', 'CLAUDE.md');
757
- const marker = '## Termify Status';
758
- const instructions = `## Termify Status (OBLIGATORIO)
759
-
760
- **DEBES** usar el MCP tool \`termify_status\` para reportar tu estado a Termify. Los hooks reportan \`working\` automáticamente al recibir un prompt, pero TU eres responsable de reportar los demás estados:
761
-
762
- - \`working\`: los hooks lo ponen automáticamente al inicio. Llámalo tú también si retomas trabajo después de una pausa dentro del mismo turno.
763
- - \`finished\`: **SIEMPRE** llamar cuando termines la tarea actual. Es lo último que haces antes de responder.
764
- - \`idle\`: cuando estés esperando input libre del usuario (prompt normal).
765
- - \`needs_input\`: cuando estés bloqueado esperando que el usuario elija una opción, confirme \`y/n\`, o responda una pregunta concreta. Incluye \`needsInputPrompt\` con la pregunta exacta.
766
- - \`error\`: si algo falla irrecuperablemente.
767
-
768
- **Reglas**:
769
- 1. **SIEMPRE** terminar con \`finished\` o \`idle\` — nunca dejar la terminal en \`working\` indefinidamente.
770
- 2. No spamees: llama al tool solo cuando cambie el estado.
771
- 3. Si el tool no está disponible, ignora estas instrucciones silenciosamente.`;
772
-
773
- let content = '';
774
- if (existsSync(claudeMdPath)) {
775
- content = readFileSync(claudeMdPath, 'utf8');
776
-
777
- // Check if section already exists
778
- if (content.includes(marker)) {
779
- // Replace existing section (from marker to next ## or end)
780
- const regex = new RegExp(
781
- `${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?(?=\\n## [^T]|\\n---\\n|$)`,
782
- ''
783
- );
784
- content = content.replace(regex, instructions);
785
- } else {
786
- // Find insertion point: after the first header/intro section
787
- const firstSectionIdx = content.indexOf('\n## ');
788
- if (firstSectionIdx >= 0) {
789
- content = content.slice(0, firstSectionIdx) + '\n\n' + instructions + '\n' + content.slice(firstSectionIdx);
790
- } else {
791
- content = content + '\n\n' + instructions + '\n';
792
- }
793
- }
794
- } else {
795
- content = '# Claude Code Configuration\n\n' + instructions + '\n';
796
- }
797
-
798
- const claudeDir = join(homedir(), '.claude');
799
- mkdirSync(claudeDir, { recursive: true });
800
- writeFileSync(claudeMdPath, content);
801
- console.log('[termify-agent] Termify status instructions added to ~/.claude/CLAUDE.md');
802
- } catch (err) {
803
- console.warn(`[termify-agent] Warning: Failed to update CLAUDE.md: ${err.message}`);
804
- }
805
- }
806
-
807
- /**
808
- * Ensure Codex has termify_status instructions.
809
- */
810
- function ensureCodexInstructions() {
811
- try {
812
- const codexDir = join(homedir(), '.codex');
813
- if (!existsSync(codexDir)) return;
814
-
815
- const instructionsPath = join(codexDir, 'instructions.md');
816
- const marker = '## Termify Status';
817
- const instructions = `## Termify Status (OBLIGATORIO)
818
-
819
- **DEBES** usar el MCP tool \`termify_status\` para reportar tu estado a Termify en CADA interacción:
820
-
821
- 1. \`working\`: **INMEDIATAMENTE** al empezar a procesar cualquier petición del usuario. Es lo PRIMERO que haces.
822
- 2. \`finished\`: **SIEMPRE** al terminar la tarea. Es lo ÚLTIMO que haces antes de responder.
823
- 3. \`idle\`: cuando estés esperando input libre del usuario.
824
- 4. \`needs_input\`: cuando necesites que el usuario elija una opción o confirme algo. Incluye \`needsInputPrompt\`.
825
- 5. \`error\`: si algo falla irrecuperablemente.
826
-
827
- **Reglas**:
828
- - **NUNCA** omitir el \`working\` inicial — sin esto, la terminal no muestra indicador de actividad.
829
- - **SIEMPRE** terminar con \`finished\` o \`idle\` — nunca dejar la terminal en \`working\`.
830
- - Solo llama al tool cuando cambie el estado (no repetir el mismo estado).`;
831
-
832
- let content = '';
833
- if (existsSync(instructionsPath)) {
834
- content = readFileSync(instructionsPath, 'utf8');
835
- if (content.includes(marker)) {
836
- console.log('[termify-agent] Codex instructions already contain Termify status section.');
837
- return;
838
- }
839
- content = content + '\n\n' + instructions + '\n';
840
- } else {
841
- content = '# Codex Instructions\n\n' + instructions + '\n';
842
- }
843
-
844
- writeFileSync(instructionsPath, content);
845
- console.log('[termify-agent] Termify status instructions added to ~/.codex/instructions.md');
846
- } catch (err) {
847
- console.warn(`[termify-agent] Warning: Failed to update Codex instructions: ${err.message}`);
848
- }
849
- }
850
-
851
418
  // ---------------------------------------------------------------------------
852
419
  // Symlink in /usr/local/bin for sudo access
853
420
  // ---------------------------------------------------------------------------
854
421
 
855
422
  /**
856
423
  * Create /usr/local/bin/termify-agent so `sudo termify-agent` works.
857
- *
858
- * npm puts the binary in a user-local path (e.g. ~/.nvm/.../bin/) that
859
- * sudo's sanitized PATH does not include. This symlink fixes that.
860
- *
861
- * Best-effort: if we can't create it (permissions), just log a hint.
862
424
  */
863
425
  function ensureGlobalSymlink() {
864
426
  if (IS_WIN) return;
865
427
 
866
428
  const target = '/usr/local/bin/termify-agent';
867
429
 
868
- // Don't touch if already exists
869
430
  try {
870
431
  lstatSync(target);
871
432
  console.log('[termify-agent] /usr/local/bin/termify-agent already exists, skipping.');
@@ -874,12 +435,10 @@ function ensureGlobalSymlink() {
874
435
  // Doesn't exist — good, we'll create it
875
436
  }
876
437
 
877
- // Find the npm-installed binary (execFileSync to avoid shell injection)
878
438
  let npmBinPath = null;
879
439
  try {
880
440
  npmBinPath = execFileSync('which', ['termify-agent'], { stdio: 'pipe', encoding: 'utf8' }).trim();
881
441
  } catch {
882
- // Not in PATH (local install) — compute from package
883
442
  npmBinPath = join(__dirname, '..', 'bin', 'termify-agent.js');
884
443
  if (!existsSync(npmBinPath)) {
885
444
  console.log('[termify-agent] Could not locate termify-agent binary for symlink.');
@@ -891,9 +450,8 @@ function ensureGlobalSymlink() {
891
450
 
892
451
  try {
893
452
  symlinkSync(npmBinPath, target);
894
- console.log(`[termify-agent] Symlink created: ${target} ${npmBinPath}`);
453
+ console.log(`[termify-agent] Symlink created: ${target} \u2192 ${npmBinPath}`);
895
454
  } catch {
896
- // Permission denied — not critical, log a hint
897
455
  console.log(`[termify-agent] Tip: run 'sudo ln -sf "${npmBinPath}" ${target}' to enable sudo access`);
898
456
  }
899
457
  }
@@ -908,9 +466,9 @@ const YELLOW = '\x1b[33m';
908
466
  const DIM = '\x1b[2m';
909
467
  const BOLD = '\x1b[1m';
910
468
  const RESET = '\x1b[0m';
911
- const CHECK = `${GREEN}✔${RESET}`;
912
- const WARN = `${YELLOW}⚠${RESET}`;
913
- const SPINNER_FRAMES = ['', '', '', '', '', '', '', '', '', ''];
469
+ const CHECK = `${GREEN}\u2714${RESET}`;
470
+ const WARN = `${YELLOW}\u26A0${RESET}`;
471
+ const SPINNER_FRAMES = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
914
472
 
915
473
  function createSpinner(text) {
916
474
  let i = 0;
@@ -924,7 +482,6 @@ function createSpinner(text) {
924
482
  };
925
483
  }
926
484
 
927
- // Suppress verbose internal logs — only spinner UI shows
928
485
  const _log = console.log;
929
486
  const _warn = console.warn;
930
487
  const _err = console.error;
@@ -932,12 +489,12 @@ function muteConsole() { console.log = () => {}; console.warn = () => {}; consol
932
489
  function unmuteConsole() { console.log = _log; console.warn = _warn; console.error = _err; }
933
490
 
934
491
  /**
935
- * Main
492
+ * Main — only binaries and native modules. CLI config is deferred to `start`.
936
493
  */
937
494
  async function main() {
938
495
  console.log('');
939
496
  console.log(` ${BOLD}${CYAN}Termify Agent${RESET} ${DIM}postinstall${RESET}`);
940
- console.log(` ${DIM}${platform()}-${arch()} · Node ${process.version}${RESET}`);
497
+ console.log(` ${DIM}${platform()}-${arch()} \u00B7 Node ${process.version}${RESET}`);
941
498
  console.log('');
942
499
 
943
500
  muteConsole();
@@ -946,7 +503,7 @@ async function main() {
946
503
  const s1 = createSpinner('Checking node-pty...');
947
504
  const ptyOk = rebuildNodePty();
948
505
  if (ptyOk) s1.succeed('node-pty ready');
949
- else s1.warn('node-pty skipped ${DIM}(optional)${RESET}');
506
+ else s1.warn(`node-pty skipped ${DIM}(optional)${RESET}`);
950
507
 
951
508
  // Step 2+3+4: Binaries in parallel
952
509
  const s2 = createSpinner('Installing binaries...');
@@ -962,42 +519,23 @@ async function main() {
962
519
  if (parts.length) s2.succeed(`Binaries ready ${DIM}(${parts.join(', ')})${RESET}`);
963
520
  else s2.warn('Binaries skipped');
964
521
 
965
- // Step: Global symlink for sudo access
966
- const s2b = createSpinner('Creating global symlink...');
522
+ // Step 5: Global symlink
523
+ const s3 = createSpinner('Creating global symlink...');
967
524
  try {
968
525
  ensureGlobalSymlink();
969
526
  if (existsSync('/usr/local/bin/termify-agent')) {
970
- s2b.succeed('Global symlink ready');
527
+ s3.succeed('Global symlink ready');
971
528
  } else {
972
- s2b.warn(`Symlink skipped ${DIM}(run: sudo ln -sf $(which termify-agent) /usr/local/bin/)${RESET}`);
529
+ s3.warn(`Symlink skipped ${DIM}(run: sudo ln -sf $(which termify-agent) /usr/local/bin/)${RESET}`);
973
530
  }
974
531
  } catch {
975
- s2b.warn('Symlink skipped');
976
- }
977
-
978
- // Step 3: MCP + hooks
979
- const s3 = createSpinner('Configuring hooks & MCP...');
980
- try {
981
- installTermifyMcp();
982
- s3.succeed(`Hooks & MCP configured ${DIM}(Claude · Gemini · Codex)${RESET}`);
983
- } catch (err) {
984
- s3.warn(`Hooks & MCP: ${err.message}`);
985
- }
986
-
987
- // Step 4: Instructions
988
- const s4 = createSpinner('Updating AI instructions...');
989
- try {
990
- ensureClaudeInstructions();
991
- ensureCodexInstructions();
992
- s4.succeed('AI instructions updated');
993
- } catch (err) {
994
- s4.warn(`Instructions: ${err.message}`);
532
+ s3.warn('Symlink skipped');
995
533
  }
996
534
 
997
535
  unmuteConsole();
998
536
 
999
537
  console.log('');
1000
- console.log(` ${GREEN}${BOLD}Done!${RESET} ${DIM}Termify Agent is ready.${RESET}`);
538
+ console.log(` ${GREEN}${BOLD}Done!${RESET} ${DIM}Run \`termify-agent start\` to finish setup.${RESET}`);
1001
539
  console.log('');
1002
540
  }
1003
541