@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,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleUninstall = 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 agents_js_1 = require("./agents.js");
|
|
9
|
+
const templates_js_1 = require("./templates.js");
|
|
10
|
+
const config_js_1 = require("./config.js");
|
|
11
|
+
const HOME = (0, os_1.homedir)();
|
|
12
|
+
const ok = (msg) => console.log(` + ${msg}`);
|
|
13
|
+
const skip = (msg) => console.log(` - ${msg}`);
|
|
14
|
+
// ── Removal primitives ───────────────────────────────────────────
|
|
15
|
+
// Each primitive returns a short human description of what it did (or
|
|
16
|
+
// would do, in dry-run), or null when there was nothing to remove.
|
|
17
|
+
/** Past/conditional verb so dry-run output reads honestly. */
|
|
18
|
+
const verb = (dry) => (dry ? 'would remove' : 'removed');
|
|
19
|
+
/** Delete a file Zeph fully owns. */
|
|
20
|
+
const rmFile = (filePath, dry) => {
|
|
21
|
+
if (!(0, fs_1.existsSync)(filePath))
|
|
22
|
+
return null;
|
|
23
|
+
if (!dry)
|
|
24
|
+
(0, fs_1.rmSync)(filePath, { force: true });
|
|
25
|
+
return `${verb(dry)} ${filePath}`;
|
|
26
|
+
};
|
|
27
|
+
/** Remove just the `zeph` entry from an mcpServers JSON file. */
|
|
28
|
+
const rmMcpEntry = (filePath, dry) => {
|
|
29
|
+
if (!(0, fs_1.existsSync)(filePath))
|
|
30
|
+
return null;
|
|
31
|
+
let data;
|
|
32
|
+
try {
|
|
33
|
+
data = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const servers = data.mcpServers;
|
|
39
|
+
if (!servers || !('zeph' in servers))
|
|
40
|
+
return null;
|
|
41
|
+
if (!dry) {
|
|
42
|
+
delete servers.zeph;
|
|
43
|
+
(0, fs_1.writeFileSync)(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
44
|
+
}
|
|
45
|
+
return `${verb(dry)} zeph from ${filePath}`;
|
|
46
|
+
};
|
|
47
|
+
/** Strip the <!-- ZEPH:START/END --> block from a shared rule file. */
|
|
48
|
+
const stripManagedRule = (filePath, dry) => {
|
|
49
|
+
if (!(0, fs_1.existsSync)(filePath))
|
|
50
|
+
return null;
|
|
51
|
+
const existing = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
52
|
+
const stripped = (0, templates_js_1.removeManagedBlock)(existing);
|
|
53
|
+
if (stripped === existing)
|
|
54
|
+
return null; // no Zeph block present
|
|
55
|
+
if (!dry) {
|
|
56
|
+
if (stripped.trim() === '') {
|
|
57
|
+
(0, fs_1.rmSync)(filePath, { force: true }); // file was ours alone
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
(0, fs_1.writeFileSync)(filePath, stripped);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return `${verb(dry)} Zeph block from ${filePath}`;
|
|
64
|
+
};
|
|
65
|
+
/** Drop the Zeph `read:` directive from ~/.aider.conf.yml. */
|
|
66
|
+
const rmAiderReadDirective = (confPath, dry) => {
|
|
67
|
+
if (!(0, fs_1.existsSync)(confPath))
|
|
68
|
+
return null;
|
|
69
|
+
const conf = (0, fs_1.readFileSync)(confPath, 'utf-8');
|
|
70
|
+
if (!conf.includes('# Added by Zeph'))
|
|
71
|
+
return null;
|
|
72
|
+
// Drop the "# Added by Zeph" line and the "read:" line that follows it.
|
|
73
|
+
const lines = conf.split('\n');
|
|
74
|
+
const out = [];
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
if (lines[i].trim() === '# Added by Zeph') {
|
|
77
|
+
if (lines[i + 1]?.trimStart().startsWith('read:'))
|
|
78
|
+
i++; // skip read: too
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
out.push(lines[i]);
|
|
82
|
+
}
|
|
83
|
+
if (!dry)
|
|
84
|
+
(0, fs_1.writeFileSync)(confPath, out.join('\n').replace(/\n{3,}/g, '\n\n'));
|
|
85
|
+
return `${verb(dry)} Zeph read: directive from ${confPath}`;
|
|
86
|
+
};
|
|
87
|
+
/** Remove just the zeph-notify entry from Gemini's settings.json. */
|
|
88
|
+
const rmGeminiHook = (filePath, dry) => {
|
|
89
|
+
if (!(0, fs_1.existsSync)(filePath))
|
|
90
|
+
return null;
|
|
91
|
+
let data;
|
|
92
|
+
try {
|
|
93
|
+
data = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const hooks = data.hooks;
|
|
99
|
+
const afterAgent = hooks?.AfterAgent;
|
|
100
|
+
if (!Array.isArray(afterAgent))
|
|
101
|
+
return null;
|
|
102
|
+
const kept = afterAgent.filter((entry) => !(entry.hooks ?? []).some((h) => h.name === 'zeph-notify'));
|
|
103
|
+
if (kept.length === afterAgent.length)
|
|
104
|
+
return null; // nothing of ours
|
|
105
|
+
if (!dry) {
|
|
106
|
+
if (kept.length === 0) {
|
|
107
|
+
delete hooks.AfterAgent;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
hooks.AfterAgent = kept;
|
|
111
|
+
}
|
|
112
|
+
(0, fs_1.writeFileSync)(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
113
|
+
}
|
|
114
|
+
return `${verb(dry)} zeph-notify hook from ${filePath}`;
|
|
115
|
+
};
|
|
116
|
+
const runSteps = (steps) => {
|
|
117
|
+
let did = false;
|
|
118
|
+
for (const step of steps) {
|
|
119
|
+
const result = step();
|
|
120
|
+
if (result) {
|
|
121
|
+
ok(result);
|
|
122
|
+
did = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!did)
|
|
126
|
+
skip('nothing to remove');
|
|
127
|
+
};
|
|
128
|
+
const AGENT_UNINSTALLERS = {
|
|
129
|
+
claude: (dry) => {
|
|
130
|
+
if (dry) {
|
|
131
|
+
skip('would run: claude plugin uninstall zeph@zeph');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
(0, child_process_1.execSync)('claude plugin uninstall zeph@zeph', { stdio: 'pipe' });
|
|
136
|
+
ok('plugin uninstalled');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
skip('plugin not installed (or claude CLI unavailable)');
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
cursor: (dry) => runSteps([
|
|
143
|
+
() => rmMcpEntry((0, path_1.join)(HOME, '.cursor', 'mcp.json'), dry),
|
|
144
|
+
() => rmFile((0, path_1.join)(HOME, '.cursor', 'hooks.json'), dry),
|
|
145
|
+
() => rmFile((0, path_1.join)(HOME, '.cursor', 'rules', 'zeph.mdc'), dry),
|
|
146
|
+
]),
|
|
147
|
+
windsurf: (dry) => runSteps([
|
|
148
|
+
() => rmMcpEntry((0, path_1.join)(HOME, '.codeium', 'windsurf', 'mcp_config.json'), dry),
|
|
149
|
+
() => rmFile((0, path_1.join)(HOME, '.codeium', 'windsurf', 'hooks.json'), dry),
|
|
150
|
+
() => stripManagedRule((0, path_1.join)(HOME, '.codeium', 'windsurf', 'memories', 'global_rules.md'), dry),
|
|
151
|
+
]),
|
|
152
|
+
gemini: (dry) => {
|
|
153
|
+
if (!dry) {
|
|
154
|
+
try {
|
|
155
|
+
(0, child_process_1.execSync)('gemini mcp remove zeph', { stdio: 'pipe' });
|
|
156
|
+
ok('MCP server removed');
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
skip('gemini MCP entry not found');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
skip('would run: gemini mcp remove zeph');
|
|
164
|
+
}
|
|
165
|
+
runSteps([
|
|
166
|
+
() => rmGeminiHook((0, path_1.join)(HOME, '.gemini', 'settings.json'), dry),
|
|
167
|
+
() => stripManagedRule((0, path_1.join)(HOME, '.gemini', 'GEMINI.md'), dry),
|
|
168
|
+
]);
|
|
169
|
+
},
|
|
170
|
+
codex: (dry) => runSteps([
|
|
171
|
+
() => rmFile((0, path_1.join)(HOME, '.codex', 'hooks.json'), dry),
|
|
172
|
+
() => stripManagedRule((0, path_1.join)(HOME, '.codex', 'AGENTS.md'), dry),
|
|
173
|
+
]),
|
|
174
|
+
copilot: (dry) => runSteps([
|
|
175
|
+
() => rmFile((0, path_1.join)(HOME, '.copilot', 'hooks', 'zeph.json'), dry),
|
|
176
|
+
() => rmFile((0, path_1.join)(HOME, '.copilot', 'instructions', 'zeph.instructions.md'), dry),
|
|
177
|
+
]),
|
|
178
|
+
cline: (dry) => runSteps([
|
|
179
|
+
() => rmFile((0, path_1.join)(HOME, '.cline', 'rules', 'zeph.md'), dry),
|
|
180
|
+
]),
|
|
181
|
+
aider: (dry) => runSteps([
|
|
182
|
+
() => rmFile((0, path_1.join)(HOME, '.zeph', 'aider-conventions.md'), dry),
|
|
183
|
+
() => rmAiderReadDirective((0, path_1.join)(HOME, '.aider.conf.yml'), dry),
|
|
184
|
+
]),
|
|
185
|
+
};
|
|
186
|
+
// ── Entry point ──────────────────────────────────────────────────
|
|
187
|
+
const handleUninstall = async (args) => {
|
|
188
|
+
const dry = args['dry-run'] === true;
|
|
189
|
+
const purge = args.purge === true;
|
|
190
|
+
console.log(`\n Zeph uninstall${dry ? ' (dry-run)' : ''} — v${config_js_1.VERSION}\n`);
|
|
191
|
+
const detected = (0, agents_js_1.detectAgents)().filter((a) => a.detected);
|
|
192
|
+
if (detected.length === 0) {
|
|
193
|
+
console.log(' No supported agents detected.\n');
|
|
194
|
+
}
|
|
195
|
+
for (const agent of detected) {
|
|
196
|
+
console.log(` ${agent.name}:`);
|
|
197
|
+
AGENT_UNINSTALLERS[agent.id]?.(dry);
|
|
198
|
+
}
|
|
199
|
+
// ~/.zeph/config.json holds the API key — kept by default so a
|
|
200
|
+
// re-install doesn't need the key re-entered. --purge removes it.
|
|
201
|
+
console.log('\n Config:');
|
|
202
|
+
if (purge) {
|
|
203
|
+
const removed = rmFile(config_js_1.CONFIG_FILE, dry);
|
|
204
|
+
if (removed)
|
|
205
|
+
ok(removed);
|
|
206
|
+
else
|
|
207
|
+
skip('no config file');
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
skip(`kept ${config_js_1.CONFIG_FILE} (pass --purge to remove)`);
|
|
211
|
+
}
|
|
212
|
+
console.log(dry
|
|
213
|
+
? '\n Dry-run complete — nothing was changed.\n'
|
|
214
|
+
: '\n Uninstall complete. Restart your agents.\n');
|
|
215
|
+
return 0;
|
|
216
|
+
};
|
|
217
|
+
exports.handleUninstall = handleUninstall;
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AA0CA,eAAO,MAAM,YAAY,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAwEzF,CAAC"}
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleVerify = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const os_1 = require("os");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const agents_js_1 = require("./agents.js");
|
|
8
|
+
const config_js_1 = require("./config.js");
|
|
9
|
+
const zeph_hook_js_1 = require("./zeph-hook.js");
|
|
10
|
+
const HOME = (0, os_1.homedir)();
|
|
11
|
+
const pass = (msg) => console.log(` ✓ ${msg}`);
|
|
12
|
+
const warn = (msg) => console.log(` ! ${msg}`);
|
|
13
|
+
const failMsg = (msg) => console.log(` ✗ ${msg}`);
|
|
14
|
+
/** Does a shared rule file contain the Zeph managed block? */
|
|
15
|
+
const hasManagedBlock = (filePath) => {
|
|
16
|
+
try {
|
|
17
|
+
return (0, fs_1.readFileSync)(filePath, 'utf-8').includes('ZEPH:START');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
// Per-agent: report whether the rule artifact Zeph installs is present.
|
|
24
|
+
const AGENT_RULE_PRESENT = {
|
|
25
|
+
claude: () => {
|
|
26
|
+
try {
|
|
27
|
+
return /zeph/.test((0, fs_1.readFileSync)((0, path_1.join)(HOME, '.claude.json'), 'utf-8'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return (0, fs_1.existsSync)((0, path_1.join)(HOME, '.claude', 'plugins'));
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
cursor: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.cursor', 'rules', 'zeph.mdc')),
|
|
34
|
+
windsurf: () => hasManagedBlock((0, path_1.join)(HOME, '.codeium', 'windsurf', 'memories', 'global_rules.md')),
|
|
35
|
+
gemini: () => hasManagedBlock((0, path_1.join)(HOME, '.gemini', 'GEMINI.md')),
|
|
36
|
+
codex: () => hasManagedBlock((0, path_1.join)(HOME, '.codex', 'AGENTS.md')),
|
|
37
|
+
copilot: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.copilot', 'instructions', 'zeph.instructions.md')),
|
|
38
|
+
cline: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.cline', 'rules', 'zeph.md')),
|
|
39
|
+
aider: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.zeph', 'aider-conventions.md')),
|
|
40
|
+
};
|
|
41
|
+
const handleVerify = async (args) => {
|
|
42
|
+
const doPing = args.ping === true;
|
|
43
|
+
const checks = [];
|
|
44
|
+
const record = (label, state) => {
|
|
45
|
+
checks.push({ label, state });
|
|
46
|
+
if (state === 'pass')
|
|
47
|
+
pass(label);
|
|
48
|
+
else if (state === 'warn')
|
|
49
|
+
warn(label);
|
|
50
|
+
else
|
|
51
|
+
failMsg(label);
|
|
52
|
+
};
|
|
53
|
+
console.log(`\n Zeph verify — v${config_js_1.VERSION}\n`);
|
|
54
|
+
// ── Credentials ──────────────────────────────────────────────
|
|
55
|
+
console.log(' Credentials:');
|
|
56
|
+
const config = (0, config_js_1.loadConfig)();
|
|
57
|
+
const apiKey = (0, config_js_1.resolvedEnv)('ZEPH_API_KEY') || config.apiKey;
|
|
58
|
+
const hookId = (0, config_js_1.resolvedEnv)('ZEPH_HOOK_ID') || config.hookId;
|
|
59
|
+
record(apiKey ? 'ZEPH_API_KEY is set' : 'ZEPH_API_KEY not set (env or ~/.zeph/config.json)', apiKey ? 'pass' : 'fail');
|
|
60
|
+
record(hookId
|
|
61
|
+
? 'ZEPH_HOOK_ID is set (two-way zeph_ask/prompt/input enabled)'
|
|
62
|
+
: 'ZEPH_HOOK_ID not set (notify-only — set it for remote control)', hookId ? 'pass' : 'warn');
|
|
63
|
+
// ── Runtime ──────────────────────────────────────────────────
|
|
64
|
+
console.log('\n Runtime:');
|
|
65
|
+
record((0, agents_js_1.hasCommand)('node') ? 'node available' : 'node not found', (0, agents_js_1.hasCommand)('node') ? 'pass' : 'fail');
|
|
66
|
+
record((0, agents_js_1.hasCommand)('npx') ? 'npx available (MCP server runs via npx)' : 'npx not found', (0, agents_js_1.hasCommand)('npx') ? 'pass' : 'fail');
|
|
67
|
+
record((0, agents_js_1.hasCommand)('zeph')
|
|
68
|
+
? 'zeph CLI on PATH'
|
|
69
|
+
: 'zeph CLI not on PATH (hooks fall back to npx — slower first call)', (0, agents_js_1.hasCommand)('zeph') ? 'pass' : 'warn');
|
|
70
|
+
// ── Per-agent config ─────────────────────────────────────────
|
|
71
|
+
console.log('\n Agents:');
|
|
72
|
+
const detected = (0, agents_js_1.detectAgents)().filter((a) => a.detected);
|
|
73
|
+
if (detected.length === 0) {
|
|
74
|
+
warn('no supported agents detected');
|
|
75
|
+
}
|
|
76
|
+
for (const agent of detected) {
|
|
77
|
+
const present = AGENT_RULE_PRESENT[agent.id]?.() ?? false;
|
|
78
|
+
record(`${agent.name}: ${present ? 'Zeph rules installed' : 'Zeph rules NOT installed — run: zeph install'}`, present ? 'pass' : 'warn');
|
|
79
|
+
}
|
|
80
|
+
// ── Optional live API ping ───────────────────────────────────
|
|
81
|
+
if (doPing) {
|
|
82
|
+
console.log('\n API ping:');
|
|
83
|
+
if (!apiKey) {
|
|
84
|
+
record('skipped — no API key', 'warn');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
try {
|
|
88
|
+
const hook = new zeph_hook_js_1.ZephHook({ apiKey, ...(config.baseUrl && { baseUrl: config.baseUrl }) });
|
|
89
|
+
await hook.list({ limit: 1 });
|
|
90
|
+
record('API reachable, key accepted', 'pass');
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
record(`API call failed: ${err instanceof Error ? err.message : 'unknown'}`, 'fail');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// ── Summary ──────────────────────────────────────────────────
|
|
98
|
+
const fails = checks.filter((c) => c.state === 'fail').length;
|
|
99
|
+
const warns = checks.filter((c) => c.state === 'warn').length;
|
|
100
|
+
console.log('');
|
|
101
|
+
if (fails === 0 && warns === 0) {
|
|
102
|
+
console.log(' ✓ All checks passed.\n');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.log(` ${fails} failed, ${warns} warnings.${doPing ? '' : ' (run with --ping to test the API)'}\n`);
|
|
106
|
+
}
|
|
107
|
+
return fails === 0 ? 0 : 1;
|
|
108
|
+
};
|
|
109
|
+
exports.handleVerify = handleVerify;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Resolve a project name for the tmux session: env > git root > cwd basename. */
|
|
2
|
+
export declare const detectProjectName: () => string;
|
|
3
|
+
/** `zeph-<project>` — the canonical tmux session base name. */
|
|
4
|
+
export declare const tmuxSessionName: (project: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Pick a tmux session name that won't steal focus from another live
|
|
7
|
+
* `zeph cc`. Strategy:
|
|
8
|
+
* - If `<base>` doesn't exist → use it (create new).
|
|
9
|
+
* - If `<base>` exists but is detached → use it (reattach).
|
|
10
|
+
* - If `<base>` exists *and* has a client attached → try `<base>-2`,
|
|
11
|
+
* `<base>-3`, … so the new `zeph cc` gets an independent session
|
|
12
|
+
* instead of joining the existing one.
|
|
13
|
+
* Falls back to `<base>` after 20 attempts (shouldn't realistically hit).
|
|
14
|
+
*
|
|
15
|
+
* Detection uses `tmux has-session` and `tmux list-clients`; both are
|
|
16
|
+
* dependency-free against the user's running tmux server.
|
|
17
|
+
*/
|
|
18
|
+
export declare const findAvailableSession: (base: string) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Launch the agent in a named tmux session (or directly if nested) and
|
|
21
|
+
* forward its exit code. `extra` is appended to the agent invocation, so
|
|
22
|
+
* `zeph cc --resume foo` runs `claude --resume foo` inside the session.
|
|
23
|
+
* Returns when the agent exits.
|
|
24
|
+
*/
|
|
25
|
+
export declare const handleAgentSession: (agent: string, extra?: string[]) => Promise<number>;
|
|
26
|
+
//# sourceMappingURL=wrapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"AAwBA,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,QAAO,MAapC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,MAA2B,CAAC;AAI9E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAenD,CAAC;AA6HF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CAmCtF,CAAC"}
|
package/dist/wrapper.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleAgentSession = exports.findAvailableSession = exports.tmuxSessionName = exports.detectProjectName = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `zeph cc` / `zeph codex` / `zeph gemini` — spawn an agent inside a named
|
|
6
|
+
* tmux session so the resident listener (`zeph listener`) can address it
|
|
7
|
+
* by session name to inject messages later.
|
|
8
|
+
*
|
|
9
|
+
* The tmux session name follows `zeph-<project>` where <project> resolves
|
|
10
|
+
* from CLAUDE/CURSOR/WINDSURF_PROJECT_DIR → git repo root → cwd basename.
|
|
11
|
+
* When the wrapper is invoked from inside an existing tmux session
|
|
12
|
+
* ($TMUX set) it skips the outer tmux to avoid nesting and execs the
|
|
13
|
+
* agent directly — letting power users keep their own multiplexer setup.
|
|
14
|
+
*/
|
|
15
|
+
const child_process_1 = require("child_process");
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const os_1 = require("os");
|
|
18
|
+
const path_1 = require("path");
|
|
19
|
+
/** First non-empty value among the supported per-agent project dir env vars. */
|
|
20
|
+
const PROJECT_DIR_ENVS = ['CLAUDE_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'WINDSURF_PROJECT_DIR'];
|
|
21
|
+
const FALLBACK_NAME = 'project';
|
|
22
|
+
/** basename(), with a stable fallback for edge paths like `/`. */
|
|
23
|
+
const safeBasename = (path) => (0, path_1.basename)(path) || FALLBACK_NAME;
|
|
24
|
+
/** Resolve a project name for the tmux session: env > git root > cwd basename. */
|
|
25
|
+
const detectProjectName = () => {
|
|
26
|
+
for (const key of PROJECT_DIR_ENVS) {
|
|
27
|
+
const v = process.env[key];
|
|
28
|
+
if (v)
|
|
29
|
+
return safeBasename(v.replace(/\/+$/, ''));
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const root = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], {
|
|
33
|
+
encoding: 'utf-8',
|
|
34
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
35
|
+
}).trim();
|
|
36
|
+
if (root)
|
|
37
|
+
return safeBasename(root);
|
|
38
|
+
}
|
|
39
|
+
catch { /* not a git repo — fall through */ }
|
|
40
|
+
return safeBasename(process.cwd());
|
|
41
|
+
};
|
|
42
|
+
exports.detectProjectName = detectProjectName;
|
|
43
|
+
/** `zeph-<project>` — the canonical tmux session base name. */
|
|
44
|
+
const tmuxSessionName = (project) => `zeph-${project}`;
|
|
45
|
+
exports.tmuxSessionName = tmuxSessionName;
|
|
46
|
+
const MAX_SUFFIX_ATTEMPTS = 20;
|
|
47
|
+
/**
|
|
48
|
+
* Pick a tmux session name that won't steal focus from another live
|
|
49
|
+
* `zeph cc`. Strategy:
|
|
50
|
+
* - If `<base>` doesn't exist → use it (create new).
|
|
51
|
+
* - If `<base>` exists but is detached → use it (reattach).
|
|
52
|
+
* - If `<base>` exists *and* has a client attached → try `<base>-2`,
|
|
53
|
+
* `<base>-3`, … so the new `zeph cc` gets an independent session
|
|
54
|
+
* instead of joining the existing one.
|
|
55
|
+
* Falls back to `<base>` after 20 attempts (shouldn't realistically hit).
|
|
56
|
+
*
|
|
57
|
+
* Detection uses `tmux has-session` and `tmux list-clients`; both are
|
|
58
|
+
* dependency-free against the user's running tmux server.
|
|
59
|
+
*/
|
|
60
|
+
const findAvailableSession = (base) => {
|
|
61
|
+
for (let i = 0; i < MAX_SUFFIX_ATTEMPTS; i++) {
|
|
62
|
+
const name = i === 0 ? base : `${base}-${i + 1}`;
|
|
63
|
+
const has = (0, child_process_1.spawnSync)('tmux', ['has-session', '-t', name], {
|
|
64
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
65
|
+
});
|
|
66
|
+
if (has.status !== 0)
|
|
67
|
+
return name; // doesn't exist — fresh session
|
|
68
|
+
const clients = (0, child_process_1.spawnSync)('tmux', ['list-clients', '-t', name, '-F', '#{client_tty}'], {
|
|
69
|
+
encoding: 'utf-8',
|
|
70
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
71
|
+
});
|
|
72
|
+
const attached = (clients.stdout ?? '').trim().length > 0;
|
|
73
|
+
if (!attached)
|
|
74
|
+
return name; // exists but detached — reattach
|
|
75
|
+
}
|
|
76
|
+
return base;
|
|
77
|
+
};
|
|
78
|
+
exports.findAvailableSession = findAvailableSession;
|
|
79
|
+
/** POSIX shell-quote so passthrough args survive being joined into a tmux shell-command string. */
|
|
80
|
+
const SHELL_SAFE = /^[\w\-./=:@%+,]+$/;
|
|
81
|
+
const shellQuote = (s) => s.length > 0 && SHELL_SAFE.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
|
|
82
|
+
const targetForAgent = (agent, extra) => {
|
|
83
|
+
// Already inside tmux → no nested session, just run the agent in the
|
|
84
|
+
// current pane. Nested tmux prefix collisions are confusing and the
|
|
85
|
+
// listener can't reach a session it didn't name anyway.
|
|
86
|
+
if (process.env.TMUX) {
|
|
87
|
+
return { cmd: agent, args: extra };
|
|
88
|
+
}
|
|
89
|
+
const base = (0, exports.tmuxSessionName)((0, exports.detectProjectName)());
|
|
90
|
+
// Auto-suffix when the default name is taken by another attached
|
|
91
|
+
// session — lets the user keep `zeph cc` workflow simple and still
|
|
92
|
+
// get independent sessions when opening multiple terminals in the
|
|
93
|
+
// same project.
|
|
94
|
+
const session = (0, exports.findAvailableSession)(base);
|
|
95
|
+
// `tmux new -A`: attach if the named session exists, else create it.
|
|
96
|
+
// tmux joins trailing argv into a single shell-command, so flags like
|
|
97
|
+
// `--resume` would be eaten by tmux's own parser. Build one quoted
|
|
98
|
+
// shell string instead, which tmux passes through verbatim.
|
|
99
|
+
const shellCmd = [agent, ...extra].map(shellQuote).join(' ');
|
|
100
|
+
return { cmd: 'tmux', args: ['new', '-A', '-s', session, shellCmd] };
|
|
101
|
+
};
|
|
102
|
+
// ── Background listener auto-start ────────────────────────────────────
|
|
103
|
+
const ZEPH_DIR = (0, path_1.join)((0, os_1.homedir)(), '.zeph');
|
|
104
|
+
const LISTENER_PID_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.pid');
|
|
105
|
+
const LISTENER_LOG_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.log');
|
|
106
|
+
/** True when the PID file points at a still-alive process. */
|
|
107
|
+
const listenerAlive = () => {
|
|
108
|
+
try {
|
|
109
|
+
const pid = Number((0, fs_1.readFileSync)(LISTENER_PID_FILE, 'utf-8').trim());
|
|
110
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
111
|
+
return false;
|
|
112
|
+
// Signal 0 = existence check; throws when the process is gone.
|
|
113
|
+
process.kill(pid, 0);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Path to the running cli.js entry. wrapper.js sits next to cli.js in
|
|
122
|
+
* dist/, so __dirname resolves it directly — independent of how the
|
|
123
|
+
* user invoked us.
|
|
124
|
+
*
|
|
125
|
+
* `process.argv[1]` is unreliable here: when `zeph` runs via the
|
|
126
|
+
* npm-installed bin shim (`/usr/local/bin/zeph` → cli.js via a wrapper
|
|
127
|
+
* script), argv[1] is the shim path (`.../bin/zeph`), NOT `cli.js`.
|
|
128
|
+
* That made the original `/cli\\.(js|ts|mjs|cjs)$/` check silently
|
|
129
|
+
* reject the entry and the autospawn never fired — exactly the bug
|
|
130
|
+
* the user hit ('원래 싱글톤으로 됐었잖아' — yes, on the local alias
|
|
131
|
+
* path where argv[1] IS cli.js; the bug only surfaced once the user
|
|
132
|
+
* switched to the global npm install).
|
|
133
|
+
*
|
|
134
|
+
* Fall back to argv[1] only when the __dirname-relative file doesn't
|
|
135
|
+
* exist (some packaging where dist layout differs).
|
|
136
|
+
*/
|
|
137
|
+
const resolveCliPath = () => {
|
|
138
|
+
const local = (0, path_1.join)(__dirname, 'cli.js');
|
|
139
|
+
if ((0, fs_1.existsSync)(local))
|
|
140
|
+
return local;
|
|
141
|
+
const entry = process.argv[1];
|
|
142
|
+
if (entry && /cli\.(js|ts|mjs|cjs)$/.test(entry))
|
|
143
|
+
return entry;
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Spawn `zeph listener` in the background if it isn't already running on
|
|
148
|
+
* this machine. The intent is that the user only ever has to know about
|
|
149
|
+
* `zeph cc` — the phone-to-tmux bridge tags along automatically. Output
|
|
150
|
+
* goes to `~/.zeph/listener.log` so it isn't lost on detach; the listener
|
|
151
|
+
* itself writes its own PID to `~/.zeph/listener.pid` on startup and
|
|
152
|
+
* removes it on graceful exit, so subsequent `zeph cc` invocations skip
|
|
153
|
+
* the spawn when a listener is already up.
|
|
154
|
+
*
|
|
155
|
+
* Failure here is non-fatal — `zeph cc` still launches the agent. The
|
|
156
|
+
* user just loses the phone-bridge feature until they restart.
|
|
157
|
+
*/
|
|
158
|
+
/**
|
|
159
|
+
* Rotate the listener log once it grows past 5 MB. The daemon runs for
|
|
160
|
+
* days and writes 2-3 lines per 5-s cycle, so without rotation the file
|
|
161
|
+
* climbs into the tens of megabytes range pretty quickly. We keep the
|
|
162
|
+
* previous run's tail under `.old` for post-mortem and start fresh.
|
|
163
|
+
*/
|
|
164
|
+
const LISTENER_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
165
|
+
const rotateListenerLogIfLarge = () => {
|
|
166
|
+
try {
|
|
167
|
+
if (!(0, fs_1.existsSync)(LISTENER_LOG_FILE))
|
|
168
|
+
return;
|
|
169
|
+
if ((0, fs_1.statSync)(LISTENER_LOG_FILE).size <= LISTENER_LOG_MAX_BYTES)
|
|
170
|
+
return;
|
|
171
|
+
(0, fs_1.renameSync)(LISTENER_LOG_FILE, LISTENER_LOG_FILE + '.old');
|
|
172
|
+
}
|
|
173
|
+
catch { /* best-effort */ }
|
|
174
|
+
};
|
|
175
|
+
const ensureListenerRunning = () => {
|
|
176
|
+
if (listenerAlive())
|
|
177
|
+
return;
|
|
178
|
+
const cliPath = resolveCliPath();
|
|
179
|
+
if (!cliPath || !(0, fs_1.existsSync)(cliPath))
|
|
180
|
+
return;
|
|
181
|
+
try {
|
|
182
|
+
(0, fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
|
|
183
|
+
rotateListenerLogIfLarge();
|
|
184
|
+
const out = (0, fs_1.openSync)(LISTENER_LOG_FILE, 'a');
|
|
185
|
+
const child = (0, child_process_1.spawn)(process.execPath, [cliPath, 'listener'], {
|
|
186
|
+
detached: true,
|
|
187
|
+
stdio: ['ignore', out, out],
|
|
188
|
+
env: { ...process.env, ZEPH_LISTENER_AUTOSTART: '1' },
|
|
189
|
+
});
|
|
190
|
+
child.unref();
|
|
191
|
+
console.log(`zeph: listener autostarted in background (log: ${LISTENER_LOG_FILE})`);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
console.error(`zeph: listener autostart failed: ${err.message}`);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Launch the agent in a named tmux session (or directly if nested) and
|
|
199
|
+
* forward its exit code. `extra` is appended to the agent invocation, so
|
|
200
|
+
* `zeph cc --resume foo` runs `claude --resume foo` inside the session.
|
|
201
|
+
* Returns when the agent exits.
|
|
202
|
+
*/
|
|
203
|
+
const handleAgentSession = (agent, extra = []) => {
|
|
204
|
+
// Best-effort: make sure the phone-bridge daemon is running before we
|
|
205
|
+
// launch the agent. The user shouldn't need to remember a second
|
|
206
|
+
// command for the picker on their phone to work.
|
|
207
|
+
ensureListenerRunning();
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
const { cmd, args } = targetForAgent(agent, extra);
|
|
210
|
+
const start = Date.now();
|
|
211
|
+
const child = (0, child_process_1.spawn)(cmd, args, { stdio: 'inherit' });
|
|
212
|
+
child.on('exit', (code) => {
|
|
213
|
+
const dur = Date.now() - start;
|
|
214
|
+
// Short-lived non-zero exits are the symptom of "ran from a
|
|
215
|
+
// pane that isn't a real TTY" (iTerm tmux integration pane,
|
|
216
|
+
// some IDE terminals). The user otherwise just sees their
|
|
217
|
+
// shell return with `[exited]` and no clue what went wrong.
|
|
218
|
+
if (code && code !== 0 && dur < 2000) {
|
|
219
|
+
console.error(`zeph: ${cmd} ${args.join(' ')} exited ${code} after ${dur}ms.\n` +
|
|
220
|
+
` If this terminal is itself inside tmux (or an iTerm/Warp\n` +
|
|
221
|
+
` tmux-integration pane), run \`zeph cc\` from a plain shell\n` +
|
|
222
|
+
` pane instead — \`tmux new\` needs a real TTY to attach.`);
|
|
223
|
+
}
|
|
224
|
+
resolve(code ?? 0);
|
|
225
|
+
});
|
|
226
|
+
child.on('error', (err) => {
|
|
227
|
+
if (err.code === 'ENOENT') {
|
|
228
|
+
console.error(`zeph: '${cmd}' not found on PATH`);
|
|
229
|
+
resolve(127);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
console.error(`zeph: failed to spawn ${cmd}: ${err.message}`);
|
|
233
|
+
resolve(1);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
exports.handleAgentSession = handleAgentSession;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ZephOptions, NotifyPayload, NotifyResult, ListParams, ListResult, DismissOneResult, DismissAllResult, UploadRequestResult } from './types.js';
|
|
2
|
+
export declare class ZephHook {
|
|
3
|
+
private readonly apiKey;
|
|
4
|
+
private readonly baseUrl;
|
|
5
|
+
private readonly timeoutMs;
|
|
6
|
+
private cryptoInitialized;
|
|
7
|
+
constructor(options: ZephOptions);
|
|
8
|
+
private ensureCrypto;
|
|
9
|
+
notify(payload: NotifyPayload): Promise<NotifyResult>;
|
|
10
|
+
private notifyWithFile;
|
|
11
|
+
requestUpload(params: {
|
|
12
|
+
fileName: string;
|
|
13
|
+
fileType: string;
|
|
14
|
+
fileSize: number;
|
|
15
|
+
}): Promise<UploadRequestResult>;
|
|
16
|
+
uploadToS3(url: string, content: string | Buffer, contentType: string): Promise<void>;
|
|
17
|
+
list(params?: ListParams): Promise<ListResult>;
|
|
18
|
+
dismiss(pushId: string): Promise<DismissOneResult>;
|
|
19
|
+
dismissAll(): Promise<DismissAllResult>;
|
|
20
|
+
private request;
|
|
21
|
+
private parseError;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=zeph-hook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zeph-hook.d.ts","sourceRoot":"","sources":["../src/zeph-hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAY,gBAAgB,EAAE,gBAAgB,EAAoB,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAexL,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,OAAO,EAAE,WAAW;YASlB,YAAY;IAYpB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;YA6B7C,cAAc;IAsDtB,aAAa,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAK7G,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcrF,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAmB9C,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAKlD,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;YAK/B,OAAO;IAiCrB,OAAO,CAAC,UAAU;CASnB"}
|