@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.
- package/dist/agentsetup.js +67 -0
- package/dist/cli.js +26 -0
- package/dist/mcp.js +100 -0
- package/package.json +1 -1
|
@@ -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