@paigy/mcp 0.40.26 → 0.40.27
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 +17 -14
- package/dist/{chunk-OVSAQW3K.js → chunk-4C6SDQ7A.js} +19 -33
- package/dist/{chunk-5MQ3J6S3.js → chunk-A5FYZK6P.js} +18 -36
- package/dist/{chunk-BLWC44CQ.js → chunk-UQDA7X3W.js} +3 -3
- package/dist/{chunk-7LQKX73K.js → chunk-X3GTDW5G.js} +11 -14
- package/dist/{dist-WGFUXFS4.js → dist-N6ZJUJV7.js} +3 -5
- package/dist/enable.js +5 -5
- package/dist/index.js +46 -51
- package/dist/listen.js +4 -4
- package/dist/onboard.js +6 -6
- package/dist/slot.js +1 -1
- package/dist/stalled.js +1 -1
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ args = ["-y", "@paigy/mcp@latest"]
|
|
|
84
84
|
env = { PAIGY_AGENT = "codex" }
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
Then pair: `PAIGY_AGENT=codex npx -p @paigy/mcp@latest paigy-mcp-onboard` (or have the agent call the `
|
|
87
|
+
Then pair: `PAIGY_AGENT=codex npx -p @paigy/mcp@latest paigy-mcp-onboard` (or have the agent call the `configure` tool — it pairs under its own name automatically).
|
|
88
88
|
|
|
89
89
|
### Gemini CLI
|
|
90
90
|
|
|
@@ -117,11 +117,12 @@ Then pair: `PAIGY_AGENT=gemini npx -p @paigy/mcp@latest paigy-mcp-onboard`.
|
|
|
117
117
|
## Tools
|
|
118
118
|
|
|
119
119
|
One catalog, on every transport (2026-09-11): the agent tools are `AGENT_TOOLS` in
|
|
120
|
-
`packages/schema/src/tools.ts` —
|
|
121
|
-
|
|
120
|
+
`packages/schema/src/tools.ts` — contact, manage_goals, get_goal, search, check_activity,
|
|
121
|
+
send_feedback — and both this server and the hosted MCP
|
|
122
122
|
(`apps/api/src/mcp`) publish that list and dispatch it through the SDK's one `runTool`.
|
|
123
|
-
This server adds only
|
|
124
|
-
file this machine holds, and listen.
|
|
123
|
+
This server adds only `configure`, the one identity tool, which pairs, reports who you are,
|
|
124
|
+
renames and logs this agent out (`logOut: true`) over the token file this machine holds, and listen.
|
|
125
|
+
`onboard` is now `configure`; `pair` and `unpair` are folded into it (2026-10-07). See the [SDK contract](../../packages/sdk/README.md) for the
|
|
125
126
|
contact shape. Sending returns immediately unless it asks to `wait`: `wait: true` holds one
|
|
126
127
|
cancellable ~45-second window here for a response to what was sent (the hosted transport
|
|
127
128
|
returns after one read), and `contact({})` holds one for anything addressed to you; after an
|
|
@@ -134,8 +135,10 @@ acknowledged with `ackEventIds`, and a Goal is postponed with manage_goals `defe
|
|
|
134
135
|
when its time comes). Durable Entries and accepted answers can
|
|
135
136
|
repeat on reads; contact with nothing to send receives what is addressed to you (a request the
|
|
136
137
|
user started toward you, an answer relayed to something you asked, a handoff, a question you owe) without
|
|
137
|
-
consuming any of it until you acknowledge it (`ackEventIds`), and
|
|
138
|
-
|
|
138
|
+
consuming any of it until you acknowledge it (`ackEventIds`), and get_goal reads a Goal.
|
|
139
|
+
`claim_goal` is gone (2026-10-07): you are on a Goal from your first write to it (a contact
|
|
140
|
+
naming it, or a manage_goals change), never from a read, so there is nothing to claim or join;
|
|
141
|
+
`who_is_working` is now `check_activity`. manage_goals creates, edits, assigns, organizes and closes work, a change at a
|
|
139
142
|
time. Bookkeeping (`operationId`, the manage request id, the revision an edit was read at) is
|
|
140
143
|
minted here, never asked of the model. Retired
|
|
141
144
|
reply-lease/ACK/work/callback tools and hidden notification aliases are rejected.
|
|
@@ -165,10 +168,10 @@ with `paigy-listen --install` (launchd on macOS, systemd user unit on Linux;
|
|
|
165
168
|
|
|
166
169
|
The read is `receive` (behind `contact({})`) — the open Deliveries addressed to this agent — and it
|
|
167
170
|
**consumes nothing**: no lease, no acknowledgement, and the same read twice returns the
|
|
168
|
-
same list. So the daemon hands the launcher what it found and stops there;
|
|
169
|
-
|
|
170
|
-
token with the agent it launches
|
|
171
|
-
|
|
171
|
+
same list. So the daemon hands the launcher what it found and stops there; it never
|
|
172
|
+
writes, because an agent is on a Goal from its first write and the daemon shares this
|
|
173
|
+
machine's token with the agent it launches: a write here would put that agent on work it
|
|
174
|
+
never started.
|
|
172
175
|
|
|
173
176
|
To launch an agent — any agent, not just Claude — when work arrives, set
|
|
174
177
|
`PAIGY_ON_WAKE` to a command before `--install`:
|
|
@@ -179,7 +182,7 @@ To launch an agent — any agent, not just Claude — when work arrives, set
|
|
|
179
182
|
| `PAIGY_EVENT` | the wake that caused this run — `boot`, `wake:reply`, `wake:request`… |
|
|
180
183
|
| `PAIGY_PARENT_ID` | the thread to continue on (`PAIGY_THREAD_ID` is the same value, for existing scripts) |
|
|
181
184
|
| `PAIGY_DELIVERY_ID` | the Delivery being acted on (`get_goal` on `PAIGY_GOAL_ID` reads its conversation) |
|
|
182
|
-
| `PAIGY_GOAL_ID` | the Goal it belongs to — `
|
|
185
|
+
| `PAIGY_GOAL_ID` | the Goal it belongs to — read it with `get_goal` **first** |
|
|
183
186
|
| `PAIGY_TEXT` | what the person said, in prose |
|
|
184
187
|
|
|
185
188
|
Hand `$PAIGY_WORK` to a harness that can read JSON and decide for itself; use the
|
|
@@ -190,7 +193,7 @@ Goal" from "an empty id".
|
|
|
190
193
|
|
|
191
194
|
```sh
|
|
192
195
|
# Claude Code — hand it everything and let it plan:
|
|
193
|
-
PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK —
|
|
196
|
+
PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — read each Goal it names with get_goal first."' \
|
|
194
197
|
npx -y -p @paigy/mcp paigy-listen --install
|
|
195
198
|
|
|
196
199
|
# Codex terminal session: no custom script and no new conversation.
|
|
@@ -234,7 +237,7 @@ script and branch on `$PAIGY_EVENT` there:
|
|
|
234
237
|
```sh
|
|
235
238
|
#!/bin/sh
|
|
236
239
|
# ~/.paigy/on-wake.sh — chmod +x, then PAIGY_ON_WAKE=~/.paigy/on-wake.sh
|
|
237
|
-
codex exec "Paigy goal $PAIGY_GOAL_ID.
|
|
240
|
+
codex exec "Paigy goal $PAIGY_GOAL_ID. Read it with get_goal first. The user said: $PAIGY_TEXT. Reply with contact when done."
|
|
238
241
|
```
|
|
239
242
|
|
|
240
243
|
## Stalled work (turn-end hook — Claude Code, Codex, Gemini CLI, Antigravity)
|
|
@@ -1689,7 +1689,7 @@ CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an ans
|
|
|
1689
1689
|
|
|
1690
1690
|
UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first (manage_goals), then send one update saying what is done and anything they need to do or check. On a Goal whose report card is still open, an update that reaches them is added to that card, with no new push. If the person is already on a call, anything that reaches them joins that call, with no ring.
|
|
1691
1691
|
|
|
1692
|
-
RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goal); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`;
|
|
1692
|
+
RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goal); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
|
|
1693
1693
|
var CreateGoalSchema = z3.object({
|
|
1694
1694
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
1695
1695
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -1735,7 +1735,7 @@ var CreateChange = z3.object({
|
|
|
1735
1735
|
kind: z3.literal("create"),
|
|
1736
1736
|
title: goalTitle,
|
|
1737
1737
|
outcome: goalOutcome,
|
|
1738
|
-
ownerId: z3.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as
|
|
1738
|
+
ownerId: z3.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as check_activity shows it) or the person's. Omitted: you."),
|
|
1739
1739
|
parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
|
|
1740
1740
|
sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
|
|
1741
1741
|
}).strict();
|
|
@@ -1780,8 +1780,7 @@ var ManageGoalsSchema = z3.object({
|
|
|
1780
1780
|
var ManageGoalsToolSchema = z3.object({
|
|
1781
1781
|
changes: z3.array(z3.discriminatedUnion("kind", [CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
|
|
1782
1782
|
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
1783
|
-
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open,
|
|
1784
|
-
var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
|
|
1783
|
+
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls; edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
|
|
1785
1784
|
var GetGoalSchema = z3.object({
|
|
1786
1785
|
goalId: z3.string().uuid(),
|
|
1787
1786
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
@@ -1792,8 +1791,7 @@ var GetGoalSchema = z3.object({
|
|
|
1792
1791
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
1793
1792
|
diagnose: z3.boolean().optional()
|
|
1794
1793
|
}).strict();
|
|
1795
|
-
var GET_GOAL_DESCRIPTION = "Read one Goal
|
|
1796
|
-
var CLAIM_GOAL_DESCRIPTION = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change it; use manage_goals (assign) when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
|
|
1794
|
+
var GET_GOAL_DESCRIPTION = "Read one Goal (a read never puts you on it), including others (the ten most recent other contributors, with names, latest entry headlines and times): its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
1797
1795
|
var SearchToolSchema = z3.object({
|
|
1798
1796
|
query: z3.string().trim().min(1).max(500),
|
|
1799
1797
|
types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
@@ -1801,7 +1799,7 @@ var SearchToolSchema = z3.object({
|
|
|
1801
1799
|
limit: z3.number().int().min(1).max(20).optional()
|
|
1802
1800
|
}).strict();
|
|
1803
1801
|
var SEARCH_DESCRIPTION = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and Answers (found by their Question or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, its Questions' Answers, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (an Answer carries its Question, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
|
|
1804
|
-
var
|
|
1802
|
+
var CheckActivitySchema = z3.object({}).strict();
|
|
1805
1803
|
var FEEDBACK_TEXT_MAX = 5e4;
|
|
1806
1804
|
var SendFeedbackSchema = z3.object({
|
|
1807
1805
|
text: z3.string().trim().min(1).max(FEEDBACK_TEXT_MAX).describe(`The report, in markdown: what happened, what was expected, how to reproduce it, versions, evidence. At most ${FEEDBACK_TEXT_MAX} characters.`),
|
|
@@ -1810,12 +1808,11 @@ var SendFeedbackSchema = z3.object({
|
|
|
1810
1808
|
}).strict();
|
|
1811
1809
|
var SEND_FEEDBACK_DESCRIPTION = "Send feedback about Paigy itself (a bug report, an idea) straight to the Paigy team. Send it only when the person asked you to, or agreed when you offered: it goes to Paigy, never to the person, and nobody answers it here. Not for your work or a question for the person: use contact for those. Returns whether it was stored.";
|
|
1812
1810
|
var AGENT_TOOLS = [
|
|
1813
|
-
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(WhoIsWorkingSchema) },
|
|
1814
1811
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
1815
1812
|
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
1816
|
-
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
1817
1813
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
1818
1814
|
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
|
|
1815
|
+
{ name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckActivitySchema) },
|
|
1819
1816
|
{ name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
|
|
1820
1817
|
];
|
|
1821
1818
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
@@ -3057,7 +3054,7 @@ var HostDecisionSchema = z7.object({
|
|
|
3057
3054
|
/** The agent's token id: the row's `recipient`. */
|
|
3058
3055
|
agent: z7.string().uuid(),
|
|
3059
3056
|
decision: z7.enum(["stood_back", "took_over"]),
|
|
3060
|
-
/** The work it was about: the Goal
|
|
3057
|
+
/** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
|
|
3061
3058
|
goalId: z7.string().uuid().nullable().optional(),
|
|
3062
3059
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
3063
3060
|
seenAt: z7.string().datetime().nullable().optional(),
|
|
@@ -3438,12 +3435,6 @@ async function manageGoals(input, opts = {}) {
|
|
|
3438
3435
|
if (!res.ok) await fail("manage_goals", res);
|
|
3439
3436
|
return await res.json();
|
|
3440
3437
|
}
|
|
3441
|
-
async function claimGoal(goalId, opts = {}) {
|
|
3442
|
-
const token = authToken(opts.token) ?? "";
|
|
3443
|
-
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify(goalId ? { goalId } : {}) }));
|
|
3444
|
-
if (!res.ok) await fail("claim_goal", res);
|
|
3445
|
-
return await res.json();
|
|
3446
|
-
}
|
|
3447
3438
|
async function getGoal(goalId, opts = {}, read3 = {}) {
|
|
3448
3439
|
const token = authToken(opts.token) ?? "";
|
|
3449
3440
|
const query = [read3.history ? "history=1" : "", read3.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
|
|
@@ -3622,11 +3613,11 @@ async function sendFeedback(input, opts = {}) {
|
|
|
3622
3613
|
if (!res.ok) await fail("send_feedback", res);
|
|
3623
3614
|
return await res.json();
|
|
3624
3615
|
}
|
|
3625
|
-
async function
|
|
3616
|
+
async function checkActivity(opts = {}) {
|
|
3626
3617
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/agents/working`, {
|
|
3627
3618
|
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` }
|
|
3628
3619
|
}));
|
|
3629
|
-
if (!res.ok) await fail("
|
|
3620
|
+
if (!res.ok) await fail("check_activity", res);
|
|
3630
3621
|
return await res.json();
|
|
3631
3622
|
}
|
|
3632
3623
|
var NOT_SENT = `Recorded as the Goal's progress, not sent: nobody was notified. The person is told only a question, an answer to one they asked you, and what you say once the Goal is done. To close the work, mark the Goal done first, then send the one update saying what is done and anything they need to do or check. If the person asked you, in so many words, to send them this, send it again with userExplicitlyRequested: "any" on that update.`;
|
|
@@ -3905,7 +3896,7 @@ function deliveryView(d) {
|
|
|
3905
3896
|
acknowledged: d.acknowledged?.filter((a) => a.acknowledged).map((a) => a.eventId),
|
|
3906
3897
|
next: [
|
|
3907
3898
|
failures(d.results),
|
|
3908
|
-
d.joinedCall ? `${JOINED_CALL}${d.message ? ` ${d.message}` : ""}` : d.kind === "notification" ? `${d.demoted ? `${d.demoted} ` : ""}${d.joinedCard ? `Added to the open report card on its Goal; no new push was sent. ${answers}` : answers}` : pending ? `${d.demoted ? `${d.demoted} ` : ""}Decision pending on this call. ${d.waitOutcome === "expired" ? "Nothing was said in the window. " : ""}Wait for the answer with contact({wait:true}) \u2014 one bounded window each time, and never resend the question. When a window comes back with nothing new, they are not typing: stop waiting, leave the question open, and collect the answer with contact({wait:false}) or
|
|
3899
|
+
d.joinedCall ? `${JOINED_CALL}${d.message ? ` ${d.message}` : ""}` : d.kind === "notification" ? `${d.demoted ? `${d.demoted} ` : ""}${d.joinedCard ? `Added to the open report card on its Goal; no new push was sent. ${answers}` : answers}` : pending ? `${d.demoted ? `${d.demoted} ` : ""}Decision pending on this call. ${d.waitOutcome === "expired" ? "Nothing was said in the window. " : ""}Wait for the answer with contact({wait:true}) \u2014 one bounded window each time, and never resend the question. When a window comes back with nothing new, they are not typing: stop waiting, leave the question open, and collect the answer with contact({wait:false}) or get_goal on your next wake. ${d.message ?? ""}`.trim() : d.message
|
|
3909
3900
|
].filter(Boolean).join(" ")
|
|
3910
3901
|
});
|
|
3911
3902
|
}
|
|
@@ -3942,8 +3933,8 @@ function receivedView(r, now = Date.now()) {
|
|
|
3942
3933
|
notAcknowledged: refused.length ? refused.map((a) => ({ eventId: a.eventId, refused: a.refused })) : void 0,
|
|
3943
3934
|
// WORK ASSIGNED TO YOU (owner, 2026-09-24): your Goals nobody has started, however they became yours.
|
|
3944
3935
|
assigned,
|
|
3945
|
-
//
|
|
3946
|
-
// sleeping agent, so "nothing is waiting" is never said while
|
|
3936
|
+
// THE WORK WAITING ON YOU NEXT (owner, 2026-09-27: "Wake on any work"): the predicate that starts a
|
|
3937
|
+
// sleeping agent, so "nothing is waiting" is never said while work is.
|
|
3947
3938
|
claimable: w.claimable,
|
|
3948
3939
|
// YOUR STALLED WORK (#2257), and OTHER AGENTS' (owner, 2026-09-23): any agent may take it over.
|
|
3949
3940
|
stalled,
|
|
@@ -3953,10 +3944,10 @@ function receivedView(r, now = Date.now()) {
|
|
|
3953
3944
|
...owed.length ? [`You owe ${owed.length === 1 ? "an answer" : `${owed.length} answers`}: contact({ answers: [{ questionId: "<questionId>", answer: { text: <your answer> } }] }) for each question; a question is answered, never acknowledged.`] : [],
|
|
3954
3945
|
...r.hasMore ? ["More are waiting: acknowledge these, then receive again."] : [],
|
|
3955
3946
|
...r.waitOutcome === "expired" ? ["Nothing arrived in the window; receive again to keep waiting, without sending again."] : [],
|
|
3956
|
-
...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned):
|
|
3957
|
-
...w.claimable && !assigned.length ? [`
|
|
3947
|
+
...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned): read one with get_goal({goalId}); your first write to it starts it.`] : [],
|
|
3948
|
+
...w.claimable && !assigned.length ? [`Next waiting on you: ${w.claimable.title ?? w.claimable.goalId} (get_goal({ goalId: "${w.claimable.goalId}" })).`] : [],
|
|
3958
3949
|
...stalled.length ? [`${stalled.length} of your Goals have had no progress${stalled.length === 1 ? " " : ""}${quietFor(stalled, now)}: report on each with a contact update, or finish or cancel it with manage_goals.`] : [],
|
|
3959
|
-
...others.length ? [`${others.length} of other agents' Goals have gone quiet${others.length === 1 ? " " : ""}${quietFor(others, now)}:
|
|
3950
|
+
...others.length ? [`${others.length} of other agents' Goals have gone quiet${others.length === 1 ? " " : ""}${quietFor(others, now)}: read one with get_goal({goalId}); a write to it puts you on it without changing its owner.`] : [],
|
|
3960
3951
|
// No name of its own (#2525): the API's ask, verbatim.
|
|
3961
3952
|
...w.unnamed ? [w.unnamed] : []
|
|
3962
3953
|
].join(" ")
|
|
@@ -3993,9 +3984,9 @@ async function runTool(name, args, opts) {
|
|
|
3993
3984
|
const sent = await contact(parsed, { ...client, signal, waits });
|
|
3994
3985
|
return "sent" in sent ? notSentView(sent) : deliveryView(sent);
|
|
3995
3986
|
}
|
|
3996
|
-
case "
|
|
3997
|
-
|
|
3998
|
-
return
|
|
3987
|
+
case "check_activity":
|
|
3988
|
+
CheckActivitySchema.parse(input);
|
|
3989
|
+
return checkActivity(client);
|
|
3999
3990
|
case "send_feedback":
|
|
4000
3991
|
return sendFeedback(SendFeedbackSchema.parse(input), client);
|
|
4001
3992
|
case "manage_goals": {
|
|
@@ -4009,10 +4000,6 @@ async function runTool(name, args, opts) {
|
|
|
4009
4000
|
for (const g of managed.goals ?? []) remember(client.token, g.goalId, g.revision);
|
|
4010
4001
|
return managedView(managed, changes);
|
|
4011
4002
|
}
|
|
4012
|
-
case "claim_goal": {
|
|
4013
|
-
const { goalId } = ClaimGoalSchema.parse(input);
|
|
4014
|
-
return goalView(read2(client.token, await claimGoal(goalId, client)));
|
|
4015
|
-
}
|
|
4016
4003
|
case "get_goal": {
|
|
4017
4004
|
const { goalId, history, diagnose } = GetGoalSchema.parse(input);
|
|
4018
4005
|
const goal = read2(client.token, await getGoal(goalId, client, { history, diagnose }));
|
|
@@ -4287,7 +4274,6 @@ export {
|
|
|
4287
4274
|
overrideToken,
|
|
4288
4275
|
authToken,
|
|
4289
4276
|
manageGoals,
|
|
4290
|
-
claimGoal,
|
|
4291
4277
|
getGoal,
|
|
4292
4278
|
AWAIT_WINDOW_MS,
|
|
4293
4279
|
hatch,
|
|
@@ -4307,7 +4293,7 @@ export {
|
|
|
4307
4293
|
acceptTriage,
|
|
4308
4294
|
dismissTriage,
|
|
4309
4295
|
searchRecords,
|
|
4310
|
-
|
|
4296
|
+
checkActivity,
|
|
4311
4297
|
readDelivery,
|
|
4312
4298
|
contact,
|
|
4313
4299
|
receive,
|
|
@@ -4,54 +4,38 @@ import {
|
|
|
4
4
|
serverInstructions,
|
|
5
5
|
sessionStartHook,
|
|
6
6
|
withSessionStartHook
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-X3GTDW5G.js";
|
|
8
8
|
import {
|
|
9
9
|
CODEX_ENV,
|
|
10
10
|
agentName,
|
|
11
11
|
withCodexEnv
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-4C6SDQ7A.js";
|
|
13
13
|
|
|
14
14
|
// src/toolset.ts
|
|
15
|
-
var
|
|
15
|
+
var CONFIGURE_DESCRIPTION = "Set this agent up with Paigy \u2014 call it FIRST, before contact, and any time you're unsure who you are. One call does whatever the situation needs: NOT SET UP \u2192 if this machine holds a device credential (the user ran the Paigy desktop app or harness) it hatches an identity INSTANTLY, no code and no approval; otherwise it starts the code ceremony. ALREADY SET UP \u2192 returns your current identity and the two things left to decide, renaming it or logging out. 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 (pick your own; 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.\n\nTHE CODE CEREMONY does not open a browser; the user enters the code in the Paigy app (or scans `qr`). A result with { user_code, device_code, qr, user_message } has already started polling for approval in the background: you MUST immediately print the `user_message` (the bare code) to the user, AND in that same turn call configure again with that `device_code` to collect the result. It returns the moment the user approves; on { status:'pending' } call again to keep waiting; on { status:'awaiting_confirmation' } (E2EE) show the bare `user_message` verify code and call again to finish. The leading text block of every result states the code plainly, so it shows even if you emit no prose.\n\nOn { status:'paired' }, or whenever it returns `enable_command`, ALWAYS follow the `enable_prompt`: PRINT that command for the user to run, so Paigy's tools stop prompting each time. 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.\n\nLOG OUT with { logOut: true }: it logs out THIS agent only \u2014 its token is revoked server-side and its local entry removed. Other agents and this device's credential are untouched, and a later configure re-hatches instantly while the device credential exists. Disconnecting the device itself is not an agent tool.";
|
|
16
16
|
var SERVER_INSTRUCTIONS = serverInstructions({ waits: true });
|
|
17
17
|
|
|
18
18
|
// src/tools.ts
|
|
19
19
|
import { z } from "zod";
|
|
20
|
-
var
|
|
21
|
-
name: z.string().max(60).optional(),
|
|
22
|
-
voice: z.string().max(40).optional(),
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
var PairSchema = z.object({
|
|
29
|
-
device_code: z.string().optional().describe("Omit to start pairing (returns an approval link to show the user). Pass the device_code from that first call to finish, once the user has approved."),
|
|
30
|
-
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."),
|
|
31
|
-
voice: z.string().optional().describe("Hatch path only: your voice on calls \u2014 one of rachel, george, jessica, brian, lily.")
|
|
20
|
+
var ConfigureSchema = z.object({
|
|
21
|
+
name: z.string().min(1).max(60).optional().describe("Who you are: the name you choose when hatching, or the new one to RENAME yourself to when already set up. 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."),
|
|
22
|
+
voice: z.string().max(40).optional().describe("Your voice on calls \u2014 one of rachel, george, jessica, brian, lily."),
|
|
23
|
+
device_code: z.string().optional().describe("Continue a code ceremony already in flight: the device_code a previous configure returned. Omit it otherwise."),
|
|
24
|
+
/** Which allowlist to REPORT on (never written). */
|
|
25
|
+
scope: z.enum(["user", "project"]).optional().describe("Which tool allowlist to report on (user or project settings). Read-only."),
|
|
26
|
+
logOut: z.boolean().optional().describe("true: log THIS agent out \u2014 its token is revoked and its local entry removed. Other agents and this device's credential are untouched.")
|
|
32
27
|
});
|
|
33
28
|
var IDENTITY_TOOLS = [
|
|
34
29
|
{
|
|
35
|
-
// ONE rail for starting a session (#875)
|
|
36
|
-
// `
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
inputSchema: mcpInputSchema(OnboardSchema)
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: "pair",
|
|
44
|
-
description: "Pair this agent with the user's Paigy account (one-time) \u2014 required before contact work. FAST PATH: if this machine already holds a device credential (the user ran the Paigy desktop harness or app), calling pair hatches a fresh identity INSTANTLY \u2014 no code, no approval. Pass { name, voice } to choose who you are (pick your own; voices: rachel, george, jessica, brian, lily). Only when no device credential exists does the code ceremony below run. It does NOT open a browser; the user enters the code in the Paigy app (or scans `qr`). Step 1: call with NO args \u2014 returns { user_code, device_code, qr, user_message } AND starts polling for approval in the background. REQUIRED: You MUST immediately print the `user_message` (the bare code) as a text message to the user, AND in that same turn call step 2 (pair with the device_code). This ensures the user sees the code in chat while the tool blocks/polls in the background for approval. Step 2: call with that device_code to collect the result. Because approval is already being polled in the background, this returns the moment the user approves; on { status:'pending' } just call again to keep waiting; on { status:'awaiting_confirmation' } (E2EE) show the bare `user_message` verify code and call again to finish. The leading text block of every result states the code plainly, so it shows even if you emit no prose. On { status:'paired' } ALWAYS follow the `enable_prompt` \u2014 PRINT the returned `enable_command` so the user can allowlist Paigy's tools and notify/await stop prompting each time. Printing is the whole job: that command writes your own permission allowlist, so you must not run it and a host will block you if you try.",
|
|
45
|
-
inputSchema: mcpInputSchema(PairSchema)
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: "unpair",
|
|
49
|
-
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.",
|
|
50
|
-
inputSchema: mcpInputSchema(z.object({}))
|
|
30
|
+
// ONE rail for starting a session (#875), and since 2026-10-07 the only identity tool: `onboard`
|
|
31
|
+
// renamed, and `pair` (which it already did everything of) and `unpair` (`logOut`) folded in.
|
|
32
|
+
name: "configure",
|
|
33
|
+
description: CONFIGURE_DESCRIPTION,
|
|
34
|
+
inputSchema: mcpInputSchema(ConfigureSchema)
|
|
51
35
|
},
|
|
52
36
|
{
|
|
53
37
|
name: "listen",
|
|
54
|
-
description: "Am I listening for answers, and can Paigy resume this session? Takes no arguments. Returns status `listening` for harness delivery or a running listener with a ready wake adapter; `receiving` means a process is running but automatic resume is not confirmed (read its wake error or watch its output); `unpaired` means call
|
|
38
|
+
description: "Am I listening for answers, and can Paigy resume this session? Takes no arguments. Returns status `listening` for harness delivery or a running listener with a ready wake adapter; `receiving` means a process is running but automatic resume is not confirmed (read its wake error or watch its output); `unpaired` means call configure; `start` gives the exact command to run NOW as a watched background task for this session. Start it once. Codex queues into CODEX_THREAD_ID automatically when its CLI supports queue; PAIGY_ON_WAKE overrides it. A successful queue is not evidence the agent handled the answer: receive with contact({ wait: false }) and read the relevant Goal, then respond with contact. This tool never subscribes or starts anything itself.",
|
|
55
39
|
inputSchema: mcpInputSchema(z.object({}))
|
|
56
40
|
}
|
|
57
41
|
];
|
|
@@ -218,9 +202,8 @@ function claudeInstallHint(skip = agentName()) {
|
|
|
218
202
|
return "Claude Code detected \u2014 to add Paigy there, run /plugin marketplace add paigy-ai/mcp then /plugin install paigy (pairing carries over; no need to pair again). Note: If you run this inside a live session, type /reload-plugins afterward so the agent connects to the new tools.";
|
|
219
203
|
}
|
|
220
204
|
var ENABLE_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
|
|
221
|
-
var PROMPTS_ON_PURPOSE = /* @__PURE__ */ new Set(["pair", "unpair"]);
|
|
222
205
|
var PAIGY_TOOL_IDS = [
|
|
223
|
-
...TOOL_NAMES
|
|
206
|
+
...TOOL_NAMES,
|
|
224
207
|
...HIDDEN_ALIASES
|
|
225
208
|
].map((n) => `mcp__paigy__${n}`);
|
|
226
209
|
function withPaigyAllowlist(existing, tools) {
|
|
@@ -332,8 +315,7 @@ function installSessionListening() {
|
|
|
332
315
|
|
|
333
316
|
export {
|
|
334
317
|
SERVER_INSTRUCTIONS,
|
|
335
|
-
|
|
336
|
-
PairSchema,
|
|
318
|
+
ConfigureSchema,
|
|
337
319
|
TOOLS,
|
|
338
320
|
openBrowser,
|
|
339
321
|
autoConfigureClients,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
saveToken,
|
|
7
7
|
setIdentity,
|
|
8
8
|
sleep
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-4C6SDQ7A.js";
|
|
10
10
|
|
|
11
11
|
// src/identity.ts
|
|
12
12
|
var CLIENT_LABELS = {
|
|
@@ -34,12 +34,12 @@ async function identify(who, ask) {
|
|
|
34
34
|
who = await setIdentity({ ...ask.name ? { name: ask.name } : {}, ...ask.voice ? { voice: ask.voice } : {} });
|
|
35
35
|
renamed = true;
|
|
36
36
|
} catch (e) {
|
|
37
|
-
const message2 = e instanceof NameTakenError ? e.message : `The rename did not go through (${e.message}); nothing changed. Call
|
|
37
|
+
const message2 = e instanceof NameTakenError ? e.message : `The rename did not go through (${e.message}); nothing changed. Call configure again to retry, or carry on.`;
|
|
38
38
|
return { name: who.name, voice: who.voice, message: message2 };
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
const voice = who.voice ? ` (voice: ${who.voice})` : "";
|
|
42
|
-
const message = !who.name ? [renamed && who.voice ? `Your voice is ${who.voice} now.` : "", who.unnamed ?? "Set up, with no name yet."].filter(Boolean).join(" ") : renamed ? `Renamed \u2014 this agent is "${who.name}"${voice} now.` : `Already set up as "${who.name}"${voice}. Ask the user if they want to rename it or
|
|
42
|
+
const message = !who.name ? [renamed && who.voice ? `Your voice is ${who.voice} now.` : "", who.unnamed ?? "Set up, with no name yet."].filter(Boolean).join(" ") : renamed ? `Renamed \u2014 this agent is "${who.name}"${voice} now.` : `Already set up as "${who.name}"${voice}. Ask the user if they want to rename it or log it out; otherwise carry on.`;
|
|
43
43
|
return { name: who.name, voice: who.voice, message };
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -219,7 +219,7 @@ CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an ans
|
|
|
219
219
|
|
|
220
220
|
UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first (manage_goals), then send one update saying what is done and anything they need to do or check. On a Goal whose report card is still open, an update that reaches them is added to that card, with no new push. If the person is already on a call, anything that reaches them joins that call, with no ring.
|
|
221
221
|
|
|
222
|
-
RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goal); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`;
|
|
222
|
+
RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goal); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
|
|
223
223
|
var CreateGoalSchema = z3.object({
|
|
224
224
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
225
225
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -265,7 +265,7 @@ var CreateChange = z3.object({
|
|
|
265
265
|
kind: z3.literal("create"),
|
|
266
266
|
title: goalTitle,
|
|
267
267
|
outcome: goalOutcome,
|
|
268
|
-
ownerId: z3.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as
|
|
268
|
+
ownerId: z3.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as check_activity shows it) or the person's. Omitted: you."),
|
|
269
269
|
parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
|
|
270
270
|
sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
|
|
271
271
|
}).strict();
|
|
@@ -310,8 +310,7 @@ var ManageGoalsSchema = z3.object({
|
|
|
310
310
|
var ManageGoalsToolSchema = z3.object({
|
|
311
311
|
changes: z3.array(z3.discriminatedUnion("kind", [CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
|
|
312
312
|
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
313
|
-
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open,
|
|
314
|
-
var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
|
|
313
|
+
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls; edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
|
|
315
314
|
var GetGoalSchema = z3.object({
|
|
316
315
|
goalId: z3.string().uuid(),
|
|
317
316
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
@@ -322,8 +321,7 @@ var GetGoalSchema = z3.object({
|
|
|
322
321
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
323
322
|
diagnose: z3.boolean().optional()
|
|
324
323
|
}).strict();
|
|
325
|
-
var GET_GOAL_DESCRIPTION = "Read one Goal
|
|
326
|
-
var CLAIM_GOAL_DESCRIPTION = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change it; use manage_goals (assign) when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
|
|
324
|
+
var GET_GOAL_DESCRIPTION = "Read one Goal (a read never puts you on it), including others (the ten most recent other contributors, with names, latest entry headlines and times): its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
327
325
|
var SearchToolSchema = z3.object({
|
|
328
326
|
query: z3.string().trim().min(1).max(500),
|
|
329
327
|
types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
@@ -331,7 +329,7 @@ var SearchToolSchema = z3.object({
|
|
|
331
329
|
limit: z3.number().int().min(1).max(20).optional()
|
|
332
330
|
}).strict();
|
|
333
331
|
var SEARCH_DESCRIPTION = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and Answers (found by their Question or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, its Questions' Answers, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (an Answer carries its Question, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
|
|
334
|
-
var
|
|
332
|
+
var CheckActivitySchema = z3.object({}).strict();
|
|
335
333
|
var FEEDBACK_TEXT_MAX = 5e4;
|
|
336
334
|
var SendFeedbackSchema = z3.object({
|
|
337
335
|
text: z3.string().trim().min(1).max(FEEDBACK_TEXT_MAX).describe(`The report, in markdown: what happened, what was expected, how to reproduce it, versions, evidence. At most ${FEEDBACK_TEXT_MAX} characters.`),
|
|
@@ -340,24 +338,23 @@ var SendFeedbackSchema = z3.object({
|
|
|
340
338
|
}).strict();
|
|
341
339
|
var SEND_FEEDBACK_DESCRIPTION = "Send feedback about Paigy itself (a bug report, an idea) straight to the Paigy team. Send it only when the person asked you to, or agreed when you offered: it goes to Paigy, never to the person, and nobody answers it here. Not for your work or a question for the person: use contact for those. Returns whether it was stored.";
|
|
342
340
|
var AGENT_TOOLS = [
|
|
343
|
-
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(WhoIsWorkingSchema) },
|
|
344
341
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
345
342
|
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
346
|
-
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
347
343
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
348
344
|
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
|
|
345
|
+
{ name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckActivitySchema) },
|
|
349
346
|
{ name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
|
|
350
347
|
];
|
|
351
348
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
352
349
|
function serverInstructions(opts) {
|
|
353
350
|
const waits = opts.waits ? "contact({..., wait:true}) holds one bounded ~45 s window for a response to what you sent; contact({}) holds one for anything addressed to you." : "A contact here returns after one read (`waitOutcome: not_waited`); answers wake you, and contact({wait:false}) collects them.";
|
|
354
|
-
return `On startup and after a wake, call contact({wait:false}) for the events addressed to you (acknowledge the ones you handled with contact({ackEventIds})),
|
|
351
|
+
return `On startup and after a wake, call contact({wait:false}) for the events addressed to you (acknowledge the ones you handled with contact({ackEventIds})), which hand you your work: answers, assignments, reviews and due Goals. get_goal reads a Goal and the conversation on it; a read never puts you on it. You are on a Goal from your first write to it (a contact update, question or answer naming it, or a manage_goals change); there is nothing to claim or join. Every Goal of your person is open to you, whichever of their agents owns it: read it with get_goal, write on it (a contact naming its id) and change it (manage_goals). Create, edit, assign, organize or close work with manage_goals, never as a contact; a contact only names Goals that already exist. Every read ends in \`next\`, the one step to take. Sending returns immediately unless you ask it to wait: keep working and collect answers by receiving or through get_goal. ${waits} Events repeat until you acknowledge them: reading alone acknowledges nothing; an \`update\` event acknowledged is a review cleared. Report progress as a contact update: one the person does not need yet is kept as the Goal's progress and reaches no one. To follow up later, defer the Goal with manage_goals: you are woken when its time comes. Never infer ringing from an open Call Delivery. Soft waiting and re-presentation are unsupported. Your user is remote. Always interact with the user through Paigy. For decisions, approvals, or questions, contact them with structured options. Never assume anyone is reading the terminal stdout.
|
|
355
352
|
|
|
356
353
|
HOW TO ASK:
|
|
357
354
|
1. One question per question object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
|
|
358
355
|
2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
|
|
359
356
|
3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
|
|
360
|
-
4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`
|
|
357
|
+
4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goal\` on that wake.
|
|
361
358
|
5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
|
|
362
359
|
6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
|
|
363
360
|
7. A question carries its options. Without them it reaches the person as a bare title nobody can answer. Each option names its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Your question is sent exactly as you wrote it. They can always answer in their own words, so no "Other" option.
|
|
@@ -1585,7 +1582,7 @@ var HostDecisionSchema = z7.object({
|
|
|
1585
1582
|
/** The agent's token id: the row's `recipient`. */
|
|
1586
1583
|
agent: z7.string().uuid(),
|
|
1587
1584
|
decision: z7.enum(["stood_back", "took_over"]),
|
|
1588
|
-
/** The work it was about: the Goal
|
|
1585
|
+
/** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
|
|
1589
1586
|
goalId: z7.string().uuid().nullable().optional(),
|
|
1590
1587
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
1591
1588
|
seenAt: z7.string().datetime().nullable().optional(),
|
|
@@ -1816,7 +1813,7 @@ next contact({ wait: false }). Start your own listener now, as a BACKGROUND task
|
|
|
1816
1813
|
|
|
1817
1814
|
Each line it prints is a Delivery waiting for you: read the Goal, and reply with contact when
|
|
1818
1815
|
the answer needs a decision. Start it once; if it is already running, do nothing. Then call
|
|
1819
|
-
|
|
1816
|
+
contact({ wait: false }) once: work handed to you before this session started waits there.
|
|
1820
1817
|
PAIGY
|
|
1821
1818
|
exit 0
|
|
1822
1819
|
`;
|
|
@@ -1852,7 +1849,7 @@ function decideListen(f) {
|
|
|
1852
1849
|
return {
|
|
1853
1850
|
status: "start",
|
|
1854
1851
|
command: `PAIGY_AGENT=${word(f.slot)} PAIGY_SESSION_ID=${word(f.session)} ${f.env.CODEX_THREAD_ID ? `CODEX_THREAD_ID=${word(f.env.CODEX_THREAD_ID)} ` : ""}${f.env.PAIGY_ON_WAKE ? `PAIGY_ON_WAKE=${word(f.env.PAIGY_ON_WAKE)} ` : ""}${word(f.execPath)} ${word(f.listenJs)} --brief`,
|
|
1855
|
-
how: "Run this command NOW as a watched background task (your harness's background-task/monitor tool), and leave it running for this session. Each line it prints is a Delivery waiting for you:
|
|
1852
|
+
how: "Run this command NOW as a watched background task (your harness's background-task/monitor tool), and leave it running for this session. Each line it prints is a Delivery waiting for you: read the Goal it names with get_goal, and answer with contact. Start it once."
|
|
1856
1853
|
};
|
|
1857
1854
|
}
|
|
1858
1855
|
function listenMark(slot, home = homedir()) {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
acknowledge,
|
|
18
18
|
agentName,
|
|
19
19
|
authToken,
|
|
20
|
-
|
|
20
|
+
checkActivity,
|
|
21
21
|
claimSessionEnds,
|
|
22
22
|
claimSessions,
|
|
23
23
|
contact,
|
|
@@ -64,9 +64,8 @@ import {
|
|
|
64
64
|
subscribeWake,
|
|
65
65
|
updateSlot,
|
|
66
66
|
whoAmI,
|
|
67
|
-
whoIsWorking,
|
|
68
67
|
withCodexEnv
|
|
69
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-4C6SDQ7A.js";
|
|
70
69
|
export {
|
|
71
70
|
AGENT_TOOLS,
|
|
72
71
|
AGENT_TOOL_NAMES,
|
|
@@ -86,7 +85,7 @@ export {
|
|
|
86
85
|
acknowledge,
|
|
87
86
|
agentName,
|
|
88
87
|
authToken,
|
|
89
|
-
|
|
88
|
+
checkActivity,
|
|
90
89
|
claimSessionEnds,
|
|
91
90
|
claimSessions,
|
|
92
91
|
contact,
|
|
@@ -133,6 +132,5 @@ export {
|
|
|
133
132
|
subscribeWake,
|
|
134
133
|
updateSlot,
|
|
135
134
|
whoAmI,
|
|
136
|
-
whoIsWorking,
|
|
137
135
|
withCodexEnv
|
|
138
136
|
};
|
package/dist/enable.js
CHANGED
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
PAIGY_TOOL_IDS,
|
|
4
4
|
enablePaigyTools,
|
|
5
5
|
installSessionListening
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-A5FYZK6P.js";
|
|
7
|
+
import "./chunk-X3GTDW5G.js";
|
|
8
|
+
import "./chunk-4C6SDQ7A.js";
|
|
9
9
|
|
|
10
10
|
// src/enable.ts
|
|
11
11
|
function main() {
|
|
@@ -16,14 +16,14 @@ function main() {
|
|
|
16
16
|
if (arg === "-h" || arg === "--help") {
|
|
17
17
|
console.log("usage: paigy-enable-tools [--scope user|project]");
|
|
18
18
|
console.log("");
|
|
19
|
-
console.log("Allowlist Paigy's
|
|
19
|
+
console.log("Allowlist Paigy's tools in Claude Code so they run without an");
|
|
20
20
|
console.log("approval prompt each time \u2014 an unattended session can't stall on a dialog.");
|
|
21
21
|
console.log("");
|
|
22
22
|
console.log(" --scope user ~/.claude/settings.json (default) \u2014 every project");
|
|
23
23
|
console.log(" --scope project ./.claude/settings.json \u2014 just this repo");
|
|
24
24
|
console.log("");
|
|
25
25
|
console.log(`Adds: ${PAIGY_TOOL_IDS.join(", ")}`);
|
|
26
|
-
console.log("
|
|
26
|
+
console.log("Safe to re-run.");
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
if (arg === "--scope") {
|
package/dist/index.js
CHANGED
|
@@ -9,23 +9,22 @@ import {
|
|
|
9
9
|
resolvePairing,
|
|
10
10
|
startPairing,
|
|
11
11
|
suggestedAgentName
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-UQDA7X3W.js";
|
|
13
13
|
import {
|
|
14
|
+
ConfigureSchema,
|
|
14
15
|
ENABLE_COMMAND,
|
|
15
|
-
OnboardSchema,
|
|
16
16
|
PAIGY_TOOL_IDS,
|
|
17
|
-
PairSchema,
|
|
18
17
|
SERVER_INSTRUCTIONS,
|
|
19
18
|
TOOLS,
|
|
20
19
|
paigyToolsAllowlisted
|
|
21
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-A5FYZK6P.js";
|
|
22
21
|
import {
|
|
23
22
|
decideListen,
|
|
24
23
|
existingListener,
|
|
25
24
|
harnessDriven,
|
|
26
25
|
listenerKey,
|
|
27
26
|
wakeStatusPath
|
|
28
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-X3GTDW5G.js";
|
|
29
28
|
import {
|
|
30
29
|
clearSurface,
|
|
31
30
|
writeSurface
|
|
@@ -46,7 +45,7 @@ import {
|
|
|
46
45
|
setClient,
|
|
47
46
|
slotName,
|
|
48
47
|
whoAmI
|
|
49
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-4C6SDQ7A.js";
|
|
50
49
|
|
|
51
50
|
// src/index.ts
|
|
52
51
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -113,7 +112,7 @@ function pairedResult(token, sas, note) {
|
|
|
113
112
|
if (note) base.note = note;
|
|
114
113
|
base.enable_tool_ids = PAIGY_TOOL_IDS;
|
|
115
114
|
base.enable_command = ENABLE_COMMAND;
|
|
116
|
-
base.enable_prompt = "REQUIRED \u2014 before you do anything else, print `enable_command` to the user and say: \"Run this to let Paigy's tools work without an approval prompt each time \u2014 it only allowlists
|
|
115
|
+
base.enable_prompt = "REQUIRED \u2014 before you do anything else, print `enable_command` to the user and say: \"Run this to let Paigy's tools work without an approval prompt each time \u2014 it only allowlists Paigy's own tools.\" Print it every time you pair; don't skip it.";
|
|
117
116
|
base.enable_apply = "You cannot apply this yourself, and must not try: it writes YOUR OWN permission allowlist, which hosts block as privilege escalation no matter what the user said here. PRINT the command and move on \u2014 don't wait for them to run it, and don't re-ask. Paigy's tools still work meanwhile; they just prompt for approval each time until the user runs it. If the host isn't Claude Code, tell them to add `enable_tool_ids` to that tool's own allowlist instead, merging \u2014 never clobbering.";
|
|
118
117
|
return { content: [{ type: "text", text: JSON.stringify(base) }] };
|
|
119
118
|
}
|
|
@@ -130,7 +129,7 @@ Check it matches the code on the phone, then type it there to confirm.` },
|
|
|
130
129
|
sas,
|
|
131
130
|
device_code,
|
|
132
131
|
user_message: `# ${sas}`,
|
|
133
|
-
verify_message: `STOP HERE: end your turn now with \`user_message\` (the bare code) as your entire reply \u2014 do NOT call
|
|
132
|
+
verify_message: `STOP HERE: end your turn now with \`user_message\` (the bare code) as your entire reply \u2014 do NOT call configure again in this same turn, or the code text is dropped before the user sees it. This is the verification code they compare + type on their phone; it REPLACES any earlier one. Poll for their confirmation by calling configure with this device_code on your NEXT turn.`,
|
|
134
133
|
message: "No token is issued until the user confirms the code match on their phone (E2EE account)."
|
|
135
134
|
})
|
|
136
135
|
}
|
|
@@ -152,7 +151,7 @@ function renderPairOutcome(outcome, device_code) {
|
|
|
152
151
|
text: JSON.stringify({
|
|
153
152
|
status: "pending",
|
|
154
153
|
device_code,
|
|
155
|
-
message: "Still awaiting approval \u2014 the user hasn't entered the code yet. Call
|
|
154
|
+
message: "Still awaiting approval \u2014 the user hasn't entered the code yet. Call configure again with this device_code RIGHT NOW to keep waiting; each call returns after ~45s so it can't be killed by a host tool-call timeout. Nothing is lost between calls: the code stays valid and approval keeps being polled in the background, so a re-call returns the instant they approve."
|
|
156
155
|
})
|
|
157
156
|
}]
|
|
158
157
|
};
|
|
@@ -162,7 +161,7 @@ function unidentifiedResult() {
|
|
|
162
161
|
return { isError: true, content: [{ type: "text", text: JSON.stringify({
|
|
163
162
|
error: "no_identity",
|
|
164
163
|
slot: agentName(),
|
|
165
|
-
message: `This session has no Paigy identity yet, so nothing was sent \u2014 it will not speak as another session. This machine holds a device credential but hatching under it just failed (it may have been revoked), so call \`
|
|
164
|
+
message: `This session has no Paigy identity yet, so nothing was sent \u2014 it will not speak as another session. This machine holds a device credential but hatching under it just failed (it may have been revoked), so call \`configure\` (no arguments) to set this session up, then retry \u2014 it re-hatches if the credential recovered and runs the code ceremony if it didn't. To reuse an existing identity instead, start the session with PAIGY_AGENT set to its slot (${listSlots().filter((s) => s !== "Desktop").join(", ") || "none yet"}).`
|
|
166
165
|
}) }] };
|
|
167
166
|
}
|
|
168
167
|
function pairStartResult(start, loggedOut) {
|
|
@@ -204,7 +203,7 @@ This session isn't on a connected computer, so it asks by code.`;
|
|
|
204
203
|
expires_in: start.expiresIn,
|
|
205
204
|
qr: ascii,
|
|
206
205
|
user_message: userMessage,
|
|
207
|
-
message: "REQUIRED: print user_message for the user \u2014 the LINK is the primary action, the code is the fallback, and `qr` is there for a phone when they ask. Then call
|
|
206
|
+
message: "REQUIRED: print user_message for the user \u2014 the LINK is the primary action, the code is the fallback, and `qr` is there for a phone when they ask. Then call configure again with device_code to collect approval. Do not open a browser yourself."
|
|
208
207
|
}) }
|
|
209
208
|
] };
|
|
210
209
|
}
|
|
@@ -241,7 +240,7 @@ async function runPair(device_code, name, voice, note) {
|
|
|
241
240
|
if ("ok" in dt) return pairedResult(
|
|
242
241
|
dt,
|
|
243
242
|
void 0,
|
|
244
|
-
"Hatched instantly under this device's credential \u2014 no code needed. " + (dt.message ? `${dt.message} Set it with
|
|
243
|
+
"Hatched instantly under this device's credential \u2014 no code needed. " + (dt.message ? `${dt.message} Set it with configure({ name }).` : name ? "" : `You were given a default name ("${dt.name}") \u2014 the user never chose it, so don't announce it as their agent's identity. They can rename it in the app, or you can call configure with { name, voice }.`)
|
|
245
244
|
);
|
|
246
245
|
miss = dt;
|
|
247
246
|
}
|
|
@@ -260,7 +259,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
260
259
|
return await handleTool(request, extra?.signal);
|
|
261
260
|
} catch (e) {
|
|
262
261
|
if (e instanceof UnpairedError) {
|
|
263
|
-
const IDENTITY_TOOLS = /* @__PURE__ */ new Set(["
|
|
262
|
+
const IDENTITY_TOOLS = /* @__PURE__ */ new Set(["configure"]);
|
|
264
263
|
const hatched = IDENTITY_TOOLS.has(request.params.name) ? null : await hatchUnderDevice();
|
|
265
264
|
const next = afterUnpaired(hatched, listSlots().includes("Desktop"));
|
|
266
265
|
if (next === "retry") return await handleTool(request, extra?.signal);
|
|
@@ -273,26 +272,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
273
272
|
function suggestedAgentName2() {
|
|
274
273
|
return suggestedAgentName(server.getClientVersion?.()?.name);
|
|
275
274
|
}
|
|
275
|
+
async function logOutResult() {
|
|
276
|
+
const token = readToken();
|
|
277
|
+
let revoked = false;
|
|
278
|
+
if (token) {
|
|
279
|
+
try {
|
|
280
|
+
revoked = await revokeToken(token);
|
|
281
|
+
} catch {
|
|
282
|
+
revoked = false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const removed = deleteToken();
|
|
286
|
+
clearPairing();
|
|
287
|
+
return {
|
|
288
|
+
content: [{
|
|
289
|
+
type: "text",
|
|
290
|
+
text: JSON.stringify({
|
|
291
|
+
ok: true,
|
|
292
|
+
revoked,
|
|
293
|
+
removed,
|
|
294
|
+
message: token ? `Logged out. Server token ${revoked ? "revoked" : "revoke not confirmed"}; local token ${removed ? "deleted" : "was already absent"}.` : "No token found \u2014 already logged out."
|
|
295
|
+
})
|
|
296
|
+
}]
|
|
297
|
+
};
|
|
298
|
+
}
|
|
276
299
|
async function handleTool(request, signal) {
|
|
277
300
|
switch (request.params.name) {
|
|
278
|
-
case "pair": {
|
|
279
|
-
const a = PairSchema.parse(request.params.arguments ?? {});
|
|
280
|
-
return runPair(a.device_code, a.name, a.voice);
|
|
281
|
-
}
|
|
282
301
|
// ONE rail for "get this agent talking to Paigy" (#875). `pair`, the `paigy-mcp-onboard`
|
|
283
302
|
// CLI and `enable_tools` were three doors into one flow, each knowing a different slice
|
|
284
|
-
// of the state
|
|
285
|
-
//
|
|
286
|
-
//
|
|
303
|
+
// of the state, and a half-configured agent (paired but not allowlisted, or holding a token
|
|
304
|
+
// for a slot it can no longer resolve) had nothing to ask. Since 2026-10-07 (owner) it is
|
|
305
|
+
// `configure`, the only identity tool: `onboard` renamed, `pair` gone (this did all of it),
|
|
306
|
+
// and `unpair` folded in as `logOut`.
|
|
287
307
|
//
|
|
288
308
|
// It reports state and does the next thing, and it is IDEMPOTENT: calling it when
|
|
289
309
|
// everything is fine costs one round trip and returns who you are. Reporting never
|
|
290
310
|
// mutates — the allowlist check is read-only on purpose, because a status call that
|
|
291
311
|
// quietly edits the user's settings is the opposite of what this is for.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const { name, voice, device_code, scope } = OnboardSchema.parse(request.params.arguments ?? {});
|
|
312
|
+
case "configure": {
|
|
313
|
+
const { name, voice, device_code, scope, logOut } = ConfigureSchema.parse(request.params.arguments ?? {});
|
|
314
|
+
if (logOut) return logOutResult();
|
|
296
315
|
if (device_code) return runPair(device_code, name, voice);
|
|
297
316
|
const slot = agentName();
|
|
298
317
|
const secret = readToken();
|
|
@@ -311,14 +330,14 @@ async function handleTool(request, signal) {
|
|
|
311
330
|
...allowlisted ? {} : {
|
|
312
331
|
enable_tool_ids: PAIGY_TOOL_IDS,
|
|
313
332
|
enable_command: ENABLE_COMMAND,
|
|
314
|
-
enable_prompt: "Print `enable_command` to the user: \"Run this to let Paigy's tools work without an approval prompt each time \u2014 it only allowlists
|
|
333
|
+
enable_prompt: "Print `enable_command` to the user: \"Run this to let Paigy's tools work without an approval prompt each time \u2014 it only allowlists Paigy's own tools.\" You cannot run it yourself (it writes your own permission allowlist, which hosts block as privilege escalation) \u2014 print it, don't wait for it, and carry on."
|
|
315
334
|
},
|
|
316
335
|
// Setup is done, so the only things left are the user's to decide. Named here
|
|
317
336
|
// rather than left implicit: an agent that reports "ready" and stops gives the
|
|
318
337
|
// user no way to act on an identity they may not want.
|
|
319
338
|
actions: {
|
|
320
|
-
rename: "Call
|
|
321
|
-
|
|
339
|
+
rename: "Call configure again with { name } (and optionally { voice }) to change it.",
|
|
340
|
+
logOut: "Call configure({ logOut: true }) to log this agent out (other agents and this device stay paired)."
|
|
322
341
|
},
|
|
323
342
|
message: me.message
|
|
324
343
|
}) }]
|
|
@@ -328,30 +347,6 @@ async function handleTool(request, signal) {
|
|
|
328
347
|
}
|
|
329
348
|
return runPair(void 0, name, voice);
|
|
330
349
|
}
|
|
331
|
-
case "unpair": {
|
|
332
|
-
const token = readToken();
|
|
333
|
-
let revoked = false;
|
|
334
|
-
if (token) {
|
|
335
|
-
try {
|
|
336
|
-
revoked = await revokeToken(token);
|
|
337
|
-
} catch {
|
|
338
|
-
revoked = false;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const removed = deleteToken();
|
|
342
|
-
clearPairing();
|
|
343
|
-
return {
|
|
344
|
-
content: [{
|
|
345
|
-
type: "text",
|
|
346
|
-
text: JSON.stringify({
|
|
347
|
-
ok: true,
|
|
348
|
-
revoked,
|
|
349
|
-
removed,
|
|
350
|
-
message: token ? `Unpaired. Server token ${revoked ? "revoked" : "revoke not confirmed"}; local token ${removed ? "deleted" : "was already absent"}.` : "No token found \u2014 already unpaired."
|
|
351
|
-
})
|
|
352
|
-
}]
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
350
|
// AM I LISTENING? (#2265) Decides and hands over — `listening.ts` owns the rule and the four
|
|
356
351
|
// answers. The facts are read here, once: the harness's session mark, a token to listen as,
|
|
357
352
|
// a live listener's mark for this slot, and where THIS server's node and daemon live. The
|
package/dist/listen.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
removeListenMark,
|
|
12
12
|
wakeStatusPath,
|
|
13
13
|
writeListenMark
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-X3GTDW5G.js";
|
|
15
15
|
import {
|
|
16
16
|
agentName,
|
|
17
17
|
getGoal,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
receive,
|
|
21
21
|
slotName,
|
|
22
22
|
subscribeWake
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-4C6SDQ7A.js";
|
|
24
24
|
|
|
25
25
|
// src/listen.ts
|
|
26
26
|
import { createHash } from "crypto";
|
|
@@ -145,9 +145,9 @@ function brief(waiting, reason, as, goalId) {
|
|
|
145
145
|
const stalled = waiting.stalled?.length ? ` \xB7 ${waiting.stalled.length} of your Goals stalled 3+ days: update, finish or cancel each` : "";
|
|
146
146
|
const review = waiting.review ?? [];
|
|
147
147
|
const named = (goalId ? review.find((g) => g.goalId === goalId) : void 0) ?? review[0];
|
|
148
|
-
const fresh = review.length ? ` \xB7 new on ${review.length === 1 ? "your Goal" : `${review.length} of your Goals`}, first ${named.goalId} ("${named.title}"):
|
|
148
|
+
const fresh = review.length ? ` \xB7 new on ${review.length === 1 ? "your Goal" : `${review.length} of your Goals`}, first ${named.goalId} ("${named.title}"): get_goal to read it` : "";
|
|
149
149
|
const assigned = waiting.assigned ?? [];
|
|
150
|
-
const mine = assigned.length ? ` \xB7 assigned to you: ${assigned.length === 1 ? "1 Goal" : `${assigned.length} Goals`}, first ${assigned[0].goalId} ("${assigned[0].title}"):
|
|
150
|
+
const mine = assigned.length ? ` \xB7 assigned to you: ${assigned.length === 1 ? "1 Goal" : `${assigned.length} Goals`}, first ${assigned[0].goalId} ("${assigned[0].title}"): get_goal({goalId}) to read it; your first write starts it` : "";
|
|
151
151
|
if (!lead) return `Paigy \xB7 ${reason === "boot" ? `listening as ${as}` : reason}${mine}${fresh || (mine ? "" : " \xB7 nothing waiting")}${stalled}`;
|
|
152
152
|
const said = saidIn(lead) || ("about" in lead ? lead.about : "");
|
|
153
153
|
const shown = said.length > 160 ? `${said.slice(0, 160)}\u2026` : said;
|
package/dist/onboard.js
CHANGED
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
resolvePairing,
|
|
4
4
|
startPairing,
|
|
5
5
|
suggestedAgentName
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-UQDA7X3W.js";
|
|
7
7
|
import {
|
|
8
8
|
autoConfigureClients,
|
|
9
9
|
claudeInstallHint,
|
|
10
10
|
openBrowser
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-A5FYZK6P.js";
|
|
12
|
+
import "./chunk-X3GTDW5G.js";
|
|
13
13
|
import {
|
|
14
14
|
TOKEN_PATH,
|
|
15
15
|
agentName,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
saveToken,
|
|
21
21
|
setIdentity,
|
|
22
22
|
whoAmI
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-4C6SDQ7A.js";
|
|
24
24
|
|
|
25
25
|
// src/onboard.ts
|
|
26
26
|
function reportRegistered() {
|
|
@@ -58,8 +58,8 @@ Paigy MCP onboarding (agent: "${agentName()}")
|
|
|
58
58
|
console.log(`Already set up as "${who.name}"${who.voice ? ` (voice: ${who.voice})` : ""}.
|
|
59
59
|
`);
|
|
60
60
|
console.log("Nothing to do. What's left is yours to decide:");
|
|
61
|
-
console.log(' rename \u2014 PAIGY_NAME="New name" paigy-mcp-onboard (or the `
|
|
62
|
-
console.log("
|
|
61
|
+
console.log(' rename \u2014 PAIGY_NAME="New name" paigy-mcp-onboard (or the `configure` MCP tool)');
|
|
62
|
+
console.log(" log out \u2014 the MCP `configure` tool with { logOut: true }, or delete this slot from ~/.paigy/token.json\n");
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
console.log("The saved token is no longer valid \u2014 re-pairing.\n");
|
package/dist/slot.js
CHANGED
package/dist/stalled.js
CHANGED
|
@@ -29,7 +29,7 @@ async function main() {
|
|
|
29
29
|
const dir = join(homedir(), ".paigy", "stalled-reminded");
|
|
30
30
|
const mark = join(dir, `${session || "session"}-${day}`);
|
|
31
31
|
if (existsSync(mark)) return;
|
|
32
|
-
const { receive } = await import("./dist-
|
|
32
|
+
const { receive } = await import("./dist-N6ZJUJV7.js");
|
|
33
33
|
const reply = stopReply((await receive()).stalled ?? [], input.hook_event_name ?? "Stop");
|
|
34
34
|
if (!reply) return;
|
|
35
35
|
mkdirSync(dir, { recursive: true });
|
package/dist/statusline.js
CHANGED
package/package.json
CHANGED