@hwip/cli 0.1.11 → 0.1.12

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
@@ -239,13 +239,16 @@ back when it writes again.
239
239
  Detection is exact-match: a terminal keystroke racing a phone message
240
240
  can never false-flag. Muted projects are never flagged.
241
241
 
242
- Agents without that hook (Cursor, Windsurf, Copilot, Cline, Aider, OpenCode) have one
242
+ Agents without that hook (Cursor, Windsurf, Copilot, Cline, Aider) have one
243
243
  other way in: a `hwip_ask` answer reporting `hwipState: "REMOTE"`. Their rule
244
244
  files therefore keep the after-real-work `hwip_ask` — the "Entering REMOTE
245
245
  without a prompt hook" preamble in `src/templates.ts` — so the phone always
246
246
  has a button to tap that starts the loop. The one exception is an answer sent
247
247
  with the phone's "send and exit": that turn carries out the instruction and
248
- ends without an ask. Agents with the hook drop that
248
+ ends without an ask. OpenCode has no prompt hook either but leaves the
249
+ preamble out: its after-work ask reached the phone on turns typed at the
250
+ terminal, so it owes no ask in NORMAL, and a phone message to an OpenCode
251
+ session gets the completion push rather than buttons. Agents with the hook drop that
249
252
  obligation while at the terminal — and their rule file carries only the
250
253
  NORMAL branch of the core (about a quarter of it), since it is read on every
251
254
  turn; the hook injects the REMOTE sections in full on the turn a phone
@@ -685,7 +688,9 @@ A tmux name is used again when an agent starts in the same folder, so each
685
688
  chunk also names its **run** — the tmux session it was recorded in (`runId`,
686
689
  tmux's `session_created` in epoch seconds). The app uses it to keep one run's
687
690
  conversation apart from the one before it under the same name; a chunk read
688
- after a restart keeps the run it was recorded in. The listener's record of past
691
+ after a restart keeps the run it was recorded in. What a session writes with no
692
+ run to name (a name whose tmux session has ended while its file is still written
693
+ to) is not recorded: the server takes no chunk without a run. The listener's record of past
689
694
  sessions keeps runs apart the same way: closing `hwip cc` and starting it again
690
695
  in the same folder leaves the first run as a past session of its own (the
691
696
  newest ten runs of a name are kept, 100 in all, and up to 50 are reported to
@@ -16,7 +16,16 @@ export declare const CLINE_RULE: string;
16
16
  export declare const AIDER_RULE: string;
17
17
  /** Pi — managed block in ~/.pi/agent/AGENTS.md. Extension = Stop-equivalent + prompt hook + waiting-on-you push (PI_EXTENSION), so NORMAL-only core. */
18
18
  export declare const PI_RULE: string;
19
- /** OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook (v1). */
19
+ /**
20
+ * OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook.
21
+ *
22
+ * No REMOTE_ENTRY_NO_HOOK, unlike the other hook-less agents: its after-work
23
+ * ask landed on the phone while the user sat at the terminal (2026-10-29,
24
+ * OpenCode 2.1.3), and the 2.x plugin API (event stream only) has no per-turn
25
+ * hook to tell the two apart. So a NORMAL session owes no ask, and REMOTE
26
+ * starts only from an ask the agent chose to send. Full core, because no hook
27
+ * delivers the REMOTE sections on entry.
28
+ */
20
29
  export declare const OPENCODE_RULE: string;
21
30
  export declare const CURSOR_HOOKS: {
22
31
  version: number;
package/dist/templates.js CHANGED
@@ -21,7 +21,8 @@
21
21
  // hook note (a phone message) or by a `hwip_ask` answer. Only Gemini, Codex
22
22
  // and Pi have that hook (remoteHookCmd below). Every other agent's only door is the
23
23
  // ask itself — so for them the NORMAL branch must still send one after real
24
- // work, or the phone loop can never begin. That is REMOTE_ENTRY_NO_HOOK.
24
+ // work, or the phone loop can never begin. That is REMOTE_ENTRY_NO_HOOK
25
+ // (OpenCode excepted — OPENCODE_RULE says why).
25
26
  //
26
27
  // The same axis decides how much of the core the static rule file carries.
27
28
  // That file is read on every turn, and a session is NORMAL nearly all of the
@@ -125,7 +126,7 @@ refactor, multi-file changes) call hwip_notify. Skip it for trivial
125
126
  operations (file reads, simple searches). Set priority "high" for
126
127
  errors/blockers.`;
127
128
  // REMOTE-entry preamble — agents with NO prompt-submit hook (Cursor, Windsurf,
128
- // Copilot, Cline, Aider, OpenCode). The shared core scopes Rules 1/2/8/9 to REMOTE
129
+ // Copilot, Cline, Aider; OpenCode opts out, see OPENCODE_RULE). The shared core scopes Rules 1/2/8/9 to REMOTE
129
130
  // and tells a NORMAL session it owes no `hwip_ask`. That is right where a
130
131
  // hook can announce the phone message that starts REMOTE; here nothing can,
131
132
  // and the only remaining entry is a `hwip_ask` answer reporting REMOTE.
@@ -266,10 +267,18 @@ exports.PI_RULE = buildRule({
266
267
  pushSignal: PI_PUSH_SIGNAL,
267
268
  core: PROMPT_HOOK_CORE,
268
269
  });
269
- /** OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook (v1). */
270
+ /**
271
+ * OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook.
272
+ *
273
+ * No REMOTE_ENTRY_NO_HOOK, unlike the other hook-less agents: its after-work
274
+ * ask landed on the phone while the user sat at the terminal (2026-10-29,
275
+ * OpenCode 2.1.3), and the 2.x plugin API (event stream only) has no per-turn
276
+ * hook to tell the two apart. So a NORMAL session owes no ask, and REMOTE
277
+ * starts only from an ask the agent chose to send. Full core, because no hook
278
+ * delivers the REMOTE sections on entry.
279
+ */
270
280
  exports.OPENCODE_RULE = buildRule({
271
281
  notify: HOOK_DRIVEN_NOTIFY,
272
- remoteEntry: REMOTE_ENTRY_NO_HOOK,
273
282
  core: hwip_core_generated_js_1.HWIP_CORE_HOOK_DRIVEN,
274
283
  });
275
284
  // ── Hook configs ─────────────────────────────────────────────────
@@ -98,7 +98,7 @@ const createChunkUploader = (opts) => {
98
98
  try {
99
99
  res = await (0, api_js_1.apiFetch)(fetchFn, (0, api_js_1.apiUrl)(opts.baseUrl, `/sessions/${encodeURIComponent(sessionKey)}/turns`), opts.apiKey, UPLOAD_TIMEOUT_MS, {
100
100
  method: 'POST',
101
- body: JSON.stringify({ seq: chunk.seq, firstAt: chunk.firstAt, lastAt: chunk.lastAt, ...(chunk.runId ? { runId: chunk.runId } : {}), sealed: chunk.sealed }),
101
+ body: JSON.stringify({ seq: chunk.seq, firstAt: chunk.firstAt, lastAt: chunk.lastAt, runId: chunk.runId, sealed: chunk.sealed }),
102
102
  });
103
103
  }
104
104
  catch (err) {
@@ -98,8 +98,8 @@ export interface OutboxChunk {
98
98
  seq: number;
99
99
  firstAt: string;
100
100
  lastAt: string;
101
- /** The run the events were recorded in. Absent from chunks made before runs were recorded. */
102
- runId?: string;
101
+ /** The run the events were recorded in: the server takes no chunk without one. */
102
+ runId: string;
103
103
  sealed: SealedChunk;
104
104
  }
105
105
  export type UploadOutcome =
@@ -90,7 +90,7 @@ const isOutboxChunk = (c) => {
90
90
  const o = c;
91
91
  const s = o.sealed;
92
92
  return (Number.isSafeInteger(o.seq) && typeof o.firstAt === 'string' && typeof o.lastAt === 'string' &&
93
- !!s && typeof s.body === 'string' && typeof s.senderPublicKey === 'string' &&
93
+ typeof o.runId === 'string' && !!o.runId && !!s && typeof s.body === 'string' && typeof s.senderPublicKey === 'string' &&
94
94
  !!s.deviceKeyMap && typeof s.deviceKeyMap === 'object');
95
95
  };
96
96
  /** What is on disk, or null when it is missing or not ours (a bad file is a fresh start, never a crash). */
@@ -273,13 +273,21 @@ const createTurnRecorder = (deps) => {
273
273
  const chunks = [];
274
274
  const runId = runOf(live, live.tailPath);
275
275
  let seq = base.seq;
276
+ // No run to name (a session name that ended, its file still written to): the server takes no such chunk,
277
+ // so what was read is passed over rather than sealed and sent to be refused.
278
+ if (!runId && !live.noRunWarned) {
279
+ live.noRunWarned = true;
280
+ deps.log(`⧉ turn-recorder ${live.name}: no run to tag its chunks with — what it writes is not recorded`);
281
+ }
276
282
  try {
277
283
  for (const page of pages) {
284
+ if (!runId)
285
+ break;
278
286
  seq += 1;
279
287
  const firstAt = eventAt(page, 'first', fallbackAt);
280
288
  const lastAt = eventAt(page, 'last', fallbackAt);
281
289
  const sealed = await deps.seal(JSON.stringify({ sessionKey: live.sessionKey, seq, events: page }), recipients);
282
- chunks.push({ seq, firstAt, lastAt, ...(runId ? { runId } : {}), sealed });
290
+ chunks.push({ seq, firstAt, lastAt, runId, sealed });
283
291
  }
284
292
  }
285
293
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hwip/cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Hwip CLI + push notification SDK for AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",