@nyuchi/mzizi-mcp 0.9.1 → 0.10.1
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/LICENSE +202 -0
- package/README.md +117 -86
- package/dist/access.d.ts +69 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +72 -0
- package/dist/access.js.map +1 -0
- package/dist/auth/authkit-handler.d.ts +4 -3
- package/dist/auth/authkit-handler.d.ts.map +1 -1
- package/dist/auth/authkit-handler.js +52 -6
- package/dist/auth/authkit-handler.js.map +1 -1
- package/dist/auth/front.d.ts +17 -0
- package/dist/auth/front.d.ts.map +1 -0
- package/dist/auth/front.js +25 -0
- package/dist/auth/front.js.map +1 -0
- package/dist/auth/mcp-api-handler.d.ts +20 -7
- package/dist/auth/mcp-api-handler.d.ts.map +1 -1
- package/dist/auth/mcp-api-handler.js +43 -29
- package/dist/auth/mcp-api-handler.js.map +1 -1
- package/dist/desk-context.d.ts +33 -0
- package/dist/desk-context.d.ts.map +1 -0
- package/dist/desk-context.js +49 -0
- package/dist/desk-context.js.map +1 -0
- package/dist/docs-federation.d.ts +68 -0
- package/dist/docs-federation.d.ts.map +1 -0
- package/dist/docs-federation.js +282 -0
- package/dist/docs-federation.js.map +1 -0
- package/dist/env.d.ts +7 -8
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +5 -5
- package/dist/fundi-a2a.d.ts +1 -1
- package/dist/fundi-a2a.js +2 -2
- package/dist/fundi-a2a.js.map +1 -1
- package/dist/fundi-client.d.ts +24 -4
- package/dist/fundi-client.d.ts.map +1 -1
- package/dist/fundi-client.js +91 -4
- package/dist/fundi-client.js.map +1 -1
- package/dist/generated/registry-pin.d.ts +5 -0
- package/dist/generated/registry-pin.d.ts.map +1 -0
- package/dist/generated/registry-pin.js +3 -0
- package/dist/generated/registry-pin.js.map +1 -0
- package/dist/generated/registry.d.ts +23 -0
- package/dist/generated/registry.d.ts.map +1 -0
- package/dist/generated/registry.js +1204 -0
- package/dist/generated/registry.js.map +1 -0
- package/dist/generated/skills.d.ts +18 -0
- package/dist/generated/skills.d.ts.map +1 -0
- package/dist/generated/skills.js +67 -0
- package/dist/generated/skills.js.map +1 -0
- package/dist/http.d.ts +18 -9
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +16 -13
- package/dist/http.js.map +1 -1
- package/dist/issue-desk.d.ts +152 -0
- package/dist/issue-desk.d.ts.map +1 -0
- package/dist/issue-desk.js +392 -0
- package/dist/issue-desk.js.map +1 -0
- package/dist/issue-log.d.ts +80 -0
- package/dist/issue-log.d.ts.map +1 -0
- package/dist/issue-log.js +146 -0
- package/dist/issue-log.js.map +1 -0
- package/dist/issue-report.d.ts +138 -0
- package/dist/issue-report.d.ts.map +1 -0
- package/dist/issue-report.js +112 -0
- package/dist/issue-report.js.map +1 -0
- package/dist/rate-limit.d.ts +41 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +58 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/registry.d.ts +126 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +280 -0
- package/dist/registry.js.map +1 -0
- package/dist/server.d.ts +96 -22
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +76 -32
- package/dist/server.js.map +1 -1
- package/dist/stdio.d.ts +6 -13
- package/dist/stdio.d.ts.map +1 -1
- package/dist/stdio.js +6 -29
- package/dist/stdio.js.map +1 -1
- package/dist/tools.d.ts +45 -6
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +299 -82
- package/dist/tools.js.map +1 -1
- package/dist/worker.d.ts +26 -14
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +42 -14
- package/dist/worker.js.map +1 -1
- package/package.json +25 -20
- package/dist/api-client.d.ts +0 -101
- package/dist/api-client.d.ts.map +0 -1
- package/dist/api-client.js +0 -200
- package/dist/api-client.js.map +0 -1
- package/dist/defaults.d.ts +0 -29
- package/dist/defaults.d.ts.map +0 -1
- package/dist/defaults.js +0 -29
- package/dist/defaults.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-desk.d.ts","sourceRoot":"","sources":["../src/issue-desk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAGnC,OAAO,EAKL,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,MAAM,EAIZ,MAAM,mBAAmB,CAAA;AAE1B,cAAc,mBAAmB,CAAA;AAsCjC,qBAAa,SAAU,SAAQ,aAAa,CAAC,GAAG,CAAC;gBACnC,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,GAAG;IAuD7C;;;;;;;;OAQG;IACG,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBjE,gEAAgE;IAC1D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAO7C;;;;;;;;;;;;OAYG;IACG,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAwB1E;;;;;;OAMG;IACG,IAAI,CAAC,KAAK,SAAK,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAOzC,sDAAsD;IAChD,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;YAarD,OAAO;IAkDrB;;;;;;;;;;;;;;;OAeG;YACW,SAAS;IAsDvB;;;;;;;;;;;;;;OAcG;YACW,GAAG;IAmCjB,OAAO,CAAC,IAAI;CAWb"}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `IssueDesk` — one Durable Object per signed-in user, holding that person's
|
|
3
|
+
* issue reports and driving each one from "something is wrong" to an issue
|
|
4
|
+
* logged with fundi, which files it on GitHub and tracks it from there.
|
|
5
|
+
*
|
|
6
|
+
* WHY A DURABLE OBJECT, AND WHY PER USER
|
|
7
|
+
*
|
|
8
|
+
* `mzizi_report_issue` used to be a stub that errored on every input (issue
|
|
9
|
+
* #78). Wiring it to a fire-and-forget write would have fixed the error and
|
|
10
|
+
* kept the real problem: a bug report is not a message, it is a small piece of
|
|
11
|
+
* work with stages — reproduce it, write the fix, file it — and each stage is
|
|
12
|
+
* slow, can fail on its own, and is worth keeping.
|
|
13
|
+
*
|
|
14
|
+
* A stateless Worker cannot hold that. Every call would start from nothing, so
|
|
15
|
+
* a user who reported a flaky button on Tuesday and again on Thursday would be
|
|
16
|
+
* two strangers, and the second report could not say "this is the third time".
|
|
17
|
+
*
|
|
18
|
+
* Per USER rather than per report is the deliberate part. A per-report object
|
|
19
|
+
* would be simpler and would lose exactly the thing that makes the history
|
|
20
|
+
* useful: reports cluster by person, not by incident. The people who report
|
|
21
|
+
* problems tend to report several, in the same area, in the same voice, and the
|
|
22
|
+
* pattern across them is often the actual bug. Keying on the WorkOS subject
|
|
23
|
+
* (`idFromName(userId)`) means a returning user's earlier reports are already
|
|
24
|
+
* loaded when the next one arrives — context, not a lookup.
|
|
25
|
+
*
|
|
26
|
+
* It also draws the privacy boundary in the right place. One person's reports
|
|
27
|
+
* live in one object; there is no shared table to accidentally select across,
|
|
28
|
+
* and "show me my history" is the natural query rather than a filtered one.
|
|
29
|
+
*
|
|
30
|
+
* THE THREE STAGES, AND WHAT ACTUALLY WORKS TODAY
|
|
31
|
+
*
|
|
32
|
+
* 1. reproduce — delegate a run to fundi over the existing Service Binding.
|
|
33
|
+
* 2. draft — turn the report plus whatever stage 1 learned into an issue
|
|
34
|
+
* body a maintainer can act on.
|
|
35
|
+
* 3. log — hand that draft to fundi, which records it in the platform
|
|
36
|
+
* database and files the GitHub issue.
|
|
37
|
+
*
|
|
38
|
+
* Stated honestly, because the alternative is a pipeline that looks like it
|
|
39
|
+
* works: **fundi records a submitted task and parks it.** Its executors are not
|
|
40
|
+
* built (`executionStatus: "accepted-not-executed"`, see `fundi-a2a.ts`), so
|
|
41
|
+
* stage 1 today produces a task id and fundi's own note that it did not run.
|
|
42
|
+
* The desk records exactly that and the drafted issue says so. It does not
|
|
43
|
+
* claim a reproduction it does not have — a report asserting "reproduced" when
|
|
44
|
+
* nothing ran is worse than one that says nothing, because a maintainer will
|
|
45
|
+
* believe it.
|
|
46
|
+
*
|
|
47
|
+
* THIS WORKER NO LONGER TOUCHES GITHUB, AND HOLDS NO PAT
|
|
48
|
+
*
|
|
49
|
+
* Stage 3 used to be `POST https://api.github.com/repos/…/issues` from here,
|
|
50
|
+
* authorised with `env.GITHUB_TOKEN`. That secret was never set on this Worker
|
|
51
|
+
* and was never going to be — it belongs on `mzizi-fundi`, a separate Worker
|
|
52
|
+
* with a separate secret store, which is exactly why the gap stayed invisible
|
|
53
|
+
* for so long. Every report therefore reproduced, drafted, and parked at
|
|
54
|
+
* `drafted` with a note suggesting the reporter paste the body into GitHub by
|
|
55
|
+
* hand. That is a write sink with extra steps.
|
|
56
|
+
*
|
|
57
|
+
* The division of labour now:
|
|
58
|
+
*
|
|
59
|
+
* this desk reproduce, draft, log (no GitHub, no PAT, no database)
|
|
60
|
+
* fundi record in D1, file the GitHub issue WITH the healing plan,
|
|
61
|
+
* and write that issue's lifecycle back onto the same row
|
|
62
|
+
*
|
|
63
|
+
* So the terminal stage is `logged`, not `filed`: at the instant this desk
|
|
64
|
+
* finishes, the issue is RECORDED — durably, where a maintainer and the console
|
|
65
|
+
* can both see it — and the GitHub issue follows on fundi's next heal pass. The
|
|
66
|
+
* desk learns the issue's number and URL when the report is next read, from
|
|
67
|
+
* fundi rather than from GitHub.
|
|
68
|
+
*
|
|
69
|
+
* An issue is never blocked on this Worker holding a credential again. The one
|
|
70
|
+
* remaining way stage 3 can fail is fundi being unreachable, and that failure
|
|
71
|
+
* parks the report at `drafted` with the body in hand and a `retry` that
|
|
72
|
+
* re-enters at exactly that stage.
|
|
73
|
+
*/
|
|
74
|
+
import { DurableObject } from "cloudflare:workers";
|
|
75
|
+
import { createFundiCaller, fundiConfigured } from "./fundi-client.js";
|
|
76
|
+
import { fetchIssueStatus, isTerminal, logIssueToFundi } from "./issue-log.js";
|
|
77
|
+
import { defaultSkill, renderIssueBody, reproInstruction, issueTitle, } from "./issue-report.js";
|
|
78
|
+
export * from "./issue-report.js";
|
|
79
|
+
function toReport(r) {
|
|
80
|
+
return {
|
|
81
|
+
id: r.id,
|
|
82
|
+
createdAt: r.created_at,
|
|
83
|
+
updatedAt: r.updated_at,
|
|
84
|
+
kind: r.kind,
|
|
85
|
+
component: r.component,
|
|
86
|
+
severity: r.severity,
|
|
87
|
+
message: r.message,
|
|
88
|
+
stage: r.stage,
|
|
89
|
+
repro: r.repro ? JSON.parse(r.repro) : null,
|
|
90
|
+
draft: r.draft,
|
|
91
|
+
fundiIssueId: r.fundi_issue_id,
|
|
92
|
+
issueStatus: r.issue_status,
|
|
93
|
+
issueUrl: r.issue_url,
|
|
94
|
+
error: r.error,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export class IssueDesk extends DurableObject {
|
|
98
|
+
constructor(ctx, env) {
|
|
99
|
+
super(ctx, env);
|
|
100
|
+
// `blockConcurrencyWhile` so no RPC observes a half-built schema. The DO is
|
|
101
|
+
// single-threaded per instance, which is also why the read-modify-write in
|
|
102
|
+
// `advance` below needs no locking of its own.
|
|
103
|
+
ctx.blockConcurrencyWhile(async () => {
|
|
104
|
+
ctx.storage.sql.exec(`
|
|
105
|
+
CREATE TABLE IF NOT EXISTS reports (
|
|
106
|
+
id TEXT PRIMARY KEY,
|
|
107
|
+
created_at TEXT NOT NULL,
|
|
108
|
+
updated_at TEXT NOT NULL,
|
|
109
|
+
kind TEXT NOT NULL,
|
|
110
|
+
component TEXT,
|
|
111
|
+
severity TEXT,
|
|
112
|
+
message TEXT NOT NULL,
|
|
113
|
+
stage TEXT NOT NULL,
|
|
114
|
+
repro TEXT,
|
|
115
|
+
draft TEXT,
|
|
116
|
+
fundi_issue_id INTEGER,
|
|
117
|
+
issue_status TEXT,
|
|
118
|
+
issue_url TEXT,
|
|
119
|
+
error TEXT
|
|
120
|
+
);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS reports_created_at ON reports (created_at DESC);
|
|
122
|
+
`);
|
|
123
|
+
// `CREATE TABLE IF NOT EXISTS` does NOTHING to a desk that already
|
|
124
|
+
// exists, and these desks are live: every person who has ever used
|
|
125
|
+
// `mzizi_report_issue` has a Durable Object holding their reports in the
|
|
126
|
+
// shape this class shipped with. The two columns above therefore appear
|
|
127
|
+
// in the CREATE (for a desk opened from now on) AND are added explicitly
|
|
128
|
+
// below (for every desk that already exists) — the CREATE alone would
|
|
129
|
+
// leave existing users on the old schema, with every read of the new
|
|
130
|
+
// columns throwing "no such column". That is the worst possible
|
|
131
|
+
// distribution of a failure: it lands only on the people who have used
|
|
132
|
+
// the feature before.
|
|
133
|
+
//
|
|
134
|
+
// So the columns are added explicitly, and `PRAGMA table_info` decides
|
|
135
|
+
// rather than a try/catch: a swallowed error here cannot tell "already
|
|
136
|
+
// added" apart from "storage is broken".
|
|
137
|
+
const existing = new Set(ctx.storage.sql
|
|
138
|
+
.exec(`PRAGMA table_info(reports)`)
|
|
139
|
+
.toArray()
|
|
140
|
+
.map((c) => c.name));
|
|
141
|
+
if (!existing.has("fundi_issue_id")) {
|
|
142
|
+
ctx.storage.sql.exec(`ALTER TABLE reports ADD COLUMN fundi_issue_id INTEGER`);
|
|
143
|
+
}
|
|
144
|
+
if (!existing.has("issue_status")) {
|
|
145
|
+
ctx.storage.sql.exec(`ALTER TABLE reports ADD COLUMN issue_status TEXT`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Open a report and drive it as far as it will go in one call.
|
|
151
|
+
*
|
|
152
|
+
* Deliberately synchronous end-to-end rather than queued: an MCP tool call is
|
|
153
|
+
* a conversation turn, and returning "queued, ask again later" to an agent
|
|
154
|
+
* that has no reason to ask again is how a write sink becomes a black hole.
|
|
155
|
+
* The stages are bounded — one fundi submit, one fundi log — so the call
|
|
156
|
+
* stays inside a Worker's budget.
|
|
157
|
+
*/
|
|
158
|
+
async open(input, caller) {
|
|
159
|
+
const now = new Date().toISOString();
|
|
160
|
+
const id = crypto.randomUUID();
|
|
161
|
+
this.ctx.storage.sql.exec(`INSERT INTO reports (id, created_at, updated_at, kind, component, severity, message, stage)
|
|
162
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'opened')`, id, now, now, input.kind, input.component ?? null, input.severity ?? null, input.message);
|
|
163
|
+
return this.advance(id, input, caller);
|
|
164
|
+
}
|
|
165
|
+
/** One report, or null. Scoped to this user by construction. */
|
|
166
|
+
async get(id) {
|
|
167
|
+
const rows = this.ctx.storage.sql
|
|
168
|
+
.exec(`SELECT * FROM reports WHERE id = ?`, id)
|
|
169
|
+
.toArray();
|
|
170
|
+
return rows[0] ? toReport(rows[0]) : null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* One report, with its issue status refreshed from fundi first.
|
|
174
|
+
*
|
|
175
|
+
* The desk and fundi hold two halves of one story: the desk knows what the
|
|
176
|
+
* reporter said, fundi knows what happened to the issue afterwards. Without
|
|
177
|
+
* this, a report would sit at `logged` for ever and the only honest answer to
|
|
178
|
+
* "what happened to my bug?" would be "look somewhere else" — which is the
|
|
179
|
+
* dead end the old GitHub path had, one layer along.
|
|
180
|
+
*
|
|
181
|
+
* Only on a single `get`, never on `list`: refreshing a list would be one
|
|
182
|
+
* fundi round trip per row, on a read someone does casually. And never for a
|
|
183
|
+
* report whose issue is already closed — there is nothing left to learn.
|
|
184
|
+
*/
|
|
185
|
+
async getRefreshed(id, caller) {
|
|
186
|
+
const report = await this.get(id);
|
|
187
|
+
if (!report)
|
|
188
|
+
return null;
|
|
189
|
+
if (report.fundiIssueId == null || isTerminal(report.issueStatus))
|
|
190
|
+
return report;
|
|
191
|
+
const userToken = caller.userToken;
|
|
192
|
+
if (!userToken || !fundiConfigured(this.env))
|
|
193
|
+
return report;
|
|
194
|
+
const outcome = await fetchIssueStatus(createFundiCaller(this.env), async () => userToken, report.fundiIssueId);
|
|
195
|
+
// A refresh that fails costs the refresh. The report is the durable thing
|
|
196
|
+
// and it is right here; answering an error because a status read failed
|
|
197
|
+
// would be the tail wagging the dog.
|
|
198
|
+
if (!outcome.ok)
|
|
199
|
+
return report;
|
|
200
|
+
this.save(id, {
|
|
201
|
+
issue_status: outcome.issue.status,
|
|
202
|
+
issue_url: outcome.issue.githubIssueUrl,
|
|
203
|
+
});
|
|
204
|
+
return (await this.get(id));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* This user's reports, newest first.
|
|
208
|
+
*
|
|
209
|
+
* The point of the whole design: an agent handling a new report can read what
|
|
210
|
+
* this person already said, and a maintainer can see whether a complaint is
|
|
211
|
+
* the first or the fourth.
|
|
212
|
+
*/
|
|
213
|
+
async list(limit = 20) {
|
|
214
|
+
const rows = this.ctx.storage.sql
|
|
215
|
+
.exec(`SELECT * FROM reports ORDER BY created_at DESC LIMIT ?`, Math.min(limit, 100))
|
|
216
|
+
.toArray();
|
|
217
|
+
return rows.map(toReport);
|
|
218
|
+
}
|
|
219
|
+
/** Re-enter the pipeline at the stage that failed. */
|
|
220
|
+
async retry(id, caller) {
|
|
221
|
+
const existing = await this.get(id);
|
|
222
|
+
if (!existing)
|
|
223
|
+
return null;
|
|
224
|
+
return this.advance(id, {
|
|
225
|
+
kind: existing.kind,
|
|
226
|
+
message: existing.message,
|
|
227
|
+
component: existing.component ?? undefined,
|
|
228
|
+
severity: existing.severity ?? undefined,
|
|
229
|
+
}, caller);
|
|
230
|
+
}
|
|
231
|
+
// ── the pipeline ────────────────────────────────────────────────────────
|
|
232
|
+
async advance(id, input, caller) {
|
|
233
|
+
let report = (await this.get(id));
|
|
234
|
+
// Stage 1 — reproduce. Never fatal: a report whose reproduction could not
|
|
235
|
+
// run is still a report, and refusing to file it would lose the only
|
|
236
|
+
// signal a maintainer actually has.
|
|
237
|
+
if (report.stage === "opened" || report.stage === "failed") {
|
|
238
|
+
this.save(id, { stage: "reproducing" });
|
|
239
|
+
const repro = await this.reproduce(input, caller);
|
|
240
|
+
this.save(id, { repro: JSON.stringify(repro) });
|
|
241
|
+
report = (await this.get(id));
|
|
242
|
+
}
|
|
243
|
+
// Stage 2 — draft. Pure, so it cannot fail on its own.
|
|
244
|
+
if (report.stage !== "logged") {
|
|
245
|
+
const history = await this.list(6);
|
|
246
|
+
const draft = renderIssueBody(report, history, caller.actor);
|
|
247
|
+
this.save(id, { stage: "drafted", draft, error: null });
|
|
248
|
+
report = (await this.get(id));
|
|
249
|
+
}
|
|
250
|
+
// Stage 3 — log it to fundi.
|
|
251
|
+
if (report.stage === "drafted") {
|
|
252
|
+
const logged = await this.log(report, caller);
|
|
253
|
+
if (logged.ok) {
|
|
254
|
+
this.save(id, {
|
|
255
|
+
stage: "logged",
|
|
256
|
+
fundi_issue_id: logged.issue.id,
|
|
257
|
+
issue_status: logged.issue.status,
|
|
258
|
+
issue_url: logged.issue.githubIssueUrl,
|
|
259
|
+
error: null,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// NOT `failed`. The draft is done and in hand; the report is parked at
|
|
264
|
+
// `drafted` with the reason, and `retry` picks up from here. fundi
|
|
265
|
+
// deduplicates on the report id, so a retry after a failure that
|
|
266
|
+
// actually reached D1 returns the existing row rather than logging the
|
|
267
|
+
// same complaint twice.
|
|
268
|
+
this.save(id, { error: logged.reason });
|
|
269
|
+
}
|
|
270
|
+
report = (await this.get(id));
|
|
271
|
+
}
|
|
272
|
+
return report;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Delegate the reproduction to fundi.
|
|
276
|
+
*
|
|
277
|
+
* Returns fundi's answer verbatim rather than a verdict of our own. Today
|
|
278
|
+
* that answer is "accepted, not executed", and passing it through unedited is
|
|
279
|
+
* what stops the drafted issue claiming a run that did not happen.
|
|
280
|
+
*
|
|
281
|
+
* Routed through `createFundiCaller` rather than `FUNDI_SERVICE.fetch`
|
|
282
|
+
* directly. This used to hand-roll the binding call with a lone
|
|
283
|
+
* `Content-Type` header, so it never sent `X-Fundi-User-Token` — and fundi,
|
|
284
|
+
* correctly, refused every one with `403 fundi only acts for an authenticated
|
|
285
|
+
* user` (AUTH.md §2). The caller is the only code that satisfies that
|
|
286
|
+
* contract, and it also covers the HTTP + M2M path this method previously
|
|
287
|
+
* ignored: without a binding it used to give up, even on a Worker configured
|
|
288
|
+
* to reach fundi over HTTP.
|
|
289
|
+
*/
|
|
290
|
+
async reproduce(input, caller) {
|
|
291
|
+
// No skill means no reproduction — see `defaultSkill`. `feedback` reports
|
|
292
|
+
// take this path unless the caller names one, which is why an unreproduced
|
|
293
|
+
// report is a normal outcome rather than a failure.
|
|
294
|
+
const skill = input.skill ?? defaultSkill(input.kind);
|
|
295
|
+
if (!skill) {
|
|
296
|
+
return {
|
|
297
|
+
attempted: false,
|
|
298
|
+
reason: `no reproduction skill applies to a '${input.kind}' report; pass \`skill\` to force one`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (!fundiConfigured(this.env)) {
|
|
302
|
+
return { attempted: false, skill, reason: "fundi is not configured on this Worker" };
|
|
303
|
+
}
|
|
304
|
+
// Forwarding an absent token would earn the same 403 this method exists to
|
|
305
|
+
// stop misreporting, so name the real reason instead.
|
|
306
|
+
const userToken = caller.userToken;
|
|
307
|
+
if (!userToken) {
|
|
308
|
+
return {
|
|
309
|
+
attempted: false,
|
|
310
|
+
skill,
|
|
311
|
+
reason: "no signed-in user token to forward to fundi; the report is filed without a reproduction",
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const res = await createFundiCaller(this.env).call("/a2a", async () => userToken, {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: { "Content-Type": "application/json" },
|
|
318
|
+
body: JSON.stringify({
|
|
319
|
+
jsonrpc: "2.0",
|
|
320
|
+
id: crypto.randomUUID(),
|
|
321
|
+
method: "message/send",
|
|
322
|
+
params: {
|
|
323
|
+
message: {
|
|
324
|
+
role: "user",
|
|
325
|
+
parts: [{ kind: "text", text: reproInstruction(input) }],
|
|
326
|
+
metadata: { skill },
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
const body = await res.text();
|
|
332
|
+
let parsed = body.slice(0, 4000);
|
|
333
|
+
try {
|
|
334
|
+
parsed = JSON.parse(body);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// keep the truncated text — a non-JSON body is itself the finding
|
|
338
|
+
}
|
|
339
|
+
return { attempted: true, skill, status: res.status, response: parsed };
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
return { attempted: true, skill, error: err instanceof Error ? err.message : String(err) };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Stage 3 — hand the drafted report to fundi.
|
|
347
|
+
*
|
|
348
|
+
* This is the only outbound call the desk makes besides the reproduction, and
|
|
349
|
+
* it goes over the SAME Service Binding: fundi is the Worker that holds the
|
|
350
|
+
* GitHub PAT and the platform D1 binding, and it already owns `fundi_issues`,
|
|
351
|
+
* the cron pass that files unfiled rows, and the webhook that tracks them
|
|
352
|
+
* afterwards. Sending the report there rather than writing D1 from here keeps
|
|
353
|
+
* one writer on that table and keeps this Worker free of credentials.
|
|
354
|
+
*
|
|
355
|
+
* It cannot throw. `logIssueToFundi` turns every failure — no binding, no
|
|
356
|
+
* user token, fundi 5xx, D1 refusing the insert — into a reason string, and
|
|
357
|
+
* the caller parks the report at `drafted` with it. The tool call that
|
|
358
|
+
* reported the issue survives every one of them.
|
|
359
|
+
*/
|
|
360
|
+
async log(report, caller) {
|
|
361
|
+
if (!fundiConfigured(this.env)) {
|
|
362
|
+
return {
|
|
363
|
+
ok: false,
|
|
364
|
+
reason: "fundi is not configured on this Worker, so the issue was drafted but not logged. " +
|
|
365
|
+
"The full body is in this report's `draft` field. fundi is reached over the " +
|
|
366
|
+
"FUNDI_SERVICE binding (or FUNDI_URL + the M2M credentials); it is the Worker that " +
|
|
367
|
+
"files the GitHub issue, and this one deliberately holds no GitHub token.",
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const userToken = caller.userToken;
|
|
371
|
+
if (!userToken) {
|
|
372
|
+
// fundi refuses a call that acts for nobody (403), and it is right to:
|
|
373
|
+
// the issue is recorded against the REPORTER, taken from the verified
|
|
374
|
+
// token rather than from anything this Worker could assert. Naming the
|
|
375
|
+
// cause beats forwarding nothing and reporting fundi's refusal.
|
|
376
|
+
return {
|
|
377
|
+
ok: false,
|
|
378
|
+
reason: "no signed-in user token to forward to fundi, so the issue was drafted but not logged. " +
|
|
379
|
+
"Reconnect the Mzizi MCP server and retry this report.",
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return logIssueToFundi(createFundiCaller(this.env), async () => userToken, report, issueTitle(report));
|
|
383
|
+
}
|
|
384
|
+
save(id, fields) {
|
|
385
|
+
const entries = Object.entries(fields);
|
|
386
|
+
if (entries.length === 0)
|
|
387
|
+
return;
|
|
388
|
+
const sets = entries.map(([k]) => `${k} = ?`).join(", ");
|
|
389
|
+
this.ctx.storage.sql.exec(`UPDATE reports SET ${sets}, updated_at = ? WHERE id = ?`, ...entries.map(([, v]) => v), new Date().toISOString(), id);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
//# sourceMappingURL=issue-desk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-desk.js","sourceRoot":"","sources":["../src/issue-desk.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handing a drafted report to fundi, and reading back what became of it.
|
|
3
|
+
*
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* WHY THIS IS NOT A GITHUB CALL
|
|
6
|
+
*
|
|
7
|
+
* It used to be. `issue-desk.ts` POSTed to `https://api.github.com/repos/…/issues`
|
|
8
|
+
* with `env.GITHUB_TOKEN` — a secret that was never set on this Worker, and was
|
|
9
|
+
* never going to be. The result was a pipeline that looked finished and wasn't:
|
|
10
|
+
* every report reproduced, drafted, then parked at `drafted` inside a per-user
|
|
11
|
+
* Durable Object with a polite note suggesting the reporter paste the body into
|
|
12
|
+
* GitHub themselves. Nobody did. No maintainer ever saw one.
|
|
13
|
+
*
|
|
14
|
+
* The architecture that replaces it puts each job where the credential and the
|
|
15
|
+
* data already are:
|
|
16
|
+
*
|
|
17
|
+
* mzizi-mcp reproduce, draft, and LOG the issue — no GitHub, no PAT
|
|
18
|
+
* fundi record it in D1, file the GitHub issue WITH the fix in it,
|
|
19
|
+
* and track that issue's lifecycle back into the same row
|
|
20
|
+
*
|
|
21
|
+
* So this module's whole job is one call over the existing fundi transport. The
|
|
22
|
+
* MCP holds no GitHub credential and no database binding; it holds a Service
|
|
23
|
+
* Binding to a Worker that has both.
|
|
24
|
+
*
|
|
25
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
* WHY IT LIVES HERE RATHER THAN IN THE DURABLE OBJECT
|
|
27
|
+
*
|
|
28
|
+
* `issue-desk.ts` imports `cloudflare:workers` for the `DurableObject` base
|
|
29
|
+
* class, which only resolves inside workerd — so nothing in that file can be
|
|
30
|
+
* driven by a test running under plain Node. This module is ordinary
|
|
31
|
+
* TypeScript over a `FundiCaller` interface, which means the behaviour that
|
|
32
|
+
* MUST hold can actually be asserted:
|
|
33
|
+
*
|
|
34
|
+
* a failure to log an issue NEVER propagates to the tool call.
|
|
35
|
+
*
|
|
36
|
+
* That is not a nicety. `mzizi_report_issue` is a conversation turn; if fundi
|
|
37
|
+
* is down, or its D1 binding is unresolved, or the insert throws, the reporter
|
|
38
|
+
* must still get their report back with the drafted body in hand and a reason
|
|
39
|
+
* they can read — not an MCP error. Every path below returns an outcome. None
|
|
40
|
+
* of them throws.
|
|
41
|
+
*/
|
|
42
|
+
import type { FundiCaller } from "./fundi-client.js";
|
|
43
|
+
import type { Report } from "./issue-report.js";
|
|
44
|
+
/** fundi's view of a logged issue — the row the console reads. */
|
|
45
|
+
export interface LoggedIssue {
|
|
46
|
+
id: number;
|
|
47
|
+
status: string;
|
|
48
|
+
title: string | null;
|
|
49
|
+
githubIssueNumber: number | null;
|
|
50
|
+
githubIssueUrl: string | null;
|
|
51
|
+
githubFiledAt: string | null;
|
|
52
|
+
workStartedAt: string | null;
|
|
53
|
+
closedAt: string | null;
|
|
54
|
+
createdAt: string | null;
|
|
55
|
+
updatedAt: string | null;
|
|
56
|
+
}
|
|
57
|
+
export type LogOutcome = {
|
|
58
|
+
ok: true;
|
|
59
|
+
issue: LoggedIssue;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
reason: string;
|
|
63
|
+
};
|
|
64
|
+
export declare function isTerminal(status: string | null | undefined): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Log a drafted report as a fundi issue.
|
|
67
|
+
*
|
|
68
|
+
* `report.id` travels as `reportId` and makes this IDEMPOTENT: the desk's
|
|
69
|
+
* `retry` action re-enters the pipeline at the stage that failed, so the same
|
|
70
|
+
* report legitimately arrives twice, and fundi answers with the row it already
|
|
71
|
+
* has rather than logging the complaint again.
|
|
72
|
+
*
|
|
73
|
+
* The reporter is NOT sent. fundi takes it from the verified user token on the
|
|
74
|
+
* call, because an identity the body can choose is an identity anyone can
|
|
75
|
+
* forge — and that column is what the console filters a person's own issues by.
|
|
76
|
+
*/
|
|
77
|
+
export declare function logIssueToFundi(caller: FundiCaller, resolveUserToken: () => Promise<string>, report: Report, title: string): Promise<LogOutcome>;
|
|
78
|
+
/** Where a logged issue got to. Same failure contract: never throws. */
|
|
79
|
+
export declare function fetchIssueStatus(caller: FundiCaller, resolveUserToken: () => Promise<string>, issueId: number): Promise<LogOutcome>;
|
|
80
|
+
//# sourceMappingURL=issue-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-log.d.ts","sourceRoot":"","sources":["../src/issue-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAE/C,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAUjC,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAErE;AAmED;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,WAAW,EACnB,gBAAgB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EACvC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,UAAU,CAAC,CAmBrB;AAED,wEAAwE;AACxE,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,WAAW,EACnB,gBAAgB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EACvC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,CAAC,CAErB"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handing a drafted report to fundi, and reading back what became of it.
|
|
3
|
+
*
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* WHY THIS IS NOT A GITHUB CALL
|
|
6
|
+
*
|
|
7
|
+
* It used to be. `issue-desk.ts` POSTed to `https://api.github.com/repos/…/issues`
|
|
8
|
+
* with `env.GITHUB_TOKEN` — a secret that was never set on this Worker, and was
|
|
9
|
+
* never going to be. The result was a pipeline that looked finished and wasn't:
|
|
10
|
+
* every report reproduced, drafted, then parked at `drafted` inside a per-user
|
|
11
|
+
* Durable Object with a polite note suggesting the reporter paste the body into
|
|
12
|
+
* GitHub themselves. Nobody did. No maintainer ever saw one.
|
|
13
|
+
*
|
|
14
|
+
* The architecture that replaces it puts each job where the credential and the
|
|
15
|
+
* data already are:
|
|
16
|
+
*
|
|
17
|
+
* mzizi-mcp reproduce, draft, and LOG the issue — no GitHub, no PAT
|
|
18
|
+
* fundi record it in D1, file the GitHub issue WITH the fix in it,
|
|
19
|
+
* and track that issue's lifecycle back into the same row
|
|
20
|
+
*
|
|
21
|
+
* So this module's whole job is one call over the existing fundi transport. The
|
|
22
|
+
* MCP holds no GitHub credential and no database binding; it holds a Service
|
|
23
|
+
* Binding to a Worker that has both.
|
|
24
|
+
*
|
|
25
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
* WHY IT LIVES HERE RATHER THAN IN THE DURABLE OBJECT
|
|
27
|
+
*
|
|
28
|
+
* `issue-desk.ts` imports `cloudflare:workers` for the `DurableObject` base
|
|
29
|
+
* class, which only resolves inside workerd — so nothing in that file can be
|
|
30
|
+
* driven by a test running under plain Node. This module is ordinary
|
|
31
|
+
* TypeScript over a `FundiCaller` interface, which means the behaviour that
|
|
32
|
+
* MUST hold can actually be asserted:
|
|
33
|
+
*
|
|
34
|
+
* a failure to log an issue NEVER propagates to the tool call.
|
|
35
|
+
*
|
|
36
|
+
* That is not a nicety. `mzizi_report_issue` is a conversation turn; if fundi
|
|
37
|
+
* is down, or its D1 binding is unresolved, or the insert throws, the reporter
|
|
38
|
+
* must still get their report back with the drafted body in hand and a reason
|
|
39
|
+
* they can read — not an MCP error. Every path below returns an outcome. None
|
|
40
|
+
* of them throws.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Statuses with nothing left to happen.
|
|
44
|
+
*
|
|
45
|
+
* Used to stop asking. A report whose issue is closed does not get a fundi call
|
|
46
|
+
* every time its reporter looks at it.
|
|
47
|
+
*/
|
|
48
|
+
const TERMINAL = new Set(["resolved", "closed", "wontfix"]);
|
|
49
|
+
export function isTerminal(status) {
|
|
50
|
+
return status != null && TERMINAL.has(status);
|
|
51
|
+
}
|
|
52
|
+
/** Truncated so a failure reason stays a sentence, not a page of HTML. */
|
|
53
|
+
function snippet(text) {
|
|
54
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
55
|
+
return flat.length > 300 ? `${flat.slice(0, 297)}…` : flat;
|
|
56
|
+
}
|
|
57
|
+
function readIssue(parsed) {
|
|
58
|
+
const issue = parsed?.issue;
|
|
59
|
+
if (!issue || typeof issue !== "object")
|
|
60
|
+
return null;
|
|
61
|
+
const row = issue;
|
|
62
|
+
if (typeof row["id"] !== "number")
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
id: row["id"],
|
|
66
|
+
status: typeof row["status"] === "string" ? row["status"] : "open",
|
|
67
|
+
title: row["title"] ?? null,
|
|
68
|
+
githubIssueNumber: row["githubIssueNumber"] ?? null,
|
|
69
|
+
githubIssueUrl: row["githubIssueUrl"] ?? null,
|
|
70
|
+
githubFiledAt: row["githubFiledAt"] ?? null,
|
|
71
|
+
workStartedAt: row["workStartedAt"] ?? null,
|
|
72
|
+
closedAt: row["closedAt"] ?? null,
|
|
73
|
+
createdAt: row["createdAt"] ?? null,
|
|
74
|
+
updatedAt: row["updatedAt"] ?? null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function call(caller, resolveUserToken, path, init) {
|
|
78
|
+
let res;
|
|
79
|
+
try {
|
|
80
|
+
res = await caller.call(path, resolveUserToken, init);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
// The transport itself failed: no binding, a throwing token resolver, a
|
|
84
|
+
// network error on the HTTP fallback. Named, never rethrown.
|
|
85
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
86
|
+
}
|
|
87
|
+
const text = await res.text().catch(() => "");
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
return { ok: false, reason: `fundi answered ${res.status}: ${snippet(text)}` };
|
|
90
|
+
}
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(text);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
reason: `fundi answered ${res.status} with a body that is not JSON: ${snippet(text)}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const issue = readIssue(parsed);
|
|
102
|
+
if (!issue) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
reason: `fundi accepted the issue but returned no row: ${snippet(text)}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { ok: true, issue };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Log a drafted report as a fundi issue.
|
|
112
|
+
*
|
|
113
|
+
* `report.id` travels as `reportId` and makes this IDEMPOTENT: the desk's
|
|
114
|
+
* `retry` action re-enters the pipeline at the stage that failed, so the same
|
|
115
|
+
* report legitimately arrives twice, and fundi answers with the row it already
|
|
116
|
+
* has rather than logging the complaint again.
|
|
117
|
+
*
|
|
118
|
+
* The reporter is NOT sent. fundi takes it from the verified user token on the
|
|
119
|
+
* call, because an identity the body can choose is an identity anyone can
|
|
120
|
+
* forge — and that column is what the console filters a person's own issues by.
|
|
121
|
+
*/
|
|
122
|
+
export async function logIssueToFundi(caller, resolveUserToken, report, title) {
|
|
123
|
+
return call(caller, resolveUserToken, "/issues", {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { "Content-Type": "application/json" },
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
reportId: report.id,
|
|
128
|
+
kind: report.kind,
|
|
129
|
+
title,
|
|
130
|
+
message: report.message,
|
|
131
|
+
// The drafted body is the point of the desk: the symptom in the
|
|
132
|
+
// reporter's words, what the reproduction actually returned, and their
|
|
133
|
+
// earlier reports. fundi renders it into the GitHub issue above its own
|
|
134
|
+
// healing plan.
|
|
135
|
+
body: report.draft ?? report.message,
|
|
136
|
+
component: report.component,
|
|
137
|
+
severity: report.severity,
|
|
138
|
+
repro: report.repro,
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** Where a logged issue got to. Same failure contract: never throws. */
|
|
143
|
+
export async function fetchIssueStatus(caller, resolveUserToken, issueId) {
|
|
144
|
+
return call(caller, resolveUserToken, `/issues/${issueId}`, { method: "GET" });
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=issue-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-log.js","sourceRoot":"","sources":["../src/issue-log.ts"],"names":[],"mappings":"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"}
|