@miadi/connector 0.1.5 → 0.1.7

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/dist/tools.js CHANGED
@@ -16,7 +16,7 @@ import { reviewIdOf, videoIdOf } from "./reviews.js";
16
16
  import { SCOPE_APPROVE, SCOPE_CONTRIBUTE } from "./oauth.js";
17
17
  import { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, current, newRecordId, } from "./records.js";
18
18
  import { VERSION } from "./version.js";
19
- import { DECISIONS, consequenceOf, decide, isApprovable, readQueueState } from "./approve.js";
19
+ import { AlreadyDecided, DECISIONS, KIND_LABELS, consequenceOf, decide, isApprovable, readQueueState } from "./approve.js";
20
20
  import { DECISIONS_CARD_HTML, DECISIONS_CARD_URI, DECISION_TOKENS_META } from "./decisions.js";
21
21
  import { join } from "node:path";
22
22
  import { artefactVisibility, readArtefactWeave, saveResearchArtefact } from "@miadi/inquiry-weave";
@@ -84,6 +84,39 @@ function episodeRef(input) {
84
84
  }
85
85
  export function registerTools(server, ctx) {
86
86
  const { config, miadi, reviews, ledger } = ctx;
87
+ // One server is made per request, so the person a tool call runs for is this request's viewer.
88
+ let viewer;
89
+ /**
90
+ * The signed-in person, held to Miadi's read tier unless the tool says
91
+ * otherwise: a person Miadi gives no API tier reaches only get_capabilities.
92
+ */
93
+ const who = (extra, minTier = "read") => {
94
+ const found = personOf(extra);
95
+ if (minTier === "read")
96
+ requireTier(found.person, "read");
97
+ viewer = found.person;
98
+ return found;
99
+ };
100
+ /**
101
+ * What a record shows to the viewer. Below the write tier, someone else's
102
+ * record keeps its story but not the operator details a session view also
103
+ * withholds (session, cwd, the seat a brief went to) nor long bodies.
104
+ */
105
+ const redact = (r) => {
106
+ if (!viewer || viewer.api_tier === "write" || r.actor.person_id === viewer.id)
107
+ return r;
108
+ const body = { ...r.body };
109
+ for (const k of ["session", "cwd", "session_id"])
110
+ delete body[k];
111
+ for (const k of ["markdown", "transcript"])
112
+ if (typeof body[k] === "string")
113
+ body[k] = `${body[k].length} chars`;
114
+ const receipt = r.completion_receipt ? { ...r.completion_receipt } : undefined;
115
+ if (receipt)
116
+ for (const k of ["to", "file", "session_id"])
117
+ delete receipt[k];
118
+ return { ...r, body, ...(receipt ? { completion_receipt: receipt } : {}) };
119
+ };
87
120
  const actorOf = (person, auth) => {
88
121
  const client = ctx.oauthState.read().clients[auth.clientId];
89
122
  return { person_id: person.id, name: person.name, client_id: auth.clientId, ...(client?.client_name ? { client_name: client.client_name } : {}) };
@@ -91,7 +124,11 @@ export function registerTools(server, ctx) {
91
124
  const approvalUrl = (id) => `${config.publicUrl}/approve/${encodeURIComponent(id)}`;
92
125
  const append = (kind, person, auth, fields) => {
93
126
  if (fields.idempotency_key) {
94
- const prior = ledger.all().find((r) => r.kind === kind && r.idempotency_key === fields.idempotency_key && r.actor.person_id === person.id);
127
+ // A request already decided (approved, declined, sent back) is not handed back: the same key makes a new one.
128
+ const prior = all().find((r) => r.kind === kind &&
129
+ r.idempotency_key === fields.idempotency_key &&
130
+ r.actor.person_id === person.id &&
131
+ (r.approval_state === undefined || r.approval_state === "awaiting_approval" || r.approval_state === "not_required"));
95
132
  if (prior)
96
133
  return prior;
97
134
  }
@@ -105,7 +142,9 @@ export function registerTools(server, ctx) {
105
142
  ...fields,
106
143
  });
107
144
  };
108
- const records = () => current(ledger.all());
145
+ /** Every record, folded; for logic. What a tool returns goes through `shown`. */
146
+ const all = () => current(ledger.all());
147
+ const records = () => all().map(redact);
109
148
  const uploads = new ResearchUploads(join(config.stateDir, "uploads"));
110
149
  const touching = (ref) => records().filter((r) => r.refs.includes(ref) || r.evidence_refs.includes(ref) || r.id === ref);
111
150
  // ── reads ──────────────────────────────────────────────────────────────
@@ -115,7 +154,7 @@ export function registerTools(server, ctx) {
115
154
  inputSchema: {},
116
155
  annotations: READ,
117
156
  }, wrap(async (_args, extra) => {
118
- const { person, auth } = personOf(extra);
157
+ const { person, auth } = who(extra, "none");
119
158
  const miadiUp = await miadi.get(person.token, "/api/identity/me").then(() => "reachable", (e) => (e instanceof MiadiError && e.status === 0 ? "unavailable" : "reachable"));
120
159
  return {
121
160
  service: "@miadi/connector",
@@ -146,7 +185,7 @@ export function registerTools(server, ctx) {
146
185
  inputSchema: { ref: z.string().min(1) },
147
186
  annotations: READ,
148
187
  }, wrap(async ({ ref }, extra) => {
149
- const { person } = personOf(extra);
188
+ const { person } = who(extra);
150
189
  const r = ref.trim();
151
190
  const cited = touching(r);
152
191
  if (/^session:/.test(r)) {
@@ -163,6 +202,7 @@ export function registerTools(server, ctx) {
163
202
  if (issue) {
164
203
  const res = await fetch(`https://api.github.com/repos/${issue[1]}/${issue[2]}/issues/${issue[3]}`, {
165
204
  headers: { accept: "application/vnd.github+json", ...(process.env.GITHUB_TOKEN ? { authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}) },
205
+ signal: AbortSignal.timeout(15_000),
166
206
  });
167
207
  if (!res.ok)
168
208
  return { ref: r, kind: "github_issue", state: res.status === 404 ? "not_found_or_private" : `github_${res.status}`, cited_by: cited };
@@ -200,7 +240,7 @@ export function registerTools(server, ctx) {
200
240
  inputSchema: {},
201
241
  annotations: READ,
202
242
  }, wrap(async (_args, extra) => {
203
- const { person } = personOf(extra);
243
+ const { person } = who(extra);
204
244
  const open = records().filter((r) => r.kind === "handoff" && r.state !== "completed" && r.state !== "withdrawn");
205
245
  const talking = await miadi.get(person.token, "/api/circles").then((b) => (b.circles ?? []).map((c) => ({ id: c.id, name: c.name, intention: c.intention, member: c.member ?? undefined })), (e) => ({ state: e instanceof MiadiError ? `miadi_${e.status}` : "unavailable" }));
206
246
  return {
@@ -215,7 +255,7 @@ export function registerTools(server, ctx) {
215
255
  inputSchema: { circle_id: z.string().min(1) },
216
256
  annotations: READ,
217
257
  }, wrap(async ({ circle_id }, extra) => {
218
- const { person } = personOf(extra);
258
+ const { person } = who(extra);
219
259
  const five = CIRCLES.find((c) => c.id === circle_id.toLowerCase());
220
260
  if (five) {
221
261
  const mine = records().filter((r) => r.circle === five.id || r.to_circle === five.id || r.from_circle === five.id);
@@ -241,7 +281,7 @@ export function registerTools(server, ctx) {
241
281
  },
242
282
  annotations: READ,
243
283
  }, wrap(async (args, extra) => {
244
- const { person } = personOf(extra);
284
+ const { person } = who(extra);
245
285
  requireTier(person, "write");
246
286
  const qs = new URLSearchParams();
247
287
  if (args.episode)
@@ -263,7 +303,7 @@ export function registerTools(server, ctx) {
263
303
  inputSchema: { session_id: z.string().min(8) },
264
304
  annotations: READ,
265
305
  }, wrap(async ({ session_id }, extra) => {
266
- const { person } = personOf(extra);
306
+ const { person } = who(extra);
267
307
  requireTier(person, "write");
268
308
  const id = session_id.replace(/^session:/, "");
269
309
  const session = await miadi.get(person.token, `/api/session/${encodeURIComponent(id)}`);
@@ -275,7 +315,7 @@ export function registerTools(server, ctx) {
275
315
  inputSchema: { episode: z.string().min(1).describe("miadi-chronicle://<n>, an episode number or its folder.") },
276
316
  annotations: READ,
277
317
  }, wrap(async ({ episode }, extra) => {
278
- const { person } = personOf(extra);
318
+ const { person } = who(extra);
279
319
  const ref = episodeRef(episode);
280
320
  const [files, screenwalks, epReviews] = await Promise.all([
281
321
  miadi.get(person.token, `/api/chronicle/episodes/${ref}/files`).catch(asState),
@@ -296,7 +336,7 @@ export function registerTools(server, ctx) {
296
336
  inputSchema: { episode: z.string().optional(), subject_ref: z.string().optional() },
297
337
  annotations: READ,
298
338
  }, wrap(async ({ episode, subject_ref }, extra) => {
299
- const { person } = personOf(extra);
339
+ const { person } = who(extra);
300
340
  const evals = records().filter((r) => r.kind === "observation" && typeof r.body?.assessment === "string");
301
341
  const scoped = subject_ref ? evals.filter((r) => r.refs.includes(subject_ref)) : evals;
302
342
  return {
@@ -311,7 +351,7 @@ export function registerTools(server, ctx) {
311
351
  inputSchema: { project: z.string().optional().describe("NCP studio project id, e.g. chimera."), episode: z.string().optional() },
312
352
  annotations: READ,
313
353
  }, wrap(async ({ project, episode }, extra) => {
314
- const { person } = personOf(extra);
354
+ const { person } = who(extra);
315
355
  let ncp = "give a project id to read its story beats";
316
356
  if (project) {
317
357
  ncp = await miadi.get(person.token, `/api/ncp/projects/${encodeURIComponent(project)}`).then((b) => ({ project: project, beats: collectBeats(b?.project), validation: b?.validation ?? null }), asState);
@@ -325,7 +365,7 @@ export function registerTools(server, ctx) {
325
365
  inputSchema: { query: z.string().min(1) },
326
366
  annotations: READ,
327
367
  }, wrap(async ({ query }, extra) => {
328
- const { person } = personOf(extra);
368
+ const { person } = who(extra);
329
369
  const id = reviewIdOf(query);
330
370
  if (id) {
331
371
  const r = await reviews.get(id);
@@ -371,7 +411,7 @@ export function registerTools(server, ctx) {
371
411
  inputSchema: { ref: z.string().optional(), circle: circle.optional(), state: z.enum(HANDOFF_STATES).optional() },
372
412
  annotations: READ,
373
413
  }, wrap(async ({ ref, circle: c, state }, extra) => {
374
- personOf(extra);
414
+ who(extra);
375
415
  let list = records().filter((r) => r.kind === "handoff" || r.kind === "source_intent");
376
416
  if (ref)
377
417
  list = list.filter((r) => r.id === ref || r.refs.includes(ref) || r.evidence_refs.includes(ref));
@@ -394,7 +434,7 @@ export function registerTools(server, ctx) {
394
434
  },
395
435
  annotations: READ,
396
436
  }, wrap(async (a, extra) => {
397
- personOf(extra);
437
+ who(extra);
398
438
  let list = records();
399
439
  if (a.kind)
400
440
  list = list.filter((r) => r.kind === a.kind);
@@ -413,7 +453,7 @@ export function registerTools(server, ctx) {
413
453
  inputSchema: { record_id: z.string().min(1) },
414
454
  annotations: READ,
415
455
  }, wrap(async ({ record_id }, extra) => {
416
- personOf(extra);
456
+ who(extra);
417
457
  const r = records().find((x) => x.id === record_id);
418
458
  if (!r)
419
459
  return { state: "not_found", record_id };
@@ -433,7 +473,7 @@ export function registerTools(server, ctx) {
433
473
  inputSchema: { query: z.string().min(1) },
434
474
  annotations: READ,
435
475
  }, wrap(async ({ query }, extra) => {
436
- personOf(extra);
476
+ who(extra);
437
477
  const needle = query.toLowerCase();
438
478
  const results = [];
439
479
  if (reviews.canSearch) {
@@ -456,7 +496,7 @@ export function registerTools(server, ctx) {
456
496
  inputSchema: { id: z.string().min(1) },
457
497
  annotations: READ,
458
498
  }, wrap(async ({ id }, extra) => {
459
- personOf(extra);
499
+ who(extra);
460
500
  const reviewId = reviewIdOf(id);
461
501
  if (reviewId) {
462
502
  const r = await reviews.get(reviewId);
@@ -490,7 +530,7 @@ export function registerTools(server, ctx) {
490
530
  },
491
531
  annotations: WRITE,
492
532
  }, wrap(async (a, extra) => {
493
- const { person, auth } = personOf(extra);
533
+ const { person, auth } = who(extra);
494
534
  requireContribute(auth);
495
535
  return append("observation", person, auth, {
496
536
  claim_category: a.claim_category,
@@ -511,7 +551,7 @@ export function registerTools(server, ctx) {
511
551
  inputSchema: { question: z.string().min(1).max(5000), refs, circle: circle.optional(), episode: z.string().optional(), ...base },
512
552
  annotations: WRITE,
513
553
  }, wrap(async (a, extra) => {
514
- const { person, auth } = personOf(extra);
554
+ const { person, auth } = who(extra);
515
555
  requireContribute(auth);
516
556
  return append("inquiry", person, auth, {
517
557
  circle: a.circle,
@@ -529,7 +569,7 @@ export function registerTools(server, ctx) {
529
569
  inputSchema: { from: z.string().min(1), to: z.string().min(1), relation: z.string().min(1).max(80), note: z.string().max(2000).optional(), ...base },
530
570
  annotations: WRITE,
531
571
  }, wrap(async (a, extra) => {
532
- const { person, auth } = personOf(extra);
572
+ const { person, auth } = who(extra);
533
573
  requireContribute(auth);
534
574
  return append("link", person, auth, {
535
575
  refs: [a.from, a.to],
@@ -552,7 +592,7 @@ export function registerTools(server, ctx) {
552
592
  },
553
593
  annotations: WRITE,
554
594
  }, wrap(async (a, extra) => {
555
- const { person, auth } = personOf(extra);
595
+ const { person, auth } = who(extra);
556
596
  requireContribute(auth);
557
597
  return append("observation", person, auth, {
558
598
  circle: "story",
@@ -579,7 +619,7 @@ export function registerTools(server, ctx) {
579
619
  },
580
620
  annotations: WRITE,
581
621
  }, wrap(async (a, extra) => {
582
- const { person, auth } = personOf(extra);
622
+ const { person, auth } = who(extra);
583
623
  requireContribute(auth);
584
624
  return append("observation", person, auth, {
585
625
  circle: "evaluation",
@@ -604,7 +644,7 @@ export function registerTools(server, ctx) {
604
644
  },
605
645
  annotations: WRITE,
606
646
  }, wrap(async (a, extra) => {
607
- const { person, auth } = personOf(extra);
647
+ const { person, auth } = who(extra);
608
648
  requireContribute(auth);
609
649
  const video = videoIdOf(a.video);
610
650
  if (!video)
@@ -634,7 +674,7 @@ export function registerTools(server, ctx) {
634
674
  },
635
675
  annotations: WRITE,
636
676
  }, wrap(async (a, extra) => {
637
- const { person, auth } = personOf(extra);
677
+ const { person, auth } = who(extra);
638
678
  requireContribute(auth);
639
679
  return append("handoff", person, auth, {
640
680
  from_circle: a.from_circle,
@@ -662,14 +702,20 @@ export function registerTools(server, ctx) {
662
702
  },
663
703
  annotations: WRITE,
664
704
  }, wrap(async (a, extra) => {
665
- const { person, auth } = personOf(extra);
705
+ const { person, auth } = who(extra);
666
706
  requireContribute(auth);
667
- const target = records().find((r) => r.id === a.record_id);
707
+ const target = all().find((r) => r.id === a.record_id);
668
708
  if (!target)
669
709
  return { state: "not_found", record_id: a.record_id };
670
710
  if (!["handoff", "inquiry", "source_intent", "observation", "work_proposal"].includes(target.kind)) {
671
711
  throw new ToolRefusal("not_updatable", `A ${target.kind} changes through its approval, not here.`);
672
712
  }
713
+ if (target.actor.person_id !== person.id && person.api_tier !== "write") {
714
+ throw new ToolRefusal("forbidden", "Only the person who made this record, or someone with Miadi's write tier, moves it on.");
715
+ }
716
+ if (isApprovable(target) && a.completion_receipt) {
717
+ throw new ToolRefusal("not_updatable", "A request's receipt comes from its decision, not from update_handoff.");
718
+ }
673
719
  append(target.kind, person, auth, {
674
720
  supersedes: target.id,
675
721
  refs: target.refs,
@@ -693,7 +739,7 @@ export function registerTools(server, ctx) {
693
739
  },
694
740
  annotations: WRITE,
695
741
  }, wrap(async (a, extra) => {
696
- const { person, auth } = personOf(extra);
742
+ const { person, auth } = who(extra);
697
743
  requireContribute(auth);
698
744
  const r = append("work_proposal", person, auth, {
699
745
  circle: a.circle,
@@ -720,7 +766,7 @@ export function registerTools(server, ctx) {
720
766
  },
721
767
  annotations: WRITE,
722
768
  }, wrap(async (a, extra) => {
723
- const { person, auth } = personOf(extra);
769
+ const { person, auth } = who(extra);
724
770
  requireContribute(auth);
725
771
  const start = new Date(a.start);
726
772
  if (Number.isNaN(start.getTime()))
@@ -744,7 +790,7 @@ export function registerTools(server, ctx) {
744
790
  },
745
791
  annotations: WRITE,
746
792
  }, wrap(async (a, extra) => {
747
- const { person, auth } = personOf(extra);
793
+ const { person, auth } = who(extra);
748
794
  requireContribute(auth);
749
795
  const id = a.session_id.replace(/^session:/, "");
750
796
  const r = append("session_completion_proposal", person, auth, {
@@ -771,7 +817,7 @@ export function registerTools(server, ctx) {
771
817
  },
772
818
  annotations: WRITE,
773
819
  }, wrap(async (a, extra) => {
774
- const { person, auth } = personOf(extra);
820
+ const { person, auth } = who(extra);
775
821
  requireContribute(auth);
776
822
  const r = append("run_request", person, auth, {
777
823
  circle: a.circle,
@@ -799,7 +845,7 @@ export function registerTools(server, ctx) {
799
845
  },
800
846
  annotations: WRITE,
801
847
  }, wrap(async (a, extra) => {
802
- const { person, auth } = personOf(extra);
848
+ const { person, auth } = who(extra);
803
849
  requireContribute(auth);
804
850
  const reviewId = a.review_id ? reviewIdOf(a.review_id) : null;
805
851
  const video = a.video ? videoIdOf(a.video) : null;
@@ -834,7 +880,7 @@ export function registerTools(server, ctx) {
834
880
  inputSchema: { review_id: z.string().min(8), version: z.number().int().min(1), episode: z.string().optional(), ...base },
835
881
  annotations: WRITE,
836
882
  }, wrap(async (a, extra) => {
837
- const { person, auth } = personOf(extra);
883
+ const { person, auth } = who(extra);
838
884
  requireContribute(auth);
839
885
  const id = reviewIdOf(a.review_id);
840
886
  if (!id)
@@ -865,7 +911,7 @@ export function registerTools(server, ctx) {
865
911
  },
866
912
  annotations: WRITE,
867
913
  }, wrap(async (a, extra) => {
868
- const { person, auth } = personOf(extra);
914
+ const { person, auth } = who(extra);
869
915
  requireContribute(auth);
870
916
  const id = reviewIdOf(a.review_id);
871
917
  if (!id)
@@ -899,7 +945,7 @@ export function registerTools(server, ctx) {
899
945
  },
900
946
  annotations: WRITE,
901
947
  }, wrap(async (a, extra) => {
902
- const { person, auth } = personOf(extra);
948
+ const { person, auth } = who(extra);
903
949
  requireContribute(auth);
904
950
  if (!config.inquiryRoot || !config.inquiryRepo)
905
951
  throw new ToolRefusal("unavailable", "This connector host has no inquiry shelf configured.");
@@ -968,7 +1014,7 @@ export function registerTools(server, ctx) {
968
1014
  },
969
1015
  annotations: WRITE,
970
1016
  }, wrap(async (a, extra) => {
971
- const { person, auth } = personOf(extra);
1017
+ const { person, auth } = who(extra);
972
1018
  requireContribute(auth);
973
1019
  if (Boolean(a.episode) === Boolean(a.new_episode_title)) {
974
1020
  throw new ToolRefusal("invalid_input", "Name either an existing episode or a new episode title, not both.");
@@ -1003,15 +1049,6 @@ export function registerTools(server, ctx) {
1003
1049
  return { ...r, approval_url: approvalUrl(r.id) };
1004
1050
  }));
1005
1051
  // ── decisions inside the conversation ─────────────────────────────────
1006
- const KIND_LABEL = {
1007
- run_request: "Agent run",
1008
- work_proposal: "Work proposal",
1009
- review_operation_request: "Review operation",
1010
- deep_diver_request: "Deep Diver",
1011
- review_episode_link_request: "Review into an episode",
1012
- inquiry_episode_request: "Inquiry into an episode",
1013
- session_completion_proposal: "Session ending",
1014
- };
1015
1052
  const titleOf = (r) => {
1016
1053
  const b = r.body;
1017
1054
  const t = b.title ?? b.task ?? b.desired_outcome ?? b.reason ?? b.operation ?? b.artefact ?? r.intention ?? r.kind;
@@ -1043,9 +1080,9 @@ export function registerTools(server, ctx) {
1043
1080
  _meta: { ui: { resourceUri: DECISIONS_CARD_URI }, "openai/outputTemplate": DECISIONS_CARD_URI, "openai/toolInvocation/invoking": "Gathering requests", "openai/toolInvocation/invoked": "Requests shown" },
1044
1081
  }, async ({ record_id }, extra) => {
1045
1082
  try {
1046
- const { person, auth } = personOf(extra);
1083
+ const { person, auth } = who(extra);
1047
1084
  const writer = person.api_tier === "write";
1048
- const waiting = records()
1085
+ const waiting = all()
1049
1086
  .filter((r) => r.approval_state === "awaiting_approval" && isApprovable(r))
1050
1087
  .filter((r) => writer || r.actor.person_id === person.id)
1051
1088
  .filter((r) => !record_id || r.id === record_id)
@@ -1060,7 +1097,7 @@ export function registerTools(server, ctx) {
1060
1097
  const decisions = waiting.map((r) => ({
1061
1098
  id: r.id,
1062
1099
  kind: r.kind,
1063
- kind_label: KIND_LABEL[r.kind] ?? r.kind,
1100
+ kind_label: KIND_LABELS[r.kind] ?? r.kind,
1064
1101
  title: titleOf(r),
1065
1102
  by: r.actor.name,
1066
1103
  via: r.actor.client_name,
@@ -1100,16 +1137,17 @@ export function registerTools(server, ctx) {
1100
1137
  decision_token: z.string().min(10).max(200),
1101
1138
  },
1102
1139
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1103
- _meta: { ui: { resourceUri: DECISIONS_CARD_URI, visibility: ["app"] }, "openai/widgetAccessible": true },
1140
+ // A card-only tool renders nothing itself: ChatGPT refuses a widget on a private tool.
1141
+ _meta: { ui: { visibility: ["app"] }, "openai/widgetAccessible": true },
1104
1142
  }, async (a, extra) => {
1105
1143
  try {
1106
- const { person, auth } = personOf(extra);
1144
+ const { person, auth } = who(extra);
1107
1145
  if (!auth.scopes.includes(SCOPE_APPROVE))
1108
1146
  throw new ToolRefusal("insufficient_scope", "This sign-in does not allow approving inside this client.");
1109
1147
  requireTier(person, "write");
1110
1148
  if (!ctx.decisionTokens || !ctx.approval)
1111
1149
  throw new ToolRefusal("unavailable", "In-chat decisions are not configured on this connector.");
1112
- const r = records().find((x) => x.id === a.record_id);
1150
+ const r = all().find((x) => x.id === a.record_id);
1113
1151
  if (!isApprovable(r))
1114
1152
  throw new ToolRefusal("not_found", "No request with that id.");
1115
1153
  const check = ctx.decisionTokens.verify(a.decision_token, r.id, person.id, auth.clientId);
@@ -1135,8 +1173,8 @@ export function registerTools(server, ctx) {
1135
1173
  return { structuredContent: out, content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
1136
1174
  }
1137
1175
  catch (error) {
1138
- if (error instanceof ToolRefusal) {
1139
- const out = { error: error.code, message: error.message };
1176
+ if (error instanceof ToolRefusal || error instanceof AlreadyDecided) {
1177
+ const out = error instanceof AlreadyDecided ? { error: "already_decided", message: error.message } : { error: error.code, message: error.message };
1140
1178
  return { isError: true, structuredContent: out, content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
1141
1179
  }
1142
1180
  return fail({ error: "failed", message: error instanceof Error ? error.message : String(error) });
package/dist/uploads.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export declare const MAX_UPLOAD_CHARS = 5000000;
2
+ /** An upload whose last part never came is dropped after a day. */
3
+ export declare const UPLOAD_TTL_MS: number;
2
4
  export declare class UploadRefusal extends Error {
3
5
  }
4
6
  export declare class ResearchUploads {
@@ -24,4 +26,5 @@ export declare class ResearchUploads {
24
26
  text: string;
25
27
  meta: Record<string, unknown>;
26
28
  };
29
+ private prune;
27
30
  }
package/dist/uploads.js CHANGED
@@ -4,10 +4,12 @@
4
4
  * joined in order, exactly, and the upload is removed. One person's upload is
5
5
  * never readable by another.
6
6
  */
7
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { randomUUID } from "node:crypto";
10
10
  export const MAX_UPLOAD_CHARS = 5_000_000;
11
+ /** An upload whose last part never came is dropped after a day. */
12
+ export const UPLOAD_TTL_MS = 24 * 3600 * 1000;
11
13
  export class UploadRefusal extends Error {
12
14
  }
13
15
  export class ResearchUploads {
@@ -22,6 +24,7 @@ export class ResearchUploads {
22
24
  }
23
25
  receive(personId, input) {
24
26
  mkdirSync(this.dir, { recursive: true, mode: 0o700 });
27
+ this.prune();
25
28
  let up;
26
29
  if (input.upload_id) {
27
30
  const p = this.path(input.upload_id);
@@ -51,4 +54,18 @@ export class ResearchUploads {
51
54
  rmSync(this.path(up.id), { force: true });
52
55
  return { complete: true, upload_id: up.id, text, meta: up.meta };
53
56
  }
57
+ prune(now = Date.now()) {
58
+ for (const name of readdirSync(this.dir)) {
59
+ if (!/^upload-[0-9a-f-]{36}\.json$/.test(name))
60
+ continue;
61
+ const p = join(this.dir, name);
62
+ try {
63
+ if (now - statSync(p).mtimeMs > UPLOAD_TTL_MS)
64
+ rmSync(p, { force: true });
65
+ }
66
+ catch {
67
+ /* removed by another call meanwhile */
68
+ }
69
+ }
70
+ }
54
71
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.5";
1
+ export declare const VERSION = "0.1.7";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.5";
1
+ export const VERSION = "0.1.7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miadi/connector",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "The Miadi connector: one remote MCP service through which a person signed in with their Miadi person token, from ChatGPT or any MCP client, reads sessions, reviews, episodes and circles and records observations, inquiries, handoffs and proposals with their name on each record.",
5
5
  "license": "MIT",
6
6
  "author": "Miadi",
@@ -30,6 +30,7 @@
30
30
  "@miadi/node-service-kit": "0.1.2",
31
31
  "@modelcontextprotocol/sdk": "^1.30.0",
32
32
  "express": "^5.2.1",
33
+ "express-rate-limit": "^8.2.1",
33
34
  "zod": "^4.4.0"
34
35
  },
35
36
  "devDependencies": {