@paigy/mcp 0.8.2 → 0.8.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/README.md +21 -0
- package/dist/{chunk-BUELEUTV.js → chunk-FDJFLJHR.js} +17 -2
- package/dist/index.js +2 -2
- package/dist/listen.js +1 -1
- package/dist/statusline.js +66 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -102,6 +102,27 @@ Then pair: `npx -p @paigy/mcp@latest paigy-mcp-onboard`.
|
|
|
102
102
|
|
|
103
103
|
- `PAIGY_BACKEND_URL` — the Paigy API base (defaults to the hosted backend).
|
|
104
104
|
|
|
105
|
+
## Statusline (Claude Code)
|
|
106
|
+
|
|
107
|
+
`paigy-statusline` prints a one-line connection status — the account's session
|
|
108
|
+
mode and whether a phone is paired — for Claude Code's status bar:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
paigy: all calls · phone ✓
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Enable it in `~/.claude/settings.json`:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
"statusLine": { "type": "command", "command": "npx -y @paigy/mcp@latest paigy-statusline" }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It reads `GET /api/status` with the pairing token and caches the result in
|
|
121
|
+
`~/.paigy/status.json` for 60 seconds, so the bar's constant refreshes never
|
|
122
|
+
hammer the API; when offline it shows the last known state. `paigy: unpaired`
|
|
123
|
+
means there's no token (or the pairing was revoked) — run `/paigy-onboard` to
|
|
124
|
+
pair; `paigy: …` means the status isn't known yet (first run while offline).
|
|
125
|
+
|
|
105
126
|
## Publishing (maintainers)
|
|
106
127
|
|
|
107
128
|
Tool input schemas are generated from zod in `src/schema.ts` as draft-2020-12 JSON
|
|
@@ -162,7 +162,8 @@ var SnoozeRequestSchema = z.object({
|
|
|
162
162
|
var PushTokenSchema = z.object({
|
|
163
163
|
voipToken: z.string().min(1).optional(),
|
|
164
164
|
alertToken: z.string().min(1).optional(),
|
|
165
|
-
|
|
165
|
+
fcmToken: z.string().min(1).optional(),
|
|
166
|
+
platform: z.enum(["ios", "android"])
|
|
166
167
|
});
|
|
167
168
|
var MissedCallSchema = z.enum([
|
|
168
169
|
"retry_10m",
|
|
@@ -185,7 +186,14 @@ var UserSettingsSchema = z.object({
|
|
|
185
186
|
autoCallback: z.boolean(),
|
|
186
187
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
187
188
|
improveConsent: z.boolean(),
|
|
188
|
-
missedCall: MissedCallSchema.default("backoff_standard")
|
|
189
|
+
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
190
|
+
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
191
|
+
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
192
|
+
* synthesizes and transcribes locally — no audio or spoken text leaves it.
|
|
193
|
+
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
194
|
+
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
195
|
+
* write, 'hosted' on read (see store.ts). */
|
|
196
|
+
voiceMode: z.enum(["hosted", "on_device"]).optional()
|
|
189
197
|
});
|
|
190
198
|
var HistoryItemSchema = z.object({
|
|
191
199
|
id: z.string(),
|
|
@@ -236,6 +244,13 @@ var DeliveryConfigSchema = z.object({
|
|
|
236
244
|
* back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
|
|
237
245
|
realtime: z.object({ url: z.string(), anonKey: z.string() }).nullable()
|
|
238
246
|
});
|
|
247
|
+
var StatusSchema = z.object({
|
|
248
|
+
agent: z.string(),
|
|
249
|
+
nickname: z.string(),
|
|
250
|
+
sessionMode: z.enum(["default", "all_calls", "silent"]),
|
|
251
|
+
/** A phone is registered for push/ring (any push token on the account). */
|
|
252
|
+
phone: z.boolean()
|
|
253
|
+
});
|
|
239
254
|
var WakeNudgeSchema = z.object({
|
|
240
255
|
kind: z.enum(["reply", "request", "callback"]),
|
|
241
256
|
notificationId: z.string().optional(),
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
scheduleCallback,
|
|
12
12
|
setTaskState,
|
|
13
13
|
submitNotification
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-FDJFLJHR.js";
|
|
15
15
|
import {
|
|
16
16
|
deleteToken,
|
|
17
17
|
openBrowser,
|
|
@@ -99,7 +99,7 @@ var server = new Server(
|
|
|
99
99
|
{ name: "paigy", version: "0.0.0" },
|
|
100
100
|
{
|
|
101
101
|
capabilities: { tools: {} },
|
|
102
|
-
instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. To wait for the answer to something you just asked, call await_reply with that notification's id \u2014 it's scoped to that one request, so it never returns replies meant for other requests. Use check_replies again only when re-booting or after waiting a long time on something else. Never end a turn that still needs the user without notify_user + await_reply. When you need a decision or input, MATCH the answer shape to the question \u2014 don't default everything to free text, and don't reflexively make everything yes/no. Pick the best tool for the job: yes/no \u2192 select:'confirm'; approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve'; pick one of several \u2192 options + select:'one'; pick several / a subset \u2192 options + select:'many'; rank or prioritize \u2192 options + select:'rank'. Reserve open-ended free text only for answers that genuinely can't be structured (the user can always add free text on top of any shape). If a reply comes back as { kind: 'clarify', chunks: [...] }, the user wants more detail on those chunks \u2014 respond by calling notify_user again with the SAME threadId and an expanded description covering them. When urgency is 'call', remember the title + description are spoken aloud \u2014 write them short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle."
|
|
102
|
+
instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. To wait for the answer to something you just asked, call await_reply with that notification's id \u2014 it's scoped to that one request, so it never returns replies meant for other requests. Use check_replies again only when re-booting or after waiting a long time on something else. Never end a turn that still needs the user without notify_user + await_reply. When you need a decision or input, MATCH the answer shape to the question \u2014 don't default everything to free text, and don't reflexively make everything yes/no. Pick the best tool for the job: yes/no \u2192 select:'confirm'; approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve'; pick one of several \u2192 options + select:'one'; pick several / a subset \u2192 options + select:'many'; rank or prioritize \u2192 options + select:'rank'. Reserve open-ended free text only for answers that genuinely can't be structured (the user can always add free text on top of any shape). If a reply comes back as { kind: 'clarify', chunks: [...] }, the user wants more detail on those chunks \u2014 respond by calling notify_user again with the SAME threadId and an expanded description covering them. When urgency is 'call', remember the title + description are spoken aloud \u2014 write them short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle. If you're about to start a genuinely long-running or blocking piece of work \u2014 one where the user would otherwise sit and wait \u2014 mention ONCE, in passing, that you can text or call them when it's done or if you hit a blocker, instead of them needing to babysit the terminal. Don't offer this for quick tasks, and don't repeat the offer if they've already said yes or no earlier in the conversation. Escalate silence, don't just wait on it: if you notified at a lower urgency (inbox/push/banner) for something that's genuinely blocking real progress, call await_reply up to twice (~5 min each, ~10 min total) \u2014 if it's still idle after that AND the item is genuinely blocking, send a fresh notify_user on the SAME threadId at urgency:'call'. Skip this for anything that isn't truly blocking; a normal question can just sit in the inbox."
|
|
103
103
|
}
|
|
104
104
|
);
|
|
105
105
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
package/dist/listen.js
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readToken
|
|
4
|
+
} from "./chunk-AILC3H3U.js";
|
|
5
|
+
import {
|
|
6
|
+
BACKEND_URL
|
|
7
|
+
} from "./chunk-GU7C5H6L.js";
|
|
8
|
+
|
|
9
|
+
// src/statusline.ts
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var DIR = join(homedir(), ".paigy");
|
|
14
|
+
var CACHE = join(DIR, "status.json");
|
|
15
|
+
var TTL_MS = 6e4;
|
|
16
|
+
var MODE_LABEL = {
|
|
17
|
+
default: "default",
|
|
18
|
+
all_calls: "all calls",
|
|
19
|
+
silent: "silent"
|
|
20
|
+
};
|
|
21
|
+
function render(status) {
|
|
22
|
+
if (!status) return "paigy: unpaired";
|
|
23
|
+
return `paigy: ${MODE_LABEL[status.sessionMode]} \xB7 ${status.phone ? "phone \u2713" : "no phone"}`;
|
|
24
|
+
}
|
|
25
|
+
function readCache() {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(CACHE, "utf8"));
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function fetchStatus(token) {
|
|
33
|
+
const res = await fetch(`${BACKEND_URL}/api/status`, {
|
|
34
|
+
headers: { authorization: `Bearer ${token}` },
|
|
35
|
+
signal: AbortSignal.timeout(2e3)
|
|
36
|
+
// a statusline must render fast or not at all
|
|
37
|
+
});
|
|
38
|
+
if (res.status === 401) return null;
|
|
39
|
+
if (res.status === 404) {
|
|
40
|
+
if ((await res.text()).includes("not_found")) return null;
|
|
41
|
+
throw new Error("status 404");
|
|
42
|
+
}
|
|
43
|
+
if (!res.ok) throw new Error(`status ${res.status}`);
|
|
44
|
+
return await res.json();
|
|
45
|
+
}
|
|
46
|
+
async function main() {
|
|
47
|
+
const token = readToken();
|
|
48
|
+
if (!token) return render(null);
|
|
49
|
+
const cached = readCache();
|
|
50
|
+
if (cached && Date.now() - cached.at < TTL_MS) return render(cached.status);
|
|
51
|
+
try {
|
|
52
|
+
const status = await fetchStatus(token);
|
|
53
|
+
mkdirSync(DIR, { recursive: true });
|
|
54
|
+
writeFileSync(CACHE, JSON.stringify({ at: Date.now(), status }));
|
|
55
|
+
return render(status);
|
|
56
|
+
} catch {
|
|
57
|
+
return cached ? render(cached.status) : "paigy: \u2026";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/^file:\/\//, ""))) {
|
|
61
|
+
void main().then((line) => console.log(line));
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
main,
|
|
65
|
+
render
|
|
66
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paigy/mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Paigy MCP server — a voice inbox for your AI agents. Lets an agent notify a user and await their reply.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"mcp": "./dist/index.js",
|
|
9
9
|
"paigy-mcp": "./dist/index.js",
|
|
10
10
|
"paigy-mcp-onboard": "./dist/onboard.js",
|
|
11
|
-
"paigy-listen": "./dist/listen.js"
|
|
11
|
+
"paigy-listen": "./dist/listen.js",
|
|
12
|
+
"paigy-statusline": "./dist/statusline.js"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"dist"
|
|
@@ -35,8 +36,8 @@
|
|
|
35
36
|
"@paigy/schema": "0.0.0"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
|
-
"build": "tsup src/index.ts src/onboard.ts src/listen.ts --format esm --clean",
|
|
39
|
-
"dev": "tsup src/index.ts src/onboard.ts src/listen.ts --format esm --watch",
|
|
39
|
+
"build": "tsup src/index.ts src/onboard.ts src/listen.ts src/statusline.ts --format esm --clean",
|
|
40
|
+
"dev": "tsup src/index.ts src/onboard.ts src/listen.ts src/statusline.ts --format esm --watch",
|
|
40
41
|
"typecheck": "tsc --noEmit",
|
|
41
42
|
"test": "vitest run"
|
|
42
43
|
}
|