@paigy/mcp 0.40.17 → 0.40.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -215,19 +215,48 @@ script and branch on `$PAIGY_EVENT` there:
215
215
  codex exec "Paigy goal $PAIGY_GOAL_ID. claim_goal it first. The user said: $PAIGY_TEXT. Reply with contact when done."
216
216
  ```
217
217
 
218
- ## Stalled work (Claude Code stop hook)
218
+ ## Stalled work (turn-end hook — Claude Code, Codex, Gemini CLI, Antigravity)
219
219
 
220
220
  An agent that goes quiet leaves its Goals behind. Three things hand it that work back, all
221
221
  reading one rule (no progress for 3 days, `COLD_AFTER_MS`): `check_replies` lists it under
222
222
  `stalled`, `paigy-listen` carries it in `PAIGY_WORK` (and launches for it on boot), and this
223
- hook holds a Claude Code session's stop once a day to list it:
223
+ hook holds a session's turn end once a day to list it. One program, four tools:
224
+
225
+ **Claude Code** (`~/.claude/settings.json`) — installed by the paigy plugin:
224
226
 
225
227
  ```json
226
228
  { "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "npx -y -p @paigy/mcp paigy-stalled" }] }] } }
227
229
  ```
228
230
 
231
+ **Codex** (`~/.codex/config.toml`) — its `Stop` event takes the same `{ decision, reason }`, so
232
+ the same program serves it. Written for you when you pair from another tool:
233
+
234
+ ```toml
235
+ [[hooks.Stop]]
236
+
237
+ [[hooks.Stop.hooks]]
238
+ type = "command"
239
+ command = "npx -y -p @paigy/mcp@latest paigy-stalled --agent codex"
240
+ timeout = 60
241
+ ```
242
+
243
+ **Gemini CLI / Antigravity** (`~/.gemini/settings.json`, `~/.gemini/antigravity-cli/settings.json`)
244
+ — the turn-end event there is `AfterAgent`, and the reminder also arrives as a `systemMessage`,
245
+ which is the field that event promises will reach somebody:
246
+
247
+ ```json
248
+ { "hooks": { "AfterAgent": [{ "matcher": "*", "hooks": [{ "name": "paigy-stalled", "type": "command",
249
+ "command": "npx -y -p @paigy/mcp@latest paigy-stalled --agent antigravity", "timeout": 60000 }] }] } }
250
+ ```
251
+
252
+ `--agent <slot>` is not optional outside Claude Code: a hook is not the MCP server, so it never
253
+ inherits the `PAIGY_AGENT` that server was registered with, and without the flag it reads this
254
+ session's own slot — a different identity, with no stalled work of its own. Pass the same value as
255
+ the tool's MCP registration (`codex`, `antigravity`, or whatever you set `PAIGY_AGENT` to).
256
+
229
257
  It is silent when nothing is stalled, when the stop is already being continued by a hook, and
230
- after it has reminded that session once today.
258
+ after it has reminded that session once today. Codex's older `notify` program is not a place for
259
+ this: it is spawned fire-and-forget with stdio null, so nothing it prints can reach the agent.
231
260
 
232
261
  ## Statusline (Claude Code)
233
262
 
@@ -4,10 +4,10 @@ import {
4
4
  serverInstructions,
5
5
  sessionStartHook,
6
6
  withSessionStartHook
7
- } from "./chunk-MWHYYLCE.js";
7
+ } from "./chunk-HGXXGOGG.js";
8
8
  import {
9
9
  agentName
10
- } from "./chunk-V5AQASR7.js";
10
+ } from "./chunk-VYWIPFEY.js";
11
11
 
12
12
  // src/toolset.ts
13
13
  var ONBOARD_DESCRIPTION = "Get this agent talking to Paigy \u2014 call it FIRST, before contact, and any time you're unsure who you are. One call, and it does whatever the situation needs: NOT SET UP \u2192 hatches an identity instantly if this machine holds a device credential (the user ran the Paigy desktop app or harness), otherwise starts the code ceremony; ALREADY SET UP \u2192 returns your current identity and offers the two things left to decide, renaming it or unpairing; TOKEN NO LONGER VALID \u2192 says so, then re-pairs. Pass { name, voice } to choose who you are when hatching, or to RENAME yourself when already set up (voices: rachel, george, jessica, brian, lily). Safe to call any time: idempotent, and it never writes settings \u2014 the tool-allowlist state it reports is read-only. If it returns a `user_code`, print it to the user immediately and call onboard again with the `device_code`. If it returns `enable_command`, PRINT that command for the user to run \u2014 you cannot apply it yourself (it writes your own permission allowlist, which hosts block as privilege escalation), so print it, don't wait for it, and carry on.";
@@ -99,6 +99,55 @@ function withPaigyMcpToml(existing, agent) {
99
99
  ].join("\n");
100
100
  return existing + entry;
101
101
  }
102
+ function stalledHookCommand(agent) {
103
+ return `npx -y -p @paigy/mcp@latest paigy-stalled --agent ${agent}`;
104
+ }
105
+ function withPaigyStopHookToml(existing, agent) {
106
+ if (existing.includes("paigy-stalled")) return null;
107
+ const entry = [
108
+ "",
109
+ "[[hooks.Stop]]",
110
+ "",
111
+ "[[hooks.Stop.hooks]]",
112
+ 'type = "command"',
113
+ `command = "${stalledHookCommand(agent)}"`,
114
+ "timeout = 60",
115
+ // seconds, in Codex — a cold `npx` resolve is slow the first time
116
+ ""
117
+ ].join("\n");
118
+ return existing + entry;
119
+ }
120
+ function withPaigyStopHookJson(existing, agent) {
121
+ let config = {};
122
+ if (existing) {
123
+ try {
124
+ config = JSON.parse(existing);
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ const hooks = config.hooks ?? {};
130
+ const kept = (hooks.AfterAgent ?? []).map((group) => ({ ...group, hooks: (group.hooks ?? []).filter((hook) => hook.name !== "paigy-stalled") })).filter((group) => group.hooks.length > 0);
131
+ config.hooks = {
132
+ ...hooks,
133
+ AfterAgent: [
134
+ ...kept,
135
+ {
136
+ matcher: "*",
137
+ hooks: [
138
+ {
139
+ name: "paigy-stalled",
140
+ type: "command",
141
+ command: stalledHookCommand(agent),
142
+ timeout: 6e4
143
+ // milliseconds, in Gemini CLI
144
+ }
145
+ ]
146
+ }
147
+ ]
148
+ };
149
+ return JSON.stringify(config, null, 2);
150
+ }
102
151
  function autoConfigureClients(skip = agentName()) {
103
152
  const home = homedir();
104
153
  const registered = [];
@@ -118,6 +167,20 @@ function autoConfigureClients(skip = agentName()) {
118
167
  } catch {
119
168
  }
120
169
  }
170
+ for (const path of [
171
+ join(home, ".gemini", "settings.json"),
172
+ join(home, ".gemini", "antigravity-cli", "settings.json")
173
+ ]) {
174
+ try {
175
+ if (!existsSync(dirname(path))) continue;
176
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
177
+ const updated = withPaigyStopHookJson(existing, "antigravity");
178
+ if (updated === null) continue;
179
+ writeFileSync(path, updated);
180
+ registered.push(`Paigy's turn-end reminder (${path})`);
181
+ } catch {
182
+ }
183
+ }
121
184
  }
122
185
  if (skip !== "codex") {
123
186
  const codexConfig = join(home, ".codex", "config.toml");
@@ -129,6 +192,12 @@ function autoConfigureClients(skip = agentName()) {
129
192
  writeFileSync(codexConfig, updated);
130
193
  registered.push(`OpenAI Codex (${codexConfig})`);
131
194
  }
195
+ const afterMcp = existsSync(codexConfig) ? readFileSync(codexConfig, "utf8") : "";
196
+ const withHook = withPaigyStopHookToml(afterMcp, "codex");
197
+ if (withHook !== null) {
198
+ writeFileSync(codexConfig, withHook);
199
+ registered.push(`Paigy's turn-end reminder (${codexConfig})`);
200
+ }
132
201
  }
133
202
  } catch {
134
203
  }
@@ -113,8 +113,13 @@ var UpdateGoalSchema = z3.object({
113
113
  }).strict();
114
114
  var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z3.string().uuid() }).strict();
115
115
  var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
116
- var GetGoalSchema = z3.object({ goalId: z3.string().uuid() }).strict();
117
- var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
116
+ var GetGoalSchema = z3.object({
117
+ goalId: z3.string().uuid(),
118
+ /** Every entry in full. Without it the read carries the person's words, open questions and your
119
+ * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
120
+ history: z3.boolean().optional()
121
+ }).strict();
122
+ var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: 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. 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. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.";
118
123
  var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
119
124
  var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
120
125
  var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
@@ -128,9 +133,10 @@ var AGENT_TOOLS = [
128
133
  { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
129
134
  ];
130
135
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
136
+ var DONE_MESSAGE = "When the work is done, send one message through contact saying what is done and anything the person needs to do or check.";
131
137
  function serverInstructions(opts) {
132
138
  const calls = opts.waits ? "A Call holds one bounded window here; continue with contact({deliveryId}) to hold the next or reread that exact Delivery." : "A Call returns after one read; continue with contact({deliveryId}) to reread that exact Delivery.";
133
- return `On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and the conversation on it. get_goal rereads it without claiming. Every read ends in \`next\`, the one step to take. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. ${calls} Evidence can repeat: reads do not acknowledge or hide it. Report progress and acknowledge review with update_goal, never as a contact. 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.
139
+ return `On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and the conversation on it. get_goal rereads it without claiming. Every read ends in \`next\`, the one step to take. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. ${calls} Evidence can repeat: reads do not acknowledge or hide it. Report progress and acknowledge review with update_goal, never as a contact. ${DONE_MESSAGE} 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.
134
140
 
135
141
  HOW TO ASK:
136
142
  1. One question per ask. Five questions are five objects in \`asks\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
@@ -558,7 +564,13 @@ var InboxItemSchema = z4.object({
558
564
  * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
559
565
  * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
560
566
  * author is not an agent this account holds (unpaired since, or a person). */
561
- agent: z4.object({ tokenId: z4.string(), name: z4.string(), voice: VoiceKeySchema.optional() }).optional()
567
+ agent: z4.object({ tokenId: z4.string(), name: z4.string(), voice: VoiceKeySchema.optional() }).optional(),
568
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
569
+ * its need offers, exactly as its own card carries them, present only when one of them has a
570
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
571
+ * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
572
+ * says those, and the list stays small (an `html` is up to 16 KB). */
573
+ options: z4.array(OptionSchema).optional()
562
574
  })).optional(),
563
575
  visuals: z4.array(VisualSchema).optional(),
564
576
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -954,7 +966,19 @@ var QueueItemSchema = z4.object({
954
966
  /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
955
967
  * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
956
968
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
957
- lastProgressAt: z4.string().nullable().optional()
969
+ lastProgressAt: z4.string().nullable().optional(),
970
+ /** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
971
+ * kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
972
+ * note. A progress note is an Entry, so this is already the newer of the two: the person's note
973
+ * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
974
+ * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
975
+ * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
976
+ latest: z4.object({
977
+ from: z4.enum(["person", "agent"]),
978
+ said: z4.string(),
979
+ at: z4.string(),
980
+ entryId: z4.string()
981
+ }).nullable().optional()
958
982
  });
959
983
  var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
960
984
  var NoteSourceSchema = z4.enum(["app", "call"]);
@@ -1130,6 +1154,104 @@ var NotificationFeedbackKindSchema = z4.enum([
1130
1154
  "other"
1131
1155
  // anything else — the note carries it.
1132
1156
  ]);
1157
+ var SlimOptionSchema = OptionSchema.omit({ html: true });
1158
+ var QuestionRowSchema = z4.object({
1159
+ /** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
1160
+ id: z4.string(),
1161
+ deliveryId: z4.string(),
1162
+ entryId: z4.string(),
1163
+ /** The decision it waits on; null for an update, which asks nothing. */
1164
+ needId: z4.string().nullable(),
1165
+ goalIds: z4.array(z4.string()),
1166
+ /** The name of the work it is about, when the read could word it. */
1167
+ goalTitle: z4.string().optional(),
1168
+ tokenId: z4.string().optional(),
1169
+ name: z4.string(),
1170
+ title: z4.string(),
1171
+ body: z4.string(),
1172
+ select: z4.enum(["one", "many", "rank", "confirm", "text"]),
1173
+ options: z4.array(SlimOptionSchema),
1174
+ hasPreview: z4.boolean(),
1175
+ blocking: z4.boolean(),
1176
+ askedAt: z4.string().datetime(),
1177
+ ring: InboxItemSchema.shape.ring,
1178
+ onCall: z4.literal(true).optional(),
1179
+ sealed: z4.boolean()
1180
+ });
1181
+ var WorkStateSchema = z4.enum(["ready", "active", "waiting", "done", "cancelled"]);
1182
+ var WorkRowSchema = z4.object({
1183
+ id: z4.string(),
1184
+ parentId: z4.string().nullable(),
1185
+ title: z4.string(),
1186
+ /** Straight off the Goal. */
1187
+ state: WorkStateSchema,
1188
+ owner: z4.string().nullable(),
1189
+ revision: z4.number().int(),
1190
+ /** Open questions on it, counted to 100. */
1191
+ waiting: z4.number().int(),
1192
+ /** Held by a gate on work that is not done. */
1193
+ blocked: z4.boolean(),
1194
+ lastProgressAt: z4.string().datetime().nullable(),
1195
+ /** The line written for its newest progress note, else that note's first words. */
1196
+ line: z4.string().nullable(),
1197
+ /** Work directly under it, counted to 100; the list carries up to 12 of them. */
1198
+ children: z4.number().int(),
1199
+ createdAt: z4.string().datetime(),
1200
+ updatedAt: z4.string().datetime(),
1201
+ /** When anything at or under it last moved — the order the list is in. */
1202
+ activeAt: z4.string().datetime(),
1203
+ /** A sealed outcome has no title here; the work's page opens it. */
1204
+ sealed: z4.boolean()
1205
+ });
1206
+ var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
1207
+ var AgentRowSchema = ComputerRowSchema.extend({
1208
+ /** Open questions it is asking the person, over every open card; null when that read failed. */
1209
+ asking: z4.number().int().nullable(),
1210
+ oldestAskAt: z4.string().datetime().nullable(),
1211
+ /** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
1212
+ * many in all among the account's 200 most recently active agent-held live Goals
1213
+ * (`agent_holds`); null when that read failed. */
1214
+ holds: z4.array(z4.object({ id: z4.string(), title: z4.string() })).nullable(),
1215
+ held: z4.number().int().nullable(),
1216
+ /** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
1217
+ * while none has, or when that read failed. */
1218
+ cold: z4.string().datetime().nullable(),
1219
+ /** The newest line of its working log, and when the harness saw it. */
1220
+ line: z4.string().nullable(),
1221
+ lineAt: z4.string().datetime().nullable()
1222
+ });
1223
+ var SnapshotSchema = z4.object({
1224
+ /** The API's clock, taken before the first read: what a later delta will start from. */
1225
+ at: z4.string().datetime(),
1226
+ questions: z4.object({
1227
+ /** The newest 30 open cards, questions before updates. */
1228
+ items: z4.array(QuestionRowSchema),
1229
+ /** Every open question, and apart from them every update, and what was put off. */
1230
+ total: z4.number().int(),
1231
+ updates: z4.number().int(),
1232
+ putOff: z4.number().int()
1233
+ }).nullable(),
1234
+ agents: z4.object({
1235
+ /** Up to 60, most recently seen first. */
1236
+ items: z4.array(AgentRowSchema),
1237
+ more: z4.boolean()
1238
+ }).nullable(),
1239
+ work: z4.object({
1240
+ /** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
1241
+ items: z4.array(WorkRowSchema),
1242
+ /** How much work is behind each of the Work tab's four filters, each counted to 100, read with
1243
+ * the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
1244
+ * `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
1245
+ * is `ready`; Done is `done` or `cancelled`. */
1246
+ counts: z4.object({ live: z4.number().int(), waiting: z4.number().int(), notStarted: z4.number().int(), done: z4.number().int() })
1247
+ }).nullable(),
1248
+ you: z4.object({
1249
+ settings: UserSettingsSchema,
1250
+ callable: z4.boolean(),
1251
+ /** Up to 20 paired computers; null when the roster read failed. */
1252
+ computers: z4.array(ComputerRowSchema).nullable()
1253
+ }).nullable()
1254
+ });
1133
1255
 
1134
1256
  // src/listening.ts
1135
1257
  import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -1,10 +1,12 @@
1
1
  import {
2
+ NameTakenError,
2
3
  isNetworkError,
3
4
  pollToken,
4
5
  requestCode,
5
6
  saveToken,
7
+ setIdentity,
6
8
  sleep
7
- } from "./chunk-V5AQASR7.js";
9
+ } from "./chunk-VYWIPFEY.js";
8
10
 
9
11
  // src/identity.ts
10
12
  var CLIENT_LABELS = {
@@ -25,6 +27,21 @@ function suggestedAgentName(clientName) {
25
27
  if (!clientName) return void 0;
26
28
  return CLIENT_LABELS[clientName.toLowerCase()] ?? clientName.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
27
29
  }
30
+ async function identify(who, ask) {
31
+ let renamed = false;
32
+ if (ask.name || ask.voice) {
33
+ try {
34
+ who = await setIdentity({ ...ask.name ? { name: ask.name } : {}, ...ask.voice ? { voice: ask.voice } : {} });
35
+ renamed = true;
36
+ } catch (e) {
37
+ const message2 = e instanceof NameTakenError ? e.message : `The rename did not go through (${e.message}); nothing changed. Call onboard again to retry, or carry on.`;
38
+ return { name: who.name, voice: who.voice, message: message2 };
39
+ }
40
+ }
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 unpair; otherwise carry on.`;
43
+ return { name: who.name, voice: who.voice, message };
44
+ }
28
45
 
29
46
  // src/pairing.ts
30
47
  async function resolvePairing(deviceCode, capMs, pollMs = 2e3) {
@@ -102,6 +119,7 @@ function clearPairing(deviceCode) {
102
119
 
103
120
  export {
104
121
  suggestedAgentName,
122
+ identify,
105
123
  resolvePairing,
106
124
  startPairing,
107
125
  joinBackgroundPair,
@@ -1581,8 +1581,13 @@ var UpdateGoalSchema = z3.object({
1581
1581
  }).strict();
1582
1582
  var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z3.string().uuid() }).strict();
1583
1583
  var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
1584
- var GetGoalSchema = z3.object({ goalId: z3.string().uuid() }).strict();
1585
- var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
1584
+ var GetGoalSchema = z3.object({
1585
+ goalId: z3.string().uuid(),
1586
+ /** Every entry in full. Without it the read carries the person's words, open questions and your
1587
+ * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
1588
+ history: z3.boolean().optional()
1589
+ }).strict();
1590
+ var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: 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. 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. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.";
1586
1591
  var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
1587
1592
  var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
1588
1593
  var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
@@ -2016,7 +2021,13 @@ var InboxItemSchema = z4.object({
2016
2021
  * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
2017
2022
  * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
2018
2023
  * author is not an agent this account holds (unpaired since, or a person). */
2019
- agent: z4.object({ tokenId: z4.string(), name: z4.string(), voice: VoiceKeySchema.optional() }).optional()
2024
+ agent: z4.object({ tokenId: z4.string(), name: z4.string(), voice: VoiceKeySchema.optional() }).optional(),
2025
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
2026
+ * its need offers, exactly as its own card carries them, present only when one of them has a
2027
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
2028
+ * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
2029
+ * says those, and the list stays small (an `html` is up to 16 KB). */
2030
+ options: z4.array(OptionSchema).optional()
2020
2031
  })).optional(),
2021
2032
  visuals: z4.array(VisualSchema).optional(),
2022
2033
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -2412,7 +2423,19 @@ var QueueItemSchema = z4.object({
2412
2423
  /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
2413
2424
  * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
2414
2425
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
2415
- lastProgressAt: z4.string().nullable().optional()
2426
+ lastProgressAt: z4.string().nullable().optional(),
2427
+ /** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
2428
+ * kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
2429
+ * note. A progress note is an Entry, so this is already the newer of the two: the person's note
2430
+ * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
2431
+ * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
2432
+ * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
2433
+ latest: z4.object({
2434
+ from: z4.enum(["person", "agent"]),
2435
+ said: z4.string(),
2436
+ at: z4.string(),
2437
+ entryId: z4.string()
2438
+ }).nullable().optional()
2416
2439
  });
2417
2440
  var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
2418
2441
  var NoteSourceSchema = z4.enum(["app", "call"]);
@@ -2590,6 +2613,104 @@ var NotificationFeedbackKindSchema = z4.enum([
2590
2613
  "other"
2591
2614
  // anything else — the note carries it.
2592
2615
  ]);
2616
+ var SlimOptionSchema = OptionSchema.omit({ html: true });
2617
+ var QuestionRowSchema = z4.object({
2618
+ /** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
2619
+ id: z4.string(),
2620
+ deliveryId: z4.string(),
2621
+ entryId: z4.string(),
2622
+ /** The decision it waits on; null for an update, which asks nothing. */
2623
+ needId: z4.string().nullable(),
2624
+ goalIds: z4.array(z4.string()),
2625
+ /** The name of the work it is about, when the read could word it. */
2626
+ goalTitle: z4.string().optional(),
2627
+ tokenId: z4.string().optional(),
2628
+ name: z4.string(),
2629
+ title: z4.string(),
2630
+ body: z4.string(),
2631
+ select: z4.enum(["one", "many", "rank", "confirm", "text"]),
2632
+ options: z4.array(SlimOptionSchema),
2633
+ hasPreview: z4.boolean(),
2634
+ blocking: z4.boolean(),
2635
+ askedAt: z4.string().datetime(),
2636
+ ring: InboxItemSchema.shape.ring,
2637
+ onCall: z4.literal(true).optional(),
2638
+ sealed: z4.boolean()
2639
+ });
2640
+ var WorkStateSchema = z4.enum(["ready", "active", "waiting", "done", "cancelled"]);
2641
+ var WorkRowSchema = z4.object({
2642
+ id: z4.string(),
2643
+ parentId: z4.string().nullable(),
2644
+ title: z4.string(),
2645
+ /** Straight off the Goal. */
2646
+ state: WorkStateSchema,
2647
+ owner: z4.string().nullable(),
2648
+ revision: z4.number().int(),
2649
+ /** Open questions on it, counted to 100. */
2650
+ waiting: z4.number().int(),
2651
+ /** Held by a gate on work that is not done. */
2652
+ blocked: z4.boolean(),
2653
+ lastProgressAt: z4.string().datetime().nullable(),
2654
+ /** The line written for its newest progress note, else that note's first words. */
2655
+ line: z4.string().nullable(),
2656
+ /** Work directly under it, counted to 100; the list carries up to 12 of them. */
2657
+ children: z4.number().int(),
2658
+ createdAt: z4.string().datetime(),
2659
+ updatedAt: z4.string().datetime(),
2660
+ /** When anything at or under it last moved — the order the list is in. */
2661
+ activeAt: z4.string().datetime(),
2662
+ /** A sealed outcome has no title here; the work's page opens it. */
2663
+ sealed: z4.boolean()
2664
+ });
2665
+ var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
2666
+ var AgentRowSchema = ComputerRowSchema.extend({
2667
+ /** Open questions it is asking the person, over every open card; null when that read failed. */
2668
+ asking: z4.number().int().nullable(),
2669
+ oldestAskAt: z4.string().datetime().nullable(),
2670
+ /** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
2671
+ * many in all among the account's 200 most recently active agent-held live Goals
2672
+ * (`agent_holds`); null when that read failed. */
2673
+ holds: z4.array(z4.object({ id: z4.string(), title: z4.string() })).nullable(),
2674
+ held: z4.number().int().nullable(),
2675
+ /** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
2676
+ * while none has, or when that read failed. */
2677
+ cold: z4.string().datetime().nullable(),
2678
+ /** The newest line of its working log, and when the harness saw it. */
2679
+ line: z4.string().nullable(),
2680
+ lineAt: z4.string().datetime().nullable()
2681
+ });
2682
+ var SnapshotSchema = z4.object({
2683
+ /** The API's clock, taken before the first read: what a later delta will start from. */
2684
+ at: z4.string().datetime(),
2685
+ questions: z4.object({
2686
+ /** The newest 30 open cards, questions before updates. */
2687
+ items: z4.array(QuestionRowSchema),
2688
+ /** Every open question, and apart from them every update, and what was put off. */
2689
+ total: z4.number().int(),
2690
+ updates: z4.number().int(),
2691
+ putOff: z4.number().int()
2692
+ }).nullable(),
2693
+ agents: z4.object({
2694
+ /** Up to 60, most recently seen first. */
2695
+ items: z4.array(AgentRowSchema),
2696
+ more: z4.boolean()
2697
+ }).nullable(),
2698
+ work: z4.object({
2699
+ /** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
2700
+ items: z4.array(WorkRowSchema),
2701
+ /** How much work is behind each of the Work tab's four filters, each counted to 100, read with
2702
+ * the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
2703
+ * `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
2704
+ * is `ready`; Done is `done` or `cancelled`. */
2705
+ counts: z4.object({ live: z4.number().int(), waiting: z4.number().int(), notStarted: z4.number().int(), done: z4.number().int() })
2706
+ }).nullable(),
2707
+ you: z4.object({
2708
+ settings: UserSettingsSchema,
2709
+ callable: z4.boolean(),
2710
+ /** Up to 20 paired computers; null when the roster read failed. */
2711
+ computers: z4.array(ComputerRowSchema).nullable()
2712
+ }).nullable()
2713
+ });
2593
2714
  function sessionSlot(sessionId2) {
2594
2715
  const id = sessionId2 ?? sessionId();
2595
2716
  return `session:${id.slice(0, 8)}`;
@@ -2757,9 +2878,9 @@ async function claimGoal(goalId, opts = {}) {
2757
2878
  if (!res.ok) await fail("claim_goal", res);
2758
2879
  return await res.json();
2759
2880
  }
2760
- async function getGoal(goalId, opts = {}) {
2881
+ async function getGoal(goalId, opts = {}, read = {}) {
2761
2882
  const token = authToken(opts.token) ?? "";
2762
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, {
2883
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${read.history ? "?history=1" : ""}`, {
2763
2884
  method: "GET",
2764
2885
  headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
2765
2886
  }));
@@ -2793,13 +2914,27 @@ async function whoAmI(opts = {}) {
2793
2914
  return null;
2794
2915
  }
2795
2916
  }
2917
+ var NameTakenError = class extends Error {
2918
+ code = "agent_name_taken";
2919
+ };
2796
2920
  async function setIdentity(patch, opts = {}) {
2797
2921
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/identity`, {
2798
2922
  method: "PATCH",
2799
2923
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
2800
2924
  body: JSON.stringify(patch)
2801
2925
  }));
2802
- if (!res.ok) throw new Error(`set_identity failed: ${res.status} ${await res.text()}`);
2926
+ if (!res.ok) {
2927
+ const text = await res.text();
2928
+ const refusal = (() => {
2929
+ try {
2930
+ return JSON.parse(text);
2931
+ } catch {
2932
+ return null;
2933
+ }
2934
+ })();
2935
+ if (res.status === 409 && refusal?.error === "agent_name_taken" && refusal.message) throw new NameTakenError(refusal.message);
2936
+ throw new Error(`set_identity failed: ${res.status} ${text}`);
2937
+ }
2803
2938
  return await res.json();
2804
2939
  }
2805
2940
  async function claimSessions(opts = {}) {
@@ -3008,38 +3143,53 @@ function decided(result) {
3008
3143
  return void 0;
3009
3144
  }
3010
3145
  }
3146
+ var handle = (entryId) => entryId.slice(0, 8);
3011
3147
  function conversation(e) {
3012
3148
  const answers = new Map((e.answers ?? []).map((a) => [a.decisionNeedId, a]));
3149
+ const shown = new Set((e.entries ?? []).map((entry) => entry.entryId));
3150
+ const repliedTo = new Set((e.entries ?? []).flatMap((entry) => entry.aboutId && shown.has(entry.aboutId) ? [entry.aboutId] : []));
3013
3151
  return (e.entries ?? []).map((entry) => {
3014
3152
  const sealed = !!entry.content && "sealed" in entry.content;
3015
3153
  const plain = entry.content && "plain" in entry.content ? entry.content.plain : null;
3016
3154
  const options = plain && typeof plain === "object" && Array.isArray(plain.options) ? plain.options.map((o) => o.label).filter((l) => typeof l === "string") : [];
3017
3155
  const need = (e.decisionNeeds ?? []).find((n) => n.requestEntryId === entry.entryId);
3018
3156
  const answer = need ? answers.get(need.decisionNeedId) : void 0;
3019
- const answered = answer ? decided(answer.result) : void 0;
3157
+ const decision = answer ? decided(answer.result) : void 0;
3020
3158
  return compact({
3021
- id: entry.entryId,
3159
+ id: repliedTo.has(entry.entryId) ? handle(entry.entryId) : void 0,
3022
3160
  from: who(entry.authorParticipant),
3023
3161
  at: at(entry.createdAt),
3024
3162
  said: sealed ? "[encrypted]" : entryWords(entry).trim(),
3025
- re: entry.aboutId ?? void 0,
3163
+ re: entry.aboutId && shown.has(entry.aboutId) ? handle(entry.aboutId) : void 0,
3026
3164
  options,
3027
- decision: need ? compact({ id: need.decisionNeedId, state: need.state, answer: answered }) : void 0
3165
+ decision: need ? compact({ state: need.state, answer: decision }) : void 0
3028
3166
  });
3029
3167
  });
3030
3168
  }
3169
+ function more(g) {
3170
+ const earlier = g.earlier && g.earlier.omitted + g.earlier.shortened ? `${[g.earlier.omitted ? `${g.earlier.omitted} earlier entries left out` : "", g.earlier.shortened ? `${g.earlier.shortened} shortened to their headline` : ""].filter(Boolean).join(", ")}: get_goal({ goalId: "${g.goalId}", history: true }) reads every one in full.` : null;
3171
+ return [
3172
+ earlier,
3173
+ g.subtree?.length ? "A Goal below, in full: get_goal({ goalId }) with its goalId above." : null,
3174
+ g.parentGoalId ? `The Goal above: get_goal({ goalId: "${g.parentGoalId}" }).` : null,
3175
+ "Anything else waiting for you (other requests, answers, reviews): check_replies()."
3176
+ ].filter((line) => !!line);
3177
+ }
3031
3178
  function goalView(g) {
3032
3179
  if (!g.goalId) return compact({ state: g.state, next: g.message });
3180
+ const lines2 = conversation(g);
3181
+ const newest = [...lines2].reverse().find((l) => l.from !== "person");
3033
3182
  return compact({
3034
3183
  goalId: g.goalId,
3035
3184
  revision: g.revision,
3036
3185
  state: g.state,
3037
3186
  outcome: g.outcome,
3038
- progress: g.progress,
3187
+ progress: g.progress && g.progress.trim() !== newest?.said ? g.progress : void 0,
3039
3188
  reviewPending: g.reviewPending || void 0,
3040
3189
  owner: g.ownerParticipant,
3041
3190
  parentGoalId: g.parentGoalId,
3042
- childGoalIds: g.childGoalIds,
3191
+ // Every child is in the subtree below, with its goalId: listed again, it was said twice.
3192
+ childGoalIds: g.subtree?.length ? void 0 : g.childGoalIds,
3043
3193
  // EVERYTHING BELOW, read through this Goal (umbrella reads): one line per Goal, indented by
3044
3194
  // depth, then each question with its answer — so an agent that asked through an umbrella
3045
3195
  // reads every answer in one call, whoever owns the child it landed on.
@@ -3051,8 +3201,9 @@ function goalView(g) {
3051
3201
  blockedBy: g.blockedDependencyGoalIds,
3052
3202
  dueAt: g.dueAt,
3053
3203
  leaseExpiresAt: g.leaseExpiresAt,
3054
- conversation: conversation(g),
3055
- next: g.message
3204
+ conversation: lines2,
3205
+ next: g.message,
3206
+ more: more(g)
3056
3207
  });
3057
3208
  }
3058
3209
  function deliveryView(d) {
@@ -3089,6 +3240,10 @@ function repliesView(r) {
3089
3240
  // WORK ASSIGNED TO YOU (owner, 2026-09-24): your Goals nobody has started, however they
3090
3241
  // became yours — a Goal handed to you is not a Delivery, and must not wait for a claim to be seen.
3091
3242
  ...assigned.length ? { assigned } : {},
3243
+ // WHAT claim_goal HANDS YOU NEXT (owner, 2026-09-27: "Wake on any work"): the same predicate that
3244
+ // starts a sleeping agent, so "nothing is waiting" is never said while a claim would hand you work
3245
+ // — a Goal you started in a process that died is on no other list.
3246
+ ...r.claimable ? { claimable: r.claimable } : {},
3092
3247
  // NEW ON YOUR GOALS (owner, 2026-09-23): a reply is an Entry on the Goal, not a Delivery, so the
3093
3248
  // Goals flagged for your review are listed here until you acknowledge them.
3094
3249
  ...review.length ? { review } : {},
@@ -3098,11 +3253,13 @@ function repliesView(r) {
3098
3253
  // OTHER AGENTS' STALLED WORK (owner, 2026-09-23): any agent may take it over and close it.
3099
3254
  ...others.length ? { stalledOthers: others } : {},
3100
3255
  next: [
3101
- deliveries.length || review.length || assigned.length ? "Act on a Goal with claim_goal; reread one Delivery in full with contact({deliveryId})." : "Nothing is waiting.",
3256
+ deliveries.length || r.claimable || review.length || assigned.length ? "Act on a Goal with claim_goal; reread one Delivery in full with contact({deliveryId})." : "Nothing is waiting.",
3102
3257
  ...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned): claim_goal({goalId}) to start one.`] : [],
3103
3258
  ...review.length ? [`${review.length} of your Goals have something new (review): read each with claim_goal or get_goal, then update_goal with reviewed: true.`] : [],
3104
3259
  ...stalled.length ? [`${stalled.length} of your Goals have had no progress for 3 days: update_goal each with progress, finish it, or cancel it.`] : [],
3105
- ...others.length ? [`${others.length} of other agents' Goals have gone quiet for 3 days: claim_goal({goalId}) takes one over, then finish or cancel it with update_goal.`] : []
3260
+ ...others.length ? [`${others.length} of other agents' Goals have gone quiet for 3 days: claim_goal({goalId}) takes one over, then finish or cancel it with update_goal.`] : [],
3261
+ // No name of its own (#2525): the API's ask, verbatim.
3262
+ ...r.unnamed ? [r.unnamed] : []
3106
3263
  ].join(" ")
3107
3264
  };
3108
3265
  }
@@ -3126,8 +3283,8 @@ async function runTool(name, args, opts) {
3126
3283
  return goalView(await claimGoal(goalId, client));
3127
3284
  }
3128
3285
  case "get_goal": {
3129
- const { goalId } = GetGoalSchema.parse(input);
3130
- return goalView(await getGoal(goalId, client));
3286
+ const { goalId, history } = GetGoalSchema.parse(input);
3287
+ return goalView(await getGoal(goalId, client, { history }));
3131
3288
  }
3132
3289
  case "update_goal": {
3133
3290
  const { goalId, ...body } = UpdateGoalToolSchema.parse(input);
@@ -3352,6 +3509,7 @@ export {
3352
3509
  AWAIT_WINDOW_MS,
3353
3510
  hatch,
3354
3511
  whoAmI,
3512
+ NameTakenError,
3355
3513
  setIdentity,
3356
3514
  claimSessions,
3357
3515
  heartbeat,
@@ -6,6 +6,7 @@ import {
6
6
  BACKEND_URL,
7
7
  KEY_PATH,
8
8
  NETWORK_MSG,
9
+ NameTakenError,
9
10
  NotifyRequestSchema,
10
11
  PROTO,
11
12
  TOKEN_PATH,
@@ -52,7 +53,7 @@ import {
52
53
  updateGoal,
53
54
  updateSlot,
54
55
  whoAmI
55
- } from "./chunk-V5AQASR7.js";
56
+ } from "./chunk-VYWIPFEY.js";
56
57
  export {
57
58
  AGENT_TOOLS,
58
59
  AGENT_TOOL_NAMES,
@@ -61,6 +62,7 @@ export {
61
62
  BACKEND_URL,
62
63
  KEY_PATH,
63
64
  NETWORK_MSG,
65
+ NameTakenError,
64
66
  NotifyRequestSchema,
65
67
  PROTO,
66
68
  TOKEN_PATH,
package/dist/enable.js CHANGED
@@ -3,9 +3,9 @@ import {
3
3
  PAIGY_TOOL_IDS,
4
4
  enablePaigyTools,
5
5
  installSessionListening
6
- } from "./chunk-7VLZIOLF.js";
7
- import "./chunk-MWHYYLCE.js";
8
- import "./chunk-V5AQASR7.js";
6
+ } from "./chunk-76ILN3YV.js";
7
+ import "./chunk-HGXXGOGG.js";
8
+ import "./chunk-VYWIPFEY.js";
9
9
 
10
10
  // src/enable.ts
11
11
  function main() {
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  clearPairing,
4
+ identify,
4
5
  joinBackgroundPair,
5
6
  resolvePairing,
6
7
  startPairing,
7
8
  suggestedAgentName
8
- } from "./chunk-UIZLBPRG.js";
9
+ } from "./chunk-TRMYDJ5H.js";
9
10
  import {
10
11
  ENABLE_COMMAND,
11
12
  OnboardSchema,
@@ -14,11 +15,11 @@ import {
14
15
  SERVER_INSTRUCTIONS,
15
16
  TOOLS,
16
17
  paigyToolsAllowlisted
17
- } from "./chunk-7VLZIOLF.js";
18
+ } from "./chunk-76ILN3YV.js";
18
19
  import {
19
20
  decideListen,
20
21
  listenerAlive
21
- } from "./chunk-MWHYYLCE.js";
22
+ } from "./chunk-HGXXGOGG.js";
22
23
  import {
23
24
  clearSurface,
24
25
  writeSurface
@@ -36,10 +37,9 @@ import {
36
37
  runTool,
37
38
  saveToken,
38
39
  sessionId,
39
- setIdentity,
40
40
  slotName,
41
41
  whoAmI
42
- } from "./chunk-V5AQASR7.js";
42
+ } from "./chunk-VYWIPFEY.js";
43
43
 
44
44
  // src/index.ts
45
45
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -177,7 +177,7 @@ async function hatchUnderDevice(name, voice) {
177
177
  const minted = await hatch(name ?? suggestedAgentName2() ?? "Agent", voice ?? null);
178
178
  const dt = { ok: true, access_token: minted.token, name: minted.name, device: null };
179
179
  saveToken(dt);
180
- return dt;
180
+ return minted.message ? { ...dt, message: minted.message } : dt;
181
181
  } catch (e) {
182
182
  return { why: "credential-rejected", detail: String(e?.message ?? e).slice(0, 200) };
183
183
  } finally {
@@ -191,7 +191,7 @@ async function runPair(device_code, name, voice, note) {
191
191
  if ("ok" in dt) return pairedResult(
192
192
  dt,
193
193
  void 0,
194
- "Hatched instantly under this device's credential \u2014 no code needed. " + (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 re-call pair with { name, voice }.`)
194
+ "Hatched instantly under this device's credential \u2014 no code needed. " + (dt.message ? `${dt.message} Set it with onboard({ 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 re-call pair with { name, voice }.`)
195
195
  );
196
196
  miss = dt;
197
197
  }
@@ -246,24 +246,16 @@ async function handleTool(request, signal) {
246
246
  const slot = agentName();
247
247
  const secret = readToken();
248
248
  if (secret) {
249
- let who = await whoAmI().catch(() => null);
249
+ const who = await whoAmI().catch(() => null);
250
250
  if (who) {
251
- let renamed = false;
252
- if (name || voice) {
253
- try {
254
- who = await setIdentity({ ...name ? { name } : {}, ...voice ? { voice } : {} });
255
- renamed = true;
256
- } catch {
257
- }
258
- }
251
+ const me = await identify(who, { ...name ? { name } : {}, ...voice ? { voice } : {} });
259
252
  const allowlisted = paigyToolsAllowlisted(scope ?? "user");
260
- const display = who?.name ?? slotName(slot) ?? slot;
261
253
  return {
262
254
  content: [{ type: "text", text: JSON.stringify({
263
255
  status: "ready",
264
256
  slot,
265
- name: display,
266
- voice: who?.voice ?? null,
257
+ name: me.name,
258
+ voice: me.voice,
267
259
  tools_allowlisted: allowlisted,
268
260
  ...allowlisted ? {} : {
269
261
  enable_tool_ids: PAIGY_TOOL_IDS,
@@ -277,7 +269,7 @@ async function handleTool(request, signal) {
277
269
  rename: "Call onboard again with { name } (and optionally { voice }) to change it.",
278
270
  unpair: "Call the `unpair` tool to disconnect this agent from the account."
279
271
  },
280
- message: renamed ? `Renamed \u2014 this agent is "${display}" now.` : `Already set up as "${display}". Ask the user if they want to rename it or unpair; otherwise carry on.`
272
+ message: me.message
281
273
  }) }]
282
274
  };
283
275
  }
package/dist/listen.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  entryWords,
4
4
  removeListenMark,
5
5
  writeListenMark
6
- } from "./chunk-MWHYYLCE.js";
6
+ } from "./chunk-HGXXGOGG.js";
7
7
  import {
8
8
  agentName,
9
9
  checkReplies,
@@ -11,7 +11,7 @@ import {
11
11
  getGoal,
12
12
  slotName,
13
13
  subscribeWake
14
- } from "./chunk-V5AQASR7.js";
14
+ } from "./chunk-VYWIPFEY.js";
15
15
 
16
16
  // src/listen.ts
17
17
  import { spawn } from "child_process";
package/dist/onboard.js CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  resolvePairing,
4
4
  startPairing,
5
5
  suggestedAgentName
6
- } from "./chunk-UIZLBPRG.js";
6
+ } from "./chunk-TRMYDJ5H.js";
7
7
  import {
8
8
  autoConfigureClients,
9
9
  claudeInstallHint,
10
10
  openBrowser
11
- } from "./chunk-7VLZIOLF.js";
12
- import "./chunk-MWHYYLCE.js";
11
+ } from "./chunk-76ILN3YV.js";
12
+ import "./chunk-HGXXGOGG.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-V5AQASR7.js";
23
+ } from "./chunk-VYWIPFEY.js";
24
24
 
25
25
  // src/onboard.ts
26
26
  function reportRegistered() {
package/dist/slot.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  agentName
4
- } from "./chunk-V5AQASR7.js";
4
+ } from "./chunk-VYWIPFEY.js";
5
5
 
6
6
  // src/slot.ts
7
7
  process.stdout.write(agentName());
package/dist/stalled.js CHANGED
@@ -4,26 +4,33 @@
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
5
  import { homedir } from "os";
6
6
  import { join } from "path";
7
- function stopReply(stalled) {
7
+ function agentFlag(argv) {
8
+ const inline = argv.find((arg) => arg.startsWith("--agent="));
9
+ if (inline) return inline.slice("--agent=".length) || null;
10
+ const at = argv.indexOf("--agent");
11
+ if (at === -1) return null;
12
+ return argv[at + 1] ?? null;
13
+ }
14
+ function stopReply(stalled, event = "Stop") {
8
15
  if (!stalled.length) return null;
9
16
  const list = stalled.slice(0, 8).map((g) => `"${g.title}" (${g.goalId})`).join(", ");
10
17
  const more = stalled.length > 8 ? ` and ${stalled.length - 8} more` : "";
11
- return {
12
- decision: "block",
13
- reason: `Before you stop: ${stalled.length} of your Paigy Goals have had no progress for 3 days: ${list}${more}. For each, update_goal with progress, finish it, or cancel it.`
14
- };
18
+ const reason = `Before you stop: ${stalled.length} of your Paigy Goals have had no progress for 3 days: ${list}${more}. For each, update_goal with progress, finish it, or cancel it.`;
19
+ return event === "AfterAgent" ? { decision: "block", reason, systemMessage: reason } : { decision: "block", reason };
15
20
  }
16
21
  async function main() {
17
22
  const input = JSON.parse(readFileSync(0, "utf8") || "{}");
18
23
  if (input.stop_hook_active) return;
19
24
  const session = input.session_id ?? "";
20
25
  if (session) process.env.PAIGY_SESSION_ID = session;
26
+ const slot = agentFlag(process.argv);
27
+ if (slot && !process.env.PAIGY_AGENT) process.env.PAIGY_AGENT = slot;
21
28
  const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
22
29
  const dir = join(homedir(), ".paigy", "stalled-reminded");
23
30
  const mark = join(dir, `${session || "session"}-${day}`);
24
31
  if (existsSync(mark)) return;
25
- const { checkReplies } = await import("./dist-PJLF3B27.js");
26
- const reply = stopReply((await checkReplies()).stalled ?? []);
32
+ const { checkReplies } = await import("./dist-GJOOJ4FR.js");
33
+ const reply = stopReply((await checkReplies()).stalled ?? [], input.hook_event_name ?? "Stop");
27
34
  if (!reply) return;
28
35
  mkdirSync(dir, { recursive: true });
29
36
  writeFileSync(mark, "");
@@ -31,5 +38,6 @@ async function main() {
31
38
  }
32
39
  if (process.argv[1] && /stalled(\.[jt]s)?$/.test(process.argv[1])) main().catch(() => void 0);
33
40
  export {
41
+ agentFlag,
34
42
  stopReply
35
43
  };
@@ -7,7 +7,7 @@ import {
7
7
  readToken,
8
8
  sessionSlot,
9
9
  slotName
10
- } from "./chunk-V5AQASR7.js";
10
+ } from "./chunk-VYWIPFEY.js";
11
11
 
12
12
  // src/statusline.ts
13
13
  import { realpathSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/mcp",
3
- "version": "0.40.17",
3
+ "version": "0.40.19",
4
4
  "description": "Paigy MCP server — the AI agent harness that calls you. Lets an agent notify a user and await their reply.",
5
5
  "license": "MIT",
6
6
  "type": "module",