@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/README.md +11 -7
- package/dist/approve.d.ts +6 -1
- package/dist/approve.js +130 -62
- package/dist/decisions.d.ts +1 -1
- package/dist/decisions.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/miadi.d.ts +1 -1
- package/dist/miadi.js +2 -1
- package/dist/oauth.d.ts +15 -0
- package/dist/oauth.js +101 -16
- package/dist/people.d.ts +3 -0
- package/dist/people.js +8 -2
- package/dist/reviews.js +2 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +10 -1
- package/dist/store.d.ts +2 -0
- package/dist/store.js +8 -2
- package/dist/tools.js +91 -53
- package/dist/uploads.d.ts +3 -0
- package/dist/uploads.js +18 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
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
|
-
|
|
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
|
-
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
705
|
+
const { person, auth } = who(extra);
|
|
666
706
|
requireContribute(auth);
|
|
667
|
-
const target =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
1083
|
+
const { person, auth } = who(extra);
|
|
1047
1084
|
const writer = person.api_tier === "write";
|
|
1048
|
-
const waiting =
|
|
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:
|
|
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
|
-
|
|
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 } =
|
|
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 =
|
|
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.
|
|
1
|
+
export declare const VERSION = "0.1.7";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
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.
|
|
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": {
|