@raidiant/notifai 0.2.1 → 0.4.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 (94) hide show
  1. package/README.md +20 -4
  2. package/dist/atomic-file.d.ts +4 -0
  3. package/dist/atomic-file.js +60 -10
  4. package/dist/atomic-file.js.map +1 -1
  5. package/dist/client.d.ts +25 -2
  6. package/dist/client.js +45 -1
  7. package/dist/client.js.map +1 -1
  8. package/dist/commands.d.ts +85 -6
  9. package/dist/commands.js +1133 -322
  10. package/dist/commands.js.map +1 -1
  11. package/dist/config-schema.d.ts +62 -0
  12. package/dist/config-schema.js +312 -0
  13. package/dist/config-schema.js.map +1 -0
  14. package/dist/config.d.ts +38 -3
  15. package/dist/config.js +61 -14
  16. package/dist/config.js.map +1 -1
  17. package/dist/file-lock.d.ts +26 -0
  18. package/dist/file-lock.js +268 -0
  19. package/dist/file-lock.js.map +1 -0
  20. package/dist/harnesses.d.ts +28 -0
  21. package/dist/harnesses.js +40 -0
  22. package/dist/harnesses.js.map +1 -0
  23. package/dist/hook-adapter.d.ts +27 -0
  24. package/dist/hook-adapter.js +188 -0
  25. package/dist/hook-adapter.js.map +1 -0
  26. package/dist/hook-input.d.ts +10 -0
  27. package/dist/hook-input.js +64 -0
  28. package/dist/hook-input.js.map +1 -0
  29. package/dist/hooks.d.ts +66 -10
  30. package/dist/hooks.js +751 -276
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/install-hooks.d.ts +40 -16
  33. package/dist/install-hooks.js +217 -73
  34. package/dist/install-hooks.js.map +1 -1
  35. package/dist/interactive.d.ts +4 -0
  36. package/dist/interactive.js +604 -0
  37. package/dist/interactive.js.map +1 -0
  38. package/dist/logging.d.ts +142 -0
  39. package/dist/logging.js +533 -0
  40. package/dist/logging.js.map +1 -0
  41. package/dist/main.js +201 -51
  42. package/dist/main.js.map +1 -1
  43. package/dist/opencode-plugin.d.ts +7 -7
  44. package/dist/opencode-plugin.js +12 -13
  45. package/dist/opencode-plugin.js.map +1 -1
  46. package/dist/ui/banner.d.ts +22 -0
  47. package/dist/ui/banner.js +87 -0
  48. package/dist/ui/banner.js.map +1 -0
  49. package/dist/ui/config-view.d.ts +32 -0
  50. package/dist/ui/config-view.js +108 -0
  51. package/dist/ui/config-view.js.map +1 -0
  52. package/dist/ui/help.d.ts +46 -0
  53. package/dist/ui/help.js +105 -0
  54. package/dist/ui/help.js.map +1 -0
  55. package/dist/ui/theme.d.ts +108 -0
  56. package/dist/ui/theme.js +261 -0
  57. package/dist/ui/theme.js.map +1 -0
  58. package/package.json +11 -9
  59. package/src/atomic-file.ts +86 -7
  60. package/src/client.test.ts +12 -0
  61. package/src/client.ts +72 -4
  62. package/src/commands.test.ts +971 -44
  63. package/src/commands.ts +1323 -337
  64. package/src/config-schema.test.ts +152 -0
  65. package/src/config-schema.ts +363 -0
  66. package/src/config.test.ts +6 -5
  67. package/src/config.ts +85 -16
  68. package/src/file-lock-process-worker.test.ts +64 -0
  69. package/src/file-lock.test.ts +279 -0
  70. package/src/file-lock.ts +318 -0
  71. package/src/harnesses.test.ts +48 -0
  72. package/src/harnesses.ts +59 -0
  73. package/src/hook-adapter.test.ts +187 -0
  74. package/src/hook-adapter.ts +212 -0
  75. package/src/hook-input.test.ts +39 -0
  76. package/src/hook-input.ts +72 -0
  77. package/src/hooks.test.ts +1318 -240
  78. package/src/hooks.ts +1046 -296
  79. package/src/install-hooks.test.ts +90 -41
  80. package/src/install-hooks.ts +259 -74
  81. package/src/interactive.ts +692 -0
  82. package/src/logging-process-worker.test.ts +39 -0
  83. package/src/logging.test.ts +453 -0
  84. package/src/logging.ts +639 -0
  85. package/src/main.ts +220 -48
  86. package/src/opencode-plugin.ts +15 -16
  87. package/src/session-end-log-lock-worker.test.ts +35 -0
  88. package/src/skill.test.ts +11 -0
  89. package/src/ui/banner.ts +95 -0
  90. package/src/ui/config-view.test.ts +88 -0
  91. package/src/ui/config-view.ts +138 -0
  92. package/src/ui/help.ts +124 -0
  93. package/src/ui/theme.test.ts +92 -0
  94. package/src/ui/theme.ts +279 -0
@@ -0,0 +1,26 @@
1
+ export type FileLockObservation = {
2
+ phase: 'choosing-published';
3
+ entry: string;
4
+ } | {
5
+ phase: 'stale-entry';
6
+ entry: string;
7
+ } | {
8
+ phase: 'waiting';
9
+ blockers: string[];
10
+ };
11
+ interface FileLockOptions {
12
+ waitMs?: number;
13
+ observe?: (observation: FileLockObservation) => void;
14
+ /** Installer transactions report lock replacement after a successful action. */
15
+ strictRelease?: boolean;
16
+ }
17
+ export declare function withFileLock<T>(file: string, action: () => T, options?: FileLockOptions): T;
18
+ /**
19
+ * Serialize a read/merge/write transaction for one user-owned target file.
20
+ *
21
+ * This is the single installer-facing lock policy: the target's direct parent
22
+ * is a real current-user-owned directory, remains the same inode throughout,
23
+ * and the bakery rendezvous lives beside the target rather than inside it.
24
+ */
25
+ export declare function withTargetFileLock<T>(target: string, action: () => T, options?: Omit<FileLockOptions, 'strictRelease'>): T;
26
+ export {};
@@ -0,0 +1,268 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { chmodSync, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, renameSync, rmdirSync, unlinkSync, } from 'node:fs';
3
+ import path from 'node:path';
4
+ /**
5
+ * Serialize a short cross-process filesystem transaction.
6
+ *
7
+ * Each contender publishes all of its ownership in one unique filename. A
8
+ * two-phase bakery lock then orders live contenders by ticket and process
9
+ * identity. Because paths are never reused, recovering an entry left by a dead
10
+ * process cannot remove a later owner's entry; because process liveness, rather
11
+ * than age, decides recovery, a delayed holder is never mistaken for a dead one.
12
+ *
13
+ * Acquisition is deliberately bounded because both harness hooks and logging
14
+ * must fail open rather than hold an agent turn indefinitely.
15
+ */
16
+ const FILE_LOCK_WAIT_MS = 1_000;
17
+ const FILE_LOCK_POLL_MS = 5;
18
+ const FILE_LOCK_TOKEN_BYTES = 12;
19
+ const ENTRY_PATTERN = /^(choosing|ticket-(\d+))-(\d+)-([0-9a-f]+)$/;
20
+ const lockSleep = new Int32Array(new SharedArrayBuffer(4));
21
+ function parseEntry(name) {
22
+ const match = ENTRY_PATTERN.exec(name);
23
+ if (match === null)
24
+ return null;
25
+ const pid = Number(match[3]);
26
+ if (!Number.isSafeInteger(pid) || pid <= 0)
27
+ return null;
28
+ const token = match[4];
29
+ if (match[1] === 'choosing')
30
+ return { kind: 'choosing', name, pid, token };
31
+ try {
32
+ const ticket = BigInt(match[2]);
33
+ if (ticket <= 0n)
34
+ return null;
35
+ return { kind: 'ticket', name, pid, token, ticket };
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function processIsAlive(pid) {
42
+ if (pid === process.pid)
43
+ return true;
44
+ try {
45
+ process.kill(pid, 0);
46
+ return true;
47
+ }
48
+ catch (err) {
49
+ const code = err.code;
50
+ if (code === 'ESRCH')
51
+ return false;
52
+ // EPERM means the process exists but is owned by somebody else. Unknown
53
+ // failures are also treated as live: losing recovery is safer than allowing
54
+ // two holders into the transaction.
55
+ return true;
56
+ }
57
+ }
58
+ function currentUid() {
59
+ return typeof process.getuid === 'function' ? process.getuid() : undefined;
60
+ }
61
+ function assertOwnedDirectory(directory) {
62
+ const stat = lstatSync(directory);
63
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
64
+ throw new Error(`${directory} is not a regular directory; refusing to use it as a file lock.`);
65
+ }
66
+ const uid = currentUid();
67
+ if (uid !== undefined && stat.uid !== uid) {
68
+ throw new Error(`${directory} is owned by uid ${stat.uid}, not the current user.`);
69
+ }
70
+ return { dev: stat.dev, ino: stat.ino };
71
+ }
72
+ function assertSameDirectory(directory, expected) {
73
+ const current = lstatSync(directory);
74
+ if (current.isSymbolicLink() ||
75
+ !current.isDirectory() ||
76
+ current.dev !== expected.dev ||
77
+ current.ino !== expected.ino) {
78
+ throw new Error(`${directory} changed while the file lock was active.`);
79
+ }
80
+ }
81
+ function sameIdentity(file, expected) {
82
+ try {
83
+ const current = lstatSync(file);
84
+ return current.dev === expected.dev && current.ino === expected.ino;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ /** Live protocol entries, pruning only unique paths whose process is gone. */
91
+ function liveEntries(directory, directoryIdentity, observe) {
92
+ assertSameDirectory(directory, directoryIdentity);
93
+ const live = [];
94
+ for (const name of readdirSync(directory)) {
95
+ const entry = parseEntry(name);
96
+ if (entry === null)
97
+ continue;
98
+ const entryPath = path.join(directory, name);
99
+ let stat;
100
+ try {
101
+ stat = lstatSync(entryPath);
102
+ }
103
+ catch (err) {
104
+ if (err.code === 'ENOENT')
105
+ continue;
106
+ throw err;
107
+ }
108
+ const uid = currentUid();
109
+ if (stat.isSymbolicLink() || !stat.isFile() || (uid !== undefined && stat.uid !== uid)) {
110
+ throw new Error(`${entryPath} is not a current-user-owned regular file-lock entry.`);
111
+ }
112
+ if (processIsAlive(entry.pid)) {
113
+ live.push(entry);
114
+ continue;
115
+ }
116
+ observe?.({ phase: 'stale-entry', entry: name });
117
+ try {
118
+ // Recheck the unique dead owner's inode so delayed recovery cannot reap
119
+ // a replacement published at the same path by external interference.
120
+ const current = lstatSync(entryPath);
121
+ if (current.dev === stat.dev && current.ino === stat.ino)
122
+ unlinkSync(entryPath);
123
+ }
124
+ catch {
125
+ // Another contender already recovered this exact dead owner's unique path.
126
+ }
127
+ }
128
+ return live;
129
+ }
130
+ function publishEmpty(file) {
131
+ const handle = openSync(file, 'wx', 0o600);
132
+ try {
133
+ const stat = fstatSync(handle);
134
+ return { dev: stat.dev, ino: stat.ino };
135
+ }
136
+ finally {
137
+ closeSync(handle);
138
+ }
139
+ }
140
+ function publishChoosing(directory, name, deadline, observe) {
141
+ const file = path.join(directory, name);
142
+ for (;;) {
143
+ try {
144
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
145
+ const directoryIdentity = assertOwnedDirectory(directory);
146
+ chmodSync(directory, 0o700);
147
+ // Recover before registration so a delayed stale cleanup can only name an
148
+ // old owner's unique path, never this contender's future one.
149
+ liveEntries(directory, directoryIdentity, observe);
150
+ const fileIdentity = publishEmpty(file);
151
+ assertSameDirectory(directory, directoryIdentity);
152
+ return { file, fileIdentity, directoryIdentity };
153
+ }
154
+ catch (err) {
155
+ if (err.code !== 'ENOENT')
156
+ throw err;
157
+ // The previous holder removed an empty rendezvous directory between our
158
+ // mkdir and publication. Recreate it; once our entry exists, no cooperating
159
+ // releaser can remove the non-empty directory underneath us.
160
+ if (Date.now() >= deadline) {
161
+ throw new Error(`timed out publishing file lock contender in ${directory}`);
162
+ }
163
+ Atomics.wait(lockSleep, 0, 0, FILE_LOCK_POLL_MS);
164
+ }
165
+ }
166
+ }
167
+ function comesBefore(entry, ticket, pid, token) {
168
+ if (entry.ticket !== ticket)
169
+ return entry.ticket < ticket;
170
+ if (entry.pid !== pid)
171
+ return entry.pid < pid;
172
+ return entry.token < token;
173
+ }
174
+ export function withFileLock(file, action, options = {}) {
175
+ const waitMs = options.waitMs ?? FILE_LOCK_WAIT_MS;
176
+ const deadline = Date.now() + waitMs;
177
+ const pid = process.pid;
178
+ const token = randomBytes(FILE_LOCK_TOKEN_BYTES).toString('hex');
179
+ const choosingName = `choosing-${pid}-${token}`;
180
+ let ownedPath = null;
181
+ let ownedIdentity = null;
182
+ let directoryIdentity = null;
183
+ let operationFailed = false;
184
+ try {
185
+ const published = publishChoosing(file, choosingName, deadline, options.observe);
186
+ ownedPath = published.file;
187
+ ownedIdentity = published.fileIdentity;
188
+ directoryIdentity = published.directoryIdentity;
189
+ options.observe?.({ phase: 'choosing-published', entry: choosingName });
190
+ let highest = 0n;
191
+ for (const entry of liveEntries(file, directoryIdentity, options.observe)) {
192
+ if (entry.kind === 'ticket' && entry.ticket > highest)
193
+ highest = entry.ticket;
194
+ }
195
+ const ticket = highest + 1n;
196
+ const ticketName = `ticket-${ticket}-${pid}-${token}`;
197
+ const ticketPath = path.join(file, ticketName);
198
+ // The rename is the doorway: observers see either a chooser or its complete
199
+ // ticket, never a gap in which a live contender has no published state.
200
+ renameSync(ownedPath, ticketPath);
201
+ ownedPath = ticketPath;
202
+ for (;;) {
203
+ const blockers = [];
204
+ for (const entry of liveEntries(file, directoryIdentity, options.observe)) {
205
+ if (entry.pid === pid && entry.token === token)
206
+ continue;
207
+ if (entry.kind === 'choosing' || comesBefore(entry, ticket, pid, token)) {
208
+ blockers.push(entry.name);
209
+ }
210
+ }
211
+ if (blockers.length === 0) {
212
+ assertSameDirectory(file, directoryIdentity);
213
+ return action();
214
+ }
215
+ options.observe?.({ phase: 'waiting', blockers });
216
+ if (Date.now() >= deadline)
217
+ throw new Error(`timed out waiting for file lock ${file}`);
218
+ Atomics.wait(lockSleep, 0, 0, FILE_LOCK_POLL_MS);
219
+ }
220
+ }
221
+ catch (err) {
222
+ operationFailed = true;
223
+ throw err;
224
+ }
225
+ finally {
226
+ let releaseChanged = false;
227
+ if (ownedPath !== null && ownedIdentity !== null) {
228
+ try {
229
+ if (sameIdentity(ownedPath, ownedIdentity))
230
+ unlinkSync(ownedPath);
231
+ else
232
+ releaseChanged = true;
233
+ }
234
+ catch {
235
+ // External cleanup cannot turn a release failure into a caller failure.
236
+ }
237
+ }
238
+ try {
239
+ if (directoryIdentity !== null)
240
+ assertSameDirectory(file, directoryIdentity);
241
+ rmdirSync(file);
242
+ }
243
+ catch {
244
+ // Another contender has published, or external cleanup already removed it.
245
+ }
246
+ if (releaseChanged && options.strictRelease === true && !operationFailed) {
247
+ throw new Error(`${ownedPath ?? file} changed while held; refusing to remove the replacement.`);
248
+ }
249
+ }
250
+ }
251
+ /**
252
+ * Serialize a read/merge/write transaction for one user-owned target file.
253
+ *
254
+ * This is the single installer-facing lock policy: the target's direct parent
255
+ * is a real current-user-owned directory, remains the same inode throughout,
256
+ * and the bakery rendezvous lives beside the target rather than inside it.
257
+ */
258
+ export function withTargetFileLock(target, action, options = {}) {
259
+ const parent = path.dirname(target);
260
+ mkdirSync(parent, { recursive: true });
261
+ const parentIdentity = assertOwnedDirectory(parent);
262
+ const lockDirectory = path.join(parent, `.${path.basename(target)}.notifai.lock`);
263
+ return withFileLock(lockDirectory, () => {
264
+ assertSameDirectory(parent, parentIdentity);
265
+ return action();
266
+ }, { ...options, strictRelease: true });
267
+ }
268
+ //# sourceMappingURL=file-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-lock.js","sourceRoot":"","sources":["../src/file-lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /** Every harness Notifai ships. This is the sole source of the harness union. */
2
+ export declare const HARNESSES: readonly ["claude-code", "codex", "cursor", "opencode"];
3
+ export type Harness = (typeof HARNESSES)[number];
4
+ /** One canonical wall-clock budget shared by config, hooks, and installers. */
5
+ export declare const HOOK_TIMING: Readonly<{
6
+ totalSeconds: 480;
7
+ finalizationReserveSeconds: 45;
8
+ submissionReserveSeconds: 40;
9
+ minimumReplySeconds: 60;
10
+ schedulingSlackSeconds: 1;
11
+ stdoutReserveSeconds: 5;
12
+ hostHeadroomSeconds: 60;
13
+ maxGraceSeconds: 334;
14
+ }>;
15
+ export type StopContinuation = 'decision-block' | 'followup-message' | 'unsupported';
16
+ export interface HarnessCapability {
17
+ /** How an answer is admitted to another turn in the already-open session. */
18
+ stopContinuation: StopContinuation;
19
+ /** Whether delivery remains native after the turn-end event has returned. */
20
+ dormantWake: 'unsupported' | 'native-session-prompt';
21
+ /** Concise, honest readiness text for doctor and installation guidance. */
22
+ deliveryContract: string;
23
+ }
24
+ /**
25
+ * Exhaustive runtime contract. Adding a harness without deciding every
26
+ * continuation joint is a type error, not a silently degraded notification.
27
+ */
28
+ export declare const HARNESS_CAPABILITIES: Record<Harness, HarnessCapability>;
@@ -0,0 +1,40 @@
1
+ /** Every harness Notifai ships. This is the sole source of the harness union. */
2
+ export const HARNESSES = ['claude-code', 'codex', 'cursor', 'opencode'];
3
+ /** One canonical wall-clock budget shared by config, hooks, and installers. */
4
+ export const HOOK_TIMING = Object.freeze({
5
+ totalSeconds: 480,
6
+ finalizationReserveSeconds: 45,
7
+ submissionReserveSeconds: 40,
8
+ minimumReplySeconds: 60,
9
+ schedulingSlackSeconds: 1,
10
+ stdoutReserveSeconds: 5,
11
+ hostHeadroomSeconds: 60,
12
+ maxGraceSeconds: 334,
13
+ });
14
+ /**
15
+ * Exhaustive runtime contract. Adding a harness without deciding every
16
+ * continuation joint is a type error, not a silently degraded notification.
17
+ */
18
+ export const HARNESS_CAPABILITIES = {
19
+ 'claude-code': {
20
+ stopContinuation: 'decision-block',
21
+ dormantWake: 'unsupported',
22
+ deliveryContract: 'live Stop continuation; no automatic wake after its bounded wait returns',
23
+ },
24
+ codex: {
25
+ stopContinuation: 'decision-block',
26
+ dormantWake: 'unsupported',
27
+ deliveryContract: 'live Stop continuation; no automatic wake after its bounded wait returns',
28
+ },
29
+ cursor: {
30
+ stopContinuation: 'unsupported',
31
+ dormantWake: 'unsupported',
32
+ deliveryContract: 'the hook can return a live follow-up, but the invoking shell exposes no exact conversation id; asynchronous ask is unsupported',
33
+ },
34
+ opencode: {
35
+ stopContinuation: 'unsupported',
36
+ dormantWake: 'unsupported',
37
+ deliveryContract: 'no proven answer continuation after session.idle; use a blocking reply command',
38
+ },
39
+ };
40
+ //# sourceMappingURL=harnesses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harnesses.js","sourceRoot":"","sources":["../src/harnesses.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,aAAa,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAU,CAAA;AAIhF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC;IACvC,YAAY,EAAE,GAAG;IACjB,0BAA0B,EAAE,EAAE;IAC9B,wBAAwB,EAAE,EAAE;IAC5B,mBAAmB,EAAE,EAAE;IACvB,sBAAsB,EAAE,CAAC;IACzB,oBAAoB,EAAE,CAAC;IACvB,mBAAmB,EAAE,EAAE;IACvB,eAAe,EAAE,GAAG;CACrB,CAAC,CAAA;AAgBF;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAuC;IACtE,aAAa,EAAE;QACb,gBAAgB,EAAE,gBAAgB;QAClC,WAAW,EAAE,aAAa;QAC1B,gBAAgB,EAAE,0EAA0E;KAC7F;IACD,KAAK,EAAE;QACL,gBAAgB,EAAE,gBAAgB;QAClC,WAAW,EAAE,aAAa;QAC1B,gBAAgB,EAAE,0EAA0E;KAC7F;IACD,MAAM,EAAE;QACN,gBAAgB,EAAE,aAAa;QAC/B,WAAW,EAAE,aAAa;QAC1B,gBAAgB,EACd,gIAAgI;KACnI;IACD,QAAQ,EAAE;QACR,gBAAgB,EAAE,aAAa;QAC/B,WAAW,EAAE,aAAa;QAC1B,gBAAgB,EACd,gFAAgF;KACnF;CACF,CAAA"}
@@ -0,0 +1,27 @@
1
+ export interface HookAdapterTarget {
2
+ execPath: string;
3
+ scriptPath: string;
4
+ }
5
+ export interface HookAdapterInspection {
6
+ path: string;
7
+ target: HookAdapterTarget | null;
8
+ problems: string[];
9
+ }
10
+ /**
11
+ * One user-level identity for every harness and every project/global install.
12
+ *
13
+ * The pathname is deliberately outside XDG configuration/state routing. XDG
14
+ * overrides routinely differ between shells, package managers, and harness
15
+ * processes; allowing one to select this path would put mutable environment
16
+ * back into every trusted definition. Only the user's home boundary selects
17
+ * it; CLI, Node, package-manager, checkout, and preference paths stay behind
18
+ * it and may change without changing hook identity.
19
+ */
20
+ export declare function hookAdapterPath(homeDir?: string): string;
21
+ /** Install or repair the stable adapter and atomically retarget its implementation. */
22
+ export declare function installHookAdapter(target: HookAdapterTarget, homeDir?: string): {
23
+ path: string;
24
+ changed: boolean;
25
+ };
26
+ /** Inspect the shared adapter without executing either it or its registered CLI. */
27
+ export declare function inspectHookAdapter(homeDir?: string): HookAdapterInspection;
@@ -0,0 +1,188 @@
1
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, statSync, } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { atomicWriteFileSync } from './atomic-file.js';
5
+ import { withTargetFileLock } from './file-lock.js';
6
+ const ADAPTER_MARKER = '# notifai managed hook adapter';
7
+ const ADAPTER_VERSION = 1;
8
+ /**
9
+ * One user-level identity for every harness and every project/global install.
10
+ *
11
+ * The pathname is deliberately outside XDG configuration/state routing. XDG
12
+ * overrides routinely differ between shells, package managers, and harness
13
+ * processes; allowing one to select this path would put mutable environment
14
+ * back into every trusted definition. Only the user's home boundary selects
15
+ * it; CLI, Node, package-manager, checkout, and preference paths stay behind
16
+ * it and may change without changing hook identity.
17
+ */
18
+ export function hookAdapterPath(homeDir = os.userInfo().homedir) {
19
+ return path.join(homeDir, '.notifai', 'bin', 'hook-adapter');
20
+ }
21
+ /** Install or repair the stable adapter and atomically retarget its implementation. */
22
+ export function installHookAdapter(target, homeDir) {
23
+ assertUsableTarget(target);
24
+ const file = hookAdapterPath(homeDir);
25
+ ensureManagedDirectories(homeDir ?? os.userInfo().homedir);
26
+ const source = hookAdapterSource(target);
27
+ return withTargetFileLock(file, () => {
28
+ const existing = existsSync(file) ? readSafeManagedFile(file) : null;
29
+ const changed = existing === null || existing.contents !== source || (existing.mode & 0o777) !== 0o700;
30
+ if (changed) {
31
+ atomicWriteFileSync(file, source, {
32
+ mode: 0o700,
33
+ preserveMode: false,
34
+ requireCurrentUserOwner: true,
35
+ });
36
+ }
37
+ return { path: file, changed };
38
+ });
39
+ }
40
+ function assertUsableTarget(target) {
41
+ let exec;
42
+ try {
43
+ exec = statSync(target.execPath);
44
+ }
45
+ catch {
46
+ throw new Error(`Hook adapter runtime ${target.execPath} does not exist.`);
47
+ }
48
+ if (!exec.isFile() || (exec.mode & 0o111) === 0) {
49
+ throw new Error(`Hook adapter runtime ${target.execPath} is not executable.`);
50
+ }
51
+ try {
52
+ if (!statSync(target.scriptPath).isFile()) {
53
+ throw new Error(`Hook adapter CLI ${target.scriptPath} is not a regular file.`);
54
+ }
55
+ }
56
+ catch (err) {
57
+ if (err instanceof Error && err.message.startsWith('Hook adapter CLI '))
58
+ throw err;
59
+ throw new Error(`Hook adapter CLI ${target.scriptPath} does not exist.`);
60
+ }
61
+ }
62
+ /** Inspect the shared adapter without executing either it or its registered CLI. */
63
+ export function inspectHookAdapter(homeDir) {
64
+ const file = hookAdapterPath(homeDir);
65
+ if (!existsSync(file)) {
66
+ return { path: file, target: null, problems: [`${file} is missing; rerun \`notifai hooks install\``] };
67
+ }
68
+ let stat;
69
+ try {
70
+ stat = lstatSync(file);
71
+ }
72
+ catch (err) {
73
+ return { path: file, target: null, problems: [String(err)] };
74
+ }
75
+ if (stat.isSymbolicLink() || !stat.isFile()) {
76
+ return {
77
+ path: file,
78
+ target: null,
79
+ problems: [`${file} is not a regular file; refusing the adapter`],
80
+ };
81
+ }
82
+ const problems = [];
83
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
84
+ if (uid !== undefined && stat.uid !== uid) {
85
+ problems.push(`${file} is owned by uid ${stat.uid}, not the current user`);
86
+ }
87
+ if ((stat.mode & 0o077) !== 0 || (stat.mode & 0o700) !== 0o700) {
88
+ problems.push(`${file} permissions are ${(stat.mode & 0o777).toString(8)}, expected 700`);
89
+ }
90
+ const contents = readFileSync(file, 'utf8');
91
+ if (!contents.startsWith(`#!/bin/sh\n${ADAPTER_MARKER}\n`)) {
92
+ problems.push(`${file} has no managed adapter marker`);
93
+ }
94
+ const version = Number(/^# adapter-version: (\d+)$/m.exec(contents)?.[1] ?? Number.NaN);
95
+ if (version !== ADAPTER_VERSION)
96
+ problems.push(`${file} uses obsolete adapter version ${version}`);
97
+ const target = parseTarget(contents);
98
+ if (target === null) {
99
+ problems.push(`${file} has malformed target metadata`);
100
+ }
101
+ else {
102
+ try {
103
+ const exec = statSync(target.execPath);
104
+ if (!exec.isFile() || (exec.mode & 0o111) === 0) {
105
+ problems.push(`registered runtime ${target.execPath} is not executable`);
106
+ }
107
+ }
108
+ catch {
109
+ problems.push(`registered runtime ${target.execPath} is missing`);
110
+ }
111
+ try {
112
+ if (!statSync(target.scriptPath).isFile()) {
113
+ problems.push(`registered CLI ${target.scriptPath} is not a regular file`);
114
+ }
115
+ }
116
+ catch {
117
+ problems.push(`registered CLI ${target.scriptPath} is missing`);
118
+ }
119
+ }
120
+ return { path: file, target, problems };
121
+ }
122
+ function hookAdapterSource(target) {
123
+ return `#!/bin/sh
124
+ ${ADAPTER_MARKER}
125
+ # adapter-version: ${ADAPTER_VERSION}
126
+ # target-exec-json: ${JSON.stringify(target.execPath)}
127
+ # target-script-json: ${JSON.stringify(target.scriptPath)}
128
+ set -eu
129
+
130
+ registered_exec=${quote(target.execPath)}
131
+ registered_script=${quote(target.scriptPath)}
132
+
133
+ if [ -x "$registered_exec" ] && [ -f "$registered_script" ]; then
134
+ exec "$registered_exec" "$registered_script" "$@"
135
+ fi
136
+
137
+ printf '%s\n' 'Notifai hook adapter target is stale; run notifai hooks install.' >&2
138
+ exit 127
139
+ `;
140
+ }
141
+ function parseTarget(contents) {
142
+ const exec = /^# target-exec-json: (.+)$/m.exec(contents)?.[1];
143
+ const script = /^# target-script-json: (.+)$/m.exec(contents)?.[1];
144
+ if (exec === undefined || script === undefined)
145
+ return null;
146
+ try {
147
+ const execPath = JSON.parse(exec);
148
+ const scriptPath = JSON.parse(script);
149
+ return typeof execPath === 'string' && typeof scriptPath === 'string'
150
+ ? { execPath, scriptPath }
151
+ : null;
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function quote(value) {
158
+ return `'${value.replaceAll("'", `'\\''`)}'`;
159
+ }
160
+ function ensureManagedDirectories(homeDir) {
161
+ if (!existsSync(homeDir))
162
+ mkdirSync(homeDir, { recursive: true, mode: 0o700 });
163
+ for (const dir of [path.join(homeDir, '.notifai'), path.join(homeDir, '.notifai', 'bin')]) {
164
+ if (!existsSync(dir))
165
+ mkdirSync(dir, { mode: 0o700 });
166
+ const stat = lstatSync(dir);
167
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
168
+ throw new Error(`${dir} is not a regular directory; refusing to install the hook adapter.`);
169
+ }
170
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
171
+ if (uid !== undefined && stat.uid !== uid) {
172
+ throw new Error(`${dir} is owned by uid ${stat.uid}, not the current user.`);
173
+ }
174
+ chmodSync(dir, 0o700);
175
+ }
176
+ }
177
+ function readSafeManagedFile(file) {
178
+ const stat = lstatSync(file);
179
+ if (stat.isSymbolicLink() || !stat.isFile()) {
180
+ throw new Error(`${file} is not a regular file; refusing to replace it.`);
181
+ }
182
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
183
+ if (uid !== undefined && stat.uid !== uid) {
184
+ throw new Error(`${file} is owned by uid ${stat.uid}, not the current user.`);
185
+ }
186
+ return { contents: readFileSync(file, 'utf8'), mode: stat.mode };
187
+ }
188
+ //# sourceMappingURL=hook-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-adapter.js","sourceRoot":"","sources":["../src/hook-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { Readable } from 'node:stream';
2
+ /**
3
+ * Read one harness-hook envelope without letting a stuck pipe hold the prompt.
4
+ *
5
+ * A timeout or stream error before any bytes arrive rejects, so the hook can
6
+ * record that its input was unavailable. Once bytes exist they are more useful
7
+ * than the transport failure: complete JSON can still run, while partial or
8
+ * capped JSON reaches the hook's malformed/truncated diagnostic.
9
+ */
10
+ export declare function readStdinWithTimeout(input?: Readable, timeoutMs?: number, maxBytes?: number): Promise<string>;