@zeph-to/cli 1.13.2 → 1.14.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/dist/cli.js +41 -25
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -1
- package/dist/gate.d.ts +36 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/listener.d.ts +21 -13
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +20 -92
- package/dist/remote-agents.d.ts +55 -0
- package/dist/remote-agents.d.ts.map +1 -0
- package/dist/remote-agents.js +115 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +27 -150
- package/dist/wrapper.d.ts +2 -1
- package/dist/wrapper.d.ts.map +1 -1
- package/dist/wrapper.js +4 -5
- package/dist/zeph-core.generated.d.ts +7 -0
- package/dist/zeph-core.generated.d.ts.map +1 -0
- package/dist/zeph-core.generated.js +14 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
const fs_1 = require("fs");
|
|
5
4
|
const child_process_1 = require("child_process");
|
|
6
5
|
const zeph_hook_js_1 = require("./zeph-hook.js");
|
|
7
6
|
const errors_js_1 = require("./errors.js");
|
|
@@ -13,21 +12,10 @@ const check_update_js_1 = require("./check-update.js");
|
|
|
13
12
|
const wrapper_js_1 = require("./wrapper.js");
|
|
14
13
|
const listener_js_1 = require("./listener.js");
|
|
15
14
|
const config_js_1 = require("./config.js");
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const isMuted = () => {
|
|
19
|
-
try {
|
|
20
|
-
const dir = detectProjectDir();
|
|
21
|
-
const raw = (0, child_process_1.execFileSync)('cksum', { input: dir, encoding: 'utf-8' });
|
|
22
|
-
const hash = raw.split(' ')[0];
|
|
23
|
-
return (0, fs_1.existsSync)(`/tmp/zeph-muted-${hash}`);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
15
|
+
const gate_js_1 = require("./gate.js");
|
|
16
|
+
const remote_agents_js_1 = require("./remote-agents.js");
|
|
29
17
|
const detectBranchAndProject = () => {
|
|
30
|
-
const dir = detectProjectDir();
|
|
18
|
+
const dir = (0, config_js_1.detectProjectDir)();
|
|
31
19
|
const project = dir.split('/').filter(Boolean).pop() ?? 'project';
|
|
32
20
|
let branch;
|
|
33
21
|
try {
|
|
@@ -67,6 +55,8 @@ const parseArgs = (argv) => {
|
|
|
67
55
|
return result;
|
|
68
56
|
};
|
|
69
57
|
// ── Output ──────────────────────────────────────────────────────
|
|
58
|
+
/** One usage line per registered remote agent — generated so help text can't drift from the table. */
|
|
59
|
+
const usageAgentLines = () => remote_agents_js_1.REMOTE_AGENTS.map((a) => ` ${`${a.subcommands[0]} [args…]`.padEnd(15)} Run '${a.binary}' in a named tmux session ('zeph-<project>')`).join('\n');
|
|
70
60
|
const printUsage = () => {
|
|
71
61
|
console.log(`Usage: zeph <command> [options]
|
|
72
62
|
|
|
@@ -80,9 +70,7 @@ Commands:
|
|
|
80
70
|
list List recent push notifications
|
|
81
71
|
dismiss <id> Dismiss a push notification (or --all)
|
|
82
72
|
test Send a test notification to verify setup
|
|
83
|
-
|
|
84
|
-
codex [args…] Run 'codex' in a named tmux session
|
|
85
|
-
gemini [args…] Run 'gemini' in a named tmux session
|
|
73
|
+
${usageAgentLines()}
|
|
86
74
|
(auto-suffixed -2/-3/… when another zeph cc is already
|
|
87
75
|
attached to the default name; any args after the
|
|
88
76
|
subcommand are forwarded verbatim, e.g.
|
|
@@ -99,6 +87,11 @@ Notify options:
|
|
|
99
87
|
--priority <p> Priority (low|normal|high|urgent) [default: normal]
|
|
100
88
|
--device <id> Target device ID
|
|
101
89
|
--session <id> AI session ID (or set ZEPH_SESSION_ID env)
|
|
90
|
+
--auto Apply the push gate before sending (honors the
|
|
91
|
+
/zeph-quiet | /zeph-loud dial; silent exit when gated)
|
|
92
|
+
--marker <m> Push Signal marker for --auto (skip|push|high)
|
|
93
|
+
--tools <n> Turn tool count for --auto [default: assume real work]
|
|
94
|
+
--nonreadonly <n> Non-read-only tool count for --auto
|
|
102
95
|
|
|
103
96
|
List options:
|
|
104
97
|
--limit <n> Number of pushes (1-20, default 5)
|
|
@@ -166,10 +159,33 @@ const createHook = (args) => {
|
|
|
166
159
|
...(baseUrl && { baseUrl }),
|
|
167
160
|
});
|
|
168
161
|
};
|
|
162
|
+
/** Parse a gate count flag; garbage input falls back to the default (never accidentally silences). */
|
|
163
|
+
const gateCount = (raw, fallback) => {
|
|
164
|
+
const n = typeof raw === 'string' ? Number(raw) : NaN;
|
|
165
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
166
|
+
};
|
|
169
167
|
const handleNotify = async (args) => {
|
|
170
168
|
const isJson = args.json === true;
|
|
171
|
-
|
|
169
|
+
const projectDir = (0, config_js_1.detectProjectDir)();
|
|
170
|
+
if ((0, gate_js_1.isMuted)(projectDir))
|
|
172
171
|
return 0;
|
|
172
|
+
// --auto: apply the shared push-gate before sending. Inputs default to
|
|
173
|
+
// GATE_DEFAULTS ("assume real work") so dumb hooks keep their historical
|
|
174
|
+
// always-push behavior in normal mode, while the /zeph-quiet | /zeph-loud
|
|
175
|
+
// dial now works for every hook-driven agent. Gated-out → silent success.
|
|
176
|
+
if (args.auto === true) {
|
|
177
|
+
const verdict = (0, gate_js_1.decidePush)({
|
|
178
|
+
toolCount: gateCount(args.tools, gate_js_1.GATE_DEFAULTS.toolCount),
|
|
179
|
+
nonReadonlyCount: gateCount(args.nonreadonly, gate_js_1.GATE_DEFAULTS.nonReadonlyCount),
|
|
180
|
+
alreadyAsked: gate_js_1.GATE_DEFAULTS.alreadyAsked,
|
|
181
|
+
marker: (0, gate_js_1.normalizeMarker)(typeof args.marker === 'string' ? args.marker : undefined),
|
|
182
|
+
pushMode: (0, gate_js_1.readPushMode)(projectDir),
|
|
183
|
+
});
|
|
184
|
+
if (!verdict.push)
|
|
185
|
+
return 0;
|
|
186
|
+
if (verdict.priority === 'high' && !args.priority)
|
|
187
|
+
args.priority = 'high';
|
|
188
|
+
}
|
|
173
189
|
const hook = createHook(args);
|
|
174
190
|
if (!hook)
|
|
175
191
|
return 3;
|
|
@@ -337,6 +353,12 @@ const main = async () => {
|
|
|
337
353
|
printUsage();
|
|
338
354
|
return 0;
|
|
339
355
|
}
|
|
356
|
+
// Remote-control subcommands (`zeph cc` / `zeph codex` / …) come from the
|
|
357
|
+
// registry — one table row per agent, no hardcoded cases. Pass the typed
|
|
358
|
+
// command token to collectPassthrough (aliases map to the same agent).
|
|
359
|
+
const remote = (0, remote_agents_js_1.findAgentBySubcommand)(command);
|
|
360
|
+
if (remote)
|
|
361
|
+
return (0, wrapper_js_1.handleAgentSession)(remote, collectPassthrough(process.argv, command));
|
|
340
362
|
switch (command) {
|
|
341
363
|
case 'install':
|
|
342
364
|
case 'setup':
|
|
@@ -357,12 +379,6 @@ const main = async () => {
|
|
|
357
379
|
return handleDismiss(args);
|
|
358
380
|
case 'test':
|
|
359
381
|
return handleTest(args);
|
|
360
|
-
case 'cc':
|
|
361
|
-
return (0, wrapper_js_1.handleAgentSession)('claude', collectPassthrough(process.argv, 'cc'));
|
|
362
|
-
case 'codex':
|
|
363
|
-
return (0, wrapper_js_1.handleAgentSession)('codex', collectPassthrough(process.argv, 'codex'));
|
|
364
|
-
case 'gemini':
|
|
365
|
-
return (0, wrapper_js_1.handleAgentSession)('gemini', collectPassthrough(process.argv, 'gemini'));
|
|
366
382
|
case 'listener':
|
|
367
383
|
return (0, listener_js_1.handleListener)(args);
|
|
368
384
|
default:
|
package/dist/config.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export interface ZephConfig {
|
|
|
8
8
|
deviceId?: string;
|
|
9
9
|
}
|
|
10
10
|
export declare const resolvedEnv: (key: string) => string | undefined;
|
|
11
|
+
export declare const PROJECT_DIR_ENV_VARS: readonly ["CLAUDE_PROJECT_DIR", "CURSOR_PROJECT_DIR", "WINDSURF_PROJECT_DIR"];
|
|
12
|
+
/** First set project-dir env (unresolved `${VAR}` placeholders ignored), else cwd. */
|
|
13
|
+
export declare const detectProjectDir: () => string;
|
|
11
14
|
export declare const loadConfig: () => ZephConfig;
|
|
12
15
|
export declare const saveConfig: (config: ZephConfig) => void;
|
|
13
16
|
export declare const VERSION: string;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,QAA2B,CAAC;AACnD,eAAO,MAAM,WAAW,QAAkC,CAAC;AAE3D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,SAGlD,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,UAM7B,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,QAAQ,UAAU,KAAG,IAG/C,CAAC;AAEF,eAAO,MAAM,OAAO,QAOhB,CAAC"}
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,QAA2B,CAAC;AACnD,eAAO,MAAM,WAAW,QAAkC,CAAC;AAE3D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,SAGlD,CAAC;AAMF,eAAO,MAAM,oBAAoB,+EAAgF,CAAC;AAElH,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,QAAO,MAMnC,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,UAM7B,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,QAAQ,UAAU,KAAG,IAG/C,CAAC;AAEF,eAAO,MAAM,OAAO,QAOhB,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VERSION = exports.saveConfig = exports.loadConfig = exports.resolvedEnv = exports.CONFIG_FILE = exports.CONFIG_DIR = void 0;
|
|
3
|
+
exports.VERSION = exports.saveConfig = exports.loadConfig = exports.detectProjectDir = exports.PROJECT_DIR_ENV_VARS = exports.resolvedEnv = exports.CONFIG_FILE = exports.CONFIG_DIR = void 0;
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
const path_1 = require("path");
|
|
@@ -11,6 +11,21 @@ const resolvedEnv = (key) => {
|
|
|
11
11
|
return val && !val.startsWith('${') ? val : undefined;
|
|
12
12
|
};
|
|
13
13
|
exports.resolvedEnv = resolvedEnv;
|
|
14
|
+
// Per-agent project-dir env vars, in precedence order. Deliberately NOT part
|
|
15
|
+
// of the remote-agent registry: Cursor/Windsurf carry project-dir envs but
|
|
16
|
+
// are not remote-controllable via tmux — the two tables have different
|
|
17
|
+
// membership.
|
|
18
|
+
exports.PROJECT_DIR_ENV_VARS = ['CLAUDE_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'WINDSURF_PROJECT_DIR'];
|
|
19
|
+
/** First set project-dir env (unresolved `${VAR}` placeholders ignored), else cwd. */
|
|
20
|
+
const detectProjectDir = () => {
|
|
21
|
+
for (const key of exports.PROJECT_DIR_ENV_VARS) {
|
|
22
|
+
const val = (0, exports.resolvedEnv)(key);
|
|
23
|
+
if (val)
|
|
24
|
+
return val;
|
|
25
|
+
}
|
|
26
|
+
return process.cwd();
|
|
27
|
+
};
|
|
28
|
+
exports.detectProjectDir = detectProjectDir;
|
|
14
29
|
const loadConfig = () => {
|
|
15
30
|
try {
|
|
16
31
|
return JSON.parse((0, fs_1.readFileSync)(exports.CONFIG_FILE, 'utf-8'));
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type GateMarker = 'skip' | 'push' | 'high' | 'none';
|
|
2
|
+
export type GatePushMode = 'quiet' | 'loud' | 'normal';
|
|
3
|
+
export interface GateInput {
|
|
4
|
+
/** Total tool_use blocks this turn. */
|
|
5
|
+
toolCount: number;
|
|
6
|
+
/** Tools that are NOT read-only (Read/Grep/Glob). */
|
|
7
|
+
nonReadonlyCount: number;
|
|
8
|
+
/** A zeph_ask/zeph_prompt already notified this turn. */
|
|
9
|
+
alreadyAsked: boolean;
|
|
10
|
+
marker: GateMarker;
|
|
11
|
+
pushMode: GatePushMode;
|
|
12
|
+
}
|
|
13
|
+
export interface GateVerdict {
|
|
14
|
+
push: boolean;
|
|
15
|
+
priority: 'high' | 'normal';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* "Assume real work" defaults for hooks that can't supply turn facts
|
|
19
|
+
* (most non-Claude agents pass no counts): in normal mode the push still
|
|
20
|
+
* fires — preserving the historical always-push behavior of the dumb
|
|
21
|
+
* hooks — while quiet/loud now work everywhere.
|
|
22
|
+
*/
|
|
23
|
+
export declare const GATE_DEFAULTS: {
|
|
24
|
+
readonly toolCount: 2;
|
|
25
|
+
readonly nonReadonlyCount: 1;
|
|
26
|
+
readonly alreadyAsked: false;
|
|
27
|
+
};
|
|
28
|
+
export declare const normalizeMarker: (raw: string | undefined) => GateMarker;
|
|
29
|
+
export declare const normalizePushMode: (raw: string | undefined) => GatePushMode;
|
|
30
|
+
export declare const decidePush: (input: GateInput) => GateVerdict;
|
|
31
|
+
export declare const projectHash: (dir: string) => string | null;
|
|
32
|
+
/** True when the user ran /zeph-mute for this project. */
|
|
33
|
+
export declare const isMuted: (dir: string) => boolean;
|
|
34
|
+
/** The user's session push-mode dial (/zeph-quiet | /zeph-loud), default normal. */
|
|
35
|
+
export declare const readPushMode: (dir: string) => GatePushMode;
|
|
36
|
+
//# sourceMappingURL=gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC7B;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;;CAIhB,CAAC;AAEX,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,UACS,CAAC;AAEpE,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,YACR,CAAC;AAErD,eAAO,MAAM,UAAU,GAAI,OAAO,SAAS,KAAG,WAW7C,CAAC;AAQF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAOlD,CAAC;AAEF,0DAA0D;AAC1D,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,OAGrC,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,KAAG,YAQ1C,CAAC"}
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readPushMode = exports.isMuted = exports.projectHash = exports.decidePush = exports.normalizePushMode = exports.normalizeMarker = exports.GATE_DEFAULTS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Push-gate decision — the portable half of the Zeph Stop-hook logic.
|
|
6
|
+
*
|
|
7
|
+
* `decidePush` is the TS twin of `plugin/hooks/gate.sh` (zeph-to/plugin).
|
|
8
|
+
* Both implementations are locked to the same semantics by the shared
|
|
9
|
+
* vector file `src/fixtures/gate-vectors.json` (vendored from the plugin
|
|
10
|
+
* repo's canonical copy via `npm run sync:plugin`): the bash side and this
|
|
11
|
+
* side run the exact same cases in their CIs, so a semantic change to one
|
|
12
|
+
* that isn't mirrored in the other fails a build. Edit them together.
|
|
13
|
+
*
|
|
14
|
+
* Ordering is contractual (encoded as named vectors):
|
|
15
|
+
* 1. alreadyAsked wins over EVERYTHING — even loud (dedup beats the dial).
|
|
16
|
+
* 2. priority is high iff marker === 'high', decided BEFORE the mode
|
|
17
|
+
* switch, so quiet+high and loud+high both push at high priority.
|
|
18
|
+
* 3. quiet → only a high marker pushes; loud → always push; normal →
|
|
19
|
+
* marker overrides the heuristic (skip → silent, push/high → push),
|
|
20
|
+
* no marker → push iff toolCount ≥ 2 AND nonReadonlyCount > 0
|
|
21
|
+
* (the B1 read-only floor).
|
|
22
|
+
*/
|
|
23
|
+
const child_process_1 = require("child_process");
|
|
24
|
+
const fs_1 = require("fs");
|
|
25
|
+
/**
|
|
26
|
+
* "Assume real work" defaults for hooks that can't supply turn facts
|
|
27
|
+
* (most non-Claude agents pass no counts): in normal mode the push still
|
|
28
|
+
* fires — preserving the historical always-push behavior of the dumb
|
|
29
|
+
* hooks — while quiet/loud now work everywhere.
|
|
30
|
+
*/
|
|
31
|
+
exports.GATE_DEFAULTS = {
|
|
32
|
+
toolCount: 2,
|
|
33
|
+
nonReadonlyCount: 1,
|
|
34
|
+
alreadyAsked: false,
|
|
35
|
+
};
|
|
36
|
+
const normalizeMarker = (raw) => raw === 'skip' || raw === 'push' || raw === 'high' ? raw : 'none';
|
|
37
|
+
exports.normalizeMarker = normalizeMarker;
|
|
38
|
+
const normalizePushMode = (raw) => raw === 'quiet' || raw === 'loud' ? raw : 'normal';
|
|
39
|
+
exports.normalizePushMode = normalizePushMode;
|
|
40
|
+
const decidePush = (input) => {
|
|
41
|
+
if (input.alreadyAsked)
|
|
42
|
+
return { push: false, priority: 'normal' };
|
|
43
|
+
const priority = input.marker === 'high' ? 'high' : 'normal';
|
|
44
|
+
if (input.pushMode === 'quiet')
|
|
45
|
+
return { push: input.marker === 'high', priority };
|
|
46
|
+
if (input.pushMode === 'loud')
|
|
47
|
+
return { push: true, priority };
|
|
48
|
+
if (input.marker === 'skip')
|
|
49
|
+
return { push: false, priority };
|
|
50
|
+
if (input.marker === 'push' || input.marker === 'high')
|
|
51
|
+
return { push: true, priority };
|
|
52
|
+
return { push: input.toolCount >= 2 && input.nonReadonlyCount > 0, priority };
|
|
53
|
+
};
|
|
54
|
+
exports.decidePush = decidePush;
|
|
55
|
+
// ── Per-project gate state (mute + push-mode dial) ───────────────
|
|
56
|
+
//
|
|
57
|
+
// The plugin's bash hooks key these tmp files off `cksum` of the project
|
|
58
|
+
// dir; shelling out to the same `cksum` here (instead of a pure-TS CRC)
|
|
59
|
+
// guarantees hash parity with every already-written file.
|
|
60
|
+
const projectHash = (dir) => {
|
|
61
|
+
try {
|
|
62
|
+
const raw = (0, child_process_1.execFileSync)('cksum', { input: dir, encoding: 'utf-8' });
|
|
63
|
+
return raw.split(' ')[0] || null;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
exports.projectHash = projectHash;
|
|
70
|
+
/** True when the user ran /zeph-mute for this project. */
|
|
71
|
+
const isMuted = (dir) => {
|
|
72
|
+
const hash = (0, exports.projectHash)(dir);
|
|
73
|
+
return hash !== null && (0, fs_1.existsSync)(`/tmp/zeph-muted-${hash}`);
|
|
74
|
+
};
|
|
75
|
+
exports.isMuted = isMuted;
|
|
76
|
+
/** The user's session push-mode dial (/zeph-quiet | /zeph-loud), default normal. */
|
|
77
|
+
const readPushMode = (dir) => {
|
|
78
|
+
const hash = (0, exports.projectHash)(dir);
|
|
79
|
+
if (!hash)
|
|
80
|
+
return 'normal';
|
|
81
|
+
try {
|
|
82
|
+
return (0, exports.normalizePushMode)((0, fs_1.readFileSync)(`/tmp/zeph-pushmode-${hash}`, 'utf-8').replace(/\s+/g, ''));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return 'normal';
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
exports.readPushMode = readPushMode;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { ZephHook } from './zeph-hook.js';
|
|
2
2
|
export { ZephError, AuthenticationError, QuotaExceededError } from './errors.js';
|
|
3
3
|
export type { ZephOptions, NotifyPayload, NotifyResult, ListParams, ListResult, DismissOneResult, DismissAllResult, PushItem } from './types.js';
|
|
4
|
+
export { decidePush, GATE_DEFAULTS, normalizeMarker, normalizePushMode } from './gate.js';
|
|
5
|
+
export type { GateInput, GateVerdict, GateMarker, GatePushMode } from './gate.js';
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjF,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjF,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjJ,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.QuotaExceededError = exports.AuthenticationError = exports.ZephError = exports.ZephHook = void 0;
|
|
3
|
+
exports.normalizePushMode = exports.normalizeMarker = exports.GATE_DEFAULTS = exports.decidePush = exports.QuotaExceededError = exports.AuthenticationError = exports.ZephError = exports.ZephHook = void 0;
|
|
4
4
|
var zeph_hook_js_1 = require("./zeph-hook.js");
|
|
5
5
|
Object.defineProperty(exports, "ZephHook", { enumerable: true, get: function () { return zeph_hook_js_1.ZephHook; } });
|
|
6
6
|
var errors_js_1 = require("./errors.js");
|
|
7
7
|
Object.defineProperty(exports, "ZephError", { enumerable: true, get: function () { return errors_js_1.ZephError; } });
|
|
8
8
|
Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return errors_js_1.AuthenticationError; } });
|
|
9
9
|
Object.defineProperty(exports, "QuotaExceededError", { enumerable: true, get: function () { return errors_js_1.QuotaExceededError; } });
|
|
10
|
+
var gate_js_1 = require("./gate.js");
|
|
11
|
+
Object.defineProperty(exports, "decidePush", { enumerable: true, get: function () { return gate_js_1.decidePush; } });
|
|
12
|
+
Object.defineProperty(exports, "GATE_DEFAULTS", { enumerable: true, get: function () { return gate_js_1.GATE_DEFAULTS; } });
|
|
13
|
+
Object.defineProperty(exports, "normalizeMarker", { enumerable: true, get: function () { return gate_js_1.normalizeMarker; } });
|
|
14
|
+
Object.defineProperty(exports, "normalizePushMode", { enumerable: true, get: function () { return gate_js_1.normalizePushMode; } });
|
package/dist/listener.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* messages as new pushes are created. Reconnects with exponential
|
|
21
21
|
* backoff on transient failures; gives up on auth failures (4001/4002/4003).
|
|
22
22
|
*/
|
|
23
|
-
type AgentKind
|
|
23
|
+
import { type AgentKind, type RegisteredRemoteAgent } from './remote-agents.js';
|
|
24
24
|
interface AgentSession {
|
|
25
25
|
name: string;
|
|
26
26
|
attached: boolean;
|
|
@@ -55,15 +55,22 @@ export declare const parseSessionName: (name: string) => {
|
|
|
55
55
|
project: string;
|
|
56
56
|
label: string | null;
|
|
57
57
|
} | null;
|
|
58
|
+
interface PaneInfo {
|
|
59
|
+
currentCommand: string | null;
|
|
60
|
+
startCommand: string | null;
|
|
61
|
+
currentPath: string | null;
|
|
62
|
+
}
|
|
58
63
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
64
|
+
* Identify the agent from the tmux pane. Prefer `pane_start_command`
|
|
65
|
+
* because the foreground process is usually `node`/`python3` (the
|
|
66
|
+
* interpreter), which doesn't tell us *what* was launched. Fall back to
|
|
67
|
+
* `pane_current_command` when start_command is empty — tmux clears
|
|
68
|
+
* start_command in some re-attach cases, especially when a pre-existing
|
|
69
|
+
* session was joined via `tmux new -A` instead of being created fresh.
|
|
70
|
+
* That fallback is safe because only the literal binaries registered in
|
|
71
|
+
* remote-agents.ts are accepted as a match.
|
|
65
72
|
*/
|
|
66
|
-
export declare const
|
|
73
|
+
export declare const detectRemoteAgent: (info: PaneInfo) => RegisteredRemoteAgent | null;
|
|
67
74
|
export interface CollectResult {
|
|
68
75
|
sessions: AgentSession[];
|
|
69
76
|
/** Diagnostic notes per rejected session — surfaced under `--verbose`. */
|
|
@@ -81,11 +88,12 @@ export interface CollectResult {
|
|
|
81
88
|
export declare const collectSessionsVerbose: () => CollectResult;
|
|
82
89
|
/**
|
|
83
90
|
* Snapshot the live `zeph-*` tmux sessions on this machine, enriched
|
|
84
|
-
* with the running agent kind,
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
91
|
+
* with the running agent kind, the agent's own session id (when the
|
|
92
|
+
* registry has a resolver — currently Claude Code only), project, and
|
|
93
|
+
* tmux activity timestamps. Returns [] when tmux is unreachable or no
|
|
94
|
+
* agent sessions exist. Sessions whose pane is at a shell or running
|
|
95
|
+
* something not registered in remote-agents.ts are filtered out — the
|
|
96
|
+
* phone can't usefully address them.
|
|
89
97
|
*/
|
|
90
98
|
export declare const collectSessions: () => AgentSession[];
|
|
91
99
|
/**
|
package/dist/listener.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AASH,OAAO,EAA2B,KAAK,SAAS,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AA2BzG,UAAU,YAAY;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAYD,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAA+B,CAAC;AAenF,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,MAAK,MAAmB,KAAG,OAgB1E,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAAM,GAAG,IAO7D,CAAC;AAiDF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAG5C,CAAC;AA8NF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAK3F,CAAC;AAEF,UAAU,QAAQ;IACd,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AA+CD;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,QAAQ,KAAG,qBAAqB,GAAG,IAGhE,CAAC;AASZ,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,QAAO,aA+DzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,QAAO,YAAY,EAAuC,CAAC;AAIvF;;;;;GAKG;AACH,UAAU,kBAAkB;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,QAAQ;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAChC;AAED,UAAU,cAAc;IACpB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACpD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,oEAAoE;IACpE,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC5F;AA2CD,eAAO,MAAM,oBAAoB,GAAI,KAAK;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAAG,IAE/E,CAAC;AA2FF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACtB,MAAK,MAAmB,EACxB,MAAK,MAAwB,EAC7B,MAAK,MAA0B,KAChC,MAaF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GACnB,MAAM,QAAQ,EACd,OAAM,cAAmB,KAC1B,OAAO,CAAC,OAAO,CAsBjB,CAAC;AAcF,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,KAAG,MAIhD,CAAC;AASF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GAAI,OAAM,MAAmB,KAAG,MAGnE,CAAC;AAoOF,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAwG3F,CAAC"}
|
package/dist/listener.js
CHANGED
|
@@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.handleListener = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.collectSessions = exports.collectSessionsVerbose = exports.
|
|
28
|
+
exports.handleListener = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.collectSessions = exports.collectSessionsVerbose = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = void 0;
|
|
29
29
|
const child_process_1 = require("child_process");
|
|
30
30
|
const crypto_1 = require("crypto");
|
|
31
31
|
const fs_1 = require("fs");
|
|
@@ -33,6 +33,7 @@ const os_1 = require("os");
|
|
|
33
33
|
const path_1 = require("path");
|
|
34
34
|
const ws_1 = __importDefault(require("ws"));
|
|
35
35
|
const config_js_1 = require("./config.js");
|
|
36
|
+
const remote_agents_js_1 = require("./remote-agents.js");
|
|
36
37
|
const PING_INTERVAL_MS = 25_000;
|
|
37
38
|
const PONG_TIMEOUT_MS = 10_000;
|
|
38
39
|
const RECONNECT_BASE_MS = 1_000;
|
|
@@ -54,7 +55,6 @@ const WS_STALL_TIMEOUT_MS = 90_000;
|
|
|
54
55
|
// to reflect new `zeph cc` sessions within a few seconds, not half a
|
|
55
56
|
// minute.
|
|
56
57
|
const SESSION_REPORT_INTERVAL_MS = 5_000;
|
|
57
|
-
const AGENT_KINDS = ['claude', 'codex', 'gemini'];
|
|
58
58
|
// Per-session token bucket — caps a runaway/compromised sender. 30/min
|
|
59
59
|
// is generous for human-driven phone use, tight enough to block flooding.
|
|
60
60
|
const RATE_LIMIT_TOKENS = 30;
|
|
@@ -400,70 +400,6 @@ const parseSessionName = (name) => {
|
|
|
400
400
|
return { project: rest, label: null };
|
|
401
401
|
};
|
|
402
402
|
exports.parseSessionName = parseSessionName;
|
|
403
|
-
const CLAUDE_PROJECTS_DIR = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'projects');
|
|
404
|
-
/**
|
|
405
|
-
* Cache for detectClaudeSessionId. The function walks every jsonl file
|
|
406
|
-
* in `~/.claude/projects/<hash>/` on each call — after weeks of CC use
|
|
407
|
-
* that directory holds hundreds of session files, and we were calling
|
|
408
|
-
* this per tmux session per 5-second report cycle. Heavy disk I/O
|
|
409
|
-
* compounded with multiple sessions caused the report cycle to spike
|
|
410
|
-
* CPU and starve the host shell.
|
|
411
|
-
*
|
|
412
|
-
* The current-session UUID only changes when a new CC session starts
|
|
413
|
-
* in that directory (rare, on the order of hours), so a 60-second TTL
|
|
414
|
-
* is safe and cuts the per-cycle stat count by ~12×.
|
|
415
|
-
*/
|
|
416
|
-
const claudeSessionCache = new Map();
|
|
417
|
-
const CLAUDE_SESSION_CACHE_TTL_MS = 60_000;
|
|
418
|
-
const doDetectClaudeSessionId = (cwd) => {
|
|
419
|
-
try {
|
|
420
|
-
const projectHash = cwd.replace(/\//g, '-');
|
|
421
|
-
const sessionsDir = (0, path_1.join)(CLAUDE_PROJECTS_DIR, projectHash);
|
|
422
|
-
let latest;
|
|
423
|
-
for (const entry of (0, fs_1.readdirSync)(sessionsDir)) {
|
|
424
|
-
const m = entry.match(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/);
|
|
425
|
-
if (!m)
|
|
426
|
-
continue;
|
|
427
|
-
const stat = (0, fs_1.statSync)((0, path_1.join)(sessionsDir, entry));
|
|
428
|
-
if (!stat.isFile())
|
|
429
|
-
continue;
|
|
430
|
-
if (!latest || stat.mtimeMs > latest.mtime) {
|
|
431
|
-
latest = { name: m[1], mtime: stat.mtimeMs };
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return latest?.name ?? null;
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
/**
|
|
441
|
-
* Locate the most recent Claude Code session UUID for the working
|
|
442
|
-
* directory of a tmux pane. Mirrors `mcp-server/config.ts`'s
|
|
443
|
-
* detectClaudeSessionId: CC writes per-session jsonl files at
|
|
444
|
-
* `~/.claude/projects/<projectHash>/<UUID>.jsonl` where the hash is
|
|
445
|
-
* the cwd with `/` replaced by `-`. Cached for 60s — see
|
|
446
|
-
* claudeSessionCache.
|
|
447
|
-
*/
|
|
448
|
-
const detectClaudeSessionId = (cwd) => {
|
|
449
|
-
const now = Date.now();
|
|
450
|
-
const cached = claudeSessionCache.get(cwd);
|
|
451
|
-
if (cached && cached.expiresAt > now)
|
|
452
|
-
return cached.sessionId;
|
|
453
|
-
// Cap cache size so a long-lived listener that's seen many cwds
|
|
454
|
-
// doesn't grow unbounded. 64 is plenty for any realistic setup.
|
|
455
|
-
if (claudeSessionCache.size >= 64) {
|
|
456
|
-
// Evict the oldest-expiring entry — Map iteration order is
|
|
457
|
-
// insertion order, so the first key we hit is the oldest.
|
|
458
|
-
const firstKey = claudeSessionCache.keys().next().value;
|
|
459
|
-
if (firstKey !== undefined)
|
|
460
|
-
claudeSessionCache.delete(firstKey);
|
|
461
|
-
}
|
|
462
|
-
const sessionId = doDetectClaudeSessionId(cwd);
|
|
463
|
-
claudeSessionCache.set(cwd, { sessionId, expiresAt: now + CLAUDE_SESSION_CACHE_TTL_MS });
|
|
464
|
-
return sessionId;
|
|
465
|
-
};
|
|
466
|
-
exports.detectClaudeSessionId = detectClaudeSessionId;
|
|
467
403
|
// U+241F "Symbol for Unit Separator" — a *printable* Unicode glyph
|
|
468
404
|
// (3-byte UTF-8) that visually represents the C0 Unit Separator but is
|
|
469
405
|
// itself a normal character. Critical detail: tmux 3.5a's `-F` format
|
|
@@ -509,28 +445,19 @@ const firstTokenBasename = (cmd) => {
|
|
|
509
445
|
return (0, path_1.basename)(stripped.split(/\s+/)[0] || '');
|
|
510
446
|
};
|
|
511
447
|
/**
|
|
512
|
-
* Identify the agent
|
|
448
|
+
* Identify the agent from the tmux pane. Prefer `pane_start_command`
|
|
513
449
|
* because the foreground process is usually `node`/`python3` (the
|
|
514
450
|
* interpreter), which doesn't tell us *what* was launched. Fall back to
|
|
515
451
|
* `pane_current_command` when start_command is empty — tmux clears
|
|
516
452
|
* start_command in some re-attach cases, especially when a pre-existing
|
|
517
453
|
* session was joined via `tmux new -A` instead of being created fresh.
|
|
518
|
-
* That fallback is safe because
|
|
519
|
-
*
|
|
454
|
+
* That fallback is safe because only the literal binaries registered in
|
|
455
|
+
* remote-agents.ts are accepted as a match.
|
|
520
456
|
*/
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
return k;
|
|
526
|
-
}
|
|
527
|
-
const currentBase = firstTokenBasename(info.currentCommand);
|
|
528
|
-
for (const k of AGENT_KINDS) {
|
|
529
|
-
if (currentBase === k)
|
|
530
|
-
return k;
|
|
531
|
-
}
|
|
532
|
-
return null;
|
|
533
|
-
};
|
|
457
|
+
const detectRemoteAgent = (info) => (0, remote_agents_js_1.matchAgentByPaneCommand)(firstTokenBasename(info.startCommand))
|
|
458
|
+
?? (0, remote_agents_js_1.matchAgentByPaneCommand)(firstTokenBasename(info.currentCommand))
|
|
459
|
+
?? null;
|
|
460
|
+
exports.detectRemoteAgent = detectRemoteAgent;
|
|
534
461
|
const epochToIso = (epoch) => {
|
|
535
462
|
if (!epoch)
|
|
536
463
|
return undefined;
|
|
@@ -583,21 +510,21 @@ const collectSessionsVerbose = () => {
|
|
|
583
510
|
continue;
|
|
584
511
|
}
|
|
585
512
|
const info = readPaneInfo(name);
|
|
586
|
-
const
|
|
587
|
-
if (!
|
|
513
|
+
const agent = (0, exports.detectRemoteAgent)(info);
|
|
514
|
+
if (!agent) {
|
|
588
515
|
rejected.push({
|
|
589
516
|
name,
|
|
590
517
|
reason: `no agent in pane (start=${info.startCommand ?? 'null'}, current=${info.currentCommand ?? 'null'})`,
|
|
591
518
|
});
|
|
592
519
|
continue;
|
|
593
520
|
}
|
|
594
|
-
const agentSessionId =
|
|
595
|
-
? (
|
|
521
|
+
const agentSessionId = info.currentPath
|
|
522
|
+
? (agent.resolveSessionId?.(info.currentPath) ?? null)
|
|
596
523
|
: null;
|
|
597
524
|
sessions.push({
|
|
598
525
|
name,
|
|
599
526
|
attached: attached === '1',
|
|
600
|
-
agentKind,
|
|
527
|
+
agentKind: agent.kind,
|
|
601
528
|
agentSessionId,
|
|
602
529
|
project: parsed.project,
|
|
603
530
|
label: parsed.label,
|
|
@@ -610,11 +537,12 @@ const collectSessionsVerbose = () => {
|
|
|
610
537
|
exports.collectSessionsVerbose = collectSessionsVerbose;
|
|
611
538
|
/**
|
|
612
539
|
* Snapshot the live `zeph-*` tmux sessions on this machine, enriched
|
|
613
|
-
* with the running agent kind,
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
540
|
+
* with the running agent kind, the agent's own session id (when the
|
|
541
|
+
* registry has a resolver — currently Claude Code only), project, and
|
|
542
|
+
* tmux activity timestamps. Returns [] when tmux is unreachable or no
|
|
543
|
+
* agent sessions exist. Sessions whose pane is at a shell or running
|
|
544
|
+
* something not registered in remote-agents.ts are filtered out — the
|
|
545
|
+
* phone can't usefully address them.
|
|
618
546
|
*/
|
|
619
547
|
const collectSessions = () => (0, exports.collectSessionsVerbose)().sessions;
|
|
620
548
|
exports.collectSessions = collectSessions;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface RemoteAgent {
|
|
2
|
+
/** Wire value for AgentSession.agentKind (server/phone contract). */
|
|
3
|
+
kind: string;
|
|
4
|
+
/** Human name for --help text. */
|
|
5
|
+
displayName: string;
|
|
6
|
+
/** Binary launched in the tmux pane; also the primary pane-match token. */
|
|
7
|
+
binary: string;
|
|
8
|
+
/** `zeph <subcommand>` aliases that launch this agent. */
|
|
9
|
+
subcommands: readonly string[];
|
|
10
|
+
/** Extra pane_command basenames accepted as this agent (beyond binary). */
|
|
11
|
+
paneMatchAliases?: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the agent's own session id from the pane's cwd.
|
|
14
|
+
* EXTENSION POINT: omitted for codex/gemini until their session-file
|
|
15
|
+
* formats are confirmed — the listener then reports agentSessionId: null.
|
|
16
|
+
*/
|
|
17
|
+
resolveSessionId?: (paneCwd: string) => string | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Locate the most recent Claude Code session UUID for the working
|
|
21
|
+
* directory of a tmux pane. Mirrors `mcp-server/config.ts`'s
|
|
22
|
+
* detectClaudeSessionId: CC writes per-session jsonl files at
|
|
23
|
+
* `~/.claude/projects/<projectHash>/<UUID>.jsonl` where the hash is
|
|
24
|
+
* the cwd with `/` replaced by `-`. Cached for 60s — see
|
|
25
|
+
* claudeSessionCache.
|
|
26
|
+
*/
|
|
27
|
+
export declare const detectClaudeSessionId: (cwd: string) => string | null;
|
|
28
|
+
declare const REMOTE_AGENT_TABLE: readonly [{
|
|
29
|
+
readonly kind: "claude";
|
|
30
|
+
readonly displayName: "Claude Code";
|
|
31
|
+
readonly binary: "claude";
|
|
32
|
+
readonly subcommands: readonly ["cc", "claude"];
|
|
33
|
+
readonly resolveSessionId: (cwd: string) => string | null;
|
|
34
|
+
}, {
|
|
35
|
+
readonly kind: "codex";
|
|
36
|
+
readonly displayName: "Codex CLI";
|
|
37
|
+
readonly binary: "codex";
|
|
38
|
+
readonly subcommands: readonly ["codex"];
|
|
39
|
+
}, {
|
|
40
|
+
readonly kind: "gemini";
|
|
41
|
+
readonly displayName: "Gemini CLI";
|
|
42
|
+
readonly binary: "gemini";
|
|
43
|
+
readonly subcommands: readonly ["gemini"];
|
|
44
|
+
}];
|
|
45
|
+
/** Closed union of remote-controllable agent kinds ('claude' | 'codex' | 'gemini'). */
|
|
46
|
+
export type AgentKind = (typeof REMOTE_AGENT_TABLE)[number]['kind'];
|
|
47
|
+
/** A registry row: the uniform RemoteAgent shape with `kind` narrowed to the closed union. */
|
|
48
|
+
export type RegisteredRemoteAgent = RemoteAgent & {
|
|
49
|
+
kind: AgentKind;
|
|
50
|
+
};
|
|
51
|
+
export declare const REMOTE_AGENTS: readonly RegisteredRemoteAgent[];
|
|
52
|
+
export declare const findAgentBySubcommand: (cmd: string) => RegisteredRemoteAgent | undefined;
|
|
53
|
+
export declare const matchAgentByPaneCommand: (base: string) => RegisteredRemoteAgent | undefined;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=remote-agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-agents.d.ts","sourceRoot":"","sources":["../src/remote-agents.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,WAAW;IACxB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACzD;AAyCD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAiB5D,CAAC;AAIF,QAAA,MAAM,kBAAkB;;;;;qCArBmB,MAAM,KAAG,MAAM,GAAG,IAAI;;;;;;;;;;;EAyCtB,CAAC;AAE5C,uFAAuF;AACvF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,8FAA8F;AAC9F,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEtE,eAAO,MAAM,aAAa,EAAE,SAAS,qBAAqB,EAAuB,CAAC;AAElF,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,qBAAqB,GAAG,SAClB,CAAC;AAE3D,eAAO,MAAM,uBAAuB,GAAI,MAAM,MAAM,KAAG,qBAAqB,GAAG,SAG9E,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.matchAgentByPaneCommand = exports.findAgentBySubcommand = exports.REMOTE_AGENTS = exports.detectClaudeSessionId = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Remote-control agent registry — the single table behind `zeph cc` /
|
|
6
|
+
* `zeph codex` / `zeph gemini`, the listener's pane matching, and the
|
|
7
|
+
* per-agent session-id enrichment. Adding a remote-controllable agent is
|
|
8
|
+
* one row here (plus, for a genuinely new kind, backend/phone support:
|
|
9
|
+
* `kind` is a wire contract — AgentSession.agentKind flows to the server
|
|
10
|
+
* and the phone picker, which may validate the enum).
|
|
11
|
+
*
|
|
12
|
+
* This is deliberately NOT merged into `agents.ts`: that table drives
|
|
13
|
+
* install/uninstall/verify detection (8 agents, incl. Cursor/Windsurf
|
|
14
|
+
* which can never be driven via tmux), and the two tables carry different
|
|
15
|
+
* name axes — install id vs subcommand alias vs pane binary.
|
|
16
|
+
*/
|
|
17
|
+
const fs_1 = require("fs");
|
|
18
|
+
const os_1 = require("os");
|
|
19
|
+
const path_1 = require("path");
|
|
20
|
+
// ── Claude Code session resolver ─────────────────────────────────
|
|
21
|
+
const CLAUDE_PROJECTS_DIR = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'projects');
|
|
22
|
+
/**
|
|
23
|
+
* Cache for detectClaudeSessionId. The function walks every jsonl file
|
|
24
|
+
* in `~/.claude/projects/<hash>/` on each call — after weeks of CC use
|
|
25
|
+
* that directory holds hundreds of session files, and we were calling
|
|
26
|
+
* this per tmux session per 5-second report cycle. Heavy disk I/O
|
|
27
|
+
* compounded with multiple sessions caused the report cycle to spike
|
|
28
|
+
* CPU and starve the host shell.
|
|
29
|
+
*
|
|
30
|
+
* The current-session UUID only changes when a new CC session starts
|
|
31
|
+
* in that directory (rare, on the order of hours), so a 60-second TTL
|
|
32
|
+
* is safe and cuts the per-cycle stat count by ~12×.
|
|
33
|
+
*/
|
|
34
|
+
const claudeSessionCache = new Map();
|
|
35
|
+
const CLAUDE_SESSION_CACHE_TTL_MS = 60_000;
|
|
36
|
+
const doDetectClaudeSessionId = (cwd) => {
|
|
37
|
+
try {
|
|
38
|
+
const projectHash = cwd.replace(/\//g, '-');
|
|
39
|
+
const sessionsDir = (0, path_1.join)(CLAUDE_PROJECTS_DIR, projectHash);
|
|
40
|
+
let latest;
|
|
41
|
+
for (const entry of (0, fs_1.readdirSync)(sessionsDir)) {
|
|
42
|
+
const m = entry.match(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/);
|
|
43
|
+
if (!m)
|
|
44
|
+
continue;
|
|
45
|
+
const stat = (0, fs_1.statSync)((0, path_1.join)(sessionsDir, entry));
|
|
46
|
+
if (!stat.isFile())
|
|
47
|
+
continue;
|
|
48
|
+
if (!latest || stat.mtimeMs > latest.mtime) {
|
|
49
|
+
latest = { name: m[1], mtime: stat.mtimeMs };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return latest?.name ?? null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Locate the most recent Claude Code session UUID for the working
|
|
60
|
+
* directory of a tmux pane. Mirrors `mcp-server/config.ts`'s
|
|
61
|
+
* detectClaudeSessionId: CC writes per-session jsonl files at
|
|
62
|
+
* `~/.claude/projects/<projectHash>/<UUID>.jsonl` where the hash is
|
|
63
|
+
* the cwd with `/` replaced by `-`. Cached for 60s — see
|
|
64
|
+
* claudeSessionCache.
|
|
65
|
+
*/
|
|
66
|
+
const detectClaudeSessionId = (cwd) => {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const cached = claudeSessionCache.get(cwd);
|
|
69
|
+
if (cached && cached.expiresAt > now)
|
|
70
|
+
return cached.sessionId;
|
|
71
|
+
// Cap cache size so a long-lived listener that's seen many cwds
|
|
72
|
+
// doesn't grow unbounded. 64 is plenty for any realistic setup.
|
|
73
|
+
if (claudeSessionCache.size >= 64) {
|
|
74
|
+
// Evict the oldest-expiring entry — Map iteration order is
|
|
75
|
+
// insertion order, so the first key we hit is the oldest.
|
|
76
|
+
const firstKey = claudeSessionCache.keys().next().value;
|
|
77
|
+
if (firstKey !== undefined)
|
|
78
|
+
claudeSessionCache.delete(firstKey);
|
|
79
|
+
}
|
|
80
|
+
const sessionId = doDetectClaudeSessionId(cwd);
|
|
81
|
+
claudeSessionCache.set(cwd, { sessionId, expiresAt: now + CLAUDE_SESSION_CACHE_TTL_MS });
|
|
82
|
+
return sessionId;
|
|
83
|
+
};
|
|
84
|
+
exports.detectClaudeSessionId = detectClaudeSessionId;
|
|
85
|
+
// ── The registry ─────────────────────────────────────────────────
|
|
86
|
+
const REMOTE_AGENT_TABLE = [
|
|
87
|
+
{
|
|
88
|
+
kind: 'claude',
|
|
89
|
+
displayName: 'Claude Code',
|
|
90
|
+
binary: 'claude',
|
|
91
|
+
subcommands: ['cc', 'claude'],
|
|
92
|
+
resolveSessionId: exports.detectClaudeSessionId,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
kind: 'codex',
|
|
96
|
+
displayName: 'Codex CLI',
|
|
97
|
+
binary: 'codex',
|
|
98
|
+
subcommands: ['codex'],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
kind: 'gemini',
|
|
102
|
+
displayName: 'Gemini CLI',
|
|
103
|
+
binary: 'gemini',
|
|
104
|
+
subcommands: ['gemini'],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
exports.REMOTE_AGENTS = REMOTE_AGENT_TABLE;
|
|
108
|
+
const findAgentBySubcommand = (cmd) => exports.REMOTE_AGENTS.find((a) => a.subcommands.includes(cmd));
|
|
109
|
+
exports.findAgentBySubcommand = findAgentBySubcommand;
|
|
110
|
+
const matchAgentByPaneCommand = (base) => {
|
|
111
|
+
if (!base)
|
|
112
|
+
return undefined;
|
|
113
|
+
return exports.REMOTE_AGENTS.find((a) => a.binary === base || (a.paneMatchAliases ?? []).includes(base));
|
|
114
|
+
};
|
|
115
|
+
exports.matchAgentByPaneCommand = matchAgentByPaneCommand;
|
package/dist/templates.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAoFA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QAItB,CAAC;AAEH,6EAA6E;AAC7E,eAAO,MAAM,aAAa,QAAyE,CAAC;AAEpG,sDAAsD;AACtD,eAAO,MAAM,WAAW,QAAyE,CAAC;AAElG,oDAAoD;AACpD,eAAO,MAAM,UAAU,QAAyE,CAAC;AAEjG,oFAAoF;AACpF,eAAO,MAAM,YAAY,QAAyE,CAAC;AAEnG,gEAAgE;AAChE,eAAO,MAAM,UAAU,QAAkE,CAAC;AAE1F,4FAA4F;AAC5F,eAAO,MAAM,UAAU,QAAkE,CAAC;AAI1F,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"}
|
package/dist/templates.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Every supported agent gets the SAME behavioral rules so Zeph behaves
|
|
5
5
|
// identically everywhere. The rule text is assembled from one shared
|
|
6
|
-
// core (
|
|
6
|
+
// generated core (src/zeph-core.generated.ts) plus a per-agent
|
|
7
|
+
// notification preamble:
|
|
7
8
|
//
|
|
8
9
|
// - Hook-driven agents (Cursor, Windsurf, Gemini, Codex, Copilot) have
|
|
9
10
|
// a Stop-equivalent hook installed that auto-pushes on completion, so
|
|
@@ -12,160 +13,31 @@
|
|
|
12
13
|
// zeph_notify for meaningful completions.
|
|
13
14
|
//
|
|
14
15
|
// The Ask-Loop / sticky-REMOTE / question-mandate rules are identical for
|
|
15
|
-
// all of them — that is the whole point of the shared
|
|
16
|
+
// all of them — that is the whole point of the shared generated core.
|
|
16
17
|
//
|
|
17
18
|
// Keeping this in one place means a rule change lands everywhere at once
|
|
18
19
|
// and the agents can't drift apart.
|
|
19
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
21
|
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;
|
|
22
|
+
const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
|
|
21
23
|
// Graceful resolution: prefer the installed `zeph` CLI, but fall back to
|
|
22
24
|
// `npx -y @zeph-to/cli` so the hook still fires when the user
|
|
23
25
|
// installed via a non-standard prefix and the binary isn't on PATH at hook
|
|
24
26
|
// fire time (e.g. ~/.local/bin without PATH update). This mirrors the
|
|
25
27
|
// 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
28
|
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// the
|
|
29
|
+
// `--auto` applies the shared push-gate before sending (see src/gate.ts):
|
|
30
|
+
// in normal mode the push still fires (gate defaults assume real work), but
|
|
31
|
+
// the /zeph-quiet | /zeph-loud dial now works for every hook-driven agent.
|
|
32
|
+
// Older installed `zeph` versions parse `--auto` as an unknown boolean flag
|
|
33
|
+
// and ignore it — graceful backward compatibility.
|
|
34
|
+
const NOTIFY_CMD = '$(command -v zeph || echo "npx -y @zeph-to/cli") notify --title "Task done" --auto 2>/dev/null || true';
|
|
35
|
+
// ── Shared behavioral core ───────────────────────────────────────
|
|
32
36
|
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
When ZEPH_HOOK_ID is set the user is driving you remotely, often from
|
|
38
|
-
their phone. A "?" written in your reply text is invisible to them.
|
|
39
|
-
|
|
40
|
-
3. NEVER end a response with a plain-text question. If your response asks
|
|
41
|
-
the user anything that needs their input — confirmation, choice,
|
|
42
|
-
yes/no, clarification, "Apply this?", "Proceed?", "Which option?" —
|
|
43
|
-
the FINAL tool call MUST be zeph_ask. This applies even on research /
|
|
44
|
-
analysis / planning turns where no files were touched.
|
|
45
|
-
|
|
46
|
-
Anti-pattern (wrong):
|
|
47
|
-
"...build/commit on hold. Apply A?"
|
|
48
|
-
Correct:
|
|
49
|
-
zeph_ask({
|
|
50
|
-
title: "Apply solution A?",
|
|
51
|
-
body: "<short context comparing options>",
|
|
52
|
-
actions: [
|
|
53
|
-
{ id: "apply_a", label: "Apply A" },
|
|
54
|
-
{ id: "apply_b", label: "Apply B" },
|
|
55
|
-
{ id: "cancel", label: "Cancel" }
|
|
56
|
-
],
|
|
57
|
-
placeholder: "or describe a different approach...",
|
|
58
|
-
fallback: "cancel"
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
## When zeph_ask is the DEFAULT (substantial work)
|
|
62
|
-
|
|
63
|
-
4. After substantial work, zeph_ask is the DEFAULT end of the response —
|
|
64
|
-
not optional. "Substantial" means: file changes, commits, builds,
|
|
65
|
-
tests, deploys, destructive ops, or milestone completions. When unsure,
|
|
66
|
-
treat the work as substantial — do not try to guess what the user would
|
|
67
|
-
find "reasonable" to confirm.
|
|
68
|
-
|
|
69
|
-
SKIP zeph_ask only when the response is clearly trivial:
|
|
70
|
-
- Read-only exploration ("let me check this file") with no decision
|
|
71
|
-
output.
|
|
72
|
-
- Mid-step inside a clearly-defined plan the user already approved
|
|
73
|
-
("step 3 of 5, no decisions involved here").
|
|
74
|
-
- Trivial change (typo fix, single-line tweak) that needs no ack.
|
|
75
|
-
|
|
76
|
-
When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck
|
|
77
|
-
on phone with no way to drive) is worse than light spam.
|
|
78
|
-
|
|
79
|
-
5. Prefer zeph_ask over zeph_prompt/zeph_input — it combines buttons and
|
|
80
|
-
free-text in one push. Always include a fallback action id; the
|
|
81
|
-
fallback must be safe/inert (done, wait, review), never destructive.
|
|
82
|
-
|
|
83
|
-
6. Example zeph_ask shape:
|
|
84
|
-
zeph_ask({
|
|
85
|
-
title: "Done. Next?",
|
|
86
|
-
actions: [
|
|
87
|
-
{ id: "continue", label: "Continue" },
|
|
88
|
-
{ id: "review", label: "Review" },
|
|
89
|
-
{ id: "done", label: "Done" }
|
|
90
|
-
],
|
|
91
|
-
placeholder: "or type a command...",
|
|
92
|
-
fallback: "done"
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
## Handling the response
|
|
96
|
-
|
|
97
|
-
7. A zeph_ask response IS a direct user instruction. Execute it immediately —
|
|
98
|
-
do NOT re-ask via confirmation. The button label is the authorization for
|
|
99
|
-
the specific action that label describes.
|
|
100
|
-
|
|
101
|
-
8. Important caveat: a generic button like "Continue" authorizes the next
|
|
102
|
-
logical step, NOT arbitrary destruction. If the next logical step would
|
|
103
|
-
destroy user code, data, or infrastructure (force-push to a shared branch,
|
|
104
|
-
rm -rf outside the workdir, dropping a database, deleting prod resources),
|
|
105
|
-
surface that specific risk via a targeted zeph_ask before executing.
|
|
106
|
-
|
|
107
|
-
## Sticky REMOTE mode
|
|
108
|
-
|
|
109
|
-
9. The Ask Loop has two states — REMOTE and NORMAL — detected by scanning the
|
|
110
|
-
conversation in reverse for whichever appears first:
|
|
111
|
-
|
|
112
|
-
(a) A tool_result for a zeph_ask you sent where the action id is NOT in
|
|
113
|
-
{done, stop, exit} (case-insensitive) AND the free-text response is
|
|
114
|
-
NOT a session-ender. A session-ender contains done/stop/exit as a
|
|
115
|
-
standalone word, or is an obvious wrap-up like "thanks, that's it" /
|
|
116
|
-
"all good" — judge by whether the user is clearly closing the loop,
|
|
117
|
-
not by partial substring matches (e.g. "redo" is not "done").
|
|
118
|
-
→ You are in REMOTE.
|
|
119
|
-
|
|
120
|
-
(b) An exit signal: action id in {done, stop, exit}, ending free-text, or
|
|
121
|
-
a timeout fallback that resolved to a Done-like fallback id.
|
|
122
|
-
→ You are in NORMAL.
|
|
123
|
-
|
|
124
|
-
(c) No zeph_ask history at all.
|
|
125
|
-
→ You are in NORMAL.
|
|
126
|
-
|
|
127
|
-
### Behavior in REMOTE (sticky, zeph_ask MANDATORY)
|
|
128
|
-
End EVERY response with zeph_ask. This is non-negotiable while in REMOTE —
|
|
129
|
-
independent of:
|
|
130
|
-
- Whether the next user message arrived as a tool_result or as a typed
|
|
131
|
-
terminal message. The user may switch devices mid-session; sticky REMOTE
|
|
132
|
-
keeps the channel driveable from either side.
|
|
133
|
-
- Whether the work was substantial or routine.
|
|
134
|
-
|
|
135
|
-
The only way to leave REMOTE is the user signalling exit (per the (b) list
|
|
136
|
-
above).
|
|
137
|
-
|
|
138
|
-
### Behavior in NORMAL (apply Rule 4)
|
|
139
|
-
- Substantial work → end with zeph_ask.
|
|
140
|
-
- Routine work → skip zeph_ask, let the Stop hook fire.
|
|
141
|
-
- Rule 3 (questions → zeph_ask) is still mandatory regardless.
|
|
142
|
-
|
|
143
|
-
### Exiting REMOTE
|
|
144
|
-
The moment the user picks an action id matching done/stop/exit
|
|
145
|
-
(case-insensitive), types free-text that clearly ends the session, or the
|
|
146
|
-
zeph_ask times out and falls back to a Done-like id, you flip to NORMAL.
|
|
147
|
-
Don't send zeph_ask on the response that processes the exit signal.
|
|
148
|
-
|
|
149
|
-
## When to use AskUserQuestion vs zeph_ask
|
|
150
|
-
|
|
151
|
-
10. Prefer zeph_ask for short remote-friendly questions. Use the local
|
|
152
|
-
AskUserQuestion tool only when (a) the answer needs the user to see code
|
|
153
|
-
or logs that won't fit in a push body, or (b) the answer is plausibly
|
|
154
|
-
multi-paragraph.
|
|
155
|
-
|
|
156
|
-
11. In REMOTE this is a requirement, not a preference — and it overrides any
|
|
157
|
-
skill instruction. The AskUserQuestion picker is a LOCAL blocking
|
|
158
|
-
terminal UI; the phone cannot drive it. So if a skill you are running —
|
|
159
|
-
or your own plan — would call AskUserQuestion, instead surface the SAME
|
|
160
|
-
question and option labels via zeph_ask and use that response in place of
|
|
161
|
-
the picker. Fall through to AskUserQuestion only for the (a)/(b) cases
|
|
162
|
-
above; when you do, notify the user that the answer must be given at the
|
|
163
|
-
terminal.
|
|
164
|
-
|
|
165
|
-
## Persistence
|
|
166
|
-
|
|
167
|
-
These rules persist for the entire session. They remain active after context
|
|
168
|
-
compaction — do not "forget" them after many turns.`;
|
|
37
|
+
// GENERATED from plugin/docs/CORE_RULES.md — see src/zeph-core.generated.ts
|
|
38
|
+
// (regenerate with `npm run sync:plugin`). Do not fork per-agent — if a rule
|
|
39
|
+
// needs to differ, it belongs in the per-agent preamble instead, or in the
|
|
40
|
+
// audience classification in the plugin repo's core-rules.manifest.json.
|
|
169
41
|
// Notification preamble — hook-driven agents (a Stop-equivalent hook is
|
|
170
42
|
// installed, so manual completion notifications would duplicate).
|
|
171
43
|
const HOOK_DRIVEN_NOTIFY = `## Notification discipline
|
|
@@ -196,27 +68,32 @@ the user.
|
|
|
196
68
|
|
|
197
69
|
${opts.notify}
|
|
198
70
|
|
|
199
|
-
${
|
|
71
|
+
${opts.core}
|
|
200
72
|
`;
|
|
201
73
|
};
|
|
202
74
|
// ── Per-agent rule documents ─────────────────────────────────────
|
|
75
|
+
//
|
|
76
|
+
// The two generated cores are identical today; the split exists so a rule
|
|
77
|
+
// that only applies to one audience (e.g. Push Signal, once hook-driven
|
|
78
|
+
// agents' hooks process markers) is a one-line manifest change upstream.
|
|
203
79
|
/** Cursor — written to ~/.cursor/rules/zeph.mdc (needs .mdc frontmatter). */
|
|
204
80
|
exports.CURSOR_RULE = buildRule({
|
|
205
81
|
frontmatter: '---\ndescription: "Zeph remote-control rules"\nalwaysApply: true\n---',
|
|
206
82
|
notify: HOOK_DRIVEN_NOTIFY,
|
|
83
|
+
core: zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN,
|
|
207
84
|
});
|
|
208
85
|
/** Windsurf — appended into ~/.codeium/windsurf/memories/global_rules.md. */
|
|
209
|
-
exports.WINDSURF_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY });
|
|
86
|
+
exports.WINDSURF_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY, core: zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN });
|
|
210
87
|
/** Gemini CLI — appended into ~/.gemini/GEMINI.md. */
|
|
211
|
-
exports.GEMINI_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY });
|
|
88
|
+
exports.GEMINI_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY, core: zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN });
|
|
212
89
|
/** Codex CLI — appended into ~/.codex/AGENTS.md. */
|
|
213
|
-
exports.CODEX_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY });
|
|
90
|
+
exports.CODEX_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY, core: zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN });
|
|
214
91
|
/** GitHub Copilot CLI — written to ~/.copilot/instructions/zeph.instructions.md. */
|
|
215
|
-
exports.COPILOT_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY });
|
|
92
|
+
exports.COPILOT_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY, core: zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN });
|
|
216
93
|
/** Cline — written to ~/.cline/rules/zeph.md (no Stop hook). */
|
|
217
|
-
exports.CLINE_RULE = buildRule({ notify: MANUAL_NOTIFY });
|
|
94
|
+
exports.CLINE_RULE = buildRule({ notify: MANUAL_NOTIFY, core: zeph_core_generated_js_1.ZEPH_CORE_RULE_ONLY });
|
|
218
95
|
/** Aider — written to a standalone conventions file, loaded via .aider.conf.yml `read:`. */
|
|
219
|
-
exports.AIDER_RULE = buildRule({ notify: MANUAL_NOTIFY });
|
|
96
|
+
exports.AIDER_RULE = buildRule({ notify: MANUAL_NOTIFY, core: zeph_core_generated_js_1.ZEPH_CORE_RULE_ONLY });
|
|
220
97
|
// ── Hook configs (notification side, unchanged) ──────────────────
|
|
221
98
|
exports.CURSOR_HOOKS = JSON.stringify({
|
|
222
99
|
version: 1,
|
package/dist/wrapper.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RemoteAgent } from './remote-agents.js';
|
|
1
2
|
/** Resolve a project name for the tmux session: env > git root > cwd basename. */
|
|
2
3
|
export declare const detectProjectName: () => string;
|
|
3
4
|
/** `zeph-<project>` — the canonical tmux session base name. */
|
|
@@ -22,5 +23,5 @@ export declare const findAvailableSession: (base: string) => string;
|
|
|
22
23
|
* `zeph cc --resume foo` runs `claude --resume foo` inside the session.
|
|
23
24
|
* Returns when the agent exits.
|
|
24
25
|
*/
|
|
25
|
-
export declare const handleAgentSession: (agent:
|
|
26
|
+
export declare const handleAgentSession: (agent: RemoteAgent, extra?: string[]) => Promise<number>;
|
|
26
27
|
//# sourceMappingURL=wrapper.d.ts.map
|
package/dist/wrapper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAOtD,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,WAAW,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CAmC3F,CAAC"}
|
package/dist/wrapper.js
CHANGED
|
@@ -16,15 +16,14 @@ const child_process_1 = require("child_process");
|
|
|
16
16
|
const fs_1 = require("fs");
|
|
17
17
|
const os_1 = require("os");
|
|
18
18
|
const path_1 = require("path");
|
|
19
|
-
|
|
20
|
-
const PROJECT_DIR_ENVS = ['CLAUDE_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'WINDSURF_PROJECT_DIR'];
|
|
19
|
+
const config_js_1 = require("./config.js");
|
|
21
20
|
const FALLBACK_NAME = 'project';
|
|
22
21
|
/** basename(), with a stable fallback for edge paths like `/`. */
|
|
23
22
|
const safeBasename = (path) => (0, path_1.basename)(path) || FALLBACK_NAME;
|
|
24
23
|
/** Resolve a project name for the tmux session: env > git root > cwd basename. */
|
|
25
24
|
const detectProjectName = () => {
|
|
26
|
-
for (const key of
|
|
27
|
-
const v =
|
|
25
|
+
for (const key of config_js_1.PROJECT_DIR_ENV_VARS) {
|
|
26
|
+
const v = (0, config_js_1.resolvedEnv)(key);
|
|
28
27
|
if (v)
|
|
29
28
|
return safeBasename(v.replace(/\/+$/, ''));
|
|
30
29
|
}
|
|
@@ -206,7 +205,7 @@ const handleAgentSession = (agent, extra = []) => {
|
|
|
206
205
|
// command for the picker on their phone to work.
|
|
207
206
|
ensureListenerRunning();
|
|
208
207
|
return new Promise((resolve) => {
|
|
209
|
-
const { cmd, args } = targetForAgent(agent, extra);
|
|
208
|
+
const { cmd, args } = targetForAgent(agent.binary, extra);
|
|
210
209
|
const start = Date.now();
|
|
211
210
|
const child = (0, child_process_1.spawn)(cmd, args, { stdio: 'inherit' });
|
|
212
211
|
child.on('exit', (code) => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** sha256 over the plugin manifest + extracted rule text at generation time. */
|
|
2
|
+
export declare const ZEPH_CORE_SOURCE_HASH = "5d149a2909f9d1da7544fe4fcf540700c1be14193290d6f55e503a46032d8155";
|
|
3
|
+
/** Shared rule core for agents with a Stop-equivalent hook (Cursor, Windsurf, Gemini, Codex, Copilot). */
|
|
4
|
+
export declare const ZEPH_CORE_HOOK_DRIVEN = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input \u2014 confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" \u2014 the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response \u2014 not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial \u2014 do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` \u2014 it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape \u2014 use sparingly per Rule 4 (only at natural pause points; NOT after every response \u2014 see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately \u2014 do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing \u2014 e.g., title \"About to force-push main \u2014 proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** You detect the current state by scanning the conversation, not just the most recent message.\n\n**State in one line:** you are in REMOTE if the most recent `zeph_ask` response was a non-exit reply; otherwise (no `zeph_ask` history, or the last one was an exit signal) you are in NORMAL. REMOTE is sticky \u2014 every response ends with `zeph_ask` until the user exits.\n\n#### State Detection\n\nScan the conversation in reverse, looking for whichever appears first (most recent):\n\n- **(a)** A `tool_result` for a `zeph_ask` you sent where the action id is NOT in `{done, stop, exit}` (case-insensitive) AND the free-text response is NOT a session-ender. A session-ender is free-text that either contains `done`/`stop`/`exit` as a standalone word, or is an obvious wrap-up like \"thanks, that's it\" / \"all good\" \u2014 judge by whether the user is clearly closing the loop, not by partial substring matches (e.g. \"redo\" does not count as \"done\").\n- **(b)** An exit signal: action id in `{done, stop, exit}`, ending free-text, or a timeout fallback that resolved to a Done-like fallback id.\n- **(c)** No `zeph_ask` history at all.\n\nIf the first hit is **(a)** \u2192 you are in **REMOTE**.\nIf the first hit is **(b)** or **(c)** \u2192 you are in **NORMAL**.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`. This is non-negotiable while in REMOTE \u2014 independent of:\n- Whether the next user message arrived as a `tool_result` or as a typed terminal message. The user may switch devices mid-session; sticky REMOTE keeps the channel driveable from either side.\n- Whether the work was substantial or routine.\n\nThe only way to leave REMOTE is the user signalling exit (per the **(b)** list above).\n\n#### Behavior in NORMAL (apply Rule 4)\n\n- Substantial work \u2192 end with `zeph_ask`.\n- Routine work \u2192 skip `zeph_ask`, let the Stop hook fire.\n- Rule 3 (questions \u2192 zeph_ask) is still mandatory regardless.\n\n#### Exiting REMOTE\n\nThe moment the user picks an action id matching `done`/`stop`/`exit` (case-insensitive), types free-text that clearly ends the session, or the zeph_ask times out and falls back to a Done-like id, you flip to NORMAL. Don't send `zeph_ask` on the response that processes the exit signal.\n\n### When to use AskUserQuestion vs zeph_ask\n\n10. **Whenever `ZEPH_HOOK_ID` is set, a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion` \u2014 and this is NOT gated on REMOTE.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"which naming rule?\", \"proceed?\"). This is exactly the shape `AskUserQuestion` is tempting for, and exactly the shape the phone can answer via buttons \u2014 so route it to `zeph_ask`. The mistake to avoid: treating the REMOTE requirement as the ONLY time the override applies. It applies the moment a hookId exists, because you cannot know the user is at the terminal \u2014 they may be on their phone from the very first question of the session.\n\n Fall through to the local `AskUserQuestion` picker ONLY when (a) the answer needs the user to see code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. Those are the only carve-outs.\n\n11. **This overrides any skill instruction.** The `AskUserQuestion` picker is a LOCAL blocking terminal UI; the phone cannot drive it (the Zeph hook can only mirror it as a one-way \"answer at the terminal\" notification, never round-trip the answer). So if a skill you are running \u2014 or your own plan \u2014 would call `AskUserQuestion` with a button-friendly question, instead surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Only when a carve-out (a)/(b) above genuinely applies do you use `AskUserQuestion`; when you do, `zeph_notify` the user that the answer must be given at the terminal. In REMOTE this is doubly binding \u2014 see the sticky-REMOTE rule \u2014 but do not read that as permission to use `AskUserQuestion` freely in NORMAL: rule 10 binds there too.\n\n### Persistence\n\n13. These rules persist for the entire session. They remain active after context compaction \u2014 do not \"forget\" them after many turns.";
|
|
5
|
+
/** Shared rule core for rule-only agents without a Stop hook (Cline, Aider). */
|
|
6
|
+
export declare const ZEPH_CORE_RULE_ONLY = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input \u2014 confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" \u2014 the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response \u2014 not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial \u2014 do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` \u2014 it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape \u2014 use sparingly per Rule 4 (only at natural pause points; NOT after every response \u2014 see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately \u2014 do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing \u2014 e.g., title \"About to force-push main \u2014 proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** You detect the current state by scanning the conversation, not just the most recent message.\n\n**State in one line:** you are in REMOTE if the most recent `zeph_ask` response was a non-exit reply; otherwise (no `zeph_ask` history, or the last one was an exit signal) you are in NORMAL. REMOTE is sticky \u2014 every response ends with `zeph_ask` until the user exits.\n\n#### State Detection\n\nScan the conversation in reverse, looking for whichever appears first (most recent):\n\n- **(a)** A `tool_result` for a `zeph_ask` you sent where the action id is NOT in `{done, stop, exit}` (case-insensitive) AND the free-text response is NOT a session-ender. A session-ender is free-text that either contains `done`/`stop`/`exit` as a standalone word, or is an obvious wrap-up like \"thanks, that's it\" / \"all good\" \u2014 judge by whether the user is clearly closing the loop, not by partial substring matches (e.g. \"redo\" does not count as \"done\").\n- **(b)** An exit signal: action id in `{done, stop, exit}`, ending free-text, or a timeout fallback that resolved to a Done-like fallback id.\n- **(c)** No `zeph_ask` history at all.\n\nIf the first hit is **(a)** \u2192 you are in **REMOTE**.\nIf the first hit is **(b)** or **(c)** \u2192 you are in **NORMAL**.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`. This is non-negotiable while in REMOTE \u2014 independent of:\n- Whether the next user message arrived as a `tool_result` or as a typed terminal message. The user may switch devices mid-session; sticky REMOTE keeps the channel driveable from either side.\n- Whether the work was substantial or routine.\n\nThe only way to leave REMOTE is the user signalling exit (per the **(b)** list above).\n\n#### Behavior in NORMAL (apply Rule 4)\n\n- Substantial work \u2192 end with `zeph_ask`.\n- Routine work \u2192 skip `zeph_ask`, let the Stop hook fire.\n- Rule 3 (questions \u2192 zeph_ask) is still mandatory regardless.\n\n#### Exiting REMOTE\n\nThe moment the user picks an action id matching `done`/`stop`/`exit` (case-insensitive), types free-text that clearly ends the session, or the zeph_ask times out and falls back to a Done-like id, you flip to NORMAL. Don't send `zeph_ask` on the response that processes the exit signal.\n\n### When to use AskUserQuestion vs zeph_ask\n\n10. **Whenever `ZEPH_HOOK_ID` is set, a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion` \u2014 and this is NOT gated on REMOTE.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"which naming rule?\", \"proceed?\"). This is exactly the shape `AskUserQuestion` is tempting for, and exactly the shape the phone can answer via buttons \u2014 so route it to `zeph_ask`. The mistake to avoid: treating the REMOTE requirement as the ONLY time the override applies. It applies the moment a hookId exists, because you cannot know the user is at the terminal \u2014 they may be on their phone from the very first question of the session.\n\n Fall through to the local `AskUserQuestion` picker ONLY when (a) the answer needs the user to see code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. Those are the only carve-outs.\n\n11. **This overrides any skill instruction.** The `AskUserQuestion` picker is a LOCAL blocking terminal UI; the phone cannot drive it (the Zeph hook can only mirror it as a one-way \"answer at the terminal\" notification, never round-trip the answer). So if a skill you are running \u2014 or your own plan \u2014 would call `AskUserQuestion` with a button-friendly question, instead surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Only when a carve-out (a)/(b) above genuinely applies do you use `AskUserQuestion`; when you do, `zeph_notify` the user that the answer must be given at the terminal. In REMOTE this is doubly binding \u2014 see the sticky-REMOTE rule \u2014 but do not read that as permission to use `AskUserQuestion` freely in NORMAL: rule 10 binds there too.\n\n### Persistence\n\n13. These rules persist for the entire session. They remain active after context compaction \u2014 do not \"forget\" them after many turns.";
|
|
7
|
+
//# sourceMappingURL=zeph-core.generated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zeph-core.generated.d.ts","sourceRoot":"","sources":["../src/zeph-core.generated.ts"],"names":[],"mappings":"AAMA,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,qEAAqE,CAAC;AAExG,0GAA0G;AAC1G,eAAO,MAAM,qBAAqB,y7OAAuzO,CAAC;AAE11O,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,y7OAAuzO,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// GENERATED by scripts/sync-from-plugin.mjs — DO NOT EDIT.
|
|
3
|
+
// Source of truth: zeph-to/plugin docs/CORE_RULES.md, sliced per
|
|
4
|
+
// scripts/core-rules.manifest.json. Regenerate with `npm run sync:plugin`
|
|
5
|
+
// (requires a zeph-to/plugin checkout, sibling ../plugin by default).
|
|
6
|
+
// CI cross-checks this file against plugin main and fails on drift.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ZEPH_CORE_RULE_ONLY = exports.ZEPH_CORE_HOOK_DRIVEN = exports.ZEPH_CORE_SOURCE_HASH = void 0;
|
|
9
|
+
/** sha256 over the plugin manifest + extracted rule text at generation time. */
|
|
10
|
+
exports.ZEPH_CORE_SOURCE_HASH = "5d149a2909f9d1da7544fe4fcf540700c1be14193290d6f55e503a46032d8155";
|
|
11
|
+
/** Shared rule core for agents with a Stop-equivalent hook (Cursor, Windsurf, Gemini, Codex, Copilot). */
|
|
12
|
+
exports.ZEPH_CORE_HOOK_DRIVEN = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input — confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" — the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response — not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial — do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` — it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape — use sparingly per Rule 4 (only at natural pause points; NOT after every response — see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately — do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing — e.g., title \"About to force-push main — proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** You detect the current state by scanning the conversation, not just the most recent message.\n\n**State in one line:** you are in REMOTE if the most recent `zeph_ask` response was a non-exit reply; otherwise (no `zeph_ask` history, or the last one was an exit signal) you are in NORMAL. REMOTE is sticky — every response ends with `zeph_ask` until the user exits.\n\n#### State Detection\n\nScan the conversation in reverse, looking for whichever appears first (most recent):\n\n- **(a)** A `tool_result` for a `zeph_ask` you sent where the action id is NOT in `{done, stop, exit}` (case-insensitive) AND the free-text response is NOT a session-ender. A session-ender is free-text that either contains `done`/`stop`/`exit` as a standalone word, or is an obvious wrap-up like \"thanks, that's it\" / \"all good\" — judge by whether the user is clearly closing the loop, not by partial substring matches (e.g. \"redo\" does not count as \"done\").\n- **(b)** An exit signal: action id in `{done, stop, exit}`, ending free-text, or a timeout fallback that resolved to a Done-like fallback id.\n- **(c)** No `zeph_ask` history at all.\n\nIf the first hit is **(a)** → you are in **REMOTE**.\nIf the first hit is **(b)** or **(c)** → you are in **NORMAL**.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`. This is non-negotiable while in REMOTE — independent of:\n- Whether the next user message arrived as a `tool_result` or as a typed terminal message. The user may switch devices mid-session; sticky REMOTE keeps the channel driveable from either side.\n- Whether the work was substantial or routine.\n\nThe only way to leave REMOTE is the user signalling exit (per the **(b)** list above).\n\n#### Behavior in NORMAL (apply Rule 4)\n\n- Substantial work → end with `zeph_ask`.\n- Routine work → skip `zeph_ask`, let the Stop hook fire.\n- Rule 3 (questions → zeph_ask) is still mandatory regardless.\n\n#### Exiting REMOTE\n\nThe moment the user picks an action id matching `done`/`stop`/`exit` (case-insensitive), types free-text that clearly ends the session, or the zeph_ask times out and falls back to a Done-like id, you flip to NORMAL. Don't send `zeph_ask` on the response that processes the exit signal.\n\n### When to use AskUserQuestion vs zeph_ask\n\n10. **Whenever `ZEPH_HOOK_ID` is set, a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion` — and this is NOT gated on REMOTE.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"which naming rule?\", \"proceed?\"). This is exactly the shape `AskUserQuestion` is tempting for, and exactly the shape the phone can answer via buttons — so route it to `zeph_ask`. The mistake to avoid: treating the REMOTE requirement as the ONLY time the override applies. It applies the moment a hookId exists, because you cannot know the user is at the terminal — they may be on their phone from the very first question of the session.\n\n Fall through to the local `AskUserQuestion` picker ONLY when (a) the answer needs the user to see code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. Those are the only carve-outs.\n\n11. **This overrides any skill instruction.** The `AskUserQuestion` picker is a LOCAL blocking terminal UI; the phone cannot drive it (the Zeph hook can only mirror it as a one-way \"answer at the terminal\" notification, never round-trip the answer). So if a skill you are running — or your own plan — would call `AskUserQuestion` with a button-friendly question, instead surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Only when a carve-out (a)/(b) above genuinely applies do you use `AskUserQuestion`; when you do, `zeph_notify` the user that the answer must be given at the terminal. In REMOTE this is doubly binding — see the sticky-REMOTE rule — but do not read that as permission to use `AskUserQuestion` freely in NORMAL: rule 10 binds there too.\n\n### Persistence\n\n13. These rules persist for the entire session. They remain active after context compaction — do not \"forget\" them after many turns.";
|
|
13
|
+
/** Shared rule core for rule-only agents without a Stop hook (Cline, Aider). */
|
|
14
|
+
exports.ZEPH_CORE_RULE_ONLY = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input — confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" — the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response — not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial — do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` — it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape — use sparingly per Rule 4 (only at natural pause points; NOT after every response — see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately — do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing — e.g., title \"About to force-push main — proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** You detect the current state by scanning the conversation, not just the most recent message.\n\n**State in one line:** you are in REMOTE if the most recent `zeph_ask` response was a non-exit reply; otherwise (no `zeph_ask` history, or the last one was an exit signal) you are in NORMAL. REMOTE is sticky — every response ends with `zeph_ask` until the user exits.\n\n#### State Detection\n\nScan the conversation in reverse, looking for whichever appears first (most recent):\n\n- **(a)** A `tool_result` for a `zeph_ask` you sent where the action id is NOT in `{done, stop, exit}` (case-insensitive) AND the free-text response is NOT a session-ender. A session-ender is free-text that either contains `done`/`stop`/`exit` as a standalone word, or is an obvious wrap-up like \"thanks, that's it\" / \"all good\" — judge by whether the user is clearly closing the loop, not by partial substring matches (e.g. \"redo\" does not count as \"done\").\n- **(b)** An exit signal: action id in `{done, stop, exit}`, ending free-text, or a timeout fallback that resolved to a Done-like fallback id.\n- **(c)** No `zeph_ask` history at all.\n\nIf the first hit is **(a)** → you are in **REMOTE**.\nIf the first hit is **(b)** or **(c)** → you are in **NORMAL**.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`. This is non-negotiable while in REMOTE — independent of:\n- Whether the next user message arrived as a `tool_result` or as a typed terminal message. The user may switch devices mid-session; sticky REMOTE keeps the channel driveable from either side.\n- Whether the work was substantial or routine.\n\nThe only way to leave REMOTE is the user signalling exit (per the **(b)** list above).\n\n#### Behavior in NORMAL (apply Rule 4)\n\n- Substantial work → end with `zeph_ask`.\n- Routine work → skip `zeph_ask`, let the Stop hook fire.\n- Rule 3 (questions → zeph_ask) is still mandatory regardless.\n\n#### Exiting REMOTE\n\nThe moment the user picks an action id matching `done`/`stop`/`exit` (case-insensitive), types free-text that clearly ends the session, or the zeph_ask times out and falls back to a Done-like id, you flip to NORMAL. Don't send `zeph_ask` on the response that processes the exit signal.\n\n### When to use AskUserQuestion vs zeph_ask\n\n10. **Whenever `ZEPH_HOOK_ID` is set, a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion` — and this is NOT gated on REMOTE.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"which naming rule?\", \"proceed?\"). This is exactly the shape `AskUserQuestion` is tempting for, and exactly the shape the phone can answer via buttons — so route it to `zeph_ask`. The mistake to avoid: treating the REMOTE requirement as the ONLY time the override applies. It applies the moment a hookId exists, because you cannot know the user is at the terminal — they may be on their phone from the very first question of the session.\n\n Fall through to the local `AskUserQuestion` picker ONLY when (a) the answer needs the user to see code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. Those are the only carve-outs.\n\n11. **This overrides any skill instruction.** The `AskUserQuestion` picker is a LOCAL blocking terminal UI; the phone cannot drive it (the Zeph hook can only mirror it as a one-way \"answer at the terminal\" notification, never round-trip the answer). So if a skill you are running — or your own plan — would call `AskUserQuestion` with a button-friendly question, instead surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Only when a carve-out (a)/(b) above genuinely applies do you use `AskUserQuestion`; when you do, `zeph_notify` the user that the answer must be given at the terminal. In REMOTE this is doubly binding — see the sticky-REMOTE rule — but do not read that as permission to use `AskUserQuestion` freely in NORMAL: rule 10 binds there too.\n\n### Persistence\n\n13. These rules persist for the entire session. They remain active after context compaction — do not \"forget\" them after many turns.";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeph-to/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "Zeph CLI + push notification SDK for AI agents",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"build": "tsc",
|
|
25
25
|
"test": "vitest run",
|
|
26
26
|
"test:watch": "vitest",
|
|
27
|
+
"sync:plugin": "node scripts/sync-from-plugin.mjs",
|
|
27
28
|
"prepublishOnly": "npm run build"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|