@zeph-to/cli 2.16.0 → 2.17.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 CHANGED
@@ -664,6 +664,28 @@ others cannot: a turn with no tool calls, a turn with exactly one (a
664
664
  lone edit included — the gate wants two), and a turn whose calls were
665
665
  all reads. `/zeph-loud` still pushes on all of them.
666
666
 
667
+ The Pi extension also pushes when pi is **waiting on you**: any blocking
668
+ extension dialog — a bash guard's Run/Abort, an ask-user tool, a
669
+ `ctx.ui.confirm` — that is still open 10 seconds after it appeared sends a
670
+ `high` push ("pi asks: <project>", body = the dialog title or the guarded
671
+ bash command). `high` gets through `quiet`; only `/zeph-mute` stops it.
672
+ Answering inside the 10 seconds sends nothing. It is the pi twin of the
673
+ Claude Code plugin's AskUserQuestion push, driven by pi's
674
+ `ui_prompt_start` / `ui_prompt_end` events. Only dialogs that open while a
675
+ turn is running count: a settings picker you open yourself between turns
676
+ (`/caveman config`) never pushes.
677
+
678
+ Two more end-of-turn cases differ from Claude Code:
679
+
680
+ - **Push Signal markers** — pi's rules teach the same `<!-- zeph: skip|push|high -->`
681
+ markers, and the extension reads them from the final assistant message
682
+ (then strips them, since pi's terminal would otherwise print the comment).
683
+ `high` is how pi gets a "the turn ended with a question for you" push
684
+ through `quiet`.
685
+ - **Errors and aborts** — a turn that ended on a provider error sends a
686
+ `high` "pi stopped: <project>" push with the error text instead of
687
+ "Task done"; a turn you aborted with Esc sends nothing.
688
+
667
689
  #### Quiet while away
668
690
 
669
691
  `quiet` silences routine pushes because you are watching the pane. When
@@ -789,7 +811,7 @@ try {
789
811
  | Copilot CLI | Session end hook + rules |
790
812
  | Cline | Rules file (`~/.cline/rules/zeph.md`) |
791
813
  | 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) |
814
+ | Pi | Extension (settle hook + waiting-on-you push + remote detection) + rules — no MCP (pi has none; rules map zeph tools to the CLI) |
793
815
  | OpenCode | MCP server + idle-notify plugin + rules |
794
816
 
795
817
  For remote-control via `zeph listener` the per-agent setup is the same
@@ -1,3 +1,5 @@
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. */
@@ -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). */
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;
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AA6KA,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"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AA+FA,oGAAoG;AACpG,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAoHzC,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,mIAAmI;AACnI,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
@@ -26,7 +26,7 @@
26
26
  // Keeping this in one place means a rule change lands everywhere at once
27
27
  // and the agents can't drift apart.
28
28
  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;
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 = exports.PI_PROMPT_GRACE_MS = void 0;
30
30
  const gate_js_1 = require("./gate.js");
31
31
  const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
32
32
  // Graceful resolution: prefer the installed `zeph` CLI, but fall back to
@@ -73,6 +73,19 @@ const TURN_FACT_FLAGS = ` --${gate_js_1.TOOL_COUNT_FLAG} \${tools} --${gate_js_1
73
73
  // stderr is discarded; `|| true` keeps a broken install from ever blocking
74
74
  // a prompt.
75
75
  const remoteHookCmd = (agent) => `$(command -v zeph || echo "npx -y @zeph-to/cli") remote-hook ${agent} 2>/dev/null || true`;
76
+ // One-off `high` push from the pi extension — a prompt left waiting (the twin
77
+ // of the plugin's zeph-ask.sh) or a turn that died on an error. `high` gets
78
+ // through the quiet dial, and plain `notify` (no --auto) means only a mute
79
+ // stops it. Title and body arrive as env vars, never spliced into the string:
80
+ // the body can carry the model's own bash command or a provider error.
81
+ const highNotifyCmd = '$(command -v zeph || echo "npx -y @zeph-to/cli") notify'
82
+ + ' --title "$ZEPH_PUSH_TITLE" --body "$ZEPH_PUSH_BODY" --priority high 2>/dev/null || true';
83
+ // The pi turn's Push Signal marker, as a JS template placeholder (single quotes:
84
+ // `${marker}` must reach the artifact unexpanded). The artifact only ever
85
+ // assigns it a word the marker regex captured, or "none".
86
+ const PUSH_SIGNAL_FLAG = ' --marker ${marker}';
87
+ /** Grace before the pi extension turns an unanswered prompt into a push. Exported for the tests. */
88
+ exports.PI_PROMPT_GRACE_MS = 10_000;
76
89
  // ── Shared behavioral core ───────────────────────────────────────
77
90
  //
78
91
  // GENERATED from plugin/docs/CORE_RULES.md — see src/zeph-core.generated.ts
@@ -137,10 +150,31 @@ tool instead — same semantics:
137
150
  unreachable) is a Done-like outcome — treat it as NORMAL.
138
151
  - zeph_notify → \`zeph notify --title "…" --body "…" [--priority high]\`
139
152
  - AskUserQuestion → pi's own terminal prompt.`;
153
+ // Push Signal preamble — pi only. The shared core has no Push Signal section:
154
+ // Claude Code gets it from the plugin's SessionStart hook, per push dial. The
155
+ // pi extension reads the same markers (PI_EXTENSION, message_end), so pi's
156
+ // rules carry a dial-independent version here.
157
+ const PI_PUSH_SIGNAL = `## Push Signal — steer the end-of-turn push
158
+
159
+ When a turn ends, the zeph extension decides whether to push by tool volume
160
+ and the user's push dial. Put ONE marker anywhere in your final response to
161
+ override that for this turn. The extension removes it before the message is
162
+ shown or kept:
163
+
164
+ - \`<!-- zeph: high -->\` — push at high priority. The only marker that gets
165
+ through the quiet dial, so use it whenever the user must act: you ended the
166
+ turn with a question for them, or you are blocked on a decision only they
167
+ can make. Never on routine completions.
168
+ - \`<!-- zeph: push -->\` — push a turn the heuristic would skip.
169
+ - \`<!-- zeph: skip -->\` — no push for a turn not worth a ping.
170
+
171
+ No marker → the heuristic: fewer than 2 tool calls, or only read-only ones,
172
+ stays silent. Markers are lowercase and exact.`;
140
173
  /** Assemble a full rule document from optional frontmatter + preambles + core. */
141
174
  const buildRule = (opts) => {
142
175
  const fm = opts.frontmatter ? `${opts.frontmatter}\n\n` : '';
143
176
  const tools = opts.toolAccess ? `${opts.toolAccess}\n\n` : '';
177
+ const signal = opts.pushSignal ? `${opts.pushSignal}\n\n` : '';
144
178
  const entry = opts.remoteEntry ? `${opts.remoteEntry}\n\n` : '';
145
179
  return `${fm}# Zeph — Remote-Control Rules
146
180
 
@@ -150,7 +184,7 @@ the user.
150
184
 
151
185
  ${opts.notify}
152
186
 
153
- ${tools}${entry}${opts.core}
187
+ ${tools}${signal}${entry}${opts.core}
154
188
  `;
155
189
  };
156
190
  // ── Per-agent rule documents ─────────────────────────────────────
@@ -193,10 +227,11 @@ exports.AIDER_RULE = buildRule({
193
227
  remoteEntry: REMOTE_ENTRY_NO_HOOK,
194
228
  core: zeph_core_generated_js_1.ZEPH_CORE_RULE_ONLY,
195
229
  });
196
- /** Pi — managed block in ~/.pi/agent/AGENTS.md. Extension = Stop-equivalent + prompt hook (PI_EXTENSION). */
230
+ /** Pi — managed block in ~/.pi/agent/AGENTS.md. Extension = Stop-equivalent + prompt hook + waiting-on-you push (PI_EXTENSION). */
197
231
  exports.PI_RULE = buildRule({
198
232
  notify: HOOK_DRIVEN_NOTIFY,
199
233
  toolAccess: PI_TOOL_ACCESS,
234
+ pushSignal: PI_PUSH_SIGNAL,
200
235
  core: zeph_core_generated_js_1.ZEPH_CORE_HOOK_DRIVEN,
201
236
  });
202
237
  /** OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook (v1). */
@@ -320,20 +355,108 @@ const sh = (cmd: string, cwd: string, stdin?: string): Promise<string> =>
320
355
  // an unknown tool errs toward pushing rather than toward silence.
321
356
  const READ_ONLY = new Set(["read", "grep", "find", "ls"]);
322
357
 
358
+ // How long a blocking prompt may stay open before it becomes a push. An answer
359
+ // inside the window means the user is at the terminal, and a guard extension
360
+ // can raise several prompts a turn — pushing each one at once would be noise.
361
+ const PROMPT_GRACE_MS = ${exports.PI_PROMPT_GRACE_MS};
362
+
363
+ // Push Signal markers — the pattern the Claude Code Stop hook reads
364
+ // (plugin/hooks/zeph-stop.sh MARKER_RE). pi's markdown renders an HTML comment
365
+ // as plain text, so message_end also strips it from what the user sees.
366
+ const MARKER_RE = /<!--[ \\t]*zeph:[ \\t]*(skip|push|high)[ \\t]*-->/g;
367
+
368
+ const projectOf = (cwd: string): string => cwd.split("/").pop() || cwd;
369
+
370
+ // Fire-and-forget: never block pi on the notify network call.
371
+ const pushHigh = (cwd: string, title: string, body: string): void => {
372
+ const env = {
373
+ ...process.env,
374
+ ZEPH_PUSH_TITLE: title,
375
+ ZEPH_PUSH_BODY: body.length > 200 ? body.slice(0, 199) + "…" : body,
376
+ };
377
+ const child = spawn("sh", ["-c", ${JSON.stringify(highNotifyCmd)}], { cwd, env, stdio: "ignore", detached: true });
378
+ child.on("error", () => {});
379
+ child.unref();
380
+ };
381
+
323
382
  export default function (pi: ExtensionAPI) {
324
383
  // Turn facts for the push gate. One agent per process, so plain counters
325
384
  // suffice — no session keying.
326
385
  let tools = 0;
327
386
  let nonReadonly = 0;
387
+ // The tool call in flight. tool_execution_start fires before pi runs the
388
+ // tool_call handlers, so a guard's prompt always finds its own call here.
389
+ let running: { toolName: string; args: any } | undefined;
390
+ let promptTimer: ReturnType<typeof setTimeout> | undefined;
391
+ // A prompt only counts inside a turn. Outside one it is a dialog the user
392
+ // opened at the terminal themselves (/caveman config, a settings picker).
393
+ let agentRunning = false;
394
+ // This turn's Push Signal, and why its last assistant message stopped.
395
+ let marker = "none";
396
+ let stop: { reason: string; error?: string } | undefined;
328
397
 
398
+ pi.on("agent_start", () => {
399
+ agentRunning = true;
400
+ });
401
+ pi.on("message_end", (event) => {
402
+ const message = event.message;
403
+ if (message.role !== "assistant") return;
404
+ stop = { reason: message.stopReason, error: message.errorMessage };
405
+ let found = false;
406
+ const content = message.content.map((part) => {
407
+ if (part.type !== "text") return part;
408
+ const text = part.text.replace(MARKER_RE, (_match: string, word: string) => {
409
+ marker = word;
410
+ found = true;
411
+ return "";
412
+ });
413
+ return text === part.text ? part : { ...part, text: text.trimEnd() };
414
+ });
415
+ if (found) return { message: { ...message, content } };
416
+ });
417
+ pi.on("tool_execution_start", (event) => {
418
+ running = event;
419
+ });
329
420
  pi.on("tool_execution_end", (event) => {
421
+ running = undefined;
330
422
  tools += 1;
331
423
  if (!READ_ONLY.has(event.toolName)) nonReadonly += 1;
332
424
  });
425
+ // Waiting-on-you: pi fires ui_prompt_start for every blocking extension
426
+ // dialog (select/confirm/input/editor/custom — bash guards, ask-user tools).
427
+ // A custom dialog carries no title, so name the call that raised it instead.
428
+ pi.on("ui_prompt_start", (event, ctx) => {
429
+ clearTimeout(promptTimer);
430
+ if (!agentRunning) return;
431
+ const command = running?.toolName === "bash" ? running.args?.command : undefined;
432
+ const body = event.title
433
+ ?? (typeof command === "string" ? "$ " + command : undefined)
434
+ ?? (running ? running.toolName + " is waiting for your answer" : "Waiting for your input");
435
+ promptTimer = setTimeout(() => {
436
+ promptTimer = undefined;
437
+ pushHigh(ctx.cwd, "pi asks: " + projectOf(ctx.cwd), body);
438
+ }, PROMPT_GRACE_MS);
439
+ promptTimer.unref?.();
440
+ });
441
+ pi.on("ui_prompt_end", () => {
442
+ clearTimeout(promptTimer);
443
+ promptTimer = undefined;
444
+ });
333
445
  // Stop-equivalent: agent_settled fires once per user turn, after retries/compaction.
334
446
  // Fire-and-forget: never block pi's turn-end on the notify network call.
335
447
  pi.on("agent_settled", (_event, ctx) => {
336
- const child = spawn("sh", ["-c", ${notifyCmdLiteral(TURN_FACT_FLAGS)}], { cwd: ctx.cwd, stdio: "ignore", detached: true });
448
+ agentRunning = false;
449
+ clearTimeout(promptTimer);
450
+ promptTimer = undefined;
451
+ // Esc (at the terminal, or a key sent from the phone): whoever stopped the
452
+ // turn is already looking at it.
453
+ if (stop?.reason === "aborted") return;
454
+ // A turn that died on a provider error is a blocker, not a completion.
455
+ if (stop?.reason === "error") {
456
+ pushHigh(ctx.cwd, "pi stopped: " + projectOf(ctx.cwd), stop.error || "The model request failed");
457
+ return;
458
+ }
459
+ const child = spawn("sh", ["-c", ${notifyCmdLiteral(TURN_FACT_FLAGS + PUSH_SIGNAL_FLAG)}], { cwd: ctx.cwd, stdio: "ignore", detached: true });
337
460
  child.on("error", () => {});
338
461
  child.unref();
339
462
  });
@@ -343,6 +466,8 @@ export default function (pi: ExtensionAPI) {
343
466
  pi.on("before_agent_start", async (event, ctx) => {
344
467
  tools = 0;
345
468
  nonReadonly = 0;
469
+ marker = "none";
470
+ stop = undefined;
346
471
  const out = await sh(${JSON.stringify(remoteHookCmd('pi'))}, ctx.cwd,
347
472
  JSON.stringify({ prompt: event.prompt, cwd: ctx.cwd }));
348
473
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/cli",
3
- "version": "2.16.0",
3
+ "version": "2.17.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",