@miadi/connector 0.1.10 → 0.1.11
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 +2 -1
- package/dist/episode-files.d.ts +62 -0
- package/dist/episode-files.js +115 -0
- package/dist/tools.js +118 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -20,12 +20,13 @@ Call `get_capabilities` first. It names the signed-in person, their Miadi API ti
|
|
|
20
20
|
|
|
21
21
|
| kind | tools |
|
|
22
22
|
|---|---|
|
|
23
|
-
| reads | `get_capabilities` `get_context` `list_circles` `get_circle` `list_sessions` `get_session` `get_evidence` `get_evaluations` `get_narrative_beats` `review_lookup` `review_versions` `handoff_status` `list_records` `get_run_result` `mission_status` `provider_capabilities` `search` `fetch` |
|
|
23
|
+
| reads | `get_capabilities` `get_context` `list_circles` `get_circle` `list_sessions` `get_session` `get_evidence` `read_episode_file` `get_evaluations` `get_narrative_beats` `review_lookup` `review_versions` `handoff_status` `list_records` `get_run_result` `mission_status` `provider_capabilities` `search` `fetch` |
|
|
24
24
|
| contributions | `append_observation` `open_inquiry` `link_records` `propose_narrative_beat` `record_evaluation` `register_source_intent` `record_handoff` `update_handoff` `prepare_calendar_event` `save_research` |
|
|
25
25
|
| requests (approved by a person) | `propose_work` `propose_session_completion` `request_authorized_run` `request_session_steer` `request_review_operation` `request_deep_diver` `link_review_to_episode` `request_inquiry_episode` |
|
|
26
26
|
| decisions in the conversation | `show_decisions` (cards), `decide_request` (called by the card only) |
|
|
27
27
|
|
|
28
28
|
- **Reads** need Miadi's read tier and stay within it. A person Miadi gives no API tier reaches only `get_capabilities`. Sessions need the write tier, because a session's name and pane are operator data. For the same reason, below the write tier a record someone else made shows no session, working folder or seat, and no long bodies.
|
|
29
|
+
- **Episode files.** `get_evidence` lists an episode's files; each text file Miadi can preview carries a `read_ref` (`miadi-chronicle://<n>/<path>`). `read_episode_file` (or `fetch` with that ref) returns the file's whole UTF-8 text, read as the person through Miadi's file door, so Miadi's audience and room rules apply. The answer names the episode, the path, the content type, the byte count and the sha256, and `verified` says whether the text received hashes to what Miadi read. Only md, txt, yaml, yml and json files up to 512 KiB are read. A traversal, absolute, hidden or encoded path is refused before any URL is built. A longer text comes in pages cut on character boundaries: pass `page.next` back, and the joined pages hash to `sha256`. Other kinds answer `unsupported`, a file over the limit `too_large`, a missing one `not_found`, a ceremony note below the write gate `forbidden`.
|
|
29
30
|
- **Contributions** need Miadi's read tier and the `miadi:contribute` scope, chosen at sign-in. A record is moved on (`update_handoff`) only by its author or the write tier, and a request's receipt comes only from its decision. They append a record to the ledger and change nothing else, except `save_research`: it saves a report on the inquiry shelf as a new folder (the Markdown exactly as given, owner and visibility recorded, private by default), opens the folder's inquiry issue, and commits and pushes the folder in the shelf's repository. A long report is sent in parts (`part`, `parts`, `upload_id`) and saved when the last part arrives; the answer returns its bytes, sha256 and first and last lines.
|
|
30
31
|
- **Requests** append a record that waits for a decision. The answer gives an `approval_url`; Miadi's `/research` page offers the same decisions to a signed-in person with the write tier, and `show_decisions` shows them as cards inside the conversation (below). The person approves, approves with changes (their instructions are carried into what runs), sends it back for revision with instructions (`revision_requested`, nothing runs, `get_run_result` returns the instructions), or declines. Only the approval door runs a request, once, as that person; a second decision on the same request, at the same time or later, is refused:
|
|
31
32
|
- a run becomes a brief in the factory queue, which the intake delivers to a session;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** The extensions Miadi's vessel reads as text (@medicine-wheel/episode-vessel TEXT_EXTENSIONS). */
|
|
2
|
+
export declare const TEXT_TYPES: Record<string, string>;
|
|
3
|
+
/** The vessel's own limit for reading text whole (MAX_TEXT_BYTES). */
|
|
4
|
+
export declare const MAX_TEXT_BYTES: number;
|
|
5
|
+
export declare const DEFAULT_PAGE_BYTES = 100000;
|
|
6
|
+
/** A page must hold the longest UTF-8 character. */
|
|
7
|
+
export declare const MIN_PAGE_BYTES = 4;
|
|
8
|
+
export declare class FileRefusal extends Error {
|
|
9
|
+
readonly code: "invalid_path" | "invalid_offset" | "unsupported" | "changed";
|
|
10
|
+
constructor(code: "invalid_path" | "invalid_offset" | "unsupported" | "changed", message: string);
|
|
11
|
+
}
|
|
12
|
+
/** The same rule as the vessel's assertRelativeFile, applied before any URL exists. */
|
|
13
|
+
export declare function checkRelativePath(value: string): string[];
|
|
14
|
+
export declare function extensionOf(path: string): string;
|
|
15
|
+
/** `miadi-chronicle://548/foundations/x/README.md` → its episode and path; null for any other id. */
|
|
16
|
+
export declare function parseFileRef(id: string): {
|
|
17
|
+
episode: string;
|
|
18
|
+
path: string;
|
|
19
|
+
} | null;
|
|
20
|
+
export declare function fileRef(episodeNumber: number | string, path: string): string;
|
|
21
|
+
/** The episode number a folder carries (`2026-09-27-episode-548-…` → 548). */
|
|
22
|
+
export declare function episodeNumberOf(folder: string): number | null;
|
|
23
|
+
export interface MiadiText {
|
|
24
|
+
episode: string;
|
|
25
|
+
file: string;
|
|
26
|
+
content: string;
|
|
27
|
+
size: number;
|
|
28
|
+
revision?: {
|
|
29
|
+
sha256?: string;
|
|
30
|
+
bytes?: number;
|
|
31
|
+
modifiedAt?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One page of a file Miadi returned whole. `offset` must be 0 or a
|
|
36
|
+
* `next.offset` this function gave, so a page never starts inside a character;
|
|
37
|
+
* `revision`, when given, must still be the file's sha256, so pages of two
|
|
38
|
+
* versions are never joined.
|
|
39
|
+
*/
|
|
40
|
+
export declare function pageOf(text: MiadiText, opts?: {
|
|
41
|
+
offset?: number;
|
|
42
|
+
maxBytes?: number;
|
|
43
|
+
revision?: string;
|
|
44
|
+
}): {
|
|
45
|
+
page: {
|
|
46
|
+
offset: number;
|
|
47
|
+
end: number;
|
|
48
|
+
bytes: number;
|
|
49
|
+
sha256: string;
|
|
50
|
+
complete: boolean;
|
|
51
|
+
next: {
|
|
52
|
+
offset: number;
|
|
53
|
+
revision: string;
|
|
54
|
+
} | null;
|
|
55
|
+
};
|
|
56
|
+
text: string;
|
|
57
|
+
modified_at?: string | undefined;
|
|
58
|
+
verification?: string | undefined;
|
|
59
|
+
bytes: number;
|
|
60
|
+
sha256: string;
|
|
61
|
+
verified: boolean;
|
|
62
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading one text file an episode holds, through Miadi's file door
|
|
3
|
+
* (`GET /api/chronicle/episodes/<ref>/files/<path>`, jgwill/Miadi#653), as the
|
|
4
|
+
* signed-in person. Miadi decides what that person may read: the episode's
|
|
5
|
+
* audience, the room rule (ceremony notes and captures), the vessel's path
|
|
6
|
+
* guard and its 512 KiB text limit. The connector adds two things Miadi cannot
|
|
7
|
+
* see from its side of the URL:
|
|
8
|
+
*
|
|
9
|
+
* - the path is checked before a URL is built, because a URL parser folds
|
|
10
|
+
* `a/../../people` into another route before Miadi ever reads it;
|
|
11
|
+
* - the text is paged by UTF-8 bytes on character boundaries, each page
|
|
12
|
+
* carrying the whole file's sha256, so a reader can join the pages and prove
|
|
13
|
+
* they hold the file exactly (jgwill/Miadi#815).
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
/** The extensions Miadi's vessel reads as text (@medicine-wheel/episode-vessel TEXT_EXTENSIONS). */
|
|
17
|
+
export const TEXT_TYPES = {
|
|
18
|
+
".md": "text/markdown; charset=utf-8",
|
|
19
|
+
".txt": "text/plain; charset=utf-8",
|
|
20
|
+
".yaml": "application/yaml; charset=utf-8",
|
|
21
|
+
".yml": "application/yaml; charset=utf-8",
|
|
22
|
+
".json": "application/json; charset=utf-8",
|
|
23
|
+
};
|
|
24
|
+
/** The vessel's own limit for reading text whole (MAX_TEXT_BYTES). */
|
|
25
|
+
export const MAX_TEXT_BYTES = 512 * 1024;
|
|
26
|
+
export const DEFAULT_PAGE_BYTES = 100_000;
|
|
27
|
+
/** A page must hold the longest UTF-8 character. */
|
|
28
|
+
export const MIN_PAGE_BYTES = 4;
|
|
29
|
+
export class FileRefusal extends Error {
|
|
30
|
+
code;
|
|
31
|
+
constructor(code, message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The same rule as the vessel's assertRelativeFile, applied before any URL exists. */
|
|
37
|
+
export function checkRelativePath(value) {
|
|
38
|
+
if (!value || value.includes("\\") || value.includes("\0") || value.startsWith("/") || /^[a-z]+:/i.test(value)) {
|
|
39
|
+
throw new FileRefusal("invalid_path", "Name a file by its path inside the episode, as get_evidence lists it (e.g. foundations/<topic>/README.md).");
|
|
40
|
+
}
|
|
41
|
+
const segments = value.split("/");
|
|
42
|
+
if (segments.some((s) => !s || s === "." || s === ".." || s.startsWith(".") || /%2e|%2f|%5c|%00/i.test(s))) {
|
|
43
|
+
throw new FileRefusal("invalid_path", "The path has an empty, '.', '..', hidden or encoded segment; episode files are read by their listed path only.");
|
|
44
|
+
}
|
|
45
|
+
return segments;
|
|
46
|
+
}
|
|
47
|
+
export function extensionOf(path) {
|
|
48
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
49
|
+
const dot = name.lastIndexOf(".");
|
|
50
|
+
return dot > 0 ? name.slice(dot).toLowerCase() : "";
|
|
51
|
+
}
|
|
52
|
+
/** `miadi-chronicle://548/foundations/x/README.md` → its episode and path; null for any other id. */
|
|
53
|
+
export function parseFileRef(id) {
|
|
54
|
+
const m = /^miadi-chronicle:(?:\/\/)?([^/\s]+)\/(.+)$/.exec(id.trim());
|
|
55
|
+
return m ? { episode: m[1], path: m[2] } : null;
|
|
56
|
+
}
|
|
57
|
+
export function fileRef(episodeNumber, path) {
|
|
58
|
+
return `miadi-chronicle://${episodeNumber}/${path}`;
|
|
59
|
+
}
|
|
60
|
+
/** The episode number a folder carries (`2026-09-27-episode-548-…` → 548). */
|
|
61
|
+
export function episodeNumberOf(folder) {
|
|
62
|
+
const m = /(?:^|-)episode-0*(\d+)(?:-|$)/.exec(folder);
|
|
63
|
+
return m ? Number.parseInt(m[1], 10) : null;
|
|
64
|
+
}
|
|
65
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
66
|
+
/**
|
|
67
|
+
* One page of a file Miadi returned whole. `offset` must be 0 or a
|
|
68
|
+
* `next.offset` this function gave, so a page never starts inside a character;
|
|
69
|
+
* `revision`, when given, must still be the file's sha256, so pages of two
|
|
70
|
+
* versions are never joined.
|
|
71
|
+
*/
|
|
72
|
+
export function pageOf(text, opts = {}) {
|
|
73
|
+
const bytes = Buffer.from(text.content, "utf8");
|
|
74
|
+
const total = bytes.byteLength;
|
|
75
|
+
const whole = sha256(bytes);
|
|
76
|
+
if (opts.revision && opts.revision !== whole) {
|
|
77
|
+
throw new FileRefusal("changed", `The file changed since that page was read (it was ${opts.revision}, it is ${whole} now). Read it again from offset 0.`);
|
|
78
|
+
}
|
|
79
|
+
const offset = opts.offset ?? 0;
|
|
80
|
+
const maxBytes = Math.max(MIN_PAGE_BYTES, opts.maxBytes ?? DEFAULT_PAGE_BYTES);
|
|
81
|
+
if (!Number.isInteger(offset) || offset < 0 || offset > total || (offset < total && (bytes[offset] & 0xc0) === 0x80)) {
|
|
82
|
+
throw new FileRefusal("invalid_offset", `offset must be 0 or a next.offset this tool returned (the file has ${total} bytes).`);
|
|
83
|
+
}
|
|
84
|
+
let end = Math.min(total, offset + maxBytes);
|
|
85
|
+
while (end < total && (bytes[end] & 0xc0) === 0x80)
|
|
86
|
+
end--;
|
|
87
|
+
const page = bytes.subarray(offset, end);
|
|
88
|
+
const complete = end === total;
|
|
89
|
+
// Miadi computed its sha256 over the text it decoded; the bytes on disk are
|
|
90
|
+
// `size`. Both agree only when the file is valid UTF-8 and was read whole.
|
|
91
|
+
const miadiSha = text.revision?.sha256;
|
|
92
|
+
const verified = miadiSha === whole && total === text.size;
|
|
93
|
+
return {
|
|
94
|
+
bytes: total,
|
|
95
|
+
sha256: whole,
|
|
96
|
+
verified,
|
|
97
|
+
...(verified
|
|
98
|
+
? {}
|
|
99
|
+
: {
|
|
100
|
+
verification: miadiSha !== whole
|
|
101
|
+
? `Miadi named sha256 ${miadiSha ?? "none"}; the text received hashes to ${whole}.`
|
|
102
|
+
: `The file is ${text.size} bytes on disk and ${total} bytes as UTF-8 text: it is not valid UTF-8, and the text holds replacement characters.`,
|
|
103
|
+
}),
|
|
104
|
+
...(text.revision?.modifiedAt ? { modified_at: text.revision.modifiedAt } : {}),
|
|
105
|
+
page: {
|
|
106
|
+
offset,
|
|
107
|
+
end,
|
|
108
|
+
bytes: page.byteLength,
|
|
109
|
+
sha256: sha256(page),
|
|
110
|
+
complete,
|
|
111
|
+
next: complete ? null : { offset: end, revision: whole },
|
|
112
|
+
},
|
|
113
|
+
text: page.toString("utf8"),
|
|
114
|
+
};
|
|
115
|
+
}
|
package/dist/tools.js
CHANGED
|
@@ -24,6 +24,7 @@ import { checkGrant, isBusy, steerAttempts, steeringGrantOf, steerSession } from
|
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { artefactVisibility, readArtefactWeave, saveResearchArtefact } from "@miadi/inquiry-weave";
|
|
26
26
|
import { ResearchUploads, UploadRefusal } from "./uploads.js";
|
|
27
|
+
import { DEFAULT_PAGE_BYTES, FileRefusal, MAX_TEXT_BYTES, MIN_PAGE_BYTES, TEXT_TYPES, checkRelativePath, episodeNumberOf, extensionOf, fileRef, pageOf, parseFileRef, } from "./episode-files.js";
|
|
27
28
|
class ToolRefusal extends Error {
|
|
28
29
|
code;
|
|
29
30
|
detail;
|
|
@@ -63,6 +64,8 @@ function wrap(fn) {
|
|
|
63
64
|
return fail({ error: error.code, message: error.message, ...(error.detail ? { detail: error.detail } : {}) });
|
|
64
65
|
if (error instanceof UploadRefusal)
|
|
65
66
|
return fail({ error: "invalid_upload", message: error.message });
|
|
67
|
+
if (error instanceof FileRefusal)
|
|
68
|
+
return fail({ error: error.code, message: error.message });
|
|
66
69
|
if (error instanceof MiadiError) {
|
|
67
70
|
const unavailable = error.status === 0;
|
|
68
71
|
return fail({
|
|
@@ -327,12 +330,105 @@ export function registerTools(server, ctx) {
|
|
|
327
330
|
]);
|
|
328
331
|
return {
|
|
329
332
|
episode: decodeURIComponent(ref),
|
|
330
|
-
files,
|
|
333
|
+
files: withReadRefs(files),
|
|
331
334
|
screenwalks,
|
|
332
335
|
reviews: epReviews,
|
|
333
336
|
connector_records: touching(`miadi-chronicle://${decodeURIComponent(ref)}`),
|
|
334
337
|
};
|
|
335
338
|
}));
|
|
339
|
+
/**
|
|
340
|
+
* One text file of one episode, read as the person through Miadi's file
|
|
341
|
+
* door; Miadi applies their audience, the room rule and the vessel's path
|
|
342
|
+
* guard, and the path is checked here before a URL is built (jgwill/Miadi#815).
|
|
343
|
+
*/
|
|
344
|
+
const readEpisodeFile = async (person, a) => {
|
|
345
|
+
const path = a.path.trim();
|
|
346
|
+
const segments = checkRelativePath(path);
|
|
347
|
+
const extension = extensionOf(path);
|
|
348
|
+
const contentType = TEXT_TYPES[extension];
|
|
349
|
+
if (!contentType) {
|
|
350
|
+
throw new FileRefusal("unsupported", `${path} is not read as text here: only ${Object.keys(TEXT_TYPES).join(", ")} files are. Miadi serves documents (html, pdf, csv, svg) as downloads and images and media by their own routes; get_evidence lists them.`);
|
|
351
|
+
}
|
|
352
|
+
const ref = episodeRef(a.episode);
|
|
353
|
+
// A number, epNNN or a folder name; `..` would be folded by the URL parser into another route.
|
|
354
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(decodeURIComponent(ref))) {
|
|
355
|
+
throw new FileRefusal("invalid_path", "Name the episode as miadi-chronicle://<n>, its number or its folder.");
|
|
356
|
+
}
|
|
357
|
+
const url = `/api/chronicle/episodes/${ref}/files/${segments.map(encodeURIComponent).join("/")}`;
|
|
358
|
+
let text;
|
|
359
|
+
try {
|
|
360
|
+
text = await miadi.get(person.token, url);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
if (!(error instanceof MiadiError))
|
|
364
|
+
throw error;
|
|
365
|
+
const body = (error.body ?? {});
|
|
366
|
+
const detail = { episode: decodeURIComponent(ref), path, miadi: { status: error.status, ...body } };
|
|
367
|
+
if (error.status === 0)
|
|
368
|
+
throw new ToolRefusal("unavailable", "Miadi could not be reached.", detail);
|
|
369
|
+
if (body.error === "not_in_the_room")
|
|
370
|
+
throw new ToolRefusal("forbidden", body.message ?? "This file is not part of the episode room.", detail);
|
|
371
|
+
if (error.status === 401 || error.status === 403)
|
|
372
|
+
throw new ToolRefusal("forbidden", body.message ?? "Miadi does not let this person read that file.", detail);
|
|
373
|
+
if (error.status === 404)
|
|
374
|
+
throw new ToolRefusal("not_found", body.message ?? `No file ${path} in ${decodeURIComponent(ref)}.`, detail);
|
|
375
|
+
if (body.error === "TOO_LARGE")
|
|
376
|
+
throw new ToolRefusal("too_large", `${path} is larger than the ${MAX_TEXT_BYTES / 1024} KiB Miadi reads as text whole; nothing of it is returned.`, detail);
|
|
377
|
+
if (error.status === 415)
|
|
378
|
+
throw new ToolRefusal("unsupported", body.message ?? `${path} is not text Miadi reads.`, detail);
|
|
379
|
+
if (error.status === 400)
|
|
380
|
+
throw new ToolRefusal("invalid_path", body.message ?? `${path} is not a readable path in the episode.`, detail);
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
if (typeof text?.content !== "string")
|
|
384
|
+
throw new ToolRefusal("unsupported", `Miadi did not return ${path} as text.`);
|
|
385
|
+
const number = episodeNumberOf(text.episode);
|
|
386
|
+
const episodeAddress = number === null ? `chronicle:${text.episode}` : `miadi-chronicle://${number}`;
|
|
387
|
+
return {
|
|
388
|
+
ref: fileRef(number ?? text.episode, text.file ?? path),
|
|
389
|
+
episode: { ref: episodeAddress, number, folder: text.episode },
|
|
390
|
+
path: text.file ?? path,
|
|
391
|
+
content_type: contentType,
|
|
392
|
+
...pageOf(text, { offset: a.offset, maxBytes: a.max_bytes, revision: a.revision }),
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
/** Each text file Miadi lists as previewable carries the ref read_episode_file and fetch take. */
|
|
396
|
+
const withReadRefs = (listing) => {
|
|
397
|
+
const folder = listing?.episode?.path;
|
|
398
|
+
if (!folder || !Array.isArray(listing?.files))
|
|
399
|
+
return listing;
|
|
400
|
+
const number = listing.episode.number ?? episodeNumberOf(folder);
|
|
401
|
+
return {
|
|
402
|
+
...listing,
|
|
403
|
+
files: listing.files.map((f) => f?.kind === "text" && f.previewable && TEXT_TYPES[extensionOf(String(f.relativePath))]
|
|
404
|
+
? { ...f, read_ref: fileRef(number ?? folder, f.relativePath) }
|
|
405
|
+
: f),
|
|
406
|
+
connector_read: "A file with read_ref is read whole, in pages if long, by read_episode_file or fetch. Files without one are listed only.",
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
server.registerTool("read_episode_file", {
|
|
410
|
+
title: "Read one file an episode holds",
|
|
411
|
+
description: "The full UTF-8 text of one text file (md, txt, yaml, yml, json) in a Chronicle episode, read as you through Miadi, with its content type, byte count and sha256. Name the episode and the path get_evidence lists (or pass its read_ref as episode alone). A file longer than max_bytes comes in pages: pass page.next back to read the next one; joined, the pages hash to sha256. Read only: nothing is changed.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
episode: z.string().min(1).describe("miadi-chronicle://<n>, an episode number or its folder; or a read_ref (miadi-chronicle://<n>/<path>) with path left out."),
|
|
414
|
+
path: z.string().optional().describe("The file's path inside the episode, as get_evidence lists it, e.g. foundations/broker-adapters/README.md."),
|
|
415
|
+
offset: z.number().int().min(0).optional().describe("0, or page.next.offset from the previous page."),
|
|
416
|
+
revision: z.string().regex(/^[0-9a-f]{64}$/).optional().describe("page.next.revision from the previous page: refuses with changed if the file moved since."),
|
|
417
|
+
max_bytes: z.number().int().min(MIN_PAGE_BYTES).max(MAX_TEXT_BYTES).optional().describe(`Page size in bytes (default ${DEFAULT_PAGE_BYTES}).`),
|
|
418
|
+
},
|
|
419
|
+
annotations: READ,
|
|
420
|
+
}, wrap(async (a, extra) => {
|
|
421
|
+
const { person } = who(extra);
|
|
422
|
+
let episode = a.episode;
|
|
423
|
+
let path = a.path;
|
|
424
|
+
if (!path) {
|
|
425
|
+
const parsed = parseFileRef(a.episode);
|
|
426
|
+
if (!parsed)
|
|
427
|
+
throw new FileRefusal("invalid_path", "Name the file: give path, or pass a read_ref (miadi-chronicle://<n>/<path>) as episode.");
|
|
428
|
+
({ episode, path } = parsed);
|
|
429
|
+
}
|
|
430
|
+
return readEpisodeFile(person, { ...a, episode, path });
|
|
431
|
+
}));
|
|
336
432
|
server.registerTool("get_evaluations", {
|
|
337
433
|
title: "Evaluations: provider traces and recorded assessments",
|
|
338
434
|
description: "The tracing records (LLMOps provider traces and prompts) an episode holds, and the assessments people recorded with record_evaluation, operational and creator-experience kept apart.",
|
|
@@ -563,11 +659,29 @@ export function registerTools(server, ctx) {
|
|
|
563
659
|
}));
|
|
564
660
|
server.registerTool("fetch", {
|
|
565
661
|
title: "Fetch one item found by search",
|
|
566
|
-
description: "The full text of an item returned by search: a review (miadi-review:<uuid>) or a connector record id.",
|
|
662
|
+
description: "The full text of an item returned by search: a review (miadi-review:<uuid>) or a connector record id; or of an episode file by the read_ref get_evidence lists (miadi-chronicle://<n>/<path>), its first page when long.",
|
|
567
663
|
inputSchema: { id: z.string().min(1) },
|
|
568
664
|
annotations: READ,
|
|
569
665
|
}, wrap(async ({ id }, extra) => {
|
|
570
|
-
who(extra);
|
|
666
|
+
const { person } = who(extra);
|
|
667
|
+
const file = parseFileRef(id);
|
|
668
|
+
if (file) {
|
|
669
|
+
const read = await readEpisodeFile(person, file);
|
|
670
|
+
return {
|
|
671
|
+
id: read.ref,
|
|
672
|
+
title: `${read.path} (${read.episode.ref})`,
|
|
673
|
+
text: read.text,
|
|
674
|
+
url: `${config.miadiPublicUrl}/chronicle/${encodeURIComponent(read.episode.folder)}`,
|
|
675
|
+
metadata: {
|
|
676
|
+
content_type: read.content_type,
|
|
677
|
+
bytes: read.bytes,
|
|
678
|
+
sha256: read.sha256,
|
|
679
|
+
verified: read.verified,
|
|
680
|
+
complete: read.page.complete,
|
|
681
|
+
...(read.page.next ? { next: { tool: "read_episode_file", episode: read.ref, ...read.page.next } } : {}),
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
}
|
|
571
685
|
const reviewId = reviewIdOf(id);
|
|
572
686
|
if (reviewId) {
|
|
573
687
|
const r = await reviews.get(reviewId);
|
|
@@ -1434,6 +1548,7 @@ export const TOOL_STATUS = {
|
|
|
1434
1548
|
list_sessions: "live, Miadi write tier",
|
|
1435
1549
|
get_session: "live, Miadi write tier",
|
|
1436
1550
|
get_evidence: "live",
|
|
1551
|
+
read_episode_file: "live (text files an episode holds: md, txt, yaml, yml, json, up to 512 KiB, paged; read as you, nothing changed)",
|
|
1437
1552
|
get_evaluations: "live (tracing records and recorded assessments; no provider scores)",
|
|
1438
1553
|
get_narrative_beats: "live (NCP project beats and candidate beats)",
|
|
1439
1554
|
review_lookup: "live (video search needs the review token on the connector host)",
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.11";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.11";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miadi/connector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@miadi/inquiry-weave": "0.18.
|
|
29
|
+
"@miadi/inquiry-weave": "0.18.8",
|
|
30
30
|
"@miadi/node-service-kit": "0.1.2",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
32
|
"express": "^5.2.1",
|