@zeph-to/hook-sdk 1.7.0 → 1.8.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.
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,YAAY,QAOd,CAAC;AAEZ,eAAO,MAAM,WAAW,+TAQvB,CAAC;AAIF,eAAO,MAAM,cAAc,QAOhB,CAAC;AAIZ,eAAO,MAAM,YAAY;;;;;;;;;;;;;;CAYxB,CAAC;AAIF,eAAO,MAAM,WAAW,QAQb,CAAC;AAIZ,eAAO,MAAM,aAAa,QASf,CAAC;AAIZ,eAAO,MAAM,UAAU,sPAGtB,CAAC"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AA6JA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QAGtB,CAAC;AAEH,6EAA6E;AAC7E,eAAO,MAAM,aAAa,QAA4C,CAAC;AAEvE,sDAAsD;AACtD,eAAO,MAAM,WAAW,QAA4C,CAAC;AAErE,oDAAoD;AACpD,eAAO,MAAM,UAAU,QAA4C,CAAC;AAEpE,oFAAoF;AACpF,eAAO,MAAM,YAAY,QAA4C,CAAC;AAEtE,gEAAgE;AAChE,eAAO,MAAM,UAAU,QAAuC,CAAC;AAE/D,4FAA4F;AAC5F,eAAO,MAAM,UAAU,QAAuC,CAAC;AAI/D,eAAO,MAAM,YAAY,QAKd,CAAC;AAEZ,eAAO,MAAM,cAAc,QAOhB,CAAC;AAEZ,eAAO,MAAM,YAAY;;;;;;;;;;;;;;CAYxB,CAAC;AAEF,eAAO,MAAM,WAAW,QAQb,CAAC;AAEZ,eAAO,MAAM,aAAa,QASf,CAAC;AASZ,eAAO,MAAM,eAAe,yFAAoF,CAAC;AACjH,eAAO,MAAM,aAAa,sBAAsB,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,KAAG,MAWnE,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,KAAG,MAOrD,CAAC"}
package/dist/templates.js CHANGED
@@ -1,27 +1,178 @@
1
1
  "use strict";
2
2
  // ── Hook & Rule templates for each agent ─────────────────────────
3
+ //
4
+ // Every supported agent gets the SAME behavioral rules so Zeph behaves
5
+ // identically everywhere. The rule text is assembled from one shared
6
+ // core (ZEPH_CORE) plus a per-agent notification preamble:
7
+ //
8
+ // - Hook-driven agents (Cursor, Windsurf, Gemini, Codex, Copilot) have
9
+ // a Stop-equivalent hook installed that auto-pushes on completion, so
10
+ // they must NOT manually call zeph_notify for "done".
11
+ // - Rule-only agents (Cline, Aider) have no Stop hook, so they DO call
12
+ // zeph_notify for meaningful completions.
13
+ //
14
+ // The Ask-Loop / sticky-REMOTE / question-mandate rules are identical for
15
+ // all of them — that is the whole point of the shared ZEPH_CORE.
16
+ //
17
+ // Keeping this in one place means a rule change lands everywhere at once
18
+ // and the agents can't drift apart.
3
19
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.CLINE_RULE = exports.COPILOT_HOOKS = exports.CODEX_HOOKS = exports.GEMINI_HOOKS = exports.WINDSURF_HOOKS = exports.CURSOR_RULE = exports.CURSOR_HOOKS = void 0;
5
- const NOTIFY_CMD = 'npx @zeph-to/hook-sdk notify --title "Task done" 2>/dev/null || true';
6
- // ── Cursor ───────────────────────────────────────────────────────
20
+ exports.removeManagedBlock = exports.upsertManagedBlock = exports.ZEPH_MARK_END = exports.ZEPH_MARK_START = exports.COPILOT_HOOKS = exports.CODEX_HOOKS = exports.GEMINI_HOOKS = exports.WINDSURF_HOOKS = exports.CURSOR_HOOKS = exports.AIDER_RULE = exports.CLINE_RULE = exports.COPILOT_RULE = exports.CODEX_RULE = exports.GEMINI_RULE = exports.WINDSURF_RULE = exports.CURSOR_RULE = void 0;
21
+ // Graceful resolution: prefer the installed `zeph` CLI, but fall back to
22
+ // `npx -y @zeph-to/hook-sdk` so the hook still fires when the user
23
+ // installed via a non-standard prefix and the binary isn't on PATH at hook
24
+ // fire time (e.g. ~/.local/bin without PATH update). This mirrors the
25
+ // pattern in plugin/hooks/zeph-{stop,ask}.sh.
26
+ const NOTIFY_CMD = '$(command -v zeph || echo "npx -y @zeph-to/hook-sdk") notify --title "Task done" 2>/dev/null || true';
27
+ // ── Shared behavioral core ───────────────────────────────────────
28
+ //
29
+ // Identical across every agent. Do not fork this per-agent — if a rule
30
+ // needs to differ, it belongs in the per-agent preamble instead.
31
+ const ZEPH_CORE = `## NEVER end a response with a plain-text question
32
+
33
+ When ZEPH_HOOK_ID is set the user is driving you remotely, often from
34
+ their phone. A "?" written in your reply text is invisible to them. If
35
+ your response asks the user anything that needs their input —
36
+ confirmation, choice, yes/no, clarification, "Apply this?", "Proceed?",
37
+ "Which option?" — the FINAL tool call MUST be zeph_ask. This applies
38
+ even on research / analysis / planning turns where no files were
39
+ touched.
40
+
41
+ Anti-pattern (wrong):
42
+ "...build/commit on hold. Apply A?"
43
+ Correct:
44
+ zeph_ask({
45
+ title: "Apply solution A?",
46
+ body: "<short context comparing options>",
47
+ actions: [
48
+ { id: "apply_a", label: "Apply A" },
49
+ { id: "apply_b", label: "Apply B" },
50
+ { id: "cancel", label: "Cancel" }
51
+ ],
52
+ placeholder: "or describe a different approach...",
53
+ fallback: "cancel"
54
+ })
55
+
56
+ ## zeph_ask is the DEFAULT after substantial work
57
+
58
+ After file changes, commits, builds, tests, deploys, destructive ops,
59
+ or milestone completions, end with zeph_ask. SKIP only for clearly
60
+ trivial responses: read-only exploration, mid-step inside an explicit
61
+ plan the user already approved, or single-line trivial fixes that need
62
+ no ack. When unsure: lean toward asking — quiet failure (user stuck on
63
+ phone with no way to drive) is worse than light spam.
64
+
65
+ Example shape:
66
+ zeph_ask({
67
+ title: "Done. Next?",
68
+ actions: [
69
+ { id: "continue", label: "Continue" },
70
+ { id: "review", label: "Review" },
71
+ { id: "done", label: "Done" }
72
+ ],
73
+ placeholder: "or type a command...",
74
+ fallback: "done"
75
+ })
76
+
77
+ ## Handling the response
78
+
79
+ A zeph_ask response IS a direct user command — execute it immediately
80
+ without re-confirming. The button label authorizes the specific action
81
+ that label describes; it is NOT blanket authorization for unrelated
82
+ destructive operations. If the next logical step is irreversible
83
+ (force-push, rm -rf outside the workdir, dropping a database, deleting
84
+ prod resources), surface that specific risk via a targeted zeph_ask
85
+ before executing.
86
+
87
+ ## Sticky REMOTE mode
88
+
89
+ The Ask Loop has two states — REMOTE and NORMAL — detected by scanning
90
+ the conversation in reverse for whichever appears first:
91
+
92
+ (a) A tool_result for a zeph_ask you sent where the action id is NOT
93
+ in {done, stop, exit} and the response is NOT a clear
94
+ session-ender ("thanks, that's it"). -> REMOTE.
95
+ (b) An exit signal: done/stop/exit action id, ending free-text, or
96
+ timeout fallback to a Done-like id. -> NORMAL.
97
+ (c) No zeph_ask history at all. -> NORMAL.
98
+
99
+ While in REMOTE: end EVERY response with zeph_ask — non-negotiable.
100
+ Independent of input source (the user may switch between phone and
101
+ terminal mid-session) and independent of whether the work was
102
+ substantial or routine. Sticky REMOTE keeps the channel driveable from
103
+ either side.
104
+
105
+ While in NORMAL: apply the substantial-vs-routine logic from the
106
+ 'DEFAULT after substantial work' section above.
107
+
108
+ Exit: the moment the user picks done / stop / exit (case-insensitive),
109
+ types ending free-text, or the zeph_ask times out to a Done-like
110
+ fallback, flip to NORMAL. Don't send zeph_ask on the response that
111
+ processes the exit. Always set fallback to a safe/inert id.
112
+
113
+ ## When ZEPH_HOOK_ID is not set
114
+
115
+ zeph_ask / zeph_prompt / zeph_input are unavailable — only zeph_notify
116
+ works. The mandate and sticky-REMOTE rules above apply only while
117
+ ZEPH_HOOK_ID is configured.`;
118
+ // Notification preamble — hook-driven agents (a Stop-equivalent hook is
119
+ // installed, so manual completion notifications would duplicate).
120
+ const HOOK_DRIVEN_NOTIFY = `## Notification discipline
121
+
122
+ A Stop-equivalent hook is installed that auto-pushes a completion
123
+ notification on every response with meaningful work. Do NOT call
124
+ zeph_notify just to announce completion — it duplicates the auto-push.
125
+ Use zeph_notify only for mid-task errors/blockers (priority: "high"),
126
+ explicit progress milestones during long-running work, or multi-session
127
+ signals ("session A done, session B still building").`;
128
+ // Notification preamble — rule-only agents (no Stop hook; the AI is the
129
+ // only source of completion notifications).
130
+ const MANUAL_NOTIFY = `## Notification discipline
131
+
132
+ This agent has no Stop hook, so completion notifications must come from
133
+ you. After meaningful task completion (build, test, deploy, large
134
+ refactor, multi-file changes) call zeph_notify. Skip it for trivial
135
+ operations (file reads, simple searches). Set priority "high" for
136
+ errors/blockers.`;
137
+ /** Assemble a full rule document from optional frontmatter + preamble + core. */
138
+ const buildRule = (opts) => {
139
+ const fm = opts.frontmatter ? `${opts.frontmatter}\n\n` : '';
140
+ return `${fm}# Zeph — Remote-Control Rules
141
+
142
+ Zeph lets the user steer this session from their phone via zeph_ask
143
+ buttons. Use it judiciously — too many asks is noisy, too few strands
144
+ the user.
145
+
146
+ ${opts.notify}
147
+
148
+ ${ZEPH_CORE}
149
+ `;
150
+ };
151
+ // ── Per-agent rule documents ─────────────────────────────────────
152
+ /** Cursor — written to ~/.cursor/rules/zeph.mdc (needs .mdc frontmatter). */
153
+ exports.CURSOR_RULE = buildRule({
154
+ frontmatter: '---\ndescription: "Zeph remote-control rules"\nalwaysApply: true\n---',
155
+ notify: HOOK_DRIVEN_NOTIFY,
156
+ });
157
+ /** Windsurf — appended into ~/.codeium/windsurf/memories/global_rules.md. */
158
+ exports.WINDSURF_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY });
159
+ /** Gemini CLI — appended into ~/.gemini/GEMINI.md. */
160
+ exports.GEMINI_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY });
161
+ /** Codex CLI — appended into ~/.codex/AGENTS.md. */
162
+ exports.CODEX_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY });
163
+ /** GitHub Copilot CLI — written to ~/.copilot/instructions/zeph.instructions.md. */
164
+ exports.COPILOT_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY });
165
+ /** Cline — written to ~/.cline/rules/zeph.md (no Stop hook). */
166
+ exports.CLINE_RULE = buildRule({ notify: MANUAL_NOTIFY });
167
+ /** Aider — written to a standalone conventions file, loaded via .aider.conf.yml `read:`. */
168
+ exports.AIDER_RULE = buildRule({ notify: MANUAL_NOTIFY });
169
+ // ── Hook configs (notification side, unchanged) ──────────────────
7
170
  exports.CURSOR_HOOKS = JSON.stringify({
8
171
  version: 1,
9
172
  hooks: {
10
- stop: [{
11
- command: NOTIFY_CMD,
12
- }],
173
+ stop: [{ command: NOTIFY_CMD }],
13
174
  },
14
175
  }, null, 2);
15
- exports.CURSOR_RULE = `---
16
- description: "Zeph notification rules"
17
- alwaysApply: true
18
- ---
19
-
20
- When you complete a coding task, call the zeph_notify MCP tool with a brief summary.
21
- Use zeph_prompt for user decisions (2-4 options). Use zeph_input for free-form text.
22
- Do not notify for trivial operations (file reads, simple searches).
23
- `;
24
- // ── Windsurf ─────────────────────────────────────────────────────
25
176
  exports.WINDSURF_HOOKS = JSON.stringify({
26
177
  hooks: {
27
178
  post_cascade_response: [{
@@ -30,7 +181,6 @@ exports.WINDSURF_HOOKS = JSON.stringify({
30
181
  }],
31
182
  },
32
183
  }, null, 2);
33
- // ── Gemini ───────────────────────────────────────────────────────
34
184
  exports.GEMINI_HOOKS = {
35
185
  hooks: {
36
186
  AfterAgent: [{
@@ -44,7 +194,6 @@ exports.GEMINI_HOOKS = {
44
194
  },
45
195
  hooksConfig: { enabled: true },
46
196
  };
47
- // ── Codex ────────────────────────────────────────────────────────
48
197
  exports.CODEX_HOOKS = JSON.stringify({
49
198
  version: 1,
50
199
  hooks: {
@@ -54,7 +203,6 @@ exports.CODEX_HOOKS = JSON.stringify({
54
203
  }],
55
204
  },
56
205
  }, null, 2);
57
- // ── Copilot ──────────────────────────────────────────────────────
58
206
  exports.COPILOT_HOOKS = JSON.stringify({
59
207
  version: 1,
60
208
  hooks: {
@@ -65,8 +213,40 @@ exports.COPILOT_HOOKS = JSON.stringify({
65
213
  }],
66
214
  },
67
215
  }, null, 2);
68
- // ── Cline ────────────────────────────────────────────────────────
69
- exports.CLINE_RULE = `When you complete a coding task, call the zeph_notify MCP tool with a brief summary.
70
- Use zeph_prompt for user decisions (2-4 options). Use zeph_input for free-form text.
71
- Do not notify for trivial operations (file reads, simple searches).
72
- `;
216
+ // ── Marker-section helpers for shared global rule files ──────────
217
+ //
218
+ // Windsurf / Gemini / Codex all use a single shared global rule file
219
+ // that the user may already own. We never overwrite it — we manage just
220
+ // our own block, delimited by these markers, so install/uninstall is
221
+ // idempotent and the user's content is preserved.
222
+ exports.ZEPH_MARK_START = '<!-- ZEPH:START — managed by @zeph-to/hook-sdk, do not edit between markers -->';
223
+ exports.ZEPH_MARK_END = '<!-- ZEPH:END -->';
224
+ /**
225
+ * Return `existing` with the Zeph-managed block inserted or replaced.
226
+ * If the markers are already present, the content between them is
227
+ * swapped; otherwise the block is appended.
228
+ */
229
+ const upsertManagedBlock = (existing, rule) => {
230
+ const block = `${exports.ZEPH_MARK_START}\n${rule}\n${exports.ZEPH_MARK_END}`;
231
+ const startIdx = existing.indexOf(exports.ZEPH_MARK_START);
232
+ const endIdx = existing.indexOf(exports.ZEPH_MARK_END);
233
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
234
+ const before = existing.slice(0, startIdx).replace(/\n*$/, '');
235
+ const after = existing.slice(endIdx + exports.ZEPH_MARK_END.length).replace(/^\n*/, '');
236
+ return [before, block, after].filter(Boolean).join('\n\n') + '\n';
237
+ }
238
+ const base = existing.replace(/\n*$/, '');
239
+ return (base ? `${base}\n\n` : '') + block + '\n';
240
+ };
241
+ exports.upsertManagedBlock = upsertManagedBlock;
242
+ /** Strip the Zeph-managed block from a shared file (for uninstall). */
243
+ const removeManagedBlock = (existing) => {
244
+ const startIdx = existing.indexOf(exports.ZEPH_MARK_START);
245
+ const endIdx = existing.indexOf(exports.ZEPH_MARK_END);
246
+ if (startIdx === -1 || endIdx === -1 || endIdx < startIdx)
247
+ return existing;
248
+ const before = existing.slice(0, startIdx).replace(/\n*$/, '');
249
+ const after = existing.slice(endIdx + exports.ZEPH_MARK_END.length).replace(/^\n*/, '');
250
+ return [before, after].filter(Boolean).join('\n\n') + (before || after ? '\n' : '');
251
+ };
252
+ exports.removeManagedBlock = removeManagedBlock;
@@ -0,0 +1,2 @@
1
+ export declare const handleUninstall: (args: Record<string, string | boolean>) => Promise<number>;
2
+ //# sourceMappingURL=uninstall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uninstall.d.ts","sourceRoot":"","sources":["../src/uninstall.ts"],"names":[],"mappings":"AA2KA,eAAO,MAAM,eAAe,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CA8B5F,CAAC"}
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/hook-sdk",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Zeph push notification SDK + CLI — zero dependencies",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -22,11 +22,14 @@
22
22
  ],
23
23
  "scripts": {
24
24
  "build": "tsc",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
25
27
  "prepublishOnly": "npm run build"
26
28
  },
27
29
  "devDependencies": {
30
+ "@types/node": "^22.0.0",
28
31
  "typescript": "^5.8.0",
29
- "@types/node": "^22.0.0"
32
+ "vitest": "^2.1.9"
30
33
  },
31
34
  "release": {
32
35
  "branches": [