@paigy/mcp 0.40.26 → 0.40.28
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-7LQKX73K.js → chunk-632JM6QA.js} +39 -34
- package/dist/{chunk-OVSAQW3K.js → chunk-AHLJPVEP.js} +50 -54
- package/dist/{chunk-BLWC44CQ.js → chunk-WKAXAJEM.js} +3 -3
- package/dist/{chunk-5MQ3J6S3.js → chunk-ZP6NBVEE.js} +18 -36
- package/dist/{dist-WGFUXFS4.js → dist-JTZJFUWD.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)
|
|
@@ -12,10 +12,12 @@ import { z as z6 } from "zod";
|
|
|
12
12
|
import { zodToJsonSchema as zodToJsonSchema4 } from "zod-to-json-schema";
|
|
13
13
|
var OPTIONS_MIN = 2;
|
|
14
14
|
var OPTIONS_MAX = 6;
|
|
15
|
+
var OPTION_LABEL_MAX = 50;
|
|
16
|
+
var OPTION_HINT_MAX = 200;
|
|
15
17
|
var OptionSchema = z.object({
|
|
16
18
|
id: z.string(),
|
|
17
19
|
label: z.string(),
|
|
18
|
-
hint: z.string().max(500).describe(
|
|
20
|
+
hint: z.string().max(500).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
|
|
19
21
|
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
20
22
|
// the constraints below are what an agent reads when deciding to use these.
|
|
21
23
|
html: z.string().max(16384).describe(
|
|
@@ -29,8 +31,10 @@ var OptionInputSchema = z.object({
|
|
|
29
31
|
id: z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
|
|
30
32
|
`Optional: this option's own id, which answers name it by (selectedOptionIds). Give every option one, or none: without them the options are numbered "1", "2", \u2026 in order.`
|
|
31
33
|
),
|
|
32
|
-
|
|
33
|
-
hint
|
|
34
|
+
// SHORT CHOICES (owner, 2026-10-07: "options label should be maybe max 50 characters and the hint 200"):
|
|
35
|
+
// a label is one choice the person reads, or hears on a call; a hint says its consequence in a line.
|
|
36
|
+
label: z.string().trim().min(1).max(OPTION_LABEL_MAX, `option_label_too_long: an option's label is at most ${OPTION_LABEL_MAX} characters; say the choice, and put its consequence in the hint.`).describe(`The choice, at most ${OPTION_LABEL_MAX} characters, standing on its own.`),
|
|
37
|
+
hint: z.string().max(OPTION_HINT_MAX, `option_hint_too_long: an option's hint is at most ${OPTION_HINT_MAX} characters.`).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
|
|
34
38
|
htmlPreview: OptionSchema.shape.html,
|
|
35
39
|
imgUrl: OptionSchema.shape.image
|
|
36
40
|
}).strict();
|
|
@@ -140,7 +144,7 @@ var questionFields = {
|
|
|
140
144
|
),
|
|
141
145
|
goalId: z2.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
|
|
142
146
|
units,
|
|
143
|
-
options: z2.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
|
|
147
|
+
options: z2.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own, at most 50 characters; a hint, at most 200."),
|
|
144
148
|
// HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
|
|
145
149
|
// brain_prompts.md §1: structured agent questions need no model). The app draws all three.
|
|
146
150
|
pickMode: z2.enum(["one", "many", "rank"]).optional().describe(
|
|
@@ -219,7 +223,7 @@ CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an ans
|
|
|
219
223
|
|
|
220
224
|
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
225
|
|
|
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\`;
|
|
226
|
+
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
227
|
var CreateGoalSchema = z3.object({
|
|
224
228
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
225
229
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -260,12 +264,13 @@ var UpdateGoalSchema = z3.object({
|
|
|
260
264
|
}).strict();
|
|
261
265
|
var changedGoal = z3.string().uuid().describe("The Goal to change, as a read shows it.");
|
|
262
266
|
var goalTitle = z3.string().trim().min(1).max(80).describe("The Goal's short display name: one to five words, how a person refers to it out loud.");
|
|
263
|
-
var
|
|
267
|
+
var GOAL_OUTCOME_MAX = 500;
|
|
268
|
+
var goalOutcome = z3.string().trim().min(1).max(GOAL_OUTCOME_MAX, `outcome_too_long: a Goal's outcome is one paragraph, at most ${GOAL_OUTCOME_MAX} characters; split the rest into child Goals (create with parentGoalId).`).describe(`The desired result, one paragraph of at most ${GOAL_OUTCOME_MAX} characters. More than that is more work: create child Goals under it (parentGoalId). There is no third description field.`);
|
|
264
269
|
var CreateChange = z3.object({
|
|
265
270
|
kind: z3.literal("create"),
|
|
266
271
|
title: goalTitle,
|
|
267
272
|
outcome: goalOutcome,
|
|
268
|
-
ownerId: z3.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as
|
|
273
|
+
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
274
|
parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
|
|
270
275
|
sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
|
|
271
276
|
}).strict();
|
|
@@ -286,7 +291,7 @@ var DependencyChange = z3.object({
|
|
|
286
291
|
goalId: changedGoal,
|
|
287
292
|
dependsOnGoalId: z3.string().uuid().describe("The Goal it waits on."),
|
|
288
293
|
action: z3.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
|
|
289
|
-
reason: z3.string().trim().min(1).max(
|
|
294
|
+
reason: z3.string().trim().min(1).max(250).optional().describe("Why, at most 250 characters, as an explanation, never as policy.")
|
|
290
295
|
}).strict();
|
|
291
296
|
var read = { revision: z3.number().int().positive().optional() };
|
|
292
297
|
var GoalChangeSchema = z3.discriminatedUnion("kind", [
|
|
@@ -310,8 +315,7 @@ var ManageGoalsSchema = z3.object({
|
|
|
310
315
|
var ManageGoalsToolSchema = z3.object({
|
|
311
316
|
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
317
|
}).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();
|
|
318
|
+
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 (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); 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
319
|
var GetGoalSchema = z3.object({
|
|
316
320
|
goalId: z3.string().uuid(),
|
|
317
321
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
@@ -322,8 +326,7 @@ var GetGoalSchema = z3.object({
|
|
|
322
326
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
323
327
|
diagnose: z3.boolean().optional()
|
|
324
328
|
}).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).";
|
|
329
|
+
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
330
|
var SearchToolSchema = z3.object({
|
|
328
331
|
query: z3.string().trim().min(1).max(500),
|
|
329
332
|
types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
@@ -331,7 +334,7 @@ var SearchToolSchema = z3.object({
|
|
|
331
334
|
limit: z3.number().int().min(1).max(20).optional()
|
|
332
335
|
}).strict();
|
|
333
336
|
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
|
|
337
|
+
var CheckActivitySchema = z3.object({}).strict();
|
|
335
338
|
var FEEDBACK_TEXT_MAX = 5e4;
|
|
336
339
|
var SendFeedbackSchema = z3.object({
|
|
337
340
|
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 +343,23 @@ var SendFeedbackSchema = z3.object({
|
|
|
340
343
|
}).strict();
|
|
341
344
|
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
345
|
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
346
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
345
347
|
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
346
|
-
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
347
348
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
348
349
|
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
|
|
350
|
+
{ 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
351
|
{ name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
|
|
350
352
|
];
|
|
351
353
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
352
354
|
function serverInstructions(opts) {
|
|
353
355
|
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})),
|
|
356
|
+
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
357
|
|
|
356
358
|
HOW TO ASK:
|
|
357
359
|
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
360
|
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
361
|
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 \`
|
|
362
|
+
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
363
|
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
364
|
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
365
|
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.
|
|
@@ -408,6 +410,7 @@ var QuestionChangeSchema = z4.union([
|
|
|
408
410
|
z4.object({
|
|
409
411
|
kind: z4.literal("create"),
|
|
410
412
|
text: z4.string().min(1),
|
|
413
|
+
summary: z4.string().optional(),
|
|
411
414
|
answererId: participant,
|
|
412
415
|
goal: ResultRefSchema.optional(),
|
|
413
416
|
blocks: z4.array(z4.object({ question: ResultRefSchema }).strict()),
|
|
@@ -458,12 +461,12 @@ var BrainSearchSchema = z4.object({
|
|
|
458
461
|
}).strict();
|
|
459
462
|
var BrainResultSchema = z4.object({
|
|
460
463
|
messages: z4.array(BrainMessageSchema),
|
|
461
|
-
entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema) }).strict()),
|
|
464
|
+
entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema), summary: z4.string().optional() }).strict()),
|
|
462
465
|
questions: z4.array(z4.object({ key, entryKeys: z4.array(key), change: QuestionChangeSchema }).strict()),
|
|
463
466
|
answers: z4.array(z4.object({
|
|
464
467
|
question: ResultRefSchema,
|
|
465
468
|
entryKeys: z4.array(key).min(1),
|
|
466
|
-
summary: z4.string()
|
|
469
|
+
summary: z4.string(),
|
|
467
470
|
selectedOptionIds: z4.array(z4.string().min(1)).optional()
|
|
468
471
|
}).strict()),
|
|
469
472
|
goals: z4.array(z4.object({ key, entryKeys: z4.array(key), change: BrainGoalChangeSchema }).strict()),
|
|
@@ -508,8 +511,9 @@ var TalkerReplySchema = z5.object({
|
|
|
508
511
|
messages: z5.array(z5.object({ key: str2, to: str2, text: str2, about: strs2, blocks: str2 }).strict()),
|
|
509
512
|
/** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
|
|
510
513
|
then: z5.enum(["listen", "hold", "end"]),
|
|
511
|
-
/** Items the person's words settle: the item's handle, the chosen option IDs, the line handles
|
|
512
|
-
|
|
514
|
+
/** Items the person's words settle: the item's handle, the chosen option IDs, the line handles, and
|
|
515
|
+
* the answer in at most ten words, keeping any condition. */
|
|
516
|
+
answers: z5.array(z5.object({ item: str2, options: strs2, lines: strs2, summary: str2 }).strict()),
|
|
513
517
|
/** The line handles that hold a new instruction from the person. */
|
|
514
518
|
instruction: strs2,
|
|
515
519
|
/** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
|
|
@@ -526,8 +530,9 @@ var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
|
526
530
|
var str3 = z6.string();
|
|
527
531
|
var strs3 = z6.array(z6.string());
|
|
528
532
|
var FilerReplySchema = z6.object({
|
|
529
|
-
/** Which Goals each new line goes on: a line's handle
|
|
530
|
-
|
|
533
|
+
/** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals), and what
|
|
534
|
+
* the line says in at most ten words. */
|
|
535
|
+
filed: z6.array(z6.object({ line: str3, goals: strs3, summary: str3 }).strict()),
|
|
531
536
|
goals: z6.array(z6.object({
|
|
532
537
|
op: z6.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
|
|
533
538
|
key: str3,
|
|
@@ -546,6 +551,7 @@ var FilerReplySchema = z6.object({
|
|
|
546
551
|
key: str3,
|
|
547
552
|
question: str3,
|
|
548
553
|
text: str3,
|
|
554
|
+
summary: str3,
|
|
549
555
|
to: str3,
|
|
550
556
|
goal: str3,
|
|
551
557
|
options: z6.array(z6.object({ id: str3, label: str3 }).strict()),
|
|
@@ -1265,7 +1271,7 @@ var ConnectionSummarySchema = z7.object({
|
|
|
1265
1271
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
1266
1272
|
lastSeenAt: z7.string().datetime().nullable().optional(),
|
|
1267
1273
|
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
1268
|
-
* its Goals —
|
|
1274
|
+
* its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
|
|
1269
1275
|
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
1270
1276
|
lastWorkedAt: z7.string().datetime().nullable().optional(),
|
|
1271
1277
|
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
@@ -1463,7 +1469,7 @@ var QueueItemSchema = z7.object({
|
|
|
1463
1469
|
*
|
|
1464
1470
|
* IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
|
|
1465
1471
|
* writes change which field — the comparison is over the bytes the phone is holding, so a fact
|
|
1466
|
-
* the row shows that no `updated_at` moves for (
|
|
1472
|
+
* the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
|
|
1467
1473
|
* sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
|
|
1468
1474
|
* queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
|
|
1469
1475
|
rev: z7.string().optional()
|
|
@@ -1585,7 +1591,7 @@ var HostDecisionSchema = z7.object({
|
|
|
1585
1591
|
/** The agent's token id: the row's `recipient`. */
|
|
1586
1592
|
agent: z7.string().uuid(),
|
|
1587
1593
|
decision: z7.enum(["stood_back", "took_over"]),
|
|
1588
|
-
/** The work it was about: the Goal
|
|
1594
|
+
/** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
|
|
1589
1595
|
goalId: z7.string().uuid().nullable().optional(),
|
|
1590
1596
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
1591
1597
|
seenAt: z7.string().datetime().nullable().optional(),
|
|
@@ -1778,14 +1784,13 @@ var CallRecapSchema = z7.object({
|
|
|
1778
1784
|
owner: z7.string(),
|
|
1779
1785
|
state: z7.string(),
|
|
1780
1786
|
questions: z7.array(z7.object({ id: z7.string().uuid(), state: z7.string(), title: z7.string() })),
|
|
1781
|
-
/** `words` is always what they SAID, verbatim — the record, never replaced. `
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
* anything that is not an answer. */
|
|
1787
|
+
/** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
|
|
1788
|
+
* the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
|
|
1789
|
+
* gave, so the row scans like a chosen option and their own words stay under it. Absent when
|
|
1790
|
+
* neither wrote one. */
|
|
1786
1791
|
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
1787
1792
|
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
1788
|
-
lines: z7.array(z7.object({ entryId: z7.string().uuid(), words: z7.string(),
|
|
1793
|
+
lines: z7.array(z7.object({ entryId: z7.string().uuid(), words: z7.string(), summary: z7.string().optional(), about: z7.string().nullable().optional() }))
|
|
1789
1794
|
})),
|
|
1790
1795
|
unfiled: z7.array(z7.object({ lineId: z7.string().uuid(), words: z7.string(), atMs: z7.number() })),
|
|
1791
1796
|
more: z7.object({ lines: z7.number(), entries: z7.number(), topics: z7.number() })
|
|
@@ -1816,7 +1821,7 @@ next contact({ wait: false }). Start your own listener now, as a BACKGROUND task
|
|
|
1816
1821
|
|
|
1817
1822
|
Each line it prints is a Delivery waiting for you: read the Goal, and reply with contact when
|
|
1818
1823
|
the answer needs a decision. Start it once; if it is already running, do nothing. Then call
|
|
1819
|
-
|
|
1824
|
+
contact({ wait: false }) once: work handed to you before this session started waits there.
|
|
1820
1825
|
PAIGY
|
|
1821
1826
|
exit 0
|
|
1822
1827
|
`;
|
|
@@ -1852,7 +1857,7 @@ function decideListen(f) {
|
|
|
1852
1857
|
return {
|
|
1853
1858
|
status: "start",
|
|
1854
1859
|
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:
|
|
1860
|
+
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
1861
|
};
|
|
1857
1862
|
}
|
|
1858
1863
|
function listenMark(slot, home = homedir()) {
|
|
@@ -1314,10 +1314,12 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
1314
1314
|
};
|
|
1315
1315
|
var OPTIONS_MIN = 2;
|
|
1316
1316
|
var OPTIONS_MAX = 6;
|
|
1317
|
+
var OPTION_LABEL_MAX = 50;
|
|
1318
|
+
var OPTION_HINT_MAX = 200;
|
|
1317
1319
|
var OptionSchema = z.object({
|
|
1318
1320
|
id: z.string(),
|
|
1319
1321
|
label: z.string(),
|
|
1320
|
-
hint: z.string().max(500).describe(
|
|
1322
|
+
hint: z.string().max(500).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
|
|
1321
1323
|
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
1322
1324
|
// the constraints below are what an agent reads when deciding to use these.
|
|
1323
1325
|
html: z.string().max(16384).describe(
|
|
@@ -1331,8 +1333,10 @@ var OptionInputSchema = z.object({
|
|
|
1331
1333
|
id: z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
|
|
1332
1334
|
`Optional: this option's own id, which answers name it by (selectedOptionIds). Give every option one, or none: without them the options are numbered "1", "2", \u2026 in order.`
|
|
1333
1335
|
),
|
|
1334
|
-
|
|
1335
|
-
hint
|
|
1336
|
+
// SHORT CHOICES (owner, 2026-10-07: "options label should be maybe max 50 characters and the hint 200"):
|
|
1337
|
+
// a label is one choice the person reads, or hears on a call; a hint says its consequence in a line.
|
|
1338
|
+
label: z.string().trim().min(1).max(OPTION_LABEL_MAX, `option_label_too_long: an option's label is at most ${OPTION_LABEL_MAX} characters; say the choice, and put its consequence in the hint.`).describe(`The choice, at most ${OPTION_LABEL_MAX} characters, standing on its own.`),
|
|
1339
|
+
hint: z.string().max(OPTION_HINT_MAX, `option_hint_too_long: an option's hint is at most ${OPTION_HINT_MAX} characters.`).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
|
|
1336
1340
|
htmlPreview: OptionSchema.shape.html,
|
|
1337
1341
|
imgUrl: OptionSchema.shape.image
|
|
1338
1342
|
}).strict();
|
|
@@ -1610,7 +1614,7 @@ var questionFields = {
|
|
|
1610
1614
|
),
|
|
1611
1615
|
goalId: z2.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
|
|
1612
1616
|
units,
|
|
1613
|
-
options: z2.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
|
|
1617
|
+
options: z2.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own, at most 50 characters; a hint, at most 200."),
|
|
1614
1618
|
// HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
|
|
1615
1619
|
// brain_prompts.md §1: structured agent questions need no model). The app draws all three.
|
|
1616
1620
|
pickMode: z2.enum(["one", "many", "rank"]).optional().describe(
|
|
@@ -1689,7 +1693,7 @@ CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an ans
|
|
|
1689
1693
|
|
|
1690
1694
|
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
1695
|
|
|
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\`;
|
|
1696
|
+
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
1697
|
var CreateGoalSchema = z3.object({
|
|
1694
1698
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
1695
1699
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -1730,12 +1734,13 @@ var UpdateGoalSchema = z3.object({
|
|
|
1730
1734
|
}).strict();
|
|
1731
1735
|
var changedGoal = z3.string().uuid().describe("The Goal to change, as a read shows it.");
|
|
1732
1736
|
var goalTitle = z3.string().trim().min(1).max(80).describe("The Goal's short display name: one to five words, how a person refers to it out loud.");
|
|
1733
|
-
var
|
|
1737
|
+
var GOAL_OUTCOME_MAX = 500;
|
|
1738
|
+
var goalOutcome = z3.string().trim().min(1).max(GOAL_OUTCOME_MAX, `outcome_too_long: a Goal's outcome is one paragraph, at most ${GOAL_OUTCOME_MAX} characters; split the rest into child Goals (create with parentGoalId).`).describe(`The desired result, one paragraph of at most ${GOAL_OUTCOME_MAX} characters. More than that is more work: create child Goals under it (parentGoalId). There is no third description field.`);
|
|
1734
1739
|
var CreateChange = z3.object({
|
|
1735
1740
|
kind: z3.literal("create"),
|
|
1736
1741
|
title: goalTitle,
|
|
1737
1742
|
outcome: goalOutcome,
|
|
1738
|
-
ownerId: z3.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as
|
|
1743
|
+
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
1744
|
parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
|
|
1740
1745
|
sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
|
|
1741
1746
|
}).strict();
|
|
@@ -1756,7 +1761,7 @@ var DependencyChange = z3.object({
|
|
|
1756
1761
|
goalId: changedGoal,
|
|
1757
1762
|
dependsOnGoalId: z3.string().uuid().describe("The Goal it waits on."),
|
|
1758
1763
|
action: z3.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
|
|
1759
|
-
reason: z3.string().trim().min(1).max(
|
|
1764
|
+
reason: z3.string().trim().min(1).max(250).optional().describe("Why, at most 250 characters, as an explanation, never as policy.")
|
|
1760
1765
|
}).strict();
|
|
1761
1766
|
var read = { revision: z3.number().int().positive().optional() };
|
|
1762
1767
|
var GoalChangeSchema = z3.discriminatedUnion("kind", [
|
|
@@ -1780,8 +1785,7 @@ var ManageGoalsSchema = z3.object({
|
|
|
1780
1785
|
var ManageGoalsToolSchema = z3.object({
|
|
1781
1786
|
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
1787
|
}).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();
|
|
1788
|
+
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 (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); 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
1789
|
var GetGoalSchema = z3.object({
|
|
1786
1790
|
goalId: z3.string().uuid(),
|
|
1787
1791
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
@@ -1792,8 +1796,7 @@ var GetGoalSchema = z3.object({
|
|
|
1792
1796
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
1793
1797
|
diagnose: z3.boolean().optional()
|
|
1794
1798
|
}).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).";
|
|
1799
|
+
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
1800
|
var SearchToolSchema = z3.object({
|
|
1798
1801
|
query: z3.string().trim().min(1).max(500),
|
|
1799
1802
|
types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
@@ -1801,7 +1804,7 @@ var SearchToolSchema = z3.object({
|
|
|
1801
1804
|
limit: z3.number().int().min(1).max(20).optional()
|
|
1802
1805
|
}).strict();
|
|
1803
1806
|
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
|
|
1807
|
+
var CheckActivitySchema = z3.object({}).strict();
|
|
1805
1808
|
var FEEDBACK_TEXT_MAX = 5e4;
|
|
1806
1809
|
var SendFeedbackSchema = z3.object({
|
|
1807
1810
|
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 +1813,11 @@ var SendFeedbackSchema = z3.object({
|
|
|
1810
1813
|
}).strict();
|
|
1811
1814
|
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
1815
|
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
1816
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
1815
1817
|
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
1816
|
-
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
1817
1818
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
1818
1819
|
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
|
|
1820
|
+
{ 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
1821
|
{ name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
|
|
1820
1822
|
];
|
|
1821
1823
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
@@ -1864,6 +1866,7 @@ var QuestionChangeSchema = z4.union([
|
|
|
1864
1866
|
z4.object({
|
|
1865
1867
|
kind: z4.literal("create"),
|
|
1866
1868
|
text: z4.string().min(1),
|
|
1869
|
+
summary: z4.string().optional(),
|
|
1867
1870
|
answererId: participant,
|
|
1868
1871
|
goal: ResultRefSchema.optional(),
|
|
1869
1872
|
blocks: z4.array(z4.object({ question: ResultRefSchema }).strict()),
|
|
@@ -1914,12 +1917,12 @@ var BrainSearchSchema = z4.object({
|
|
|
1914
1917
|
}).strict();
|
|
1915
1918
|
var BrainResultSchema = z4.object({
|
|
1916
1919
|
messages: z4.array(BrainMessageSchema),
|
|
1917
|
-
entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema) }).strict()),
|
|
1920
|
+
entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema), summary: z4.string().optional() }).strict()),
|
|
1918
1921
|
questions: z4.array(z4.object({ key, entryKeys: z4.array(key), change: QuestionChangeSchema }).strict()),
|
|
1919
1922
|
answers: z4.array(z4.object({
|
|
1920
1923
|
question: ResultRefSchema,
|
|
1921
1924
|
entryKeys: z4.array(key).min(1),
|
|
1922
|
-
summary: z4.string()
|
|
1925
|
+
summary: z4.string(),
|
|
1923
1926
|
selectedOptionIds: z4.array(z4.string().min(1)).optional()
|
|
1924
1927
|
}).strict()),
|
|
1925
1928
|
goals: z4.array(z4.object({ key, entryKeys: z4.array(key), change: BrainGoalChangeSchema }).strict()),
|
|
@@ -1964,8 +1967,9 @@ var TalkerReplySchema = z5.object({
|
|
|
1964
1967
|
messages: z5.array(z5.object({ key: str2, to: str2, text: str2, about: strs2, blocks: str2 }).strict()),
|
|
1965
1968
|
/** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
|
|
1966
1969
|
then: z5.enum(["listen", "hold", "end"]),
|
|
1967
|
-
/** Items the person's words settle: the item's handle, the chosen option IDs, the line handles
|
|
1968
|
-
|
|
1970
|
+
/** Items the person's words settle: the item's handle, the chosen option IDs, the line handles, and
|
|
1971
|
+
* the answer in at most ten words, keeping any condition. */
|
|
1972
|
+
answers: z5.array(z5.object({ item: str2, options: strs2, lines: strs2, summary: str2 }).strict()),
|
|
1969
1973
|
/** The line handles that hold a new instruction from the person. */
|
|
1970
1974
|
instruction: strs2,
|
|
1971
1975
|
/** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
|
|
@@ -1982,8 +1986,9 @@ var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
|
1982
1986
|
var str3 = z6.string();
|
|
1983
1987
|
var strs3 = z6.array(z6.string());
|
|
1984
1988
|
var FilerReplySchema = z6.object({
|
|
1985
|
-
/** Which Goals each new line goes on: a line's handle
|
|
1986
|
-
|
|
1989
|
+
/** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals), and what
|
|
1990
|
+
* the line says in at most ten words. */
|
|
1991
|
+
filed: z6.array(z6.object({ line: str3, goals: strs3, summary: str3 }).strict()),
|
|
1987
1992
|
goals: z6.array(z6.object({
|
|
1988
1993
|
op: z6.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
|
|
1989
1994
|
key: str3,
|
|
@@ -2002,6 +2007,7 @@ var FilerReplySchema = z6.object({
|
|
|
2002
2007
|
key: str3,
|
|
2003
2008
|
question: str3,
|
|
2004
2009
|
text: str3,
|
|
2010
|
+
summary: str3,
|
|
2005
2011
|
to: str3,
|
|
2006
2012
|
goal: str3,
|
|
2007
2013
|
options: z6.array(z6.object({ id: str3, label: str3 }).strict()),
|
|
@@ -2735,7 +2741,7 @@ var ConnectionSummarySchema = z7.object({
|
|
|
2735
2741
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
2736
2742
|
lastSeenAt: z7.string().datetime().nullable().optional(),
|
|
2737
2743
|
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
2738
|
-
* its Goals —
|
|
2744
|
+
* its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
|
|
2739
2745
|
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
2740
2746
|
lastWorkedAt: z7.string().datetime().nullable().optional(),
|
|
2741
2747
|
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
@@ -2933,7 +2939,7 @@ var QueueItemSchema = z7.object({
|
|
|
2933
2939
|
*
|
|
2934
2940
|
* IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
|
|
2935
2941
|
* writes change which field — the comparison is over the bytes the phone is holding, so a fact
|
|
2936
|
-
* the row shows that no `updated_at` moves for (
|
|
2942
|
+
* the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
|
|
2937
2943
|
* sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
|
|
2938
2944
|
* queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
|
|
2939
2945
|
rev: z7.string().optional()
|
|
@@ -3057,7 +3063,7 @@ var HostDecisionSchema = z7.object({
|
|
|
3057
3063
|
/** The agent's token id: the row's `recipient`. */
|
|
3058
3064
|
agent: z7.string().uuid(),
|
|
3059
3065
|
decision: z7.enum(["stood_back", "took_over"]),
|
|
3060
|
-
/** The work it was about: the Goal
|
|
3066
|
+
/** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
|
|
3061
3067
|
goalId: z7.string().uuid().nullable().optional(),
|
|
3062
3068
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
3063
3069
|
seenAt: z7.string().datetime().nullable().optional(),
|
|
@@ -3250,14 +3256,13 @@ var CallRecapSchema = z7.object({
|
|
|
3250
3256
|
owner: z7.string(),
|
|
3251
3257
|
state: z7.string(),
|
|
3252
3258
|
questions: z7.array(z7.object({ id: z7.string().uuid(), state: z7.string(), title: z7.string() })),
|
|
3253
|
-
/** `words` is always what they SAID, verbatim — the record, never replaced. `
|
|
3254
|
-
*
|
|
3255
|
-
*
|
|
3256
|
-
*
|
|
3257
|
-
* anything that is not an answer. */
|
|
3259
|
+
/** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
|
|
3260
|
+
* the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
|
|
3261
|
+
* gave, so the row scans like a chosen option and their own words stay under it. Absent when
|
|
3262
|
+
* neither wrote one. */
|
|
3258
3263
|
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
3259
3264
|
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
3260
|
-
lines: z7.array(z7.object({ entryId: z7.string().uuid(), words: z7.string(),
|
|
3265
|
+
lines: z7.array(z7.object({ entryId: z7.string().uuid(), words: z7.string(), summary: z7.string().optional(), about: z7.string().nullable().optional() }))
|
|
3261
3266
|
})),
|
|
3262
3267
|
unfiled: z7.array(z7.object({ lineId: z7.string().uuid(), words: z7.string(), atMs: z7.number() })),
|
|
3263
3268
|
more: z7.object({ lines: z7.number(), entries: z7.number(), topics: z7.number() })
|
|
@@ -3438,12 +3443,6 @@ async function manageGoals(input, opts = {}) {
|
|
|
3438
3443
|
if (!res.ok) await fail("manage_goals", res);
|
|
3439
3444
|
return await res.json();
|
|
3440
3445
|
}
|
|
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
3446
|
async function getGoal(goalId, opts = {}, read3 = {}) {
|
|
3448
3447
|
const token = authToken(opts.token) ?? "";
|
|
3449
3448
|
const query = [read3.history ? "history=1" : "", read3.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
|
|
@@ -3622,11 +3621,11 @@ async function sendFeedback(input, opts = {}) {
|
|
|
3622
3621
|
if (!res.ok) await fail("send_feedback", res);
|
|
3623
3622
|
return await res.json();
|
|
3624
3623
|
}
|
|
3625
|
-
async function
|
|
3624
|
+
async function checkActivity(opts = {}) {
|
|
3626
3625
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/agents/working`, {
|
|
3627
3626
|
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` }
|
|
3628
3627
|
}));
|
|
3629
|
-
if (!res.ok) await fail("
|
|
3628
|
+
if (!res.ok) await fail("check_activity", res);
|
|
3630
3629
|
return await res.json();
|
|
3631
3630
|
}
|
|
3632
3631
|
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.`;
|
|
@@ -3861,7 +3860,9 @@ function goalView(g) {
|
|
|
3861
3860
|
})) : void 0,
|
|
3862
3861
|
blockedBy: g.blockedDependencyGoalIds,
|
|
3863
3862
|
dueAt: g.dueAt,
|
|
3864
|
-
|
|
3863
|
+
// Who last wrote on it, and when: any agent can work on any Goal, and working on it is a write.
|
|
3864
|
+
workedBy: g.workedBy,
|
|
3865
|
+
workedAt: g.workedAt,
|
|
3865
3866
|
conversation: lines2,
|
|
3866
3867
|
next: owes ? `${owes}${g.message ?? ""}`.trim() : g.message,
|
|
3867
3868
|
more: more(g)
|
|
@@ -3905,7 +3906,7 @@ function deliveryView(d) {
|
|
|
3905
3906
|
acknowledged: d.acknowledged?.filter((a) => a.acknowledged).map((a) => a.eventId),
|
|
3906
3907
|
next: [
|
|
3907
3908
|
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
|
|
3909
|
+
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
3910
|
].filter(Boolean).join(" ")
|
|
3910
3911
|
});
|
|
3911
3912
|
}
|
|
@@ -3942,8 +3943,8 @@ function receivedView(r, now = Date.now()) {
|
|
|
3942
3943
|
notAcknowledged: refused.length ? refused.map((a) => ({ eventId: a.eventId, refused: a.refused })) : void 0,
|
|
3943
3944
|
// WORK ASSIGNED TO YOU (owner, 2026-09-24): your Goals nobody has started, however they became yours.
|
|
3944
3945
|
assigned,
|
|
3945
|
-
//
|
|
3946
|
-
// sleeping agent, so "nothing is waiting" is never said while
|
|
3946
|
+
// THE WORK WAITING ON YOU NEXT (owner, 2026-09-27: "Wake on any work"): the predicate that starts a
|
|
3947
|
+
// sleeping agent, so "nothing is waiting" is never said while work is.
|
|
3947
3948
|
claimable: w.claimable,
|
|
3948
3949
|
// YOUR STALLED WORK (#2257), and OTHER AGENTS' (owner, 2026-09-23): any agent may take it over.
|
|
3949
3950
|
stalled,
|
|
@@ -3953,10 +3954,10 @@ function receivedView(r, now = Date.now()) {
|
|
|
3953
3954
|
...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
3955
|
...r.hasMore ? ["More are waiting: acknowledge these, then receive again."] : [],
|
|
3955
3956
|
...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 ? [`
|
|
3957
|
+
...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.`] : [],
|
|
3958
|
+
...w.claimable && !assigned.length ? [`Next waiting on you: ${w.claimable.title ?? w.claimable.goalId} (get_goal({ goalId: "${w.claimable.goalId}" })).`] : [],
|
|
3958
3959
|
...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)}:
|
|
3960
|
+
...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
3961
|
// No name of its own (#2525): the API's ask, verbatim.
|
|
3961
3962
|
...w.unnamed ? [w.unnamed] : []
|
|
3962
3963
|
].join(" ")
|
|
@@ -3993,9 +3994,9 @@ async function runTool(name, args, opts) {
|
|
|
3993
3994
|
const sent = await contact(parsed, { ...client, signal, waits });
|
|
3994
3995
|
return "sent" in sent ? notSentView(sent) : deliveryView(sent);
|
|
3995
3996
|
}
|
|
3996
|
-
case "
|
|
3997
|
-
|
|
3998
|
-
return
|
|
3997
|
+
case "check_activity":
|
|
3998
|
+
CheckActivitySchema.parse(input);
|
|
3999
|
+
return checkActivity(client);
|
|
3999
4000
|
case "send_feedback":
|
|
4000
4001
|
return sendFeedback(SendFeedbackSchema.parse(input), client);
|
|
4001
4002
|
case "manage_goals": {
|
|
@@ -4009,10 +4010,6 @@ async function runTool(name, args, opts) {
|
|
|
4009
4010
|
for (const g of managed.goals ?? []) remember(client.token, g.goalId, g.revision);
|
|
4010
4011
|
return managedView(managed, changes);
|
|
4011
4012
|
}
|
|
4012
|
-
case "claim_goal": {
|
|
4013
|
-
const { goalId } = ClaimGoalSchema.parse(input);
|
|
4014
|
-
return goalView(read2(client.token, await claimGoal(goalId, client)));
|
|
4015
|
-
}
|
|
4016
4013
|
case "get_goal": {
|
|
4017
4014
|
const { goalId, history, diagnose } = GetGoalSchema.parse(input);
|
|
4018
4015
|
const goal = read2(client.token, await getGoal(goalId, client, { history, diagnose }));
|
|
@@ -4287,7 +4284,6 @@ export {
|
|
|
4287
4284
|
overrideToken,
|
|
4288
4285
|
authToken,
|
|
4289
4286
|
manageGoals,
|
|
4290
|
-
claimGoal,
|
|
4291
4287
|
getGoal,
|
|
4292
4288
|
AWAIT_WINDOW_MS,
|
|
4293
4289
|
hatch,
|
|
@@ -4307,7 +4303,7 @@ export {
|
|
|
4307
4303
|
acceptTriage,
|
|
4308
4304
|
dismissTriage,
|
|
4309
4305
|
searchRecords,
|
|
4310
|
-
|
|
4306
|
+
checkActivity,
|
|
4311
4307
|
readDelivery,
|
|
4312
4308
|
contact,
|
|
4313
4309
|
receive,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
saveToken,
|
|
7
7
|
setIdentity,
|
|
8
8
|
sleep
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-AHLJPVEP.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
|
|
|
@@ -4,54 +4,38 @@ import {
|
|
|
4
4
|
serverInstructions,
|
|
5
5
|
sessionStartHook,
|
|
6
6
|
withSessionStartHook
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-632JM6QA.js";
|
|
8
8
|
import {
|
|
9
9
|
CODEX_ENV,
|
|
10
10
|
agentName,
|
|
11
11
|
withCodexEnv
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-AHLJPVEP.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,
|
|
@@ -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-AHLJPVEP.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-ZP6NBVEE.js";
|
|
7
|
+
import "./chunk-632JM6QA.js";
|
|
8
|
+
import "./chunk-AHLJPVEP.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-WKAXAJEM.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-ZP6NBVEE.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-632JM6QA.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-AHLJPVEP.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-632JM6QA.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-AHLJPVEP.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-WKAXAJEM.js";
|
|
7
7
|
import {
|
|
8
8
|
autoConfigureClients,
|
|
9
9
|
claudeInstallHint,
|
|
10
10
|
openBrowser
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-ZP6NBVEE.js";
|
|
12
|
+
import "./chunk-632JM6QA.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-AHLJPVEP.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-JTZJFUWD.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