@cosmovex/agentpager 0.1.2 → 0.1.4
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/cli.js +145 -39
- package/dist/guard/index.js +2 -2
- package/dist/hub.js +33 -1
- package/dist/service.js +2 -2
- package/dist/update.js +52 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,8 @@ import { runRuleCommand, USAGE as GUARD_USAGE } from './guard/cli.js';
|
|
|
22
22
|
import { install as guardInstall, runDeskHook, runHook, status as guardStatus, uninstall as guardUninstall } from './guard/index.js';
|
|
23
23
|
import { Channel } from './relay.js';
|
|
24
24
|
import { installService, uninstallService } from './service.js';
|
|
25
|
-
import { loadState,
|
|
25
|
+
import { loadState, updateState } from './store.js';
|
|
26
|
+
import { isNewer, latestVersion, UPDATE_COMMAND } from './update.js';
|
|
26
27
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
27
28
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
28
29
|
const log = (line) => console.log(`${dim(new Date().toLocaleTimeString())} ${line}`);
|
|
@@ -34,7 +35,22 @@ function duration(v) {
|
|
|
34
35
|
const n = Number(m[1]);
|
|
35
36
|
return n * (m[2] === 'h' ? 3600_000 : m[2] === 'm' ? 60_000 : 1000);
|
|
36
37
|
}
|
|
38
|
+
/** One line per paired phone, with the id `unpair` wants. Never leaves someone guessing what is paired. */
|
|
39
|
+
function printPhones(devices, indent = ' ') {
|
|
40
|
+
for (const d of devices) {
|
|
41
|
+
console.log(`${indent}${bold(d.name)} ${dim(`id ${d.id} · paired ${new Date(d.pairedAt).toLocaleDateString()}`)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const UNPAIR_HINT = 'Remove a phone: `agentpager unpair <id>` (or `agentpager unpair --all`). List them: `agentpager devices`.';
|
|
37
45
|
async function pair(state) {
|
|
46
|
+
// Re-read: this snapshot may be as old as the terminal. "Paired" with no way to see what, or to
|
|
47
|
+
// undo it, is what made setup feel like a trap.
|
|
48
|
+
const current = loadState().devices;
|
|
49
|
+
if (current.length) {
|
|
50
|
+
console.log(`\n${bold(`Already paired on ${state.machineName}:`)}`);
|
|
51
|
+
printPhones(current);
|
|
52
|
+
console.log(dim(`${UNPAIR_HINT}\nContinuing adds one more phone.`));
|
|
53
|
+
}
|
|
38
54
|
const offer = offerPairing(state);
|
|
39
55
|
console.log(`\n${bold('Pair your phone')} — in the AgentPager app tap "Scan the code" (or point your phone camera here):\n`);
|
|
40
56
|
qrcode.generate(offer.link, { small: true });
|
|
@@ -45,6 +61,10 @@ async function pair(state) {
|
|
|
45
61
|
updateState((s) => s.devices.push(device));
|
|
46
62
|
state.devices.push(device); // keep the in-memory copy usable for a caller that goes on to serve
|
|
47
63
|
console.log(`\n✅ Paired with ${bold(device.name)}.\n`);
|
|
64
|
+
const all = loadState().devices;
|
|
65
|
+
console.log(`${bold(`Paired phones (${all.length}):`)}`);
|
|
66
|
+
printPhones(all);
|
|
67
|
+
console.log(dim(`${UNPAIR_HINT}\n`));
|
|
48
68
|
// Pairing does not make this computer reachable — serving does. Standalone `agentpager pair`
|
|
49
69
|
// printed this tick and exited, leaving the phone attached to nothing: it showed "waiting for the
|
|
50
70
|
// computer to say which agents it has", then "computer is offline". Both were true, and neither
|
|
@@ -87,37 +107,78 @@ async function serve(state, keepAwake) {
|
|
|
87
107
|
['codex', codex],
|
|
88
108
|
]);
|
|
89
109
|
const hub = new Hub(agents, state.machineName, log, keepAwake);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
// Persist only what THIS process owns (sequence numbers, push tokens) and merge it into the file
|
|
111
|
+
// as it is now. Saving our startup snapshot would resurrect a phone that `agentpager unpair`
|
|
112
|
+
// just removed — a revoked credential coming back to life.
|
|
113
|
+
const channels = new Map();
|
|
114
|
+
let dirty = false;
|
|
115
|
+
const flush = () => {
|
|
116
|
+
if (!dirty)
|
|
117
|
+
return;
|
|
118
|
+
dirty = false;
|
|
119
|
+
updateState((s) => {
|
|
120
|
+
for (const d of s.devices) {
|
|
121
|
+
const live = channels.get(d.id)?.device;
|
|
122
|
+
if (live)
|
|
123
|
+
Object.assign(d, { sendSeq: live.sendSeq, recvSeq: live.recvSeq, pushToken: live.pushToken });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
let flushTimer;
|
|
128
|
+
const persist = () => {
|
|
129
|
+
dirty = true;
|
|
130
|
+
flushTimer ??= setTimeout(() => {
|
|
131
|
+
flushTimer = undefined;
|
|
132
|
+
flush();
|
|
133
|
+
}, 250);
|
|
134
|
+
};
|
|
135
|
+
const connect = (d) => {
|
|
136
|
+
if (channels.has(d.id))
|
|
137
|
+
return;
|
|
138
|
+
const ch = new Channel(d, persist, (msg, c) => hub.handle(msg, c));
|
|
139
|
+
channels.set(d.id, ch);
|
|
140
|
+
hub.attach(ch);
|
|
141
|
+
ch.start();
|
|
142
|
+
};
|
|
143
|
+
const disconnect = async (id, told) => {
|
|
144
|
+
const ch = channels.get(id);
|
|
145
|
+
if (!ch)
|
|
146
|
+
return;
|
|
147
|
+
channels.delete(id);
|
|
148
|
+
hub.detach(ch);
|
|
149
|
+
if (told) {
|
|
150
|
+
ch.send({ type: 'unpaired', machine: state.machineName });
|
|
151
|
+
await new Promise((r) => setTimeout(r, 1500)); // let the message leave before the channel closes
|
|
106
152
|
}
|
|
107
|
-
|
|
108
|
-
|
|
153
|
+
await ch.stop();
|
|
154
|
+
};
|
|
155
|
+
for (const d of state.devices)
|
|
156
|
+
connect(d);
|
|
157
|
+
// `agentpager unpair` / `agentpager pair` in ANOTHER terminal edit the file. Without this, an
|
|
158
|
+
// unpaired phone stayed connected (and showed "online") until this window was restarted, and a
|
|
159
|
+
// newly paired one got nothing until then too.
|
|
160
|
+
const watch = setInterval(() => {
|
|
161
|
+
const now = loadState().devices;
|
|
162
|
+
for (const id of [...channels.keys()]) {
|
|
163
|
+
if (!now.some((d) => d.id === id)) {
|
|
164
|
+
log(`unpaired: ${channels.get(id)?.deviceName}`);
|
|
165
|
+
void disconnect(id, true);
|
|
166
|
+
}
|
|
109
167
|
}
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const c = new Channel(d, persist, (msg, ch) => hub.handle(msg, ch));
|
|
116
|
-
hub.attach(c);
|
|
117
|
-
c.start();
|
|
118
|
-
log(`paired while running: ${d.name}`);
|
|
168
|
+
for (const d of now) {
|
|
169
|
+
if (!channels.has(d.id)) {
|
|
170
|
+
log(`paired while running: ${d.name}`);
|
|
171
|
+
connect(d);
|
|
172
|
+
}
|
|
119
173
|
}
|
|
120
|
-
},
|
|
174
|
+
}, 2000);
|
|
175
|
+
hub.onUnpair = (ch) => {
|
|
176
|
+
// The phone forgot us in the app: drop it from disk first, then close the channel.
|
|
177
|
+
updateState((s) => {
|
|
178
|
+
s.devices = s.devices.filter((d) => d.id !== ch.device.id);
|
|
179
|
+
});
|
|
180
|
+
void disconnect(ch.device.id, false);
|
|
181
|
+
};
|
|
121
182
|
const stopControl = serveControl((req) => hub.job(req));
|
|
122
183
|
// Detect the ACP agents in the background and announce them as they appear.
|
|
123
184
|
void (async () => {
|
|
@@ -136,18 +197,43 @@ async function serve(state, keepAwake) {
|
|
|
136
197
|
console.log(` ⚠️ ${stale}`);
|
|
137
198
|
for (const s of statuses)
|
|
138
199
|
console.log(` ${s.available ? '●' : '○'} ${s.name}${s.version ? ` ${dim(s.version)}` : ''}${s.available ? '' : dim(` ${s.reason}`)}`);
|
|
139
|
-
console.log(` ${state.devices.length} phone${state.devices.length === 1 ? '' : 's'} paired
|
|
140
|
-
|
|
200
|
+
console.log(` ${state.devices.length} phone${state.devices.length === 1 ? '' : 's'} paired:`);
|
|
201
|
+
printPhones(state.devices, ' ');
|
|
202
|
+
if (process.argv[1]?.includes('_npx')) {
|
|
203
|
+
// Started with npx: this window IS AgentPager. Say so before someone closes it.
|
|
204
|
+
console.log(`\n${bold('Keep this window open.')} Closing it (or Ctrl+C) disconnects your phone until you start it again.`);
|
|
205
|
+
console.log(dim('Want it always on, even after a restart? npm install -g @cosmovex/agentpager then: agentpager service install'));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log(dim('\nKeep this running (or run `agentpager service install` to start it at login).'));
|
|
209
|
+
}
|
|
141
210
|
// Someone who reinstalls the app, or picks up a second phone, reasonably runs `agentpager` again
|
|
142
211
|
// and waits for a code that never comes — this is the only place they will look for why.
|
|
143
212
|
console.log(dim('Pairing another phone (or the same one after a reinstall)? Run `agentpager pair`.'));
|
|
213
|
+
console.log(dim(UNPAIR_HINT));
|
|
144
214
|
console.log(dim('Your sessions stay on this computer; the relay only sees encrypted bytes.\n'));
|
|
215
|
+
// Tell the terminal AND the phone when a newer AgentPager is out.
|
|
216
|
+
const checkUpdate = async () => {
|
|
217
|
+
const latest = await latestVersion();
|
|
218
|
+
if (latest && isNewer(latest, BRIDGE_VERSION)) {
|
|
219
|
+
console.log(`\n⬆️ ${bold(`AgentPager ${latest} is available`)} (you have ${BRIDGE_VERSION}). Update: ${bold(UPDATE_COMMAND)}, then restart.\n`);
|
|
220
|
+
await hub.setLatest(latest);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
void checkUpdate();
|
|
224
|
+
const updateTimer = setInterval(() => void checkUpdate(), 12 * 3600_000);
|
|
145
225
|
const quit = async () => {
|
|
146
226
|
console.log(dim('\nDisconnecting…'));
|
|
227
|
+
for (const c of channels.values())
|
|
228
|
+
c.send({ type: 'bye' });
|
|
229
|
+
await new Promise((r) => setTimeout(r, 800)); // let it leave before the socket closes
|
|
147
230
|
hub.shutdown();
|
|
148
231
|
stopControl();
|
|
149
232
|
codex.shutdown();
|
|
150
|
-
|
|
233
|
+
clearInterval(watch);
|
|
234
|
+
clearInterval(updateTimer);
|
|
235
|
+
flush();
|
|
236
|
+
await Promise.all([...channels.values()].map((c) => c.stop()));
|
|
151
237
|
process.exit(0);
|
|
152
238
|
};
|
|
153
239
|
process.on('SIGINT', quit);
|
|
@@ -195,16 +281,36 @@ async function main() {
|
|
|
195
281
|
case 'devices':
|
|
196
282
|
if (!state.devices.length)
|
|
197
283
|
console.log('No phones paired. Run `agentpager pair`.');
|
|
198
|
-
|
|
199
|
-
console.log(`${
|
|
284
|
+
else {
|
|
285
|
+
console.log(bold(`${state.devices.length} phone${state.devices.length === 1 ? '' : 's'} paired with ${state.machineName}:`));
|
|
286
|
+
printPhones(state.devices);
|
|
287
|
+
console.log(dim(`\n${UNPAIR_HINT}`));
|
|
288
|
+
}
|
|
200
289
|
process.exit(0);
|
|
201
290
|
case 'unpair': {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
291
|
+
if (!arg) {
|
|
292
|
+
console.log('Usage: agentpager unpair <id | name> | --all\n');
|
|
293
|
+
if (state.devices.length)
|
|
294
|
+
printPhones(state.devices, '');
|
|
295
|
+
else
|
|
296
|
+
console.log('No phones are paired.');
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
const q = arg.toLowerCase();
|
|
300
|
+
const hit = (d) => arg === '--all' || d.id === arg || d.name.toLowerCase() === q;
|
|
301
|
+
const removed = state.devices.filter(hit);
|
|
302
|
+
if (!removed.length) {
|
|
303
|
+
console.log(`No phone matches "${arg}". Paired phones:`);
|
|
304
|
+
printPhones(state.devices, ' ');
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
updateState((s) => {
|
|
308
|
+
s.devices = s.devices.filter((d) => !hit(d));
|
|
205
309
|
});
|
|
206
|
-
|
|
207
|
-
|
|
310
|
+
for (const d of removed)
|
|
311
|
+
console.log(`Removed ${bold(d.name)}. It can no longer reach this computer.`);
|
|
312
|
+
const running = await bridgeRunning();
|
|
313
|
+
console.log(dim(running ? 'The running AgentPager drops it within a couple of seconds and tells the phone.' : 'Nothing is running here, so it is gone for good.'));
|
|
208
314
|
process.exit(0);
|
|
209
315
|
}
|
|
210
316
|
case 'notify':
|
package/dist/guard/index.js
CHANGED
|
@@ -204,7 +204,7 @@ function HEADLESS_WAYS_OUT(rule) {
|
|
|
204
204
|
/**
|
|
205
205
|
* Running from npx's throwaway cache?
|
|
206
206
|
*
|
|
207
|
-
* `npx agentpager` is the right way to try this — it needs no commitment and leaves nothing behind.
|
|
207
|
+
* `npx @cosmovex/agentpager` is the right way to try this — it needs no commitment and leaves nothing behind.
|
|
208
208
|
* But "leaves nothing behind" is fatal for a hook: the absolute path we register would point into a
|
|
209
209
|
* directory npx is free to delete, and the guard would stop running without ever saying so. Rules
|
|
210
210
|
* need the package to still exist tomorrow.
|
|
@@ -217,7 +217,7 @@ export function install() {
|
|
|
217
217
|
console.log('✖ This copy is running from npx\u2019s temporary cache, which gets deleted.');
|
|
218
218
|
console.log(' A hook pointing there would stop working silently, so it is not written.');
|
|
219
219
|
console.log(' Install it properly first, then rules will stick:');
|
|
220
|
-
console.log(' npm install -g agentpager');
|
|
220
|
+
console.log(' npm install -g @cosmovex/agentpager');
|
|
221
221
|
console.log(' agentpager-guard install');
|
|
222
222
|
process.exitCode = 1;
|
|
223
223
|
return;
|
package/dist/hub.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
1
3
|
// Routes phone messages to agents and agent events back to every paired phone.
|
|
2
4
|
// Margin rules live here: stream text only to a phone someone is looking at; approvals and
|
|
3
5
|
// "done" always go out (those are what make the app worth having), and they are tiny.
|
|
@@ -7,10 +9,21 @@ import { random, b64u } from './crypto.js';
|
|
|
7
9
|
import { record as recordLimits } from './guard/limits.js';
|
|
8
10
|
import { PROTOCOL } from './protocol.js';
|
|
9
11
|
import { addRoot, allowedCwd, browseFolders, discoverProjects, MAX_PHONE_FILE_BYTES, readFileForPhone, writeUploadedFile } from './projects.js';
|
|
12
|
+
import { isNewer } from './update.js';
|
|
10
13
|
import { AwakeLock } from './awake.js';
|
|
11
14
|
import { deskActive, pruneDesk } from './guard/desk.js';
|
|
12
15
|
import { loadRules, PRESETS, saveRules, validateRules } from './guard/rules.js';
|
|
13
|
-
|
|
16
|
+
// Read from package.json, never typed twice: this constant sat at 0.1.0 while npm had 0.1.2, and
|
|
17
|
+
// the update check would have told every user, for ever, that the version they run is out of date.
|
|
18
|
+
export const BRIDGE_VERSION = (() => {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'));
|
|
21
|
+
return String(pkg.version);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return '0.0.0';
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
14
27
|
const FLUSH_MS = 400;
|
|
15
28
|
// A script that asks and is never answered must give up on its own. Ten minutes is long enough to
|
|
16
29
|
// walk back from the kitchen and short enough that a CI job doesn't sit on a runner all night.
|
|
@@ -43,6 +56,21 @@ export class Hub {
|
|
|
43
56
|
attach(channel) {
|
|
44
57
|
this.channels.push(channel);
|
|
45
58
|
}
|
|
59
|
+
/** Stop talking to a phone. Anything it still has pending stays pending; it can no longer answer. */
|
|
60
|
+
detach(channel) {
|
|
61
|
+
this.channels = this.channels.filter((c) => c !== channel);
|
|
62
|
+
}
|
|
63
|
+
/** Called with the phone that asked to be unpaired; the CLI owns the state file. */
|
|
64
|
+
onUnpair;
|
|
65
|
+
/** Newest version on npm, once known. Sent with every hello so the phone can say "update". */
|
|
66
|
+
latest;
|
|
67
|
+
async setLatest(v) {
|
|
68
|
+
if (!v || v === this.latest)
|
|
69
|
+
return;
|
|
70
|
+
this.latest = v;
|
|
71
|
+
for (const c of this.channels)
|
|
72
|
+
await this.hello(c);
|
|
73
|
+
}
|
|
46
74
|
/** Agents detected after startup (probing them must never delay the relay). */
|
|
47
75
|
async addAgent(agent) {
|
|
48
76
|
this.agents.set(agent.id, agent);
|
|
@@ -69,6 +97,9 @@ export class Hub {
|
|
|
69
97
|
switch (msg.type) {
|
|
70
98
|
case 'hello':
|
|
71
99
|
return await this.hello(from);
|
|
100
|
+
case 'unpair':
|
|
101
|
+
this.log(`${from.deviceName} unpaired itself from the app`);
|
|
102
|
+
return this.onUnpair?.(from);
|
|
72
103
|
case 'list':
|
|
73
104
|
return from.send({ type: 'sessions', items: await this.list() });
|
|
74
105
|
case 'open':
|
|
@@ -263,6 +294,7 @@ export class Hub {
|
|
|
263
294
|
protocol: PROTOCOL,
|
|
264
295
|
agents: await this.statuses(),
|
|
265
296
|
projects: await this.offerProjects(),
|
|
297
|
+
...(this.latest && isNewer(this.latest, BRIDGE_VERSION) ? { latest: this.latest } : {}),
|
|
266
298
|
});
|
|
267
299
|
// A phone that reconnects must see every approval still waiting — never miss one.
|
|
268
300
|
for (const live of this.live.values()) {
|
package/dist/service.js
CHANGED
|
@@ -16,14 +16,14 @@ function run(cmd, args) {
|
|
|
16
16
|
/** npx runs from a throwaway cache; a service pointing there breaks on the next cache clean. */
|
|
17
17
|
function assertStableInstall() {
|
|
18
18
|
if (/[\\/]_npx[\\/]/.test(cli)) {
|
|
19
|
-
throw new Error('Install it permanently first: npm install -g agentpager then run: agentpager service install');
|
|
19
|
+
throw new Error('Install it permanently first: npm install -g @cosmovex/agentpager then run: agentpager service install');
|
|
20
20
|
}
|
|
21
21
|
// A macOS LaunchAgent cannot read an external/removable volume: the job starts and hangs before
|
|
22
22
|
// the first line of code, with no error anywhere. Measured 2026-09-23.
|
|
23
23
|
if (platform() === 'darwin' && cli.startsWith('/Volumes/')) {
|
|
24
24
|
throw new Error(`This copy lives on an external volume (${cli}).\n` +
|
|
25
25
|
'macOS login items cannot read those, so the service would start and hang silently.\n' +
|
|
26
|
-
'Install it on the internal disk instead: npm install -g agentpager then: agentpager service install');
|
|
26
|
+
'Install it on the internal disk instead: npm install -g @cosmovex/agentpager then: agentpager service install');
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
export function installService() {
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Is there a newer AgentPager on npm? The phone can only say "update" if the computer knows.
|
|
2
|
+
// One small GET, cached on disk for 12 hours, and every failure means "no news" — an update
|
|
3
|
+
// check must never be able to slow down or break a bridge that is otherwise working.
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { stateDir } from './store.js';
|
|
7
|
+
const REGISTRY = 'https://registry.npmjs.org/@cosmovex%2Fagentpager/latest';
|
|
8
|
+
const CACHE_MS = 12 * 3600_000;
|
|
9
|
+
const cacheFile = () => join(stateDir, 'update.json');
|
|
10
|
+
/** True when `latest` is a strictly newer x.y.z than `current`. Pre-release tags are ignored. */
|
|
11
|
+
export function isNewer(latest, current) {
|
|
12
|
+
const parse = (v) => v.split('-')[0].split('.').map((n) => parseInt(n, 10) || 0);
|
|
13
|
+
const a = parse(latest);
|
|
14
|
+
const b = parse(current);
|
|
15
|
+
for (let i = 0; i < 3; i++) {
|
|
16
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0))
|
|
17
|
+
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
/** The newest published version, or undefined when we cannot tell. */
|
|
22
|
+
export async function latestVersion(fetchImpl = fetch) {
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(cacheFile())) {
|
|
25
|
+
const c = JSON.parse(readFileSync(cacheFile(), 'utf8'));
|
|
26
|
+
if (Date.now() - c.at < CACHE_MS && c.latest)
|
|
27
|
+
return c.latest;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* unreadable cache: ask again */
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetchImpl(REGISTRY, { signal: AbortSignal.timeout(5000) });
|
|
35
|
+
if (!res.ok)
|
|
36
|
+
return undefined; // 404 until the first publish
|
|
37
|
+
const latest = String((await res.json()).version ?? '');
|
|
38
|
+
if (!/^\d+\.\d+\.\d+/.test(latest))
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(cacheFile(), JSON.stringify({ at: Date.now(), latest }));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* read-only home: fine, we just ask again next time */
|
|
45
|
+
}
|
|
46
|
+
return latest;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export const UPDATE_COMMAND = 'npm install -g @cosmovex/agentpager@latest';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmovex/agentpager",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Pager for your AI coding agents
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Pager for your AI coding agents — approve, reply and hear results on your phone. End-to-end encrypted.",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsc && node -e \"const fs=require('fs');for(const f of ['dist/cli.js','dist/guard/bin.js'])fs.chmodSync(f,0o755)\"",
|