@paigy/mcp 0.34.0 → 0.35.0

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AGENT_NAME
3
- } from "./chunk-X42EUXML.js";
3
+ } from "./chunk-U6SLIORK.js";
4
4
 
5
5
  // src/clients.ts
6
6
  import { execFile } from "child_process";
@@ -2645,10 +2645,10 @@ var IntentSchema = z.object({
2645
2645
  // questions included. Found auditing five calls' stored feedback, 2026-08-01.
2646
2646
  kind: z.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
2647
2647
  detail: z.string(),
2648
- /** Landed defer (#397): the MCP parses common spoken forms ("in 20 minutes",
2649
- * "after lunch") against the agent machine's clock — the user's — and attaches
2650
- * the seconds, ready to pass straight to schedule_callback. Absent when the
2651
- * detail didn't parse (the agent interprets it) or the kind isn't defer. */
2648
+ /** Defer only: seconds until the callback the caller asked for, when something upstream
2649
+ * already read the time. Nothing sets it today (#397 documented an MCP parser that was
2650
+ * never written) — the API reads the defer's `detail` itself with `notes/when.ts`
2651
+ * (`parseDelay`, #1292), and a value here simply wins over that reading. */
2652
2652
  dueInSeconds: z.number().int().positive().optional(),
2653
2653
  /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
2654
2654
  * already read the utterance, so `feedback_from_call.kind` stops defaulting to
@@ -3080,7 +3080,13 @@ var BrokerTuningSchema = z.object({
3080
3080
  * uninterpretable, 'high' = whenever not fully certain. */
3081
3081
  clarifyEagerness: z.enum(["low", "normal", "high"]).optional(),
3082
3082
  /** The user's own shorthand: when they say `say`, they mean `mean`. */
3083
- phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional()
3083
+ phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional(),
3084
+ /** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
3085
+ * which is every account today — means the agent's own words decide, per ask: a call
3086
+ * about an English ask opens in English. This is the only thing that overrides that,
3087
+ * and a live caller who switches language mid-call still outranks it (broker/lang.ts).
3088
+ * Set per user (no UI yet), like `voiceTuning`. */
3089
+ language: z.enum(["en", "es"]).optional()
3084
3090
  });
3085
3091
  var UserSettingsSchema = z.object({
3086
3092
  permissions: z.object({
@@ -3288,7 +3294,9 @@ var NoteSchema = z.object({
3288
3294
  /** The request thread minted at assignment; null until assigned. */
3289
3295
  parentId: z.string().nullable(),
3290
3296
  /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
3291
- * call — never a deadline, and nothing rings when it passes. Null = "the very next
3297
+ * call — never a deadline. It only ever comes from the user's own words, so when it
3298
+ * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
3299
+ * a ring at that time); after that it rides like any other. Null = "the very next
3292
3300
  * call", the right reading of "remind me to…" with no time attached. */
3293
3301
  // Defaulted, not required: a Note from an API deploy older than the reminders
3294
3302
  // migration has none of these, and the defaults ARE what it means — no not-before,
@@ -3331,6 +3339,59 @@ var AssignNoteSchema = z.object({
3331
3339
  workspace: z.string().min(1)
3332
3340
  }).optional()
3333
3341
  }).refine((a) => !!a.target !== !!a.spawn, { message: "exactly one of target or spawn" });
3342
+ var TriageVerdictSchema = z.enum(["keep", "stale", "close", "assign"]);
3343
+ var TriageItemSchema = z.object({
3344
+ noteId: z.string(),
3345
+ /** The note's headline at run time. */
3346
+ title: z.string(),
3347
+ /** WHY, in one short human line, evidence first — this is read on a phone underneath
3348
+ * the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
3349
+ * Never a model's reasoning transcript, never an id. */
3350
+ why: z.string()
3351
+ });
3352
+ var TriageAssignmentSchema = z.object({
3353
+ /** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
3354
+ agent: z.string(),
3355
+ /** Its display name at run time (the name on the hatchling's card). Denormalized for the
3356
+ * same reason as `title`: the card must render from the proposal alone. */
3357
+ agentName: z.string(),
3358
+ notes: z.array(TriageItemSchema)
3359
+ });
3360
+ var TriageStatusSchema = z.enum(["open", "superseded", "dismissed"]);
3361
+ var SubmitTriageSchema = z.object({
3362
+ /** Which runtime judged: "ollama" (inference never left the machine) or a harness the
3363
+ * user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
3364
+ * so the phone can say where the content went — an unattributed privacy claim is worth
3365
+ * nothing, and #1106's promise is precisely "Paigy's servers never see this". */
3366
+ provider: z.string().min(1).max(60),
3367
+ /** The concrete model when the provider names one (an ollama tag); null otherwise. */
3368
+ model: z.string().max(200).nullable().optional(),
3369
+ /** How many open notes the run actually looked at — the denominator on the phone
3370
+ * ("6 of 50"), and the honest answer to "did it read the whole queue?". */
3371
+ reviewed: z.number().int().min(0).max(1e4).default(0),
3372
+ close: z.array(TriageItemSchema).max(200).default([]),
3373
+ stale: z.array(TriageItemSchema).max(200).default([]),
3374
+ assign: z.array(TriageAssignmentSchema).max(50).default([])
3375
+ });
3376
+ var TriageProposalSchema = SubmitTriageSchema.extend({
3377
+ id: z.string(),
3378
+ runAt: z.string(),
3379
+ status: TriageStatusSchema,
3380
+ model: z.string().nullable().default(null)
3381
+ });
3382
+ var AcceptTriageSchema = z.discriminatedUnion("group", [
3383
+ z.object({ group: z.literal("close"), noteIds: z.array(z.string()).max(200).optional() }),
3384
+ z.object({ group: z.literal("stale"), noteIds: z.array(z.string()).max(200).optional() }),
3385
+ z.object({
3386
+ group: z.literal("assign"),
3387
+ agent: z.string().min(1),
3388
+ noteIds: z.array(z.string()).max(200).optional()
3389
+ })
3390
+ ]);
3391
+ var AcceptTriageResultSchema = z.object({
3392
+ accepted: z.array(z.string()),
3393
+ failed: z.array(z.object({ noteId: z.string(), reason: z.string() }))
3394
+ });
3334
3395
  var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
3335
3396
  var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
3336
3397
  var OAuthStartSchema = z.object({
@@ -4112,7 +4173,6 @@ async function submitNotification(req, opts = {}) {
4112
4173
  if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
4113
4174
  return await res.json();
4114
4175
  }
4115
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
4116
4176
  function decryptAnswer(reply, deps = {}) {
4117
4177
  if (!reply.sealed) return reply.answer;
4118
4178
  const keyFile = (deps.readKeyFile ?? readKeyFile)();
@@ -4121,15 +4181,6 @@ function decryptAnswer(reply, deps = {}) {
4121
4181
  }
4122
4182
  return openSealedAnswer(reply.sealed, keyFile.keypair);
4123
4183
  }
4124
- async function pollAnswer(notificationId) {
4125
- const token = readToken();
4126
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`, {
4127
- headers: { authorization: `Bearer ${token}` }
4128
- }));
4129
- if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
4130
- const item = await res.json();
4131
- return item.type === "idle" ? item : decryptItem(item);
4132
- }
4133
4184
  var AWAIT_WINDOW_MS = 45e3;
4134
4185
  var _partialSeen = /* @__PURE__ */ new Map();
4135
4186
  async function pollPartials(notificationId) {
@@ -4150,30 +4201,6 @@ async function pollPartials(notificationId) {
4150
4201
  return null;
4151
4202
  }
4152
4203
  }
4153
- async function awaitReply(notificationId, opts = {}) {
4154
- const intervalMs = opts.intervalMs ?? 5e3;
4155
- const windowMs = opts.windowMs ?? AWAIT_WINDOW_MS;
4156
- const doSleep = opts.sleep ?? sleep2;
4157
- const now = opts.now ?? Date.now;
4158
- const start = now();
4159
- while (true) {
4160
- if (opts.signal?.aborted) return { type: "idle" };
4161
- const item = await pollAnswer(notificationId);
4162
- if (item.type !== "idle") return item;
4163
- const partial = await pollPartials(notificationId);
4164
- if (partial) return partial;
4165
- if (now() - start >= windowMs) return { type: "idle" };
4166
- await doSleep(intervalMs);
4167
- }
4168
- }
4169
- function decryptItem(item) {
4170
- if (item.type !== "reply") return item;
4171
- const enriched = { ...item, intents: landIntents(item.intents) };
4172
- if (!enriched.sealed) return enriched;
4173
- const answer = decryptAnswer(enriched);
4174
- const { sealed: _s, ...rest } = enriched;
4175
- return { ...rest, answer };
4176
- }
4177
4204
  function landIntents(intents) {
4178
4205
  if (!intents?.length) return intents;
4179
4206
  return intents.map((i) => {
@@ -4199,10 +4226,14 @@ async function searchThreads(q) {
4199
4226
  async function checkReplies(opts = {}) {
4200
4227
  const token = authToken(opts.token);
4201
4228
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
4202
- headers: { authorization: `Bearer ${token}` }
4229
+ headers: {
4230
+ authorization: `Bearer ${token}`,
4231
+ ...opts.replyAck ? { "x-paigy-await-ack": "1" } : {}
4232
+ }
4203
4233
  }));
4204
4234
  if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
4205
4235
  const pending = await res.json();
4236
+ if (opts.replyAck) return pending;
4206
4237
  return {
4207
4238
  ...pending,
4208
4239
  replies: pending.replies.map((raw) => {
@@ -4267,15 +4298,6 @@ async function heartbeat(runtime, opts = {}) {
4267
4298
  }));
4268
4299
  if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
4269
4300
  }
4270
- async function setTaskState(notificationId, state, opts = {}) {
4271
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${notificationId}/state`, {
4272
- method: "PATCH",
4273
- headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
4274
- body: JSON.stringify({ state })
4275
- }));
4276
- if (!res.ok) throw new Error(`set_task_state failed: ${res.status} ${await res.text()}`);
4277
- return await res.json();
4278
- }
4279
4301
  async function registerDelivery(mode) {
4280
4302
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
4281
4303
  method: "POST",
@@ -4305,6 +4327,204 @@ async function handoff(req) {
4305
4327
  if (!res.ok) throw new Error(`handoff failed: ${res.status} ${await res.text()}`);
4306
4328
  return await res.json();
4307
4329
  }
4330
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4331
+ var ReplyLeaseExpiredError = class extends Error {
4332
+ constructor() {
4333
+ super(
4334
+ "The Paigy reply lease expired before acknowledgement. The answer remains durable; call await_reply again to reclaim it before acting."
4335
+ );
4336
+ this.name = "ReplyLeaseExpiredError";
4337
+ }
4338
+ };
4339
+ var ReplyAckError = class extends Error {
4340
+ constructor(message, retryable) {
4341
+ super(message);
4342
+ this.retryable = retryable;
4343
+ this.name = "ReplyAckError";
4344
+ }
4345
+ retryable;
4346
+ };
4347
+ function ensureAuthed2(res) {
4348
+ if (res.status === 401) throw new UnpairedError();
4349
+ return res;
4350
+ }
4351
+ function decryptAnswer2(reply, deps = {}) {
4352
+ if (!reply.sealed) return reply.answer;
4353
+ const keyFile = (deps.readKeyFile ?? readKeyFile)();
4354
+ if (!keyFile?.keypair) {
4355
+ throw new Error(
4356
+ "received a sealed answer but this agent has no device key to open it \u2014 re-pair."
4357
+ );
4358
+ }
4359
+ return openSealedAnswer(reply.sealed, keyFile.keypair);
4360
+ }
4361
+ function landIntents2(intents) {
4362
+ if (!intents?.length) return intents;
4363
+ return intents.map((intent) => {
4364
+ if (intent.kind !== "defer") return intent;
4365
+ const due = parseDue(intent.detail);
4366
+ return due !== null ? { ...intent, dueInSeconds: due } : intent;
4367
+ });
4368
+ }
4369
+ function decodeReply(item, deps = {}) {
4370
+ const { delivery: _delivery, ...withoutDelivery } = item;
4371
+ const enriched = {
4372
+ ...withoutDelivery,
4373
+ intents: landIntents2(withoutDelivery.intents)
4374
+ };
4375
+ if (!enriched.sealed) return enriched;
4376
+ const answer = decryptAnswer2(enriched, deps);
4377
+ const { sealed: _sealed, ...plaintext } = enriched;
4378
+ return { ...plaintext, answer };
4379
+ }
4380
+ function deliveryOf(item) {
4381
+ if (item.type !== "reply" || !("delivery" in item) || item.delivery === void 0) {
4382
+ return null;
4383
+ }
4384
+ const delivery = item.delivery;
4385
+ if (!delivery || typeof delivery.leaseId !== "string" || !delivery.leaseId || typeof delivery.expiresAt !== "string" || !delivery.expiresAt) {
4386
+ throw new Error(
4387
+ "Paigy returned invalid reply-delivery metadata; the answer was not acknowledged and remains recoverable."
4388
+ );
4389
+ }
4390
+ return delivery;
4391
+ }
4392
+ async function responseDetail(res) {
4393
+ try {
4394
+ return (await res.text()).slice(0, 300);
4395
+ } catch {
4396
+ return "";
4397
+ }
4398
+ }
4399
+ async function acknowledge(token, leaseId, opts = {}) {
4400
+ const attempts = Math.max(1, Math.min(opts.attempts ?? 3, 5));
4401
+ const backoffMs = Math.max(0, opts.backoffMs ?? 100);
4402
+ const doSleep = opts.doSleep ?? sleep2;
4403
+ let lastError;
4404
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
4405
+ try {
4406
+ const res = ensureAuthed2(await reach(`${BACKEND_URL}/api/await/ack`, {
4407
+ method: "POST",
4408
+ headers: {
4409
+ authorization: `Bearer ${token}`,
4410
+ "content-type": "application/json"
4411
+ },
4412
+ body: JSON.stringify({ leaseId })
4413
+ }));
4414
+ if (res.ok) return;
4415
+ const detail2 = await responseDetail(res);
4416
+ if (res.status === 409) throw new ReplyLeaseExpiredError();
4417
+ const retryable = res.status === 408 || res.status === 425 || res.status === 429 || res.status >= 500;
4418
+ throw new ReplyAckError(
4419
+ `Paigy reply acknowledgement failed: ${res.status}${detail2 ? ` ${detail2}` : ""}`,
4420
+ retryable
4421
+ );
4422
+ } catch (error) {
4423
+ if (error instanceof UnpairedError || error instanceof ReplyLeaseExpiredError || error instanceof ReplyAckError && !error.retryable) {
4424
+ throw error;
4425
+ }
4426
+ lastError = error;
4427
+ if (attempt + 1 < attempts) {
4428
+ await doSleep(backoffMs * 2 ** attempt);
4429
+ }
4430
+ }
4431
+ }
4432
+ const detail = lastError instanceof Error ? lastError.message : String(lastError);
4433
+ throw new Error(
4434
+ `Could not acknowledge the Paigy reply after ${attempts} attempts (${detail}). The answer was not returned to the agent and remains recoverable after the lease expires.`
4435
+ );
4436
+ }
4437
+ async function receiveReply(item, token, opts = {}) {
4438
+ const decoded = decodeReply(item, opts);
4439
+ const delivery = deliveryOf(item);
4440
+ if (!delivery) {
4441
+ if (opts.requireDelivery) {
4442
+ throw new Error("Paigy returned a catch-up reply without a delivery lease; the answer was not exposed.");
4443
+ }
4444
+ return decoded;
4445
+ }
4446
+ await acknowledge(token, delivery.leaseId, {
4447
+ attempts: opts.ackAttempts,
4448
+ backoffMs: opts.ackBackoffMs,
4449
+ doSleep: opts.ackSleep
4450
+ });
4451
+ return decoded;
4452
+ }
4453
+ async function pollAnswer(notificationId, opts = {}) {
4454
+ const token = authToken() ?? "";
4455
+ const res = ensureAuthed2(await reach(
4456
+ `${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`,
4457
+ {
4458
+ headers: {
4459
+ authorization: `Bearer ${token}`,
4460
+ "x-paigy-await-ack": "1"
4461
+ }
4462
+ }
4463
+ ));
4464
+ if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
4465
+ const wire = await res.json();
4466
+ if (wire.type !== "reply") return wire;
4467
+ return receiveReply(wire, token, opts);
4468
+ }
4469
+ async function awaitReply(notificationId, opts = {}) {
4470
+ const intervalMs = opts.intervalMs ?? 5e3;
4471
+ const windowMs = opts.windowMs ?? AWAIT_WINDOW_MS;
4472
+ const doSleep = opts.sleep ?? sleep2;
4473
+ const now = opts.now ?? Date.now;
4474
+ const start = now();
4475
+ while (true) {
4476
+ if (opts.signal?.aborted) return { type: "idle" };
4477
+ const item = await pollAnswer(notificationId);
4478
+ if (item.type !== "idle") return item;
4479
+ const partial = await pollPartials(notificationId);
4480
+ if (partial) return partial;
4481
+ if (now() - start >= windowMs) return { type: "idle" };
4482
+ await doSleep(intervalMs);
4483
+ }
4484
+ }
4485
+ async function checkReplies2(opts = {}) {
4486
+ const pending = await checkReplies({ ...opts, replyAck: true });
4487
+ const token = authToken(opts.token) ?? "";
4488
+ const replies = await Promise.all(pending.replies.map(async (reply) => {
4489
+ const received = await receiveReply(
4490
+ { type: "reply", ...reply },
4491
+ token,
4492
+ { requireDelivery: true }
4493
+ );
4494
+ const { type: _type, ...withoutType } = received;
4495
+ return withoutType;
4496
+ }));
4497
+ return {
4498
+ ...pending,
4499
+ replies
4500
+ };
4501
+ }
4502
+ function ensureAuthed3(res) {
4503
+ if (res.status === 401) throw new UnpairedError();
4504
+ return res;
4505
+ }
4506
+ async function setTaskState(notificationId, state, opts = {}) {
4507
+ const token = authToken(opts.token) ?? "";
4508
+ const res = ensureAuthed3(await reach(
4509
+ `${BACKEND_URL}/api/notify/${encodeURIComponent(notificationId)}/state`,
4510
+ {
4511
+ method: "PATCH",
4512
+ headers: {
4513
+ "content-type": "application/json",
4514
+ authorization: `Bearer ${token}`,
4515
+ "x-paigy-await-ack": "1"
4516
+ },
4517
+ body: JSON.stringify({ state })
4518
+ }
4519
+ ));
4520
+ if (!res.ok) {
4521
+ throw new Error(`set_task_state failed: ${res.status} ${await res.text()}`);
4522
+ }
4523
+ return await res.json();
4524
+ }
4525
+ function overrideToken2(secret) {
4526
+ overrideToken(secret);
4527
+ }
4308
4528
 
4309
4529
  export {
4310
4530
  BACKEND_URL,
@@ -4334,18 +4554,18 @@ export {
4334
4554
  pairStep,
4335
4555
  UnpairedError,
4336
4556
  submitNotification,
4337
- awaitReply,
4338
4557
  getThread,
4339
4558
  searchThreads,
4340
- checkReplies,
4341
4559
  answerCallerQuestion,
4342
- overrideToken,
4343
4560
  hatch,
4344
4561
  whoAmI,
4345
4562
  setIdentity,
4346
4563
  heartbeat,
4347
- setTaskState,
4348
4564
  registerDelivery,
4349
4565
  scheduleCallback,
4350
- handoff
4566
+ handoff,
4567
+ awaitReply,
4568
+ checkReplies2,
4569
+ setTaskState,
4570
+ overrideToken2
4351
4571
  };
@@ -251,10 +251,10 @@ var IntentSchema = z.object({
251
251
  // questions included. Found auditing five calls' stored feedback, 2026-08-01.
252
252
  kind: z.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
253
253
  detail: z.string(),
254
- /** Landed defer (#397): the MCP parses common spoken forms ("in 20 minutes",
255
- * "after lunch") against the agent machine's clock — the user's — and attaches
256
- * the seconds, ready to pass straight to schedule_callback. Absent when the
257
- * detail didn't parse (the agent interprets it) or the kind isn't defer. */
254
+ /** Defer only: seconds until the callback the caller asked for, when something upstream
255
+ * already read the time. Nothing sets it today (#397 documented an MCP parser that was
256
+ * never written) — the API reads the defer's `detail` itself with `notes/when.ts`
257
+ * (`parseDelay`, #1292), and a value here simply wins over that reading. */
258
258
  dueInSeconds: z.number().int().positive().optional(),
259
259
  /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
260
260
  * already read the utterance, so `feedback_from_call.kind` stops defaulting to
@@ -696,7 +696,13 @@ var BrokerTuningSchema = z.object({
696
696
  * uninterpretable, 'high' = whenever not fully certain. */
697
697
  clarifyEagerness: z.enum(["low", "normal", "high"]).optional(),
698
698
  /** The user's own shorthand: when they say `say`, they mean `mean`. */
699
- phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional()
699
+ phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional(),
700
+ /** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
701
+ * which is every account today — means the agent's own words decide, per ask: a call
702
+ * about an English ask opens in English. This is the only thing that overrides that,
703
+ * and a live caller who switches language mid-call still outranks it (broker/lang.ts).
704
+ * Set per user (no UI yet), like `voiceTuning`. */
705
+ language: z.enum(["en", "es"]).optional()
700
706
  });
701
707
  var UserSettingsSchema = z.object({
702
708
  permissions: z.object({
@@ -904,7 +910,9 @@ var NoteSchema = z.object({
904
910
  /** The request thread minted at assignment; null until assigned. */
905
911
  parentId: z.string().nullable(),
906
912
  /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
907
- * call — never a deadline, and nothing rings when it passes. Null = "the very next
913
+ * call — never a deadline. It only ever comes from the user's own words, so when it
914
+ * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
915
+ * a ring at that time); after that it rides like any other. Null = "the very next
908
916
  * call", the right reading of "remind me to…" with no time attached. */
909
917
  // Defaulted, not required: a Note from an API deploy older than the reminders
910
918
  // migration has none of these, and the defaults ARE what it means — no not-before,
@@ -947,6 +955,59 @@ var AssignNoteSchema = z.object({
947
955
  workspace: z.string().min(1)
948
956
  }).optional()
949
957
  }).refine((a) => !!a.target !== !!a.spawn, { message: "exactly one of target or spawn" });
958
+ var TriageVerdictSchema = z.enum(["keep", "stale", "close", "assign"]);
959
+ var TriageItemSchema = z.object({
960
+ noteId: z.string(),
961
+ /** The note's headline at run time. */
962
+ title: z.string(),
963
+ /** WHY, in one short human line, evidence first — this is read on a phone underneath
964
+ * the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
965
+ * Never a model's reasoning transcript, never an id. */
966
+ why: z.string()
967
+ });
968
+ var TriageAssignmentSchema = z.object({
969
+ /** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
970
+ agent: z.string(),
971
+ /** Its display name at run time (the name on the hatchling's card). Denormalized for the
972
+ * same reason as `title`: the card must render from the proposal alone. */
973
+ agentName: z.string(),
974
+ notes: z.array(TriageItemSchema)
975
+ });
976
+ var TriageStatusSchema = z.enum(["open", "superseded", "dismissed"]);
977
+ var SubmitTriageSchema = z.object({
978
+ /** Which runtime judged: "ollama" (inference never left the machine) or a harness the
979
+ * user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
980
+ * so the phone can say where the content went — an unattributed privacy claim is worth
981
+ * nothing, and #1106's promise is precisely "Paigy's servers never see this". */
982
+ provider: z.string().min(1).max(60),
983
+ /** The concrete model when the provider names one (an ollama tag); null otherwise. */
984
+ model: z.string().max(200).nullable().optional(),
985
+ /** How many open notes the run actually looked at — the denominator on the phone
986
+ * ("6 of 50"), and the honest answer to "did it read the whole queue?". */
987
+ reviewed: z.number().int().min(0).max(1e4).default(0),
988
+ close: z.array(TriageItemSchema).max(200).default([]),
989
+ stale: z.array(TriageItemSchema).max(200).default([]),
990
+ assign: z.array(TriageAssignmentSchema).max(50).default([])
991
+ });
992
+ var TriageProposalSchema = SubmitTriageSchema.extend({
993
+ id: z.string(),
994
+ runAt: z.string(),
995
+ status: TriageStatusSchema,
996
+ model: z.string().nullable().default(null)
997
+ });
998
+ var AcceptTriageSchema = z.discriminatedUnion("group", [
999
+ z.object({ group: z.literal("close"), noteIds: z.array(z.string()).max(200).optional() }),
1000
+ z.object({ group: z.literal("stale"), noteIds: z.array(z.string()).max(200).optional() }),
1001
+ z.object({
1002
+ group: z.literal("assign"),
1003
+ agent: z.string().min(1),
1004
+ noteIds: z.array(z.string()).max(200).optional()
1005
+ })
1006
+ ]);
1007
+ var AcceptTriageResultSchema = z.object({
1008
+ accepted: z.array(z.string()),
1009
+ failed: z.array(z.object({ noteId: z.string(), reason: z.string() }))
1010
+ });
950
1011
  var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
951
1012
  var WAKE_EVENT = "wake";
952
1013
  var wakeChannel = (tokenId) => `wake:${tokenId}`;
package/dist/enable.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  PAIGY_TOOL_IDS,
4
4
  enablePaigyTools
5
- } from "./chunk-G2WR7FCJ.js";
6
- import "./chunk-X42EUXML.js";
5
+ } from "./chunk-KNMI3CJ3.js";
6
+ import "./chunk-U6SLIORK.js";
7
7
 
8
8
  // src/enable.ts
9
9
  function main() {
package/dist/index.js CHANGED
@@ -2,14 +2,14 @@
2
2
  import {
3
3
  HandoffSchema,
4
4
  MISSED_CALL_PLAN
5
- } from "./chunk-45CIZLCJ.js";
5
+ } from "./chunk-YOGAG4YY.js";
6
6
  import {
7
7
  ENABLE_COMMAND,
8
8
  PAIGY_TOOL_IDS,
9
9
  autoConfigureClients,
10
10
  claudeInstallHint,
11
11
  paigyToolsAllowlisted
12
- } from "./chunk-G2WR7FCJ.js";
12
+ } from "./chunk-KNMI3CJ3.js";
13
13
  import {
14
14
  clearSurface,
15
15
  writeSurface
@@ -22,7 +22,7 @@ import {
22
22
  UnpairedError,
23
23
  answerCallerQuestion,
24
24
  awaitReply,
25
- checkReplies,
25
+ checkReplies2,
26
26
  deleteKeyFile,
27
27
  deleteToken,
28
28
  fetchCredential,
@@ -34,7 +34,7 @@ import {
34
34
  lintNotify,
35
35
  listSlots,
36
36
  normalizeSpeech,
37
- overrideToken,
37
+ overrideToken2,
38
38
  pairStep,
39
39
  readKeyFile,
40
40
  readToken,
@@ -51,7 +51,7 @@ import {
51
51
  startE2ee,
52
52
  submitNotification,
53
53
  whoAmI
54
- } from "./chunk-X42EUXML.js";
54
+ } from "./chunk-U6SLIORK.js";
55
55
 
56
56
  // src/index.ts
57
57
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -404,7 +404,7 @@ function prependNote(result, note) {
404
404
  }
405
405
  async function hatchUnderDevice(name, voice) {
406
406
  if (!listSlots().includes("Desktop")) return null;
407
- overrideToken(readToken("Desktop"));
407
+ overrideToken2(readToken("Desktop"));
408
408
  try {
409
409
  const minted = await hatch(name ?? suggestedAgentName() ?? "Agent", voice ?? null);
410
410
  const dt = { ok: true, access_token: minted.token, name: minted.name, device: null };
@@ -413,7 +413,7 @@ async function hatchUnderDevice(name, voice) {
413
413
  } catch {
414
414
  return null;
415
415
  } finally {
416
- overrideToken(null);
416
+ overrideToken2(null);
417
417
  }
418
418
  }
419
419
  async function runPair(device_code, name, voice, note) {
@@ -658,7 +658,7 @@ async function handleTool(request, signal) {
658
658
  return { content: [{ type: "text", text: JSON.stringify(item) }] };
659
659
  }
660
660
  case "check_replies": {
661
- const result = await checkReplies();
661
+ const result = await checkReplies2();
662
662
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
663
663
  }
664
664
  case "get_thread": {
package/dist/listen.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  WAKE_EVENT,
4
4
  wakeChannel
5
- } from "./chunk-45CIZLCJ.js";
5
+ } from "./chunk-YOGAG4YY.js";
6
6
  import {
7
- checkReplies,
7
+ checkReplies2,
8
8
  registerDelivery
9
- } from "./chunk-X42EUXML.js";
9
+ } from "./chunk-U6SLIORK.js";
10
10
 
11
11
  // src/listen.ts
12
12
  import { createClient } from "@supabase/supabase-js";
@@ -170,7 +170,7 @@ function launchEnv(work, reason) {
170
170
  }
171
171
  async function sweep(reason) {
172
172
  try {
173
- const work = await checkReplies();
173
+ const work = await checkReplies2();
174
174
  emit({ type: "sweep", reason, ...work });
175
175
  const cmd = process.env.PAIGY_ON_WAKE;
176
176
  const inbound = work.replies.length > 0 || work.requests.length > 0;
package/dist/onboard.js CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  autoConfigureClients,
4
4
  claudeInstallHint,
5
5
  openBrowser
6
- } from "./chunk-G2WR7FCJ.js";
6
+ } from "./chunk-KNMI3CJ3.js";
7
7
  import {
8
8
  AGENT_NAME,
9
9
  TOKEN_PATH,
10
10
  hatch,
11
11
  listSlots,
12
- overrideToken,
12
+ overrideToken2,
13
13
  pollToken,
14
14
  readToken,
15
15
  requestCode,
@@ -17,7 +17,7 @@ import {
17
17
  setIdentity,
18
18
  sleep,
19
19
  whoAmI
20
- } from "./chunk-X42EUXML.js";
20
+ } from "./chunk-U6SLIORK.js";
21
21
 
22
22
  // src/onboard.ts
23
23
  async function main() {
@@ -50,7 +50,7 @@ Paigy MCP onboarding (agent: "${AGENT_NAME}")
50
50
  console.log("The saved token is no longer valid \u2014 re-pairing.\n");
51
51
  }
52
52
  if (listSlots().includes("Desktop")) {
53
- overrideToken(readToken("Desktop"));
53
+ overrideToken2(readToken("Desktop"));
54
54
  try {
55
55
  const minted = await hatch(process.env.PAIGY_NAME ?? "Agent", null);
56
56
  saveToken({ ok: true, access_token: minted.token, name: minted.name, device: null });
@@ -67,7 +67,7 @@ Paigy MCP onboarding (agent: "${AGENT_NAME}")
67
67
  } catch {
68
68
  console.log("(This machine's saved credential didn't work \u2014 falling back to a code.)\n");
69
69
  } finally {
70
- overrideToken(null);
70
+ overrideToken2(null);
71
71
  }
72
72
  }
73
73
  const code = await requestCode();
@@ -6,7 +6,7 @@ import {
6
6
  BACKEND_URL,
7
7
  reach,
8
8
  readToken
9
- } from "./chunk-X42EUXML.js";
9
+ } from "./chunk-U6SLIORK.js";
10
10
 
11
11
  // src/statusline.ts
12
12
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/mcp",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
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",