@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.
- package/LICENSE +202 -0
- package/README.md +76 -0
- package/dist/client.d.ts +40 -0
- package/dist/client.js +118 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.d.ts +225 -0
- package/dist/commands.js +1855 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +138 -0
- package/dist/config.js +224 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +31 -0
- package/dist/credentials.js +94 -0
- package/dist/credentials.js.map +1 -0
- package/dist/hooks.d.ts +250 -0
- package/dist/hooks.js +792 -0
- package/dist/hooks.js.map +1 -0
- package/dist/idle.d.ts +16 -0
- package/dist/idle.js +44 -0
- package/dist/idle.js.map +1 -0
- package/dist/install-hooks.d.ts +194 -0
- package/dist/install-hooks.js +579 -0
- package/dist/install-hooks.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +259 -0
- package/dist/main.js.map +1 -0
- package/dist/opencode-plugin.d.ts +65 -0
- package/dist/opencode-plugin.js +181 -0
- package/dist/opencode-plugin.js.map +1 -0
- package/dist/readiness.d.ts +87 -0
- package/dist/readiness.js +40 -0
- package/dist/readiness.js.map +1 -0
- package/dist/send.d.ts +86 -0
- package/dist/send.js +202 -0
- package/dist/send.js.map +1 -0
- package/package.json +56 -0
- package/src/client.test.ts +83 -0
- package/src/client.ts +183 -0
- package/src/commands.test.ts +1199 -0
- package/src/commands.ts +2201 -0
- package/src/config.test.ts +243 -0
- package/src/config.ts +347 -0
- package/src/credentials.ts +119 -0
- package/src/hooks.test.ts +1030 -0
- package/src/hooks.ts +1099 -0
- package/src/idle.test.ts +32 -0
- package/src/idle.ts +41 -0
- package/src/install-hooks.test.ts +500 -0
- package/src/install-hooks.ts +708 -0
- package/src/main.ts +315 -0
- package/src/opencode-plugin.ts +203 -0
- package/src/readiness.ts +88 -0
- package/src/send.ts +272 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { globalConfigDir } from './config.js';
|
|
5
|
+
const SERVICE = 'io.notifai.cli';
|
|
6
|
+
/** macOS Keychain via the `security` CLI — no native addons (proposal concern). */
|
|
7
|
+
export class KeychainStore {
|
|
8
|
+
load() {
|
|
9
|
+
try {
|
|
10
|
+
const raw = execFileSync('security', ['find-generic-password', '-s', SERVICE, '-a', 'machine', '-w'], { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
11
|
+
.toString()
|
|
12
|
+
.trim();
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
save(credential) {
|
|
20
|
+
execFileSync('security', [
|
|
21
|
+
'add-generic-password',
|
|
22
|
+
'-U',
|
|
23
|
+
'-s',
|
|
24
|
+
SERVICE,
|
|
25
|
+
'-a',
|
|
26
|
+
'machine',
|
|
27
|
+
'-w',
|
|
28
|
+
JSON.stringify(credential),
|
|
29
|
+
], { stdio: 'ignore' });
|
|
30
|
+
}
|
|
31
|
+
clear() {
|
|
32
|
+
try {
|
|
33
|
+
execFileSync('security', ['delete-generic-password', '-s', SERVICE, '-a', 'machine'], {
|
|
34
|
+
stdio: 'ignore',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// nothing stored
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
describe() {
|
|
42
|
+
return `macOS Keychain (${SERVICE})`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** 0600 file fallback for hosts without a keychain. */
|
|
46
|
+
export class FileStore {
|
|
47
|
+
env;
|
|
48
|
+
constructor(env = process.env) {
|
|
49
|
+
this.env = env;
|
|
50
|
+
}
|
|
51
|
+
filePath() {
|
|
52
|
+
return path.join(globalConfigDir(this.env), 'credentials.json');
|
|
53
|
+
}
|
|
54
|
+
load() {
|
|
55
|
+
const file = this.filePath();
|
|
56
|
+
if (!existsSync(file))
|
|
57
|
+
return null;
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
save(credential) {
|
|
66
|
+
const file = this.filePath();
|
|
67
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
68
|
+
writeFileSync(file, `${JSON.stringify(credential, null, 2)}\n`, { mode: 0o600 });
|
|
69
|
+
chmodSync(file, 0o600);
|
|
70
|
+
}
|
|
71
|
+
clear() {
|
|
72
|
+
rmSync(this.filePath(), { force: true });
|
|
73
|
+
}
|
|
74
|
+
describe() {
|
|
75
|
+
return this.filePath();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function keychainAvailable() {
|
|
79
|
+
if (process.platform !== 'darwin')
|
|
80
|
+
return false;
|
|
81
|
+
try {
|
|
82
|
+
execFileSync('security', ['help'], { stdio: 'ignore' });
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function defaultCredentialStore(env = process.env) {
|
|
90
|
+
if (env['NOTIFAI_CREDENTIALS'] === 'file')
|
|
91
|
+
return new FileStore(env);
|
|
92
|
+
return keychainAvailable() ? new KeychainStore() : new FileStore(env);
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { LifecycleEndState, ReplyView } from '@raidiant/notifai-protocol';
|
|
2
|
+
import type { ApiClient } from './client.js';
|
|
3
|
+
import { type CliConfig } from './config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Harness hook handlers.
|
|
6
|
+
*
|
|
7
|
+
* Claude Code, Codex and OpenCode all expose the same three joints: a blocking
|
|
8
|
+
* pre-approval hook that returns a decision, a turn-end hook, and a hook that
|
|
9
|
+
* fires when the user submits a prompt. That is enough to answer a blocked
|
|
10
|
+
* agent from the phone without the agent cooperating at all — no question
|
|
11
|
+
* detection, no pending state in a context window that compaction will eat.
|
|
12
|
+
*
|
|
13
|
+
* The load-bearing constraint is that these hooks are synchronous: while one
|
|
14
|
+
* blocks, the harness cannot show its own prompt either. So a hook may only
|
|
15
|
+
* take over when the user is demonstrably absent. Present user, or no evidence
|
|
16
|
+
* either way, means exit immediately and let the terminal do its job.
|
|
17
|
+
*/
|
|
18
|
+
/** Fields we read from harness hook JSON. Everything else is passed through. */
|
|
19
|
+
export interface HookEnvelope {
|
|
20
|
+
session_id?: string;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
hook_event_name?: string;
|
|
23
|
+
/** Set by the harness when this Stop follows a previous Stop continuation. */
|
|
24
|
+
stop_hook_active?: boolean;
|
|
25
|
+
/** Cursor's stable per-conversation identifier. */
|
|
26
|
+
conversation_id?: string;
|
|
27
|
+
/** Cursor's project roots; the first is the hook's configuration root. */
|
|
28
|
+
workspace_roots?: string[];
|
|
29
|
+
/** Cursor increments this after each stop-hook automatic follow-up. */
|
|
30
|
+
loop_count?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface SessionState {
|
|
33
|
+
/** Epoch ms of the user's last prompt in this session — our presence signal. */
|
|
34
|
+
last_prompt_at?: number;
|
|
35
|
+
/** Question registered by `notifai ask`, awaiting the turn to end. */
|
|
36
|
+
pending?: PendingQuestion;
|
|
37
|
+
/**
|
|
38
|
+
* Questions that have been delivered to the user's devices and are now dead,
|
|
39
|
+
* but whose retirement has not been confirmed yet.
|
|
40
|
+
*
|
|
41
|
+
* A retirement needs a network call and the moment we learn a question is
|
|
42
|
+
* dead is not always a moment we can make one — `notifai ask` supersedes the
|
|
43
|
+
* previous question from a bare shell command, and the machine may be
|
|
44
|
+
* offline. Dropping the ids there is how a delivered question becomes
|
|
45
|
+
* permanently unretirable, so they are parked here instead and every later
|
|
46
|
+
* hook with a client drains them. Retirement is idempotent, so a duplicate
|
|
47
|
+
* attempt costs nothing and a missed one costs a stale notification for ever.
|
|
48
|
+
*/
|
|
49
|
+
retiring?: RetiringQuestion[];
|
|
50
|
+
}
|
|
51
|
+
/** A delivered question awaiting its retirement push. */
|
|
52
|
+
export interface RetiringQuestion {
|
|
53
|
+
request_id: string;
|
|
54
|
+
collapse_key: string;
|
|
55
|
+
/** Shown if the companion has no history entry to correlate against. */
|
|
56
|
+
question: string;
|
|
57
|
+
state: LifecycleEndState;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A retirement that outlived its session.
|
|
61
|
+
*
|
|
62
|
+
* Per-session parking assumes some later hook in the SAME session will hold a
|
|
63
|
+
* client, and `SessionEnd` is exactly where that assumption breaks: it may not
|
|
64
|
+
* touch the network, and no hook for that session ever fires again. Deleting
|
|
65
|
+
* the state there lost the only copy of the delivered question's ids, so the
|
|
66
|
+
* phone kept an answerable question nobody was listening to. These entries are
|
|
67
|
+
* moved to a machine-global queue instead, drained by whichever session's hook
|
|
68
|
+
* next holds a client.
|
|
69
|
+
*/
|
|
70
|
+
export interface OrphanRetirement extends RetiringQuestion {
|
|
71
|
+
/** Label of the session that asked, so the retirement sync matches its badge. */
|
|
72
|
+
session?: string;
|
|
73
|
+
/** Epoch ms when the entry was orphaned; entries beyond the TTL are dropped. */
|
|
74
|
+
enqueued_at: number;
|
|
75
|
+
}
|
|
76
|
+
export interface PendingQuestion {
|
|
77
|
+
question: string;
|
|
78
|
+
/**
|
|
79
|
+
* Epoch ms when `notifai ask` registered this. The grace window runs from
|
|
80
|
+
* here, not from the turn's end: a question the agent asked five minutes ago
|
|
81
|
+
* while it kept working has already served its wait in the terminal.
|
|
82
|
+
*/
|
|
83
|
+
asked_at?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Canonical answer labels, already split and validated by `notifai ask`.
|
|
86
|
+
* Stored as a list rather than a comma-joined string so a label containing a
|
|
87
|
+
* comma survives the round trip.
|
|
88
|
+
*/
|
|
89
|
+
choices?: string[];
|
|
90
|
+
/** Set once the question has actually been pushed, so it can be retired. */
|
|
91
|
+
request_id?: string;
|
|
92
|
+
collapse_key?: string;
|
|
93
|
+
}
|
|
94
|
+
export type GraceOutcome =
|
|
95
|
+
/** The window elapsed with the user still gone; escalate. */
|
|
96
|
+
'absent'
|
|
97
|
+
/** The user touched the machine; the terminal is theirs. */
|
|
98
|
+
| 'user-returned'
|
|
99
|
+
/** No idle source, so waiting would hold a terminal we cannot monitor. */
|
|
100
|
+
| 'no-signal';
|
|
101
|
+
/**
|
|
102
|
+
* The terminal-first wait: the question sits in the terminal for
|
|
103
|
+
* `ask_grace_seconds` from when it was sent, and only then reaches companion
|
|
104
|
+
* devices.
|
|
105
|
+
*
|
|
106
|
+
* Holding a blocking Stop hook open is normally hostile — while it blocks, the
|
|
107
|
+
* harness cannot show its prompt either, so a user wanting to answer locally is
|
|
108
|
+
* locked out. What makes it safe is that the wait is abandoned the moment the
|
|
109
|
+
* user touches the keyboard or mouse. The block therefore only ever persists
|
|
110
|
+
* while they are demonstrably not using the machine, which costs them nothing.
|
|
111
|
+
*
|
|
112
|
+
* That safety depends entirely on the idle signal, so with no idle source this
|
|
113
|
+
* refuses to wait at all rather than holding a terminal it cannot monitor.
|
|
114
|
+
*
|
|
115
|
+
* With `require_idle` off the whole calculus changes: the user has said they
|
|
116
|
+
* want the question to reach them whether or not they are at the keyboard, so
|
|
117
|
+
* there is nothing to watch for and this becomes what its name always claimed —
|
|
118
|
+
* a plain timer. It then works on machines with no idle source too.
|
|
119
|
+
*/
|
|
120
|
+
export declare function awaitGrace(ctx: HookContext, askedAt: number): Promise<GraceOutcome>;
|
|
121
|
+
export interface HookOutcome {
|
|
122
|
+
/** Written to stdout verbatim — the harness parses this as the decision. */
|
|
123
|
+
stdout?: string;
|
|
124
|
+
/** Diagnostics; harnesses surface hook stderr in the transcript. */
|
|
125
|
+
notes: string[];
|
|
126
|
+
}
|
|
127
|
+
export declare function readSessionState(sessionId: string, env: NodeJS.ProcessEnv): SessionState;
|
|
128
|
+
export declare function writeSessionState(sessionId: string, env: NodeJS.ProcessEnv, state: SessionState): void;
|
|
129
|
+
export declare function clearSessionState(sessionId: string, env: NodeJS.ProcessEnv): void;
|
|
130
|
+
export declare function pruneAbandonedSessions(env: NodeJS.ProcessEnv, now?: number, maxAgeMs?: number): number;
|
|
131
|
+
export declare function claimQuestionPush(sessionId: string, env: NodeJS.ProcessEnv, now?: number): boolean;
|
|
132
|
+
export declare function releaseQuestionPush(sessionId: string, env: NodeJS.ProcessEnv): void;
|
|
133
|
+
/** Records which session is working in a directory, for `notifai ask`. */
|
|
134
|
+
export declare function writeProjectSession(cwd: string, env: NodeJS.ProcessEnv, sessionId: string, now: number): void;
|
|
135
|
+
/**
|
|
136
|
+
* Resolves the session working in `cwd`. Stale pointers are ignored rather than
|
|
137
|
+
* trusted: an id left by a session that ended days ago would send the question
|
|
138
|
+
* into state nothing is watching.
|
|
139
|
+
*/
|
|
140
|
+
export declare function readProjectSession(cwd: string, env: NodeJS.ProcessEnv, now: number, maxAgeMs?: number): string | null;
|
|
141
|
+
/**
|
|
142
|
+
* Absent means the user is not at this machine's keyboard.
|
|
143
|
+
*
|
|
144
|
+
* OS idle time answers that directly, so where it exists it decides alone.
|
|
145
|
+
* Silence since the user's last prompt is only ever a proxy for it, and a poor
|
|
146
|
+
* one — it is wrong in both directions:
|
|
147
|
+
*
|
|
148
|
+
* - Too long: it counts the agent's own turn, so a user watching a build was
|
|
149
|
+
* read as absent and had the question pushed at them.
|
|
150
|
+
* - Too short: a session that has just been spawned always has a fresh
|
|
151
|
+
* prompt, so its FIRST question could never escalate however long its
|
|
152
|
+
* owner had been gone. That is the "kick off some agents and
|
|
153
|
+
* walk away" case this feature mainly exists for, and requiring both
|
|
154
|
+
* signals to agree is what broke it.
|
|
155
|
+
*
|
|
156
|
+
* The proxy therefore survives only as the fallback for machines with no idle
|
|
157
|
+
* source. There, never having seen a prompt is not evidence of absence — it is
|
|
158
|
+
* a missing `UserPromptSubmit` hook — so it resolves to "present".
|
|
159
|
+
*
|
|
160
|
+
* Answering from a companion device does not make the user present; only touching this
|
|
161
|
+
* machine does. Answering on a device is evidence of being away from it.
|
|
162
|
+
*
|
|
163
|
+
* All of which only matters if presence is being consulted at all. With
|
|
164
|
+
* `require_idle` off the user has said their whereabouts are not a
|
|
165
|
+
* precondition, so this stops guessing and answers yes.
|
|
166
|
+
*/
|
|
167
|
+
export declare function isUserAway(state: SessionState, config: CliConfig, now: number, idleSeconds: number | null): boolean;
|
|
168
|
+
export interface HookContext {
|
|
169
|
+
client: ApiClient;
|
|
170
|
+
config: CliConfig;
|
|
171
|
+
env: NodeJS.ProcessEnv;
|
|
172
|
+
now: () => number;
|
|
173
|
+
/** Seconds since the last keyboard/mouse event, or null if unknowable. */
|
|
174
|
+
idleSeconds: () => number | null;
|
|
175
|
+
/** Injected so tests advance a virtual clock instead of sleeping. */
|
|
176
|
+
sleep: (milliseconds: number) => Promise<void>;
|
|
177
|
+
/** Bounded wait for the first reply; injected so tests do not sleep. */
|
|
178
|
+
waitForFirstReply: (requestId: string, timeoutSeconds: number) => Promise<{
|
|
179
|
+
replies: ReplyView[];
|
|
180
|
+
timedOut: boolean;
|
|
181
|
+
degraded?: boolean;
|
|
182
|
+
}>;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Everything retirement needs. Narrower than a HookContext on purpose:
|
|
186
|
+
* `notifai ask` supersedes the previous question and it is a plain command with
|
|
187
|
+
* no hook payload, no idle probe and nothing to sleep for.
|
|
188
|
+
*/
|
|
189
|
+
export type RetireDeps = Pick<HookContext, 'client' | 'config'>;
|
|
190
|
+
/**
|
|
191
|
+
* Park a delivered question for retirement. Called at the moment we learn it is
|
|
192
|
+
* dead, which is frequently not a moment we can reach the network.
|
|
193
|
+
*
|
|
194
|
+
* Nothing is parked for a question that never reached a device: with no
|
|
195
|
+
* request_id there is nothing on any device to retire.
|
|
196
|
+
*/
|
|
197
|
+
export declare function parkForRetirement(sessionId: string, env: NodeJS.ProcessEnv, pending: PendingQuestion, state: LifecycleEndState): void;
|
|
198
|
+
/**
|
|
199
|
+
* Send every parked retirement, and forget the ones that landed. A failure
|
|
200
|
+
* leaves the entry in place for the next hook rather than losing it, which is
|
|
201
|
+
* the whole reason the queue exists.
|
|
202
|
+
*/
|
|
203
|
+
export declare function drainRetirements(ctx: RetireDeps, sessionId: string, env: NodeJS.ProcessEnv, session?: string | undefined): Promise<string[]>;
|
|
204
|
+
/** Move retirements into the global queue; deduped so a retry costs nothing. */
|
|
205
|
+
export declare function orphanRetirements(env: NodeJS.ProcessEnv, entries: RetiringQuestion[], session: string | undefined, now: number): void;
|
|
206
|
+
/**
|
|
207
|
+
* Retire everything a dead session left behind. Failures stay queued for the
|
|
208
|
+
* next holder of a client; entries past the TTL are dropped as already dead.
|
|
209
|
+
*/
|
|
210
|
+
export declare function drainOrphanRetirements(ctx: RetireDeps, env: NodeJS.ProcessEnv, now: number): Promise<string[]>;
|
|
211
|
+
/**
|
|
212
|
+
* Records presence and retires anything still asking on companion devices. This is the
|
|
213
|
+
* "answered in the terminal" case from the original design: we cannot tell
|
|
214
|
+
* whether the new prompt answers the question, but we do not need to — the
|
|
215
|
+
* user being here is what makes the notification noise.
|
|
216
|
+
*/
|
|
217
|
+
export declare function handleUserPromptSubmit(ctx: HookContext, envelope: HookEnvelope): Promise<HookOutcome>;
|
|
218
|
+
/**
|
|
219
|
+
* Only engages for a question the agent explicitly registered with
|
|
220
|
+
* `notifai ask`. Guessing from the last assistant message was the alternative
|
|
221
|
+
* and it is not worth it: a false positive here hijacks the terminal.
|
|
222
|
+
*/
|
|
223
|
+
export declare function handleStop(ctx: HookContext, envelope: HookEnvelope): Promise<HookOutcome>;
|
|
224
|
+
/**
|
|
225
|
+
* Claude Code gives SessionEnd hooks a 1.5-second shared budget and Codex 1
|
|
226
|
+
* second, so this cannot make a network call. It drops the local marker — but
|
|
227
|
+
* first moves anything still live on the user's devices into the global
|
|
228
|
+
* retirement queue, because this file was the only record of those ids and no
|
|
229
|
+
* hook for this session will ever run again. A question whose
|
|
230
|
+
* agent just exited can receive no answer, so it is orphaned as `expired`.
|
|
231
|
+
*/
|
|
232
|
+
export declare function handleSessionEnd(env: NodeJS.ProcessEnv, envelope: HookEnvelope, now?: number): HookOutcome;
|
|
233
|
+
/**
|
|
234
|
+
* One session holds one live question, so registering a second one ends the
|
|
235
|
+
* first.
|
|
236
|
+
*
|
|
237
|
+
* This used to replace `pending` wholesale, which silently discarded the
|
|
238
|
+
* `request_id` and `collapse_key` of a question already delivered to the user's
|
|
239
|
+
* devices. Nothing else knows those ids, so the notification became
|
|
240
|
+
* unretirable — it sat on the lock screen for ever asking a question no answer
|
|
241
|
+
* could reach. That is where the stale pile-up came from.
|
|
242
|
+
*
|
|
243
|
+
* Supersession is keyed on the session, not the project: several agents may be
|
|
244
|
+
* running in one project at once, and one agent's new question killing another
|
|
245
|
+
* agent's live one would be worse than the staleness this fixes.
|
|
246
|
+
*/
|
|
247
|
+
export declare function registerQuestion(sessionId: string, env: NodeJS.ProcessEnv, question: PendingQuestion, now?: number): void;
|
|
248
|
+
/** Parses hook JSON from stdin, tolerating an empty or malformed body. */
|
|
249
|
+
export declare function parseHookInput(raw: string): HookEnvelope;
|
|
250
|
+
export declare function resolveHookConfig(cwd: string, env: NodeJS.ProcessEnv, sessionId: string | undefined): CliConfig;
|