@zeph-to/cli 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +499 -0
  3. package/dist/agents.d.ts +8 -0
  4. package/dist/agents.d.ts.map +1 -0
  5. package/dist/agents.js +29 -0
  6. package/dist/check-update.d.ts +4 -0
  7. package/dist/check-update.d.ts.map +1 -0
  8. package/dist/check-update.js +80 -0
  9. package/dist/cli.d.ts +3 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +374 -0
  12. package/dist/config.d.ts +14 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +36 -0
  15. package/dist/crypto.d.ts +82 -0
  16. package/dist/crypto.d.ts.map +1 -0
  17. package/dist/crypto.js +291 -0
  18. package/dist/errors.d.ts +12 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +28 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +9 -0
  24. package/dist/installer.d.ts +14 -0
  25. package/dist/installer.d.ts.map +1 -0
  26. package/dist/installer.js +464 -0
  27. package/dist/listener.d.ts +126 -0
  28. package/dist/listener.d.ts.map +1 -0
  29. package/dist/listener.js +1008 -0
  30. package/dist/login.d.ts +38 -0
  31. package/dist/login.d.ts.map +1 -0
  32. package/dist/login.js +182 -0
  33. package/dist/templates.d.ts +44 -0
  34. package/dist/templates.d.ts.map +1 -0
  35. package/dist/templates.js +257 -0
  36. package/dist/types.d.ts +54 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +2 -0
  39. package/dist/uninstall.d.ts +2 -0
  40. package/dist/uninstall.d.ts.map +1 -0
  41. package/dist/uninstall.js +217 -0
  42. package/dist/verify.d.ts +2 -0
  43. package/dist/verify.d.ts.map +1 -0
  44. package/dist/verify.js +109 -0
  45. package/dist/wrapper.d.ts +26 -0
  46. package/dist/wrapper.d.ts.map +1 -0
  47. package/dist/wrapper.js +238 -0
  48. package/dist/zeph-hook.d.ts +23 -0
  49. package/dist/zeph-hook.d.ts.map +1 -0
  50. package/dist/zeph-hook.js +196 -0
  51. package/package.json +75 -0
@@ -0,0 +1,464 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleInstall = exports.filterAgentsByIds = exports.shouldTriggerLogin = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const fs_1 = require("fs");
6
+ const os_1 = require("os");
7
+ const path_1 = require("path");
8
+ const readline_1 = require("readline");
9
+ const zeph_hook_js_1 = require("./zeph-hook.js");
10
+ const config_js_1 = require("./config.js");
11
+ const login_js_1 = require("./login.js");
12
+ const agents_js_1 = require("./agents.js");
13
+ const templates_js_1 = require("./templates.js");
14
+ const HOME = (0, os_1.homedir)();
15
+ // ── Helpers ──────────────────────────────────────────────────────
16
+ const ok = (msg) => console.log(` + ${msg}`);
17
+ const fail = (msg) => console.log(` - ${msg}`);
18
+ /**
19
+ * True when install should auto-open browser login (ADR 0002): interactive
20
+ * context with no existing credential (--key/env/config all absent).
21
+ */
22
+ const shouldTriggerLogin = (nonInteractive, currentKey) => !nonInteractive && !currentKey;
23
+ exports.shouldTriggerLogin = shouldTriggerLogin;
24
+ const promptInput = (question) => {
25
+ const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
26
+ return new Promise((resolve) => {
27
+ rl.question(question, (answer) => {
28
+ rl.close();
29
+ resolve(answer.trim());
30
+ });
31
+ });
32
+ };
33
+ const writeFile = (filePath, content) => {
34
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(filePath), { recursive: true });
35
+ (0, fs_1.writeFileSync)(filePath, content + '\n');
36
+ };
37
+ const mergeJsonFile = (filePath, patch) => {
38
+ let data = {};
39
+ try {
40
+ data = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
41
+ }
42
+ catch { /* new file */ }
43
+ const merged = { ...data, ...patch };
44
+ writeFile(filePath, JSON.stringify(merged, null, 2));
45
+ };
46
+ /**
47
+ * Write a Zeph rule into a SHARED agent rule file (Windsurf global_rules.md,
48
+ * Gemini GEMINI.md, Codex AGENTS.md) without clobbering the user's own
49
+ * content. The rule lands inside <!-- ZEPH:START/END --> markers; a re-run
50
+ * replaces just that block.
51
+ */
52
+ const writeManagedRule = (filePath, rule) => {
53
+ let existing = '';
54
+ try {
55
+ existing = (0, fs_1.readFileSync)(filePath, 'utf-8');
56
+ }
57
+ catch { /* new file */ }
58
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(filePath), { recursive: true });
59
+ (0, fs_1.writeFileSync)(filePath, (0, templates_js_1.upsertManagedBlock)(existing, rule));
60
+ };
61
+ /**
62
+ * Add a `read:` entry to ~/.aider.conf.yml so Aider always loads the Zeph
63
+ * conventions file. Idempotent — skips if the path is already referenced.
64
+ * Aider's config is YAML; we do a minimal text-level append to avoid
65
+ * pulling in a YAML dependency (the SDK is zero-dep by design).
66
+ */
67
+ const addAiderReadDirective = (confPath, conventionsPath) => {
68
+ let conf = '';
69
+ try {
70
+ conf = (0, fs_1.readFileSync)(confPath, 'utf-8');
71
+ }
72
+ catch { /* new file */ }
73
+ if (conf.includes(conventionsPath))
74
+ return; // already wired up
75
+ const marker = '# Added by Zeph';
76
+ const line = `${marker}\nread: ${conventionsPath}\n`;
77
+ const base = conf.replace(/\n*$/, '');
78
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(confPath), { recursive: true });
79
+ (0, fs_1.writeFileSync)(confPath, (base ? `${base}\n\n` : '') + line);
80
+ };
81
+ // ── Per-Agent Installers ─────────────────────────────────────────
82
+ const injectMcpJson = (filePath) => {
83
+ let data = {};
84
+ try {
85
+ data = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
86
+ }
87
+ catch { /* new file */ }
88
+ if (!data.mcpServers)
89
+ data.mcpServers = {};
90
+ // Pass through env explicitly so the MCP server doesn't have to rely on
91
+ // process-env inheritance (which behaves differently per IDE — Cursor and
92
+ // Windsurf spawn the MCP from a graphical context that may not inherit
93
+ // shell env). Mirrors plugin/.mcp.json.
94
+ data.mcpServers.zeph = {
95
+ command: 'npx',
96
+ args: ['-y', '@zeph-to/mcp-server'],
97
+ env: { ZEPH_API_KEY: '${ZEPH_API_KEY}' },
98
+ };
99
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(filePath), { recursive: true });
100
+ (0, fs_1.writeFileSync)(filePath, JSON.stringify(data, null, 2) + '\n');
101
+ };
102
+ const installClaude = () => {
103
+ try {
104
+ (0, child_process_1.execSync)('claude plugin marketplace add zeph-to/plugin', { stdio: 'pipe' });
105
+ (0, child_process_1.execSync)('claude plugin install zeph@zeph', { stdio: 'pipe' });
106
+ ok('Plugin installed');
107
+ }
108
+ catch {
109
+ fail('Plugin install failed. Manual:');
110
+ console.log(' claude plugin marketplace add zeph-to/plugin');
111
+ console.log(' claude plugin install zeph@zeph');
112
+ }
113
+ };
114
+ const installCursor = () => {
115
+ try {
116
+ injectMcpJson((0, path_1.join)(HOME, '.cursor', 'mcp.json'));
117
+ ok('MCP server added');
118
+ }
119
+ catch {
120
+ fail('MCP injection failed. Manual: add zeph to ~/.cursor/mcp.json');
121
+ }
122
+ try {
123
+ writeFile((0, path_1.join)(HOME, '.cursor', 'hooks.json'), templates_js_1.CURSOR_HOOKS);
124
+ ok('Stop hook added');
125
+ }
126
+ catch {
127
+ fail('Hook install failed');
128
+ }
129
+ try {
130
+ writeFile((0, path_1.join)(HOME, '.cursor', 'rules', 'zeph.mdc'), templates_js_1.CURSOR_RULE);
131
+ ok('Rule file added');
132
+ }
133
+ catch {
134
+ fail('Rule install failed');
135
+ }
136
+ };
137
+ const installWindsurf = () => {
138
+ try {
139
+ injectMcpJson((0, path_1.join)(HOME, '.codeium', 'windsurf', 'mcp_config.json'));
140
+ ok('MCP server added');
141
+ }
142
+ catch {
143
+ fail('MCP injection failed. Manual: add zeph to windsurf mcp_config.json');
144
+ }
145
+ try {
146
+ writeFile((0, path_1.join)(HOME, '.codeium', 'windsurf', 'hooks.json'), templates_js_1.WINDSURF_HOOKS);
147
+ ok('Response hook added');
148
+ }
149
+ catch {
150
+ fail('Hook install failed');
151
+ }
152
+ try {
153
+ // Windsurf reads ~/.codeium/windsurf/memories/global_rules.md as always-on
154
+ // global rules. Managed-block append preserves the user's own rules.
155
+ writeManagedRule((0, path_1.join)(HOME, '.codeium', 'windsurf', 'memories', 'global_rules.md'), templates_js_1.WINDSURF_RULE);
156
+ ok('Rules added to global_rules.md');
157
+ }
158
+ catch {
159
+ fail('Rule install failed. Manual: add zeph rules to ~/.codeium/windsurf/memories/global_rules.md');
160
+ }
161
+ };
162
+ const installGemini = () => {
163
+ try {
164
+ (0, child_process_1.execSync)('gemini mcp add zeph -- npx -y @zeph-to/mcp-server', { stdio: 'pipe' });
165
+ ok('MCP server added');
166
+ }
167
+ catch {
168
+ fail('MCP add failed. Manual: gemini mcp add zeph -- npx -y @zeph-to/mcp-server');
169
+ }
170
+ try {
171
+ mergeJsonFile((0, path_1.join)(HOME, '.gemini', 'settings.json'), templates_js_1.GEMINI_HOOKS);
172
+ ok('AfterAgent hook added');
173
+ }
174
+ catch {
175
+ fail('Hook install failed');
176
+ }
177
+ try {
178
+ // Gemini CLI loads ~/.gemini/GEMINI.md as global context every prompt.
179
+ writeManagedRule((0, path_1.join)(HOME, '.gemini', 'GEMINI.md'), templates_js_1.GEMINI_RULE);
180
+ ok('Rules added to GEMINI.md');
181
+ }
182
+ catch {
183
+ fail('Rule install failed. Manual: add zeph rules to ~/.gemini/GEMINI.md');
184
+ }
185
+ };
186
+ const installCodex = () => {
187
+ try {
188
+ writeFile((0, path_1.join)(HOME, '.codex', 'hooks.json'), templates_js_1.CODEX_HOOKS);
189
+ ok('Stop hook added');
190
+ }
191
+ catch {
192
+ fail('Hook install failed. Manual: add zeph to ~/.codex/hooks.json');
193
+ }
194
+ try {
195
+ // Codex CLI loads ~/.codex/AGENTS.md as global instructions.
196
+ writeManagedRule((0, path_1.join)(HOME, '.codex', 'AGENTS.md'), templates_js_1.CODEX_RULE);
197
+ ok('Rules added to AGENTS.md');
198
+ }
199
+ catch {
200
+ fail('Rule install failed. Manual: add zeph rules to ~/.codex/AGENTS.md');
201
+ }
202
+ };
203
+ const installCopilot = () => {
204
+ try {
205
+ writeFile((0, path_1.join)(HOME, '.copilot', 'hooks', 'zeph.json'), templates_js_1.COPILOT_HOOKS);
206
+ ok('Session end hook added');
207
+ }
208
+ catch {
209
+ fail('Hook install failed. Manual: add zeph to ~/.copilot/hooks/');
210
+ }
211
+ try {
212
+ // Copilot CLI loads ~/.copilot/instructions/*.instructions.md globally.
213
+ // A dedicated file means no merge needed — overwrite is safe.
214
+ writeFile((0, path_1.join)(HOME, '.copilot', 'instructions', 'zeph.instructions.md'), templates_js_1.COPILOT_RULE);
215
+ ok('Rule file added');
216
+ }
217
+ catch {
218
+ fail('Rule install failed. Manual: add zeph rules to ~/.copilot/instructions/');
219
+ }
220
+ };
221
+ const installCline = () => {
222
+ try {
223
+ writeFile((0, path_1.join)(HOME, '.cline', 'rules', 'zeph.md'), templates_js_1.CLINE_RULE);
224
+ ok('Rule file added');
225
+ }
226
+ catch {
227
+ fail('Rule install failed. Manual: add zeph to ~/.cline/rules/');
228
+ }
229
+ };
230
+ const installAider = () => {
231
+ // Aider has no hooks; rules reach it via a conventions file loaded by the
232
+ // `read:` directive in ~/.aider.conf.yml. We keep the conventions file in
233
+ // ~/.zeph/ (our own dir — no conflict) and just wire the read directive.
234
+ const conventionsPath = (0, path_1.join)(HOME, '.zeph', 'aider-conventions.md');
235
+ try {
236
+ writeFile(conventionsPath, templates_js_1.AIDER_RULE);
237
+ ok('Conventions file added');
238
+ }
239
+ catch {
240
+ fail('Conventions install failed. Manual: save zeph rules somewhere readable');
241
+ return;
242
+ }
243
+ try {
244
+ addAiderReadDirective((0, path_1.join)(HOME, '.aider.conf.yml'), conventionsPath);
245
+ ok('read: directive added to ~/.aider.conf.yml');
246
+ }
247
+ catch {
248
+ fail(`Config wiring failed. Manual: add "read: ${conventionsPath}" to ~/.aider.conf.yml`);
249
+ }
250
+ };
251
+ const AGENT_INSTALLERS = {
252
+ claude: installClaude,
253
+ cursor: installCursor,
254
+ windsurf: installWindsurf,
255
+ gemini: installGemini,
256
+ codex: installCodex,
257
+ copilot: installCopilot,
258
+ cline: installCline,
259
+ aider: installAider,
260
+ };
261
+ // One-line summary of what each agent's installer does — shown in the
262
+ // interactive plan before anything is written.
263
+ const AGENT_PLAN_LABELS = {
264
+ claude: 'Claude Code — install plugin',
265
+ cursor: 'Cursor — MCP + hooks + rules',
266
+ windsurf: 'Windsurf — MCP + hooks + rules',
267
+ gemini: 'Gemini CLI — MCP + hooks + rules',
268
+ codex: 'Codex CLI — hooks + rules',
269
+ copilot: 'Copilot CLI — hooks + rules',
270
+ cline: 'Cline — rules',
271
+ aider: 'Aider — conventions',
272
+ };
273
+ // ── Agent selection ──────────────────────────────────────────────
274
+ /**
275
+ * Interactive agent picker — an @inquirer/prompts checkbox (arrow keys
276
+ * to move, space to toggle, enter to confirm). Every agent starts
277
+ * checked, so a bare Enter installs for all. Returns the chosen Agent[].
278
+ *
279
+ * Dynamic import keeps @inquirer/prompts (ESM) loadable from this
280
+ * CommonJS build, and means the dependency is only touched on the
281
+ * interactive path — `notify` / `list` / scripted `install --only`
282
+ * never load it.
283
+ */
284
+ const pickAgentsInteractive = async (detected) => {
285
+ const { checkbox } = await import('@inquirer/prompts');
286
+ const picked = await checkbox({
287
+ message: 'Install Zeph for which agents? (space to toggle, enter to confirm)',
288
+ choices: detected.map((agent) => ({
289
+ name: AGENT_PLAN_LABELS[agent.id] ?? agent.name,
290
+ value: agent.id,
291
+ checked: true,
292
+ })),
293
+ loop: false,
294
+ });
295
+ return detected.filter((a) => picked.includes(a.id));
296
+ };
297
+ /**
298
+ * Resolve agents from a non-interactive `--only cursor,gemini` flag.
299
+ * Matches on agent id; unknown ids are silently dropped. Exported for
300
+ * unit testing.
301
+ */
302
+ const filterAgentsByIds = (detected, only) => {
303
+ const ids = new Set(only.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean));
304
+ return detected.filter((a) => ids.has(a.id));
305
+ };
306
+ exports.filterAgentsByIds = filterAgentsByIds;
307
+ // ── Test Connection ──────────────────────────────────────────────
308
+ const testConnection = async (apiKey, baseUrl) => {
309
+ try {
310
+ const hook = new zeph_hook_js_1.ZephHook({ apiKey, ...(baseUrl && { baseUrl }) });
311
+ const result = await hook.notify({
312
+ title: 'Zeph Setup',
313
+ body: `Connected successfully (v${config_js_1.VERSION})`,
314
+ });
315
+ ok(`Test push sent: ${result.pushId}`);
316
+ return true;
317
+ }
318
+ catch (err) {
319
+ fail(`Test failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
320
+ return false;
321
+ }
322
+ };
323
+ /** Interactive path when a credential already exists: prompt to keep/replace. */
324
+ const promptExistingCredentials = async (currentKey, existing) => {
325
+ if (currentKey)
326
+ console.log(` Current API Key: ${currentKey.slice(0, 12)}...`);
327
+ const keyInput = await promptInput(currentKey ? ' New API Key (Enter to keep): ' : ' API Key (from app > Settings > API Keys): ');
328
+ const currentHook = (0, config_js_1.resolvedEnv)('ZEPH_HOOK_ID') || existing.hookId;
329
+ if (currentHook)
330
+ console.log(` Current Hook ID: ${currentHook}`);
331
+ const hookInput = await promptInput(currentHook ? ' New Hook ID (Enter to keep, "none" to remove): ' : ' Hook ID (optional, for prompt/input): ');
332
+ return {
333
+ apiKey: keyInput || currentKey,
334
+ hookId: hookInput === 'none' ? undefined : (hookInput || currentHook),
335
+ baseUrl: existing.baseUrl,
336
+ };
337
+ };
338
+ /**
339
+ * Resolve API key + hook for install. Priority: --key/env/config (non-interactive
340
+ * or "keep existing") → brand-new interactive opens browser login (ADR 0002),
341
+ * falling back to manual paste when headless. wsUrl/deviceId from a login are
342
+ * persisted by runLoginFlow and re-read at config-save time.
343
+ */
344
+ const collectCredentials = async (args, installArgs, nonInteractive, existing) => {
345
+ if (nonInteractive) {
346
+ return {
347
+ apiKey: installArgs.key || (0, config_js_1.resolvedEnv)('ZEPH_API_KEY') || existing.apiKey,
348
+ hookId: installArgs.hook === 'none' ? undefined : (installArgs.hook || (0, config_js_1.resolvedEnv)('ZEPH_HOOK_ID') || existing.hookId),
349
+ baseUrl: installArgs['base-url'] || (0, config_js_1.resolvedEnv)('ZEPH_BASE_URL') || existing.baseUrl,
350
+ };
351
+ }
352
+ console.log('');
353
+ const currentKey = (0, config_js_1.resolvedEnv)('ZEPH_API_KEY') || existing.apiKey;
354
+ if (!(0, exports.shouldTriggerLogin)(nonInteractive, currentKey)) {
355
+ return promptExistingCredentials(currentKey, existing);
356
+ }
357
+ const result = await (0, login_js_1.runLoginFlow)({
358
+ webUrl: (0, login_js_1.resolveWebUrl)(args['web-url']),
359
+ timeoutSec: (0, login_js_1.resolveTimeoutSec)(args.timeout),
360
+ });
361
+ if (result) {
362
+ return { apiKey: result.apiKey, hookId: result.hookId, baseUrl: result.baseUrl };
363
+ }
364
+ // headless / timeout → manual paste
365
+ const apiKey = (await promptInput(' API Key (from app > Settings > API Keys): ')) || undefined;
366
+ const hookInput = await promptInput(' Hook ID (optional, for prompt/input): ');
367
+ return { apiKey, hookId: hookInput || undefined, baseUrl: existing.baseUrl };
368
+ };
369
+ // ── Main Install Flow ────────────────────────────────────────────
370
+ const handleInstall = async (args) => {
371
+ const installArgs = {
372
+ key: args.key,
373
+ hook: args.hook,
374
+ 'base-url': args['base-url'],
375
+ };
376
+ const nonInteractive = !!(installArgs.key || installArgs.hook || installArgs['base-url']);
377
+ console.log(`\n Zeph v${config_js_1.VERSION}\n`);
378
+ // 1. Detect agents
379
+ console.log(' Detecting agents...');
380
+ const agents = (0, agents_js_1.detectAgents)();
381
+ const detected = agents.filter((a) => a.detected);
382
+ for (const agent of agents) {
383
+ if (agent.detected) {
384
+ ok(agent.name);
385
+ }
386
+ else {
387
+ fail(`${agent.name} (not found)`);
388
+ }
389
+ }
390
+ if (detected.length === 0) {
391
+ console.log('\n No supported agents found. Config will still be saved.\n');
392
+ }
393
+ // 2. Choose which agents to install for — asked up front so the user
394
+ // sees the choice before being walked through credential prompts.
395
+ let selected = detected;
396
+ const onlyArg = args.only?.trim();
397
+ if (detected.length > 0) {
398
+ if (onlyArg) {
399
+ // Non-interactive or scripted: --only cursor,gemini
400
+ selected = (0, exports.filterAgentsByIds)(detected, onlyArg);
401
+ console.log(`\n --only ${onlyArg} → ${selected.map((a) => a.name).join(', ') || '(no match)'}`);
402
+ }
403
+ else if (nonInteractive) {
404
+ // Scripted run with no --only: keep the all-detected default
405
+ selected = detected;
406
+ }
407
+ else {
408
+ try {
409
+ selected = await pickAgentsInteractive(detected);
410
+ }
411
+ catch {
412
+ // Ctrl-C in the picker (or no TTY) — treat as a clean cancel.
413
+ console.log('\n Cancelled.\n');
414
+ return 0;
415
+ }
416
+ }
417
+ }
418
+ // 3. Collect credentials (browser login auto-triggers for brand-new installs)
419
+ const existing = (0, config_js_1.loadConfig)();
420
+ const { apiKey, hookId, baseUrl } = await collectCredentials(args, installArgs, nonInteractive, existing);
421
+ if (!apiKey) {
422
+ console.error('\n Error: API key is required.\n');
423
+ return 1;
424
+ }
425
+ // 4. Show the resolved plan before touching anything (interactive only).
426
+ if (!nonInteractive) {
427
+ console.log('\n Will do:');
428
+ console.log(` - Save config to ${config_js_1.CONFIG_FILE}`);
429
+ for (const agent of selected) {
430
+ console.log(` - ${AGENT_PLAN_LABELS[agent.id] ?? `Install for ${agent.name}`}`);
431
+ }
432
+ if (selected.length === 0) {
433
+ console.log(' (no agents selected — only the config file will be saved)');
434
+ }
435
+ console.log(' - Test connection');
436
+ }
437
+ // 5. Save config — merge over the latest on-disk config (re-read, since a
438
+ // login in step 3 may have written wsUrl/deviceId). hookId set or cleared.
439
+ console.log('');
440
+ const config = {
441
+ ...(0, config_js_1.loadConfig)(),
442
+ apiKey,
443
+ ...(baseUrl && { baseUrl }),
444
+ };
445
+ if (hookId)
446
+ config.hookId = hookId;
447
+ else
448
+ delete config.hookId;
449
+ (0, config_js_1.saveConfig)(config);
450
+ ok(`Config saved to ${config_js_1.CONFIG_FILE}`);
451
+ // 6. Install for the selected agents only
452
+ for (const agent of selected) {
453
+ console.log(`\n Installing for ${agent.name}...`);
454
+ const installer = AGENT_INSTALLERS[agent.id];
455
+ if (installer)
456
+ installer();
457
+ }
458
+ // 7. Test connection
459
+ console.log('\n Testing connection...');
460
+ await testConnection(apiKey, baseUrl);
461
+ console.log('\n Done! Restart your agents.\n');
462
+ return 0;
463
+ };
464
+ exports.handleInstall = handleInstall;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `zeph listener` — resident daemon that watches the user's Zeph feed
3
+ * over a persistent WebSocket and injects matching messages into a
4
+ * named tmux session via `tmux send-keys`.
5
+ *
6
+ * Solves the MCP polling-window problem: an `zeph_ask` polling cycle
7
+ * times out (120–600 s) and the CC/Codex session becomes unaddressable
8
+ * from the phone. The listener stays subscribed indefinitely and can
9
+ * deliver to any named tmux session at any time.
10
+ *
11
+ * Wire format: pushes with `type='agent.command'` carry the tmux
12
+ * session name in `agentSessionName` and the message in `body`. The
13
+ * "AI Agent에게 명령" sheet on the phone builds these structured
14
+ * pushes from the listener-reported session inventory. Other push
15
+ * types (Stop-hook auto-pushes, zeph_ask responses, channel
16
+ * broadcasts) are ignored.
17
+ *
18
+ * Transport: WebSocket against the Zeph $connect endpoint with
19
+ * `?apiKey=<key>`. The server fan-out pushes `{ type: 'push.new', data }`
20
+ * messages as new pushes are created. Reconnects with exponential
21
+ * backoff on transient failures; gives up on auth failures (4001/4002/4003).
22
+ */
23
+ type AgentKind = 'claude' | 'codex' | 'gemini';
24
+ interface AgentSession {
25
+ name: string;
26
+ attached: boolean;
27
+ agentKind: AgentKind;
28
+ agentSessionId?: string | null;
29
+ project: string;
30
+ label?: string | null;
31
+ createdAt?: string;
32
+ lastActivityAt?: string;
33
+ }
34
+ export declare const checkRateLimit: (session: string, now?: number) => boolean;
35
+ /** Read the foreground command in the named tmux session's active pane. */
36
+ export declare const paneCurrentCommand: (session: string) => string | null;
37
+ /**
38
+ * Mark the cached socket as no longer trustworthy — call this when a
39
+ * tmux command fails against the cached path. The next findTmuxSocket()
40
+ * will redo full discovery (default probe → /var/folders walk → lsof
41
+ * fallback) instead of returning a stale answer. Without this the
42
+ * listener wedged at "reported 0 session(s)" forever after a tmux
43
+ * server restart, even when a new server was live and discoverable.
44
+ */
45
+ export declare const invalidateTmuxSocketCache: () => void;
46
+ /**
47
+ * Parse a `zeph-*` tmux session name into `{project, label}`. For
48
+ * Phase 1 the wrapper only emits `zeph-<project>` (no labels), so the
49
+ * whole tail becomes the project. When labels land in Phase 2 the
50
+ * wrapper will sidecar `{project, label}` so the listener doesn't need
51
+ * to guess from a name that allows dashes in project names.
52
+ */
53
+ export declare const parseSessionName: (name: string) => {
54
+ project: string;
55
+ label: string | null;
56
+ } | null;
57
+ /**
58
+ * Locate the most recent Claude Code session UUID for the working
59
+ * directory of a tmux pane. Mirrors `mcp-server/config.ts`'s
60
+ * detectClaudeSessionId: CC writes per-session jsonl files at
61
+ * `~/.claude/projects/<projectHash>/<UUID>.jsonl` where the hash is
62
+ * the cwd with `/` replaced by `-`. Cached for 60s — see
63
+ * claudeSessionCache.
64
+ */
65
+ export declare const detectClaudeSessionId: (cwd: string) => string | null;
66
+ export interface CollectResult {
67
+ sessions: AgentSession[];
68
+ /** Diagnostic notes per rejected session — surfaced under `--verbose`. */
69
+ rejected: Array<{
70
+ name: string;
71
+ reason: string;
72
+ }>;
73
+ }
74
+ /**
75
+ * Inventory pass that also records *why* each `zeph-*` session was
76
+ * skipped. The verbose log uses the rejection notes to explain empty
77
+ * pickers (most common cause: tmux pane lost its start_command after a
78
+ * re-attach, and the current command is `node` rather than `claude`).
79
+ */
80
+ export declare const collectSessionsVerbose: () => CollectResult;
81
+ /**
82
+ * Snapshot the live `zeph-*` tmux sessions on this machine, enriched
83
+ * with the running agent kind, CC session UUID (claude only), project,
84
+ * and tmux activity timestamps. Returns [] when tmux is unreachable
85
+ * or no agent sessions exist. Sessions whose pane is at a shell or
86
+ * running something other than claude/codex/gemini are filtered out
87
+ * — the phone can't usefully address them.
88
+ */
89
+ export declare const collectSessions: () => AgentSession[];
90
+ interface PushItem {
91
+ pushId: string;
92
+ type?: string;
93
+ body?: string;
94
+ title?: string;
95
+ createdAt?: string;
96
+ isEncrypted?: boolean;
97
+ /** Set when type='agent.command' — tmux session name to inject into. */
98
+ agentSessionName?: string;
99
+ }
100
+ interface HandlePushDeps {
101
+ paneCommand?: (session: string) => string | null;
102
+ inject?: (session: string, text: string) => boolean;
103
+ rateLimit?: (session: string) => boolean;
104
+ now?: () => number;
105
+ }
106
+ /**
107
+ * Process one push. Returns true when an injection actually fired.
108
+ * Exported for unit testing with mocked deps.
109
+ *
110
+ * Only acts on `type='agent.command'` pushes carrying both an
111
+ * `agentSessionName` (tmux session to inject into) and a non-empty
112
+ * `body`. Everything else (Stop-hook auto-pushes, zeph_ask responses,
113
+ * encrypted pushes, normal text/link/file notifications) is ignored.
114
+ */
115
+ export declare const handlePush: (push: PushItem, deps?: HandlePushDeps) => boolean;
116
+ /**
117
+ * Stable per-host device id for the listener. We hash the OS hostname so
118
+ * the same machine reuses the same DeviceRecord across listener restarts
119
+ * (otherwise the phone's session inventory grows a new ghost device every
120
+ * time `zeph listener` rebinds). `dev_listener_<sha8(hostname)>` keeps it
121
+ * human-recognisable in dev logs without leaking the raw hostname.
122
+ */
123
+ export declare const computeListenerDeviceId: (host?: string) => string;
124
+ export declare const handleListener: (args: Record<string, string | boolean>) => Promise<number>;
125
+ export {};
126
+ //# sourceMappingURL=listener.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAmCH,KAAK,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAG/C,UAAU,YAAY;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AA2BD,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,MAAK,MAAmB,KAAG,OAgB1E,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAAM,GAAG,IAO7D,CAAC;AAiDF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAG5C,CAAC;AA8NF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAK3F,CAAC;AAuCF;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAiB5D,CAAC;AAkFF,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,QAAO,aA+DzC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,QAAO,YAAY,EAAuC,CAAC;AAIvF,UAAU,QAAQ;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,UAAU,cAAc;IACpB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACpD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAgCD;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GACnB,MAAM,QAAQ,EACd,OAAM,cAAmB,KAC1B,OAQF,CAAC;AA2BF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GAAI,OAAM,MAAmB,KAAG,MAGnE,CAAC;AA8NF,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAyF3F,CAAC"}