@cosmovex/agentpager 0.1.5 → 0.1.6

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.
@@ -0,0 +1,67 @@
1
+ // "Ping me on AgentPager when you're done" — teach the agents on this computer that they can.
2
+ // `agentpager notify` already pages the phone from anything that can run a shell command; what was
3
+ // missing is that Claude Code, Codex and Gemini do not know the command exists. One short, marked
4
+ // block in each agent's own instruction file fixes that, and the markers make it idempotent and
5
+ // removable without touching a word the user wrote.
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { dirname, join } from 'node:path';
9
+ const BEGIN = '<!-- agentpager:begin -->';
10
+ const END = '<!-- agentpager:end -->';
11
+ export const INSTRUCTION = `${BEGIN}
12
+ ## AgentPager (the user's phone pager)
13
+ If the user says "ping me on agentpager", "tell me on my phone" or similar, then when you finish
14
+ (or when you need their decision) run \`agentpager notify "<one-line summary, under 100 characters>"\`.
15
+ When you need a yes/no before something risky, run \`agentpager ask "<question>"\`: it exits 0 if they
16
+ approve, 1 if they deny, 2 if nobody answered, so treat 2 as "do not proceed".
17
+ If \`agentpager\` is not installed, say so instead of failing silently.
18
+ ${END}
19
+ `;
20
+ export function targets(home = process.env.HOME || homedir()) {
21
+ const t = (agent, ...parts) => {
22
+ const file = join(home, ...parts);
23
+ return { agent, file, dir: dirname(file) };
24
+ };
25
+ return [
26
+ t('Claude Code', '.claude', 'CLAUDE.md'),
27
+ t('Codex', '.codex', 'AGENTS.md'),
28
+ t('Gemini CLI', '.gemini', 'GEMINI.md'),
29
+ t('OpenCode', '.config', 'opencode', 'AGENTS.md'),
30
+ ];
31
+ }
32
+ function strip(text) {
33
+ const a = text.indexOf(BEGIN);
34
+ const b = text.indexOf(END);
35
+ if (a < 0 || b < a)
36
+ return text;
37
+ // Trim what the block left around it, so add → remove gives back the original bytes and
38
+ // add → add gives back the same file.
39
+ const rest = (text.slice(0, a) + text.slice(b + END.length)).replace(/\s+$/, '');
40
+ return rest ? `${rest}\n` : '';
41
+ }
42
+ export function setup(all = targets()) {
43
+ return all.map((t) => {
44
+ if (!existsSync(t.dir))
45
+ return { agent: t.agent, file: t.file, outcome: 'not-installed' };
46
+ const old = existsSync(t.file) ? readFileSync(t.file, 'utf8') : '';
47
+ const had = old.includes(BEGIN);
48
+ const base = strip(old);
49
+ const next = base ? `${base.replace(/\s+$/, '')}\n\n${INSTRUCTION}` : INSTRUCTION;
50
+ if (next === old)
51
+ return { agent: t.agent, file: t.file, outcome: 'unchanged' };
52
+ mkdirSync(t.dir, { recursive: true });
53
+ writeFileSync(t.file, next);
54
+ return { agent: t.agent, file: t.file, outcome: had ? 'updated' : 'added' };
55
+ });
56
+ }
57
+ export function remove(all = targets()) {
58
+ return all.map((t) => {
59
+ if (!existsSync(t.file))
60
+ return { agent: t.agent, file: t.file, outcome: 'absent' };
61
+ const old = readFileSync(t.file, 'utf8');
62
+ if (!old.includes(BEGIN))
63
+ return { agent: t.agent, file: t.file, outcome: 'absent' };
64
+ writeFileSync(t.file, strip(old));
65
+ return { agent: t.agent, file: t.file, outcome: 'removed' };
66
+ });
67
+ }
package/dist/cli.js CHANGED
@@ -21,6 +21,8 @@ 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 { remove as agentsRemove, setup as agentsSetup } from './agentsetup.js';
25
+ import { serveMcp } from './mcp.js';
24
26
  import { installService, serviceInstalled, uninstallService } from './service.js';
25
27
  import { loadState, updateState } from './store.js';
26
28
  import { isNewer, latestVersion, UPDATE_COMMAND } from './update.js';
@@ -58,6 +60,11 @@ ${bold('Always on')}
58
60
  agentpager service install start by itself at login
59
61
  agentpager service uninstall turn that off
60
62
 
63
+ ${bold('Let your AI agents ping you')}
64
+ agentpager setup-agents teach Claude, Codex, Gemini... to say "done" on your phone
65
+ agentpager setup-agents --remove
66
+ agentpager mcp the same as MCP tools (Cursor, Claude Desktop)
67
+
61
68
  ${bold('For scripts')}
62
69
  agentpager notify "text" page your phone
63
70
  agentpager ask "question" wait for your yes or no
@@ -335,6 +342,25 @@ async function main() {
335
342
  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.'));
336
343
  process.exit(0);
337
344
  }
345
+ case 'setup-agents': {
346
+ const results = arg === '--remove' ? agentsRemove() : agentsSetup();
347
+ const say = {
348
+ added: 'added', updated: 'updated', unchanged: 'already set up', removed: 'removed', absent: 'nothing to remove', 'not-installed': 'not installed here, skipped',
349
+ };
350
+ for (const r of results)
351
+ console.log(`${r.outcome === 'not-installed' || r.outcome === 'absent' ? '○' : '●'} ${r.agent} ${dim(say[r.outcome])}`);
352
+ if (arg !== '--remove' && results.some((r) => ['added', 'updated', 'unchanged'].includes(r.outcome))) {
353
+ console.log(`\nNow tell any of them ${bold('"ping me on agentpager when you are done"')} and your phone will buzz.`);
354
+ console.log(dim('Assistants without a shell (Cursor, Claude Desktop): add an MCP server that runs `agentpager mcp`.'));
355
+ }
356
+ else if (arg !== '--remove') {
357
+ console.log('\nNo supported agent found. Any script can still use `agentpager notify "text"`.');
358
+ }
359
+ process.exit(0);
360
+ }
361
+ case 'mcp':
362
+ serveMcp();
363
+ return; // keep the process alive: stdin drives it
338
364
  case 'help':
339
365
  case '--help':
340
366
  case '-h':
package/dist/mcp.js ADDED
@@ -0,0 +1,100 @@
1
+ // `agentpager mcp` — the same two things `notify` and `ask` do, as MCP tools, for the assistants
2
+ // that have no shell to run a command in (Claude Desktop, Cursor, …). A hand-rolled stdio JSON-RPC
3
+ // loop: the surface is three methods, and a dependency for that would be more code than this file.
4
+ import { createInterface } from 'node:readline';
5
+ import { callControl } from './control.js';
6
+ export const TOOLS = [
7
+ {
8
+ name: 'ping_phone',
9
+ description: "Send a short notification to the user's phone through AgentPager. Use it when the user asked to be pinged, on finishing a long task or when you are blocked.",
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: { message: { type: 'string', description: 'One line, under 100 characters.' } },
13
+ required: ['message'],
14
+ },
15
+ },
16
+ {
17
+ name: 'ask_phone',
18
+ description: "Ask the user a yes/no question on their phone and wait for the answer. Returns 'approved', 'denied' or 'no answer'. Treat 'no answer' as do not proceed.",
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ question: { type: 'string' },
23
+ timeout_seconds: { type: 'number', description: 'How long to wait. Default 600.' },
24
+ },
25
+ required: ['question'],
26
+ },
27
+ },
28
+ ];
29
+ const text = (t, isError = false) => ({ content: [{ type: 'text', text: t }], ...(isError ? { isError: true } : {}) });
30
+ export async function callTool(name, args, call = (r) => callControl(r)) {
31
+ try {
32
+ if (name === 'ping_phone') {
33
+ const message = String(args?.message ?? '').trim();
34
+ if (!message)
35
+ return text('message is required', true);
36
+ const res = await call({ kind: 'notify', title: 'Job finished', text: message, cwd: process.cwd() });
37
+ return res.phones ? text(`Sent to ${res.phone ?? 'the phone'}.`) : text('No phone is paired. Ask the user to run `agentpager pair`.', true);
38
+ }
39
+ if (name === 'ask_phone') {
40
+ const question = String(args?.question ?? '').trim();
41
+ if (!question)
42
+ return text('question is required', true);
43
+ const secs = Number(args?.timeout_seconds);
44
+ const res = await call({
45
+ kind: 'ask',
46
+ title: question.slice(0, 60),
47
+ text: question,
48
+ cwd: process.cwd(),
49
+ timeoutMs: Number.isFinite(secs) && secs > 0 ? secs * 1000 : undefined,
50
+ });
51
+ if (!res.phones)
52
+ return text('No phone is paired. Ask the user to run `agentpager pair`.', true);
53
+ return text(res.decision === 'allow' ? 'approved' : res.decision === 'deny' ? 'denied' : 'no answer');
54
+ }
55
+ return text(`unknown tool ${name}`, true);
56
+ }
57
+ catch (e) {
58
+ return text(String(e?.message ?? e), true);
59
+ }
60
+ }
61
+ /** One JSON-RPC message in, the reply out (or undefined for a notification). */
62
+ export async function handle(msg, call) {
63
+ const reply = (result) => ({ jsonrpc: '2.0', id: msg.id, result });
64
+ switch (msg.method) {
65
+ case 'initialize':
66
+ return reply({
67
+ protocolVersion: msg.params?.protocolVersion ?? '2024-11-05',
68
+ capabilities: { tools: {} },
69
+ serverInfo: { name: 'agentpager', version: '1' },
70
+ });
71
+ case 'tools/list':
72
+ return reply({ tools: TOOLS });
73
+ case 'tools/call':
74
+ return reply(await callTool(msg.params?.name, msg.params?.arguments, call));
75
+ case 'ping':
76
+ return reply({});
77
+ default:
78
+ if (msg.id === undefined)
79
+ return undefined; // notifications/initialized and friends
80
+ return { jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } };
81
+ }
82
+ }
83
+ export function serveMcp() {
84
+ const rl = createInterface({ input: process.stdin });
85
+ rl.on('line', async (line) => {
86
+ if (!line.trim())
87
+ return;
88
+ let msg;
89
+ try {
90
+ msg = JSON.parse(line);
91
+ }
92
+ catch {
93
+ return;
94
+ }
95
+ const out = await handle(msg);
96
+ if (out)
97
+ process.stdout.write(JSON.stringify(out) + '\n');
98
+ });
99
+ rl.on('close', () => process.exit(0));
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmovex/agentpager",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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": {