@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.
Files changed (97) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +117 -86
  3. package/dist/access.d.ts +69 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +72 -0
  6. package/dist/access.js.map +1 -0
  7. package/dist/auth/authkit-handler.d.ts +4 -3
  8. package/dist/auth/authkit-handler.d.ts.map +1 -1
  9. package/dist/auth/authkit-handler.js +52 -6
  10. package/dist/auth/authkit-handler.js.map +1 -1
  11. package/dist/auth/front.d.ts +17 -0
  12. package/dist/auth/front.d.ts.map +1 -0
  13. package/dist/auth/front.js +25 -0
  14. package/dist/auth/front.js.map +1 -0
  15. package/dist/auth/mcp-api-handler.d.ts +20 -7
  16. package/dist/auth/mcp-api-handler.d.ts.map +1 -1
  17. package/dist/auth/mcp-api-handler.js +43 -29
  18. package/dist/auth/mcp-api-handler.js.map +1 -1
  19. package/dist/desk-context.d.ts +33 -0
  20. package/dist/desk-context.d.ts.map +1 -0
  21. package/dist/desk-context.js +49 -0
  22. package/dist/desk-context.js.map +1 -0
  23. package/dist/docs-federation.d.ts +68 -0
  24. package/dist/docs-federation.d.ts.map +1 -0
  25. package/dist/docs-federation.js +282 -0
  26. package/dist/docs-federation.js.map +1 -0
  27. package/dist/env.d.ts +7 -8
  28. package/dist/env.d.ts.map +1 -1
  29. package/dist/env.js +5 -5
  30. package/dist/fundi-a2a.d.ts +1 -1
  31. package/dist/fundi-a2a.js +2 -2
  32. package/dist/fundi-a2a.js.map +1 -1
  33. package/dist/fundi-client.d.ts +24 -4
  34. package/dist/fundi-client.d.ts.map +1 -1
  35. package/dist/fundi-client.js +91 -4
  36. package/dist/fundi-client.js.map +1 -1
  37. package/dist/generated/registry-pin.d.ts +5 -0
  38. package/dist/generated/registry-pin.d.ts.map +1 -0
  39. package/dist/generated/registry-pin.js +3 -0
  40. package/dist/generated/registry-pin.js.map +1 -0
  41. package/dist/generated/registry.d.ts +23 -0
  42. package/dist/generated/registry.d.ts.map +1 -0
  43. package/dist/generated/registry.js +1204 -0
  44. package/dist/generated/registry.js.map +1 -0
  45. package/dist/generated/skills.d.ts +18 -0
  46. package/dist/generated/skills.d.ts.map +1 -0
  47. package/dist/generated/skills.js +67 -0
  48. package/dist/generated/skills.js.map +1 -0
  49. package/dist/http.d.ts +18 -9
  50. package/dist/http.d.ts.map +1 -1
  51. package/dist/http.js +16 -13
  52. package/dist/http.js.map +1 -1
  53. package/dist/issue-desk.d.ts +152 -0
  54. package/dist/issue-desk.d.ts.map +1 -0
  55. package/dist/issue-desk.js +392 -0
  56. package/dist/issue-desk.js.map +1 -0
  57. package/dist/issue-log.d.ts +80 -0
  58. package/dist/issue-log.d.ts.map +1 -0
  59. package/dist/issue-log.js +146 -0
  60. package/dist/issue-log.js.map +1 -0
  61. package/dist/issue-report.d.ts +138 -0
  62. package/dist/issue-report.d.ts.map +1 -0
  63. package/dist/issue-report.js +112 -0
  64. package/dist/issue-report.js.map +1 -0
  65. package/dist/rate-limit.d.ts +41 -0
  66. package/dist/rate-limit.d.ts.map +1 -0
  67. package/dist/rate-limit.js +58 -0
  68. package/dist/rate-limit.js.map +1 -0
  69. package/dist/registry.d.ts +126 -0
  70. package/dist/registry.d.ts.map +1 -0
  71. package/dist/registry.js +280 -0
  72. package/dist/registry.js.map +1 -0
  73. package/dist/server.d.ts +96 -22
  74. package/dist/server.d.ts.map +1 -1
  75. package/dist/server.js +76 -32
  76. package/dist/server.js.map +1 -1
  77. package/dist/stdio.d.ts +6 -13
  78. package/dist/stdio.d.ts.map +1 -1
  79. package/dist/stdio.js +6 -29
  80. package/dist/stdio.js.map +1 -1
  81. package/dist/tools.d.ts +45 -6
  82. package/dist/tools.d.ts.map +1 -1
  83. package/dist/tools.js +299 -82
  84. package/dist/tools.js.map +1 -1
  85. package/dist/worker.d.ts +26 -14
  86. package/dist/worker.d.ts.map +1 -1
  87. package/dist/worker.js +42 -14
  88. package/dist/worker.js.map +1 -1
  89. package/package.json +25 -20
  90. package/dist/api-client.d.ts +0 -101
  91. package/dist/api-client.d.ts.map +0 -1
  92. package/dist/api-client.js +0 -200
  93. package/dist/api-client.js.map +0 -1
  94. package/dist/defaults.d.ts +0 -29
  95. package/dist/defaults.d.ts.map +0 -1
  96. package/dist/defaults.js +0 -29
  97. 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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"}