@paigy/mcp 0.40.0 → 0.40.2

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 CHANGED
@@ -116,14 +116,24 @@ Then pair: `PAIGY_AGENT=gemini npx -p @paigy/mcp@latest paigy-mcp-onboard`.
116
116
 
117
117
  ## Tools
118
118
 
119
- - **`pair`** — pair this agent with the user's Paigy account (one-time). No args to start: returns the code to show the user **and begins polling for approval in the background**; pass the returned `device_code` to collect the result (it returns the moment the user approves). On success it prompts you to allowlist Paigy's notify/await tools so they run without an approval prompt each time.
120
- - **`unpair`** — log this agent out of the user's Paigy account; revokes the token server-side and deletes the local one.
121
- - **`paigy-enable-tools`** (a CLI, *not* a tool) — allowlist Paigy's notify/await tools so they run without an approval prompt each time. `npx -y -p @paigy/mcp@latest paigy-enable-tools` (or `paigy-harness enable-tools`); `--scope project` limits it to the current repo instead of `~/.claude/settings.json`. Merges, never clobbers; leaves `pair`/`unpair` human-approved. **This is deliberately not an MCP tool.** It writes the calling agent's own permission allowlist, which every host worth trusting treats as privilege escalation — Claude Code's auto mode denies it outright, and the user's consent inside Paigy is invisible to the classifier making that call. `pair` returns the command for the agent to print; a human runs it.
122
- - **`contact`** — THE way to reach the user: tell them something, or ask and get their answer. Core form is two fields — `ask` (plain prose: what you need to tell them or find out) and `waiting` (what happens to your work meanwhile: `none` = just informing, `soft` = want an answer but can keep working, `hard` = stopped until answered — reaches them urgently and escalates to a real phone call). Paigy's broker picks the channel, phrasing, and answer format. When the choices themselves must be *seen*, attach `options` (each may carry a sandboxed `html` or `image` preview); attach `visuals` for context screenshots. Returns `{ notificationId, threadId }`; a threaded follow-up supersedes that thread's pending items, and an identical threaded re-send escalates in place. If a reply comes back as `{kind:'clarify', chunks:[...]}`, contact again on the SAME threadId with an expanded ask. (The former `notify_user`/`notify` names remain accepted as hidden aliases for older setups, and the fully-shaped wire form — context/select/urgency — is still accepted from code; neither is part of the model surface anymore.)
123
- - **Waiting is part of `contact`.** A contact that rings holds its first ~45 s window itself and returns the outcome in `wait` (`reply` / `partial` / `remind` / `idle`); to keep waiting on that call, call `contact` again with only `{ wait: notificationId }` — nothing is sent. A message delivery is never polled for; its reply arrives through `check_replies` or the wake. (There is no separate `await_reply` tool since 0.40.0.)
124
- - **`check_replies`** — catch-up sweep: returns replies you haven't consumed yet (now marked seen) plus still-pending notifications, new user-initiated requests, and owed callbacks.
125
- - **`set_task_state`** — report progress on a request: `in_progress` / `completed` / `needs_input`.
126
- - **`schedule_callback`** — promise the user a follow-up (`on_done` / `on_blocked` / `scheduled`) so it's not dropped if you go idle.
119
+ One catalog, on every transport (2026-09-11): the agent tools are `AGENT_TOOLS` in
120
+ `packages/schema/src/tools.ts` — contact, check_replies, get_thread, search_threads,
121
+ create_goal, claim_goal, get_goal, update_goal — and both this server and the hosted MCP
122
+ (`apps/api/src/mcp`) publish that list and dispatch it through the SDK's one `runTool`.
123
+ This server adds only onboard, pair and unpair, the tools that mint and delete the token
124
+ file this machine holds. See the [SDK contract](../../packages/sdk/README.md) for the
125
+ single-Goal start shape. Notification returns immediately. Call holds one cancellable
126
+ ~45-second window here (the hosted transport returns after one read); continue with
127
+ contact({deliveryId}) without sending another ask. Durable Entries and accepted answers can
128
+ repeat on reads; check_replies lists every open Delivery addressed to you (a request the
129
+ user started toward you, an answer relayed to something you asked, a handoff) without
130
+ consuming any of it, and claim_goal is the catch-up for your own Goals. Bookkeeping ids
131
+ (`operationId`, `idempotencyKey`) are minted here, never asked of the model. Retired
132
+ reply-lease/ACK/work/callback tools and hidden notification aliases are rejected.
133
+
134
+ The standalone listener below is still a **legacy host and target-release blocker**.
135
+ Its old reply intake must migrate before enabling it against the target runtime; it is
136
+ not a compatibility fallback for target MCP tools.
127
137
 
128
138
  ## Configuration
129
139
 
@@ -135,42 +145,48 @@ Then pair: `PAIGY_AGENT=gemini npx -p @paigy/mcp@latest paigy-mcp-onboard`.
135
145
  no extra setup. Two edges: with `HTTP_PROXY` set, an `http://localhost` backend
136
146
  proxies too unless `NO_PROXY=localhost` (hostname entries work; CIDR ranges are
137
147
  ignored), and `paigy-listen`'s realtime wake channel is a websocket that doesn't
138
- proxy — its REST sweeps do.
148
+ proxy — its REST reads do.
139
149
 
140
150
  ## Wake any harness (`paigy-listen`)
141
151
 
142
152
  `paigy-listen` is the self-hosted push daemon: it subscribes to this connection's
143
- wake channel and sweeps on every nudge. Keep it alive past the terminal with
144
- `paigy-listen --install` (launchd on macOS, systemd user unit on Linux; `--uninstall`
145
- removes it).
153
+ wake channel and reads what is waiting on every nudge. Keep it alive past the terminal
154
+ with `paigy-listen --install` (launchd on macOS, systemd user unit on Linux;
155
+ `--uninstall` removes it).
156
+
157
+ The read is `check_replies` — the open Deliveries addressed to this agent — and it
158
+ **consumes nothing**: no lease, no acknowledgement, and the same read twice returns the
159
+ same list. So the daemon hands the launcher what it found and stops there; claiming is
160
+ the agent's own first act (`claim_goal`), because the daemon shares this machine's
161
+ token with the agent it launches and a second claimer on one identity eats the first
162
+ one's claim.
146
163
 
147
164
  To launch an agent — any agent, not just Claude — when work arrives, set
148
- `PAIGY_ON_WAKE` to a command before `--install`. The sweep has already claimed the
149
- work, so the launcher reads it from the environment rather than calling
150
- `check_replies` again:
165
+ `PAIGY_ON_WAKE` to a command before `--install`:
151
166
 
152
167
  | Variable | What it holds |
153
168
  | --- | --- |
154
- | `PAIGY_WORK` | the whole swept payload as JSON (`replies`, `requests`, `owedCallbacks`, `stalled`, `threads`) |
155
- | `PAIGY_EVENT` | the wake that caused this run — `boot`, `wake:reply`, `cron:callback`… |
156
- | `PAIGY_THREAD_ID` | the thread to continue on |
157
- | `PAIGY_NOTIFICATION_ID` | the notification being answered or acted on (unset for an owed callback) |
158
- | `PAIGY_CONTEXT_THREAD_ID` | a past conversation the user seeded this with — `get_thread` it **first** |
159
- | `PAIGY_TEXT` | what the user said (or the callback note you owe them), in prose |
169
+ | `PAIGY_WORK` | everything the wake found, as JSON (`deliveries`, `goal`) |
170
+ | `PAIGY_EVENT` | the wake that caused this run — `boot`, `wake:reply`, `wake:request`… |
171
+ | `PAIGY_PARENT_ID` | the thread to continue on (`PAIGY_THREAD_ID` is the same value, for existing scripts) |
172
+ | `PAIGY_DELIVERY_ID` | the Delivery being acted on — `contact({deliveryId})` rereads it |
173
+ | `PAIGY_GOAL_ID` | the Goal it belongs to — `claim_goal` it **first** |
174
+ | `PAIGY_CONTEXT_THREAD_ID` | a past conversation this Goal names — `get_thread` it **first** |
175
+ | `PAIGY_TEXT` | what the person said, in prose |
160
176
 
161
177
  Hand `$PAIGY_WORK` to a harness that can read JSON and decide for itself; use the
162
178
  scalars for a plain shell launcher that shouldn't need `jq`. They describe **one**
163
- item — the oldest thread without a turn already in flight — because the queue rail's
164
- contract is one thread at a time. An absent fact is unset rather than empty, so
165
- `${PAIGY_CONTEXT_THREAD_ID:-}` distinguishes "no seed" from "seeded with nothing".
179
+ item — the oldest open Delivery — because the contract is one thread at a time. An
180
+ absent fact is unset rather than empty, so `${PAIGY_CONTEXT_THREAD_ID:-}`
181
+ distinguishes "no seed" from "seeded with nothing".
166
182
 
167
183
  ```sh
168
184
  # Claude Code — hand it everything and let it plan:
169
- PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — rehydrate threads you do not recognize via get_thread first."' \
185
+ PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — claim_goal first, and rehydrate threads you do not recognize via get_thread."' \
170
186
  npx -y -p @paigy/mcp paigy-listen --install
171
187
 
172
188
  # Codex (or any CLI harness) — the scalars are enough for a one-liner:
173
- PAIGY_ON_WAKE='codex exec "Continue Paigy thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. Call get_thread on it first if you do not recognize it, then reply with contact."' \
189
+ PAIGY_ON_WAKE='codex exec "Claim Paigy goal $PAIGY_GOAL_ID and continue thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. Reply with contact."' \
174
190
  npx -y -p @paigy/mcp paigy-listen --install
175
191
  ```
176
192
 
@@ -184,10 +200,7 @@ script and branch on `$PAIGY_EVENT` there:
184
200
  seed=""
185
201
  [ -n "${PAIGY_CONTEXT_THREAD_ID:-}" ] && seed="It continues thread $PAIGY_CONTEXT_THREAD_ID — get_thread that first."
186
202
 
187
- case "$PAIGY_EVENT" in
188
- cron:callback*) codex exec "You owe the user a callback on thread $PAIGY_THREAD_ID: $PAIGY_TEXT. Deliver it with contact." ;;
189
- *) codex exec "Paigy thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. $seed Reply with contact when done." ;;
190
- esac
203
+ codex exec "Paigy goal $PAIGY_GOAL_ID on thread $PAIGY_THREAD_ID. claim_goal it first. The user said: $PAIGY_TEXT. $seed Reply with contact when done."
191
204
  ```
192
205
 
193
206
  ## Statusline (Claude Code)
@@ -1,22 +1,11 @@
1
1
  import {
2
- CHECK_REPLIES_DESCRIPTION,
3
- CONTACT_DESCRIPTION,
4
- CONTACT_SCHEMA,
5
- CREATE_GOAL_DESCRIPTION,
6
- CreateGoalSchema,
7
- HandoffSchema,
8
- SCHEDULE_CALLBACK_DESCRIPTION,
9
- SEARCH_THREADS_DESCRIPTION,
10
- SET_WORK_STATE_DESCRIPTION,
11
- ScheduleCallbackSchema,
12
- SetTaskStateSchema,
13
- SetWorkStateSchema,
2
+ AGENT_TOOLS,
14
3
  mcpInputSchema,
15
4
  serverInstructions
16
- } from "./chunk-DM6YPBQH.js";
5
+ } from "./chunk-FFI3AIZE.js";
17
6
  import {
18
7
  AGENT_NAME
19
- } from "./chunk-CWGZVTPZ.js";
8
+ } from "./chunk-YJEJZT56.js";
20
9
 
21
10
  // src/toolset.ts
22
11
  var ONBOARD_DESCRIPTION = "Get this agent talking to Paigy \u2014 call it FIRST, before contact, and any time you're unsure who you are. One call, and it does whatever the situation needs: NOT SET UP \u2192 hatches an identity instantly if this machine holds a device credential (the user ran the Paigy desktop app or harness), otherwise starts the code ceremony; ALREADY SET UP \u2192 returns your current identity and offers the two things left to decide, renaming it or unpairing; TOKEN NO LONGER VALID \u2192 says so, then re-pairs. Pass { name, voice } to choose who you are when hatching, or to RENAME yourself when already set up (voices: rachel, george, jessica, brian, lily). Safe to call any time: idempotent, and it never writes settings \u2014 the tool-allowlist state it reports is read-only. If it returns a `user_code`, print it to the user immediately and call onboard again with the `device_code`. If it returns `enable_command`, PRINT that command for the user to run \u2014 you cannot apply it yourself (it writes your own permission allowlist, which hosts block as privilege escalation), so print it, don't wait for it, and carry on.";
@@ -24,8 +13,6 @@ var SERVER_INSTRUCTIONS = serverInstructions({ waits: true });
24
13
 
25
14
  // src/tools.ts
26
15
  import { z } from "zod";
27
- var UpdateGoalToolSchema = z.object({ goalId: z.string().uuid(), revision: z.number().int().positive(), changes: z.record(z.unknown()), reason: z.string().min(1) });
28
- var ClaimGoalSchema = z.object({ goalId: z.string().uuid() });
29
16
  var OnboardSchema = z.object({
30
17
  name: z.string().max(60).optional(),
31
18
  voice: z.string().max(40).optional(),
@@ -39,29 +26,7 @@ var PairSchema = z.object({
39
26
  name: z.string().min(1).max(60).optional().describe("Hatch path only: the name you choose for this identity. Pick your own \u2014 ONE or TWO words, the way you'd introduce yourself on a call (it is spoken aloud and shown in lists). 'Piper', 'Blue Heron' \u2014 never a sentence or a task description."),
40
27
  voice: z.string().optional().describe("Hatch path only: your voice on calls \u2014 one of rachel, george, jessica, brian, lily.")
41
28
  });
42
- var GetThreadSchema = z.object({
43
- parentId: z.string().describe("The thread to read \u2014 from a reply, request, or past notification.")
44
- });
45
- var SearchThreadsSchema = z.object({
46
- q: z.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
47
- });
48
- var SetTaskStateToolSchema = z.object({
49
- notificationId: z.string(),
50
- state: SetTaskStateSchema.shape.state
51
- });
52
- var AnswerCallerQuestionSchema = z.object({
53
- notificationId: z.string().describe("The notification whose call carried the caller's question \u2014 from the partial turn or the settled reply."),
54
- answer: z.string().min(1).max(1500).describe("The answer, as one or two short SPOKEN sentences \u2014 it may be read aloud on the live call.")
55
- });
56
- var TOOLS = [
57
- {
58
- // #575: THE attention verb — the one model-facing surface, every agent.
59
- // The retired names (notify/notify_user) stay callable as hidden aliases
60
- // for stale prompts and cached servers; see toolset.ts for the design.
61
- name: "contact",
62
- description: CONTACT_DESCRIPTION,
63
- inputSchema: CONTACT_SCHEMA
64
- },
29
+ var IDENTITY_TOOLS = [
65
30
  {
66
31
  // ONE rail for starting a session (#875). `pair`, the `paigy-mcp-onboard` CLI and
67
32
  // `enable_tools` were three doors into one flow, and "onboard" was the word people
@@ -79,55 +44,11 @@ var TOOLS = [
79
44
  name: "unpair",
80
45
  description: "Log out / unpair this agent from the user's Paigy account: revokes the token server-side (it stops working everywhere) and deletes the local ~/.paigy/token.json. Takes no arguments. After this, contact won't work until the user pairs again with the pair tool.",
81
46
  inputSchema: mcpInputSchema(z.object({}))
82
- },
83
- // `await_reply` is GONE (owner, 2026-09-06, hard cutover): the wait is contact's — it holds
84
- // the first window of a call it placed, and `contact({ wait: notificationId })` holds the
85
- // next. One tool, one loop; the reply-reading guidance moved into CONTACT_DESCRIPTION.
86
- {
87
- name: "check_replies",
88
- description: CHECK_REPLIES_DESCRIPTION,
89
- inputSchema: mcpInputSchema(z.object({}))
90
- },
91
- {
92
- name: "get_thread",
93
- description: "The chronological transcript of one Paigy conversation thread \u2014 every past ask, answer, and user request on it. Call this to REHYDRATE when you're resuming or being seeded: a check_replies request whose parentId you don't recognize means the user is continuing an old conversation with you, and one carrying a contextParentId means they want a past conversation (possibly with a DIFFERENT agent) as your starting context \u2014 in both cases call get_thread FIRST and read the turns as prior conversation you were part of, not as new input. Turns: { role:'agent', title, description[], answer }, { role:'user', text }, and context turns { role:'handoff'|'recap', title, description[] } \u2014 a handoff is a predecessor's brief for you; a recap SUMMARIZES everything before it (the transcript starts at the latest recap, so treat it as the base and the turns after it as what happened since). Oldest first, capped at the most recent 30.",
94
- inputSchema: mcpInputSchema(GetThreadSchema)
95
- },
96
- {
97
- name: "search_threads",
98
- description: SEARCH_THREADS_DESCRIPTION,
99
- inputSchema: mcpInputSchema(SearchThreadsSchema)
100
- },
101
- {
102
- name: "create_goal",
103
- description: CREATE_GOAL_DESCRIPTION,
104
- inputSchema: mcpInputSchema(CreateGoalSchema)
105
- },
106
- { name: "claim_goal", description: "Claim a ready Goal you own and begin work. Returns its active revision.", inputSchema: mcpInputSchema(ClaimGoalSchema) },
107
- { name: "update_goal", description: "Update an owned Goal at an exact revision; stale revisions are rejected.", inputSchema: mcpInputSchema(UpdateGoalToolSchema) },
108
- {
109
- name: "set_work_state",
110
- description: SET_WORK_STATE_DESCRIPTION,
111
- inputSchema: mcpInputSchema(SetWorkStateSchema)
112
- },
113
- {
114
- name: "answer_caller_question",
115
- description: "Answer a question the user asked DURING a live call, while they're still on it. When a partial turn or a settled reply carries a `question` intent aimed at you, answer it here immediately: if their call is still live, your answer is spoken to them on that same call (returns live: true). If the call already ended (live: false), send the answer as a threaded contact instead \u2014 never drop it. Short spoken sentences only; this may be read aloud.",
116
- inputSchema: mcpInputSchema(AnswerCallerQuestionSchema)
117
- },
118
- {
119
- name: "schedule_callback",
120
- description: SCHEDULE_CALLBACK_DESCRIPTION,
121
- inputSchema: mcpInputSchema(ScheduleCallbackSchema)
122
- },
123
- {
124
- name: "handoff",
125
- description: "Deposit your working context for a SUCCESSOR agent \u2014 what you did, what's left, links, gotchas \u2014 as one note on a thread ({ title, notes[] }). This does NOT ring the user or enter their inbox: it's context, not a question. The successor reads it back with get_thread. Pass `target` (a sibling connection's token id or agent name, SAME account only) to hand off DIRECTLY to that agent \u2014 the note is dispatched to it as a request it picks up. Pass `workId` to move that existing outcome and its pending replies to the successor without reminting it; a target is required in that form. Omit `target` to leave the thread for the user to hand off to an agent themselves in the app. Pass `parentId` to land the handoff on an existing conversation; omit it to mint a fresh thread. Returns { parentId }. Pass recap:true when the note SUMMARIZES the thread so far (for a successor OR for your own later session): a recap resets the rehydration window \u2014 get_thread returns the latest recap + only the turns after it. Write one whenever a thread has grown long and you're pausing, handing off, or nearing your context limit.",
126
- inputSchema: mcpInputSchema(HandoffSchema)
127
47
  }
128
48
  ];
49
+ var TOOLS = [...AGENT_TOOLS, ...IDENTITY_TOOLS];
129
50
  var TOOL_NAMES = TOOLS.map((t) => t.name);
130
- var HIDDEN_ALIASES = ["notify", "notify_user", "set_task_state"];
51
+ var HIDDEN_ALIASES = [];
131
52
 
132
53
  // src/clients.ts
133
54
  import { execFile } from "child_process";
@@ -290,14 +211,8 @@ function enablePaigyTools(scope = "user", cwd = process.cwd()) {
290
211
 
291
212
  export {
292
213
  SERVER_INSTRUCTIONS,
293
- UpdateGoalToolSchema,
294
- ClaimGoalSchema,
295
214
  OnboardSchema,
296
215
  PairSchema,
297
- GetThreadSchema,
298
- SearchThreadsSchema,
299
- SetTaskStateToolSchema,
300
- AnswerCallerQuestionSchema,
301
216
  TOOLS,
302
217
  openBrowser,
303
218
  autoConfigureClients,
@@ -10,7 +10,7 @@ import {
10
10
  saveToken,
11
11
  sleep,
12
12
  startE2ee
13
- } from "./chunk-CWGZVTPZ.js";
13
+ } from "./chunk-YJEJZT56.js";
14
14
 
15
15
  // src/identity.ts
16
16
  var CLIENT_LABELS = {