@raidiant/notifai 0.3.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 +19 -4
- package/dist/atomic-file.d.ts +4 -0
- package/dist/atomic-file.js +60 -10
- package/dist/atomic-file.js.map +1 -1
- package/dist/client.d.ts +25 -2
- package/dist/client.js +45 -1
- package/dist/client.js.map +1 -1
- package/dist/commands.d.ts +56 -7
- package/dist/commands.js +980 -318
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +46 -8
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +38 -3
- package/dist/config.js +61 -14
- package/dist/config.js.map +1 -1
- package/dist/file-lock.d.ts +26 -0
- package/dist/file-lock.js +268 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/harnesses.d.ts +28 -0
- package/dist/harnesses.js +40 -0
- package/dist/harnesses.js.map +1 -0
- package/dist/hook-adapter.d.ts +27 -0
- package/dist/hook-adapter.js +188 -0
- package/dist/hook-adapter.js.map +1 -0
- package/dist/hook-input.d.ts +10 -0
- package/dist/hook-input.js +64 -0
- package/dist/hook-input.js.map +1 -0
- package/dist/hooks.d.ts +66 -10
- package/dist/hooks.js +751 -276
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +40 -16
- package/dist/install-hooks.js +217 -73
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +3 -3
- package/dist/interactive.js.map +1 -1
- package/dist/logging.d.ts +142 -0
- package/dist/logging.js +533 -0
- package/dist/logging.js.map +1 -0
- package/dist/main.js +97 -36
- package/dist/main.js.map +1 -1
- package/dist/opencode-plugin.d.ts +7 -7
- package/dist/opencode-plugin.js +12 -13
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/ui/config-view.js +3 -0
- package/dist/ui/config-view.js.map +1 -1
- package/dist/ui/theme.d.ts +1 -1
- package/package.json +3 -2
- package/src/atomic-file.ts +86 -7
- package/src/claude-wake.test.ts +320 -0
- package/src/claude-wake.ts +489 -0
- package/src/client.test.ts +12 -0
- package/src/client.ts +72 -4
- package/src/codex-wake.test.ts +253 -0
- package/src/codex-wake.ts +288 -0
- package/src/commands.test.ts +1261 -77
- package/src/commands.ts +1482 -375
- package/src/config-schema.test.ts +3 -3
- package/src/config-schema.ts +57 -44
- package/src/config.test.ts +22 -16
- package/src/config.ts +81 -66
- package/src/file-lock-process-worker.test.ts +64 -0
- package/src/file-lock.test.ts +279 -0
- package/src/file-lock.ts +318 -0
- package/src/harnesses.test.ts +34 -0
- package/src/harnesses.ts +57 -0
- package/src/hook-adapter.test.ts +187 -0
- package/src/hook-adapter.ts +212 -0
- package/src/hook-input.test.ts +39 -0
- package/src/hook-input.ts +72 -0
- package/src/hooks.test.ts +1985 -417
- package/src/hooks.ts +1338 -461
- package/src/install-hooks.test.ts +131 -41
- package/src/install-hooks.ts +284 -84
- package/src/interactive.ts +3 -8
- package/src/logging-process-worker.test.ts +39 -0
- package/src/logging.test.ts +453 -0
- package/src/logging.ts +639 -0
- package/src/main.ts +113 -32
- package/src/opencode-plugin.ts +15 -16
- package/src/session-end-log-lock-worker.test.ts +35 -0
- package/src/skill.test.ts +34 -0
- package/src/ui/config-view.test.ts +9 -7
- package/src/ui/config-view.ts +3 -0
- package/src/idle.test.ts +0 -32
- package/src/idle.ts +0 -41
|
@@ -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>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const TRUNCATED_INPUT_SUFFIX = '\n[notifai: hook input truncated]\n';
|
|
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 function readStdinWithTimeout(input = process.stdin, timeoutMs = 2_000, maxBytes = 1_000_000) {
|
|
11
|
+
if (input.isTTY === true)
|
|
12
|
+
return Promise.resolve('');
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
let total = 0;
|
|
16
|
+
let settled = false;
|
|
17
|
+
const cleanup = () => {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
input.off('data', onData);
|
|
20
|
+
input.off('end', onEnd);
|
|
21
|
+
input.off('error', onError);
|
|
22
|
+
input.destroy();
|
|
23
|
+
};
|
|
24
|
+
const finishBuffered = () => {
|
|
25
|
+
if (settled)
|
|
26
|
+
return;
|
|
27
|
+
settled = true;
|
|
28
|
+
cleanup();
|
|
29
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
30
|
+
};
|
|
31
|
+
const failEmpty = (err) => {
|
|
32
|
+
if (settled)
|
|
33
|
+
return;
|
|
34
|
+
if (chunks.length > 0)
|
|
35
|
+
return finishBuffered();
|
|
36
|
+
settled = true;
|
|
37
|
+
cleanup();
|
|
38
|
+
reject(err);
|
|
39
|
+
};
|
|
40
|
+
const onData = (chunk) => {
|
|
41
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
42
|
+
const remaining = Math.max(0, maxBytes - total);
|
|
43
|
+
if (bytes.length <= remaining) {
|
|
44
|
+
chunks.push(bytes);
|
|
45
|
+
total += bytes.length;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (remaining > 0) {
|
|
49
|
+
chunks.push(bytes.subarray(0, remaining));
|
|
50
|
+
total += remaining;
|
|
51
|
+
}
|
|
52
|
+
chunks.push(Buffer.from(TRUNCATED_INPUT_SUFFIX));
|
|
53
|
+
finishBuffered();
|
|
54
|
+
};
|
|
55
|
+
const onEnd = () => finishBuffered();
|
|
56
|
+
const onError = (err) => failEmpty(err);
|
|
57
|
+
const timer = setTimeout(() => failEmpty(new Error(`timed out waiting ${timeoutMs}ms for hook input`)), timeoutMs);
|
|
58
|
+
timer.unref?.();
|
|
59
|
+
input.on('data', onData);
|
|
60
|
+
input.once('end', onEnd);
|
|
61
|
+
input.once('error', onError);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=hook-input.js.map
|