@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,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;
@@ -0,0 +1,2 @@
1
+ export declare const handleVerify: (args: Record<string, string | boolean>) => Promise<number>;
2
+ //# sourceMappingURL=verify.d.ts.map
@@ -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"}
@@ -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"}