@gtc6244/immediacy-daemon 0.2.0 → 0.2.2
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/dist/src/agent/claude.d.ts +14 -0
- package/dist/src/agent/claude.js +171 -118
- package/dist/src/agent/claude.js.map +1 -1
- package/dist/src/agent/model-error.d.ts +8 -0
- package/dist/src/agent/model-error.js +13 -0
- package/dist/src/agent/model-error.js.map +1 -1
- package/dist/src/hub.d.ts +8 -0
- package/dist/src/hub.js +14 -0
- package/dist/src/hub.js.map +1 -1
- package/dist/src/protocol.d.ts +21 -3
- package/dist/src/protocol.js +4 -1
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/push.d.ts +104 -0
- package/dist/src/push.js +339 -0
- package/dist/src/push.js.map +1 -0
- package/dist/src/server.js +32 -4
- package/dist/src/server.js.map +1 -1
- package/dist/src/session-map.d.ts +33 -0
- package/dist/src/session-map.js +48 -0
- package/dist/src/session-map.js.map +1 -0
- package/dist/src/types.d.ts +16 -1
- package/dist/src/workspace.d.ts +16 -1
- package/dist/src/workspace.js +32 -4
- package/dist/src/workspace.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FCM push — the daemon is the sender (docs/fcm-setup.md).
|
|
3
|
+
*
|
|
4
|
+
* The phone does NOT fight to hold the WebSocket open in the background: it
|
|
5
|
+
* drops the socket and reconnects on resume. So when an agent needs attention
|
|
6
|
+
* (a deferred permission request, a finished turn, a usage limit) and NO client
|
|
7
|
+
* socket is live, the daemon wakes the app via FCM. On resume the app reconnects,
|
|
8
|
+
* the socket replays state, and the actionable card is already there.
|
|
9
|
+
*
|
|
10
|
+
* Three pieces:
|
|
11
|
+
* - {@link PushTokenStore}: the set of device FCM tokens the app registered over
|
|
12
|
+
* the socket (`push.register`), persisted like a credential (0600).
|
|
13
|
+
* - {@link FcmSender}: mints a short-lived OAuth2 access token from a Firebase
|
|
14
|
+
* service account (env `FCM_SERVICE_ACCOUNT` → a JSON key file, or
|
|
15
|
+
* `FCM_SERVICE_ACCOUNT_JSON` → the JSON inline) and POSTs to the FCM v1 API.
|
|
16
|
+
* No `firebase-admin` dependency — a signed JWT + `fetch`, matching the
|
|
17
|
+
* daemon's lightweight-deps rule. Disabled (a silent no-op) when no service
|
|
18
|
+
* account is configured, so a daemon without FCM keeps running unchanged.
|
|
19
|
+
* - {@link PushNotifier}: the seam the WorkspaceManager calls. Gated on "no live
|
|
20
|
+
* client" so a foregrounded app (which already gets the live event + a local
|
|
21
|
+
* notification) is never double-notified; prunes tokens FCM reports as stale.
|
|
22
|
+
*/
|
|
23
|
+
import type { PushPlatform } from "./protocol.js";
|
|
24
|
+
/** A device's FCM registration, keyed by the (unique) token. */
|
|
25
|
+
export interface PushToken {
|
|
26
|
+
token: string;
|
|
27
|
+
platform: PushPlatform;
|
|
28
|
+
/** ms epoch of the last register — lets a future prune drop ancient tokens. */
|
|
29
|
+
updatedAt: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Durable set of device FCM tokens. Single-user, but several devices may be
|
|
33
|
+
* paired (phone + tablet), so this is a small keyed collection rather than a
|
|
34
|
+
* single value. Written 0600 (a token lets anyone holding the FCM key wake that
|
|
35
|
+
* device), mirroring credentials.ts.
|
|
36
|
+
*/
|
|
37
|
+
export declare class PushTokenStore {
|
|
38
|
+
private tokens;
|
|
39
|
+
private writeChain;
|
|
40
|
+
load(): Promise<void>;
|
|
41
|
+
all(): PushToken[];
|
|
42
|
+
/** Add/refresh a device token (idempotent; a re-register just bumps the time). */
|
|
43
|
+
register(token: string, platform: PushPlatform): Promise<boolean>;
|
|
44
|
+
/** Drop a token (client signed off, or FCM reported it stale). */
|
|
45
|
+
unregister(token: string): Promise<void>;
|
|
46
|
+
private persist;
|
|
47
|
+
}
|
|
48
|
+
/** What a send attempt resolved to — `stale` tells the caller to prune the token. */
|
|
49
|
+
export type SendOutcome = "ok" | "stale" | "error" | "disabled";
|
|
50
|
+
/** A ready-to-send push (platform-neutral; the sender maps it to the FCM shape). */
|
|
51
|
+
export interface PushMessage {
|
|
52
|
+
title: string;
|
|
53
|
+
body: string;
|
|
54
|
+
/** All values are strings on the wire — the sender coerces before sending. */
|
|
55
|
+
data: Record<string, string>;
|
|
56
|
+
/** Android notification channel id (matches app/lib/notifications.dart). */
|
|
57
|
+
androidChannel: "permissions" | "activity";
|
|
58
|
+
/** Wake-now (permission/limit) vs. normal (done). Drives priority + APNs. */
|
|
59
|
+
urgent: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Sends messages to FCM's HTTP v1 API using a service account. Lazily mints and
|
|
63
|
+
* caches an OAuth2 access token (valid ~1h); refreshes a minute before expiry.
|
|
64
|
+
*/
|
|
65
|
+
export declare class FcmSender {
|
|
66
|
+
private sa?;
|
|
67
|
+
private access?;
|
|
68
|
+
/** Serializes token minting so a burst of sends does one token exchange. */
|
|
69
|
+
private minting?;
|
|
70
|
+
/** True once a service account has loaded — otherwise every send is a no-op. */
|
|
71
|
+
get enabled(): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Load the service account from the environment. `FCM_SERVICE_ACCOUNT` points
|
|
74
|
+
* at a JSON key file (kept OUTSIDE the repo); `FCM_SERVICE_ACCOUNT_JSON` holds
|
|
75
|
+
* the JSON inline (for container secrets). Absent → disabled (no throw).
|
|
76
|
+
*/
|
|
77
|
+
load(): Promise<void>;
|
|
78
|
+
private accessToken;
|
|
79
|
+
private mint;
|
|
80
|
+
/** Send one message to one device token. Never throws — returns an outcome. */
|
|
81
|
+
send(token: string, msg: PushMessage): Promise<SendOutcome>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The call seam the WorkspaceManager uses. Fans a wake-up out to every device
|
|
85
|
+
* token, but only when NO client socket is live — a foregrounded app already
|
|
86
|
+
* receives the event over the wire and raises its own local notification, so
|
|
87
|
+
* pushing then would double-notify. Tokens FCM reports as stale are pruned.
|
|
88
|
+
*/
|
|
89
|
+
export declare class PushNotifier {
|
|
90
|
+
private tokens;
|
|
91
|
+
private sender;
|
|
92
|
+
/** True while at least one client socket is attached (see Hub). */
|
|
93
|
+
private hasLiveClients;
|
|
94
|
+
constructor(tokens: PushTokenStore, sender: FcmSender,
|
|
95
|
+
/** True while at least one client socket is attached (see Hub). */
|
|
96
|
+
hasLiveClients: () => boolean);
|
|
97
|
+
notifyPermission(ws: string, name: string, tool: string, summary: string, seq?: number): void;
|
|
98
|
+
notifyDone(ws: string, name: string, summary: string | undefined, seq?: number): void;
|
|
99
|
+
notifyLimit(ws: string, name: string, message: string): void;
|
|
100
|
+
/** Build the FCM data map: everything stringified, undefined/empty dropped. */
|
|
101
|
+
private data;
|
|
102
|
+
private dispatch;
|
|
103
|
+
private fanOut;
|
|
104
|
+
}
|
package/dist/src/push.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FCM push — the daemon is the sender (docs/fcm-setup.md).
|
|
3
|
+
*
|
|
4
|
+
* The phone does NOT fight to hold the WebSocket open in the background: it
|
|
5
|
+
* drops the socket and reconnects on resume. So when an agent needs attention
|
|
6
|
+
* (a deferred permission request, a finished turn, a usage limit) and NO client
|
|
7
|
+
* socket is live, the daemon wakes the app via FCM. On resume the app reconnects,
|
|
8
|
+
* the socket replays state, and the actionable card is already there.
|
|
9
|
+
*
|
|
10
|
+
* Three pieces:
|
|
11
|
+
* - {@link PushTokenStore}: the set of device FCM tokens the app registered over
|
|
12
|
+
* the socket (`push.register`), persisted like a credential (0600).
|
|
13
|
+
* - {@link FcmSender}: mints a short-lived OAuth2 access token from a Firebase
|
|
14
|
+
* service account (env `FCM_SERVICE_ACCOUNT` → a JSON key file, or
|
|
15
|
+
* `FCM_SERVICE_ACCOUNT_JSON` → the JSON inline) and POSTs to the FCM v1 API.
|
|
16
|
+
* No `firebase-admin` dependency — a signed JWT + `fetch`, matching the
|
|
17
|
+
* daemon's lightweight-deps rule. Disabled (a silent no-op) when no service
|
|
18
|
+
* account is configured, so a daemon without FCM keeps running unchanged.
|
|
19
|
+
* - {@link PushNotifier}: the seam the WorkspaceManager calls. Gated on "no live
|
|
20
|
+
* client" so a foregrounded app (which already gets the live event + a local
|
|
21
|
+
* notification) is never double-notified; prunes tokens FCM reports as stale.
|
|
22
|
+
*/
|
|
23
|
+
import { createSign } from "node:crypto";
|
|
24
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import { logger } from "./log.js";
|
|
28
|
+
const TOKENS_FILE = process.env.PUSH_TOKENS_FILE ?? join(homedir(), ".immediacy", "push-tokens.json");
|
|
29
|
+
/** Cap the stored set so a buggy/hostile client can't grow the file unbounded. */
|
|
30
|
+
const MAX_TOKENS = 32;
|
|
31
|
+
/** FCM registration tokens are ~160 chars; refuse anything absurd. */
|
|
32
|
+
const MAX_TOKEN_LEN = 4096;
|
|
33
|
+
const PLATFORMS = ["android", "ios", "macos", "web"];
|
|
34
|
+
function isPlatform(v) {
|
|
35
|
+
return typeof v === "string" && PLATFORMS.includes(v);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Durable set of device FCM tokens. Single-user, but several devices may be
|
|
39
|
+
* paired (phone + tablet), so this is a small keyed collection rather than a
|
|
40
|
+
* single value. Written 0600 (a token lets anyone holding the FCM key wake that
|
|
41
|
+
* device), mirroring credentials.ts.
|
|
42
|
+
*/
|
|
43
|
+
export class PushTokenStore {
|
|
44
|
+
tokens = new Map();
|
|
45
|
+
writeChain = Promise.resolve();
|
|
46
|
+
async load() {
|
|
47
|
+
try {
|
|
48
|
+
const raw = await readFile(TOKENS_FILE, "utf8");
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (Array.isArray(parsed)) {
|
|
51
|
+
for (const e of parsed) {
|
|
52
|
+
if (e &&
|
|
53
|
+
typeof e.token === "string" &&
|
|
54
|
+
e.token &&
|
|
55
|
+
isPlatform(e.platform)) {
|
|
56
|
+
this.tokens.set(e.token, {
|
|
57
|
+
token: e.token,
|
|
58
|
+
platform: e.platform,
|
|
59
|
+
updatedAt: typeof e.updatedAt === "number" ? e.updatedAt : Date.now(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
logger.info("push", `restored ${this.tokens.size} device token(s)`);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* no prior state */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
all() {
|
|
71
|
+
return [...this.tokens.values()];
|
|
72
|
+
}
|
|
73
|
+
/** Add/refresh a device token (idempotent; a re-register just bumps the time). */
|
|
74
|
+
async register(token, platform) {
|
|
75
|
+
const t = token.trim();
|
|
76
|
+
if (!t || t.length > MAX_TOKEN_LEN || !isPlatform(platform))
|
|
77
|
+
return false;
|
|
78
|
+
this.tokens.set(t, { token: t, platform, updatedAt: Date.now() });
|
|
79
|
+
// Evict the oldest if we somehow exceeded the cap (rotated tokens on one
|
|
80
|
+
// device pile up until FCM prunes the stale ones — bound it defensively).
|
|
81
|
+
if (this.tokens.size > MAX_TOKENS) {
|
|
82
|
+
const oldest = [...this.tokens.values()].sort((a, b) => a.updatedAt - b.updatedAt)[0];
|
|
83
|
+
if (oldest)
|
|
84
|
+
this.tokens.delete(oldest.token);
|
|
85
|
+
}
|
|
86
|
+
await this.persist();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** Drop a token (client signed off, or FCM reported it stale). */
|
|
90
|
+
async unregister(token) {
|
|
91
|
+
if (this.tokens.delete(token.trim()))
|
|
92
|
+
await this.persist();
|
|
93
|
+
}
|
|
94
|
+
persist() {
|
|
95
|
+
const snapshot = JSON.stringify([...this.tokens.values()], null, 2);
|
|
96
|
+
this.writeChain = this.writeChain
|
|
97
|
+
.catch(() => { })
|
|
98
|
+
.then(async () => {
|
|
99
|
+
await mkdir(dirname(TOKENS_FILE), { recursive: true });
|
|
100
|
+
const tmp = `${TOKENS_FILE}.${process.pid}.tmp`;
|
|
101
|
+
await writeFile(tmp, snapshot, { mode: 0o600 });
|
|
102
|
+
await rename(tmp, TOKENS_FILE);
|
|
103
|
+
});
|
|
104
|
+
return this.writeChain;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
|
|
108
|
+
const DEFAULT_TOKEN_URI = "https://oauth2.googleapis.com/token";
|
|
109
|
+
/**
|
|
110
|
+
* Sends messages to FCM's HTTP v1 API using a service account. Lazily mints and
|
|
111
|
+
* caches an OAuth2 access token (valid ~1h); refreshes a minute before expiry.
|
|
112
|
+
*/
|
|
113
|
+
export class FcmSender {
|
|
114
|
+
sa;
|
|
115
|
+
access;
|
|
116
|
+
/** Serializes token minting so a burst of sends does one token exchange. */
|
|
117
|
+
minting;
|
|
118
|
+
/** True once a service account has loaded — otherwise every send is a no-op. */
|
|
119
|
+
get enabled() {
|
|
120
|
+
return Boolean(this.sa);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Load the service account from the environment. `FCM_SERVICE_ACCOUNT` points
|
|
124
|
+
* at a JSON key file (kept OUTSIDE the repo); `FCM_SERVICE_ACCOUNT_JSON` holds
|
|
125
|
+
* the JSON inline (for container secrets). Absent → disabled (no throw).
|
|
126
|
+
*/
|
|
127
|
+
async load() {
|
|
128
|
+
try {
|
|
129
|
+
let raw;
|
|
130
|
+
const path = process.env.FCM_SERVICE_ACCOUNT?.trim();
|
|
131
|
+
const inline = process.env.FCM_SERVICE_ACCOUNT_JSON?.trim();
|
|
132
|
+
if (path)
|
|
133
|
+
raw = await readFile(path, "utf8");
|
|
134
|
+
else if (inline)
|
|
135
|
+
raw = inline;
|
|
136
|
+
if (!raw) {
|
|
137
|
+
logger.info("push", "FCM disabled (no FCM_SERVICE_ACCOUNT configured)");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const parsed = JSON.parse(raw);
|
|
141
|
+
if (!parsed.client_email || !parsed.private_key || !parsed.project_id) {
|
|
142
|
+
logger.warn("push", "FCM service account missing client_email/private_key/project_id — disabled");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
this.sa = {
|
|
146
|
+
client_email: parsed.client_email,
|
|
147
|
+
private_key: parsed.private_key,
|
|
148
|
+
project_id: parsed.project_id,
|
|
149
|
+
token_uri: parsed.token_uri ?? DEFAULT_TOKEN_URI,
|
|
150
|
+
};
|
|
151
|
+
logger.info("push", `FCM enabled for project ${parsed.project_id}`);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
logger.warn("push", "failed to load FCM service account — disabled", {
|
|
155
|
+
data: { message: err instanceof Error ? err.message : String(err) },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async accessToken() {
|
|
160
|
+
if (!this.sa)
|
|
161
|
+
return undefined;
|
|
162
|
+
const now = Math.floor(Date.now() / 1000);
|
|
163
|
+
if (this.access && this.access.expiresAt - 60 > now)
|
|
164
|
+
return this.access.token;
|
|
165
|
+
// Coalesce concurrent refreshes onto one in-flight exchange.
|
|
166
|
+
if (!this.minting) {
|
|
167
|
+
this.minting = this.mint().finally(() => {
|
|
168
|
+
this.minting = undefined;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
await this.minting;
|
|
172
|
+
return this.access?.token;
|
|
173
|
+
}
|
|
174
|
+
async mint() {
|
|
175
|
+
const sa = this.sa;
|
|
176
|
+
if (!sa)
|
|
177
|
+
return;
|
|
178
|
+
const now = Math.floor(Date.now() / 1000);
|
|
179
|
+
const tokenUri = sa.token_uri ?? DEFAULT_TOKEN_URI;
|
|
180
|
+
const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
|
|
181
|
+
const claims = Buffer.from(JSON.stringify({ iss: sa.client_email, scope: SCOPE, aud: tokenUri, iat: now, exp: now + 3600 })).toString("base64url");
|
|
182
|
+
const signingInput = `${header}.${claims}`;
|
|
183
|
+
const signer = createSign("RSA-SHA256");
|
|
184
|
+
signer.update(signingInput);
|
|
185
|
+
signer.end();
|
|
186
|
+
const signature = signer.sign(sa.private_key).toString("base64url");
|
|
187
|
+
const assertion = `${signingInput}.${signature}`;
|
|
188
|
+
const res = await fetch(tokenUri, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
191
|
+
body: new URLSearchParams({
|
|
192
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
193
|
+
assertion,
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
throw new Error(`token endpoint ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
198
|
+
}
|
|
199
|
+
const json = (await res.json());
|
|
200
|
+
this.access = { token: json.access_token, expiresAt: now + json.expires_in };
|
|
201
|
+
}
|
|
202
|
+
/** Send one message to one device token. Never throws — returns an outcome. */
|
|
203
|
+
async send(token, msg) {
|
|
204
|
+
const sa = this.sa;
|
|
205
|
+
if (!sa)
|
|
206
|
+
return "disabled";
|
|
207
|
+
let access;
|
|
208
|
+
try {
|
|
209
|
+
access = await this.accessToken();
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
logger.warn("push", "FCM token mint failed", {
|
|
213
|
+
data: { message: err instanceof Error ? err.message : String(err) },
|
|
214
|
+
});
|
|
215
|
+
return "error";
|
|
216
|
+
}
|
|
217
|
+
if (!access)
|
|
218
|
+
return "error";
|
|
219
|
+
const body = {
|
|
220
|
+
message: {
|
|
221
|
+
token,
|
|
222
|
+
notification: { title: msg.title, body: msg.body },
|
|
223
|
+
data: msg.data,
|
|
224
|
+
android: {
|
|
225
|
+
priority: msg.urgent ? "high" : "normal",
|
|
226
|
+
notification: { channel_id: msg.androidChannel },
|
|
227
|
+
},
|
|
228
|
+
apns: {
|
|
229
|
+
headers: { "apns-priority": msg.urgent ? "10" : "5" },
|
|
230
|
+
// content-available wakes a backgrounded app so it can reconnect;
|
|
231
|
+
// an alert + sound covers the terminated case.
|
|
232
|
+
payload: { aps: { sound: "default", "content-available": 1 } },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch(`https://fcm.googleapis.com/v1/projects/${sa.project_id}/messages:send`, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers: {
|
|
240
|
+
authorization: `Bearer ${access}`,
|
|
241
|
+
"content-type": "application/json",
|
|
242
|
+
},
|
|
243
|
+
body: JSON.stringify(body),
|
|
244
|
+
});
|
|
245
|
+
if (res.ok)
|
|
246
|
+
return "ok";
|
|
247
|
+
const text = (await res.text()).slice(0, 300);
|
|
248
|
+
// 404 UNREGISTERED / 400 INVALID_ARGUMENT on the token ⇒ the app was
|
|
249
|
+
// uninstalled or the token rotated; drop it so we stop trying.
|
|
250
|
+
if (res.status === 404 || /UNREGISTERED|INVALID_ARGUMENT/.test(text)) {
|
|
251
|
+
logger.info("push", "pruning stale FCM token", { data: { status: res.status } });
|
|
252
|
+
return "stale";
|
|
253
|
+
}
|
|
254
|
+
// 401 usually means our cached access token went bad — clear it so the
|
|
255
|
+
// next send re-mints.
|
|
256
|
+
if (res.status === 401)
|
|
257
|
+
this.access = undefined;
|
|
258
|
+
logger.warn("push", "FCM send failed", { data: { status: res.status, body: text } });
|
|
259
|
+
return "error";
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
logger.warn("push", "FCM send threw", {
|
|
263
|
+
data: { message: err instanceof Error ? err.message : String(err) },
|
|
264
|
+
});
|
|
265
|
+
return "error";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* The call seam the WorkspaceManager uses. Fans a wake-up out to every device
|
|
271
|
+
* token, but only when NO client socket is live — a foregrounded app already
|
|
272
|
+
* receives the event over the wire and raises its own local notification, so
|
|
273
|
+
* pushing then would double-notify. Tokens FCM reports as stale are pruned.
|
|
274
|
+
*/
|
|
275
|
+
export class PushNotifier {
|
|
276
|
+
tokens;
|
|
277
|
+
sender;
|
|
278
|
+
hasLiveClients;
|
|
279
|
+
constructor(tokens, sender,
|
|
280
|
+
/** True while at least one client socket is attached (see Hub). */
|
|
281
|
+
hasLiveClients) {
|
|
282
|
+
this.tokens = tokens;
|
|
283
|
+
this.sender = sender;
|
|
284
|
+
this.hasLiveClients = hasLiveClients;
|
|
285
|
+
}
|
|
286
|
+
notifyPermission(ws, name, tool, summary, seq) {
|
|
287
|
+
this.dispatch({
|
|
288
|
+
title: `${name} — approval needed`,
|
|
289
|
+
body: summary || `${tool} needs your approval`,
|
|
290
|
+
data: this.data({ ws, kind: "permission", tool, seq }),
|
|
291
|
+
androidChannel: "permissions",
|
|
292
|
+
urgent: true,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
notifyDone(ws, name, summary, seq) {
|
|
296
|
+
this.dispatch({
|
|
297
|
+
title: `${name} — done`,
|
|
298
|
+
body: summary && summary.length ? summary : "The agent finished its turn.",
|
|
299
|
+
data: this.data({ ws, kind: "done", seq }),
|
|
300
|
+
androidChannel: "activity",
|
|
301
|
+
urgent: false,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
notifyLimit(ws, name, message) {
|
|
305
|
+
this.dispatch({
|
|
306
|
+
title: `${name} — usage limit reached`,
|
|
307
|
+
body: message || "The agent hit a usage limit.",
|
|
308
|
+
data: this.data({ ws, kind: "limit" }),
|
|
309
|
+
androidChannel: "activity",
|
|
310
|
+
urgent: true,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/** Build the FCM data map: everything stringified, undefined/empty dropped. */
|
|
314
|
+
data(fields) {
|
|
315
|
+
const out = {};
|
|
316
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
317
|
+
if (v !== undefined && v !== "")
|
|
318
|
+
out[k] = String(v);
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
dispatch(msg) {
|
|
323
|
+
// A live socket already delivers this event (and the app raises its own local
|
|
324
|
+
// notification), so only remote-wake when nothing is attached.
|
|
325
|
+
if (this.hasLiveClients())
|
|
326
|
+
return;
|
|
327
|
+
if (!this.sender.enabled)
|
|
328
|
+
return;
|
|
329
|
+
void this.fanOut(msg);
|
|
330
|
+
}
|
|
331
|
+
async fanOut(msg) {
|
|
332
|
+
for (const t of this.tokens.all()) {
|
|
333
|
+
const outcome = await this.sender.send(t.token, msg);
|
|
334
|
+
if (outcome === "stale")
|
|
335
|
+
await this.tokens.unregister(t.token);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../src/push.ts"],"names":[],"mappings":"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"}
|
package/dist/src/server.js
CHANGED
|
@@ -26,6 +26,7 @@ import { DaemonIdentity } from "./daemon-identity.js";
|
|
|
26
26
|
import { checkForUpdate, performUpdate, scheduleRestart } from "./updater.js";
|
|
27
27
|
import { originUrl } from "./gitops.js";
|
|
28
28
|
import { AllowlistStore } from "./allowlist.js";
|
|
29
|
+
import { FcmSender, PushNotifier, PushTokenStore } from "./push.js";
|
|
29
30
|
import { SystemEvents } from "./events.js";
|
|
30
31
|
import { JobStore } from "./jobs/store.js";
|
|
31
32
|
import { JobScheduler } from "./jobs/scheduler.js";
|
|
@@ -165,12 +166,21 @@ export async function startServer(opts) {
|
|
|
165
166
|
// spawn already sees any prior grants (auto-approving matching calls).
|
|
166
167
|
const allowlist = new AllowlistStore();
|
|
167
168
|
await allowlist.load();
|
|
169
|
+
// FCM remote wake. The token store holds the device tokens clients register
|
|
170
|
+
// over the socket; the sender is disabled (a no-op) unless a Firebase service
|
|
171
|
+
// account is configured via env. The notifier gates every send on "no live
|
|
172
|
+
// client" (a foregrounded app already gets the event + a local notification).
|
|
173
|
+
const pushTokens = new PushTokenStore();
|
|
174
|
+
await pushTokens.load();
|
|
175
|
+
const fcmSender = new FcmSender();
|
|
176
|
+
await fcmSender.load();
|
|
177
|
+
const pushNotifier = new PushNotifier(pushTokens, fcmSender, () => hub.hasLiveClients());
|
|
168
178
|
// Default chatlog (2nd arg undefined → its default) + event bus + credentials
|
|
169
|
-
// + app config + allowlist.
|
|
179
|
+
// + app config + allowlist + FCM notifier.
|
|
170
180
|
const workspaces = new WorkspaceManager(hub, undefined, // chatlog → default
|
|
171
181
|
undefined, // media → default
|
|
172
182
|
events, undefined, // projects → default
|
|
173
|
-
credentials, appConfig, allowlist);
|
|
183
|
+
credentials, appConfig, allowlist, pushNotifier);
|
|
174
184
|
await workspaces.load();
|
|
175
185
|
// Cron/scheduled jobs. The scheduler drives runs through the WorkspaceManager
|
|
176
186
|
// (which structurally satisfies its JobRunnerHost) and listens on the same
|
|
@@ -262,7 +272,7 @@ export async function startServer(opts) {
|
|
|
262
272
|
// Fallback device identity for clients that don't send a stable deviceId:
|
|
263
273
|
// isolates this connection's terminal, though it won't survive a reconnect.
|
|
264
274
|
const connId = randomUUID();
|
|
265
|
-
socket.on("message", (data) => handleFrame(socket, data.toString(), connId, opts.crypto, hub, workspaces, jobs, credentials, appConfig, identity, forwards, forwardScanner));
|
|
275
|
+
socket.on("message", (data) => handleFrame(socket, data.toString(), connId, opts.crypto, hub, workspaces, jobs, credentials, appConfig, identity, forwards, forwardScanner, pushTokens));
|
|
266
276
|
socket.on("close", (code) => {
|
|
267
277
|
hub.remove(socket);
|
|
268
278
|
// Reap the per-connection terminal (keyed by this connection's fallback
|
|
@@ -339,7 +349,7 @@ function sanitizeAttachments(raw) {
|
|
|
339
349
|
}
|
|
340
350
|
return out.length > 0 ? out : undefined;
|
|
341
351
|
}
|
|
342
|
-
async function handleFrame(socket, raw, connId, crypto, hub, workspaces, jobs, credentials, appConfig, identity, forwards, forwardScanner) {
|
|
352
|
+
async function handleFrame(socket, raw, connId, crypto, hub, workspaces, jobs, credentials, appConfig, identity, forwards, forwardScanner, pushTokens) {
|
|
343
353
|
let env;
|
|
344
354
|
try {
|
|
345
355
|
// Decode with this socket's own codec — cleartext until it has completed the
|
|
@@ -473,6 +483,24 @@ async function handleFrame(socket, raw, connId, crypto, hub, workspaces, jobs, c
|
|
|
473
483
|
reply(done);
|
|
474
484
|
return;
|
|
475
485
|
}
|
|
486
|
+
case "push.register": {
|
|
487
|
+
// A client registers its FCM device token so the daemon can remote-wake
|
|
488
|
+
// it when no socket is live. Fire-and-forget: no reply frame (mirrors
|
|
489
|
+
// client.hello). Invalid tokens/platforms are dropped by the store.
|
|
490
|
+
const p = env.payload;
|
|
491
|
+
if (typeof p.token === "string" && typeof p.platform === "string") {
|
|
492
|
+
const ok = await pushTokens.register(p.token, p.platform);
|
|
493
|
+
if (ok)
|
|
494
|
+
logger.info("push", "registered device token", { data: { platform: p.platform } });
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
case "push.unregister": {
|
|
499
|
+
const p = env.payload;
|
|
500
|
+
if (typeof p.token === "string")
|
|
501
|
+
await pushTokens.unregister(p.token);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
476
504
|
case "ping":
|
|
477
505
|
reply(make("pong", env.ws, {}));
|
|
478
506
|
return;
|