@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
package/dist/login.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Credentials returned by a completed login. apiKey is guaranteed present
|
|
2
|
+
* (parseCallback rejects a missing key with 400). Shared by handleLogin and
|
|
3
|
+
* handleInstall via runLoginFlow. */
|
|
4
|
+
export type LoginFlowResult = {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
hookId?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
wsUrl?: string;
|
|
9
|
+
};
|
|
10
|
+
type CallbackResult = {
|
|
11
|
+
ok: true;
|
|
12
|
+
config: LoginFlowResult;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
status: number;
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
export declare const stripUndefined: <T extends Record<string, unknown>>(obj: T) => Partial<T>;
|
|
19
|
+
export declare const buildBridgeUrl: (webUrl: string, port: number, state: string, host: string) => string;
|
|
20
|
+
export declare const persistConfig: (next: LoginFlowResult) => void;
|
|
21
|
+
export declare const parseCallback: (reqUrl: string, expectedState: string) => CallbackResult;
|
|
22
|
+
export declare const resolveWebUrl: (raw: string | boolean | undefined) => string;
|
|
23
|
+
export declare const resolveTimeoutSec: (raw: string | boolean | undefined) => number;
|
|
24
|
+
/**
|
|
25
|
+
* Core login flow shared by `handleLogin` (CLI command) and `handleInstall`
|
|
26
|
+
* (auto-trigger on missing credentials). Returns the issued credentials, or
|
|
27
|
+
* null when the browser can't open (headless) or the callback never arrives.
|
|
28
|
+
* `deps.open` is injectable for tests; defaults to the real browser launcher.
|
|
29
|
+
*/
|
|
30
|
+
export declare const runLoginFlow: (opts: {
|
|
31
|
+
webUrl: string;
|
|
32
|
+
timeoutSec: number;
|
|
33
|
+
}, deps?: {
|
|
34
|
+
open?: (url: string) => boolean;
|
|
35
|
+
}) => Promise<LoginFlowResult | null>;
|
|
36
|
+
export declare const handleLogin: (args: Record<string, string | boolean>) => Promise<number>;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=login.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"AAUA;;sCAEsC;AACtC,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,cAAc,GACf;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAIlD,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,KAAG,OAAO,CAAC,CAAC,CAMnF,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,MAI1F,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,MAAM,eAAe,KAAG,IAGrD,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,EAAE,eAAe,MAAM,KAAG,cAmBrE,CAAC;AAgFF,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,GAAG,OAAO,GAAG,SAAS,KAAG,MACjB,CAAC;AAElD,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,GAAG,OAAO,GAAG,SAAS,KAAG,MAKrE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GACvB,MAAM;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,EAC5C,OAAM;IAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;CAAO,KAC7C,OAAO,CAAC,eAAe,GAAG,IAAI,CAmChC,CAAC;AAEF,eAAO,MAAM,WAAW,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAcxF,CAAC"}
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleLogin = exports.runLoginFlow = exports.resolveTimeoutSec = exports.resolveWebUrl = exports.parseCallback = exports.persistConfig = exports.buildBridgeUrl = exports.stripUndefined = void 0;
|
|
4
|
+
const node_http_1 = require("node:http");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const config_js_1 = require("./config.js");
|
|
9
|
+
const DEFAULT_WEB_URL = 'https://app.zeph.to';
|
|
10
|
+
const DEFAULT_TIMEOUT_SEC = 300;
|
|
11
|
+
// ── Pure helpers (unit-tested) ───────────────────────────────────
|
|
12
|
+
const stripUndefined = (obj) => {
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
15
|
+
if (value !== undefined)
|
|
16
|
+
out[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
};
|
|
20
|
+
exports.stripUndefined = stripUndefined;
|
|
21
|
+
const buildBridgeUrl = (webUrl, port, state, host) => {
|
|
22
|
+
const base = webUrl.replace(/\/+$/, '');
|
|
23
|
+
const params = new URLSearchParams({ port: String(port), state, host });
|
|
24
|
+
return `${base}/auth/cli-bridge?${params.toString()}`;
|
|
25
|
+
};
|
|
26
|
+
exports.buildBridgeUrl = buildBridgeUrl;
|
|
27
|
+
const persistConfig = (next) => {
|
|
28
|
+
const existing = (0, config_js_1.loadConfig)();
|
|
29
|
+
(0, config_js_1.saveConfig)({ ...existing, ...(0, exports.stripUndefined)(next) });
|
|
30
|
+
};
|
|
31
|
+
exports.persistConfig = persistConfig;
|
|
32
|
+
const parseCallback = (reqUrl, expectedState) => {
|
|
33
|
+
const url = new URL(reqUrl, 'http://127.0.0.1');
|
|
34
|
+
if (url.pathname !== '/cb')
|
|
35
|
+
return { ok: false, status: 404, reason: 'not found' };
|
|
36
|
+
const state = url.searchParams.get('state');
|
|
37
|
+
if (state !== expectedState)
|
|
38
|
+
return { ok: false, status: 403, reason: 'state mismatch' };
|
|
39
|
+
const apiKey = url.searchParams.get('key') ?? undefined;
|
|
40
|
+
if (!apiKey)
|
|
41
|
+
return { ok: false, status: 400, reason: 'missing key' };
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
config: {
|
|
45
|
+
apiKey,
|
|
46
|
+
hookId: url.searchParams.get('hook') ?? undefined,
|
|
47
|
+
baseUrl: url.searchParams.get('baseUrl') ?? undefined,
|
|
48
|
+
wsUrl: url.searchParams.get('wsUrl') ?? undefined,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
exports.parseCallback = parseCallback;
|
|
53
|
+
// ── Browser launch ───────────────────────────────────────────────
|
|
54
|
+
const browserCommand = () => {
|
|
55
|
+
if (process.platform === 'darwin')
|
|
56
|
+
return { cmd: 'open', prefixArgs: [] };
|
|
57
|
+
if (process.platform === 'win32')
|
|
58
|
+
return { cmd: 'cmd', prefixArgs: ['/c', 'start', ''] };
|
|
59
|
+
return { cmd: 'xdg-open', prefixArgs: [] };
|
|
60
|
+
};
|
|
61
|
+
const openBrowser = (url) => {
|
|
62
|
+
try {
|
|
63
|
+
const { cmd, prefixArgs } = browserCommand();
|
|
64
|
+
const child = (0, node_child_process_1.spawn)(cmd, [...prefixArgs, url], { stdio: 'ignore', detached: true });
|
|
65
|
+
child.on('error', () => { });
|
|
66
|
+
child.unref();
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
// ── Loopback server ──────────────────────────────────────────────
|
|
74
|
+
const DONE_HTML = '<!doctype html><meta charset="utf-8"><title>Zeph</title>' +
|
|
75
|
+
'<body style="font-family:system-ui;text-align:center;padding:3rem">' +
|
|
76
|
+
'<h2>Connected</h2><p>You can close this tab and return to the terminal.</p></body>';
|
|
77
|
+
const respond = (res, status, body) => {
|
|
78
|
+
res.writeHead(status, { 'content-type': 'text/html; charset=utf-8' });
|
|
79
|
+
res.end(body);
|
|
80
|
+
};
|
|
81
|
+
const startLoopbackServer = (state) => {
|
|
82
|
+
let settle;
|
|
83
|
+
let fail;
|
|
84
|
+
const done = new Promise((res, rej) => {
|
|
85
|
+
settle = res;
|
|
86
|
+
fail = rej;
|
|
87
|
+
});
|
|
88
|
+
const server = (0, node_http_1.createServer)((req, res) => {
|
|
89
|
+
const result = (0, exports.parseCallback)(req.url ?? '/', state);
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
respond(res, result.status, `<p>${result.reason}</p>`);
|
|
92
|
+
if (result.status === 403)
|
|
93
|
+
fail(new Error('state mismatch — refused'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
(0, exports.persistConfig)(result.config);
|
|
97
|
+
respond(res, 200, DONE_HTML);
|
|
98
|
+
settle(result.config);
|
|
99
|
+
});
|
|
100
|
+
return new Promise((resolveHandle, rejectHandle) => {
|
|
101
|
+
server.once('error', rejectHandle);
|
|
102
|
+
server.listen(0, '127.0.0.1', () => {
|
|
103
|
+
const { port } = server.address();
|
|
104
|
+
resolveHandle({ port, done, close: () => server.close() });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
// ── Command ──────────────────────────────────────────────────────
|
|
109
|
+
const headlessHint = (bridgeUrl) => {
|
|
110
|
+
console.error('\n Could not open a browser.');
|
|
111
|
+
console.error(' If this machine has no local browser, use the manual flow:');
|
|
112
|
+
console.error(' npx @zeph-to/cli install --key ak_… --hook hook_…');
|
|
113
|
+
console.error(' Or open this URL in a browser on THIS computer:');
|
|
114
|
+
console.error(` ${bridgeUrl}\n`);
|
|
115
|
+
};
|
|
116
|
+
const resolveWebUrl = (raw) => typeof raw === 'string' ? raw : DEFAULT_WEB_URL;
|
|
117
|
+
exports.resolveWebUrl = resolveWebUrl;
|
|
118
|
+
const resolveTimeoutSec = (raw) => {
|
|
119
|
+
if (typeof raw !== 'string')
|
|
120
|
+
return DEFAULT_TIMEOUT_SEC;
|
|
121
|
+
const n = Number(raw);
|
|
122
|
+
// Reject NaN / non-positive — a bad --timeout must not fire setTimeout instantly.
|
|
123
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_TIMEOUT_SEC;
|
|
124
|
+
};
|
|
125
|
+
exports.resolveTimeoutSec = resolveTimeoutSec;
|
|
126
|
+
/**
|
|
127
|
+
* Core login flow shared by `handleLogin` (CLI command) and `handleInstall`
|
|
128
|
+
* (auto-trigger on missing credentials). Returns the issued credentials, or
|
|
129
|
+
* null when the browser can't open (headless) or the callback never arrives.
|
|
130
|
+
* `deps.open` is injectable for tests; defaults to the real browser launcher.
|
|
131
|
+
*/
|
|
132
|
+
const runLoginFlow = async (opts, deps = {}) => {
|
|
133
|
+
const open = deps.open ?? openBrowser;
|
|
134
|
+
const state = (0, node_crypto_1.randomBytes)(16).toString('hex');
|
|
135
|
+
let handle;
|
|
136
|
+
try {
|
|
137
|
+
handle = await startLoopbackServer(state);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.error(` Error: could not start local server (${err instanceof Error ? err.message : 'unknown'})`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const bridgeUrl = (0, exports.buildBridgeUrl)(opts.webUrl, handle.port, state, (0, node_os_1.hostname)());
|
|
144
|
+
console.log(`\n Opening browser to sign in...\n ${bridgeUrl}\n`);
|
|
145
|
+
if (!open(bridgeUrl)) {
|
|
146
|
+
headlessHint(bridgeUrl);
|
|
147
|
+
handle.close();
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
let timer;
|
|
151
|
+
const timeout = new Promise((_, reject) => {
|
|
152
|
+
timer = setTimeout(() => reject(new Error('timed out waiting for browser')), opts.timeoutSec * 1000);
|
|
153
|
+
timer.unref();
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
return await Promise.race([handle.done, timeout]);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
if (timer)
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
handle.close();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
exports.runLoginFlow = runLoginFlow;
|
|
168
|
+
const handleLogin = async (args) => {
|
|
169
|
+
const webUrl = (0, exports.resolveWebUrl)(args['web-url']);
|
|
170
|
+
const timeoutSec = (0, exports.resolveTimeoutSec)(args.timeout);
|
|
171
|
+
const result = await (0, exports.runLoginFlow)({ webUrl, timeoutSec });
|
|
172
|
+
if (!result) {
|
|
173
|
+
console.error('\n Error: login did not complete\n');
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
console.log(` + Config saved. API key: ${result.apiKey.slice(0, 12)}…`);
|
|
177
|
+
if (result.hookId)
|
|
178
|
+
console.log(` + Hook: ${result.hookId}`);
|
|
179
|
+
console.log('\n Done! Restart your agents.\n');
|
|
180
|
+
return 0;
|
|
181
|
+
};
|
|
182
|
+
exports.handleLogin = handleLogin;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Cursor — written to ~/.cursor/rules/zeph.mdc (needs .mdc frontmatter). */
|
|
2
|
+
export declare const CURSOR_RULE: string;
|
|
3
|
+
/** Windsurf — appended into ~/.codeium/windsurf/memories/global_rules.md. */
|
|
4
|
+
export declare const WINDSURF_RULE: string;
|
|
5
|
+
/** Gemini CLI — appended into ~/.gemini/GEMINI.md. */
|
|
6
|
+
export declare const GEMINI_RULE: string;
|
|
7
|
+
/** Codex CLI — appended into ~/.codex/AGENTS.md. */
|
|
8
|
+
export declare const CODEX_RULE: string;
|
|
9
|
+
/** GitHub Copilot CLI — written to ~/.copilot/instructions/zeph.instructions.md. */
|
|
10
|
+
export declare const COPILOT_RULE: string;
|
|
11
|
+
/** Cline — written to ~/.cline/rules/zeph.md (no Stop hook). */
|
|
12
|
+
export declare const CLINE_RULE: string;
|
|
13
|
+
/** Aider — written to a standalone conventions file, loaded via .aider.conf.yml `read:`. */
|
|
14
|
+
export declare const AIDER_RULE: string;
|
|
15
|
+
export declare const CURSOR_HOOKS: string;
|
|
16
|
+
export declare const WINDSURF_HOOKS: string;
|
|
17
|
+
export declare const GEMINI_HOOKS: {
|
|
18
|
+
hooks: {
|
|
19
|
+
AfterAgent: {
|
|
20
|
+
matcher: string;
|
|
21
|
+
hooks: {
|
|
22
|
+
name: string;
|
|
23
|
+
type: string;
|
|
24
|
+
command: string;
|
|
25
|
+
}[];
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
hooksConfig: {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export declare const CODEX_HOOKS: string;
|
|
33
|
+
export declare const COPILOT_HOOKS: string;
|
|
34
|
+
export declare const ZEPH_MARK_START = "<!-- ZEPH:START \u2014 managed by @zeph-to/cli, do not edit between markers -->";
|
|
35
|
+
export declare const ZEPH_MARK_END = "<!-- ZEPH:END -->";
|
|
36
|
+
/**
|
|
37
|
+
* Return `existing` with the Zeph-managed block inserted or replaced.
|
|
38
|
+
* If the markers are already present, the content between them is
|
|
39
|
+
* swapped; otherwise the block is appended.
|
|
40
|
+
*/
|
|
41
|
+
export declare const upsertManagedBlock: (existing: string, rule: string) => string;
|
|
42
|
+
/** Strip the Zeph-managed block from a shared file (for uninstall). */
|
|
43
|
+
export declare const removeManagedBlock: (existing: string) => string;
|
|
44
|
+
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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,oFAA+E,CAAC;AAC5G,eAAO,MAAM,aAAa,sBAAsB,CAAC;AAQjD;;;;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"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
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.
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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/cli` 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/cli") 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) ──────────────────
|
|
170
|
+
exports.CURSOR_HOOKS = JSON.stringify({
|
|
171
|
+
version: 1,
|
|
172
|
+
hooks: {
|
|
173
|
+
stop: [{ command: NOTIFY_CMD }],
|
|
174
|
+
},
|
|
175
|
+
}, null, 2);
|
|
176
|
+
exports.WINDSURF_HOOKS = JSON.stringify({
|
|
177
|
+
hooks: {
|
|
178
|
+
post_cascade_response: [{
|
|
179
|
+
command: NOTIFY_CMD,
|
|
180
|
+
show_output: false,
|
|
181
|
+
}],
|
|
182
|
+
},
|
|
183
|
+
}, null, 2);
|
|
184
|
+
exports.GEMINI_HOOKS = {
|
|
185
|
+
hooks: {
|
|
186
|
+
AfterAgent: [{
|
|
187
|
+
matcher: '*',
|
|
188
|
+
hooks: [{
|
|
189
|
+
name: 'zeph-notify',
|
|
190
|
+
type: 'command',
|
|
191
|
+
command: NOTIFY_CMD,
|
|
192
|
+
}],
|
|
193
|
+
}],
|
|
194
|
+
},
|
|
195
|
+
hooksConfig: { enabled: true },
|
|
196
|
+
};
|
|
197
|
+
exports.CODEX_HOOKS = JSON.stringify({
|
|
198
|
+
version: 1,
|
|
199
|
+
hooks: {
|
|
200
|
+
Stop: [{
|
|
201
|
+
type: 'command',
|
|
202
|
+
bash: NOTIFY_CMD,
|
|
203
|
+
}],
|
|
204
|
+
},
|
|
205
|
+
}, null, 2);
|
|
206
|
+
exports.COPILOT_HOOKS = JSON.stringify({
|
|
207
|
+
version: 1,
|
|
208
|
+
hooks: {
|
|
209
|
+
sessionEnd: [{
|
|
210
|
+
type: 'command',
|
|
211
|
+
bash: NOTIFY_CMD,
|
|
212
|
+
timeoutSec: 10,
|
|
213
|
+
}],
|
|
214
|
+
},
|
|
215
|
+
}, null, 2);
|
|
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/cli, do not edit between markers -->';
|
|
223
|
+
exports.ZEPH_MARK_END = '<!-- ZEPH:END -->';
|
|
224
|
+
// Match the start marker by stable prefix, not its full text. Installs from
|
|
225
|
+
// older releases wrote `… managed by @zeph-to/hook-sdk …`; matching the prefix
|
|
226
|
+
// keeps upsert/uninstall working across the rename instead of orphaning their
|
|
227
|
+
// managed blocks.
|
|
228
|
+
const ZEPH_MARK_START_PREFIX = '<!-- ZEPH:START';
|
|
229
|
+
/**
|
|
230
|
+
* Return `existing` with the Zeph-managed block inserted or replaced.
|
|
231
|
+
* If the markers are already present, the content between them is
|
|
232
|
+
* swapped; otherwise the block is appended.
|
|
233
|
+
*/
|
|
234
|
+
const upsertManagedBlock = (existing, rule) => {
|
|
235
|
+
const block = `${exports.ZEPH_MARK_START}\n${rule}\n${exports.ZEPH_MARK_END}`;
|
|
236
|
+
const startIdx = existing.indexOf(ZEPH_MARK_START_PREFIX);
|
|
237
|
+
const endIdx = existing.indexOf(exports.ZEPH_MARK_END);
|
|
238
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
239
|
+
const before = existing.slice(0, startIdx).replace(/\n*$/, '');
|
|
240
|
+
const after = existing.slice(endIdx + exports.ZEPH_MARK_END.length).replace(/^\n*/, '');
|
|
241
|
+
return [before, block, after].filter(Boolean).join('\n\n') + '\n';
|
|
242
|
+
}
|
|
243
|
+
const base = existing.replace(/\n*$/, '');
|
|
244
|
+
return (base ? `${base}\n\n` : '') + block + '\n';
|
|
245
|
+
};
|
|
246
|
+
exports.upsertManagedBlock = upsertManagedBlock;
|
|
247
|
+
/** Strip the Zeph-managed block from a shared file (for uninstall). */
|
|
248
|
+
const removeManagedBlock = (existing) => {
|
|
249
|
+
const startIdx = existing.indexOf(ZEPH_MARK_START_PREFIX);
|
|
250
|
+
const endIdx = existing.indexOf(exports.ZEPH_MARK_END);
|
|
251
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx)
|
|
252
|
+
return existing;
|
|
253
|
+
const before = existing.slice(0, startIdx).replace(/\n*$/, '');
|
|
254
|
+
const after = existing.slice(endIdx + exports.ZEPH_MARK_END.length).replace(/^\n*/, '');
|
|
255
|
+
return [before, after].filter(Boolean).join('\n\n') + (before || after ? '\n' : '');
|
|
256
|
+
};
|
|
257
|
+
exports.removeManagedBlock = removeManagedBlock;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface ZephOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
timeout?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface NotifyPayload {
|
|
7
|
+
title?: string;
|
|
8
|
+
body?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
type?: 'note' | 'link' | 'file' | 'hook';
|
|
11
|
+
priority?: 'low' | 'normal' | 'high' | 'urgent';
|
|
12
|
+
targetDeviceId?: string;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface NotifyResult {
|
|
16
|
+
pushId: string;
|
|
17
|
+
fileKey?: string;
|
|
18
|
+
autoFile?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface UploadRequestResult {
|
|
21
|
+
fileId: string;
|
|
22
|
+
fileKey: string;
|
|
23
|
+
uploadUrl: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ListParams {
|
|
26
|
+
limit?: number;
|
|
27
|
+
type?: 'note' | 'link' | 'file' | 'clipboard' | 'hook';
|
|
28
|
+
}
|
|
29
|
+
export interface PushItem {
|
|
30
|
+
pushId: string;
|
|
31
|
+
type: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
body?: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ListResult {
|
|
37
|
+
pushes: PushItem[];
|
|
38
|
+
count: number;
|
|
39
|
+
hasMore: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface DismissOneResult {
|
|
42
|
+
dismissed: true;
|
|
43
|
+
}
|
|
44
|
+
export interface DismissAllResult {
|
|
45
|
+
dismissed: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ApiErrorResponse {
|
|
48
|
+
error: {
|
|
49
|
+
code: string;
|
|
50
|
+
message: string;
|
|
51
|
+
status: number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACzC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;CACxD;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH"}
|
package/dist/types.js
ADDED
|
@@ -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"}
|