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.
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -0
- package/dist/agent.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +55 -0
- package/dist/auth.js.map +1 -1
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +26 -2
- package/dist/dashboard.js.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/pty-manager.d.ts +19 -0
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +111 -0
- package/dist/pty-manager.js.map +1 -1
- package/dist/setup.d.ts +15 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +603 -0
- package/dist/setup.js.map +1 -0
- package/hooks/termify-response.js +151 -124
- package/hooks/termify-sync.js +165 -116
- package/mcp/memsearch-mcp-server.mjs +149 -0
- package/package.json +3 -2
- package/plugins/context7/.claude-plugin/plugin.json +7 -0
- package/plugins/context7/.mcp.json +6 -0
- package/plugins/memsearch/.claude-plugin/plugin.json +5 -0
- package/plugins/memsearch/README.md +762 -0
- package/plugins/memsearch/hooks/common.sh +151 -0
- package/plugins/memsearch/hooks/hooks.json +50 -0
- package/plugins/memsearch/hooks/parse-transcript.sh +117 -0
- package/plugins/memsearch/hooks/session-end.sh +9 -0
- package/plugins/memsearch/hooks/session-start.sh +119 -0
- package/plugins/memsearch/hooks/stop.sh +117 -0
- package/plugins/memsearch/hooks/user-prompt-submit.sh +21 -0
- package/plugins/memsearch/scripts/derive-collection.sh +50 -0
- package/plugins/memsearch/skills/memory-recall/SKILL.md +42 -0
- package/scripts/postinstall.js +21 -483
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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}
|
|
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}
|
|
912
|
-
const WARN = `${YELLOW}
|
|
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()}
|
|
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(
|
|
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
|
|
966
|
-
const
|
|
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
|
-
|
|
527
|
+
s3.succeed('Global symlink ready');
|
|
971
528
|
} else {
|
|
972
|
-
|
|
529
|
+
s3.warn(`Symlink skipped ${DIM}(run: sudo ln -sf $(which termify-agent) /usr/local/bin/)${RESET}`);
|
|
973
530
|
}
|
|
974
531
|
} catch {
|
|
975
|
-
|
|
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}
|
|
538
|
+
console.log(` ${GREEN}${BOLD}Done!${RESET} ${DIM}Run \`termify-agent start\` to finish setup.${RESET}`);
|
|
1001
539
|
console.log('');
|
|
1002
540
|
}
|
|
1003
541
|
|