@raidiant/notifai 0.1.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.
Files changed (54) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +76 -0
  3. package/dist/client.d.ts +40 -0
  4. package/dist/client.js +118 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/commands.d.ts +225 -0
  7. package/dist/commands.js +1855 -0
  8. package/dist/commands.js.map +1 -0
  9. package/dist/config.d.ts +138 -0
  10. package/dist/config.js +224 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/credentials.d.ts +31 -0
  13. package/dist/credentials.js +94 -0
  14. package/dist/credentials.js.map +1 -0
  15. package/dist/hooks.d.ts +250 -0
  16. package/dist/hooks.js +792 -0
  17. package/dist/hooks.js.map +1 -0
  18. package/dist/idle.d.ts +16 -0
  19. package/dist/idle.js +44 -0
  20. package/dist/idle.js.map +1 -0
  21. package/dist/install-hooks.d.ts +194 -0
  22. package/dist/install-hooks.js +579 -0
  23. package/dist/install-hooks.js.map +1 -0
  24. package/dist/main.d.ts +2 -0
  25. package/dist/main.js +259 -0
  26. package/dist/main.js.map +1 -0
  27. package/dist/opencode-plugin.d.ts +65 -0
  28. package/dist/opencode-plugin.js +181 -0
  29. package/dist/opencode-plugin.js.map +1 -0
  30. package/dist/readiness.d.ts +87 -0
  31. package/dist/readiness.js +40 -0
  32. package/dist/readiness.js.map +1 -0
  33. package/dist/send.d.ts +86 -0
  34. package/dist/send.js +202 -0
  35. package/dist/send.js.map +1 -0
  36. package/package.json +56 -0
  37. package/src/client.test.ts +83 -0
  38. package/src/client.ts +183 -0
  39. package/src/commands.test.ts +1199 -0
  40. package/src/commands.ts +2201 -0
  41. package/src/config.test.ts +243 -0
  42. package/src/config.ts +347 -0
  43. package/src/credentials.ts +119 -0
  44. package/src/hooks.test.ts +1030 -0
  45. package/src/hooks.ts +1099 -0
  46. package/src/idle.test.ts +32 -0
  47. package/src/idle.ts +41 -0
  48. package/src/install-hooks.test.ts +500 -0
  49. package/src/install-hooks.ts +708 -0
  50. package/src/main.ts +315 -0
  51. package/src/opencode-plugin.ts +203 -0
  52. package/src/readiness.ts +88 -0
  53. package/src/send.ts +272 -0
  54. package/tsconfig.json +10 -0
package/dist/main.js ADDED
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { Command } from 'commander';
4
+ import { askCommand, authStatusCommand, capabilitiesCommand, closeCommand, configSetCommand, configShowCommand, devicesCommand, doctorCommand, hookRunCommand, hooksInstallCommand, hooksUninstallCommand, initCommand, loginCommand, logoutCommand, realIo, repliesCommand, sendCommand, statusCommand, } from './commands.js';
5
+ import { defaultCredentialStore } from './credentials.js';
6
+ const deps = {
7
+ io: realIo(),
8
+ store: defaultCredentialStore(),
9
+ env: process.env,
10
+ cwd: process.cwd(),
11
+ };
12
+ /**
13
+ * Harness hooks deliver their event payload on stdin. Bounded on both time and
14
+ * size: a wrapper that opens the pipe without writing would otherwise hold the
15
+ * read until the harness killed the hook, delaying the user's own prompt.
16
+ */
17
+ async function readStdin(timeoutMs = 2000, maxBytes = 1_000_000) {
18
+ if (process.stdin.isTTY)
19
+ return '';
20
+ return new Promise((resolve) => {
21
+ const chunks = [];
22
+ let total = 0;
23
+ let settled = false;
24
+ const finish = () => {
25
+ if (settled)
26
+ return;
27
+ settled = true;
28
+ clearTimeout(timer);
29
+ process.stdin.destroy();
30
+ resolve(Buffer.concat(chunks).toString('utf8'));
31
+ };
32
+ const timer = setTimeout(finish, timeoutMs);
33
+ timer.unref?.();
34
+ process.stdin.on('data', (chunk) => {
35
+ total += chunk.length;
36
+ if (total > maxBytes)
37
+ return finish();
38
+ chunks.push(chunk);
39
+ });
40
+ process.stdin.on('end', finish);
41
+ process.stdin.on('error', finish);
42
+ });
43
+ }
44
+ const program = new Command('notifai')
45
+ .description('Send native device notifications from agents and local programs')
46
+ .version('0.1.0');
47
+ program
48
+ .command('login')
49
+ .description('Pair this machine with your NotifAI account via browser approval')
50
+ .option('--name <name>', 'machine name shown in the dashboard (default: hostname)')
51
+ .option('--base-url <url>', 'NotifAI server URL')
52
+ .option('--no-open', 'do not open the approval page in a browser')
53
+ .action(async (opts) => {
54
+ process.exit(await loginCommand(deps, opts));
55
+ });
56
+ program
57
+ .command('logout')
58
+ .description('Remove the stored machine credential')
59
+ .action(() => {
60
+ process.exit(logoutCommand(deps));
61
+ });
62
+ const auth = program.command('auth').description('Authentication helpers');
63
+ auth
64
+ .command('status')
65
+ .description('Show the stored machine identity')
66
+ .option('--json', 'machine-readable output')
67
+ .action((opts) => {
68
+ process.exit(authStatusCommand(deps, opts));
69
+ });
70
+ program
71
+ .command('devices')
72
+ .description('List registered devices and their delivery readiness')
73
+ .option('--json', 'machine-readable output')
74
+ .action(async (opts) => {
75
+ process.exit(await devicesCommand(deps, opts));
76
+ });
77
+ program
78
+ .command('capabilities')
79
+ .description('Show a platform capability contract')
80
+ .option('--platform <platform>', 'platform to describe (default: ios)')
81
+ .option('--json', 'machine-readable output')
82
+ .action(async (opts) => {
83
+ process.exit(await capabilitiesCommand(deps, opts));
84
+ });
85
+ program
86
+ .command('send')
87
+ .description('Send a notification')
88
+ .requiredOption('--title <title>', 'notification title')
89
+ .requiredOption('--body <body>', 'notification body')
90
+ .option('--subtitle <subtitle>')
91
+ .option('--detail <markdown>', 'long-form markdown shown only in the app, never on the banner')
92
+ .option('--detail-file <path>', 'read --detail from a file (use - for stdin)')
93
+ .option('--event <event>', 'agent event name, e.g. tests_passed')
94
+ .option('--kind <kind>', 'what this is: update (default) | done | question (requires --reply)')
95
+ .option('--project <id>', 'project identifier, e.g. my-app (lazily registered)')
96
+ .option('--session <id>', 'session identity (env: NOTIFAI_SESSION); presentation varies by surface')
97
+ .option('--device <id>', 'target a device id (repeatable)', (v, all) => [...all, v], [])
98
+ .option('--all', 'target all routable devices (overrides configured devices)')
99
+ .option('--ttl <seconds>', 'delivery window in seconds', (v) => Number(v))
100
+ .option('--collapse-key <key>', 'replace earlier notifications with the same key')
101
+ .option('--platform <platform>', 'platform whose optional fields to include (default: ios)')
102
+ .option('--sound <sound>', 'default | done | attention | alert | none')
103
+ .option('--badge <n>', 'app badge count', (v) => Number(v))
104
+ .option('--thread-id <id>', 'group related notifications')
105
+ .option('--level <level>', 'interruption level: passive | active | time_sensitive')
106
+ .option('--relevance <score>', '0..1 relevance score', (v) => Number(v))
107
+ .option('--target-content-id <id>')
108
+ .option('--data <key=value>', 'custom data (repeatable)', (v, all) => [...all, v], [])
109
+ .option('--image <path|url|media_id>', 'upload or attach an image')
110
+ .option('--reply', 'enable the inline reply action and block for the answer')
111
+ .option('--reply-timeout <seconds>', 'how long to wait for a reply (default: 900)', (v) => Number(v))
112
+ .option('--reply-window <seconds>', 'how long the server accepts a reply (default: 86400)', (v) => Number(v))
113
+ .option('--reply-choice <label>', 'with --reply, ask a closed question (2-6 answers); one value splits on commas, or repeat the flag', (v, all) => [...all, v], [])
114
+ // Kept registered, and always a usage error with --reply, so the caller gets
115
+ // a message pointing at `ask` instead of "unknown option".
116
+ .option('--no-block', 'rejected with --reply; use `notifai ask` to ask and end the turn')
117
+ .option('--wait <seconds>', 'how long to wait for provider outcomes', (v) => Number(v))
118
+ .option('--no-wait', 'return immediately after acceptance')
119
+ .option('--idempotency-key <key>', 'safe-retry key (default: random)')
120
+ .option('--base-url <url>', 'NotifAI server URL')
121
+ .option('--json', 'print the full submission receipt as JSON')
122
+ .action(async (opts) => {
123
+ // commander maps --no-wait onto the same "wait" flag; disentangle.
124
+ const noWait = opts['wait'] === false;
125
+ const wait = typeof opts['wait'] === 'number' ? opts['wait'] : undefined;
126
+ const noBlock = opts['block'] === false;
127
+ const sendOpts = { ...opts };
128
+ // Same empty-collector normalisation as `ask`.
129
+ if (Array.isArray(sendOpts['replyChoice']) && sendOpts['replyChoice'].length === 0) {
130
+ delete sendOpts['replyChoice'];
131
+ }
132
+ delete sendOpts['block'];
133
+ // Long-form detail is usually a build log or a diff summary, which nobody
134
+ // wants to shell-escape onto a command line.
135
+ const detailFile = sendOpts['detailFile'];
136
+ delete sendOpts['detailFile'];
137
+ if (typeof detailFile === 'string') {
138
+ if (sendOpts['detail'] !== undefined) {
139
+ deps.io.err('Pass either --detail or --detail-file, not both.');
140
+ process.exit(2);
141
+ }
142
+ try {
143
+ sendOpts['detail'] = readFileSync(detailFile === '-' ? 0 : detailFile, 'utf8');
144
+ }
145
+ catch (err) {
146
+ deps.io.err(`Could not read ${detailFile}: ${String(err)}`);
147
+ process.exit(2);
148
+ }
149
+ }
150
+ const flags = { ...sendOpts, noWait, noBlock };
151
+ if (wait === undefined)
152
+ delete flags.wait;
153
+ else
154
+ flags.wait = wait;
155
+ process.exit(await sendCommand(deps, flags));
156
+ });
157
+ program
158
+ .command('replies <request_id>')
159
+ .description('Retrieve replies for a notification request')
160
+ .option('--wait <seconds>', 'how long to wait for a reply', (v) => Number(v))
161
+ .option('--after <seq>', 'return replies after this sequence number', (v) => Number(v))
162
+ .option('--json', 'machine-readable output')
163
+ .action(async (requestId, opts) => {
164
+ process.exit(await repliesCommand(deps, requestId, opts));
165
+ });
166
+ program
167
+ .command('status <request_id>')
168
+ .description('Show the evidence trail for a notification request')
169
+ .option('--json', 'machine-readable output')
170
+ .action(async (requestId, opts) => {
171
+ process.exit(await statusCommand(deps, requestId, opts));
172
+ });
173
+ program
174
+ .command('ask <question>')
175
+ .description('Register a question for the turn-end hook to route under your presence settings')
176
+ .option('--choice <label>', 'answers to offer instead of free text (2-6); one value splits on commas, or repeat the flag', (v, all) => [...all, v], [])
177
+ .option('--session <id>', 'session id (default: the session working in this directory, else $NOTIFAI_SESSION)')
178
+ .action((question, opts) => {
179
+ // commander's collector defaults to []; an empty list means "not asked".
180
+ const flags = {
181
+ ...(opts.session !== undefined ? { session: opts.session } : {}),
182
+ ...(opts.choice?.length ? { choice: opts.choice } : {}),
183
+ };
184
+ process.exit(askCommand(deps, question, flags));
185
+ });
186
+ program
187
+ .command('close <request_id>')
188
+ .description('Retire a question so late answers are rejected rather than lost')
189
+ .action(async (requestId) => {
190
+ process.exit(await closeCommand(deps, requestId));
191
+ });
192
+ program
193
+ .command('hook <event>')
194
+ .description('Internal: run a harness hook (reads hook JSON on stdin)')
195
+ // Inert, and the point of it is that it is inert: the installed command line
196
+ // carries a marker that says "NotifAI wrote this" independently of which
197
+ // checkout wrote it.
198
+ .option('--owner <name>', 'internal ownership marker')
199
+ .option('--harness <name>', 'internal harness output adapter')
200
+ .action(async (event, opts) => {
201
+ process.exit(await hookRunCommand(deps, event, readStdin, opts.harness === 'cursor' ? 'cursor' : undefined));
202
+ });
203
+ const hooks = program.command('hooks').description('Install harness hooks for registered-question routing');
204
+ hooks
205
+ .command('install')
206
+ .description('Wire this harness to route registered questions to your devices')
207
+ .option('--harness <name>', 'claude-code | codex | cursor | opencode (default: detected; OpenCode cannot resume an idle turn from an answer)')
208
+ .option('--global', 'install for every project instead of just this one')
209
+ .action((opts) => {
210
+ process.exit(hooksInstallCommand(deps, opts));
211
+ });
212
+ hooks
213
+ .command('uninstall')
214
+ .description('Remove the hooks this CLI installed')
215
+ .option('--harness <name>', 'claude-code | codex | cursor | opencode (default: detected)')
216
+ .option('--global', 'remove the machine-wide install')
217
+ .action((opts) => {
218
+ process.exit(hooksUninstallCommand(deps, opts));
219
+ });
220
+ const config = program.command('config').description('Show or change configuration');
221
+ config
222
+ .command('show')
223
+ .description('Show the resolved configuration')
224
+ .option('--explain', 'show where each value comes from')
225
+ .option('--json', 'machine-readable output')
226
+ .action((opts) => {
227
+ process.exit(configShowCommand(deps, opts));
228
+ });
229
+ config
230
+ .command('set <key> <value>')
231
+ .description('Set a configuration value (choose a layer interactively; unattended defaults machine-global and requires --yes)')
232
+ .option('--project', 'write to the shared .notifai/config.toml instead')
233
+ .option('--local', 'write to personal .notifai/config.local.toml (keep it gitignored)')
234
+ .option('--session <id>', 'apply only to one session')
235
+ .option('--yes', 'skip the confirmation gate')
236
+ .action(async (key, value, opts) => {
237
+ process.exit(await configSetCommand(deps, key, value, opts));
238
+ });
239
+ program
240
+ .command('init')
241
+ .description('Set up NotifAI here, idempotently: sign-in, project id, hooks, device readiness. ' +
242
+ 'Interactive at a human terminal; never prompts otherwise (agents: pass flags)')
243
+ .option('--project-id <id>', 'project identifier slug (default: derived from the directory name)')
244
+ .option('--skills', 'reserved: this build has no published agent-skill source and exits with an error')
245
+ .option('--no-skills', 'suppress the optional agent-skill status line')
246
+ .option('--hooks', 'install harness hooks for registered-question routing')
247
+ .option('--no-hooks', 'skip the hooks without being asked')
248
+ .action(async (opts) => {
249
+ process.exit(await initCommand(deps, opts));
250
+ });
251
+ program
252
+ .command('doctor')
253
+ .description('Audit config, credential, server, contract, device, and hook readiness (no live send)')
254
+ .option('--json', 'machine-readable output')
255
+ .action(async (opts) => {
256
+ process.exit(await doctorCommand(deps, opts));
257
+ });
258
+ await program.parseAsync(process.argv);
259
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACrB,WAAW,EACX,YAAY,EACZ,aAAa,EACb,MAAM,EACN,cAAc,EACd,WAAW,EACX,aAAa,GAEd,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAGzD,MAAM,IAAI,GAAgB;IACxB,EAAE,EAAE,MAAM,EAAE;IACZ,KAAK,EAAE,sBAAsB,EAAE;IAC/B,GAAG,EAAE,OAAO,CAAC,GAAG;IAChB,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;CACnB,CAAA;AAED;;;;GAIG;AACH,KAAK,UAAU,SAAS,CAAC,SAAS,GAAG,IAAI,EAAE,QAAQ,GAAG,SAAS;IAC7D,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK;QAAE,OAAO,EAAE,CAAA;IAClC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,IAAI,KAAK,GAAG,CAAC,CAAA;QACb,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,MAAM,MAAM,GAAG,GAAS,EAAE;YACxB,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;YACvB,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;QACjD,CAAC,CAAA;QACD,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;QAC3C,KAAK,CAAC,KAAK,EAAE,EAAE,CAAA;QACf,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACzC,KAAK,IAAI,KAAK,CAAC,MAAM,CAAA;YACrB,IAAI,KAAK,GAAG,QAAQ;gBAAE,OAAO,MAAM,EAAE,CAAA;YACrC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACpB,CAAC,CAAC,CAAA;QACF,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QAC/B,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IACnC,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,SAAS,CAAC;KACnC,WAAW,CAAC,iEAAiE,CAAC;KAC9E,OAAO,CAAC,OAAO,CAAC,CAAA;AAEnB,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,kEAAkE,CAAC;KAC/E,MAAM,CAAC,eAAe,EAAE,yDAAyD,CAAC;KAClF,MAAM,CAAC,kBAAkB,EAAE,oBAAoB,CAAC;KAChD,MAAM,CAAC,WAAW,EAAE,4CAA4C,CAAC;KACjE,MAAM,CAAC,KAAK,EAAE,IAAyD,EAAE,EAAE;IAC1E,OAAO,CAAC,IAAI,CAAC,MAAM,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AAC9C,CAAC,CAAC,CAAA;AAEJ,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,sCAAsC,CAAC;KACnD,MAAM,CAAC,GAAG,EAAE;IACX,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;AACnC,CAAC,CAAC,CAAA;AAEJ,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,wBAAwB,CAAC,CAAA;AAC1E,IAAI;KACD,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,kCAAkC,CAAC;KAC/C,MAAM,CAAC,QAAQ,EAAE,yBAAyB,CAAC;KAC3C,MAAM,CAAC,CAAC,IAAwB,EAAE,EAAE;IACnC,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AAC7C,CAAC,CAAC,CAAA;AAEJ,OAAO;KACJ,OAAO,CAAC,SAAS,CAAC;KAClB,WAAW,CAAC,sDAAsD,CAAC;KACnE,MAAM,CAAC,QAAQ,EAAE,yBAAyB,CAAC;KAC3C,MAAM,CAAC,KAAK,EAAE,IAAwB,EAAE,EAAE;IACzC,OAAO,CAAC,IAAI,CAAC,MAAM,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AAChD,CAAC,CAAC,CAAA;AAEJ,OAAO;KACJ,OAAO,CAAC,cAAc,CAAC;KACvB,WAAW,CAAC,qCAAqC,CAAC;KAClD,MAAM,CAAC,uBAAuB,EAAE,qCAAqC,CAAC;KACtE,MAAM,CAAC,QAAQ,EAAE,yBAAyB,CAAC;KAC3C,MAAM,CAAC,KAAK,EAAE,IAA6C,EAAE,EAAE;IAC9D,OAAO,CAAC,IAAI,CAAC,MAAM,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AACrD,CAAC,CAAC,CAAA;AAEJ,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,qBAAqB,CAAC;KAClC,cAAc,CAAC,iBAAiB,EAAE,oBAAoB,CAAC;KACvD,cAAc,CAAC,eAAe,EAAE,mBAAmB,CAAC;KACpD,MAAM,CAAC,uBAAuB,CAAC;KAC/B,MAAM,CAAC,qBAAqB,EAAE,+DAA+D,CAAC;KAC9F,MAAM,CAAC,sBAAsB,EAAE,6CAA6C,CAAC;KAC7E,MAAM,CAAC,iBAAiB,EAAE,qCAAqC,CAAC;KAChE,MAAM,CAAC,eAAe,EAAE,qEAAqE,CAAC;KAC9F,MAAM,CAAC,gBAAgB,EAAE,qDAAqD,CAAC;KAC/E,MAAM,CAAC,gBAAgB,EAAE,yEAAyE,CAAC;KACnG,MAAM,CAAC,eAAe,EAAE,iCAAiC,EAAE,CAAC,CAAS,EAAE,GAAa,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;KACzG,MAAM,CAAC,OAAO,EAAE,4DAA4D,CAAC;KAC7E,MAAM,CAAC,iBAAiB,EAAE,4BAA4B,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;KACjF,MAAM,CAAC,sBAAsB,EAAE,iDAAiD,CAAC;KACjF,MAAM,CAAC,uBAAuB,EAAE,0DAA0D,CAAC;KAC3F,MAAM,CAAC,iBAAiB,EAAE,2CAA2C,CAAC;KACtE,MAAM,CAAC,aAAa,EAAE,iBAAiB,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;KAClE,MAAM,CAAC,kBAAkB,EAAE,6BAA6B,CAAC;KACzD,MAAM,CAAC,iBAAiB,EAAE,uDAAuD,CAAC;KAClF,MAAM,CAAC,qBAAqB,EAAE,sBAAsB,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;KAC/E,MAAM,CAAC,0BAA0B,CAAC;KAClC,MAAM,CAAC,oBAAoB,EAAE,0BAA0B,EAAE,CAAC,CAAS,EAAE,GAAa,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;KACvG,MAAM,CAAC,6BAA6B,EAAE,2BAA2B,CAAC;KAClE,MAAM,CAAC,SAAS,EAAE,yDAAyD,CAAC;KAC5E,MAAM,CAAC,2BAA2B,EAAE,6CAA6C,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;KAC5G,MAAM,CAAC,0BAA0B,EAAE,sDAAsD,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;KACpH,MAAM,CACL,wBAAwB,EACxB,mGAAmG,EACnG,CAAC,CAAS,EAAE,GAAa,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAC9C;IACD,6EAA6E;IAC7E,2DAA2D;KAC1D,MAAM,CAAC,YAAY,EAAE,kEAAkE,CAAC;KACxF,MAAM,CAAC,kBAAkB,EAAE,wCAAwC,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;KAC9F,MAAM,CAAC,WAAW,EAAE,qCAAqC,CAAC;KAC1D,MAAM,CAAC,yBAAyB,EAAE,kCAAkC,CAAC;KACrE,MAAM,CAAC,kBAAkB,EAAE,oBAAoB,CAAC;KAChD,MAAM,CAAC,QAAQ,EAAE,2CAA2C,CAAC;KAC7D,MAAM,CAAC,KAAK,EAAE,IAA6B,EAAE,EAAE;IAC9C,mEAAmE;IACnE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,CAAA;IACrC,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACxE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,KAAK,CAAA;IACvC,MAAM,QAAQ,GAAG,EAAE,GAAG,IAAI,EAAE,CAAA;IAC5B,+CAA+C;IAC/C,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,IAAI,QAAQ,CAAC,aAAa,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnF,OAAO,QAAQ,CAAC,aAAa,CAAC,CAAA;IAChC,CAAC;IACD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAA;IACxB,0EAA0E;IAC1E,6CAA6C;IAC7C,MAAM,UAAU,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAA;IACzC,OAAO,QAAQ,CAAC,YAAY,CAAC,CAAA;IAC7B,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;QACnC,IAAI,QAAQ,CAAC,QAAQ,CAAC,KAAK,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,kDAAkD,CAAC,CAAA;YAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjB,CAAC;QACD,IAAI,CAAC;YACH,QAAQ,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAC,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;QAChF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,kBAAkB,UAAU,KAAK,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAC3D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjB,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAuC,CAAA;IACnF,IAAI,IAAI,KAAK,SAAS;QAAE,OAAQ,KAA2B,CAAC,IAAI,CAAA;;QAC3D,KAAK,CAAC,IAAI,GAAG,IAAI,CAAA;IACtB,OAAO,CAAC,IAAI,CAAC,MAAM,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;AAC9C,CAAC,CAAC,CAAA;AAEJ,OAAO;KACJ,OAAO,CAAC,sBAAsB,CAAC;KAC/B,WAAW,CAAC,6CAA6C,CAAC;KAC1D,MAAM,CAAC,kBAAkB,EAAE,8BAA8B,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;KACpF,MAAM,CAAC,eAAe,EAAE,2CAA2C,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;KAC9F,MAAM,CAAC,QAAQ,EAAE,yBAAyB,CAAC;KAC3C,MAAM,CAAC,KAAK,EAAE,SAAiB,EAAE,IAAuD,EAAE,EAAE;IAC3F,OAAO,CAAC,IAAI,CAAC,MAAM,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAA;AAC3D,CAAC,CAAC,CAAA;AAEJ,OAAO;KACJ,OAAO,CAAC,qBAAqB,CAAC;KAC9B,WAAW,CAAC,oDAAoD,CAAC;KACjE,MAAM,CAAC,QAAQ,EAAE,yBAAyB,CAAC;KAC3C,MAAM,CAAC,KAAK,EAAE,SAAiB,EAAE,IAAwB,EAAE,EAAE;IAC5D,OAAO,CAAC,IAAI,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAA;AAC1D,CAAC,CAAC,CAAA;AAEJ,OAAO;KACJ,OAAO,CAAC,gBAAgB,CAAC;KACzB,WAAW,CAAC,iFAAiF,CAAC;KAC9F,MAAM,CACL,kBAAkB,EAClB,6FAA6F,EAC7F,CAAC,CAAS,EAAE,GAAa,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAC9C;KACA,MAAM,CAAC,gBAAgB,EAAE,oFAAoF,CAAC;KAC9G,MAAM,CAAC,CAAC,QAAgB,EAAE,IAA6C,EAAE,EAAE;IAC1E,yEAAyE;IACzE,MAAM,KAAK,GAAqC;QAC9C,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxD,CAAA;IACD,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAA;AACjD,CAAC,CAAC,CAAA;AAEJ,OAAO;KACJ,OAAO,CAAC,oBAAoB,CAAC;KAC7B,WAAW,CAAC,iEAAiE,CAAC;KAC9E,MAAM,CAAC,KAAK,EAAE,SAAiB,EAAE,EAAE;IAClC,OAAO,CAAC,IAAI,CAAC,MAAM,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAA;AACnD,CAAC,CAAC,CAAA;AAEJ,OAAO;KACJ,OAAO,CAAC,cAAc,CAAC;KACvB,WAAW,CAAC,yDAAyD,CAAC;IACvE,6EAA6E;IAC7E,yEAAyE;IACzE,qBAAqB;KACpB,MAAM,CAAC,gBAAgB,EAAE,2BAA2B,CAAC;KACrD,MAAM,CAAC,kBAAkB,EAAE,iCAAiC,CAAC;KAC7D,MAAM,CAAC,KAAK,EAAE,KAAa,EAAE,IAA0B,EAAE,EAAE;IAC1D,OAAO,CAAC,IAAI,CACV,MAAM,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAC/F,CAAA;AACH,CAAC,CAAC,CAAA;AAEJ,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,uDAAuD,CAAC,CAAA;AAC3G,KAAK;KACF,OAAO,CAAC,SAAS,CAAC;KAClB,WAAW,CAAC,iEAAiE,CAAC;KAC9E,MAAM,CACL,kBAAkB,EAClB,iHAAiH,CAClH;KACA,MAAM,CAAC,UAAU,EAAE,oDAAoD,CAAC;KACxE,MAAM,CAAC,CAAC,IAA4C,EAAE,EAAE;IACvD,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AAC/C,CAAC,CAAC,CAAA;AACJ,KAAK;KACF,OAAO,CAAC,WAAW,CAAC;KACpB,WAAW,CAAC,qCAAqC,CAAC;KAClD,MAAM,CAAC,kBAAkB,EAAE,6DAA6D,CAAC;KACzF,MAAM,CAAC,UAAU,EAAE,iCAAiC,CAAC;KACrD,MAAM,CAAC,CAAC,IAA4C,EAAE,EAAE;IACvD,OAAO,CAAC,IAAI,CAAC,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AACjD,CAAC,CAAC,CAAA;AAEJ,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC,8BAA8B,CAAC,CAAA;AACpF,MAAM;KACH,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,iCAAiC,CAAC;KAC9C,MAAM,CAAC,WAAW,EAAE,kCAAkC,CAAC;KACvD,MAAM,CAAC,QAAQ,EAAE,yBAAyB,CAAC;KAC3C,MAAM,CAAC,CAAC,IAA2C,EAAE,EAAE;IACtD,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AAC7C,CAAC,CAAC,CAAA;AACJ,MAAM;KACH,OAAO,CAAC,mBAAmB,CAAC;KAC5B,WAAW,CACV,iHAAiH,CAClH;KACA,MAAM,CAAC,WAAW,EAAE,kDAAkD,CAAC;KACvE,MAAM,CAAC,SAAS,EAAE,mEAAmE,CAAC;KACtF,MAAM,CAAC,gBAAgB,EAAE,2BAA2B,CAAC;KACrD,MAAM,CAAC,OAAO,EAAE,4BAA4B,CAAC;KAC7C,MAAM,CACL,KAAK,EACH,GAAW,EACX,KAAa,EACb,IAA6E,EAC7E,EAAE;IACF,OAAO,CAAC,IAAI,CAAC,MAAM,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;AAC9D,CAAC,CACF,CAAA;AAEH,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CACV,mFAAmF;IACjF,+EAA+E,CAClF;KACA,MAAM,CAAC,mBAAmB,EAAE,oEAAoE,CAAC;KACjG,MAAM,CAAC,UAAU,EAAE,kFAAkF,CAAC;KACtG,MAAM,CAAC,aAAa,EAAE,+CAA+C,CAAC;KACtE,MAAM,CAAC,SAAS,EAAE,uDAAuD,CAAC;KAC1E,MAAM,CAAC,YAAY,EAAE,oCAAoC,CAAC;KAC1D,MAAM,CAAC,KAAK,EAAE,IAA+D,EAAE,EAAE;IAChF,OAAO,CAAC,IAAI,CAAC,MAAM,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AAC7C,CAAC,CAAC,CAAA;AAEJ,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,uFAAuF,CAAC;KACpG,MAAM,CAAC,QAAQ,EAAE,yBAAyB,CAAC;KAC3C,MAAM,CAAC,KAAK,EAAE,IAAwB,EAAE,EAAE;IACzC,OAAO,CAAC,IAAI,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AAC/C,CAAC,CAAC,CAAA;AAEJ,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The OpenCode adapter.
3
+ *
4
+ * OpenCode's extension point is a TypeScript/JavaScript plugin module, not a
5
+ * command hook, so it needs a different adapter rather than a different path
6
+ * through the existing one. What it is *not* is different logic: the generated
7
+ * plugin shells out to the same `notifai hook <event>` commands Claude Code and
8
+ * Codex invoke, and every decision — presence, the grace window, the reply
9
+ * wait, retirement — stays in one place.
10
+ *
11
+ * The three lifecycle joints map onto the same three events. OpenCode exposes
12
+ * `chat.message` as a direct hook, while session lifecycle notifications arrive
13
+ * through its generic `event` hook:
14
+ *
15
+ * chat.message -> user-prompt-submit (the user is at the keyboard)
16
+ * session.idle -> stop (the turn ended; escalate)
17
+ * session.deleted -> session-end (retire local session state)
18
+ *
19
+ * Permission prompts deliberately stay in OpenCode. The CLI question flow
20
+ * only escalates questions explicitly registered with `notifai ask`.
21
+ */
22
+ /** Marker line; also how an install from another checkout is recognised. */
23
+ export declare const OPENCODE_PLUGIN_MARKER = "// notifai managed opencode plugin";
24
+ /** Filename we own inside whichever plugins directory is being written. */
25
+ export declare const OPENCODE_PLUGIN_FILENAME = "notifai.js";
26
+ export declare function opencodePluginDir(global: boolean, cwd: string, env?: NodeJS.ProcessEnv): string;
27
+ export declare function opencodePluginPath(global: boolean, cwd: string, env?: NodeJS.ProcessEnv): string;
28
+ export interface OpencodePluginOptions {
29
+ execPath: string;
30
+ scriptPath: string;
31
+ /** Child-process lifetime; covers grace, reply wait, and teardown headroom. */
32
+ timeoutSeconds: number;
33
+ }
34
+ /**
35
+ * The generated module.
36
+ *
37
+ * Written as a string rather than compiled from a source file because it has to
38
+ * embed the absolute interpreter and script paths — a bare `notifai` on PATH
39
+ * fails silently under a plugin host the same way it does under a hook shell.
40
+ *
41
+ * Everything it does is build the hook envelope OpenCode's event gives it and
42
+ * hand that to the CLI on stdin. It deliberately fails open: an adapter error
43
+ * never changes OpenCode's own behavior.
44
+ */
45
+ export declare function opencodePluginSource(options: OpencodePluginOptions): string;
46
+ /** True when a plugin file on disk is one of ours, whoever installed it. */
47
+ export declare function isOurOpencodePlugin(contents: string): boolean;
48
+ /**
49
+ * The interpreter and script a generated plugin will actually run.
50
+ *
51
+ * A plugin is a module rather than a command line, so which *build* it invokes
52
+ * is not visible in the way it is for a settings-file handler — it is baked
53
+ * into the two constants at the top. `doctor` needs it to answer the question
54
+ * it exists to answer ("are two checkouts both installed?"), and without it
55
+ * the plugin's own path stands in for the build and every OpenCode install
56
+ * looks like a second one.
57
+ *
58
+ * Returns null for a plugin we did not generate, or one whose constants have
59
+ * been edited out of recognisable shape.
60
+ */
61
+ export declare function opencodePluginTarget(contents: string): {
62
+ exec: string;
63
+ script: string;
64
+ current: boolean;
65
+ } | null;
@@ -0,0 +1,181 @@
1
+ import path from 'node:path';
2
+ import { configHome } from './install-hooks.js';
3
+ /**
4
+ * The OpenCode adapter.
5
+ *
6
+ * OpenCode's extension point is a TypeScript/JavaScript plugin module, not a
7
+ * command hook, so it needs a different adapter rather than a different path
8
+ * through the existing one. What it is *not* is different logic: the generated
9
+ * plugin shells out to the same `notifai hook <event>` commands Claude Code and
10
+ * Codex invoke, and every decision — presence, the grace window, the reply
11
+ * wait, retirement — stays in one place.
12
+ *
13
+ * The three lifecycle joints map onto the same three events. OpenCode exposes
14
+ * `chat.message` as a direct hook, while session lifecycle notifications arrive
15
+ * through its generic `event` hook:
16
+ *
17
+ * chat.message -> user-prompt-submit (the user is at the keyboard)
18
+ * session.idle -> stop (the turn ended; escalate)
19
+ * session.deleted -> session-end (retire local session state)
20
+ *
21
+ * Permission prompts deliberately stay in OpenCode. The CLI question flow
22
+ * only escalates questions explicitly registered with `notifai ask`.
23
+ */
24
+ /** Marker line; also how an install from another checkout is recognised. */
25
+ export const OPENCODE_PLUGIN_MARKER = '// notifai managed opencode plugin';
26
+ /** Filename we own inside whichever plugins directory is being written. */
27
+ export const OPENCODE_PLUGIN_FILENAME = 'notifai.js';
28
+ /** Bump when an installed generated file must be rewritten to remain functional. */
29
+ const OPENCODE_ADAPTER_VERSION = 2;
30
+ export function opencodePluginDir(global, cwd, env = process.env) {
31
+ return global
32
+ ? path.join(configHome(env, 'OPENCODE_CONFIG_DIR', '.config/opencode'), 'plugins')
33
+ : path.join(cwd, '.opencode', 'plugins');
34
+ }
35
+ export function opencodePluginPath(global, cwd, env = process.env) {
36
+ return path.join(opencodePluginDir(global, cwd, env), OPENCODE_PLUGIN_FILENAME);
37
+ }
38
+ /**
39
+ * The generated module.
40
+ *
41
+ * Written as a string rather than compiled from a source file because it has to
42
+ * embed the absolute interpreter and script paths — a bare `notifai` on PATH
43
+ * fails silently under a plugin host the same way it does under a hook shell.
44
+ *
45
+ * Everything it does is build the hook envelope OpenCode's event gives it and
46
+ * hand that to the CLI on stdin. It deliberately fails open: an adapter error
47
+ * never changes OpenCode's own behavior.
48
+ */
49
+ export function opencodePluginSource(options) {
50
+ const { execPath, scriptPath, timeoutSeconds } = options;
51
+ return `${OPENCODE_PLUGIN_MARKER}
52
+ // Generated by \`notifai hooks install --harness opencode\`. Edits are lost on
53
+ // the next install; change the CLI instead, which is where the logic lives.
54
+ import { spawn } from "node:child_process"
55
+
56
+ const EXEC = ${JSON.stringify(execPath)}
57
+ const SCRIPT = ${JSON.stringify(scriptPath)}
58
+ const TIMEOUT_MS = ${timeoutSeconds * 1000}
59
+ const ADAPTER_VERSION = ${OPENCODE_ADAPTER_VERSION}
60
+
61
+ /**
62
+ * Runs \`notifai hook <event>\`, feeding it the same JSON envelope the other
63
+ * harnesses send. Resolves to stdout, or null if anything at all went wrong.
64
+ */
65
+ function runHook(event, envelope) {
66
+ return new Promise((resolve) => {
67
+ let child
68
+ try {
69
+ child = spawn(EXEC, [SCRIPT, "hook", event, "--owner", "notifai"], {
70
+ env: process.env,
71
+ shell: false,
72
+ stdio: ["pipe", "pipe", "pipe"],
73
+ })
74
+ } catch {
75
+ resolve(null)
76
+ return
77
+ }
78
+
79
+ let stdout = ""
80
+ let settled = false
81
+ const finish = (value) => {
82
+ if (settled) return
83
+ settled = true
84
+ clearTimeout(timer)
85
+ resolve(value)
86
+ }
87
+ const timer = setTimeout(() => {
88
+ child.kill("SIGTERM")
89
+ finish(null)
90
+ }, TIMEOUT_MS)
91
+
92
+ child.stdout?.setEncoding("utf-8")
93
+ child.stdout?.on("data", (chunk) => {
94
+ stdout += chunk
95
+ })
96
+ // Hook stderr is diagnostics, not plugin output; discard it here.
97
+ child.stderr?.resume()
98
+ child.on("error", () => finish(null))
99
+ child.on("close", () => finish(stdout))
100
+
101
+ try {
102
+ child.stdin?.end(JSON.stringify(envelope))
103
+ } catch {
104
+ finish(null)
105
+ }
106
+ })
107
+ }
108
+
109
+ export const NotifAIPlugin = async ({ directory }) => {
110
+ const cwd = typeof directory === "string" && directory.length > 0 ? directory : process.cwd()
111
+
112
+ return {
113
+ /**
114
+ * Presence. The user typing is the signal every harness shares, and it is
115
+ * also what publishes the project -> session pointer \`notifai ask\` reads.
116
+ */
117
+ "chat.message": async (input) => {
118
+ await runHook("user-prompt-submit", {
119
+ session_id: input?.sessionID ?? input?.info?.sessionID,
120
+ cwd,
121
+ hook_event_name: "UserPromptSubmit",
122
+ })
123
+ },
124
+
125
+ /** Session lifecycle events are delivered through OpenCode's event bus. */
126
+ event: async ({ event }) => {
127
+ if (event?.type === "session.idle") {
128
+ await runHook("stop", {
129
+ session_id: event?.properties?.sessionID,
130
+ cwd,
131
+ hook_event_name: "Stop",
132
+ })
133
+ } else if (event?.type === "session.deleted") {
134
+ await runHook("session-end", {
135
+ session_id: event?.properties?.info?.id,
136
+ cwd,
137
+ hook_event_name: "SessionEnd",
138
+ })
139
+ }
140
+ },
141
+ }
142
+ }
143
+ `;
144
+ }
145
+ /** True when a plugin file on disk is one of ours, whoever installed it. */
146
+ export function isOurOpencodePlugin(contents) {
147
+ return contents.includes(OPENCODE_PLUGIN_MARKER);
148
+ }
149
+ /**
150
+ * The interpreter and script a generated plugin will actually run.
151
+ *
152
+ * A plugin is a module rather than a command line, so which *build* it invokes
153
+ * is not visible in the way it is for a settings-file handler — it is baked
154
+ * into the two constants at the top. `doctor` needs it to answer the question
155
+ * it exists to answer ("are two checkouts both installed?"), and without it
156
+ * the plugin's own path stands in for the build and every OpenCode install
157
+ * looks like a second one.
158
+ *
159
+ * Returns null for a plugin we did not generate, or one whose constants have
160
+ * been edited out of recognisable shape.
161
+ */
162
+ export function opencodePluginTarget(contents) {
163
+ if (!isOurOpencodePlugin(contents))
164
+ return null;
165
+ const exec = /^const EXEC = (".*")$/m.exec(contents)?.[1];
166
+ const script = /^const SCRIPT = (".*")$/m.exec(contents)?.[1];
167
+ const version = Number(/^const ADAPTER_VERSION = (\d+)$/m.exec(contents)?.[1] ?? 1);
168
+ if (exec === undefined || script === undefined)
169
+ return null;
170
+ try {
171
+ return {
172
+ exec: JSON.parse(exec),
173
+ script: JSON.parse(script),
174
+ current: version === OPENCODE_ADAPTER_VERSION,
175
+ };
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ }
181
+ //# sourceMappingURL=opencode-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-plugin.js","sourceRoot":"","sources":["../src/opencode-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,sBAAsB,GAAG,oCAAoC,CAAA;AAE1E,2EAA2E;AAC3E,MAAM,CAAC,MAAM,wBAAwB,GAAG,YAAY,CAAA;AAEpD,oFAAoF;AACpF,MAAM,wBAAwB,GAAG,CAAC,CAAA;AAElC,MAAM,UAAU,iBAAiB,CAC/B,MAAe,EACf,GAAW,EACX,MAAyB,OAAO,CAAC,GAAG;IAEpC,OAAO,MAAM;QACX,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,qBAAqB,EAAE,kBAAkB,CAAC,EAAE,SAAS,CAAC;QAClF,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,SAAS,CAAC,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,MAAe,EACf,GAAW,EACX,MAAyB,OAAO,CAAC,GAAG;IAEpC,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,wBAAwB,CAAC,CAAA;AACjF,CAAC;AASD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAA8B;IACjE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,GAAG,OAAO,CAAA;IACxD,OAAO,GAAG,sBAAsB;;;;;eAKnB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;iBACtB,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;qBACtB,cAAc,GAAG,IAAI;0BAChB,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFjD,CAAA;AACD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IAClD,OAAO,QAAQ,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAAgB;IAEhB,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/C,MAAM,IAAI,GAAG,wBAAwB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IACzD,MAAM,MAAM,GAAG,0BAA0B,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,kCAAkC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IACnF,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAC3D,IAAI,CAAC;QACH,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAW;YAChC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAW;YACpC,OAAO,EAAE,OAAO,KAAK,wBAAwB;SAC9C,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * What "set up" means, in one place.
3
+ *
4
+ * `init` and `doctor` used to each carry their own idea of this — a fixed
5
+ * five-step script in one, a flat list of checks in the other — which is two
6
+ * copies of the same knowledge that can disagree, and did: doctor called
7
+ * missing hooks fine while init offered to install them, and only doctor knew
8
+ * what a revoked credential looked like.
9
+ *
10
+ * So the model lives here and both commands render it. `doctor` shows every
11
+ * state at once, because a report is what it is for. `init` walks the same
12
+ * list in order and stops at the first thing standing in the way, because
13
+ * someone handed five tasks does none of them. An agent gets the same states
14
+ * through a third renderer that never prompts.
15
+ *
16
+ * The ordering is a dependency chain, not a preference: there is no point
17
+ * asking the server about devices before there is a credential to ask with,
18
+ * and no point reporting a stale contract to someone who cannot reach the
19
+ * server at all. Assessment stops descending when a prerequisite is missing
20
+ * and marks what follows `unknown` rather than guessing.
21
+ */
22
+ /** Whether the gap can be closed from here, and by whom. */
23
+ export type Remedy =
24
+ /**
25
+ * The CLI can do it unattended — no prompt, no human. `command` is how a
26
+ * reader would ask for it later, for the case where it was declined or
27
+ * skipped; it is never needed to perform the action.
28
+ */
29
+ {
30
+ by: 'cli';
31
+ summary: string;
32
+ command?: string;
33
+ }
34
+ /**
35
+ * A human at this terminal. `interactive` marks the ones the CLI can drive
36
+ * on their behalf when someone is actually watching — a browser sign-in is
37
+ * the CLI's to launch but the human's to complete, so an agent must still
38
+ * treat it as out of reach.
39
+ */
40
+ | {
41
+ by: 'user-here';
42
+ summary: string;
43
+ command: string;
44
+ interactive?: boolean;
45
+ }
46
+ /**
47
+ * A human somewhere else entirely — phone in hand, app store, permission
48
+ * dialog. The class the old init could only print a sentence about, and the
49
+ * one most likely to end an onboarding.
50
+ */
51
+ | {
52
+ by: 'user-elsewhere';
53
+ summary: string;
54
+ };
55
+ export type StateStatus =
56
+ /** Satisfied. */
57
+ 'ready'
58
+ /** Not satisfied, and it stands in the way. */
59
+ | 'gap'
60
+ /** Not satisfied, and that is a legitimate choice. */
61
+ | 'optional-gap'
62
+ /** A prerequisite is missing, so this was never evaluated. */
63
+ | 'unknown';
64
+ export interface ReadinessState {
65
+ id: string;
66
+ /** Human words for what this is, not the field name. */
67
+ title: string;
68
+ status: StateStatus;
69
+ /** The current condition, phrased so it reads under either renderer. */
70
+ detail: string;
71
+ remedy?: Remedy;
72
+ }
73
+ export interface Readiness {
74
+ states: ReadinessState[];
75
+ }
76
+ /** The first thing standing in the way, or null when nothing is. */
77
+ export declare function firstBlocker(readiness: Readiness): ReadinessState | null;
78
+ /**
79
+ * Ready enough to be useful, which is not the same as every box ticked.
80
+ *
81
+ * Hooks and the agent skill are genuinely optional — `send` works without
82
+ * them — so an install that declined both is finished, not half-done. Saying
83
+ * otherwise trains people to ignore the summary line.
84
+ */
85
+ export declare function isReady(readiness: Readiness): boolean;
86
+ /** Everything that could still be done, blocking or not, in dependency order. */
87
+ export declare function openItems(readiness: Readiness): ReadinessState[];
@@ -0,0 +1,40 @@
1
+ /**
2
+ * What "set up" means, in one place.
3
+ *
4
+ * `init` and `doctor` used to each carry their own idea of this — a fixed
5
+ * five-step script in one, a flat list of checks in the other — which is two
6
+ * copies of the same knowledge that can disagree, and did: doctor called
7
+ * missing hooks fine while init offered to install them, and only doctor knew
8
+ * what a revoked credential looked like.
9
+ *
10
+ * So the model lives here and both commands render it. `doctor` shows every
11
+ * state at once, because a report is what it is for. `init` walks the same
12
+ * list in order and stops at the first thing standing in the way, because
13
+ * someone handed five tasks does none of them. An agent gets the same states
14
+ * through a third renderer that never prompts.
15
+ *
16
+ * The ordering is a dependency chain, not a preference: there is no point
17
+ * asking the server about devices before there is a credential to ask with,
18
+ * and no point reporting a stale contract to someone who cannot reach the
19
+ * server at all. Assessment stops descending when a prerequisite is missing
20
+ * and marks what follows `unknown` rather than guessing.
21
+ */
22
+ /** The first thing standing in the way, or null when nothing is. */
23
+ export function firstBlocker(readiness) {
24
+ return readiness.states.find((s) => s.status === 'gap') ?? null;
25
+ }
26
+ /**
27
+ * Ready enough to be useful, which is not the same as every box ticked.
28
+ *
29
+ * Hooks and the agent skill are genuinely optional — `send` works without
30
+ * them — so an install that declined both is finished, not half-done. Saying
31
+ * otherwise trains people to ignore the summary line.
32
+ */
33
+ export function isReady(readiness) {
34
+ return firstBlocker(readiness) === null;
35
+ }
36
+ /** Everything that could still be done, blocking or not, in dependency order. */
37
+ export function openItems(readiness) {
38
+ return readiness.states.filter((s) => s.status === 'gap' || s.status === 'optional-gap');
39
+ }
40
+ //# sourceMappingURL=readiness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readiness.js","sourceRoot":"","sources":["../src/readiness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAgDH,oEAAoE;AACpE,MAAM,UAAU,YAAY,CAAC,SAAoB;IAC/C,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,IAAI,IAAI,CAAA;AACjE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,SAAoB;IAC1C,OAAO,YAAY,CAAC,SAAS,CAAC,KAAK,IAAI,CAAA;AACzC,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,SAAS,CAAC,SAAoB;IAC5C,OAAO,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,CAAC,MAAM,KAAK,cAAc,CAAC,CAAA;AAC1F,CAAC"}