@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.
- package/LICENSE +190 -0
- package/README.md +499 -0
- package/dist/agents.d.ts +8 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +29 -0
- package/dist/check-update.d.ts +4 -0
- package/dist/check-update.d.ts.map +1 -0
- package/dist/check-update.js +80 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +374 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/crypto.d.ts +82 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +291 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/installer.d.ts +14 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +464 -0
- package/dist/listener.d.ts +126 -0
- package/dist/listener.d.ts.map +1 -0
- package/dist/listener.js +1008 -0
- package/dist/login.d.ts +38 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +182 -0
- package/dist/templates.d.ts +44 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +257 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +2 -0
- package/dist/uninstall.d.ts.map +1 -0
- package/dist/uninstall.js +217 -0
- package/dist/verify.d.ts +2 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +109 -0
- package/dist/wrapper.d.ts +26 -0
- package/dist/wrapper.d.ts.map +1 -0
- package/dist/wrapper.js +238 -0
- package/dist/zeph-hook.d.ts +23 -0
- package/dist/zeph-hook.d.ts.map +1 -0
- package/dist/zeph-hook.js +196 -0
- 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"}
|