@miadi/connector 0.1.2 → 0.1.4
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 +3 -2
- package/dist/approve.d.ts +11 -2
- package/dist/approve.js +67 -22
- package/dist/records.d.ts +6 -2
- package/dist/records.js +6 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +2 -1
- package/dist/tools.js +44 -17
- package/dist/uploads.d.ts +27 -0
- package/dist/uploads.js +54 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -25,9 +25,10 @@ Call `get_capabilities` first. It names the signed-in person, their Miadi API ti
|
|
|
25
25
|
| requests (approved by a person) | `propose_work` `propose_session_completion` `request_authorized_run` `request_review_operation` `request_deep_diver` `link_review_to_episode` `request_inquiry_episode` |
|
|
26
26
|
|
|
27
27
|
- **Reads** stay within the person's Miadi tier. Sessions need the write tier, because a session's name and pane are operator data.
|
|
28
|
-
- **Contributions** need the `miadi:contribute` scope, chosen at sign-in. 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.
|
|
29
|
-
- **Requests** append a record that waits for
|
|
28
|
+
- **Contributions** need the `miadi:contribute` scope, chosen at sign-in. 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.
|
|
29
|
+
- **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. 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:
|
|
30
30
|
- a run becomes a brief in the factory queue, which the intake delivers to a session;
|
|
31
|
+
- a work proposal starts its work the same way, as a brief written from the proposal;
|
|
31
32
|
- a review operation calls the review service;
|
|
32
33
|
- Deep Diver marks the review version ready;
|
|
33
34
|
- an episode link goes through Miadi's episode door;
|
package/dist/approve.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ export interface ApprovalDeps {
|
|
|
25
25
|
factoryDir?: string;
|
|
26
26
|
tmuxSessions?: () => string[];
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* What a person may decide on a request. Approving with changes runs it with the
|
|
30
|
+
* approver's instructions carried into what runs; a revision sends it back to
|
|
31
|
+
* whoever asked, with instructions, and runs nothing.
|
|
32
|
+
*/
|
|
33
|
+
export declare const DECISIONS: readonly ["approve", "approve_with_changes", "request_revision", "decline"];
|
|
34
|
+
export type Decision = (typeof DECISIONS)[number];
|
|
28
35
|
export declare function factoryDir(env?: NodeJS.ProcessEnv): string;
|
|
29
36
|
export declare function runBriefFile(record: {
|
|
30
37
|
id: string;
|
|
@@ -39,9 +46,11 @@ export declare function readQueueState(record: ConnectorRecord, dir?: string): {
|
|
|
39
46
|
file: string;
|
|
40
47
|
note: string;
|
|
41
48
|
};
|
|
42
|
-
|
|
49
|
+
/** The task a work proposal asks for, written from the proposal's own fields. */
|
|
50
|
+
export declare function proposalTask(record: ConnectorRecord): string;
|
|
51
|
+
export declare function runBrief(record: ConnectorRecord, approver: MiadiPerson, sessions: string[], instructions?: string): {
|
|
43
52
|
to: string;
|
|
44
53
|
brief: string;
|
|
45
54
|
};
|
|
46
|
-
export declare function executeApproved(record: ConnectorRecord, approver: MiadiPerson, deps: ApprovalDeps): Promise<Record<string, unknown>>;
|
|
55
|
+
export declare function executeApproved(record: ConnectorRecord, approver: MiadiPerson, deps: ApprovalDeps, instructions?: string): Promise<Record<string, unknown>>;
|
|
47
56
|
export declare function registerApprovalRoutes(app: Express, deps: ApprovalDeps): void;
|
package/dist/approve.js
CHANGED
|
@@ -26,6 +26,12 @@ const APPROVABLE = new Set([
|
|
|
26
26
|
"session_completion_proposal",
|
|
27
27
|
"inquiry_episode_request",
|
|
28
28
|
]);
|
|
29
|
+
/**
|
|
30
|
+
* What a person may decide on a request. Approving with changes runs it with the
|
|
31
|
+
* approver's instructions carried into what runs; a revision sends it back to
|
|
32
|
+
* whoever asked, with instructions, and runs nothing.
|
|
33
|
+
*/
|
|
34
|
+
export const DECISIONS = ["approve", "approve_with_changes", "request_revision", "decline"];
|
|
29
35
|
export function factoryDir(env = process.env) {
|
|
30
36
|
return env.MIADI_FACTORY_DIR || join(homedir(), ".miadi", "factory");
|
|
31
37
|
}
|
|
@@ -63,8 +69,19 @@ function yymmdd(d = new Date()) {
|
|
|
63
69
|
function attr(v) {
|
|
64
70
|
return v.replace(/["<>\n]/g, " ");
|
|
65
71
|
}
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
/** The task a work proposal asks for, written from the proposal's own fields. */
|
|
73
|
+
export function proposalTask(record) {
|
|
74
|
+
const b = record.body;
|
|
75
|
+
const lines = ["Carry out this approved work proposal, within its own scope and constraints.", ""];
|
|
76
|
+
for (const [k, v] of Object.entries(b)) {
|
|
77
|
+
if (v === undefined || v === null || v === "")
|
|
78
|
+
continue;
|
|
79
|
+
lines.push(`${k.replace(/_/g, " ")}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
80
|
+
}
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
export function runBrief(record, approver, sessions, instructions) {
|
|
84
|
+
const body = (record.kind === "work_proposal" ? { task: proposalTask(record) } : record.body);
|
|
68
85
|
const short = record.id.split(":")[1]?.slice(0, 8) ?? "run";
|
|
69
86
|
const to = body.session && sessions.includes(body.session) ? body.session : `new:${body.session ?? `connector-${yymmdd()}-${short}`}`;
|
|
70
87
|
const cwd = body.cwd || "/a/src/Miadi-18";
|
|
@@ -86,6 +103,16 @@ export function runBrief(record, approver, sessions) {
|
|
|
86
103
|
// The intake reads one <input> block per file: a closing tag inside the task would end it early.
|
|
87
104
|
body.task.replace(/<\/?(input|task)\b/gi, (m) => m.replace("<", "‹")),
|
|
88
105
|
"</task>",
|
|
106
|
+
...(instructions
|
|
107
|
+
? [
|
|
108
|
+
"",
|
|
109
|
+
`${approver.name} approved it with these instructions. Where they differ from the task, they win:`,
|
|
110
|
+
"",
|
|
111
|
+
"<instructions>",
|
|
112
|
+
instructions.replace(/<\/?(input|task|instructions)\b/gi, (m) => m.replace("<", "‹")),
|
|
113
|
+
"</instructions>",
|
|
114
|
+
]
|
|
115
|
+
: []),
|
|
89
116
|
"",
|
|
90
117
|
"When it is done, reply with what was achieved, the evidence (commits, paths, URLs), what is still open, and whether this session should end. Then record the receipt through the connector's update_handoff if a handoff names this record, and end the session per its lifecycle.",
|
|
91
118
|
"",
|
|
@@ -98,7 +125,7 @@ function plainName(name) {
|
|
|
98
125
|
const n = name.replace(/[^\w .'-]/g, "").trim().slice(0, 40);
|
|
99
126
|
return /^[^\W_]/.test(n) ? n : "Miadi connector";
|
|
100
127
|
}
|
|
101
|
-
export async function executeApproved(record, approver, deps) {
|
|
128
|
+
export async function executeApproved(record, approver, deps, instructions) {
|
|
102
129
|
const f = deps.fetchImpl ?? fetch;
|
|
103
130
|
const b = record.body;
|
|
104
131
|
const reviewCall = async (method, path, payload) => {
|
|
@@ -120,15 +147,17 @@ export async function executeApproved(record, approver, deps) {
|
|
|
120
147
|
}
|
|
121
148
|
return { status: res.status, ok: res.ok, response: parsed };
|
|
122
149
|
};
|
|
150
|
+
const queueBrief = () => {
|
|
151
|
+
const queue = join(deps.factoryDir ?? factoryDir(), "queue");
|
|
152
|
+
mkdirSync(queue, { recursive: true });
|
|
153
|
+
const { to, brief } = runBrief(record, approver, (deps.tmuxSessions ?? liveTmux)(), instructions);
|
|
154
|
+
const file = runBriefFile(record);
|
|
155
|
+
writeFileSync(join(queue, file), brief, { flag: "wx", mode: 0o640 });
|
|
156
|
+
return { outcome: "queued", to, file, note: "The factory intake delivers queued briefs every 5 minutes." };
|
|
157
|
+
};
|
|
123
158
|
switch (record.kind) {
|
|
124
|
-
case "run_request":
|
|
125
|
-
|
|
126
|
-
mkdirSync(queue, { recursive: true });
|
|
127
|
-
const { to, brief } = runBrief(record, approver, (deps.tmuxSessions ?? liveTmux)());
|
|
128
|
-
const file = runBriefFile(record);
|
|
129
|
-
writeFileSync(join(queue, file), brief, { flag: "wx", mode: 0o640 });
|
|
130
|
-
return { outcome: "queued", to, file, note: "The factory intake delivers queued briefs every 5 minutes." };
|
|
131
|
-
}
|
|
159
|
+
case "run_request":
|
|
160
|
+
return queueBrief();
|
|
132
161
|
case "review_operation_request": {
|
|
133
162
|
const url = b.video_id ? `https://youtu.be/${b.video_id}` : undefined;
|
|
134
163
|
const r = b.operation === "create"
|
|
@@ -181,7 +210,8 @@ export async function executeApproved(record, approver, deps) {
|
|
|
181
210
|
}
|
|
182
211
|
}
|
|
183
212
|
case "work_proposal":
|
|
184
|
-
|
|
213
|
+
// Approving a proposal starts its work: a factory brief written from the proposal and the approver's instructions.
|
|
214
|
+
return queueBrief();
|
|
185
215
|
case "session_completion_proposal":
|
|
186
216
|
return {
|
|
187
217
|
outcome: "decided",
|
|
@@ -233,7 +263,7 @@ ${r.refs.length ? `<dt>References</dt><dd>${r.refs.map(escapeHtml).join("<br>")}
|
|
|
233
263
|
}
|
|
234
264
|
export function registerApprovalRoutes(app, deps) {
|
|
235
265
|
const find = (id) => current(deps.ledger.all()).find((r) => r.id === id);
|
|
236
|
-
const form = express.urlencoded({ extended: false, limit: "
|
|
266
|
+
const form = express.urlencoded({ extended: false, limit: "128kb" });
|
|
237
267
|
app.get("/approve/:id", (req, res) => {
|
|
238
268
|
const r = find(String(req.params.id));
|
|
239
269
|
if (!r || !APPROVABLE.has(r.kind)) {
|
|
@@ -262,12 +292,24 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
262
292
|
res.status(403).type("html").send(approvePage(r, `Approving needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.`));
|
|
263
293
|
return;
|
|
264
294
|
}
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
295
|
+
const choice = DECISIONS.includes(String(body.decision)) ? body.decision : "decline";
|
|
296
|
+
const instructions = String(body.instructions ?? "").trim().slice(0, 20000) || undefined;
|
|
297
|
+
if ((choice === "approve_with_changes" || choice === "request_revision") && !instructions) {
|
|
298
|
+
res.status(400).type("html").send(approvePage(r, choice === "request_revision" ? "Say what to revise." : "Write the changes to apply."));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const decision = choice === "approve" || choice === "approve_with_changes" ? "approved" : choice === "request_revision" ? "revision_requested" : "declined";
|
|
302
|
+
let receipt = {
|
|
303
|
+
decided_by: { person_id: person.id, name: person.name },
|
|
304
|
+
decided_at: new Date().toISOString(),
|
|
305
|
+
decision: choice,
|
|
306
|
+
...(instructions ? { instructions } : {}),
|
|
307
|
+
note: body.note || undefined,
|
|
308
|
+
};
|
|
309
|
+
let state = decision === "approved" ? "completed" : decision === "revision_requested" ? "pending" : "withdrawn";
|
|
268
310
|
if (decision === "approved") {
|
|
269
311
|
try {
|
|
270
|
-
const outcome = await executeApproved(r, person, deps);
|
|
312
|
+
const outcome = await executeApproved(r, person, deps, instructions);
|
|
271
313
|
receipt = { ...receipt, ...outcome };
|
|
272
314
|
if (outcome.outcome === "failed")
|
|
273
315
|
state = "blocked";
|
|
@@ -283,16 +325,17 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
283
325
|
id: newRecordId(r.kind),
|
|
284
326
|
kind: r.kind,
|
|
285
327
|
created_at: new Date().toISOString(),
|
|
286
|
-
actor: { person_id: person.id, name: person.name, client_id: "approval-page" },
|
|
328
|
+
actor: { person_id: person.id, name: person.name, client_id: String(body.via ?? "approval-page").slice(0, 60) },
|
|
287
329
|
refs: r.refs,
|
|
288
330
|
evidence_refs: [],
|
|
289
331
|
supersedes: r.id,
|
|
290
332
|
approval_state: decision,
|
|
291
333
|
state,
|
|
334
|
+
...(decision === "revision_requested" ? { next_action: `Revise and submit a new request that cites ${r.id} in refs.` } : {}),
|
|
292
335
|
completion_receipt: receipt,
|
|
293
|
-
body: { decision },
|
|
336
|
+
body: { decision: choice, ...(instructions ? { instructions } : {}) },
|
|
294
337
|
});
|
|
295
|
-
res.type("html").send(page("Recorded", `<h1>${decision === "approved" ? "Approved" : "Declined"}</h1>${summary(find(r.id))}<pre>${escapeHtml(JSON.stringify(receipt, null, 2))}</pre>`));
|
|
338
|
+
res.type("html").send(page("Recorded", `<h1>${decision === "approved" ? "Approved" : decision === "revision_requested" ? "Sent back for revision" : "Declined"}</h1>${summary(find(r.id))}<pre>${escapeHtml(JSON.stringify(receipt, null, 2))}</pre>`));
|
|
296
339
|
});
|
|
297
340
|
// A record's own page, for citations in ChatGPT: any person Miadi recognises may read it.
|
|
298
341
|
app.get("/records/:id", (req, res) => {
|
|
@@ -328,10 +371,12 @@ ${r.approval_state === "awaiting_approval"
|
|
|
328
371
|
? `<form method="post">
|
|
329
372
|
<label for="t">Your Miadi person token (write tier)</label>
|
|
330
373
|
<input id="t" name="person_token" type="password" autocomplete="off" required>
|
|
331
|
-
<label for="
|
|
332
|
-
<textarea id="
|
|
374
|
+
<label for="i">Instructions (needed to approve with changes or to send back)</label>
|
|
375
|
+
<textarea id="i" name="instructions" rows="4"></textarea>
|
|
333
376
|
<div class="row">
|
|
334
377
|
<button class="primary" type="submit" name="decision" value="approve">Approve and run</button>
|
|
378
|
+
<button type="submit" name="decision" value="approve_with_changes">Approve with my changes</button>
|
|
379
|
+
<button type="submit" name="decision" value="request_revision">Send back for revision</button>
|
|
335
380
|
<button type="submit" name="decision" value="decline">Decline</button>
|
|
336
381
|
</div></form>`
|
|
337
382
|
: ""}`);
|
package/dist/records.d.ts
CHANGED
|
@@ -40,8 +40,12 @@ export type RecordKind = (typeof RECORD_KINDS)[number];
|
|
|
40
40
|
/** #816 handoff states. */
|
|
41
41
|
export declare const HANDOFF_STATES: readonly ["pending", "blocked", "ready", "in_progress", "completed", "withdrawn"];
|
|
42
42
|
export type HandoffState = (typeof HANDOFF_STATES)[number];
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Approval of a request that a person must grant before anything runs.
|
|
45
|
+
* `revision_requested`: sent back to whoever asked, with the approver's
|
|
46
|
+
* instructions in the receipt; nothing of it ran.
|
|
47
|
+
*/
|
|
48
|
+
export declare const APPROVAL_STATES: readonly ["awaiting_approval", "approved", "declined", "revision_requested", "not_required"];
|
|
45
49
|
export type ApprovalState = (typeof APPROVAL_STATES)[number];
|
|
46
50
|
export interface Actor {
|
|
47
51
|
person_id: string;
|
package/dist/records.js
CHANGED
|
@@ -63,8 +63,12 @@ export const RECORD_KINDS = [
|
|
|
63
63
|
];
|
|
64
64
|
/** #816 handoff states. */
|
|
65
65
|
export const HANDOFF_STATES = ["pending", "blocked", "ready", "in_progress", "completed", "withdrawn"];
|
|
66
|
-
/**
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Approval of a request that a person must grant before anything runs.
|
|
68
|
+
* `revision_requested`: sent back to whoever asked, with the approver's
|
|
69
|
+
* instructions in the receipt; nothing of it ran.
|
|
70
|
+
*/
|
|
71
|
+
export const APPROVAL_STATES = ["awaiting_approval", "approved", "declined", "revision_requested", "not_required"];
|
|
68
72
|
export function newRecordId(kind) {
|
|
69
73
|
return `${kind}:${randomUUID()}`;
|
|
70
74
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -15,4 +15,4 @@ export declare function createConnectorApp(config: ConnectorConfig, deps: Connec
|
|
|
15
15
|
provider: MiadiOAuthProvider;
|
|
16
16
|
ledger: Ledger<ConnectorRecord>;
|
|
17
17
|
};
|
|
18
|
-
export declare const INSTRUCTIONS = "Miadi is a relational software factory: agent sessions, Miadi reviews of screenwalks, Chronicle episodes, talking circles and the five accountable circles (Story, Stewardship, Engineering, Evaluation, Continuity) across three domains (Creative, Ceremonial, Engineering).\n\nStart with get_capabilities: it says who you are signed in as, which tools are live, and which return not_implemented.\n\nRules every tool keeps:\n- Every record names the person who made it, with a claim category: observed, hypothesis, desired_future, fiction or verified_implementation. Keep fiction and desired futures apart from what was observed.\n- Requests (runs, review operations, Deep Diver, review-to-episode links,
|
|
18
|
+
export declare const INSTRUCTIONS = "Miadi is a relational software factory: agent sessions, Miadi reviews of screenwalks, Chronicle episodes, talking circles and the five accountable circles (Story, Stewardship, Engineering, Evaluation, Continuity) across three domains (Creative, Ceremonial, Engineering).\n\nStart with get_capabilities: it says who you are signed in as, which tools are live, and which return not_implemented.\n\nRules every tool keeps:\n- Every record names the person who made it, with a claim category: observed, hypothesis, desired_future, fiction or verified_implementation. Keep fiction and desired futures apart from what was observed.\n- Requests (runs, work proposals, review operations, Deep Diver, review-to-episode links, inquiry-to-episode weaves, session completion) are recorded as awaiting_approval. Nothing runs, publishes or closes until a person decides in Miadi. The person may approve, approve with changes (their instructions are carried into what runs), send it back for revision with instructions (approval_state revision_requested; read the instructions with get_run_result and submit a revised request citing the old id in refs), or decline.\n- A research report (a Deep Research report) is delivered with save_research exactly as written, never summarised or shortened, with its sources; a long report goes in parts. Check the bytes and first and last lines the answer returns against what you sent.\n- An unknown id is answered as not found, never invented. A missing backing service is answered as unavailable.\n- Use the miadi-review:<uuid>, miadi-chronicle://<n>, owner/repo#n and session:<uuid> reference forms.";
|
package/dist/server.js
CHANGED
|
@@ -116,6 +116,7 @@ Start with get_capabilities: it says who you are signed in as, which tools are l
|
|
|
116
116
|
|
|
117
117
|
Rules every tool keeps:
|
|
118
118
|
- Every record names the person who made it, with a claim category: observed, hypothesis, desired_future, fiction or verified_implementation. Keep fiction and desired futures apart from what was observed.
|
|
119
|
-
- Requests (runs, review operations, Deep Diver, review-to-episode links,
|
|
119
|
+
- Requests (runs, work proposals, review operations, Deep Diver, review-to-episode links, inquiry-to-episode weaves, session completion) are recorded as awaiting_approval. Nothing runs, publishes or closes until a person decides in Miadi. The person may approve, approve with changes (their instructions are carried into what runs), send it back for revision with instructions (approval_state revision_requested; read the instructions with get_run_result and submit a revised request citing the old id in refs), or decline.
|
|
120
|
+
- A research report (a Deep Research report) is delivered with save_research exactly as written, never summarised or shortened, with its sources; a long report goes in parts. Check the bytes and first and last lines the answer returns against what you sent.
|
|
120
121
|
- An unknown id is answered as not found, never invented. A missing backing service is answered as unavailable.
|
|
121
122
|
- Use the miadi-review:<uuid>, miadi-chronicle://<n>, owner/repo#n and session:<uuid> reference forms.`;
|
package/dist/tools.js
CHANGED
|
@@ -19,6 +19,7 @@ import { VERSION } from "./version.js";
|
|
|
19
19
|
import { readQueueState } from "./approve.js";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { artefactVisibility, readArtefactWeave, saveResearchArtefact } from "@miadi/inquiry-weave";
|
|
22
|
+
import { ResearchUploads, UploadRefusal } from "./uploads.js";
|
|
22
23
|
class ToolRefusal extends Error {
|
|
23
24
|
code;
|
|
24
25
|
detail;
|
|
@@ -56,6 +57,8 @@ function wrap(fn) {
|
|
|
56
57
|
catch (error) {
|
|
57
58
|
if (error instanceof ToolRefusal)
|
|
58
59
|
return fail({ error: error.code, message: error.message, ...(error.detail ? { detail: error.detail } : {}) });
|
|
60
|
+
if (error instanceof UploadRefusal)
|
|
61
|
+
return fail({ error: "invalid_upload", message: error.message });
|
|
59
62
|
if (error instanceof MiadiError) {
|
|
60
63
|
const unavailable = error.status === 0;
|
|
61
64
|
return fail({
|
|
@@ -102,6 +105,7 @@ export function registerTools(server, ctx) {
|
|
|
102
105
|
});
|
|
103
106
|
};
|
|
104
107
|
const records = () => current(ledger.all());
|
|
108
|
+
const uploads = new ResearchUploads(join(config.stateDir, "uploads"));
|
|
105
109
|
const touching = (ref) => records().filter((r) => r.refs.includes(ref) || r.evidence_refs.includes(ref) || r.id === ref);
|
|
106
110
|
// ── reads ──────────────────────────────────────────────────────────────
|
|
107
111
|
server.registerTool("get_capabilities", {
|
|
@@ -404,7 +408,7 @@ export function registerTools(server, ctx) {
|
|
|
404
408
|
}));
|
|
405
409
|
server.registerTool("get_run_result", {
|
|
406
410
|
title: "Where a requested run stands",
|
|
407
|
-
description: "A
|
|
411
|
+
description: "A request's decision (approved, approved with changes, sent back for revision, declined), the approver's instructions, the brief queued for the factory intake once approved, whether the intake delivered it, and its receipt.",
|
|
408
412
|
inputSchema: { record_id: z.string().min(1) },
|
|
409
413
|
annotations: READ,
|
|
410
414
|
}, wrap(async ({ record_id }, extra) => {
|
|
@@ -415,7 +419,10 @@ export function registerTools(server, ctx) {
|
|
|
415
419
|
return {
|
|
416
420
|
record: r,
|
|
417
421
|
approval_url: r.approval_state === "awaiting_approval" ? approvalUrl(r.id) : undefined,
|
|
418
|
-
queue: r.kind === "run_request" ? readQueueState(r, ctx.factoryDir) : undefined,
|
|
422
|
+
queue: r.kind === "run_request" || r.kind === "work_proposal" ? readQueueState(r, ctx.factoryDir) : undefined,
|
|
423
|
+
...(r.approval_state === "revision_requested"
|
|
424
|
+
? { revision: { instructions: r.completion_receipt?.instructions, next: `Submit a revised request that cites ${r.id} in refs.` } }
|
|
425
|
+
: {}),
|
|
419
426
|
};
|
|
420
427
|
}));
|
|
421
428
|
// search / fetch: the pair ChatGPT uses for connectors in search and deep research.
|
|
@@ -674,7 +681,7 @@ export function registerTools(server, ctx) {
|
|
|
674
681
|
}));
|
|
675
682
|
server.registerTool("propose_work", {
|
|
676
683
|
title: "Propose work to a circle",
|
|
677
|
-
description: "Propose a bounded piece of work: what should exist when it is done, the circle accountable, the sources.
|
|
684
|
+
description: "Propose a bounded piece of work: what should exist when it is done, the circle accountable, the sources. Nothing starts until a person decides. Approving it starts the work as a factory run, with any changes the person writes; the person may instead send it back for revision with instructions (approval_state revision_requested, instructions in completion_receipt): then submit a revised proposal citing the old id in refs.",
|
|
678
685
|
inputSchema: {
|
|
679
686
|
title: z.string().min(1).max(200),
|
|
680
687
|
desired_outcome: z.string().min(1).max(6000),
|
|
@@ -877,10 +884,13 @@ export function registerTools(server, ctx) {
|
|
|
877
884
|
// ── research on the inquiry shelf (jgwill/Miadi#879) ───────────────────
|
|
878
885
|
server.registerTool("save_research", {
|
|
879
886
|
title: "Save a research report on the inquiry shelf",
|
|
880
|
-
description: "
|
|
887
|
+
description: "Deliver a finished research report (a Deep Research report) to Miadi in your name, exactly as written: never summarised, shortened or rewritten, with its sources and citations. It is saved byte-for-byte in a new folder on Miadi's inquiry shelf, gets its inquiry issue, and is committed and pushed. A report longer than about 60,000 characters is sent in parts: call with part 1 and parts N, then with the upload_id returned and parts 2..N, each part the next slice of the text cut at a line end; the report is saved when the last part arrives. The answer gives bytes, sha256 and the report's first and last lines so you can confirm all of it arrived. Private to you unless shared with a circle; never published from here. The same report saved again returns the folder already saved. Weave it into an episode afterwards with request_inquiry_episode.",
|
|
881
888
|
inputSchema: {
|
|
882
889
|
title: z.string().min(1).max(200),
|
|
883
|
-
markdown: z.string().min(1).max(1_000_000).describe("The whole report,
|
|
890
|
+
markdown: z.string().min(1).max(1_000_000).describe("The whole report, or this part of it, exactly as written."),
|
|
891
|
+
part: z.number().int().min(1).max(200).optional().describe("Which part this is, from 1, when the report is sent in parts."),
|
|
892
|
+
parts: z.number().int().min(1).max(200).optional().describe("How many parts the report is sent in."),
|
|
893
|
+
upload_id: z.string().max(80).optional().describe("Returned by part 1; give it with every later part."),
|
|
884
894
|
refs: refs.describe("Records and sources the report answers: a connector inquiry id, a DOI or URL, owner/repo#n."),
|
|
885
895
|
visibility: z.enum(["private", "circle"]).default("private"),
|
|
886
896
|
circle: circle.optional().describe("The circle it is shared with, when visibility is circle."),
|
|
@@ -894,20 +904,32 @@ export function registerTools(server, ctx) {
|
|
|
894
904
|
throw new ToolRefusal("unavailable", "This connector host has no inquiry shelf configured.");
|
|
895
905
|
if (a.visibility === "circle" && !a.circle)
|
|
896
906
|
throw new ToolRefusal("invalid_input", "Name the circle the report is shared with.");
|
|
907
|
+
let markdown = a.markdown;
|
|
908
|
+
let meta = { title: a.title, refs: a.refs, visibility: a.visibility, circle: a.circle };
|
|
909
|
+
const parts = a.parts ?? 1;
|
|
910
|
+
if (parts > 1 || a.upload_id) {
|
|
911
|
+
const got = uploads.receive(person.id, { upload_id: a.upload_id, part: a.part ?? 1, parts, text: a.markdown, meta });
|
|
912
|
+
if (!got.complete) {
|
|
913
|
+
return { upload_id: got.upload_id, received: got.received, missing: got.missing, bytes_so_far: got.bytes, next: `Send part ${got.missing[0]} of ${got.parts} with upload_id ${got.upload_id}.` };
|
|
914
|
+
}
|
|
915
|
+
markdown = got.text;
|
|
916
|
+
meta = got.meta;
|
|
917
|
+
}
|
|
897
918
|
const saved = await saveResearchArtefact({
|
|
898
919
|
inquiryRoot: config.inquiryRoot,
|
|
899
920
|
repo: config.inquiryRepo,
|
|
900
|
-
title:
|
|
901
|
-
markdown
|
|
921
|
+
title: meta.title,
|
|
922
|
+
markdown,
|
|
902
923
|
owner: { person_id: person.id, name: person.name },
|
|
903
|
-
visibility:
|
|
904
|
-
circle:
|
|
924
|
+
visibility: meta.visibility,
|
|
925
|
+
circle: meta.circle,
|
|
905
926
|
via: "miadi-connector",
|
|
906
|
-
refs:
|
|
927
|
+
refs: meta.refs,
|
|
907
928
|
});
|
|
929
|
+
const lines = markdown.split("\n").filter((l) => l.trim());
|
|
908
930
|
const r = append("research_artifact", person, auth, {
|
|
909
|
-
circle:
|
|
910
|
-
refs: [`inquiry-artefact:${saved.artefact}`, ...(saved.issue ? [saved.issue] : []), ...
|
|
931
|
+
circle: meta.circle ?? "stewardship",
|
|
932
|
+
refs: [`inquiry-artefact:${saved.artefact}`, ...(saved.issue ? [saved.issue] : []), ...meta.refs],
|
|
911
933
|
intention: a.intention,
|
|
912
934
|
role: a.role,
|
|
913
935
|
idempotency_key: a.idempotency_key ?? `research:${saved.sha256}`,
|
|
@@ -915,18 +937,23 @@ export function registerTools(server, ctx) {
|
|
|
915
937
|
state: saved.shelf.state === "landed" ? "completed" : "blocked",
|
|
916
938
|
next_action: "request_inquiry_episode to weave it into an episode or start one",
|
|
917
939
|
body: {
|
|
918
|
-
title:
|
|
940
|
+
title: meta.title,
|
|
919
941
|
artefact: saved.artefact,
|
|
920
942
|
issue: saved.issue,
|
|
921
943
|
issue_url: saved.issue_url,
|
|
922
944
|
sha256: saved.sha256,
|
|
923
945
|
bytes: saved.bytes,
|
|
924
946
|
visibility: saved.visibility,
|
|
925
|
-
circle:
|
|
947
|
+
circle: meta.circle,
|
|
926
948
|
shelf: { state: saved.shelf.state, observed: saved.shelf.observed },
|
|
927
949
|
},
|
|
928
950
|
});
|
|
929
|
-
return {
|
|
951
|
+
return {
|
|
952
|
+
...r,
|
|
953
|
+
saved: saved.status,
|
|
954
|
+
received: { bytes: saved.bytes, sha256: saved.sha256, parts, first_line: lines[0]?.slice(0, 200), last_line: lines.at(-1)?.slice(0, 200) },
|
|
955
|
+
research_url: `${config.miadiPublicUrl}/research`,
|
|
956
|
+
};
|
|
930
957
|
}));
|
|
931
958
|
server.registerTool("request_inquiry_episode", {
|
|
932
959
|
title: "Request that an inquiry be woven into an episode, or start one",
|
|
@@ -1046,13 +1073,13 @@ export const TOOL_STATUS = {
|
|
|
1046
1073
|
register_source_intent: "live, contribute",
|
|
1047
1074
|
record_handoff: "live, contribute",
|
|
1048
1075
|
update_handoff: "live, contribute",
|
|
1049
|
-
propose_work: "live, contribute, needs_approval",
|
|
1076
|
+
propose_work: "live, contribute, needs_approval (approving starts the work as a factory run)",
|
|
1050
1077
|
prepare_calendar_event: "live, contribute (draft and .ics only)",
|
|
1051
1078
|
propose_session_completion: "live, contribute, needs_approval (decision recorded, nothing closed)",
|
|
1052
1079
|
request_authorized_run: "live, contribute, needs_approval (queues a factory brief once approved)",
|
|
1053
1080
|
request_review_operation: "live, contribute, needs_approval",
|
|
1054
1081
|
request_deep_diver: "live, contribute, needs_approval",
|
|
1055
1082
|
link_review_to_episode: "live, contribute, needs_approval",
|
|
1056
|
-
save_research: "live, contribute (
|
|
1083
|
+
save_research: "live, contribute (the whole report, in parts when long; saved on the inquiry shelf with its issue, pushed; private by default)",
|
|
1057
1084
|
request_inquiry_episode: "live, contribute, needs_approval (weaves a shelf folder into an episode or starts one)",
|
|
1058
1085
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const MAX_UPLOAD_CHARS = 5000000;
|
|
2
|
+
export declare class UploadRefusal extends Error {
|
|
3
|
+
}
|
|
4
|
+
export declare class ResearchUploads {
|
|
5
|
+
private readonly dir;
|
|
6
|
+
constructor(dir: string);
|
|
7
|
+
private path;
|
|
8
|
+
receive(personId: string, input: {
|
|
9
|
+
upload_id?: string;
|
|
10
|
+
part: number;
|
|
11
|
+
parts: number;
|
|
12
|
+
text: string;
|
|
13
|
+
meta: Record<string, unknown>;
|
|
14
|
+
}): {
|
|
15
|
+
upload_id: string;
|
|
16
|
+
parts: number;
|
|
17
|
+
received: number[];
|
|
18
|
+
missing: number[];
|
|
19
|
+
bytes: number;
|
|
20
|
+
complete: false;
|
|
21
|
+
} | {
|
|
22
|
+
complete: true;
|
|
23
|
+
upload_id: string;
|
|
24
|
+
text: string;
|
|
25
|
+
meta: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
}
|
package/dist/uploads.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A research report sent in parts (save_research with part/parts): each part is
|
|
3
|
+
* kept under <state>/uploads/<id>.json until the last arrives, then the parts are
|
|
4
|
+
* joined in order, exactly, and the upload is removed. One person's upload is
|
|
5
|
+
* never readable by another.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
export const MAX_UPLOAD_CHARS = 5_000_000;
|
|
11
|
+
export class UploadRefusal extends Error {
|
|
12
|
+
}
|
|
13
|
+
export class ResearchUploads {
|
|
14
|
+
dir;
|
|
15
|
+
constructor(dir) {
|
|
16
|
+
this.dir = dir;
|
|
17
|
+
}
|
|
18
|
+
path(id) {
|
|
19
|
+
if (!/^upload-[0-9a-f-]{36}$/.test(id))
|
|
20
|
+
throw new UploadRefusal(`Unknown upload_id ${id}.`);
|
|
21
|
+
return join(this.dir, `${id}.json`);
|
|
22
|
+
}
|
|
23
|
+
receive(personId, input) {
|
|
24
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
25
|
+
let up;
|
|
26
|
+
if (input.upload_id) {
|
|
27
|
+
const p = this.path(input.upload_id);
|
|
28
|
+
if (!existsSync(p))
|
|
29
|
+
throw new UploadRefusal(`Unknown upload_id ${input.upload_id}: start again with part 1.`);
|
|
30
|
+
up = JSON.parse(readFileSync(p, "utf8"));
|
|
31
|
+
if (up.person_id !== personId)
|
|
32
|
+
throw new UploadRefusal(`Unknown upload_id ${input.upload_id}.`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (input.part !== 1)
|
|
36
|
+
throw new UploadRefusal("Start with part 1; it returns the upload_id the later parts carry.");
|
|
37
|
+
up = { id: `upload-${randomUUID()}`, person_id: personId, parts: input.parts, received: {}, meta: input.meta, created_at: new Date().toISOString() };
|
|
38
|
+
}
|
|
39
|
+
if (input.part > up.parts)
|
|
40
|
+
throw new UploadRefusal(`This upload has ${up.parts} parts; part ${input.part} does not exist.`);
|
|
41
|
+
up.received[String(input.part)] = input.text;
|
|
42
|
+
const total = Object.values(up.received).reduce((n, t) => n + t.length, 0);
|
|
43
|
+
if (total > MAX_UPLOAD_CHARS)
|
|
44
|
+
throw new UploadRefusal(`The report passes ${MAX_UPLOAD_CHARS} characters.`);
|
|
45
|
+
const missing = Array.from({ length: up.parts }, (_, i) => i + 1).filter((n) => !(String(n) in up.received));
|
|
46
|
+
if (missing.length) {
|
|
47
|
+
writeFileSync(this.path(up.id), JSON.stringify(up), { mode: 0o600 });
|
|
48
|
+
return { upload_id: up.id, parts: up.parts, received: Object.keys(up.received).map(Number).sort((a, b) => a - b), missing, bytes: total, complete: false };
|
|
49
|
+
}
|
|
50
|
+
const text = Array.from({ length: up.parts }, (_, i) => up.received[String(i + 1)]).join("");
|
|
51
|
+
rmSync(this.path(up.id), { force: true });
|
|
52
|
+
return { complete: true, upload_id: up.id, text, meta: up.meta };
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.4";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.4";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miadi/connector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"import": "./dist/index.js"
|
|
14
14
|
},
|
|
15
|
+
"./records": {
|
|
16
|
+
"types": "./dist/records.d.ts",
|
|
17
|
+
"import": "./dist/records.js"
|
|
18
|
+
},
|
|
15
19
|
"./package.json": "./package.json"
|
|
16
20
|
},
|
|
17
21
|
"bin": {
|