@zeph-to/cli 2.8.2 → 2.10.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/README.md +17 -5
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +2 -0
- package/dist/cli.js +2 -2
- package/dist/gate.d.ts +9 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +10 -1
- package/dist/installer.d.ts +15 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +79 -17
- package/dist/remote-agents.d.ts +10 -0
- package/dist/remote-agents.d.ts.map +1 -1
- package/dist/remote-agents.js +12 -0
- package/dist/remote-hook.d.ts +1 -1
- package/dist/remote-hook.d.ts.map +1 -1
- package/dist/remote-hook.js +9 -7
- package/dist/templates.d.ts +8 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +229 -20
- package/dist/uninstall.d.ts +4 -0
- package/dist/uninstall.d.ts.map +1 -1
- package/dist/uninstall.js +18 -6
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +2 -0
- package/dist/zeph-core.generated.d.ts +4 -4
- package/dist/zeph-core.generated.d.ts.map +1 -1
- package/dist/zeph-core.generated.js +4 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -141,12 +141,13 @@ an answerable `zeph_ask`).
|
|
|
141
141
|
| Claude Code | `UserPromptSubmit` → plugin's `zeph-remote.sh` | Zeph plugin |
|
|
142
142
|
| Gemini CLI | `BeforeAgent` → `zeph remote-hook gemini` | `zeph setup` |
|
|
143
143
|
| Codex CLI | `UserPromptSubmit` → `zeph remote-hook codex` | `zeph setup` |
|
|
144
|
+
| Pi | `before_agent_start` → `zeph remote-hook pi` (via extension) | `zeph setup` |
|
|
144
145
|
| Cursor CLI | — none yet | — |
|
|
145
146
|
|
|
146
147
|
Detection is exact-match: a terminal keystroke racing a phone message
|
|
147
148
|
can never false-flag. Muted projects are never flagged.
|
|
148
149
|
|
|
149
|
-
Agents without that hook (Cursor, Windsurf, Copilot, Cline, Aider) have one
|
|
150
|
+
Agents without that hook (Cursor, Windsurf, Copilot, Cline, Aider, OpenCode) have one
|
|
150
151
|
other way in: a `zeph_ask` answer that is not a Done-like button. Their rule
|
|
151
152
|
files therefore keep the after-real-work `zeph_ask` — the "Entering REMOTE
|
|
152
153
|
without a prompt hook" preamble in `src/templates.ts` — so the phone always
|
|
@@ -218,6 +219,8 @@ block here.
|
|
|
218
219
|
zeph cursor # cursor-agent → tmux session "zeph-<project>"
|
|
219
220
|
zeph gemini # gemini → tmux session "zeph-<project>"
|
|
220
221
|
zeph hermes # hermes → tmux session "zeph-<project>"
|
|
222
|
+
zeph pi # pi → tmux session "zeph-<project>"
|
|
223
|
+
zeph opencode # opencode → tmux session "zeph-<project>"
|
|
221
224
|
```
|
|
222
225
|
|
|
223
226
|
`zeph cursor` runs **`cursor-agent`**, Cursor's terminal agent — a
|
|
@@ -602,10 +605,17 @@ this order — first hit wins:
|
|
|
602
605
|
It is now `quiet`, so upgrading turns the routine per-turn push off until
|
|
603
606
|
you run `/zeph-normal`. Row 4 is why the hooks this CLI installs are
|
|
604
607
|
unaffected: they name `normal` themselves, since a hook that supplies no
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
608
|
+
`high` marker would be permanently silent under `quiet` rather than
|
|
609
|
+
merely quieter. Row 4 sits *below* the state files on purpose — the flag
|
|
610
|
+
names a default, it does not override a dial the user set.
|
|
611
|
+
|
|
612
|
+
Under `normal` the JSON hook configs push on every turn, because they see
|
|
613
|
+
no per-tool events and so pass no counts — the gate then assumes real
|
|
614
|
+
work. The Pi extension and the OpenCode plugin do see those events, so
|
|
615
|
+
they pass real `--tools` / `--nonreadonly` counts and go quiet where the
|
|
616
|
+
others cannot: a turn with no tool calls, a turn with exactly one (a
|
|
617
|
+
lone edit included — the gate wants two), and a turn whose calls were
|
|
618
|
+
all reads. `/zeph-loud` still pushes on all of them.
|
|
609
619
|
|
|
610
620
|
A dial file that exists but reads empty resolves to `normal`, not to row
|
|
611
621
|
5: an empty file is a failed write, and resolving breakage to silence
|
|
@@ -712,6 +722,8 @@ try {
|
|
|
712
722
|
| Copilot CLI | Session end hook + rules |
|
|
713
723
|
| Cline | Rules file (`~/.cline/rules/zeph.md`) |
|
|
714
724
|
| Aider | Conventions file + `read:` directive in `~/.aider.conf.yml` |
|
|
725
|
+
| Pi | Extension (settle hook + remote detection) + rules — no MCP (pi has none; rules map zeph tools to the CLI) |
|
|
726
|
+
| OpenCode | MCP server + idle-notify plugin + rules |
|
|
715
727
|
|
|
716
728
|
For remote-control via `zeph listener` the per-agent setup is the same
|
|
717
729
|
across CC/Codex/Gemini — the wrapper just spawns them in a named tmux
|
package/dist/agents.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,KAAK;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;CACrB;AAID,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,KAAG,OAOxC,CAAC;AAEF,eAAO,MAAM,YAAY,QAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,KAAK;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;CACrB;AAID,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,KAAG,OAOxC,CAAC;AAEF,eAAO,MAAM,YAAY,QAAO,KAAK,EAWpC,CAAC"}
|
package/dist/agents.js
CHANGED
|
@@ -25,5 +25,7 @@ const detectAgents = () => [
|
|
|
25
25
|
{ name: 'Copilot CLI', id: 'copilot', detected: (0, fs_1.existsSync)((0, path_1.join)(HOME, '.copilot')) },
|
|
26
26
|
{ name: 'Cline', id: 'cline', detected: (0, fs_1.existsSync)((0, path_1.join)(HOME, '.cline')) },
|
|
27
27
|
{ name: 'Aider', id: 'aider', detected: (0, exports.hasCommand)('aider') },
|
|
28
|
+
{ name: 'Pi', id: 'pi', detected: (0, fs_1.existsSync)((0, path_1.join)(HOME, '.pi')) || (0, exports.hasCommand)('pi') },
|
|
29
|
+
{ name: 'OpenCode', id: 'opencode', detected: (0, fs_1.existsSync)((0, path_1.join)(HOME, '.config', 'opencode')) || (0, exports.hasCommand)('opencode') },
|
|
28
30
|
];
|
|
29
31
|
exports.detectAgents = detectAgents;
|
package/dist/cli.js
CHANGED
|
@@ -279,8 +279,8 @@ const handleNotify = async (args) => {
|
|
|
279
279
|
// With no dial the mode falls back to --pushmode-default, then to quiet.
|
|
280
280
|
if (args.auto === true) {
|
|
281
281
|
const verdict = (0, gate_js_1.decidePush)({
|
|
282
|
-
toolCount: gateCount(args.
|
|
283
|
-
nonReadonlyCount: gateCount(args.
|
|
282
|
+
toolCount: gateCount(args[gate_js_1.TOOL_COUNT_FLAG], gate_js_1.GATE_DEFAULTS.toolCount),
|
|
283
|
+
nonReadonlyCount: gateCount(args[gate_js_1.NONREADONLY_COUNT_FLAG], gate_js_1.GATE_DEFAULTS.nonReadonlyCount),
|
|
284
284
|
alreadyAsked: gate_js_1.GATE_DEFAULTS.alreadyAsked,
|
|
285
285
|
marker: (0, gate_js_1.normalizeMarker)(typeof args.marker === 'string' ? args.marker : undefined),
|
|
286
286
|
pushMode: (0, gate_js_1.autoPushMode)(projectDir, args[gate_js_1.PUSHMODE_DEFAULT_FLAG]),
|
package/dist/gate.d.ts
CHANGED
|
@@ -88,6 +88,15 @@ export declare const PUSHMODE_DEFAULT: GatePushMode;
|
|
|
88
88
|
* read back in cli.ts — shared so the two can never drift apart.
|
|
89
89
|
*/
|
|
90
90
|
export declare const PUSHMODE_DEFAULT_FLAG = "pushmode-default";
|
|
91
|
+
/**
|
|
92
|
+
* `notify` flags carrying the turn's tool counts. Only the drop-in artifacts
|
|
93
|
+
* (pi extension, opencode plugin) can supply them — they are the only installed
|
|
94
|
+
* hooks that see per-tool events. Shared for the same reason as the flag above:
|
|
95
|
+
* templates.ts writes them, cli.ts reads them, and a rename on one side alone
|
|
96
|
+
* would send `gateCount` back to GATE_DEFAULTS with nothing to show for it.
|
|
97
|
+
*/
|
|
98
|
+
export declare const TOOL_COUNT_FLAG = "tools";
|
|
99
|
+
export declare const NONREADONLY_COUNT_FLAG = "nonreadonly";
|
|
91
100
|
/**
|
|
92
101
|
* The user's session push-mode dial (/zeph-quiet | /zeph-loud | /zeph-normal).
|
|
93
102
|
*
|
package/dist/gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAyBA,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;;;;;;;;;;GAUG;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;AAeF,eAAO,MAAM,QAAQ,QAAO,MACoD,CAAC;AA2BjF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAOlD,CAAC;AAWF,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MAA4C,CAAC;AAE7F;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MAG1B,CAAC;AAiBnB;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,QAAQ,CAAC;AAEpC,8EAA8E;AAC9E,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MACJ,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,EAAE,MAAK,MAAM,MAAiB,KAAG,OAsB1E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,EAAE,MAAK,MAAM,MAAiB,KAAG,IAS7E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,IAQ/C,CAAC;AAEF,0DAA0D;AAC1D,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,OAGrC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,YAAsB,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,GACvB,KAAK,MAAM,EACX,WAAU,YAA+B,KACxC,YAUF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,EAAE,MAAM,MAAM,GAAG,OAAO,GAAG,SAAS,KAAG,YACW,CAAC"}
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAyBA,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;;;;;;;;;;GAUG;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;AAeF,eAAO,MAAM,QAAQ,QAAO,MACoD,CAAC;AA2BjF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAOlD,CAAC;AAWF,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MAA4C,CAAC;AAE7F;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MAG1B,CAAC;AAiBnB;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,QAAQ,CAAC;AAEpC,8EAA8E;AAC9E,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MACJ,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,EAAE,MAAK,MAAM,MAAiB,KAAG,OAsB1E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,EAAE,MAAK,MAAM,MAAiB,KAAG,IAS7E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,IAQ/C,CAAC;AAEF,0DAA0D;AAC1D,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,OAGrC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,YAAsB,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,UAAU,CAAC;AACvC,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,GACvB,KAAK,MAAM,EACX,WAAU,YAA+B,KACxC,YAUF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,EAAE,MAAM,MAAM,GAAG,OAAO,GAAG,SAAS,KAAG,YACW,CAAC"}
|
package/dist/gate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.autoPushMode = exports.readPushMode = exports.PUSHMODE_DEFAULT_FLAG = exports.PUSHMODE_DEFAULT = exports.isMuted = exports.clearRemoteActive = exports.touchRemoteActive = exports.isRemoteActive = exports.remoteStatePath = exports.REMOTE_TTL_SEC = exports.remoteDigest = exports.remoteMarkerPath = exports.projectHash = exports.stateDir = exports.decidePush = exports.normalizePushMode = exports.normalizeMarker = exports.GATE_DEFAULTS = void 0;
|
|
3
|
+
exports.autoPushMode = exports.readPushMode = exports.NONREADONLY_COUNT_FLAG = exports.TOOL_COUNT_FLAG = exports.PUSHMODE_DEFAULT_FLAG = exports.PUSHMODE_DEFAULT = exports.isMuted = exports.clearRemoteActive = exports.touchRemoteActive = exports.isRemoteActive = exports.remoteStatePath = exports.REMOTE_TTL_SEC = exports.remoteDigest = exports.remoteMarkerPath = exports.projectHash = exports.stateDir = exports.decidePush = exports.normalizePushMode = exports.normalizeMarker = exports.GATE_DEFAULTS = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Push-gate decision — the portable half of the Zeph Stop-hook logic.
|
|
6
6
|
*
|
|
@@ -244,6 +244,15 @@ exports.PUSHMODE_DEFAULT = 'quiet';
|
|
|
244
244
|
* read back in cli.ts — shared so the two can never drift apart.
|
|
245
245
|
*/
|
|
246
246
|
exports.PUSHMODE_DEFAULT_FLAG = 'pushmode-default';
|
|
247
|
+
/**
|
|
248
|
+
* `notify` flags carrying the turn's tool counts. Only the drop-in artifacts
|
|
249
|
+
* (pi extension, opencode plugin) can supply them — they are the only installed
|
|
250
|
+
* hooks that see per-tool events. Shared for the same reason as the flag above:
|
|
251
|
+
* templates.ts writes them, cli.ts reads them, and a rename on one side alone
|
|
252
|
+
* would send `gateCount` back to GATE_DEFAULTS with nothing to show for it.
|
|
253
|
+
*/
|
|
254
|
+
exports.TOOL_COUNT_FLAG = 'tools';
|
|
255
|
+
exports.NONREADONLY_COUNT_FLAG = 'nonreadonly';
|
|
247
256
|
/**
|
|
248
257
|
* The user's session push-mode dial (/zeph-quiet | /zeph-loud | /zeph-normal).
|
|
249
258
|
*
|
package/dist/installer.d.ts
CHANGED
|
@@ -12,6 +12,21 @@ export declare const shouldReauth: (currentKey: string | undefined, relogin: boo
|
|
|
12
12
|
/** Exported for tests (installGemini shells out to the real `gemini` binary,
|
|
13
13
|
* so the merge semantics can't be covered through it deterministically). */
|
|
14
14
|
export declare const mergeJsonFile: (filePath: string, patch: Record<string, unknown>) => void;
|
|
15
|
+
/**
|
|
16
|
+
* opencode.json's own MCP schema — top-level `mcp` key (not `mcpServers`),
|
|
17
|
+
* `command` as ARRAY (binary + args), `type: "local"`. No `environment` —
|
|
18
|
+
* opencode is a terminal app that inherits shell env, and the MCP server
|
|
19
|
+
* falls back to ~/.zeph/config.json anyway (an unexpanded "${ZEPH_API_KEY}"
|
|
20
|
+
* literal would shadow that fallback with junk). Exported for tests (shape pin).
|
|
21
|
+
*/
|
|
22
|
+
export declare const OPENCODE_MCP_ENTRY: {
|
|
23
|
+
type: string;
|
|
24
|
+
command: string[];
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** Add the zeph entry under `key` in an MCP registry JSON file, preserving
|
|
28
|
+
* everything else in it. Exported for tests (merge semantics). */
|
|
29
|
+
export declare const injectMcpEntry: (filePath: string, key: string, entry: Record<string, unknown>) => void;
|
|
15
30
|
/**
|
|
16
31
|
* Resolve agents from a non-interactive `--only cursor,gemini` flag.
|
|
17
32
|
* Matches on agent id; unknown ids are silently dropped. Exported for
|
package/dist/installer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AA+BzC;;;GAGG;AACH;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,YAAY,MAAM,GAAG,SAAS,EAAE,SAAS,OAAO,KAAG,OACxD,CAAC;AAoBzB;6EAC6E;AAC7E,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAqBhF,CAAC;AAgDF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;;CAI9B,CAAC;AAEF;mEACmE;AACnE,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,EAAE,KAAK,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAQ9F,CAAC;AAqPF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,KAAK,EAAE,EAAE,MAAM,MAAM,KAAG,KAAK,EAKxE,CAAC;AA8EF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAC/B,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EACtC,gBAAgB,OAAO,KACtB,KAAK,GAAG,IAAI,GAAG,KAKjB,CAAC;AAgCF,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAuH1F,CAAC"}
|
package/dist/installer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.handleInstall = exports.serviceInstallChoice = exports.filterAgentsByIds = exports.mergeJsonFile = exports.shouldReauth = void 0;
|
|
3
|
+
exports.handleInstall = exports.serviceInstallChoice = exports.filterAgentsByIds = exports.injectMcpEntry = exports.OPENCODE_MCP_ENTRY = exports.mergeJsonFile = exports.shouldReauth = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const os_1 = require("os");
|
|
@@ -104,26 +104,41 @@ const addAiderReadDirective = (confPath, conventionsPath) => {
|
|
|
104
104
|
(0, fs_1.writeFileSync)(confPath, (base ? `${base}\n\n` : '') + line);
|
|
105
105
|
};
|
|
106
106
|
// ── Per-Agent Installers ─────────────────────────────────────────
|
|
107
|
-
|
|
107
|
+
// Pass through env explicitly so the MCP server doesn't have to rely on
|
|
108
|
+
// process-env inheritance (which behaves differently per IDE — Cursor and
|
|
109
|
+
// Windsurf spawn the MCP from a graphical context that may not inherit
|
|
110
|
+
// shell env). Mirrors plugin/.mcp.json.
|
|
111
|
+
const MCP_SERVERS_ENTRY = {
|
|
112
|
+
command: 'npx',
|
|
113
|
+
args: ['-y', '@zeph-to/mcp-server'],
|
|
114
|
+
env: { ZEPH_API_KEY: '${ZEPH_API_KEY}' },
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* opencode.json's own MCP schema — top-level `mcp` key (not `mcpServers`),
|
|
118
|
+
* `command` as ARRAY (binary + args), `type: "local"`. No `environment` —
|
|
119
|
+
* opencode is a terminal app that inherits shell env, and the MCP server
|
|
120
|
+
* falls back to ~/.zeph/config.json anyway (an unexpanded "${ZEPH_API_KEY}"
|
|
121
|
+
* literal would shadow that fallback with junk). Exported for tests (shape pin).
|
|
122
|
+
*/
|
|
123
|
+
exports.OPENCODE_MCP_ENTRY = {
|
|
124
|
+
type: 'local',
|
|
125
|
+
command: ['npx', '-y', '@zeph-to/mcp-server'],
|
|
126
|
+
enabled: true,
|
|
127
|
+
};
|
|
128
|
+
/** Add the zeph entry under `key` in an MCP registry JSON file, preserving
|
|
129
|
+
* everything else in it. Exported for tests (merge semantics). */
|
|
130
|
+
const injectMcpEntry = (filePath, key, entry) => {
|
|
108
131
|
let data = {};
|
|
109
132
|
try {
|
|
110
133
|
data = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
|
|
111
134
|
}
|
|
112
135
|
catch { /* new file */ }
|
|
113
|
-
if (!data
|
|
114
|
-
data
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Windsurf spawn the MCP from a graphical context that may not inherit
|
|
118
|
-
// shell env). Mirrors plugin/.mcp.json.
|
|
119
|
-
data.mcpServers.zeph = {
|
|
120
|
-
command: 'npx',
|
|
121
|
-
args: ['-y', '@zeph-to/mcp-server'],
|
|
122
|
-
env: { ZEPH_API_KEY: '${ZEPH_API_KEY}' },
|
|
123
|
-
};
|
|
124
|
-
(0, fs_1.mkdirSync)((0, path_1.dirname)(filePath), { recursive: true });
|
|
125
|
-
(0, fs_1.writeFileSync)(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
136
|
+
if (!isPlainObject(data[key]))
|
|
137
|
+
data[key] = {};
|
|
138
|
+
data[key].zeph = entry;
|
|
139
|
+
writeFile(filePath, JSON.stringify(data, null, 2));
|
|
126
140
|
};
|
|
141
|
+
exports.injectMcpEntry = injectMcpEntry;
|
|
127
142
|
const installClaude = () => {
|
|
128
143
|
try {
|
|
129
144
|
(0, child_process_1.execSync)('claude plugin marketplace add zeph-to/plugin', { stdio: 'pipe' });
|
|
@@ -138,7 +153,7 @@ const installClaude = () => {
|
|
|
138
153
|
};
|
|
139
154
|
const installCursor = () => {
|
|
140
155
|
try {
|
|
141
|
-
|
|
156
|
+
(0, exports.injectMcpEntry)((0, path_1.join)(HOME, '.cursor', 'mcp.json'), 'mcpServers', MCP_SERVERS_ENTRY);
|
|
142
157
|
ok('MCP server added');
|
|
143
158
|
}
|
|
144
159
|
catch {
|
|
@@ -161,7 +176,7 @@ const installCursor = () => {
|
|
|
161
176
|
};
|
|
162
177
|
const installWindsurf = () => {
|
|
163
178
|
try {
|
|
164
|
-
|
|
179
|
+
(0, exports.injectMcpEntry)((0, path_1.join)(HOME, '.codeium', 'windsurf', 'mcp_config.json'), 'mcpServers', MCP_SERVERS_ENTRY);
|
|
165
180
|
ok('MCP server added');
|
|
166
181
|
}
|
|
167
182
|
catch {
|
|
@@ -283,6 +298,49 @@ const installAider = () => {
|
|
|
283
298
|
fail(`Config wiring failed. Manual: add "read: ${conventionsPath}" to ~/.aider.conf.yml`);
|
|
284
299
|
}
|
|
285
300
|
};
|
|
301
|
+
const installPi = () => {
|
|
302
|
+
try {
|
|
303
|
+
// Drop-in auto-load; full-file overwrite is the idempotency model
|
|
304
|
+
// (the header comment is the ownership marker). No MCP — pi has none;
|
|
305
|
+
// PI_RULE maps the zeph_* tools to the CLI instead.
|
|
306
|
+
writeFile((0, path_1.join)(HOME, '.pi', 'agent', 'extensions', 'zeph.ts'), templates_js_1.PI_EXTENSION);
|
|
307
|
+
ok('Extension added (settle notify + remote detection)');
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
fail('Extension install failed. Manual: save the zeph extension to ~/.pi/agent/extensions/zeph.ts');
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
// Pi loads ~/.pi/agent/AGENTS.md as global context.
|
|
314
|
+
writeManagedRule((0, path_1.join)(HOME, '.pi', 'agent', 'AGENTS.md'), templates_js_1.PI_RULE);
|
|
315
|
+
ok('Rules added to AGENTS.md');
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
fail('Rule install failed. Manual: add zeph rules to ~/.pi/agent/AGENTS.md');
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const installOpencode = () => {
|
|
322
|
+
try {
|
|
323
|
+
(0, exports.injectMcpEntry)((0, path_1.join)(HOME, '.config', 'opencode', 'opencode.json'), 'mcp', exports.OPENCODE_MCP_ENTRY);
|
|
324
|
+
ok('MCP server added');
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
fail('MCP injection failed. Manual: add zeph to ~/.config/opencode/opencode.json');
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
writeFile((0, path_1.join)(HOME, '.config', 'opencode', 'plugins', 'zeph.ts'), templates_js_1.OPENCODE_PLUGIN);
|
|
331
|
+
ok('Idle-notify plugin added');
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
fail('Plugin install failed. Manual: save the zeph plugin to ~/.config/opencode/plugins/zeph.ts');
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
writeManagedRule((0, path_1.join)(HOME, '.config', 'opencode', 'AGENTS.md'), templates_js_1.OPENCODE_RULE);
|
|
338
|
+
ok('Rules added to AGENTS.md');
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
fail('Rule install failed. Manual: add zeph rules to ~/.config/opencode/AGENTS.md');
|
|
342
|
+
}
|
|
343
|
+
};
|
|
286
344
|
const AGENT_INSTALLERS = {
|
|
287
345
|
claude: installClaude,
|
|
288
346
|
cursor: installCursor,
|
|
@@ -292,6 +350,8 @@ const AGENT_INSTALLERS = {
|
|
|
292
350
|
copilot: installCopilot,
|
|
293
351
|
cline: installCline,
|
|
294
352
|
aider: installAider,
|
|
353
|
+
pi: installPi,
|
|
354
|
+
opencode: installOpencode,
|
|
295
355
|
};
|
|
296
356
|
// One-line summary of what each agent's installer does — shown in the
|
|
297
357
|
// interactive plan before anything is written.
|
|
@@ -304,6 +364,8 @@ const AGENT_PLAN_LABELS = {
|
|
|
304
364
|
copilot: 'Copilot CLI — hooks + rules',
|
|
305
365
|
cline: 'Cline — rules',
|
|
306
366
|
aider: 'Aider — conventions',
|
|
367
|
+
pi: 'Pi — extension + rules',
|
|
368
|
+
opencode: 'OpenCode — MCP + plugin + rules',
|
|
307
369
|
};
|
|
308
370
|
// ── Agent selection ──────────────────────────────────────────────
|
|
309
371
|
/**
|
package/dist/remote-agents.d.ts
CHANGED
|
@@ -186,6 +186,16 @@ declare const REMOTE_AGENT_TABLE: readonly [{
|
|
|
186
186
|
readonly binary: "hermes";
|
|
187
187
|
readonly subcommands: readonly ["hermes"];
|
|
188
188
|
readonly resolveSessionName: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) => string | null;
|
|
189
|
+
}, {
|
|
190
|
+
readonly kind: "pi";
|
|
191
|
+
readonly displayName: "Pi";
|
|
192
|
+
readonly binary: "pi";
|
|
193
|
+
readonly subcommands: readonly ["pi"];
|
|
194
|
+
}, {
|
|
195
|
+
readonly kind: "opencode";
|
|
196
|
+
readonly displayName: "OpenCode";
|
|
197
|
+
readonly binary: "opencode";
|
|
198
|
+
readonly subcommands: readonly ["opencode"];
|
|
189
199
|
}];
|
|
190
200
|
/** Closed union of remote-controllable agent kinds, derived from the table above. */
|
|
191
201
|
export type AgentKind = (typeof REMOTE_AGENT_TABLE)[number]['kind'];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-agents.d.ts","sourceRoot":"","sources":["../src/remote-agents.ts"],"names":[],"mappings":"AAoBA,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;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACxE;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAC7E;AAgFD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAI5D,CAAC;AAaF,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AA4CD,MAAM,WAAW,SAAS;IACtB,qEAAqE;IACrE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC,6EAA6E;IAC7E,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,KAAG,SAe9C,CAAC;AAaF,6EAA6E;AAC7E,eAAO,MAAM,YAAY,QAAO,GAAG,CAAC,MAAM,EAAE,MAAM,CAA8C,CAAC;AA6BjG,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,GAAI,SAAS,MAAM,KAAG,GAAG,CAAC,MAAM,CASjE,CAAC;AAEF,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC3B,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,GACjC,SAAS,MAAM,EACf,SAAS,MAAM,GAAG,IAAI,EACtB,OAAM,cAAmB,KAC1B,gBAAgB,GAAG,IAMrB,CAAC;AAEF,eAAO,MAAM,0BAA0B,GACnC,SAAS,MAAM,EACf,SAAS,MAAM,GAAG,IAAI,EACtB,OAAM,cAAmB,KAC1B,MAAM,GAAG,IAA2E,CAAC;AAExF;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,GACrC,SAAS,MAAM,EACf,SAAS,MAAM,GAAG,IAAI,EACtB,OAAM,cAAmB,KAC1B,MAAM,GAAG,IAAwE,CAAC;AAerF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAChC,MAAM,SAAS,CAAC,EAAE,EAClB,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,EACjC,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,MAAM,GAAG,CAAC,MAAM,CAAC,KAClB,CAAC,GAAG,IAeN,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,EAAE,KAAK,MAAM,KAAG,OAAO,EAAE,GAAG,IAepE,CAAC;AAEF,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC7B,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACxB,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC7B;AA2BD,eAAO,MAAM,uBAAuB,GAChC,SAAS,MAAM,EACf,UAAU,MAAM,EAChB,OAAM,gBAAqB,KAC5B,MAAM,GAAG,IAkBX,CAAC;AAOF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,SAAS,MAAM,EAAE,EAAE,SAAS,MAAM,KAAG,MAAM,GAAG,IASxF,CAAC;AAmCF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GAC/B,SAAS,MAAM,EACf,UAAU,MAAM,EAChB,OAAM,gBAAqB,KAC5B,MAAM,GAAG,IAcX,CAAC;AAIF,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;2CArBX,MAAM,YACL,MAAM,SACV,gBAAgB,KACvB,MAAM,GAAG,IAAI;;;;;;;;;;;;;;;;2CA7FH,MAAM,YACL,MAAM,SACV,gBAAgB,KACvB,MAAM,GAAG,IAAI
|
|
1
|
+
{"version":3,"file":"remote-agents.d.ts","sourceRoot":"","sources":["../src/remote-agents.ts"],"names":[],"mappings":"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"}
|
package/dist/remote-agents.js
CHANGED
|
@@ -478,6 +478,18 @@ const REMOTE_AGENT_TABLE = [
|
|
|
478
478
|
// `<timestamp>_<hash>` string, and nothing on the wire consumes it yet.
|
|
479
479
|
resolveSessionName: exports.detectHermesSessionName,
|
|
480
480
|
},
|
|
481
|
+
{
|
|
482
|
+
kind: 'pi',
|
|
483
|
+
displayName: 'Pi',
|
|
484
|
+
binary: 'pi',
|
|
485
|
+
subcommands: ['pi'],
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
kind: 'opencode',
|
|
489
|
+
displayName: 'OpenCode',
|
|
490
|
+
binary: 'opencode',
|
|
491
|
+
subcommands: ['opencode'],
|
|
492
|
+
},
|
|
481
493
|
];
|
|
482
494
|
exports.REMOTE_AGENTS = REMOTE_AGENT_TABLE;
|
|
483
495
|
const findAgentBySubcommand = (cmd) => exports.REMOTE_AGENTS.find((a) => a.subcommands.includes(cmd));
|
package/dist/remote-hook.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Agents whose prompt-submit hooks this command serves. Claude Code is
|
|
2
2
|
* NOT here — its hook ships with the plugin (hooks/zeph-remote.sh). */
|
|
3
|
-
export declare const REMOTE_HOOK_AGENTS: readonly ["gemini", "codex"];
|
|
3
|
+
export declare const REMOTE_HOOK_AGENTS: readonly ["gemini", "codex", "pi"];
|
|
4
4
|
export type RemoteHookAgent = (typeof REMOTE_HOOK_AGENTS)[number];
|
|
5
5
|
export declare const isRemoteHookAgent: (raw: string) => raw is RemoteHookAgent;
|
|
6
6
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-hook.d.ts","sourceRoot":"","sources":["../src/remote-hook.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"remote-hook.d.ts","sourceRoot":"","sources":["../src/remote-hook.ts"],"names":[],"mappings":"AA0CA;wEACwE;AACxE,eAAO,MAAM,kBAAkB,oCAAqC,CAAC;AACrE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAqClE,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,GAAG,IAAI,eACE,CAAC;AAE1D;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,OAAO,eAAe,EACtB,OAAO,MAAM,EACb,MAAK,MAAM,CAAC,UAAwB,EACpC,MAAK,MAAM,MAAiB,KAC3B,MAAM,GAAG,IA0CX,CAAC"}
|
package/dist/remote-hook.js
CHANGED
|
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runRemoteHook = exports.isRemoteHookAgent = exports.REMOTE_HOOK_AGENTS = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* `zeph remote-hook <agent>` — prompt-submit hook handler for agents whose
|
|
6
|
-
* hooks the cli installs directly: Gemini CLI (BeforeAgent)
|
|
7
|
-
* (UserPromptSubmit)
|
|
6
|
+
* hooks the cli installs directly: Gemini CLI (BeforeAgent), Codex CLI
|
|
7
|
+
* (UserPromptSubmit), and Pi (before_agent_start, consumed by the zeph
|
|
8
|
+
* extension itself). TS twin of the Claude Code plugin's
|
|
8
9
|
* hooks/zeph-remote.sh (ADR-0002).
|
|
9
10
|
*
|
|
10
11
|
* The listener records every phone→pane text injection as a one-shot
|
|
@@ -13,7 +14,7 @@ exports.runRemoteHook = exports.isRemoteHookAgent = exports.REMOTE_HOOK_AGENTS =
|
|
|
13
14
|
* record — same project (cksum of the cwd), fresh (≤15 min), and
|
|
14
15
|
* byte-identical trimmed text — the hook emits additionalContext telling
|
|
15
16
|
* the model the user is driving the session from their phone, which enters
|
|
16
|
-
* sticky REMOTE mode
|
|
17
|
+
* sticky REMOTE mode.
|
|
17
18
|
*
|
|
18
19
|
* Detection is exact: a terminal keystroke racing a phone message can
|
|
19
20
|
* never false-match. Parity with the writer (listener.ts writeRemoteMarker)
|
|
@@ -35,12 +36,13 @@ const config_js_1 = require("./config.js");
|
|
|
35
36
|
const gate_js_1 = require("./gate.js");
|
|
36
37
|
/** Agents whose prompt-submit hooks this command serves. Claude Code is
|
|
37
38
|
* NOT here — its hook ships with the plugin (hooks/zeph-remote.sh). */
|
|
38
|
-
exports.REMOTE_HOOK_AGENTS = ['gemini', 'codex'];
|
|
39
|
+
exports.REMOTE_HOOK_AGENTS = ['gemini', 'codex', 'pi'];
|
|
39
40
|
/** Per-agent hookEventName echoed back in hookSpecificOutput — each CLI
|
|
40
41
|
* validates it against the event that invoked the hook. */
|
|
41
42
|
const HOOK_EVENT_NAME = {
|
|
42
43
|
gemini: 'BeforeAgent',
|
|
43
44
|
codex: 'UserPromptSubmit',
|
|
45
|
+
pi: 'before_agent_start',
|
|
44
46
|
};
|
|
45
47
|
/**
|
|
46
48
|
* Freshness window (seconds), same as the bash reader: deliberately
|
|
@@ -52,7 +54,7 @@ const HOOK_EVENT_NAME = {
|
|
|
52
54
|
const FRESH_WINDOW_SEC = 900;
|
|
53
55
|
const TWO_WAY_CONTEXT = `# System note (Zeph remote-origin detect)
|
|
54
56
|
|
|
55
|
-
This user message arrived from the user's phone via Zeph agent chat (verified by the listener — exact text match). The user is driving this session remotely and is NOT at the terminal. Enter sticky REMOTE mode now (
|
|
57
|
+
This user message arrived from the user's phone via Zeph agent chat (verified by the listener — exact text match). The user is driving this session remotely and is NOT at the terminal. Enter sticky REMOTE mode now (the "Sticky REMOTE mode" rule in your Zeph rules): end EVERY response with \`zeph_ask\` (buttons + free-text) until the user exits — an exit signal (done/stop/exit), or a prompt they type at the terminal, which this hook will tell you about. Plain-text questions are invisible to them.`;
|
|
56
58
|
const ONE_WAY_CONTEXT = `# System note (Zeph remote-origin detect)
|
|
57
59
|
|
|
58
60
|
This user message arrived from the user's phone via Zeph agent chat (verified by the listener — exact text match), but no hook id is configured (neither \`ZEPH_HOOK_ID\` nor \`hookId\` in ~/.zeph/config.json), so two-way tools (zeph_ask/zeph_prompt/zeph_input) are unavailable. Make your final message self-contained — the completion push is the user's only feedback channel. If you have not already mentioned it this session, tell the user once that running \`npx @zeph-to/cli setup\` upgrades this into a two-way remote session (buttons + text replies from the phone).`;
|
|
@@ -64,7 +66,7 @@ This user message arrived from the user's phone via Zeph agent chat (verified by
|
|
|
64
66
|
*/
|
|
65
67
|
const EXIT_CONTEXT = `# System note (Zeph)
|
|
66
68
|
|
|
67
|
-
The user typed this prompt at the terminal, so this session has LEFT sticky REMOTE mode — answer normally (
|
|
69
|
+
The user typed this prompt at the terminal, so this session has LEFT sticky REMOTE mode — answer normally (the NORMAL branch of your Zeph rules) and do not end this response with \`zeph_ask\` just to keep the loop alive. One rule still holds: if you actually ask the user something, ask it with \`zeph_ask\`. Re-entry is automatic the moment they send another message from their phone.`;
|
|
68
70
|
const isRemoteHookAgent = (raw) => exports.REMOTE_HOOK_AGENTS.includes(raw);
|
|
69
71
|
exports.isRemoteHookAgent = isRemoteHookAgent;
|
|
70
72
|
/**
|
|
@@ -85,7 +87,7 @@ const runRemoteHook = (agent, stdin, env = process.env, now = Date.now) => {
|
|
|
85
87
|
// `cwd` keys every state file, so without it there is nothing to look up.
|
|
86
88
|
if (!cwd)
|
|
87
89
|
return null;
|
|
88
|
-
// Mute outranks everything (
|
|
90
|
+
// Mute outranks everything (the mute rule) — stay silent and leave both the marker
|
|
89
91
|
// and the state untouched (the next inject overwrites the marker anyway).
|
|
90
92
|
if ((0, gate_js_1.isMuted)(cwd))
|
|
91
93
|
return null;
|
package/dist/templates.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ export declare const COPILOT_RULE: string;
|
|
|
12
12
|
export declare const CLINE_RULE: string;
|
|
13
13
|
/** Aider — standalone conventions file via .aider.conf.yml `read:` (no Stop hook, no prompt hook). */
|
|
14
14
|
export declare const AIDER_RULE: string;
|
|
15
|
+
/** Pi — managed block in ~/.pi/agent/AGENTS.md. Extension = Stop-equivalent + prompt hook (PI_EXTENSION). */
|
|
16
|
+
export declare const PI_RULE: string;
|
|
17
|
+
/** OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook (v1). */
|
|
18
|
+
export declare const OPENCODE_RULE: string;
|
|
15
19
|
export declare const CURSOR_HOOKS: string;
|
|
16
20
|
export declare const WINDSURF_HOOKS: string;
|
|
17
21
|
export declare const GEMINI_HOOKS: {
|
|
@@ -65,6 +69,10 @@ export declare const CODEX_HOOKS: {
|
|
|
65
69
|
*/
|
|
66
70
|
export declare const isZephHookGroup: (group: unknown) => boolean;
|
|
67
71
|
export declare const COPILOT_HOOKS: string;
|
|
72
|
+
/** Pi extension source — written to ~/.pi/agent/extensions/zeph.ts (drop-in auto-load). */
|
|
73
|
+
export declare const PI_EXTENSION: string;
|
|
74
|
+
/** OpenCode plugin source — written to ~/.config/opencode/plugins/zeph.ts (drop-in auto-load). */
|
|
75
|
+
export declare const OPENCODE_PLUGIN: string;
|
|
68
76
|
export declare const ZEPH_MARK_START = "<!-- ZEPH:START \u2014 managed by @zeph-to/cli, do not edit between markers -->";
|
|
69
77
|
export declare const ZEPH_MARK_END = "<!-- ZEPH:END -->";
|
|
70
78
|
/**
|
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":"AA4KA,6FAA6F;AAC7F,eAAO,MAAM,WAAW,QAKtB,CAAC;AAEH,6FAA6F;AAC7F,eAAO,MAAM,aAAa,QAIxB,CAAC;AAEH,0FAA0F;AAC1F,eAAO,MAAM,WAAW,QAAyE,CAAC;AAElG,uFAAuF;AACvF,eAAO,MAAM,UAAU,QAAyE,CAAC;AAEjG,oGAAoG;AACpG,eAAO,MAAM,YAAY,QAIvB,CAAC;AAEH,gFAAgF;AAChF,eAAO,MAAM,UAAU,QAIrB,CAAC;AAEH,sGAAsG;AACtG,eAAO,MAAM,UAAU,QAIrB,CAAC;AAEH,6GAA6G;AAC7G,eAAO,MAAM,OAAO,QAIlB,CAAC;AAEH,2GAA2G;AAC3G,eAAO,MAAM,aAAa,QAIxB,CAAC;AAIH,eAAO,MAAM,YAAY,QAKd,CAAC;AAEZ,eAAO,MAAM,cAAc,QAOhB,CAAC;AAEZ,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;CAuBxB,CAAC;AAYF,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;CASvB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,OAAO,KAAG,OAUhD,CAAC;AAEF,eAAO,MAAM,aAAa,QASf,CAAC;AAQZ,2FAA2F;AAC3F,eAAO,MAAM,YAAY,QAsDxB,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,eAAe,QAwG3B,CAAC;AASF,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
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// generated core (src/zeph-core.generated.ts) plus a per-agent
|
|
7
7
|
// notification preamble:
|
|
8
8
|
//
|
|
9
|
-
// - Hook-driven agents (Cursor, Windsurf, Gemini, Codex, Copilot
|
|
9
|
+
// - Hook-driven agents (Cursor, Windsurf, Gemini, Codex, Copilot, Pi,
|
|
10
|
+
// OpenCode) have
|
|
10
11
|
// a Stop-equivalent hook installed that auto-pushes on completion, so
|
|
11
12
|
// they must NOT manually call zeph_notify for "done".
|
|
12
13
|
// - Rule-only agents (Cline, Aider) have no Stop hook, so they DO call
|
|
@@ -16,16 +17,16 @@
|
|
|
16
17
|
// all of them — that is the whole point of the shared generated core.
|
|
17
18
|
//
|
|
18
19
|
// One more axis, and it decides who can enter REMOTE at all: the core's
|
|
19
|
-
// Rules
|
|
20
|
-
// hook note (a phone message) or by a `zeph_ask` answer. Only Gemini
|
|
21
|
-
// have that hook (remoteHookCmd below). Every other agent's only door is the
|
|
20
|
+
// Rules 1/2/8/9 are REMOTE-scoped, and REMOTE is entered by a prompt-submit
|
|
21
|
+
// hook note (a phone message) or by a `zeph_ask` answer. Only Gemini, Codex
|
|
22
|
+
// and Pi have that hook (remoteHookCmd below). Every other agent's only door is the
|
|
22
23
|
// ask itself — so for them the NORMAL branch must still send one after real
|
|
23
24
|
// work, or the phone loop can never begin. That is REMOTE_ENTRY_NO_HOOK.
|
|
24
25
|
//
|
|
25
26
|
// Keeping this in one place means a rule change lands everywhere at once
|
|
26
27
|
// and the agents can't drift apart.
|
|
27
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.removeManagedBlock = exports.upsertManagedBlock = exports.ZEPH_MARK_END = exports.ZEPH_MARK_START = exports.COPILOT_HOOKS = exports.isZephHookGroup = 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;
|
|
29
|
+
exports.removeManagedBlock = exports.upsertManagedBlock = exports.ZEPH_MARK_END = exports.ZEPH_MARK_START = exports.OPENCODE_PLUGIN = exports.PI_EXTENSION = exports.COPILOT_HOOKS = exports.isZephHookGroup = exports.CODEX_HOOKS = exports.GEMINI_HOOKS = exports.WINDSURF_HOOKS = exports.CURSOR_HOOKS = exports.OPENCODE_RULE = exports.PI_RULE = exports.AIDER_RULE = exports.CLINE_RULE = exports.COPILOT_RULE = exports.CODEX_RULE = exports.GEMINI_RULE = exports.WINDSURF_RULE = exports.CURSOR_RULE = void 0;
|
|
29
30
|
const gate_js_1 = require("./gate.js");
|
|
30
31
|
const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
|
|
31
32
|
// Graceful resolution: prefer the installed `zeph` CLI, but fall back to
|
|
@@ -35,8 +36,9 @@ const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
|
|
|
35
36
|
// pattern in plugin/hooks/zeph-{stop,ask}.sh.
|
|
36
37
|
//
|
|
37
38
|
// `--auto` applies the shared push-gate before sending (see src/gate.ts):
|
|
38
|
-
//
|
|
39
|
-
// /zeph-quiet | /zeph-loud dial works for every hook-driven
|
|
39
|
+
// a caller that names no turn counts still pushes (gate defaults assume real
|
|
40
|
+
// work), and the /zeph-quiet | /zeph-loud dial works for every hook-driven
|
|
41
|
+
// agent. The two drop-in artifacts do name counts — see `turnFacts` below.
|
|
40
42
|
//
|
|
41
43
|
// `--pushmode-default normal` is what keeps the first half of that true. The
|
|
42
44
|
// built-in default for a project with no dial is quiet, and quiet only lets a
|
|
@@ -47,8 +49,23 @@ const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
|
|
|
47
49
|
// Older installed `zeph` versions parse both flags as unknown booleans and
|
|
48
50
|
// ignore them — graceful backward compatibility, and for `--pushmode-default`
|
|
49
51
|
// specifically the old build's behavior was already "normal when no dial".
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
//
|
|
53
|
+
// `turnFacts` carries the `--tools` / `--nonreadonly` counts, which only the
|
|
54
|
+
// two drop-in artifacts can supply — a JSON hook config sees no per-tool
|
|
55
|
+
// events, so it passes nothing and keeps riding GATE_DEFAULTS.
|
|
56
|
+
const notifyCmd = (turnFacts = '') => '$(command -v zeph || echo "npx -y @zeph-to/cli") notify --title "Task done" --auto'
|
|
57
|
+
+ turnFacts
|
|
58
|
+
+ ` --${gate_js_1.PUSHMODE_DEFAULT_FLAG} normal 2>/dev/null || true`;
|
|
59
|
+
// The same command as a JS template literal, so an artifact's runtime counters
|
|
60
|
+
// interpolate into it. JSON.stringify would emit a double-quoted string and
|
|
61
|
+
// ship the placeholders verbatim. Safe as long as the command holds no
|
|
62
|
+
// backtick and no `${` of its own — `$(command -v zeph …)` is shell, not JS.
|
|
63
|
+
const notifyCmdLiteral = (turnFacts) => '`' + notifyCmd(turnFacts) + '`';
|
|
64
|
+
// The turn-fact fragment, as JS template-literal placeholders. Both artifacts
|
|
65
|
+
// name their counters `tools` / `nonReadonly`, so one fragment serves both —
|
|
66
|
+
// templates.test.ts pins those identifiers, since a rename on the artifact side
|
|
67
|
+
// would leave `${tools}` unresolved and kill the push with a ReferenceError.
|
|
68
|
+
const TURN_FACT_FLAGS = ` --${gate_js_1.TOOL_COUNT_FLAG} \${tools} --${gate_js_1.NONREADONLY_COUNT_FLAG} \${nonReadonly}`;
|
|
52
69
|
// Prompt-submit hook command — remote-origin detection (ADR-0002). Reads
|
|
53
70
|
// the hook JSON on stdin and prints additionalContext JSON on a marker
|
|
54
71
|
// match (see src/remote-hook.ts). stdout IS the hook response, so only
|
|
@@ -81,7 +98,7 @@ refactor, multi-file changes) call zeph_notify. Skip it for trivial
|
|
|
81
98
|
operations (file reads, simple searches). Set priority "high" for
|
|
82
99
|
errors/blockers.`;
|
|
83
100
|
// REMOTE-entry preamble — agents with NO prompt-submit hook (Cursor, Windsurf,
|
|
84
|
-
// Copilot, Cline, Aider). The shared core scopes Rules
|
|
101
|
+
// Copilot, Cline, Aider, OpenCode). The shared core scopes Rules 1/2/8/9 to REMOTE
|
|
85
102
|
// and tells a NORMAL session it owes no `zeph_ask`. That is right where a
|
|
86
103
|
// hook can announce the phone message that starts REMOTE; here nothing can,
|
|
87
104
|
// and the only remaining entry is a `zeph_ask` answer that is not Done-like.
|
|
@@ -91,7 +108,7 @@ errors/blockers.`;
|
|
|
91
108
|
// says about NORMAL still holds (no ask on trivial turns, questions may go
|
|
92
109
|
// to the local picker, no `zeph_ask` just to mark a turn finished).
|
|
93
110
|
//
|
|
94
|
-
// This overrides the core's Rule
|
|
111
|
+
// This overrides the core's Rule 2 NORMAL clause for these agents. It is a
|
|
95
112
|
// per-agent preamble, not a fork of the core, for the reason the core's
|
|
96
113
|
// header gives: the rule text must stay one thing.
|
|
97
114
|
const REMOTE_ENTRY_NO_HOOK = `## Entering REMOTE without a prompt hook
|
|
@@ -99,17 +116,30 @@ const REMOTE_ENTRY_NO_HOOK = `## Entering REMOTE without a prompt hook
|
|
|
99
116
|
This agent has no prompt-submit hook, so nothing can tell you when a
|
|
100
117
|
message arrived from the user's phone. The ONLY way this session enters
|
|
101
118
|
REMOTE is a \`zeph_ask\` answer that is not a Done-like button. So — and
|
|
102
|
-
this overrides the "In NORMAL, end with nothing" clause
|
|
119
|
+
this overrides the "In NORMAL, end with nothing" clause below —
|
|
103
120
|
**after substantial work in NORMAL, end the response with \`zeph_ask\`**:
|
|
104
121
|
2–4 \`actions\` carrying the next-step candidates plus a Done-like
|
|
105
122
|
\`fallback\`, \`timeout\` 300–600 s. "Substantial" = file changes, commits,
|
|
106
123
|
builds, tests, deploys, destructive ops, milestone completions. Skip it on
|
|
107
124
|
trivial turns (read-only exploration, a mid-step in an approved plan, a
|
|
108
|
-
typo-sized fix). Once the answer reports \`zephState: "REMOTE"\`,
|
|
109
|
-
takes over.`;
|
|
125
|
+
typo-sized fix). Once the answer reports \`zephState: "REMOTE"\`, sticky REMOTE
|
|
126
|
+
mode takes over.`;
|
|
127
|
+
// Tool-access preamble — pi only.
|
|
128
|
+
const PI_TOOL_ACCESS = `## Zeph tools via the CLI (no MCP)
|
|
129
|
+
|
|
130
|
+
Pi has no MCP support, so the zeph_* tools these rules name are not in your
|
|
131
|
+
tool list. Wherever a rule says to call one, run the zeph CLI with your bash
|
|
132
|
+
tool instead — same semantics:
|
|
133
|
+
|
|
134
|
+
- zeph_ask → \`zeph ask --title "…" --body "…" --actions "id:Label,id2:Label2" --timeout 300\`
|
|
135
|
+
Blocks until answered; prints one JSON line. \`answered: false\` (timeout /
|
|
136
|
+
unreachable) is a Done-like outcome — treat it as NORMAL.
|
|
137
|
+
- zeph_notify → \`zeph notify --title "…" --body "…" [--priority high]\`
|
|
138
|
+
- AskUserQuestion → pi's own terminal prompt.`;
|
|
110
139
|
/** Assemble a full rule document from optional frontmatter + preambles + core. */
|
|
111
140
|
const buildRule = (opts) => {
|
|
112
141
|
const fm = opts.frontmatter ? `${opts.frontmatter}\n\n` : '';
|
|
142
|
+
const tools = opts.toolAccess ? `${opts.toolAccess}\n\n` : '';
|
|
113
143
|
const entry = opts.remoteEntry ? `${opts.remoteEntry}\n\n` : '';
|
|
114
144
|
return `${fm}# Zeph — Remote-Control Rules
|
|
115
145
|
|
|
@@ -119,7 +149,7 @@ the user.
|
|
|
119
149
|
|
|
120
150
|
${opts.notify}
|
|
121
151
|
|
|
122
|
-
${entry}${opts.core}
|
|
152
|
+
${tools}${entry}${opts.core}
|
|
123
153
|
`;
|
|
124
154
|
};
|
|
125
155
|
// ── Per-agent rule documents ─────────────────────────────────────
|
|
@@ -162,17 +192,29 @@ exports.AIDER_RULE = buildRule({
|
|
|
162
192
|
remoteEntry: REMOTE_ENTRY_NO_HOOK,
|
|
163
193
|
core: zeph_core_generated_js_1.ZEPH_CORE_RULE_ONLY,
|
|
164
194
|
});
|
|
195
|
+
/** Pi — managed block in ~/.pi/agent/AGENTS.md. Extension = Stop-equivalent + prompt hook (PI_EXTENSION). */
|
|
196
|
+
exports.PI_RULE = buildRule({
|
|
197
|
+
notify: HOOK_DRIVEN_NOTIFY,
|
|
198
|
+
toolAccess: PI_TOOL_ACCESS,
|
|
199
|
+
core: zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN,
|
|
200
|
+
});
|
|
201
|
+
/** OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook (v1). */
|
|
202
|
+
exports.OPENCODE_RULE = buildRule({
|
|
203
|
+
notify: HOOK_DRIVEN_NOTIFY,
|
|
204
|
+
remoteEntry: REMOTE_ENTRY_NO_HOOK,
|
|
205
|
+
core: zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN,
|
|
206
|
+
});
|
|
165
207
|
// ── Hook configs ─────────────────────────────────────────────────
|
|
166
208
|
exports.CURSOR_HOOKS = JSON.stringify({
|
|
167
209
|
version: 1,
|
|
168
210
|
hooks: {
|
|
169
|
-
stop: [{ command:
|
|
211
|
+
stop: [{ command: notifyCmd() }],
|
|
170
212
|
},
|
|
171
213
|
}, null, 2);
|
|
172
214
|
exports.WINDSURF_HOOKS = JSON.stringify({
|
|
173
215
|
hooks: {
|
|
174
216
|
post_cascade_response: [{
|
|
175
|
-
command:
|
|
217
|
+
command: notifyCmd(),
|
|
176
218
|
show_output: false,
|
|
177
219
|
}],
|
|
178
220
|
},
|
|
@@ -195,7 +237,7 @@ exports.GEMINI_HOOKS = {
|
|
|
195
237
|
hooks: [{
|
|
196
238
|
name: 'zeph-notify',
|
|
197
239
|
type: 'command',
|
|
198
|
-
command:
|
|
240
|
+
command: notifyCmd(),
|
|
199
241
|
}],
|
|
200
242
|
}],
|
|
201
243
|
},
|
|
@@ -217,7 +259,7 @@ exports.CODEX_HOOKS = {
|
|
|
217
259
|
hooks: [{ type: 'command', command: remoteHookCmd('codex'), timeout: 5 }],
|
|
218
260
|
}],
|
|
219
261
|
Stop: [{
|
|
220
|
-
hooks: [{ type: 'command', command:
|
|
262
|
+
hooks: [{ type: 'command', command: notifyCmd() }],
|
|
221
263
|
}],
|
|
222
264
|
},
|
|
223
265
|
};
|
|
@@ -245,11 +287,178 @@ exports.COPILOT_HOOKS = JSON.stringify({
|
|
|
245
287
|
hooks: {
|
|
246
288
|
sessionEnd: [{
|
|
247
289
|
type: 'command',
|
|
248
|
-
bash:
|
|
290
|
+
bash: notifyCmd(),
|
|
249
291
|
timeoutSec: 10,
|
|
250
292
|
}],
|
|
251
293
|
},
|
|
252
294
|
}, null, 2);
|
|
295
|
+
// ── Drop-in artifact sources (pi extension, opencode plugin) ─────
|
|
296
|
+
//
|
|
297
|
+
// Not named *_HOOKS: those exports are JSON configs that templates.test.ts
|
|
298
|
+
// JSON.parses — these are TS source, auto-collected there as *_EXTENSION /
|
|
299
|
+
// *_PLUGIN for the same quiet-default guard, at the string level.
|
|
300
|
+
/** Pi extension source — written to ~/.pi/agent/extensions/zeph.ts (drop-in auto-load). */
|
|
301
|
+
exports.PI_EXTENSION = `// Generated by @zeph-to/cli — reinstall overwrites; \`zeph uninstall\` removes.
|
|
302
|
+
import { spawn } from "node:child_process";
|
|
303
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
304
|
+
|
|
305
|
+
const sh = (cmd: string, cwd: string, stdin?: string): Promise<string> =>
|
|
306
|
+
new Promise((resolve) => {
|
|
307
|
+
const child = spawn("sh", ["-c", cmd], { cwd, stdio: ["pipe", "pipe", "ignore"] });
|
|
308
|
+
let out = "";
|
|
309
|
+
child.stdout.on("data", (d) => (out += d.toString()));
|
|
310
|
+
child.on("error", () => resolve(""));
|
|
311
|
+
child.on("close", () => resolve(out));
|
|
312
|
+
child.stdin.end(stdin ?? "");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Read-only tools, mirroring what the Claude Code Stop hook excludes
|
|
316
|
+
// (Read/Grep/Glob). Names read from the installed package's dist/core/tools on
|
|
317
|
+
// 2026-08-28: bash, edit, find, grep, ls, powershell, read, write — \`find\` is
|
|
318
|
+
// pi's Glob. Anything absent here, MCP tools included, counts as real work, so
|
|
319
|
+
// an unknown tool errs toward pushing rather than toward silence.
|
|
320
|
+
const READ_ONLY = new Set(["read", "grep", "find", "ls"]);
|
|
321
|
+
|
|
322
|
+
export default function (pi: ExtensionAPI) {
|
|
323
|
+
// Turn facts for the push gate. One agent per process, so plain counters
|
|
324
|
+
// suffice — no session keying.
|
|
325
|
+
let tools = 0;
|
|
326
|
+
let nonReadonly = 0;
|
|
327
|
+
|
|
328
|
+
pi.on("tool_execution_end", (event) => {
|
|
329
|
+
tools += 1;
|
|
330
|
+
if (!READ_ONLY.has(event.toolName)) nonReadonly += 1;
|
|
331
|
+
});
|
|
332
|
+
// Stop-equivalent: agent_settled fires once per user turn, after retries/compaction.
|
|
333
|
+
// Fire-and-forget: never block pi's turn-end on the notify network call.
|
|
334
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
335
|
+
const child = spawn("sh", ["-c", ${notifyCmdLiteral(TURN_FACT_FLAGS)}], { cwd: ctx.cwd, stdio: "ignore", detached: true });
|
|
336
|
+
child.on("error", () => {});
|
|
337
|
+
child.unref();
|
|
338
|
+
});
|
|
339
|
+
// Prompt-submit: remote-origin detection (ADR-0002) — additionalContext comes
|
|
340
|
+
// back as a persistent injected message (this extension is the consumer).
|
|
341
|
+
// Doubles as the turn boundary: this is where the counters start over.
|
|
342
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
343
|
+
tools = 0;
|
|
344
|
+
nonReadonly = 0;
|
|
345
|
+
const out = await sh(${JSON.stringify(remoteHookCmd('pi'))}, ctx.cwd,
|
|
346
|
+
JSON.stringify({ prompt: event.prompt, cwd: ctx.cwd }));
|
|
347
|
+
try {
|
|
348
|
+
const context = JSON.parse(out)?.hookSpecificOutput?.additionalContext;
|
|
349
|
+
if (typeof context === "string" && context) {
|
|
350
|
+
return { message: { customType: "zeph-remote", content: context, display: false } };
|
|
351
|
+
}
|
|
352
|
+
} catch { /* silent no-op — the hook only ever adds context */ }
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
`;
|
|
356
|
+
/** OpenCode plugin source — written to ~/.config/opencode/plugins/zeph.ts (drop-in auto-load). */
|
|
357
|
+
exports.OPENCODE_PLUGIN = `// Generated by @zeph-to/cli — reinstall overwrites; \`zeph uninstall\` removes.
|
|
358
|
+
import { spawn } from "node:child_process";
|
|
359
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
360
|
+
|
|
361
|
+
// Read-only tools, mirroring what the Claude Code Stop hook excludes
|
|
362
|
+
// (Read/Grep/Glob). Names read from the installed opencode binary's tool
|
|
363
|
+
// registry on 2026-08-28: read, list, glob, grep, webfetch, websearch, task,
|
|
364
|
+
// shell, bash, edit, write, patch, skill, lsp, todowrite. Anything absent
|
|
365
|
+
// here, MCP tools included, counts as real work — an unknown tool errs toward
|
|
366
|
+
// pushing rather than toward silence.
|
|
367
|
+
const READ_ONLY = new Set(["read", "grep", "glob", "list"]);
|
|
368
|
+
|
|
369
|
+
type TurnFacts = { tools: number; nonReadonly: number };
|
|
370
|
+
const zeroFacts = (): TurnFacts => ({ tools: 0, nonReadonly: 0 });
|
|
371
|
+
|
|
372
|
+
// Typed against the real plugin contract, the way the pi extension is: the
|
|
373
|
+
// hook names and payload shapes this file asserts by hand are then checked by
|
|
374
|
+
// the compiler, so an upstream rename fails loudly instead of silently
|
|
375
|
+
// handing every handler an \`any\`.
|
|
376
|
+
export const ZephPlugin: Plugin = async ({ client, directory }) => {
|
|
377
|
+
// Turn facts, keyed by session. The plugin instance is per-project, not
|
|
378
|
+
// per-session, and the \`task\` tool runs subagents in their own child
|
|
379
|
+
// sessions with their own full lifecycle — one shared counter would bill
|
|
380
|
+
// their tool calls to whatever else the project had running.
|
|
381
|
+
const facts = new Map<string, TurnFacts>();
|
|
382
|
+
|
|
383
|
+
// A child session's id is all \`session.idle\` carries; the parent link lives
|
|
384
|
+
// on the session record. This is the one blocking call in the idle handler —
|
|
385
|
+
// the notify spawn below stays fire-and-forget, but the push decision cannot
|
|
386
|
+
// be made without knowing whether this session is somebody's subagent. The
|
|
387
|
+
// generated client defaults to \`throwOnError: false\`, so a non-2xx arrives as
|
|
388
|
+
// a result with no \`data\` rather than as a throw; both paths land on
|
|
389
|
+
// undefined, which pushes — the same direction every other unknown in this
|
|
390
|
+
// file leans.
|
|
391
|
+
const parentOf = async (id: string): Promise<string | undefined> => {
|
|
392
|
+
try {
|
|
393
|
+
return (await client.session.get({ path: { id } })).data?.parentID;
|
|
394
|
+
} catch {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
// Turn start. Create-if-absent, never reset: this fires per *message*, and
|
|
401
|
+
// a second message queued while the agent is still working would otherwise
|
|
402
|
+
// zero the counts already earned. session.idle deletes the entry, so the
|
|
403
|
+
// next turn always starts from zero anyway.
|
|
404
|
+
"chat.message": async ({ sessionID }) => {
|
|
405
|
+
if (!facts.has(sessionID)) facts.set(sessionID, zeroFacts());
|
|
406
|
+
},
|
|
407
|
+
"tool.execute.after": async ({ tool, sessionID }) => {
|
|
408
|
+
const entry = facts.get(sessionID) ?? zeroFacts();
|
|
409
|
+
entry.tools += 1;
|
|
410
|
+
if (!READ_ONLY.has(tool)) entry.nonReadonly += 1;
|
|
411
|
+
facts.set(sessionID, entry);
|
|
412
|
+
},
|
|
413
|
+
// Stop-equivalent. Unlike the named hooks above, session.idle arrives only
|
|
414
|
+
// through the generic \`event\` hook — there is no per-event key for it, so
|
|
415
|
+
// filter by type.
|
|
416
|
+
event: async ({ event }) => {
|
|
417
|
+
// A deleted session never idles again, so nothing would ever consume its
|
|
418
|
+
// entry. Drop it here or it outlives the turn for the life of the
|
|
419
|
+
// process. An *aborted* turn needs no such handling: the entry survives
|
|
420
|
+
// to the session's next chat.message, which adds to it rather than
|
|
421
|
+
// resetting, so the interrupted work is reported with the retry.
|
|
422
|
+
if (event.type === "session.deleted") {
|
|
423
|
+
facts.delete(event.properties.info.id);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (event.type !== "session.idle") return;
|
|
427
|
+
const { sessionID } = event.properties;
|
|
428
|
+
// No entry means no user message was seen for this session, so there is
|
|
429
|
+
// no turn to report — the zeroes gate the push out, which is correct.
|
|
430
|
+
const settled = facts.get(sessionID) ?? zeroFacts();
|
|
431
|
+
facts.delete(sessionID);
|
|
432
|
+
|
|
433
|
+
// A \`task\` subagent settles first, in its own session. Pushing there
|
|
434
|
+
// would announce "Task done" for a slice of a turn still in flight, and
|
|
435
|
+
// leave the parent crediting the whole delegation as the single
|
|
436
|
+
// \`task\` call it saw — one tool, below the gate, so the real
|
|
437
|
+
// completion would go silent. Roll the counts up and stay quiet.
|
|
438
|
+
const parentID = await parentOf(sessionID);
|
|
439
|
+
const parent = parentID === undefined ? undefined : facts.get(parentID);
|
|
440
|
+
if (parent) {
|
|
441
|
+
parent.tools += settled.tools;
|
|
442
|
+
parent.nonReadonly += settled.nonReadonly;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
// Falling through means no live parent entry to roll into — either this
|
|
446
|
+
// is a top-level session, or a \`background\` task outlived the turn that
|
|
447
|
+
// spawned it and the parent settled and pushed already, or the lookup
|
|
448
|
+
// above failed. The three are indistinguishable from here, and all three
|
|
449
|
+
// want the same thing: report this session's own work rather than write
|
|
450
|
+
// it into an entry nobody will ever read. Never conjure that entry — that
|
|
451
|
+
// would leak one per background task.
|
|
452
|
+
|
|
453
|
+
const { tools, nonReadonly } = settled;
|
|
454
|
+
// Fire-and-forget: never block opencode's event pipeline on the notify call.
|
|
455
|
+
const child = spawn("sh", ["-c", ${notifyCmdLiteral(TURN_FACT_FLAGS)}], { cwd: directory, stdio: "ignore", detached: true });
|
|
456
|
+
child.on("error", () => {});
|
|
457
|
+
child.unref();
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
`;
|
|
253
462
|
// ── Marker-section helpers for shared global rule files ──────────
|
|
254
463
|
//
|
|
255
464
|
// Windsurf / Gemini / Codex all use a single shared global rule file
|
package/dist/uninstall.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** Remove just the `zeph` entry from an MCP registry JSON file. `key` is the
|
|
2
|
+
* container: `mcpServers` for Cursor/Windsurf, top-level `mcp` for
|
|
3
|
+
* opencode.json (opencode's own schema). Exported for tests. */
|
|
4
|
+
export declare const rmMcpEntry: (filePath: string, dry: boolean, key?: string) => string | null;
|
|
1
5
|
/** Remove just the zeph-written entries from Gemini's settings.json.
|
|
2
6
|
* Exported for tests (gemini is PATH-detected, so the full-uninstall
|
|
3
7
|
* suite can't reach this deterministically). */
|
package/dist/uninstall.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uninstall.d.ts","sourceRoot":"","sources":["../src/uninstall.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"uninstall.d.ts","sourceRoot":"","sources":["../src/uninstall.ts"],"names":[],"mappings":"AA4BA;;iEAEiE;AACjE,eAAO,MAAM,UAAU,GAAI,UAAU,MAAM,EAAE,KAAK,OAAO,EAAE,YAAkB,KAAG,MAAM,GAAG,IAexF,CAAC;AA4DF;;iDAEiD;AACjD,eAAO,MAAM,YAAY,GAAI,UAAU,MAAM,EAAE,KAAK,OAAO,KAAG,MAAM,GAAG,IAWtE,CAAC;AAEF;;;gDAGgD;AAChD,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,EAAE,KAAK,OAAO,KAAG,MAAM,GAAG,IAerE,CAAC;AA8EF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAU,KAAK,OAAO,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAM3E,CAAC;AAIF,eAAO,MAAM,eAAe,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAoC5F,CAAC"}
|
package/dist/uninstall.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.handleUninstall = exports.removeServiceStep = exports.rmCodexHook = exports.rmGeminiHook = void 0;
|
|
3
|
+
exports.handleUninstall = exports.removeServiceStep = exports.rmCodexHook = exports.rmGeminiHook = exports.rmMcpEntry = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const os_1 = require("os");
|
|
@@ -25,8 +25,10 @@ const rmFile = (filePath, dry) => {
|
|
|
25
25
|
(0, fs_1.rmSync)(filePath, { force: true });
|
|
26
26
|
return `${verb(dry)} ${filePath}`;
|
|
27
27
|
};
|
|
28
|
-
/** Remove just the `zeph` entry from an
|
|
29
|
-
|
|
28
|
+
/** Remove just the `zeph` entry from an MCP registry JSON file. `key` is the
|
|
29
|
+
* container: `mcpServers` for Cursor/Windsurf, top-level `mcp` for
|
|
30
|
+
* opencode.json (opencode's own schema). Exported for tests. */
|
|
31
|
+
const rmMcpEntry = (filePath, dry, key = 'mcpServers') => {
|
|
30
32
|
if (!(0, fs_1.existsSync)(filePath))
|
|
31
33
|
return null;
|
|
32
34
|
let data;
|
|
@@ -36,7 +38,7 @@ const rmMcpEntry = (filePath, dry) => {
|
|
|
36
38
|
catch {
|
|
37
39
|
return null;
|
|
38
40
|
}
|
|
39
|
-
const servers = data
|
|
41
|
+
const servers = data[key];
|
|
40
42
|
if (!servers || !('zeph' in servers))
|
|
41
43
|
return null;
|
|
42
44
|
if (!dry) {
|
|
@@ -45,6 +47,7 @@ const rmMcpEntry = (filePath, dry) => {
|
|
|
45
47
|
}
|
|
46
48
|
return `${verb(dry)} zeph from ${filePath}`;
|
|
47
49
|
};
|
|
50
|
+
exports.rmMcpEntry = rmMcpEntry;
|
|
48
51
|
/** Strip the <!-- ZEPH:START/END --> block from a shared rule file. */
|
|
49
52
|
const stripManagedRule = (filePath, dry) => {
|
|
50
53
|
if (!(0, fs_1.existsSync)(filePath))
|
|
@@ -185,12 +188,12 @@ const AGENT_UNINSTALLERS = {
|
|
|
185
188
|
}
|
|
186
189
|
},
|
|
187
190
|
cursor: (dry) => runSteps([
|
|
188
|
-
() => rmMcpEntry((0, path_1.join)(HOME, '.cursor', 'mcp.json'), dry),
|
|
191
|
+
() => (0, exports.rmMcpEntry)((0, path_1.join)(HOME, '.cursor', 'mcp.json'), dry),
|
|
189
192
|
() => rmFile((0, path_1.join)(HOME, '.cursor', 'hooks.json'), dry),
|
|
190
193
|
() => rmFile((0, path_1.join)(HOME, '.cursor', 'rules', 'zeph.mdc'), dry),
|
|
191
194
|
]),
|
|
192
195
|
windsurf: (dry) => runSteps([
|
|
193
|
-
() => rmMcpEntry((0, path_1.join)(HOME, '.codeium', 'windsurf', 'mcp_config.json'), dry),
|
|
196
|
+
() => (0, exports.rmMcpEntry)((0, path_1.join)(HOME, '.codeium', 'windsurf', 'mcp_config.json'), dry),
|
|
194
197
|
() => rmFile((0, path_1.join)(HOME, '.codeium', 'windsurf', 'hooks.json'), dry),
|
|
195
198
|
() => stripManagedRule((0, path_1.join)(HOME, '.codeium', 'windsurf', 'memories', 'global_rules.md'), dry),
|
|
196
199
|
]),
|
|
@@ -235,6 +238,15 @@ const AGENT_UNINSTALLERS = {
|
|
|
235
238
|
() => rmFile((0, path_1.join)(HOME, '.zeph', 'aider-conventions.md'), dry),
|
|
236
239
|
() => rmAiderReadDirective((0, path_1.join)(HOME, '.aider.conf.yml'), dry),
|
|
237
240
|
]),
|
|
241
|
+
pi: (dry) => runSteps([
|
|
242
|
+
() => rmFile((0, path_1.join)(HOME, '.pi', 'agent', 'extensions', 'zeph.ts'), dry),
|
|
243
|
+
() => stripManagedRule((0, path_1.join)(HOME, '.pi', 'agent', 'AGENTS.md'), dry),
|
|
244
|
+
]),
|
|
245
|
+
opencode: (dry) => runSteps([
|
|
246
|
+
() => (0, exports.rmMcpEntry)((0, path_1.join)(HOME, '.config', 'opencode', 'opencode.json'), dry, 'mcp'),
|
|
247
|
+
() => rmFile((0, path_1.join)(HOME, '.config', 'opencode', 'plugins', 'zeph.ts'), dry),
|
|
248
|
+
() => stripManagedRule((0, path_1.join)(HOME, '.config', 'opencode', 'AGENTS.md'), dry),
|
|
249
|
+
]),
|
|
238
250
|
};
|
|
239
251
|
/**
|
|
240
252
|
* Remove the login-time LaunchAgent, if one is installed.
|
package/dist/verify.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AA4CA,eAAO,MAAM,YAAY,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAiFzF,CAAC"}
|
package/dist/verify.js
CHANGED
|
@@ -38,6 +38,8 @@ const AGENT_RULE_PRESENT = {
|
|
|
38
38
|
copilot: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.copilot', 'instructions', 'zeph.instructions.md')),
|
|
39
39
|
cline: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.cline', 'rules', 'zeph.md')),
|
|
40
40
|
aider: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.zeph', 'aider-conventions.md')),
|
|
41
|
+
pi: () => hasManagedBlock((0, path_1.join)(HOME, '.pi', 'agent', 'AGENTS.md')),
|
|
42
|
+
opencode: () => hasManagedBlock((0, path_1.join)(HOME, '.config', 'opencode', 'AGENTS.md')),
|
|
41
43
|
};
|
|
42
44
|
const handleVerify = async (args) => {
|
|
43
45
|
const doPing = args.ping === true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** sha256 over the plugin manifest + extracted rule text at generation time. */
|
|
2
|
-
export declare const ZEPH_CORE_SOURCE_HASH = "
|
|
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\
|
|
2
|
+
export declare const ZEPH_CORE_SOURCE_HASH = "32a111f3b27446031bc6972595c9b4cb13d86aa2a2ff3ff78a450eddc44d0a14";
|
|
3
|
+
/** Shared rule core for agents with a Stop-equivalent hook (Cursor, Windsurf, Gemini, Codex, Copilot, Pi, OpenCode). */
|
|
4
|
+
export declare const ZEPH_CORE_HOOK_DRIVEN = "### When zeph_ask is MANDATORY\n\n1. **While in REMOTE, 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 holds even on research / analysis / planning turns where no files were touched.\n\n **In NORMAL it does not apply.** Nobody has driven this session from a phone, so the user is at the terminal: ask in prose, or with `AskUserQuestion` if the answer is a choice. A `zeph_ask` there blocks the turn until someone answers on a device or the timeout expires \u2014 that cost is exactly what NORMAL exists to avoid.\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n2. **In REMOTE, `zeph_ask` is the DEFAULT end of EVERY response** \u2014 not conditioned on the work being substantial (Rule 7).\n\n **In NORMAL, end with nothing.** The Stop hook's push is the completion signal. Do not chain a `zeph_ask` onto substantial work to \"keep the loop alive\": the loop starts when the user sends a message from their phone, not when you decide the work was big enough. `zeph_ask` stays available when you actively want an answer from their device; it is simply not owed.\n\n3. When you do ask, prefer `zeph_ask` over `zeph_prompt`/`zeph_input` \u2014 it combines buttons and free text in one push. **`actions` is the steering surface, not decoration:** ship 2\u20134 buttons on nearly every ask (the next-step candidates you would otherwise write as prose) plus a safe Done-like `fallback` id \u2014 never a destructive one. Leave `actions` out ONLY when the answer is inherently free-form (a name, a path, a paragraph); a text-only ask on a \"done \u2014 what next?\" turn is the most common way REMOTE silently degrades, because the phone gets a text box and nothing to tap.\n\n ```\n zeph_ask({\n title: \"Slice done \u2014 next?\",\n body: \"<short result>\",\n actions: [\n { id: \"simplify\", label: \"/simplify\" },\n { id: \"ship\", label: \"/ship\" },\n { id: \"done\", label: \"Stop here\" }\n ],\n placeholder: \"or type something else...\",\n fallback: \"done\"\n })\n ```\n\n4. Rule 7 says when to send one. Never send an ask just to mark a turn finished.\n\n### Handling the response\n\n5. 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\n6. Caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If that step would destroy user code, data, or infrastructure (force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk in a targeted `zeph_ask` first \u2014 title \"About to force-push main \u2014 proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 7)\n\n7. **The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky \u2014 every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it** as `zephState: \"REMOTE\" | \"NORMAL\"`: any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it**, where installed (this plugin, or the hooks `zeph setup` writes for Gemini/Codex): a remote-origin note on the turn a phone message arrives, and a note that the session has LEFT REMOTE on the first turn the user types at the terminal. A phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook, so a prompt with no phone marker is demonstrably the user's own keyboard \u2014 staying in REMOTE would answer the terminal with a phone loop, and re-entry costs them one message from the phone.\n- **Neither one present \u2192 NORMAL.**\n\n**The one call left to you is free text**, because it is the one signal no hook can read: the server cannot tell \"run the tests\" from \"thanks, that's it\". When the user's typed answer clearly closes the loop \u2014 an obvious wrap-up, or `done`/`stop`/`exit` as a standalone word (not a substring: \"redo\" is not \"done\") \u2014 flip to NORMAL from that response on, don't send `zeph_ask` on it, and emit `<!-- zeph: exit -->` once so the hooks agree with you. Your own flip is what ends the loop; the marker is how you tell a hook that cannot read your mind, and it is separate from any push-volume marker, which says nothing about the mode.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`, carrying 2\u20134 `actions` (the next-step candidates as buttons) plus a Done-like `fallback` \u2014 never a destructive one, since an unanswered ask resolves to it \u2014 a text-only ask is only for inherently free-form answers. Non-negotiable while in REMOTE, independent of whether the work was substantial or routine.\n\nSet `timeout` 300\u2013600 s so silence degrades cleanly: an unanswered ask exits the loop quietly \u2014 the server treats a Done-like fallback as an exit \u2014 instead of chaining more notifications at a user who stepped away.\n\nFour things leave REMOTE: a Done-like button (or a timeout that fell back to one), your own read of a free-text wrap-up, a prompt the user typed at the terminal, and \u2014 for a session nobody exited because it crashed \u2014 the state expiring.\n\n#### Behavior in NORMAL (no zeph_ask is owed)\n\nThe user is at the terminal \u2014 that is what NORMAL means. Nothing here obliges an ask:\n\n- Questions go to `AskUserQuestion` or plain prose. The Ask hook still pushes them to the user's device, so a question is never lost.\n- Completion is the Stop hook's push.\n- `zeph_ask` remains available when you actively want an answer from their device \u2014 it is not owed, and never as a way to mark a turn finished.\n\nREMOTE begins the moment the user sends a message from their phone; from that turn on, Rules 1, 2, 8 and 9 are in force.\n\n### When to use AskUserQuestion vs zeph_ask\n\n8. **While in REMOTE, a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion`.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"proceed?\"). `AskUserQuestion` is a LOCAL blocking picker; the phone reaches it only through the terminal mirror, and an answer injected there never enters REMOTE \u2014 so the *next* turn stops being phone-driveable. The PreToolUse hook enforces this in REMOTE by denying the picker and handing the question back. (Why the mirror is the worse channel on every axis: README \u2192 \"AskUserQuestion vs zeph_ask\".)\n\n **In NORMAL the picker is the right tool** and the hook lets it through \u2014 the user is at the terminal, and the Ask hook still pushes the question to their device so they know one is waiting.\n\n9. **In REMOTE this overrides any skill instruction.** If a skill you are running \u2014 or your own plan \u2014 would call `AskUserQuestion` with a button-friendly question, surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Fall through to the picker ONLY when (a) the answer needs code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n10. These rules persist for the entire session. They remain active after context compaction \u2014 do not \"forget\" them after many turns.";
|
|
5
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\
|
|
6
|
+
export declare const ZEPH_CORE_RULE_ONLY = "### When zeph_ask is MANDATORY\n\n1. **While in REMOTE, 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 holds even on research / analysis / planning turns where no files were touched.\n\n **In NORMAL it does not apply.** Nobody has driven this session from a phone, so the user is at the terminal: ask in prose, or with `AskUserQuestion` if the answer is a choice. A `zeph_ask` there blocks the turn until someone answers on a device or the timeout expires \u2014 that cost is exactly what NORMAL exists to avoid.\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n2. **In REMOTE, `zeph_ask` is the DEFAULT end of EVERY response** \u2014 not conditioned on the work being substantial (Rule 7).\n\n **In NORMAL, end with nothing.** The Stop hook's push is the completion signal. Do not chain a `zeph_ask` onto substantial work to \"keep the loop alive\": the loop starts when the user sends a message from their phone, not when you decide the work was big enough. `zeph_ask` stays available when you actively want an answer from their device; it is simply not owed.\n\n3. When you do ask, prefer `zeph_ask` over `zeph_prompt`/`zeph_input` \u2014 it combines buttons and free text in one push. **`actions` is the steering surface, not decoration:** ship 2\u20134 buttons on nearly every ask (the next-step candidates you would otherwise write as prose) plus a safe Done-like `fallback` id \u2014 never a destructive one. Leave `actions` out ONLY when the answer is inherently free-form (a name, a path, a paragraph); a text-only ask on a \"done \u2014 what next?\" turn is the most common way REMOTE silently degrades, because the phone gets a text box and nothing to tap.\n\n ```\n zeph_ask({\n title: \"Slice done \u2014 next?\",\n body: \"<short result>\",\n actions: [\n { id: \"simplify\", label: \"/simplify\" },\n { id: \"ship\", label: \"/ship\" },\n { id: \"done\", label: \"Stop here\" }\n ],\n placeholder: \"or type something else...\",\n fallback: \"done\"\n })\n ```\n\n4. Rule 7 says when to send one. Never send an ask just to mark a turn finished.\n\n### Handling the response\n\n5. 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\n6. Caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If that step would destroy user code, data, or infrastructure (force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk in a targeted `zeph_ask` first \u2014 title \"About to force-push main \u2014 proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 7)\n\n7. **The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky \u2014 every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it** as `zephState: \"REMOTE\" | \"NORMAL\"`: any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it**, where installed (this plugin, or the hooks `zeph setup` writes for Gemini/Codex): a remote-origin note on the turn a phone message arrives, and a note that the session has LEFT REMOTE on the first turn the user types at the terminal. A phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook, so a prompt with no phone marker is demonstrably the user's own keyboard \u2014 staying in REMOTE would answer the terminal with a phone loop, and re-entry costs them one message from the phone.\n- **Neither one present \u2192 NORMAL.**\n\n**The one call left to you is free text**, because it is the one signal no hook can read: the server cannot tell \"run the tests\" from \"thanks, that's it\". When the user's typed answer clearly closes the loop \u2014 an obvious wrap-up, or `done`/`stop`/`exit` as a standalone word (not a substring: \"redo\" is not \"done\") \u2014 flip to NORMAL from that response on, don't send `zeph_ask` on it, and emit `<!-- zeph: exit -->` once so the hooks agree with you. Your own flip is what ends the loop; the marker is how you tell a hook that cannot read your mind, and it is separate from any push-volume marker, which says nothing about the mode.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`, carrying 2\u20134 `actions` (the next-step candidates as buttons) plus a Done-like `fallback` \u2014 never a destructive one, since an unanswered ask resolves to it \u2014 a text-only ask is only for inherently free-form answers. Non-negotiable while in REMOTE, independent of whether the work was substantial or routine.\n\nSet `timeout` 300\u2013600 s so silence degrades cleanly: an unanswered ask exits the loop quietly \u2014 the server treats a Done-like fallback as an exit \u2014 instead of chaining more notifications at a user who stepped away.\n\nFour things leave REMOTE: a Done-like button (or a timeout that fell back to one), your own read of a free-text wrap-up, a prompt the user typed at the terminal, and \u2014 for a session nobody exited because it crashed \u2014 the state expiring.\n\n#### Behavior in NORMAL (no zeph_ask is owed)\n\nThe user is at the terminal \u2014 that is what NORMAL means. Nothing here obliges an ask:\n\n- Questions go to `AskUserQuestion` or plain prose. The Ask hook still pushes them to the user's device, so a question is never lost.\n- Completion is the Stop hook's push.\n- `zeph_ask` remains available when you actively want an answer from their device \u2014 it is not owed, and never as a way to mark a turn finished.\n\nREMOTE begins the moment the user sends a message from their phone; from that turn on, Rules 1, 2, 8 and 9 are in force.\n\n### When to use AskUserQuestion vs zeph_ask\n\n8. **While in REMOTE, a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion`.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"proceed?\"). `AskUserQuestion` is a LOCAL blocking picker; the phone reaches it only through the terminal mirror, and an answer injected there never enters REMOTE \u2014 so the *next* turn stops being phone-driveable. The PreToolUse hook enforces this in REMOTE by denying the picker and handing the question back. (Why the mirror is the worse channel on every axis: README \u2192 \"AskUserQuestion vs zeph_ask\".)\n\n **In NORMAL the picker is the right tool** and the hook lets it through \u2014 the user is at the terminal, and the Ask hook still pushes the question to their device so they know one is waiting.\n\n9. **In REMOTE this overrides any skill instruction.** If a skill you are running \u2014 or your own plan \u2014 would call `AskUserQuestion` with a button-friendly question, surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Fall through to the picker ONLY when (a) the answer needs code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n10. These rules persist for the entire session. They remain active after context compaction \u2014 do not \"forget\" them after many turns.";
|
|
7
7
|
//# sourceMappingURL=zeph-core.generated.d.ts.map
|
|
@@ -1 +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,
|
|
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,wHAAwH;AACxH,eAAO,MAAM,qBAAqB,24PAAsuP,CAAC;AAEzwP,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,24PAAsuP,CAAC"}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.ZEPH_CORE_RULE_ONLY = exports.ZEPH_CORE_HOOK_DRIVEN = exports.ZEPH_CORE_SOURCE_HASH = void 0;
|
|
9
9
|
/** sha256 over the plugin manifest + extracted rule text at generation time. */
|
|
10
|
-
exports.ZEPH_CORE_SOURCE_HASH = "
|
|
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\
|
|
10
|
+
exports.ZEPH_CORE_SOURCE_HASH = "32a111f3b27446031bc6972595c9b4cb13d86aa2a2ff3ff78a450eddc44d0a14";
|
|
11
|
+
/** Shared rule core for agents with a Stop-equivalent hook (Cursor, Windsurf, Gemini, Codex, Copilot, Pi, OpenCode). */
|
|
12
|
+
exports.ZEPH_CORE_HOOK_DRIVEN = "### When zeph_ask is MANDATORY\n\n1. **While in REMOTE, 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 holds even on research / analysis / planning turns where no files were touched.\n\n **In NORMAL it does not apply.** Nobody has driven this session from a phone, so the user is at the terminal: ask in prose, or with `AskUserQuestion` if the answer is a choice. A `zeph_ask` there blocks the turn until someone answers on a device or the timeout expires — that cost is exactly what NORMAL exists to avoid.\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n2. **In REMOTE, `zeph_ask` is the DEFAULT end of EVERY response** — not conditioned on the work being substantial (Rule 7).\n\n **In NORMAL, end with nothing.** The Stop hook's push is the completion signal. Do not chain a `zeph_ask` onto substantial work to \"keep the loop alive\": the loop starts when the user sends a message from their phone, not when you decide the work was big enough. `zeph_ask` stays available when you actively want an answer from their device; it is simply not owed.\n\n3. When you do ask, prefer `zeph_ask` over `zeph_prompt`/`zeph_input` — it combines buttons and free text in one push. **`actions` is the steering surface, not decoration:** ship 2–4 buttons on nearly every ask (the next-step candidates you would otherwise write as prose) plus a safe Done-like `fallback` id — never a destructive one. Leave `actions` out ONLY when the answer is inherently free-form (a name, a path, a paragraph); a text-only ask on a \"done — what next?\" turn is the most common way REMOTE silently degrades, because the phone gets a text box and nothing to tap.\n\n ```\n zeph_ask({\n title: \"Slice done — next?\",\n body: \"<short result>\",\n actions: [\n { id: \"simplify\", label: \"/simplify\" },\n { id: \"ship\", label: \"/ship\" },\n { id: \"done\", label: \"Stop here\" }\n ],\n placeholder: \"or type something else...\",\n fallback: \"done\"\n })\n ```\n\n4. Rule 7 says when to send one. Never send an ask just to mark a turn finished.\n\n### Handling the response\n\n5. 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\n6. Caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If that step would destroy user code, data, or infrastructure (force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk in a targeted `zeph_ask` first — title \"About to force-push main — proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 7)\n\n7. **The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky — every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it** as `zephState: \"REMOTE\" | \"NORMAL\"`: any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it**, where installed (this plugin, or the hooks `zeph setup` writes for Gemini/Codex): a remote-origin note on the turn a phone message arrives, and a note that the session has LEFT REMOTE on the first turn the user types at the terminal. A phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook, so a prompt with no phone marker is demonstrably the user's own keyboard — staying in REMOTE would answer the terminal with a phone loop, and re-entry costs them one message from the phone.\n- **Neither one present → NORMAL.**\n\n**The one call left to you is free text**, because it is the one signal no hook can read: the server cannot tell \"run the tests\" from \"thanks, that's it\". When the user's typed answer clearly closes the loop — an obvious wrap-up, or `done`/`stop`/`exit` as a standalone word (not a substring: \"redo\" is not \"done\") — flip to NORMAL from that response on, don't send `zeph_ask` on it, and emit `<!-- zeph: exit -->` once so the hooks agree with you. Your own flip is what ends the loop; the marker is how you tell a hook that cannot read your mind, and it is separate from any push-volume marker, which says nothing about the mode.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`, carrying 2–4 `actions` (the next-step candidates as buttons) plus a Done-like `fallback` — never a destructive one, since an unanswered ask resolves to it — a text-only ask is only for inherently free-form answers. Non-negotiable while in REMOTE, independent of whether the work was substantial or routine.\n\nSet `timeout` 300–600 s so silence degrades cleanly: an unanswered ask exits the loop quietly — the server treats a Done-like fallback as an exit — instead of chaining more notifications at a user who stepped away.\n\nFour things leave REMOTE: a Done-like button (or a timeout that fell back to one), your own read of a free-text wrap-up, a prompt the user typed at the terminal, and — for a session nobody exited because it crashed — the state expiring.\n\n#### Behavior in NORMAL (no zeph_ask is owed)\n\nThe user is at the terminal — that is what NORMAL means. Nothing here obliges an ask:\n\n- Questions go to `AskUserQuestion` or plain prose. The Ask hook still pushes them to the user's device, so a question is never lost.\n- Completion is the Stop hook's push.\n- `zeph_ask` remains available when you actively want an answer from their device — it is not owed, and never as a way to mark a turn finished.\n\nREMOTE begins the moment the user sends a message from their phone; from that turn on, Rules 1, 2, 8 and 9 are in force.\n\n### When to use AskUserQuestion vs zeph_ask\n\n8. **While in REMOTE, a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion`.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"proceed?\"). `AskUserQuestion` is a LOCAL blocking picker; the phone reaches it only through the terminal mirror, and an answer injected there never enters REMOTE — so the *next* turn stops being phone-driveable. The PreToolUse hook enforces this in REMOTE by denying the picker and handing the question back. (Why the mirror is the worse channel on every axis: README → \"AskUserQuestion vs zeph_ask\".)\n\n **In NORMAL the picker is the right tool** and the hook lets it through — the user is at the terminal, and the Ask hook still pushes the question to their device so they know one is waiting.\n\n9. **In REMOTE this overrides any skill instruction.** If a skill you are running — or your own plan — would call `AskUserQuestion` with a button-friendly question, surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Fall through to the picker ONLY when (a) the answer needs code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n10. These rules persist for the entire session. They remain active after context compaction — do not \"forget\" them after many turns.";
|
|
13
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\
|
|
14
|
+
exports.ZEPH_CORE_RULE_ONLY = "### When zeph_ask is MANDATORY\n\n1. **While in REMOTE, 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 holds even on research / analysis / planning turns where no files were touched.\n\n **In NORMAL it does not apply.** Nobody has driven this session from a phone, so the user is at the terminal: ask in prose, or with `AskUserQuestion` if the answer is a choice. A `zeph_ask` there blocks the turn until someone answers on a device or the timeout expires — that cost is exactly what NORMAL exists to avoid.\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n2. **In REMOTE, `zeph_ask` is the DEFAULT end of EVERY response** — not conditioned on the work being substantial (Rule 7).\n\n **In NORMAL, end with nothing.** The Stop hook's push is the completion signal. Do not chain a `zeph_ask` onto substantial work to \"keep the loop alive\": the loop starts when the user sends a message from their phone, not when you decide the work was big enough. `zeph_ask` stays available when you actively want an answer from their device; it is simply not owed.\n\n3. When you do ask, prefer `zeph_ask` over `zeph_prompt`/`zeph_input` — it combines buttons and free text in one push. **`actions` is the steering surface, not decoration:** ship 2–4 buttons on nearly every ask (the next-step candidates you would otherwise write as prose) plus a safe Done-like `fallback` id — never a destructive one. Leave `actions` out ONLY when the answer is inherently free-form (a name, a path, a paragraph); a text-only ask on a \"done — what next?\" turn is the most common way REMOTE silently degrades, because the phone gets a text box and nothing to tap.\n\n ```\n zeph_ask({\n title: \"Slice done — next?\",\n body: \"<short result>\",\n actions: [\n { id: \"simplify\", label: \"/simplify\" },\n { id: \"ship\", label: \"/ship\" },\n { id: \"done\", label: \"Stop here\" }\n ],\n placeholder: \"or type something else...\",\n fallback: \"done\"\n })\n ```\n\n4. Rule 7 says when to send one. Never send an ask just to mark a turn finished.\n\n### Handling the response\n\n5. 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\n6. Caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If that step would destroy user code, data, or infrastructure (force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk in a targeted `zeph_ask` first — title \"About to force-push main — proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 7)\n\n7. **The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky — every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it** as `zephState: \"REMOTE\" | \"NORMAL\"`: any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it**, where installed (this plugin, or the hooks `zeph setup` writes for Gemini/Codex): a remote-origin note on the turn a phone message arrives, and a note that the session has LEFT REMOTE on the first turn the user types at the terminal. A phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook, so a prompt with no phone marker is demonstrably the user's own keyboard — staying in REMOTE would answer the terminal with a phone loop, and re-entry costs them one message from the phone.\n- **Neither one present → NORMAL.**\n\n**The one call left to you is free text**, because it is the one signal no hook can read: the server cannot tell \"run the tests\" from \"thanks, that's it\". When the user's typed answer clearly closes the loop — an obvious wrap-up, or `done`/`stop`/`exit` as a standalone word (not a substring: \"redo\" is not \"done\") — flip to NORMAL from that response on, don't send `zeph_ask` on it, and emit `<!-- zeph: exit -->` once so the hooks agree with you. Your own flip is what ends the loop; the marker is how you tell a hook that cannot read your mind, and it is separate from any push-volume marker, which says nothing about the mode.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`, carrying 2–4 `actions` (the next-step candidates as buttons) plus a Done-like `fallback` — never a destructive one, since an unanswered ask resolves to it — a text-only ask is only for inherently free-form answers. Non-negotiable while in REMOTE, independent of whether the work was substantial or routine.\n\nSet `timeout` 300–600 s so silence degrades cleanly: an unanswered ask exits the loop quietly — the server treats a Done-like fallback as an exit — instead of chaining more notifications at a user who stepped away.\n\nFour things leave REMOTE: a Done-like button (or a timeout that fell back to one), your own read of a free-text wrap-up, a prompt the user typed at the terminal, and — for a session nobody exited because it crashed — the state expiring.\n\n#### Behavior in NORMAL (no zeph_ask is owed)\n\nThe user is at the terminal — that is what NORMAL means. Nothing here obliges an ask:\n\n- Questions go to `AskUserQuestion` or plain prose. The Ask hook still pushes them to the user's device, so a question is never lost.\n- Completion is the Stop hook's push.\n- `zeph_ask` remains available when you actively want an answer from their device — it is not owed, and never as a way to mark a turn finished.\n\nREMOTE begins the moment the user sends a message from their phone; from that turn on, Rules 1, 2, 8 and 9 are in force.\n\n### When to use AskUserQuestion vs zeph_ask\n\n8. **While in REMOTE, a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion`.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"proceed?\"). `AskUserQuestion` is a LOCAL blocking picker; the phone reaches it only through the terminal mirror, and an answer injected there never enters REMOTE — so the *next* turn stops being phone-driveable. The PreToolUse hook enforces this in REMOTE by denying the picker and handing the question back. (Why the mirror is the worse channel on every axis: README → \"AskUserQuestion vs zeph_ask\".)\n\n **In NORMAL the picker is the right tool** and the hook lets it through — the user is at the terminal, and the Ask hook still pushes the question to their device so they know one is waiting.\n\n9. **In REMOTE this overrides any skill instruction.** If a skill you are running — or your own plan — would call `AskUserQuestion` with a button-friendly question, surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Fall through to the picker ONLY when (a) the answer needs code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n10. 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": "2.
|
|
3
|
+
"version": "2.10.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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^22.0.0",
|
|
32
32
|
"@types/ws": "^8.18.1",
|
|
33
|
+
"esbuild": "^0.21.5",
|
|
33
34
|
"typescript": "^5.8.0",
|
|
34
35
|
"vitest": "^2.1.9"
|
|
35
36
|
},
|