@cosmovex/agentpager 0.1.3 → 0.1.5

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 CHANGED
@@ -21,7 +21,7 @@ import { staleBuildWarning } from './freshness.js';
21
21
  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
- import { installService, uninstallService } from './service.js';
24
+ import { installService, serviceInstalled, uninstallService } from './service.js';
25
25
  import { loadState, updateState } from './store.js';
26
26
  import { isNewer, latestVersion, UPDATE_COMMAND } from './update.js';
27
27
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
@@ -41,6 +41,29 @@ function printPhones(devices, indent = ' ') {
41
41
  console.log(`${indent}${bold(d.name)} ${dim(`id ${d.id} · paired ${new Date(d.pairedAt).toLocaleDateString()}`)}`);
42
42
  }
43
43
  }
44
+ const HELP = `
45
+ ${bold('AgentPager')} — your phone, one tap from your coding agents.
46
+
47
+ ${bold('Everyday')}
48
+ agentpager start it (keep the window open)
49
+ agentpager stop stop it
50
+ agentpager status is it running, who is paired, is there an update
51
+
52
+ ${bold('Phones')}
53
+ agentpager pair add a phone (or the same one after a reinstall)
54
+ agentpager devices who is paired
55
+ agentpager unpair remove a phone
56
+
57
+ ${bold('Always on')}
58
+ agentpager service install start by itself at login
59
+ agentpager service uninstall turn that off
60
+
61
+ ${bold('For scripts')}
62
+ agentpager notify "text" page your phone
63
+ agentpager ask "question" wait for your yes or no
64
+
65
+ More: agentpager doctor · agentpager guard
66
+ `;
44
67
  const UNPAIR_HINT = 'Remove a phone: `agentpager unpair <id>` (or `agentpager unpair --all`). List them: `agentpager devices`.';
45
68
  async function pair(state) {
46
69
  // Re-read: this snapshot may be as old as the terminal. "Paired" with no way to see what, or to
@@ -172,6 +195,7 @@ async function serve(state, keepAwake) {
172
195
  }
173
196
  }
174
197
  }, 2000);
198
+ hub.onStop = () => void quit();
175
199
  hub.onUnpair = (ch) => {
176
200
  // The phone forgot us in the app: drop it from disk first, then close the channel.
177
201
  updateState((s) => {
@@ -199,12 +223,17 @@ async function serve(state, keepAwake) {
199
223
  console.log(` ${s.available ? '●' : '○'} ${s.name}${s.version ? ` ${dim(s.version)}` : ''}${s.available ? '' : dim(` ${s.reason}`)}`);
200
224
  console.log(` ${state.devices.length} phone${state.devices.length === 1 ? '' : 's'} paired:`);
201
225
  printPhones(state.devices, ' ');
202
- console.log(dim('\nKeep this running (or run `agentpager service install` to start it at login).'));
226
+ if (process.argv[1]?.includes('_npx')) {
227
+ // Started with npx: this window IS AgentPager. Say so before someone closes it.
228
+ console.log(`\n${bold('Keep this window open.')} Closing it (or Ctrl+C) disconnects your phone until you start it again.`);
229
+ console.log(dim('Always on, even after a restart: npm install -g @cosmovex/agentpager, then agentpager service install'));
230
+ }
231
+ else {
232
+ console.log(dim('\nKeep this running (or run `agentpager service install` to start it at login).'));
233
+ }
203
234
  // Someone who reinstalls the app, or picks up a second phone, reasonably runs `agentpager` again
204
235
  // and waits for a code that never comes — this is the only place they will look for why.
205
- console.log(dim('Pairing another phone (or the same one after a reinstall)? Run `agentpager pair`.'));
206
- console.log(dim(UNPAIR_HINT));
207
- console.log(dim('Your sessions stay on this computer; the relay only sees encrypted bytes.\n'));
236
+ console.log(dim('Need anything else? Run `agentpager help`.\n'));
208
237
  // Tell the terminal AND the phone when a newer AgentPager is out.
209
238
  const checkUpdate = async () => {
210
239
  const latest = await latestVersion();
@@ -217,6 +246,9 @@ async function serve(state, keepAwake) {
217
246
  const updateTimer = setInterval(() => void checkUpdate(), 12 * 3600_000);
218
247
  const quit = async () => {
219
248
  console.log(dim('\nDisconnecting…'));
249
+ for (const c of channels.values())
250
+ c.send({ type: 'bye' });
251
+ await new Promise((r) => setTimeout(r, 800)); // let it leave before the socket closes
220
252
  hub.shutdown();
221
253
  stopControl();
222
254
  codex.shutdown();
@@ -303,6 +335,32 @@ async function main() {
303
335
  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.'));
304
336
  process.exit(0);
305
337
  }
338
+ case 'help':
339
+ case '--help':
340
+ case '-h':
341
+ console.log(HELP);
342
+ process.exit(0);
343
+ case 'status': {
344
+ const up = await callControl({ kind: 'ping', title: '', text: '' }, 1500).catch(() => null);
345
+ console.log(up ? `● Running on ${bold(state.machineName)}` : `○ Not running. Start it: ${bold('agentpager')} (keep the window open)`);
346
+ console.log(` ${state.devices.length} phone${state.devices.length === 1 ? '' : 's'} paired${state.devices.length ? ':' : ''}`);
347
+ printPhones(state.devices, ' ');
348
+ console.log(` Starts by itself at login: ${serviceInstalled() ? 'yes' : 'no'}`);
349
+ const latest = await latestVersion();
350
+ console.log(latest && isNewer(latest, BRIDGE_VERSION) ? ` ⬆️ ${latest} is available (you have ${BRIDGE_VERSION}): ${bold(UPDATE_COMMAND)}` : ` Version ${BRIDGE_VERSION}, up to date`);
351
+ process.exit(0);
352
+ }
353
+ case 'stop': {
354
+ const up = await callControl({ kind: 'stop', title: '', text: '' }, 3000).catch(() => null);
355
+ if (!up)
356
+ console.log('AgentPager is not running.');
357
+ else {
358
+ console.log('Stopped. Your phone will say so.');
359
+ if (serviceInstalled())
360
+ console.log(dim('It is set to start at login and may restart by itself. To turn that off: agentpager service uninstall'));
361
+ }
362
+ process.exit(0);
363
+ }
306
364
  case 'notify':
307
365
  case 'ask': {
308
366
  // Parse flags properly: a flag's VALUE must not leak into the message body.
@@ -433,7 +491,7 @@ async function main() {
433
491
  process.exit(0);
434
492
  }
435
493
  default:
436
- console.log('Usage: agentpager [start | pair | devices | unpair <id> | doctor | guard install|status|rules|test | notify <text> | ask <text> | service install|uninstall]');
494
+ console.log(HELP);
437
495
  process.exit(1);
438
496
  }
439
497
  }
package/dist/hub.js CHANGED
@@ -60,6 +60,8 @@ export class Hub {
60
60
  detach(channel) {
61
61
  this.channels = this.channels.filter((c) => c !== channel);
62
62
  }
63
+ /** `agentpager stop` from another terminal. */
64
+ onStop;
63
65
  /** Called with the phone that asked to be unpaired; the CLI owns the state file. */
64
66
  onUnpair;
65
67
  /** Newest version on npm, once known. Sent with every hello so the phone can say "update". */
@@ -343,6 +345,10 @@ export class Hub {
343
345
  };
344
346
  if (req.kind === 'ping')
345
347
  return { decision: 'allow', ...reach };
348
+ if (req.kind === 'stop') {
349
+ setTimeout(() => this.onStop?.(), 150); // answer first, then go
350
+ return { decision: 'allow', ...reach };
351
+ }
346
352
  // Nobody to ask is not a denial and not a wait — it's a setup problem, and the script must
347
353
  // learn that in milliseconds, not after an hour of blocking a deploy.
348
354
  if (!reach.phones)
package/dist/service.js CHANGED
@@ -115,3 +115,21 @@ export function uninstallService() {
115
115
  }
116
116
  return 'Nothing to remove.';
117
117
  }
118
+ /** Is the start-at-login service set up? Cheap and offline: it only looks for the file we wrote. */
119
+ export function serviceInstalled() {
120
+ const os = platform();
121
+ if (os === 'darwin')
122
+ return existsSync(join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`));
123
+ if (os === 'linux')
124
+ return existsSync(join(homedir(), '.config', 'systemd', 'user', 'agentpager.service'));
125
+ if (os === 'win32') {
126
+ try {
127
+ execFileSync('schtasks', ['/Query', '/TN', 'AgentPager'], { stdio: 'ignore' });
128
+ return true;
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ return false;
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmovex/agentpager",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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": {