@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 +32 -3
- package/dist/{chunk-7VLZIOLF.js → chunk-76ILN3YV.js} +71 -2
- package/dist/{chunk-MWHYYLCE.js → chunk-HGXXGOGG.js} +127 -5
- package/dist/{chunk-UIZLBPRG.js → chunk-TRMYDJ5H.js} +19 -1
- package/dist/{chunk-V5AQASR7.js → chunk-VYWIPFEY.js} +177 -19
- package/dist/{dist-PJLF3B27.js → dist-GJOOJ4FR.js} +3 -1
- package/dist/enable.js +3 -3
- package/dist/index.js +12 -20
- package/dist/listen.js +2 -2
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/stalled.js +15 -7
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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-
|
|
7
|
+
} from "./chunk-HGXXGOGG.js";
|
|
8
8
|
import {
|
|
9
9
|
agentName
|
|
10
|
-
} from "./chunk-
|
|
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({
|
|
117
|
-
|
|
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-
|
|
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({
|
|
1585
|
-
|
|
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)
|
|
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
|
|
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
|
|
3163
|
+
re: entry.aboutId && shown.has(entry.aboutId) ? handle(entry.aboutId) : void 0,
|
|
3026
3164
|
options,
|
|
3027
|
-
decision: need ? compact({
|
|
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
|
-
|
|
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:
|
|
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-
|
|
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-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
18
|
+
} from "./chunk-76ILN3YV.js";
|
|
18
19
|
import {
|
|
19
20
|
decideListen,
|
|
20
21
|
listenerAlive
|
|
21
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
249
|
+
const who = await whoAmI().catch(() => null);
|
|
250
250
|
if (who) {
|
|
251
|
-
|
|
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:
|
|
266
|
-
voice:
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
6
|
+
} from "./chunk-TRMYDJ5H.js";
|
|
7
7
|
import {
|
|
8
8
|
autoConfigureClients,
|
|
9
9
|
claudeInstallHint,
|
|
10
10
|
openBrowser
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
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-
|
|
23
|
+
} from "./chunk-VYWIPFEY.js";
|
|
24
24
|
|
|
25
25
|
// src/onboard.ts
|
|
26
26
|
function reportRegistered() {
|
package/dist/slot.js
CHANGED
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
|
|
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
|
-
|
|
12
|
-
|
|
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-
|
|
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
|
};
|
package/dist/statusline.js
CHANGED
package/package.json
CHANGED