@miadi/connector 0.1.24 → 0.1.26

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 CHANGED
@@ -126,6 +126,8 @@ The drafter gets two rounds: a report and a revision, twice. The third report's
126
126
 
127
127
  In a client that renders MCP Apps (ChatGPT among them), `show_decisions` draws one card per request awaiting a decision: what is asked, by whom, and what approving does. The person decides on the card with their own click: Approve, Approve with my changes, Send back for revision, or Decline. The card then shows the receipt.
128
128
 
129
+ A new card address needs a new URI, and the earlier ones stay readable (`DECISIONS_CARD_EARLIER_URIS`). A host keeps the tool metadata it read until its connection is refreshed by hand, and draws an empty card for a template it cannot read. The journal holds one line for each `show_decisions` call and each template read (`card_template_read`), so a blank card can be told from a card nobody asked for.
130
+
129
131
  A decision is recorded once. An approved run that calls another service (a review operation, Deep Diver, an episode door, a steer) is written to the ledger as approved and running before the call, so a restart can never offer the request for a second approval; a run the connector was recording when it stopped reads as failed and may be run again. A decision whose run takes longer than `MIADI_CONNECTOR_DECISION_WAIT_MS` is answered as recorded and running, and the card reads `get_run_result` every 3 seconds until the outcome is in. The same decision sent again (a host retrying a call, a second click, the page form sent twice) answers with the first one's receipt, marked `replayed`, and runs nothing. A different decision is refused as `decision_conflict`, with where the request stands. When the host gives up on a call, the card reads where the request stands before it offers the buttons again.
130
132
 
131
133
  **A created review gets its transcript (jgwill/Miadi#925).** An approved `create` calls the review service, then reads the video's original-language YouTube captions with a local `uvx yt-dlp` and stores them on the review as `[mm:ss] text`, so an agent reads them through `fetch` and the factory's kind briefs find them with `miadi_review.py transcript`. No model is called for the transcript. The service answers an empty `POST /api/reviews/:id/transcript` by asking Gemini, so an empty one is never sent. `completion_receipt.transcript` says where it stands, and the card and the approval page say the same:
package/dist/approve.js CHANGED
@@ -403,7 +403,7 @@ blockquote{margin:.2rem 0 .6rem;padding-left:.8rem;border-left:2px solid var(--l
403
403
  @media (prefers-color-scheme:dark){.err{color:#ff8a80}}
404
404
  </style></head><body><main>${body}</main></body></html>`;
405
405
  }
406
- function summary(r) {
406
+ function summary(r, o = {}) {
407
407
  const shown = { ...r.body };
408
408
  for (const k of ["markdown", "transcript"])
409
409
  if (typeof shown[k] === "string")
@@ -414,7 +414,7 @@ function summary(r) {
414
414
  <dt>By</dt><dd>${escapeHtml(r.actor.name)}${r.actor.client_name ? ` through ${escapeHtml(r.actor.client_name)}` : ""}</dd>
415
415
  <dt>When</dt><dd>${escapeHtml(r.created_at)}</dd>
416
416
  ${r.circle ? `<dt>Circle</dt><dd>${escapeHtml(r.circle)}</dd>` : ""}
417
- ${r.intention ? `<dt>Intention</dt><dd>${escapeHtml(r.intention)}</dd>` : ""}
417
+ ${r.intention && o.intention !== false ? `<dt>Intention</dt><dd>${escapeHtml(r.intention)}</dd>` : ""}
418
418
  ${r.refs.length ? `<dt>References</dt><dd>${r.refs.map(escapeHtml).join("<br>")}</dd>` : ""}
419
419
  <dt>State</dt><dd>${escapeHtml(r.approval_state ?? "—")}</dd>
420
420
  ${versionOf(r) > 1 ? `<dt>Version</dt><dd>${versionOf(r)}, the one a decision here decides. Earlier versions stay in the ledger.</dd>` : ""}
@@ -831,6 +831,8 @@ export function registerApprovalRoutes(app, deps) {
831
831
  }
832
832
  if (!person)
833
833
  return void res.status(401).type("html").send(signInToRead(r, "Miadi did not recognise that person token."));
834
+ // What the page was sent, for the journal: signing in and deciding look alike from the person's side (jgwill/Miadi#939).
835
+ logDecision(deps, { phase: "page_post", record: r.id, decision: String(body.decision ?? "") || null, approval_state: r.approval_state });
834
836
  const writer = person.api_tier === "write";
835
837
  if (!writer && r.actor.person_id !== person.id) {
836
838
  return void res.status(403).type("html").send(signInToRead(r, `Reading and deciding this request needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.`));
@@ -984,14 +986,14 @@ function signInToRead(r, error) {
984
986
  // The label names no one and no instruction: this page is shown before sign-in.
985
987
  const open = r.approval_state === "awaiting_approval";
986
988
  const label = projectRequest(r, { recording: isBeingDecided(r.id) })?.label ?? r.approval_state ?? "";
987
- return page(open ? "A Miadi request awaits a decision" : "A Miadi request was decided", `<h1>${open ? "A request awaits a decision" : "A request was decided"}</h1>
989
+ return page(open ? "A Miadi request awaits a decision" : "A Miadi request was decided", `<h1>${open ? "Sign in to read this request" : "A request was decided"}</h1>
988
990
  <p>${escapeHtml(KIND_LABELS[r.kind] ?? r.kind)}, asked ${escapeHtml(r.created_at.slice(0, 16).replace("T", " "))} UTC. ${escapeHtml(label)}.</p>
989
- <p>${open ? "Sign in to read it. A person with Miadi's write tier then decides; whoever asked can read it." : "Sign in to read what was decided and what came of it. Whoever asked, and anyone with Miadi's write tier, can read it."}</p>
991
+ <p>${open ? "Signing in approves nothing. The next page shows the request, and a person with Miadi's write tier decides there. Whoever asked can read it." : "Sign in to read what was decided and what came of it. Whoever asked, and anyone with Miadi's write tier, can read it."}</p>
990
992
  ${error ? `<p class="err" role="alert">${escapeHtml(error)}</p>` : ""}
991
993
  <form method="post">
992
994
  <label for="t">Your Miadi person token</label>
993
995
  <input id="t" name="person_token" type="password" autocomplete="off" required>
994
- <div class="row"><button class="primary" type="submit">Read the request</button></div>
996
+ <div class="row"><button class="primary" type="submit">Sign in and read the request</button></div>
995
997
  </form>`);
996
998
  }
997
999
  /** The request, and the decision form for a write-tier person (their token carried in the form they just signed into). */
@@ -1081,12 +1083,20 @@ function approvePage(deps, r, o = {}) {
1081
1083
  // Freshness binds a decision still to come; a decided run shows the authorization it holds (#926 review R1).
1082
1084
  const authorization = r.kind === "run_request" ? authorizeProvider(r, deps.ledger.all(), { requireFresh: open }) : undefined;
1083
1085
  const blocked = Boolean(authorization && !authorization.authorized);
1084
- return page("Approve a Miadi request", `<h1>${open ? "Approve a request" : "A decided request"}</h1>
1086
+ // Signed in and able to decide: the decision comes before the record's details, and no line says it waits for someone else.
1087
+ const canDecideHere = Boolean(actionable && o.token);
1088
+ const heading = canDecideHere ? "Not approved yet: decide here" : open ? "A request awaiting a decision" : (projectionOf(r, deps)?.label ?? "A decided request");
1089
+ const details = canDecideHere
1090
+ ? `<details><summary>What exactly is asked</summary>${summary(r, { intention: false })}</details>`
1091
+ : `${summary(r)}
1092
+ ${open ? draftReviewSection(r, deps.draftReview?.stored(r), false) : ""}
1093
+ ${projectionBlock(projectionOf(r, deps), open ? undefined : r.completion_receipt)}`;
1094
+ return page("Approve a Miadi request", `<h1>${escapeHtml(heading)}</h1>
1095
+ ${canDecideHere ? `<p>You are signed in. Nothing is approved until you press a button below.</p>` : ""}
1085
1096
  ${actionable ? `<p>${escapeHtml(consequenceOf(r, authorization, current(deps.ledger.all())))} Declining records your refusal. Either is kept in the ledger with your name.</p>` : ""}
1097
+ ${canDecideHere && r.intention ? `<p><strong>Intention:</strong> ${escapeHtml(r.intention)}</p>` : ""}
1086
1098
  ${authorization ? providerSection(r, authorization, actionable ? o.chooserToken : undefined) : ""}
1087
- ${summary(r)}
1088
- ${open ? draftReviewSection(r, deps.draftReview?.stored(r), Boolean(actionable && o.token && !blocked)) : ""}
1089
- ${projectionBlock(projectionOf(r, deps), open ? undefined : r.completion_receipt)}
1099
+ ${canDecideHere ? draftReviewSection(r, deps.draftReview?.stored(r), !blocked) : ""}
1090
1100
  ${o.error ? `<p class="err" role="alert">${escapeHtml(o.error)}</p>` : ""}
1091
1101
  ${actionable && o.token
1092
1102
  ? `<form method="post">
@@ -1111,7 +1121,8 @@ ${blocked
1111
1121
  <form method="post">
1112
1122
  <input type="hidden" name="person_token" value="${escapeHtml(o.token)}">
1113
1123
  <div class="row"><button class="primary" type="submit" name="decision" value="rerun">Run it again</button></div></form>`
1114
- : ""}`);
1124
+ : ""}
1125
+ ${details}`);
1115
1126
  }
1116
1127
  /**
1117
1128
  * Which provider a run launches with, who chose it, and, when the requester's own choice is
@@ -1,4 +1,10 @@
1
1
  export declare const DECISIONS_CARD_URI = "ui://miadi/decisions-v2.html";
2
+ /**
3
+ * Addresses the card had before. Every one stays readable, because a host keeps the tool metadata it read
4
+ * until the connection is refreshed by hand, and it renders nothing for a template it cannot read: removing
5
+ * v1 in 0.1.22 left ChatGPT drawing an empty card (jgwill/Miadi#939).
6
+ */
7
+ export declare const DECISIONS_CARD_EARLIER_URIS: string[];
2
8
  /** The default lifetime of a card's decision token, in seconds; MIADI_CONNECTOR_DECISION_TOKEN_TTL changes it. */
3
9
  export declare const DECISION_TOKEN_TTL: number;
4
10
  /** Where the tokens travel in a result's `_meta`, keyed by record id. */
package/dist/decisions.js CHANGED
@@ -25,6 +25,12 @@ import { join } from "node:path";
25
25
  import { newSecret } from "./store.js";
26
26
  import { DEFAULT_DECISION_TOKEN_TTL } from "./config.js";
27
27
  export const DECISIONS_CARD_URI = "ui://miadi/decisions-v2.html";
28
+ /**
29
+ * Addresses the card had before. Every one stays readable, because a host keeps the tool metadata it read
30
+ * until the connection is refreshed by hand, and it renders nothing for a template it cannot read: removing
31
+ * v1 in 0.1.22 left ChatGPT drawing an empty card (jgwill/Miadi#939).
32
+ */
33
+ export const DECISIONS_CARD_EARLIER_URIS = ["ui://miadi/decisions-v1.html"];
28
34
  /** The default lifetime of a card's decision token, in seconds; MIADI_CONNECTOR_DECISION_TOKEN_TTL changes it. */
29
35
  export const DECISION_TOKEN_TTL = DEFAULT_DECISION_TOKEN_TTL;
30
36
  /** Where the tokens travel in a result's `_meta`, keyed by record id. */
package/dist/server.js CHANGED
@@ -5,7 +5,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
5
5
  import { mcpAuthRouter, createOAuthMetadata, getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
6
6
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
7
7
  import { MiadiOAuthProvider, SCOPES, SCOPE_CONTRIBUTE, SCOPE_READ, emptyOAuthState } from "./oauth.js";
8
- import { reconcileInterrupted, registerApprovalRoutes, registerProviderPolicyRoutes, startTranscriptSettling } from "./approve.js";
8
+ import { logDecision, reconcileInterrupted, registerApprovalRoutes, registerProviderPolicyRoutes, startTranscriptSettling } from "./approve.js";
9
9
  import { DecisionTokens } from "./decisions.js";
10
10
  import { JsonFile, Ledger, ensureStateDir, statePaths } from "./store.js";
11
11
  import { registerTools } from "./tools.js";
@@ -107,6 +107,12 @@ export function createConnectorApp(config, deps) {
107
107
  bearer(req, res, next);
108
108
  };
109
109
  const handle = async (req, res) => {
110
+ // What a host asks for to draw the card, for the journal: a blank card is otherwise invisible from here (jgwill/Miadi#939).
111
+ const rpc = (req.body ?? {});
112
+ if (rpc.method === "resources/read")
113
+ logDecision(approvalDeps, { phase: "card_template_read", uri: String(rpc.params?.uri ?? "").slice(0, 120) });
114
+ if (rpc.method === "tools/call" && rpc.params?.name === "show_decisions")
115
+ logDecision(approvalDeps, { phase: "show_decisions" });
110
116
  const ctx = {
111
117
  config,
112
118
  miadi,
package/dist/tools.js CHANGED
@@ -18,7 +18,7 @@ import { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, current
18
18
  import { VERSION } from "./version.js";
19
19
  import { AlreadyDecided, DECISIONS, DecisionConflict, KIND_LABELS, ProviderChoiceRequired, STILL_RUNNING, StaleVersion, approvalStateOf, consequenceOf, decide, decisionWaitOf, isApprovable, isBeingDecided, logDecision, readQueueState, requestProjection, settleWithin, } from "./approve.js";
20
20
  import { RevisionRefused, requestHistory, revisionRecord, supersedeRecords, versionOf } from "./revisions.js";
21
- import { DECISIONS_CARD_HTML, DECISIONS_CARD_URI, DECISION_TOKENS_META } from "./decisions.js";
21
+ import { DECISIONS_CARD_EARLIER_URIS, DECISIONS_CARD_HTML, DECISIONS_CARD_URI, DECISION_TOKENS_META } from "./decisions.js";
22
22
  import { discoverProviders, jevConfigOf, resolveProvider, routerPreflight, PROVIDERS } from "./providers.js";
23
23
  import { authorizesActions, missionStatus, primaryIssueOf, resolveMissionSubject } from "./mission-status.js";
24
24
  import { authorizationReceipt, authorizeProvider, CHOICE_PROVIDERS, isPersonId, needsPersonChoice, providerPolicyOf } from "./provider-policy.js";
@@ -2256,16 +2256,18 @@ export function registerTools(server, ctx) {
2256
2256
  shown[k] = `${shown[k].length} chars`;
2257
2257
  return JSON.stringify({ ...shown, refs: r.refs.length ? r.refs : undefined }, null, 2);
2258
2258
  };
2259
- server.registerResource("miadi-decisions-card", DECISIONS_CARD_URI, { title: "Miadi decisions", description: "Requests awaiting a person's decision, decided by the person's own click.", mimeType: "text/html;profile=mcp-app" }, async () => ({
2260
- contents: [
2261
- {
2262
- uri: DECISIONS_CARD_URI,
2263
- mimeType: "text/html;profile=mcp-app",
2264
- text: DECISIONS_CARD_HTML,
2265
- _meta: { ui: { prefersBorder: true, csp: { connectDomains: [], resourceDomains: [] } }, "openai/widgetDescription": "Cards for the requests awaiting the person's decision." },
2266
- },
2267
- ],
2268
- }));
2259
+ for (const uri of [DECISIONS_CARD_URI, ...DECISIONS_CARD_EARLIER_URIS]) {
2260
+ server.registerResource(uri === DECISIONS_CARD_URI ? "miadi-decisions-card" : `miadi-decisions-card-${uri.replace(/\W+/g, "-")}`, uri, { title: "Miadi decisions", description: "Requests awaiting a person's decision, decided by the person's own click.", mimeType: "text/html;profile=mcp-app" }, async () => ({
2261
+ contents: [
2262
+ {
2263
+ uri,
2264
+ mimeType: "text/html;profile=mcp-app",
2265
+ text: DECISIONS_CARD_HTML,
2266
+ _meta: { ui: { prefersBorder: true, csp: { connectDomains: [], resourceDomains: [] } }, "openai/widgetDescription": "Cards for the requests awaiting the person's decision." },
2267
+ },
2268
+ ],
2269
+ }));
2270
+ }
2269
2271
  server.registerTool("show_decisions", {
2270
2272
  title: "Show the requests awaiting a decision",
2271
2273
  description: "Show the requests that await a person's decision (runs, work proposals, review operations, Deep Diver, episode links, session endings) as cards in the conversation. The person approves, approves with changes, sends back or declines each one with their own click; you cannot decide for them. Call it after making a request, or when the person asks what waits for them. Optionally pass record_id to show one request.",
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.24";
1
+ export declare const VERSION = "0.1.26";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.24";
1
+ export const VERSION = "0.1.26";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miadi/connector",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
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",