@trygocode/notify 0.4.0 → 0.5.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 +53 -4
- package/dist/src/cursor.js +113 -7
- package/dist/src/opencode.js +64 -15
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,14 +128,14 @@ The installed rule/skill tells the agent **not** to call the MCP tool for
|
|
|
128
128
|
done/idle/error pings — those are owned by the deterministic hook (A), so you
|
|
129
129
|
never get double-pinged.
|
|
130
130
|
|
|
131
|
-
> **Cursor `stop.status` → kind mapping (since 0.3.0
|
|
132
|
-
> carries a `status` field in its stdin JSON. The `on-stop`
|
|
133
|
-
> to the right notification kind automatically:
|
|
131
|
+
> **Cursor `stop.status` → kind mapping (since 0.3.0; `aborted` corrected 0.4.0).**
|
|
132
|
+
> Cursor's `stop` hook carries a `status` field in its stdin JSON. The `on-stop`
|
|
133
|
+
> dispatcher maps it to the right notification kind automatically:
|
|
134
134
|
>
|
|
135
135
|
> | Cursor `stop.status` | Notification kind | Meaning |
|
|
136
136
|
> |---|---|---|
|
|
137
137
|
> | `completed` | `finished` | Agent turned cleanly (default — same as before 0.3.0) |
|
|
138
|
-
> | `aborted` |
|
|
138
|
+
> | `aborted` | _(suppressed — no notification)_ | You pressed **Stop** to interrupt the turn — a manual stop sends nothing |
|
|
139
139
|
> | `error` | `error` | Agent hit an error |
|
|
140
140
|
> | absent / unrecognised | `finished` | Back-compatible fallback |
|
|
141
141
|
>
|
|
@@ -143,6 +143,28 @@ never get double-pinged.
|
|
|
143
143
|
> did. The status mapping requires no hook re-installation — run
|
|
144
144
|
> `gocode-notify setup --force` once to apply the updated hook command.
|
|
145
145
|
|
|
146
|
+
> **Question detection — "Agent needs you" (since 0.5.0).** When the agent
|
|
147
|
+
> **pauses to ask you a question** (as opposed to finishing its work), the
|
|
148
|
+
> package fires an `awaiting_input` ping titled **"Agent needs you"** instead of
|
|
149
|
+
> a misleading `finished`. The available signal differs per runtime:
|
|
150
|
+
>
|
|
151
|
+
> | Runtime | Question signal | Status |
|
|
152
|
+
> |---|---|---|
|
|
153
|
+
> | **Claude Code** | dedicated `Notification` hook → `awaiting_input` | ✅ Live — questions show as questions |
|
|
154
|
+
> | **OpenCode** | `permission.asked` plugin event → `awaiting_input` | ✅ Live — OpenCode genuinely exposes a "needs you" signal |
|
|
155
|
+
> | **Cursor** | `postToolUse` hook scoped to the `AskQuestion` tool → `awaiting_input` | ⏳ Installed but **dormant** — see note |
|
|
156
|
+
>
|
|
157
|
+
> **Cursor caveat:** Cursor currently exposes **no** "agent needs input" event
|
|
158
|
+
> (its docs list `Notification` as unsupported), and there is a
|
|
159
|
+
> [confirmed-open Cursor bug](https://forum.cursor.com/t/askquestion-tool-does-not-trigger-cursor-hooks/152230)
|
|
160
|
+
> where the `AskQuestion` tool fires **zero** hooks. So a Cursor question is
|
|
161
|
+
> indistinguishable from a completion at the only signal we get (the `stop`
|
|
162
|
+
> hook), and you'll receive `finished` until Cursor ships their fix. We install
|
|
163
|
+
> the `postToolUse(AskQuestion)` hook **now** so that the moment Cursor fixes
|
|
164
|
+
> the bug, question detection **auto-activates with no new install** for everyone
|
|
165
|
+
> already running `@trygocode/notify@latest`. Run `gocode-notify setup --force`
|
|
166
|
+
> once to wire it in.
|
|
167
|
+
|
|
146
168
|
> **OpenCode's hook (A)** is a small `session.idle` plugin written to
|
|
147
169
|
> `~/.config/opencode/plugin/gocode-notify.js`; on each `session.idle` event it
|
|
148
170
|
> fire-and-forgets `gocode-notify on-stop --source opencode` (the same
|
|
@@ -507,6 +529,33 @@ npm publish, real-device E2E), see
|
|
|
507
529
|
|
|
508
530
|
## Changelog
|
|
509
531
|
|
|
532
|
+
### 0.5.0
|
|
533
|
+
|
|
534
|
+
- **Question detection across ALL IDEs — "Agent needs you" vs "finished".** When
|
|
535
|
+
the agent **pauses to ask you a question**, you now get an `awaiting_input`
|
|
536
|
+
("Agent needs you") notification instead of a misleading `finished`, on every
|
|
537
|
+
supported runtime that exposes a question signal:
|
|
538
|
+
- **Claude Code** — already wired (`Notification` hook); unchanged.
|
|
539
|
+
- **OpenCode** — NEW: the plugin now listens to `permission.asked` (the moment
|
|
540
|
+
the agent raises a request) and fires `awaiting_input`. OpenCode genuinely
|
|
541
|
+
exposes a "the agent needs the user" signal via permission events. (We listen
|
|
542
|
+
to `permission.asked` only, not `permission.updated`/`replied`, which also
|
|
543
|
+
fire after the user answers.)
|
|
544
|
+
- **Cursor** — NEW (dormant): installs a `postToolUse` hook scoped to the
|
|
545
|
+
`AskQuestion` / `AskUserQuestion` tool. Cursor has a
|
|
546
|
+
[confirmed-open bug](https://forum.cursor.com/t/askquestion-tool-does-not-trigger-cursor-hooks/152230)
|
|
547
|
+
where that tool fires zero hooks, and Cursor exposes no `Notification` event,
|
|
548
|
+
so the hook is dormant today — but it **auto-activates with no new install**
|
|
549
|
+
the moment Cursor ships their fix. Until then, Cursor keeps its `finished`
|
|
550
|
+
fallback.
|
|
551
|
+
- **Corrected the documented Cursor `aborted` mapping.** The README table
|
|
552
|
+
previously (incorrectly) showed `aborted → awaiting_input`; the actual
|
|
553
|
+
behaviour (since the 0.4.0 fix) is that a manual **Stop** press (`aborted`)
|
|
554
|
+
sends **no notification at all**. A user force-stop is now clearly
|
|
555
|
+
distinguished from a genuine IDE question.
|
|
556
|
+
- Run `gocode-notify setup --force` once to wire the new hooks into Cursor +
|
|
557
|
+
OpenCode.
|
|
558
|
+
|
|
510
559
|
### 0.4.0
|
|
511
560
|
|
|
512
561
|
- **New: branded desktop notifications (on by default).** Every event that pings
|
package/dist/src/cursor.js
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
// Cursor. It does three things, all idempotently and without clobbering the
|
|
3
3
|
// user's existing config:
|
|
4
4
|
//
|
|
5
|
-
// 1. MERGE
|
|
6
|
-
// stop
|
|
7
|
-
//
|
|
5
|
+
// 1. MERGE two fire-and-forget hooks into `~/.cursor/hooks.json`:
|
|
6
|
+
// stop → "finished" (the agent completed a turn)
|
|
7
|
+
// postToolUse(AskQuestion) → "awaiting_input" (the agent asked a question)
|
|
8
|
+
// The commands shell out to `gocode-notify … || true` so a failed push
|
|
8
9
|
// NEVER blocks the agent's turn (PRD §4.4, §5.4). The file's `version` is
|
|
9
|
-
// preserved (or set to 1 when creating it fresh).
|
|
10
|
+
// preserved (or set to 1 when creating it fresh). The postToolUse question
|
|
11
|
+
// hook is DORMANT until Cursor fixes their AskQuestion tool-hook-skip bug
|
|
12
|
+
// (forum 152230/161836) — installing it now means it auto-activates with no
|
|
13
|
+
// new install once upstream lands. See CURSOR_ASK_QUESTION_COMMAND.
|
|
10
14
|
// 2. MERGE an `mcpServers` entry into `~/.cursor/mcp.json` pointing at
|
|
11
15
|
// `npx -y @trygocode/notify mcp`.
|
|
12
16
|
// 3. WRITE the on-demand rule to `~/.cursor/rules/gocode-notify.md` (the
|
|
@@ -57,6 +61,48 @@ export const MCP_SERVER_ENTRY = {
|
|
|
57
61
|
// version on every turn, so users self-update seamlessly. (Matchers below key on
|
|
58
62
|
// the `@trygocode/notify` substring, which `@latest` preserves.)
|
|
59
63
|
export const CURSOR_STOP_COMMAND = "npx -y @trygocode/notify@latest on-stop --source cursor --dedupe-key cursor-stop || true";
|
|
64
|
+
/**
|
|
65
|
+
* The Cursor `postToolUse` question hook (T-CUR3) — the ONLY available signal for
|
|
66
|
+
* "the Cursor agent asked the user a question and is now waiting." It fires a
|
|
67
|
+
* plain `awaiting_input` ping ("Agent needs you"), the SAME kind Claude Code's
|
|
68
|
+
* dedicated `Notification` hook fires, so a real question surfaces as a QUESTION
|
|
69
|
+
* notification on the phone/desktop instead of a misleading `finished`.
|
|
70
|
+
*
|
|
71
|
+
* ⚠️ DORMANT-BY-NECESSITY (2026-06-18): Cursor has a CONFIRMED-OPEN upstream bug
|
|
72
|
+
* where the `AskQuestion` / `AskUserQuestion` tool fires ZERO hooks — `preToolUse`
|
|
73
|
+
* and `postToolUse` are documented to fire "for all tools" but are skipped for
|
|
74
|
+
* this one tool specifically (Cursor staff confirmed, no workaround; forum
|
|
75
|
+
* threads 152230 + 161836). So today this hook will simply never fire, and the
|
|
76
|
+
* `stop`-hook `finished` fallback remains the user's notification. The moment
|
|
77
|
+
* Cursor ships their fix this hook AUTO-ACTIVATES with NO new install — every
|
|
78
|
+
* user already running `@trygocode/notify@latest` gets correct question
|
|
79
|
+
* detection for free. We install it now so we're ahead of the fix.
|
|
80
|
+
*
|
|
81
|
+
* Cursor question detection has no other path: Cursor exposes NO `Notification`
|
|
82
|
+
* event (docs: `Notification | - | No`), and the `stop` hook's
|
|
83
|
+
* `status: completed|aborted|error` cannot distinguish "asked a question" from
|
|
84
|
+
* "finished the work." This dormant tool hook is the best available approximation
|
|
85
|
+
* and becomes a real signal once upstream lands.
|
|
86
|
+
*
|
|
87
|
+
* Ends in `|| true` so a failed push NEVER blocks the turn; carries a distinct
|
|
88
|
+
* `--dedupe-key` so it coalesces with itself but not with the `stop` ping.
|
|
89
|
+
*/
|
|
90
|
+
export const CURSOR_ASK_QUESTION_COMMAND = 'npx -y @trygocode/notify@latest send --kind awaiting_input --source cursor --title "Agent needs you" --dedupe-key cursor-ask-question || true';
|
|
91
|
+
/**
|
|
92
|
+
* Tool names that mean "the agent is asking the user a question." Used as the
|
|
93
|
+
* `postToolUse` matcher (a regex alternation). Cursor maps Claude Code tool
|
|
94
|
+
* names to Cursor names, and the question tool has shipped under several names
|
|
95
|
+
* across versions/runtimes (`AskQuestion` in the IDE, `AskUserQuestion` in some
|
|
96
|
+
* builds, `ask_question` in the CLI). We match ALL of them so the hook works the
|
|
97
|
+
* instant Cursor wires any of them to `postToolUse`.
|
|
98
|
+
*/
|
|
99
|
+
export const CURSOR_ASK_QUESTION_TOOLS = [
|
|
100
|
+
"AskQuestion",
|
|
101
|
+
"AskUserQuestion",
|
|
102
|
+
"ask_question",
|
|
103
|
+
];
|
|
104
|
+
/** The `matcher` regex string written into the `postToolUse` entry. */
|
|
105
|
+
export const CURSOR_ASK_QUESTION_MATCHER = `^(${CURSOR_ASK_QUESTION_TOOLS.join("|")})$`;
|
|
60
106
|
/**
|
|
61
107
|
* Substrings that together identify a `stop` hook entry as OURS. Used for
|
|
62
108
|
* idempotent merge (replace, don't duplicate) and for surgical uninstall (remove
|
|
@@ -143,12 +189,34 @@ async function writeJsonFile(file, value) {
|
|
|
143
189
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
144
190
|
await fs.writeFile(file, JSON.stringify(value, null, 2) + "\n");
|
|
145
191
|
}
|
|
192
|
+
/** True when a command string is one of ours (BOTH a name token AND the source). */
|
|
193
|
+
function isOurCommand(cmd) {
|
|
194
|
+
return cmd.includes(HOOK_SOURCE_TOKEN) && HOOK_NAME_TOKENS.some((t) => cmd.includes(t));
|
|
195
|
+
}
|
|
146
196
|
/** True when a single `stop` hook entry (`{ command }`) is one we wrote. */
|
|
147
197
|
function isOurStopHook(h) {
|
|
148
198
|
if (!isRecord(h) || typeof h.command !== "string")
|
|
149
199
|
return false;
|
|
150
|
-
|
|
151
|
-
|
|
200
|
+
return isOurCommand(h.command);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* True when a `postToolUse` group (`{ matcher?, hooks: [{ command }] }`) is one
|
|
204
|
+
* we wrote — its `hooks` array contains a command carrying OUR markers. Cursor's
|
|
205
|
+
* tool-hook shape nests commands one level deeper than the flat `stop` array, so
|
|
206
|
+
* we look inside `hooks[].command`.
|
|
207
|
+
*/
|
|
208
|
+
function isOurToolHookGroup(g) {
|
|
209
|
+
if (!isRecord(g) || !Array.isArray(g.hooks))
|
|
210
|
+
return false;
|
|
211
|
+
return g.hooks.some((h) => isRecord(h) && typeof h.command === "string" && isOurCommand(h.command));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Strip OUR groups out of a `postToolUse` array. Returns the cleaned array plus
|
|
215
|
+
* whether anything of ours was removed. Never mutates the input.
|
|
216
|
+
*/
|
|
217
|
+
function stripOurToolHooks(entries) {
|
|
218
|
+
const kept = entries.filter((g) => !isOurToolHookGroup(g));
|
|
219
|
+
return { entries: kept, removed: kept.length !== entries.length };
|
|
152
220
|
}
|
|
153
221
|
/**
|
|
154
222
|
* Strip OUR entries out of the `stop` array. Returns the cleaned array plus
|
|
@@ -175,6 +243,30 @@ function mergeStopHook(config) {
|
|
|
175
243
|
hooks.stop = preserved;
|
|
176
244
|
config.hooks = hooks;
|
|
177
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Merge our `postToolUse` AskQuestion hook into the hooks config (T-CUR3),
|
|
248
|
+
* preserving the user's own `postToolUse` groups. Strips any prior copy of OUR
|
|
249
|
+
* group (idempotent / version-safe) then appends a single fresh group scoped to
|
|
250
|
+
* the question tools via {@link CURSOR_ASK_QUESTION_MATCHER}. Mutates `config` in
|
|
251
|
+
* place. Cursor's tool-hook shape is `{ matcher, hooks: [{ type, command }] }`.
|
|
252
|
+
*
|
|
253
|
+
* This hook is dormant until Cursor fixes the AskQuestion tool-hook-skip bug (see
|
|
254
|
+
* {@link CURSOR_ASK_QUESTION_COMMAND}) — installing it now means it auto-activates
|
|
255
|
+
* with no new install once upstream lands.
|
|
256
|
+
*/
|
|
257
|
+
function mergeAskQuestionHook(config) {
|
|
258
|
+
if (typeof config.version !== "number")
|
|
259
|
+
config.version = 1;
|
|
260
|
+
const hooks = isRecord(config.hooks) ? config.hooks : {};
|
|
261
|
+
const existing = Array.isArray(hooks.postToolUse) ? hooks.postToolUse : [];
|
|
262
|
+
const preserved = stripOurToolHooks(existing).entries;
|
|
263
|
+
preserved.push({
|
|
264
|
+
matcher: CURSOR_ASK_QUESTION_MATCHER,
|
|
265
|
+
hooks: [{ type: "command", command: CURSOR_ASK_QUESTION_COMMAND }],
|
|
266
|
+
});
|
|
267
|
+
hooks.postToolUse = preserved;
|
|
268
|
+
config.hooks = hooks;
|
|
269
|
+
}
|
|
178
270
|
/** Merge our MCP server entry into `mcp.mcpServers`. Mutates in place. */
|
|
179
271
|
function mergeMcp(mcp) {
|
|
180
272
|
const servers = isRecord(mcp.mcpServers) ? mcp.mcpServers : {};
|
|
@@ -197,6 +289,7 @@ export async function writeCursorConfig(runtime, opts) {
|
|
|
197
289
|
const hooksPath = cursorHooksPath(opts);
|
|
198
290
|
const hooksConfig = (await readJsonObject(hooksPath)) ?? {};
|
|
199
291
|
mergeStopHook(hooksConfig);
|
|
292
|
+
mergeAskQuestionHook(hooksConfig);
|
|
200
293
|
await writeJsonFile(hooksPath, hooksConfig);
|
|
201
294
|
written.push(hooksPath);
|
|
202
295
|
const mcpPath = cursorMcpPath(opts);
|
|
@@ -212,7 +305,10 @@ export async function writeCursorConfig(runtime, opts) {
|
|
|
212
305
|
runtime: name,
|
|
213
306
|
written,
|
|
214
307
|
skipped: false,
|
|
215
|
-
detail: "merged stop
|
|
308
|
+
detail: "merged stop + postToolUse(AskQuestion) hooks + MCP entry; wrote rule. " +
|
|
309
|
+
"NOTE: the AskQuestion question hook is DORMANT — Cursor currently fires no " +
|
|
310
|
+
"hooks for the AskQuestion tool (upstream bug), so question notifications " +
|
|
311
|
+
"fall back to 'finished' until Cursor ships their fix (auto-activates then).",
|
|
216
312
|
};
|
|
217
313
|
}
|
|
218
314
|
catch (err) {
|
|
@@ -250,6 +346,16 @@ export async function uninstallCursorConfig(opts) {
|
|
|
250
346
|
delete hooks.stop;
|
|
251
347
|
}
|
|
252
348
|
}
|
|
349
|
+
if (Array.isArray(hooks.postToolUse)) {
|
|
350
|
+
const { entries: kept, removed: r } = stripOurToolHooks(hooks.postToolUse);
|
|
351
|
+
if (r) {
|
|
352
|
+
changed = true;
|
|
353
|
+
if (kept.length > 0)
|
|
354
|
+
hooks.postToolUse = kept;
|
|
355
|
+
else
|
|
356
|
+
delete hooks.postToolUse;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
253
359
|
if (Object.keys(hooks).length === 0)
|
|
254
360
|
delete hooksConfig.hooks;
|
|
255
361
|
if (changed) {
|
package/dist/src/opencode.js
CHANGED
|
@@ -8,14 +8,20 @@
|
|
|
8
8
|
// "command": ["npx","-y","@trygocode/notify","mcp"],
|
|
9
9
|
// "enabled": true }
|
|
10
10
|
// `command` is a single ARRAY (binary + args), plus `type` + `enabled`.
|
|
11
|
-
// 2. WRITE
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
11
|
+
// 2. WRITE an event plugin to `<config-dir>/plugin/gocode-notify.js` that
|
|
12
|
+
// subscribes to TWO event classes:
|
|
13
|
+
// a) END-OF-TURN — `session.idle` (legacy) + `session.status` (modern),
|
|
14
|
+
// the OpenCode equivalent of Cursor `stop` / Claude `Stop`. Fires the
|
|
15
|
+
// SHARED `on-stop` dispatcher → a `finished` ping.
|
|
16
|
+
// b) QUESTION — `permission.asked` (the "asking now" edge), the OpenCode
|
|
17
|
+
// equivalent of Claude's `Notification` hook. Fires a plain
|
|
18
|
+
// `awaiting_input` ping ("Agent needs you") so a real question shows as
|
|
19
|
+
// a QUESTION, not a misleading `finished`. (Cursor has NO such event;
|
|
20
|
+
// OpenCode genuinely exposes it via permission events. We do NOT listen
|
|
21
|
+
// to permission.updated/replied — those also fire after the user
|
|
22
|
+
// answers and would re-ping a resolved question.)
|
|
23
|
+
// Each child is detached + unref'd and errors are swallowed (`|| true`), so a
|
|
24
|
+
// notification/push failure can never block the session.
|
|
19
25
|
//
|
|
20
26
|
// The on-demand rule/skill (Claude SKILL.md / Cursor rule) is SKIPPED for
|
|
21
27
|
// OpenCode: OpenCode has no auto-loaded standalone per-file rule mechanism (the
|
|
@@ -72,6 +78,21 @@ export const OPENCODE_MCP_ENTRY = {
|
|
|
72
78
|
// resolved version forever and never auto-updates (npm/cli#6664). Pinning
|
|
73
79
|
// `@latest` makes the hook always fetch the newest publish so users self-update.
|
|
74
80
|
export const OPENCODE_STOP_COMMAND = "npx -y @trygocode/notify@latest on-stop --source opencode --dedupe-key opencode-idle || true";
|
|
81
|
+
/**
|
|
82
|
+
* The shell command the plugin fires when OpenCode raises a PERMISSION request —
|
|
83
|
+
* the OpenCode equivalent of "the agent needs the user" (Claude's `Notification`
|
|
84
|
+
* hook / the Cursor AskQuestion hook). Unlike Cursor, OpenCode genuinely exposes
|
|
85
|
+
* this signal: the `permission.asked` event fires the instant the agent pauses to
|
|
86
|
+
* ask the user to approve/answer something. We map it to a plain `awaiting_input`
|
|
87
|
+
* ping ("Agent needs you") — the SAME kind Claude Code fires — so a real question
|
|
88
|
+
* surfaces as a QUESTION notification, not a misleading `finished`. (We listen to
|
|
89
|
+
* `permission.asked` ONLY, never `permission.updated`/`permission.replied`, which
|
|
90
|
+
* also fire AFTER the user answers and would re-ping a resolved question.)
|
|
91
|
+
*
|
|
92
|
+
* Ends in `|| true` so a failed push can never block the session; carries a
|
|
93
|
+
* distinct `--dedupe-key` so it coalesces with itself but not with the idle ping.
|
|
94
|
+
*/
|
|
95
|
+
export const OPENCODE_ASK_COMMAND = 'npx -y @trygocode/notify@latest send --kind awaiting_input --source opencode --title "Agent needs you" --dedupe-key opencode-permission || true';
|
|
75
96
|
/**
|
|
76
97
|
* Substrings that together identify our plugin file as OURS. Used to keep
|
|
77
98
|
* uninstall surgical: we only delete the plugin file when BOTH markers are
|
|
@@ -157,13 +178,15 @@ import { spawn } from "node:child_process";
|
|
|
157
178
|
// Per-session last-fire timestamps so idle+status for the SAME turn coalesce.
|
|
158
179
|
const lastFiredAt = new Map();
|
|
159
180
|
const DEDUPE_MS = 4000;
|
|
181
|
+
// Separate per-session debounce for permission/question events so a rapid burst
|
|
182
|
+
// of permission.asked events for one prompt coalesces into a single ping, while
|
|
183
|
+
// staying INDEPENDENT of the end-of-turn debounce (a question must not be
|
|
184
|
+
// suppressed by a recent finished ping — different event class).
|
|
185
|
+
const lastAskedAt = new Map();
|
|
160
186
|
|
|
161
|
-
function fire() {
|
|
187
|
+
function fire(command) {
|
|
162
188
|
try {
|
|
163
|
-
const child = spawn(
|
|
164
|
-
${JSON.stringify(OPENCODE_STOP_COMMAND)},
|
|
165
|
-
{ shell: true, detached: true, stdio: "ignore" },
|
|
166
|
-
);
|
|
189
|
+
const child = spawn(command, { shell: true, detached: true, stdio: "ignore" });
|
|
167
190
|
child.unref();
|
|
168
191
|
} catch {
|
|
169
192
|
// never block the session on a notification failure
|
|
@@ -195,13 +218,39 @@ function maybeFire(sessionID) {
|
|
|
195
218
|
const prev = lastFiredAt.get(key) ?? 0;
|
|
196
219
|
if (now - prev < DEDUPE_MS) return; // coalesce idle+status for one turn
|
|
197
220
|
lastFiredAt.set(key, now);
|
|
198
|
-
fire();
|
|
221
|
+
fire(${JSON.stringify(OPENCODE_STOP_COMMAND)});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// The agent paused to ask the user (permission.asked). Fire an "Agent needs
|
|
225
|
+
// you" question ping. Debounced separately from the end-of-turn ping so a burst
|
|
226
|
+
// of asks for one prompt coalesces into one.
|
|
227
|
+
function maybeAsk(sessionID) {
|
|
228
|
+
const key = sessionID || "_";
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const prev = lastAskedAt.get(key) ?? 0;
|
|
231
|
+
if (now - prev < DEDUPE_MS) return;
|
|
232
|
+
lastAskedAt.set(key, now);
|
|
233
|
+
fire(${JSON.stringify(OPENCODE_ASK_COMMAND)});
|
|
199
234
|
}
|
|
200
235
|
|
|
201
236
|
export const GocodeNotify = async () => ({
|
|
202
237
|
event: async ({ event }) => {
|
|
203
238
|
if (!event) return;
|
|
204
239
|
const props = event.properties ?? {};
|
|
240
|
+
// QUESTION signal — the agent is waiting on the user (OpenCode's real
|
|
241
|
+
// "needs you" event; Cursor has no equivalent). Fired BEFORE the idle/status
|
|
242
|
+
// checks so a permission request is never misread as a plain end-of-turn.
|
|
243
|
+
//
|
|
244
|
+
// We listen ONLY to \`permission.asked\` — the moment the agent RAISES a
|
|
245
|
+
// request. We deliberately do NOT listen to \`permission.updated\` /
|
|
246
|
+
// \`permission.replied\`: those are state transitions that ALSO fire AFTER the
|
|
247
|
+
// user answers, which would re-emit "Agent needs you" once the question is
|
|
248
|
+
// already resolved — exactly the false-notification class this fix exists to
|
|
249
|
+
// avoid. \`permission.asked\` is the unambiguous "asking now" edge.
|
|
250
|
+
if (event.type === "permission.asked") {
|
|
251
|
+
maybeAsk(props.sessionID);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
205
254
|
if (event.type === "session.idle") {
|
|
206
255
|
maybeFire(props.sessionID);
|
|
207
256
|
return;
|
|
@@ -332,7 +381,7 @@ export async function writeOpenCodeConfig(runtime, opts) {
|
|
|
332
381
|
runtime: name,
|
|
333
382
|
written,
|
|
334
383
|
skipped: false,
|
|
335
|
-
detail: "merged mcp.gocode-notify entry; wrote
|
|
384
|
+
detail: "merged mcp.gocode-notify entry; wrote end-of-turn + permission(question) plugin (rule skipped — OpenCode has no standalone rule file)",
|
|
336
385
|
};
|
|
337
386
|
}
|
|
338
387
|
catch (err) {
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Single source of truth for the CLI version. Keep in sync with package.json.
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.5.0";
|
package/package.json
CHANGED