@zeph-to/cli 2.16.0 → 2.17.1
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 +27 -2
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.d.ts.map +1 -0
- package/dist/core-sections.js +28 -0
- package/dist/remote-hook.d.ts +1 -0
- package/dist/remote-hook.d.ts.map +1 -1
- package/dist/remote-hook.js +20 -2
- package/dist/templates.d.ts +5 -3
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +167 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,7 +154,10 @@ other way in: a `zeph_ask` answer that is not a Done-like button. Their rule
|
|
|
154
154
|
files therefore keep the after-real-work `zeph_ask` — the "Entering REMOTE
|
|
155
155
|
without a prompt hook" preamble in `src/templates.ts` — so the phone always
|
|
156
156
|
has a button to tap that starts the loop. Agents with the hook drop that
|
|
157
|
-
obligation while at the terminal
|
|
157
|
+
obligation while at the terminal — and their rule file carries only the
|
|
158
|
+
NORMAL branch of the core (about a quarter of it), since it is read on every
|
|
159
|
+
turn; the hook injects the REMOTE sections in full on the turn a phone
|
|
160
|
+
message enters REMOTE, the way the Claude Code plugin's hooks do.
|
|
158
161
|
|
|
159
162
|
`zeph cursor` has no remote-origin hook, so it never enters sticky REMOTE
|
|
160
163
|
mode by itself. **Just ask for it** — one line, once per session:
|
|
@@ -664,6 +667,28 @@ others cannot: a turn with no tool calls, a turn with exactly one (a
|
|
|
664
667
|
lone edit included — the gate wants two), and a turn whose calls were
|
|
665
668
|
all reads. `/zeph-loud` still pushes on all of them.
|
|
666
669
|
|
|
670
|
+
The Pi extension also pushes when pi is **waiting on you**: any blocking
|
|
671
|
+
extension dialog — a bash guard's Run/Abort, an ask-user tool, a
|
|
672
|
+
`ctx.ui.confirm` — that is still open 10 seconds after it appeared sends a
|
|
673
|
+
`high` push ("pi asks: <project>", body = the dialog title or the guarded
|
|
674
|
+
bash command). `high` gets through `quiet`; only `/zeph-mute` stops it.
|
|
675
|
+
Answering inside the 10 seconds sends nothing. It is the pi twin of the
|
|
676
|
+
Claude Code plugin's AskUserQuestion push, driven by pi's
|
|
677
|
+
`ui_prompt_start` / `ui_prompt_end` events. Only dialogs that open while a
|
|
678
|
+
turn is running count: a settings picker you open yourself between turns
|
|
679
|
+
(`/caveman config`) never pushes.
|
|
680
|
+
|
|
681
|
+
Two more end-of-turn cases differ from Claude Code:
|
|
682
|
+
|
|
683
|
+
- **Push Signal markers** — pi's rules teach the same `<!-- zeph: skip|push|high -->`
|
|
684
|
+
markers, and the extension reads them from the final assistant message
|
|
685
|
+
(then strips them, since pi's terminal would otherwise print the comment).
|
|
686
|
+
`high` is how pi gets a "the turn ended with a question for you" push
|
|
687
|
+
through `quiet`.
|
|
688
|
+
- **Errors and aborts** — a turn that ended on a provider error sends a
|
|
689
|
+
`high` "pi stopped: <project>" push with the error text instead of
|
|
690
|
+
"Task done"; a turn you aborted with Esc sends nothing.
|
|
691
|
+
|
|
667
692
|
#### Quiet while away
|
|
668
693
|
|
|
669
694
|
`quiet` silences routine pushes because you are watching the pane. When
|
|
@@ -789,7 +814,7 @@ try {
|
|
|
789
814
|
| Copilot CLI | Session end hook + rules |
|
|
790
815
|
| Cline | Rules file (`~/.cline/rules/zeph.md`) |
|
|
791
816
|
| Aider | Conventions file + `read:` directive in `~/.aider.conf.yml` |
|
|
792
|
-
| Pi | Extension (settle hook + remote detection) + rules — no MCP (pi has none; rules map zeph tools to the CLI) |
|
|
817
|
+
| Pi | Extension (settle hook + waiting-on-you push + remote detection) + rules — no MCP (pi has none; rules map zeph tools to the CLI) |
|
|
793
818
|
| OpenCode | MCP server + idle-notify plugin + rules |
|
|
794
819
|
|
|
795
820
|
For remote-control via `zeph listener` the per-agent setup is the same
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice one `### ` section out of a generated rule core (zeph-core.generated.ts).
|
|
3
|
+
*
|
|
4
|
+
* The core is one string per audience, assembled upstream from
|
|
5
|
+
* plugin/docs/CORE_RULES.md. Two consumers need single sections of it:
|
|
6
|
+
* templates.ts, which builds the NORMAL-only rule file for agents whose
|
|
7
|
+
* prompt-submit hook can announce REMOTE, and remote-hook.ts, which injects
|
|
8
|
+
* the REMOTE sections on the turn that enters it — the twin of the plugin's
|
|
9
|
+
* zeph-remote.sh reading CORE_RULES.md on entry.
|
|
10
|
+
*
|
|
11
|
+
* Headings are matched by title prefix (`Sticky REMOTE mode`), not by the
|
|
12
|
+
* `(Rule N)` suffix: the extractor renumbers per audience, so the number is
|
|
13
|
+
* not stable across cores. A heading the core does not carry throws — every
|
|
14
|
+
* caller passes a literal, so the throw surfaces at import time in the tests
|
|
15
|
+
* rather than as a silently empty rule file.
|
|
16
|
+
*/
|
|
17
|
+
export declare const coreSection: (core: string, heading: string) => string;
|
|
18
|
+
//# sourceMappingURL=core-sections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core-sections.d.ts","sourceRoot":"","sources":["../src/core-sections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,MAO3D,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.coreSection = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Slice one `### ` section out of a generated rule core (zeph-core.generated.ts).
|
|
6
|
+
*
|
|
7
|
+
* The core is one string per audience, assembled upstream from
|
|
8
|
+
* plugin/docs/CORE_RULES.md. Two consumers need single sections of it:
|
|
9
|
+
* templates.ts, which builds the NORMAL-only rule file for agents whose
|
|
10
|
+
* prompt-submit hook can announce REMOTE, and remote-hook.ts, which injects
|
|
11
|
+
* the REMOTE sections on the turn that enters it — the twin of the plugin's
|
|
12
|
+
* zeph-remote.sh reading CORE_RULES.md on entry.
|
|
13
|
+
*
|
|
14
|
+
* Headings are matched by title prefix (`Sticky REMOTE mode`), not by the
|
|
15
|
+
* `(Rule N)` suffix: the extractor renumbers per audience, so the number is
|
|
16
|
+
* not stable across cores. A heading the core does not carry throws — every
|
|
17
|
+
* caller passes a literal, so the throw surfaces at import time in the tests
|
|
18
|
+
* rather than as a silently empty rule file.
|
|
19
|
+
*/
|
|
20
|
+
const coreSection = (core, heading) => {
|
|
21
|
+
const sections = core.split(/^(?=### )/m).map((s) => s.trim());
|
|
22
|
+
const found = sections.filter((s) => s.startsWith(`### ${heading}`));
|
|
23
|
+
if (found.length !== 1) {
|
|
24
|
+
throw new Error(`core section "${heading}" ${found.length ? 'is ambiguous' : 'not found'} in the generated core`);
|
|
25
|
+
}
|
|
26
|
+
return found[0];
|
|
27
|
+
};
|
|
28
|
+
exports.coreSection = coreSection;
|
package/dist/remote-hook.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* NOT here — its hook ships with the plugin (hooks/zeph-remote.sh). */
|
|
3
3
|
export declare const REMOTE_HOOK_AGENTS: readonly ["gemini", "codex", "pi"];
|
|
4
4
|
export type RemoteHookAgent = (typeof REMOTE_HOOK_AGENTS)[number];
|
|
5
|
+
export declare const remoteEntrySections: () => string;
|
|
5
6
|
export declare const isRemoteHookAgent: (raw: string) => raw is RemoteHookAgent;
|
|
6
7
|
/**
|
|
7
8
|
* Core of the hook: match the stdin payload against the marker and return
|
|
@@ -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":"AA4CA;wEACwE;AACxE,eAAO,MAAM,kBAAkB,oCAAqC,CAAC;AACrE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAkClE,eAAO,MAAM,mBAAmB,QAAO,MAGtB,CAAC;AAgBlB,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,IA2CX,CAAC"}
|
package/dist/remote-hook.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runRemoteHook = exports.isRemoteHookAgent = exports.REMOTE_HOOK_AGENTS = void 0;
|
|
3
|
+
exports.runRemoteHook = exports.isRemoteHookAgent = exports.remoteEntrySections = exports.REMOTE_HOOK_AGENTS = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* `zeph remote-hook <agent>` — prompt-submit hook handler for agents whose
|
|
6
6
|
* hooks the cli installs directly: Gemini CLI (BeforeAgent), Codex CLI
|
|
@@ -33,6 +33,8 @@ exports.runRemoteHook = exports.isRemoteHookAgent = exports.REMOTE_HOOK_AGENTS =
|
|
|
33
33
|
*/
|
|
34
34
|
const fs_1 = require("fs");
|
|
35
35
|
const config_js_1 = require("./config.js");
|
|
36
|
+
const core_sections_js_1 = require("./core-sections.js");
|
|
37
|
+
const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
|
|
36
38
|
const gate_js_1 = require("./gate.js");
|
|
37
39
|
/** Agents whose prompt-submit hooks this command serves. Claude Code is
|
|
38
40
|
* NOT here — its hook ships with the plugin (hooks/zeph-remote.sh). */
|
|
@@ -55,6 +57,21 @@ const FRESH_WINDOW_SEC = 900;
|
|
|
55
57
|
const TWO_WAY_CONTEXT = `# System note (Zeph remote-origin detect)
|
|
56
58
|
|
|
57
59
|
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.`;
|
|
60
|
+
// The static rule file of these agents carries the NORMAL branch only
|
|
61
|
+
// (templates.ts PROMPT_HOOK_CORE), so the turn that enters REMOTE is where the
|
|
62
|
+
// contract has to arrive in full — the twin of plugin/hooks/zeph-remote.sh
|
|
63
|
+
// reading CORE_RULES.md on entry. Read from the generated core rather than
|
|
64
|
+
// restated here: a copy would be one more place for the rule to drift. The
|
|
65
|
+
// four sections together cite only rules they carry (1–4, 7–9), so no
|
|
66
|
+
// cross-reference dangles; remote-hook.test.ts checks that. Sent once per
|
|
67
|
+
// entry: a later phone prompt on a session already in REMOTE gets the note
|
|
68
|
+
// alone, since the contract is already in its context. Sliced on that turn
|
|
69
|
+
// only — this command runs on every prompt submit, and the common path is a
|
|
70
|
+
// silent no-op. Exported for the tests.
|
|
71
|
+
const remoteEntrySections = () => ['When zeph_ask is MANDATORY', 'When zeph_ask is the DEFAULT', 'Sticky REMOTE mode', 'When to use AskUserQuestion vs zeph_ask']
|
|
72
|
+
.map((heading) => (0, core_sections_js_1.coreSection)(zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN, heading))
|
|
73
|
+
.join('\n\n');
|
|
74
|
+
exports.remoteEntrySections = remoteEntrySections;
|
|
58
75
|
const ONE_WAY_CONTEXT = `# System note (Zeph remote-origin detect)
|
|
59
76
|
|
|
60
77
|
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).`;
|
|
@@ -105,8 +122,9 @@ const runRemoteHook = (agent, stdin, env = process.env, now = Date.now) => {
|
|
|
105
122
|
return emit(ONE_WAY_CONTEXT);
|
|
106
123
|
// Only a two-way session has a mode to stay in — without zeph_ask there is
|
|
107
124
|
// nothing for a later turn to be reminded of, so no state is recorded.
|
|
125
|
+
const entering = !(0, gate_js_1.isRemoteActive)(cwd, now);
|
|
108
126
|
(0, gate_js_1.touchRemoteActive)(cwd, now);
|
|
109
|
-
return emit(TWO_WAY_CONTEXT);
|
|
127
|
+
return emit(entering ? `${TWO_WAY_CONTEXT}\n\n${(0, exports.remoteEntrySections)()}` : TWO_WAY_CONTEXT);
|
|
110
128
|
}
|
|
111
129
|
if (origin === 'keyboard' && (0, gate_js_1.isRemoteActive)(cwd, now) && (0, config_js_1.resolveHookId)(env)) {
|
|
112
130
|
(0, gate_js_1.clearRemoteActive)(cwd);
|
package/dist/templates.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
/** Grace before the pi extension turns an unanswered prompt into a push. Exported for the tests. */
|
|
2
|
+
export declare const PI_PROMPT_GRACE_MS = 10000;
|
|
1
3
|
/** Cursor — written to ~/.cursor/rules/zeph.mdc (needs .mdc frontmatter). No prompt hook. */
|
|
2
4
|
export declare const CURSOR_RULE: string;
|
|
3
5
|
/** Windsurf — appended into ~/.codeium/windsurf/memories/global_rules.md. No prompt hook. */
|
|
4
6
|
export declare const WINDSURF_RULE: string;
|
|
5
|
-
/** Gemini CLI — appended into ~/.gemini/GEMINI.md. Has the prompt hook (GEMINI_HOOKS). */
|
|
7
|
+
/** Gemini CLI — appended into ~/.gemini/GEMINI.md. Has the prompt hook (GEMINI_HOOKS), so NORMAL-only core. */
|
|
6
8
|
export declare const GEMINI_RULE: string;
|
|
7
|
-
/** Codex CLI — appended into ~/.codex/AGENTS.md. Has the prompt hook (CODEX_HOOKS). */
|
|
9
|
+
/** Codex CLI — appended into ~/.codex/AGENTS.md. Has the prompt hook (CODEX_HOOKS), so NORMAL-only core. */
|
|
8
10
|
export declare const CODEX_RULE: string;
|
|
9
11
|
/** GitHub Copilot CLI — written to ~/.copilot/instructions/zeph.instructions.md. No prompt hook. */
|
|
10
12
|
export declare const COPILOT_RULE: string;
|
|
@@ -12,7 +14,7 @@ export declare const COPILOT_RULE: string;
|
|
|
12
14
|
export declare const CLINE_RULE: string;
|
|
13
15
|
/** Aider — standalone conventions file via .aider.conf.yml `read:` (no Stop hook, no prompt hook). */
|
|
14
16
|
export declare const AIDER_RULE: string;
|
|
15
|
-
/** Pi — managed block in ~/.pi/agent/AGENTS.md. Extension = Stop-equivalent + prompt hook (PI_EXTENSION). */
|
|
17
|
+
/** Pi — managed block in ~/.pi/agent/AGENTS.md. Extension = Stop-equivalent + prompt hook + waiting-on-you push (PI_EXTENSION), so NORMAL-only core. */
|
|
16
18
|
export declare const PI_RULE: string;
|
|
17
19
|
/** OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook (v1). */
|
|
18
20
|
export declare const OPENCODE_RULE: string;
|
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":"AAwGA,oGAAoG;AACpG,eAAO,MAAM,kBAAkB,QAAS,CAAC;AA8IzC,6FAA6F;AAC7F,eAAO,MAAM,WAAW,QAKtB,CAAC;AAEH,6FAA6F;AAC7F,eAAO,MAAM,aAAa,QAIxB,CAAC;AAEH,+GAA+G;AAC/G,eAAO,MAAM,WAAW,QAAoE,CAAC;AAE7F,4GAA4G;AAC5G,eAAO,MAAM,UAAU,QAAoE,CAAC;AAE5F,oGAAoG;AACpG,eAAO,MAAM,YAAY,QAIvB,CAAC;AAEH,gFAAgF;AAChF,eAAO,MAAM,UAAU,QAIrB,CAAC;AAEH,sGAAsG;AACtG,eAAO,MAAM,UAAU,QAIrB,CAAC;AAEH,wJAAwJ;AACxJ,eAAO,MAAM,OAAO,QAKlB,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,QAgJxB,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
|
@@ -23,11 +23,20 @@
|
|
|
23
23
|
// ask itself — so for them the NORMAL branch must still send one after real
|
|
24
24
|
// work, or the phone loop can never begin. That is REMOTE_ENTRY_NO_HOOK.
|
|
25
25
|
//
|
|
26
|
+
// The same axis decides how much of the core the static rule file carries.
|
|
27
|
+
// That file is read on every turn, and a session is NORMAL nearly all of the
|
|
28
|
+
// time, so for the three hook agents it carries the NORMAL branch only
|
|
29
|
+
// (PROMPT_HOOK_CORE) — the REMOTE sections arrive from the hook on the turn
|
|
30
|
+
// that enters REMOTE (remote-hook.ts remoteEntrySections), the way the
|
|
31
|
+
// plugin's SessionStart / UserPromptSubmit pair does it for Claude Code. The
|
|
32
|
+
// hook-less agents have no such turn, so their file keeps the whole core.
|
|
33
|
+
//
|
|
26
34
|
// Keeping this in one place means a rule change lands everywhere at once
|
|
27
35
|
// and the agents can't drift apart.
|
|
28
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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;
|
|
37
|
+
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 = exports.PI_PROMPT_GRACE_MS = void 0;
|
|
30
38
|
const gate_js_1 = require("./gate.js");
|
|
39
|
+
const core_sections_js_1 = require("./core-sections.js");
|
|
31
40
|
const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
|
|
32
41
|
// Graceful resolution: prefer the installed `zeph` CLI, but fall back to
|
|
33
42
|
// `npx -y @zeph-to/cli` so the hook still fires when the user
|
|
@@ -73,6 +82,19 @@ const TURN_FACT_FLAGS = ` --${gate_js_1.TOOL_COUNT_FLAG} \${tools} --${gate_js_1
|
|
|
73
82
|
// stderr is discarded; `|| true` keeps a broken install from ever blocking
|
|
74
83
|
// a prompt.
|
|
75
84
|
const remoteHookCmd = (agent) => `$(command -v zeph || echo "npx -y @zeph-to/cli") remote-hook ${agent} 2>/dev/null || true`;
|
|
85
|
+
// One-off `high` push from the pi extension — a prompt left waiting (the twin
|
|
86
|
+
// of the plugin's zeph-ask.sh) or a turn that died on an error. `high` gets
|
|
87
|
+
// through the quiet dial, and plain `notify` (no --auto) means only a mute
|
|
88
|
+
// stops it. Title and body arrive as env vars, never spliced into the string:
|
|
89
|
+
// the body can carry the model's own bash command or a provider error.
|
|
90
|
+
const highNotifyCmd = '$(command -v zeph || echo "npx -y @zeph-to/cli") notify'
|
|
91
|
+
+ ' --title "$ZEPH_PUSH_TITLE" --body "$ZEPH_PUSH_BODY" --priority high 2>/dev/null || true';
|
|
92
|
+
// The pi turn's Push Signal marker, as a JS template placeholder (single quotes:
|
|
93
|
+
// `${marker}` must reach the artifact unexpanded). The artifact only ever
|
|
94
|
+
// assigns it a word the marker regex captured, or "none".
|
|
95
|
+
const PUSH_SIGNAL_FLAG = ' --marker ${marker}';
|
|
96
|
+
/** Grace before the pi extension turns an unanswered prompt into a push. Exported for the tests. */
|
|
97
|
+
exports.PI_PROMPT_GRACE_MS = 10_000;
|
|
76
98
|
// ── Shared behavioral core ───────────────────────────────────────
|
|
77
99
|
//
|
|
78
100
|
// GENERATED from plugin/docs/CORE_RULES.md — see src/zeph-core.generated.ts
|
|
@@ -137,10 +159,55 @@ tool instead — same semantics:
|
|
|
137
159
|
unreachable) is a Done-like outcome — treat it as NORMAL.
|
|
138
160
|
- zeph_notify → \`zeph notify --title "…" --body "…" [--priority high]\`
|
|
139
161
|
- AskUserQuestion → pi's own terminal prompt.`;
|
|
162
|
+
// Push Signal preamble — pi only. The shared core has no Push Signal section:
|
|
163
|
+
// Claude Code gets it from the plugin's SessionStart hook, per push dial. The
|
|
164
|
+
// pi extension reads the same markers (PI_EXTENSION, message_end), so pi's
|
|
165
|
+
// rules carry a dial-independent version here.
|
|
166
|
+
const PI_PUSH_SIGNAL = `## Push Signal — steer the end-of-turn push
|
|
167
|
+
|
|
168
|
+
When a turn ends, the zeph extension decides whether to push by tool volume
|
|
169
|
+
and the user's push dial. Put ONE marker anywhere in your final response to
|
|
170
|
+
override that for this turn. The extension removes it before the message is
|
|
171
|
+
shown or kept:
|
|
172
|
+
|
|
173
|
+
- \`<!-- zeph: high -->\` — push at high priority. The only marker that gets
|
|
174
|
+
through the quiet dial, so use it whenever the user must act: you ended the
|
|
175
|
+
turn with a question for them, or you are blocked on a decision only they
|
|
176
|
+
can make. Never on routine completions.
|
|
177
|
+
- \`<!-- zeph: push -->\` — push a turn the heuristic would skip.
|
|
178
|
+
- \`<!-- zeph: skip -->\` — no push for a turn not worth a ping.
|
|
179
|
+
|
|
180
|
+
No marker → the heuristic: fewer than 2 tool calls, or only read-only ones,
|
|
181
|
+
stays silent. Markers are lowercase and exact.`;
|
|
182
|
+
// NORMAL-only core — agents WITH a prompt-submit hook (Gemini, Codex, Pi).
|
|
183
|
+
// Mirrors plugin/hooks/zeph-setup.js `normal()`: the NORMAL branch owes no
|
|
184
|
+
// `zeph_ask`, so it carries none of the REMOTE-scoped sections (MANDATORY /
|
|
185
|
+
// DEFAULT / Sticky REMOTE / AskUserQuestion-vs) — about three quarters of the
|
|
186
|
+
// core, paid on every turn of a session that is almost always NORMAL. What it
|
|
187
|
+
// needs is the trigger: what flips the session, and what that turns on, stated
|
|
188
|
+
// here rather than pointed at because a `zeph_ask` answer reporting
|
|
189
|
+
// `zephState: "REMOTE"` flips it mid-turn with no hook to say so. The turn a
|
|
190
|
+
// phone message arrives, remote-hook.ts injects the REMOTE sections in full.
|
|
191
|
+
// Sections keep the numbers the core gave them (a render concern the extractor
|
|
192
|
+
// settled per audience; the plugin's NORMAL branch has the same gaps).
|
|
193
|
+
const REMOTE_STUB = `### What starts REMOTE
|
|
194
|
+
|
|
195
|
+
The user sending a message from their phone starts sticky REMOTE — the prompt-submit hook says so on that turn and injects the contract in full. A \`zeph_ask\` result reporting \`zephState: "REMOTE"\` starts it mid-turn.
|
|
196
|
+
|
|
197
|
+
From that response on: end EVERY response with \`zeph_ask\` (2–4 \`actions\` plus a Done-like \`fallback\`, \`timeout\` 300–600s), route button-friendly questions through it instead of \`AskUserQuestion\`, and never end on a plain-text question — until the user exits with a Done-like button, a free-text wrap-up you read as one (emit \`<!-- zeph: exit -->\` once), or a prompt they type at the terminal.`;
|
|
198
|
+
const PROMPT_HOOK_CORE = [
|
|
199
|
+
`## NORMAL — the user is at the terminal
|
|
200
|
+
|
|
201
|
+
Zeph can hand this session to the user's phone, but nobody has done that yet. **You owe no \`zeph_ask\`**: ask questions at the terminal or in prose, and let the end-of-turn push be the completion signal. A \`zeph_ask\` here blocks the turn until someone answers on a device or it times out.`,
|
|
202
|
+
(0, core_sections_js_1.coreSection)(zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN, 'Handling the response'),
|
|
203
|
+
REMOTE_STUB,
|
|
204
|
+
(0, core_sections_js_1.coreSection)(zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN, 'Persistence'),
|
|
205
|
+
].join('\n\n');
|
|
140
206
|
/** Assemble a full rule document from optional frontmatter + preambles + core. */
|
|
141
207
|
const buildRule = (opts) => {
|
|
142
208
|
const fm = opts.frontmatter ? `${opts.frontmatter}\n\n` : '';
|
|
143
209
|
const tools = opts.toolAccess ? `${opts.toolAccess}\n\n` : '';
|
|
210
|
+
const signal = opts.pushSignal ? `${opts.pushSignal}\n\n` : '';
|
|
144
211
|
const entry = opts.remoteEntry ? `${opts.remoteEntry}\n\n` : '';
|
|
145
212
|
return `${fm}# Zeph — Remote-Control Rules
|
|
146
213
|
|
|
@@ -150,7 +217,7 @@ the user.
|
|
|
150
217
|
|
|
151
218
|
${opts.notify}
|
|
152
219
|
|
|
153
|
-
${tools}${entry}${opts.core}
|
|
220
|
+
${tools}${signal}${entry}${opts.core}
|
|
154
221
|
`;
|
|
155
222
|
};
|
|
156
223
|
// ── Per-agent rule documents ─────────────────────────────────────
|
|
@@ -171,10 +238,10 @@ exports.WINDSURF_RULE = buildRule({
|
|
|
171
238
|
remoteEntry: REMOTE_ENTRY_NO_HOOK,
|
|
172
239
|
core: zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN,
|
|
173
240
|
});
|
|
174
|
-
/** Gemini CLI — appended into ~/.gemini/GEMINI.md. Has the prompt hook (GEMINI_HOOKS). */
|
|
175
|
-
exports.GEMINI_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY, core:
|
|
176
|
-
/** Codex CLI — appended into ~/.codex/AGENTS.md. Has the prompt hook (CODEX_HOOKS). */
|
|
177
|
-
exports.CODEX_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY, core:
|
|
241
|
+
/** Gemini CLI — appended into ~/.gemini/GEMINI.md. Has the prompt hook (GEMINI_HOOKS), so NORMAL-only core. */
|
|
242
|
+
exports.GEMINI_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY, core: PROMPT_HOOK_CORE });
|
|
243
|
+
/** Codex CLI — appended into ~/.codex/AGENTS.md. Has the prompt hook (CODEX_HOOKS), so NORMAL-only core. */
|
|
244
|
+
exports.CODEX_RULE = buildRule({ notify: HOOK_DRIVEN_NOTIFY, core: PROMPT_HOOK_CORE });
|
|
178
245
|
/** GitHub Copilot CLI — written to ~/.copilot/instructions/zeph.instructions.md. No prompt hook. */
|
|
179
246
|
exports.COPILOT_RULE = buildRule({
|
|
180
247
|
notify: HOOK_DRIVEN_NOTIFY,
|
|
@@ -193,11 +260,12 @@ exports.AIDER_RULE = buildRule({
|
|
|
193
260
|
remoteEntry: REMOTE_ENTRY_NO_HOOK,
|
|
194
261
|
core: zeph_core_generated_js_1.ZEPH_CORE_RULE_ONLY,
|
|
195
262
|
});
|
|
196
|
-
/** Pi — managed block in ~/.pi/agent/AGENTS.md. Extension = Stop-equivalent + prompt hook (PI_EXTENSION). */
|
|
263
|
+
/** Pi — managed block in ~/.pi/agent/AGENTS.md. Extension = Stop-equivalent + prompt hook + waiting-on-you push (PI_EXTENSION), so NORMAL-only core. */
|
|
197
264
|
exports.PI_RULE = buildRule({
|
|
198
265
|
notify: HOOK_DRIVEN_NOTIFY,
|
|
199
266
|
toolAccess: PI_TOOL_ACCESS,
|
|
200
|
-
|
|
267
|
+
pushSignal: PI_PUSH_SIGNAL,
|
|
268
|
+
core: PROMPT_HOOK_CORE,
|
|
201
269
|
});
|
|
202
270
|
/** OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook (v1). */
|
|
203
271
|
exports.OPENCODE_RULE = buildRule({
|
|
@@ -320,20 +388,108 @@ const sh = (cmd: string, cwd: string, stdin?: string): Promise<string> =>
|
|
|
320
388
|
// an unknown tool errs toward pushing rather than toward silence.
|
|
321
389
|
const READ_ONLY = new Set(["read", "grep", "find", "ls"]);
|
|
322
390
|
|
|
391
|
+
// How long a blocking prompt may stay open before it becomes a push. An answer
|
|
392
|
+
// inside the window means the user is at the terminal, and a guard extension
|
|
393
|
+
// can raise several prompts a turn — pushing each one at once would be noise.
|
|
394
|
+
const PROMPT_GRACE_MS = ${exports.PI_PROMPT_GRACE_MS};
|
|
395
|
+
|
|
396
|
+
// Push Signal markers — the pattern the Claude Code Stop hook reads
|
|
397
|
+
// (plugin/hooks/zeph-stop.sh MARKER_RE). pi's markdown renders an HTML comment
|
|
398
|
+
// as plain text, so message_end also strips it from what the user sees.
|
|
399
|
+
const MARKER_RE = /<!--[ \\t]*zeph:[ \\t]*(skip|push|high)[ \\t]*-->/g;
|
|
400
|
+
|
|
401
|
+
const projectOf = (cwd: string): string => cwd.split("/").pop() || cwd;
|
|
402
|
+
|
|
403
|
+
// Fire-and-forget: never block pi on the notify network call.
|
|
404
|
+
const pushHigh = (cwd: string, title: string, body: string): void => {
|
|
405
|
+
const env = {
|
|
406
|
+
...process.env,
|
|
407
|
+
ZEPH_PUSH_TITLE: title,
|
|
408
|
+
ZEPH_PUSH_BODY: body.length > 200 ? body.slice(0, 199) + "…" : body,
|
|
409
|
+
};
|
|
410
|
+
const child = spawn("sh", ["-c", ${JSON.stringify(highNotifyCmd)}], { cwd, env, stdio: "ignore", detached: true });
|
|
411
|
+
child.on("error", () => {});
|
|
412
|
+
child.unref();
|
|
413
|
+
};
|
|
414
|
+
|
|
323
415
|
export default function (pi: ExtensionAPI) {
|
|
324
416
|
// Turn facts for the push gate. One agent per process, so plain counters
|
|
325
417
|
// suffice — no session keying.
|
|
326
418
|
let tools = 0;
|
|
327
419
|
let nonReadonly = 0;
|
|
420
|
+
// The tool call in flight. tool_execution_start fires before pi runs the
|
|
421
|
+
// tool_call handlers, so a guard's prompt always finds its own call here.
|
|
422
|
+
let running: { toolName: string; args: any } | undefined;
|
|
423
|
+
let promptTimer: ReturnType<typeof setTimeout> | undefined;
|
|
424
|
+
// A prompt only counts inside a turn. Outside one it is a dialog the user
|
|
425
|
+
// opened at the terminal themselves (/caveman config, a settings picker).
|
|
426
|
+
let agentRunning = false;
|
|
427
|
+
// This turn's Push Signal, and why its last assistant message stopped.
|
|
428
|
+
let marker = "none";
|
|
429
|
+
let stop: { reason: string; error?: string } | undefined;
|
|
328
430
|
|
|
431
|
+
pi.on("agent_start", () => {
|
|
432
|
+
agentRunning = true;
|
|
433
|
+
});
|
|
434
|
+
pi.on("message_end", (event) => {
|
|
435
|
+
const message = event.message;
|
|
436
|
+
if (message.role !== "assistant") return;
|
|
437
|
+
stop = { reason: message.stopReason, error: message.errorMessage };
|
|
438
|
+
let found = false;
|
|
439
|
+
const content = message.content.map((part) => {
|
|
440
|
+
if (part.type !== "text") return part;
|
|
441
|
+
const text = part.text.replace(MARKER_RE, (_match: string, word: string) => {
|
|
442
|
+
marker = word;
|
|
443
|
+
found = true;
|
|
444
|
+
return "";
|
|
445
|
+
});
|
|
446
|
+
return text === part.text ? part : { ...part, text: text.trimEnd() };
|
|
447
|
+
});
|
|
448
|
+
if (found) return { message: { ...message, content } };
|
|
449
|
+
});
|
|
450
|
+
pi.on("tool_execution_start", (event) => {
|
|
451
|
+
running = event;
|
|
452
|
+
});
|
|
329
453
|
pi.on("tool_execution_end", (event) => {
|
|
454
|
+
running = undefined;
|
|
330
455
|
tools += 1;
|
|
331
456
|
if (!READ_ONLY.has(event.toolName)) nonReadonly += 1;
|
|
332
457
|
});
|
|
458
|
+
// Waiting-on-you: pi fires ui_prompt_start for every blocking extension
|
|
459
|
+
// dialog (select/confirm/input/editor/custom — bash guards, ask-user tools).
|
|
460
|
+
// A custom dialog carries no title, so name the call that raised it instead.
|
|
461
|
+
pi.on("ui_prompt_start", (event, ctx) => {
|
|
462
|
+
clearTimeout(promptTimer);
|
|
463
|
+
if (!agentRunning) return;
|
|
464
|
+
const command = running?.toolName === "bash" ? running.args?.command : undefined;
|
|
465
|
+
const body = event.title
|
|
466
|
+
?? (typeof command === "string" ? "$ " + command : undefined)
|
|
467
|
+
?? (running ? running.toolName + " is waiting for your answer" : "Waiting for your input");
|
|
468
|
+
promptTimer = setTimeout(() => {
|
|
469
|
+
promptTimer = undefined;
|
|
470
|
+
pushHigh(ctx.cwd, "pi asks: " + projectOf(ctx.cwd), body);
|
|
471
|
+
}, PROMPT_GRACE_MS);
|
|
472
|
+
promptTimer.unref?.();
|
|
473
|
+
});
|
|
474
|
+
pi.on("ui_prompt_end", () => {
|
|
475
|
+
clearTimeout(promptTimer);
|
|
476
|
+
promptTimer = undefined;
|
|
477
|
+
});
|
|
333
478
|
// Stop-equivalent: agent_settled fires once per user turn, after retries/compaction.
|
|
334
479
|
// Fire-and-forget: never block pi's turn-end on the notify network call.
|
|
335
480
|
pi.on("agent_settled", (_event, ctx) => {
|
|
336
|
-
|
|
481
|
+
agentRunning = false;
|
|
482
|
+
clearTimeout(promptTimer);
|
|
483
|
+
promptTimer = undefined;
|
|
484
|
+
// Esc (at the terminal, or a key sent from the phone): whoever stopped the
|
|
485
|
+
// turn is already looking at it.
|
|
486
|
+
if (stop?.reason === "aborted") return;
|
|
487
|
+
// A turn that died on a provider error is a blocker, not a completion.
|
|
488
|
+
if (stop?.reason === "error") {
|
|
489
|
+
pushHigh(ctx.cwd, "pi stopped: " + projectOf(ctx.cwd), stop.error || "The model request failed");
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const child = spawn("sh", ["-c", ${notifyCmdLiteral(TURN_FACT_FLAGS + PUSH_SIGNAL_FLAG)}], { cwd: ctx.cwd, stdio: "ignore", detached: true });
|
|
337
493
|
child.on("error", () => {});
|
|
338
494
|
child.unref();
|
|
339
495
|
});
|
|
@@ -343,6 +499,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
343
499
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
344
500
|
tools = 0;
|
|
345
501
|
nonReadonly = 0;
|
|
502
|
+
marker = "none";
|
|
503
|
+
stop = undefined;
|
|
346
504
|
const out = await sh(${JSON.stringify(remoteHookCmd('pi'))}, ctx.cwd,
|
|
347
505
|
JSON.stringify({ prompt: event.prompt, cwd: ctx.cwd }));
|
|
348
506
|
try {
|