@femtomc/mu-agent 26.2.72 → 26.2.74

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 (84) hide show
  1. package/README.md +24 -45
  2. package/dist/extensions/branding.d.ts +1 -1
  3. package/dist/extensions/branding.js +3 -3
  4. package/dist/extensions/index.d.ts +3 -17
  5. package/dist/extensions/index.d.ts.map +1 -1
  6. package/dist/extensions/index.js +5 -19
  7. package/dist/extensions/mu-operator.d.ts +2 -2
  8. package/dist/extensions/mu-operator.d.ts.map +1 -1
  9. package/dist/extensions/mu-operator.js +2 -6
  10. package/dist/extensions/mu-serve.d.ts +2 -2
  11. package/dist/extensions/mu-serve.d.ts.map +1 -1
  12. package/dist/extensions/mu-serve.js +2 -14
  13. package/dist/extensions/shared.d.ts +2 -21
  14. package/dist/extensions/shared.d.ts.map +1 -1
  15. package/dist/extensions/shared.js +0 -90
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/operator.d.ts +15 -0
  20. package/dist/operator.d.ts.map +1 -1
  21. package/dist/operator.js +366 -14
  22. package/dist/session_factory.d.ts +12 -0
  23. package/dist/session_factory.d.ts.map +1 -1
  24. package/dist/session_factory.js +21 -2
  25. package/package.json +2 -3
  26. package/prompts/roles/operator.md +22 -45
  27. package/prompts/roles/orchestrator.md +17 -12
  28. package/prompts/roles/reviewer.md +7 -8
  29. package/prompts/roles/worker.md +6 -11
  30. package/dist/extensions/activities.d.ts +0 -7
  31. package/dist/extensions/activities.d.ts.map +0 -1
  32. package/dist/extensions/activities.js +0 -236
  33. package/dist/extensions/cron.d.ts +0 -7
  34. package/dist/extensions/cron.d.ts.map +0 -1
  35. package/dist/extensions/cron.js +0 -247
  36. package/dist/extensions/heartbeats.d.ts +0 -7
  37. package/dist/extensions/heartbeats.d.ts.map +0 -1
  38. package/dist/extensions/heartbeats.js +0 -192
  39. package/dist/extensions/messaging-setup/actions.d.ts +0 -22
  40. package/dist/extensions/messaging-setup/actions.d.ts.map +0 -1
  41. package/dist/extensions/messaging-setup/actions.js +0 -229
  42. package/dist/extensions/messaging-setup/adapters.d.ts +0 -24
  43. package/dist/extensions/messaging-setup/adapters.d.ts.map +0 -1
  44. package/dist/extensions/messaging-setup/adapters.js +0 -170
  45. package/dist/extensions/messaging-setup/index.d.ts +0 -17
  46. package/dist/extensions/messaging-setup/index.d.ts.map +0 -1
  47. package/dist/extensions/messaging-setup/index.js +0 -261
  48. package/dist/extensions/messaging-setup/parser.d.ts +0 -33
  49. package/dist/extensions/messaging-setup/parser.d.ts.map +0 -1
  50. package/dist/extensions/messaging-setup/parser.js +0 -240
  51. package/dist/extensions/messaging-setup/runtime.d.ts +0 -16
  52. package/dist/extensions/messaging-setup/runtime.d.ts.map +0 -1
  53. package/dist/extensions/messaging-setup/runtime.js +0 -110
  54. package/dist/extensions/messaging-setup/types.d.ts +0 -157
  55. package/dist/extensions/messaging-setup/types.d.ts.map +0 -1
  56. package/dist/extensions/messaging-setup/types.js +0 -4
  57. package/dist/extensions/messaging-setup/ui.d.ts +0 -15
  58. package/dist/extensions/messaging-setup/ui.d.ts.map +0 -1
  59. package/dist/extensions/messaging-setup/ui.js +0 -173
  60. package/dist/extensions/messaging-setup.d.ts +0 -3
  61. package/dist/extensions/messaging-setup.d.ts.map +0 -1
  62. package/dist/extensions/messaging-setup.js +0 -2
  63. package/dist/extensions/mu-full-tools.d.ts +0 -10
  64. package/dist/extensions/mu-full-tools.d.ts.map +0 -1
  65. package/dist/extensions/mu-full-tools.js +0 -25
  66. package/dist/extensions/mu-query-tools.d.ts +0 -10
  67. package/dist/extensions/mu-query-tools.d.ts.map +0 -1
  68. package/dist/extensions/mu-query-tools.js +0 -11
  69. package/dist/extensions/operator-command.d.ts +0 -14
  70. package/dist/extensions/operator-command.d.ts.map +0 -1
  71. package/dist/extensions/operator-command.js +0 -231
  72. package/dist/extensions/orchestration-runs-readonly.d.ts +0 -4
  73. package/dist/extensions/orchestration-runs-readonly.d.ts.map +0 -1
  74. package/dist/extensions/orchestration-runs-readonly.js +0 -226
  75. package/dist/extensions/orchestration-runs.d.ts +0 -4
  76. package/dist/extensions/orchestration-runs.d.ts.map +0 -1
  77. package/dist/extensions/orchestration-runs.js +0 -315
  78. package/dist/extensions/server-tools-readonly.d.ts +0 -4
  79. package/dist/extensions/server-tools-readonly.d.ts.map +0 -1
  80. package/dist/extensions/server-tools-readonly.js +0 -5
  81. package/dist/extensions/server-tools.d.ts +0 -25
  82. package/dist/extensions/server-tools.d.ts.map +0 -1
  83. package/dist/extensions/server-tools.js +0 -833
  84. package/prompts/skills/messaging-setup-brief.md +0 -25
@@ -1,833 +0,0 @@
1
- /**
2
- * mu-server-tools — Serve-mode tools for querying mu server state.
3
- *
4
- * This is the core extension the operator relies on for repo introspection.
5
- */
6
- import { StringEnum } from "@mariozechner/pi-ai";
7
- import { Type } from "@sinclair/typebox";
8
- import { registerMuSubcommand } from "./mu-command-dispatcher.js";
9
- import { asArray, asNumber, asRecord, asString, clampInt, fetchMuJson, fetchMuStatus, muServerUrl, parseFieldPaths, previewText, selectFields, textResult, toJsonText, } from "./shared.js";
10
- function trimOrNull(value) {
11
- if (value == null)
12
- return null;
13
- const trimmed = value.trim();
14
- return trimmed.length > 0 ? trimmed : null;
15
- }
16
- function parseCommaList(value) {
17
- const text = trimOrNull(value);
18
- if (!text)
19
- return null;
20
- const items = text
21
- .split(",")
22
- .map((item) => item.trim())
23
- .filter((item) => item.length > 0);
24
- return items.length > 0 ? items : null;
25
- }
26
- function stringArray(value, max = 20) {
27
- return asArray(value)
28
- .map((item) => asString(item))
29
- .filter((item) => item != null)
30
- .slice(0, max);
31
- }
32
- function summarizeIssue(issue, opts = {}) {
33
- const title = asString(issue.title) ?? "";
34
- const body = asString(issue.body) ?? "";
35
- const summary = {
36
- id: asString(issue.id),
37
- title: previewText(title, 140),
38
- status: asString(issue.status),
39
- priority: asNumber(issue.priority),
40
- outcome: issue.outcome ?? null,
41
- tags: stringArray(issue.tags, 12),
42
- deps: asArray(issue.deps).length,
43
- updated_at: asNumber(issue.updated_at),
44
- };
45
- if (opts.includeBodyPreview) {
46
- summary.body_chars = body.length;
47
- summary.body_preview = previewText(body, 220);
48
- }
49
- return summary;
50
- }
51
- function summarizeForumMessage(message) {
52
- const body = asString(message.body) ?? "";
53
- return {
54
- author: asString(message.author) ?? "unknown",
55
- created_at: asNumber(message.created_at),
56
- body_chars: body.length,
57
- body_preview: previewText(body, 240),
58
- };
59
- }
60
- function summarizeEvent(event) {
61
- const payload = asRecord(event.payload);
62
- const payloadKeys = payload ? Object.keys(payload).slice(0, 8) : [];
63
- return {
64
- ts_ms: asNumber(event.ts_ms),
65
- type: asString(event.type),
66
- source: asString(event.source),
67
- issue_id: asString(event.issue_id),
68
- run_id: asString(event.run_id),
69
- payload_keys: payloadKeys,
70
- payload_preview: previewText(event.payload, 140),
71
- };
72
- }
73
- function deriveBindingRoleFromScopes(scopes) {
74
- const scopeSet = new Set(scopes);
75
- if (scopeSet.has("cp.ops.admin") || scopeSet.has("cp.identity.admin")) {
76
- return "operator";
77
- }
78
- if (scopeSet.has("cp.issue.write") || scopeSet.has("cp.forum.write") || scopeSet.has("cp.run.execute")) {
79
- return "contributor";
80
- }
81
- if (scopeSet.has("cp.read")) {
82
- return "viewer";
83
- }
84
- return null;
85
- }
86
- function summarizeBinding(binding) {
87
- const scopes = stringArray(binding.scopes, 20);
88
- const derivedRole = deriveBindingRoleFromScopes(scopes);
89
- const status = asString(binding.status);
90
- const active = binding.active ?? (status ? status === "active" : null);
91
- return {
92
- binding_id: asString(binding.binding_id),
93
- channel: asString(binding.channel),
94
- actor_id: asString(binding.actor_id) ?? asString(binding.channel_actor_id),
95
- tenant_id: asString(binding.tenant_id) ?? asString(binding.channel_tenant_id),
96
- role: asString(binding.role) ?? derivedRole,
97
- status,
98
- active,
99
- scopes,
100
- created_at_ms: asNumber(binding.created_at_ms) ?? asNumber(binding.linked_at_ms),
101
- updated_at_ms: asNumber(binding.updated_at_ms),
102
- };
103
- }
104
- function includeByContains(contains, ...fragments) {
105
- if (!contains) {
106
- return true;
107
- }
108
- const haystack = fragments.map((fragment) => previewText(fragment, 4_000).toLowerCase()).join("\n");
109
- return haystack.includes(contains.toLowerCase());
110
- }
111
- function cpRoutesFromStatus(routes, adapters) {
112
- if (routes && routes.length > 0) {
113
- return routes;
114
- }
115
- return adapters.map((name) => ({
116
- name,
117
- route: `/webhooks/${name}`,
118
- }));
119
- }
120
- function generationSummary(generation) {
121
- const active = generation.active_generation?.generation_id ?? "(none)";
122
- const pending = generation.pending_reload
123
- ? `${generation.pending_reload.attempt_id}:${generation.pending_reload.state}`
124
- : "(none)";
125
- const last = generation.last_reload
126
- ? `${generation.last_reload.attempt_id}:${generation.last_reload.state}`
127
- : "(none)";
128
- return `generation: active=${active} pending=${pending} last=${last}`;
129
- }
130
- function observabilitySummary(counters) {
131
- return `observability: reload_success=${counters.reload_success_total} reload_failure=${counters.reload_failure_total} duplicate=${counters.duplicate_signal_total} drop=${counters.drop_signal_total}`;
132
- }
133
- function summarizeStatus(status) {
134
- const cp = status.control_plane;
135
- const routes = cpRoutesFromStatus(cp.routes, cp.adapters);
136
- const routeText = routes.length > 0 ? routes.map((entry) => `${entry.name}:${entry.route}`).join(", ") : "(none)";
137
- const lines = [
138
- `repo: ${status.repo_root}`,
139
- `issues: open=${status.open_count} ready=${status.ready_count}`,
140
- `control_plane: ${cp.active ? "active" : "inactive"}`,
141
- `adapters: ${cp.adapters.length > 0 ? cp.adapters.join(", ") : "(none)"}`,
142
- `routes: ${routeText}`,
143
- generationSummary(cp.generation),
144
- observabilitySummary(cp.observability.counters),
145
- ];
146
- return lines.join("\n");
147
- }
148
- function sliceWithLimit(items, limitRaw, fallback = 50) {
149
- const limit = clampInt(limitRaw, fallback, 1, 200);
150
- const total = items.length;
151
- const sliced = items.slice(0, limit);
152
- return {
153
- items: sliced,
154
- limit,
155
- total,
156
- returned: sliced.length,
157
- truncated: sliced.length < total,
158
- };
159
- }
160
- function registerServerTools(pi, opts) {
161
- pi.on("before_agent_start", async (event) => {
162
- const url = muServerUrl();
163
- if (!url)
164
- return {};
165
- const extra = [
166
- "",
167
- `[MU SERVER] Connected at ${url}.`,
168
- opts.toolIntroLine,
169
- opts.usageLine,
170
- ...opts.extraSystemPromptLines,
171
- ].join("\n");
172
- return {
173
- systemPrompt: `${event.systemPrompt}${extra}`,
174
- };
175
- });
176
- pi.on("session_start", async (_event, ctx) => {
177
- const url = muServerUrl();
178
- if (!ctx.hasUI || !url)
179
- return;
180
- ctx.ui.setStatus("mu-server", ctx.ui.theme.fg("dim", `μ server ${url}`));
181
- try {
182
- const status = await fetchMuStatus(4_000);
183
- ctx.ui.setStatus("mu-status", ctx.ui.theme.fg("dim", `open ${status.open_count} · ready ${status.ready_count} · cp ${status.control_plane.active ? "on" : "off"}`));
184
- }
185
- catch {
186
- ctx.ui.setStatus("mu-status", ctx.ui.theme.fg("warning", "μ status unavailable"));
187
- }
188
- });
189
- if (opts.includeStatusTool) {
190
- pi.registerTool({
191
- name: "mu_status",
192
- label: "mu Status",
193
- description: "Get high-level mu server status (repo root, issue counts, control-plane activity).",
194
- parameters: Type.Object({}),
195
- async execute() {
196
- const status = await fetchMuStatus();
197
- return textResult(summarizeStatus(status), {
198
- status,
199
- });
200
- },
201
- });
202
- }
203
- const ControlPlaneParams = Type.Object({
204
- action: StringEnum(["status", "adapters", "routes"]),
205
- });
206
- if (opts.includeControlPlaneTool) {
207
- pi.registerTool({
208
- name: "mu_control_plane",
209
- label: "Control Plane",
210
- description: "Inspect control-plane runtime state: active flag, mounted adapters, and webhook routes.",
211
- parameters: ControlPlaneParams,
212
- async execute(_toolCallId, params) {
213
- const status = await fetchMuStatus();
214
- const cp = status.control_plane;
215
- const routes = cpRoutesFromStatus(cp.routes, cp.adapters);
216
- const generation = cp.generation;
217
- const observability = cp.observability.counters;
218
- switch (params.action) {
219
- case "status":
220
- return textResult(toJsonText({
221
- active: cp.active,
222
- adapters: cp.adapters,
223
- routes,
224
- generation,
225
- observability,
226
- }), { control_plane: cp, routes, generation, observability });
227
- case "adapters":
228
- return textResult(toJsonText(cp.adapters), { adapters: cp.adapters });
229
- case "routes":
230
- return textResult(toJsonText(routes), { routes });
231
- default:
232
- return textResult(`Unknown action: ${params.action}`);
233
- }
234
- },
235
- });
236
- }
237
- const issueActions = opts.allowIssueMutations
238
- ? ["list", "get", "ready", "create", "update", "claim", "close"]
239
- : ["list", "get", "ready"];
240
- const IssuesParams = Type.Object({
241
- action: StringEnum(issueActions),
242
- id: Type.Optional(Type.String({ description: "Issue ID (for get/update/claim/close)" })),
243
- title: Type.Optional(Type.String({ description: "Issue title (for create/update)" })),
244
- body: Type.Optional(Type.String({ description: "Issue body (for create/update)" })),
245
- status: Type.Optional(Type.String({ description: "Filter by status (list) or status to set (update)" })),
246
- tag: Type.Optional(Type.String({ description: "Filter by tag (for list)" })),
247
- tags: Type.Optional(Type.String({ description: "Comma-separated tags (for create/update)" })),
248
- priority: Type.Optional(Type.Number({ description: "Priority (for create/update)" })),
249
- outcome: Type.Optional(Type.String({ description: "Outcome (for close/update)" })),
250
- root: Type.Optional(Type.String({ description: "Root issue ID (for ready)" })),
251
- contains: Type.Optional(Type.String({ description: "Case-insensitive search text over issue title/body" })),
252
- fields: Type.Optional(Type.String({ description: "Comma-separated fields for get (e.g. title,status,tags,body)" })),
253
- limit: Type.Optional(Type.Number({ description: "Max returned items (default 20, max 200)" })),
254
- });
255
- if (opts.includeIssuesTool) {
256
- pi.registerTool({
257
- name: "mu_issues",
258
- label: "Issues",
259
- description: opts.allowIssueMutations
260
- ? "Read and update mu issues. Actions: list, get, ready, create, update, claim, close. Returns concise summaries by default; use fields for precise retrieval."
261
- : "Query mu issues. Actions: list, get, ready. Returns concise summaries by default. Use id + fields for precise retrieval.",
262
- parameters: IssuesParams,
263
- async execute(_toolCallId, params) {
264
- switch (params.action) {
265
- case "list": {
266
- const query = new URLSearchParams();
267
- const status = trimOrNull(params.status) ?? "open";
268
- const tag = trimOrNull(params.tag);
269
- const contains = trimOrNull(params.contains);
270
- const limit = clampInt(params.limit, 20, 1, 200);
271
- query.set("status", status);
272
- query.set("limit", String(limit));
273
- if (tag)
274
- query.set("tag", tag);
275
- if (contains)
276
- query.set("contains", contains);
277
- const issues = await fetchMuJson(`/api/issues?${query.toString()}`);
278
- const records = issues
279
- .map((issue) => asRecord(issue))
280
- .filter((issue) => issue != null);
281
- const sliced = sliceWithLimit(records, params.limit, 20);
282
- const summaries = sliced.items.map((issue) => summarizeIssue(issue));
283
- return textResult(toJsonText({
284
- total: sliced.total,
285
- returned: sliced.returned,
286
- truncated: sliced.truncated,
287
- issues: summaries,
288
- next: sliced.truncated
289
- ? "Refine filters or increase limit. Use mu_issues(action='get', id='...') for precise inspection."
290
- : null,
291
- }), { query: { status, tag, contains, limit }, ...sliced, issues: sliced.items });
292
- }
293
- case "get": {
294
- const id = trimOrNull(params.id);
295
- if (!id)
296
- return textResult("Error: id required for get");
297
- const issue = await fetchMuJson(`/api/issues/${encodeURIComponent(id)}`);
298
- const fields = parseFieldPaths(trimOrNull(params.fields) ?? undefined);
299
- const content = fields.length > 0
300
- ? { id, selected: selectFields(issue, fields) }
301
- : { issue: summarizeIssue(issue, { includeBodyPreview: true }) };
302
- return textResult(toJsonText(content), { id, fields, issue });
303
- }
304
- case "ready": {
305
- const root = trimOrNull(params.root);
306
- const contains = trimOrNull(params.contains);
307
- const limit = clampInt(params.limit, 20, 1, 200);
308
- const query = new URLSearchParams({ limit: String(limit) });
309
- if (root)
310
- query.set("root", root);
311
- if (contains)
312
- query.set("contains", contains);
313
- const issues = await fetchMuJson(`/api/issues/ready?${query.toString()}`);
314
- const records = issues
315
- .map((issue) => asRecord(issue))
316
- .filter((issue) => issue != null);
317
- const sliced = sliceWithLimit(records, params.limit, 20);
318
- const summaries = sliced.items.map((issue) => summarizeIssue(issue));
319
- return textResult(toJsonText({
320
- total: sliced.total,
321
- returned: sliced.returned,
322
- truncated: sliced.truncated,
323
- issues: summaries,
324
- next: sliced.truncated ? "Narrow by root/contains or increase limit." : null,
325
- }), { query: { root, contains, limit }, ...sliced, issues: sliced.items });
326
- }
327
- case "create": {
328
- if (!opts.allowIssueMutations) {
329
- return textResult("issue mutations are disabled in query-only mode.", {
330
- blocked: true,
331
- reason: "issue_query_only_mode",
332
- });
333
- }
334
- const title = trimOrNull(params.title);
335
- if (!title)
336
- return textResult("Error: title required for create");
337
- const bodyText = trimOrNull(params.body);
338
- const tags = parseCommaList(params.tags);
339
- const priority = typeof params.priority === "number" && Number.isFinite(params.priority)
340
- ? Math.trunc(params.priority)
341
- : undefined;
342
- const issue = await fetchMuJson("/api/issues", {
343
- method: "POST",
344
- body: {
345
- title,
346
- body: bodyText ?? undefined,
347
- tags: tags ?? undefined,
348
- priority,
349
- },
350
- });
351
- return textResult(toJsonText({ issue: summarizeIssue(issue, { includeBodyPreview: true }) }), {
352
- action: "create",
353
- issue,
354
- });
355
- }
356
- case "update": {
357
- if (!opts.allowIssueMutations) {
358
- return textResult("issue mutations are disabled in query-only mode.", {
359
- blocked: true,
360
- reason: "issue_query_only_mode",
361
- });
362
- }
363
- const id = trimOrNull(params.id);
364
- if (!id)
365
- return textResult("Error: id required for update");
366
- const patch = {};
367
- const title = trimOrNull(params.title);
368
- if (title != null)
369
- patch.title = title;
370
- const bodyText = trimOrNull(params.body);
371
- if (bodyText != null)
372
- patch.body = bodyText;
373
- const status = trimOrNull(params.status);
374
- if (status != null)
375
- patch.status = status;
376
- const outcome = trimOrNull(params.outcome);
377
- if (outcome != null)
378
- patch.outcome = outcome;
379
- const tags = parseCommaList(params.tags);
380
- if (tags != null)
381
- patch.tags = tags;
382
- if (typeof params.priority === "number" && Number.isFinite(params.priority)) {
383
- patch.priority = Math.trunc(params.priority);
384
- }
385
- if (Object.keys(patch).length === 0) {
386
- return textResult("Error: update requires at least one field (title/body/status/outcome/tags/priority)");
387
- }
388
- const issue = await fetchMuJson(`/api/issues/${encodeURIComponent(id)}`, {
389
- method: "PATCH",
390
- body: patch,
391
- });
392
- return textResult(toJsonText({ issue: summarizeIssue(issue, { includeBodyPreview: true }) }), {
393
- action: "update",
394
- id,
395
- patch,
396
- issue,
397
- });
398
- }
399
- case "claim": {
400
- if (!opts.allowIssueMutations) {
401
- return textResult("issue mutations are disabled in query-only mode.", {
402
- blocked: true,
403
- reason: "issue_query_only_mode",
404
- });
405
- }
406
- const id = trimOrNull(params.id);
407
- if (!id)
408
- return textResult("Error: id required for claim");
409
- const issue = await fetchMuJson(`/api/issues/${encodeURIComponent(id)}/claim`, {
410
- method: "POST",
411
- body: {},
412
- });
413
- return textResult(toJsonText({ issue: summarizeIssue(issue, { includeBodyPreview: true }) }), {
414
- action: "claim",
415
- id,
416
- issue,
417
- });
418
- }
419
- case "close": {
420
- if (!opts.allowIssueMutations) {
421
- return textResult("issue mutations are disabled in query-only mode.", {
422
- blocked: true,
423
- reason: "issue_query_only_mode",
424
- });
425
- }
426
- const id = trimOrNull(params.id);
427
- if (!id)
428
- return textResult("Error: id required for close");
429
- const outcome = trimOrNull(params.outcome) ?? "success";
430
- const issue = await fetchMuJson(`/api/issues/${encodeURIComponent(id)}/close`, {
431
- method: "POST",
432
- body: { outcome },
433
- });
434
- return textResult(toJsonText({ issue: summarizeIssue(issue, { includeBodyPreview: true }) }), {
435
- action: "close",
436
- id,
437
- outcome,
438
- issue,
439
- });
440
- }
441
- default:
442
- return textResult(`Unknown action: ${params.action}`);
443
- }
444
- },
445
- });
446
- }
447
- const forumActions = opts.allowForumPost ? ["read", "post", "topics"] : ["read", "topics"];
448
- const ForumParams = Type.Object({
449
- action: StringEnum(forumActions),
450
- topic: Type.Optional(Type.String({ description: "Topic name (for read/post)" })),
451
- body: Type.Optional(Type.String({ description: "Message body (for post)" })),
452
- prefix: Type.Optional(Type.String({ description: "Topic prefix filter (for topics)" })),
453
- contains: Type.Optional(Type.String({ description: "Case-insensitive search within message body for read" })),
454
- limit: Type.Optional(Type.Number({ description: "Max returned items (default 20, max 200)" })),
455
- });
456
- if (opts.includeForumTool) {
457
- pi.registerTool({
458
- name: "mu_forum",
459
- label: "Forum",
460
- description: opts.allowForumPost
461
- ? "Interact with mu forum. Actions: read, post, topics. Read/topics return concise summaries for context safety."
462
- : "Read forum context. Actions: read, topics. Query-only mode excludes post.",
463
- parameters: ForumParams,
464
- async execute(_toolCallId, params) {
465
- switch (params.action) {
466
- case "read": {
467
- const topic = trimOrNull(params.topic);
468
- if (!topic)
469
- return textResult("Error: topic required for read");
470
- const contains = trimOrNull(params.contains);
471
- const limit = clampInt(params.limit, 20, 1, 200);
472
- const query = new URLSearchParams({ topic, limit: String(Math.max(limit, 50)) });
473
- const messages = await fetchMuJson(`/api/forum/read?${query.toString()}`);
474
- const records = messages
475
- .map((message) => asRecord(message))
476
- .filter((message) => message != null)
477
- .filter((message) => includeByContains(contains, message.body, message.author));
478
- const sliced = sliceWithLimit(records, params.limit, 20);
479
- const summaries = sliced.items.map((message) => summarizeForumMessage(message));
480
- return textResult(toJsonText({
481
- topic,
482
- total: sliced.total,
483
- returned: sliced.returned,
484
- truncated: sliced.truncated,
485
- messages: summaries,
486
- next: sliced.truncated ? "Use contains/topic filters or lower noise with smaller limit." : null,
487
- }), { topic, contains, ...sliced, messages: sliced.items });
488
- }
489
- case "post": {
490
- if (!opts.allowForumPost) {
491
- return textResult("forum post is disabled in operator read-only mode; use approved /mu command flow for mutations.", { blocked: true, reason: "operator_read_only_tools" });
492
- }
493
- const topic = trimOrNull(params.topic);
494
- const body = trimOrNull(params.body);
495
- if (!topic)
496
- return textResult("Error: topic required for post");
497
- if (!body)
498
- return textResult("Error: body required for post");
499
- const message = await fetchMuJson("/api/forum/post", {
500
- method: "POST",
501
- body: {
502
- topic,
503
- body,
504
- author: "mu-agent",
505
- },
506
- });
507
- return textResult(toJsonText(message), { topic, posted: true });
508
- }
509
- case "topics": {
510
- const query = new URLSearchParams();
511
- const prefix = trimOrNull(params.prefix);
512
- if (prefix)
513
- query.set("prefix", prefix);
514
- const topics = await fetchMuJson(`/api/forum/topics${query.size > 0 ? `?${query.toString()}` : ""}`);
515
- const records = topics
516
- .map((topic) => asRecord(topic))
517
- .filter((topic) => topic != null)
518
- .map((topic) => ({
519
- topic: asString(topic.topic) ?? previewText(topic, 120),
520
- messages: asNumber(topic.messages) ?? null,
521
- last_at: asNumber(topic.last_at) ?? null,
522
- }));
523
- const fallback = topics
524
- .filter((topic) => typeof topic === "string")
525
- .map((topic) => ({ topic: topic, messages: null, last_at: null }));
526
- const merged = records.length > 0 ? records : fallback;
527
- const sliced = sliceWithLimit(merged, params.limit, 20);
528
- return textResult(toJsonText({
529
- total: sliced.total,
530
- returned: sliced.returned,
531
- truncated: sliced.truncated,
532
- topics: sliced.items,
533
- }), { prefix, ...sliced });
534
- }
535
- default:
536
- return textResult(`Unknown action: ${params.action}`);
537
- }
538
- },
539
- });
540
- }
541
- const EventsParams = Type.Object({
542
- action: StringEnum(["tail", "query"]),
543
- type: Type.Optional(Type.String({ description: "Filter by event type" })),
544
- source: Type.Optional(Type.String({ description: "Filter by event source" })),
545
- issue_id: Type.Optional(Type.String({ description: "Filter by correlated issue_id" })),
546
- run_id: Type.Optional(Type.String({ description: "Filter by correlated run_id" })),
547
- since: Type.Optional(Type.Number({ description: "Only events >= ts_ms" })),
548
- contains: Type.Optional(Type.String({ description: "Case-insensitive search over event payload preview" })),
549
- limit: Type.Optional(Type.Number({ description: "Max returned items (default 20, max 200)" })),
550
- });
551
- if (opts.includeEventsTool) {
552
- pi.registerTool({
553
- name: "mu_events",
554
- label: "Events",
555
- description: "Query mu event log. Actions: tail, query. Returns compact event previews by default.",
556
- parameters: EventsParams,
557
- async execute(_toolCallId, params) {
558
- switch (params.action) {
559
- case "tail": {
560
- const type = trimOrNull(params.type);
561
- const source = trimOrNull(params.source);
562
- const issueId = trimOrNull(params.issue_id);
563
- const runId = trimOrNull(params.run_id);
564
- const contains = trimOrNull(params.contains);
565
- const limit = clampInt(params.limit, 20, 1, 200);
566
- const tailFetch = Math.max(limit * 3, 50);
567
- const events = await fetchMuJson(`/api/events/tail?n=${tailFetch}`);
568
- const records = events
569
- .map((event) => asRecord(event))
570
- .filter((event) => event != null)
571
- .filter((event) => (type ? asString(event.type) === type : true))
572
- .filter((event) => (source ? asString(event.source) === source : true))
573
- .filter((event) => (issueId ? asString(event.issue_id) === issueId : true))
574
- .filter((event) => (runId ? asString(event.run_id) === runId : true))
575
- .filter((event) => includeByContains(contains, event.type, event.source, event.issue_id, event.run_id, event.payload));
576
- const sliced = sliceWithLimit(records, params.limit, 20);
577
- const summaries = sliced.items.map((event) => summarizeEvent(event));
578
- return textResult(toJsonText({
579
- filters: { type, source, issue_id: issueId, run_id: runId, contains },
580
- total: sliced.total,
581
- returned: sliced.returned,
582
- truncated: sliced.truncated,
583
- events: summaries,
584
- }), { action: "tail", type, source, issueId, runId, contains, ...sliced, events: sliced.items });
585
- }
586
- case "query": {
587
- const query = new URLSearchParams();
588
- const type = trimOrNull(params.type);
589
- const source = trimOrNull(params.source);
590
- const issueId = trimOrNull(params.issue_id);
591
- const runId = trimOrNull(params.run_id);
592
- const contains = trimOrNull(params.contains);
593
- const limit = clampInt(params.limit, 20, 1, 200);
594
- if (type)
595
- query.set("type", type);
596
- if (source)
597
- query.set("source", source);
598
- if (issueId)
599
- query.set("issue_id", issueId);
600
- if (runId)
601
- query.set("run_id", runId);
602
- if (params.since != null)
603
- query.set("since", String(Math.trunc(params.since)));
604
- query.set("limit", String(Math.max(limit, 50)));
605
- const events = await fetchMuJson(`/api/events?${query.toString()}`);
606
- const records = events
607
- .map((event) => asRecord(event))
608
- .filter((event) => event != null)
609
- .filter((event) => (issueId ? asString(event.issue_id) === issueId : true))
610
- .filter((event) => (runId ? asString(event.run_id) === runId : true))
611
- .filter((event) => includeByContains(contains, event.type, event.source, event.issue_id, event.run_id, event.payload));
612
- const sliced = sliceWithLimit(records, params.limit, 20);
613
- const summaries = sliced.items.map((event) => summarizeEvent(event));
614
- return textResult(toJsonText({
615
- filters: {
616
- type,
617
- source,
618
- issue_id: issueId,
619
- run_id: runId,
620
- since: params.since ?? null,
621
- contains,
622
- },
623
- total: sliced.total,
624
- returned: sliced.returned,
625
- truncated: sliced.truncated,
626
- events: summaries,
627
- }), {
628
- type,
629
- source,
630
- issueId,
631
- runId,
632
- since: params.since ?? null,
633
- contains,
634
- ...sliced,
635
- events: sliced.items,
636
- });
637
- }
638
- default:
639
- return textResult(`Unknown action: ${params.action}`);
640
- }
641
- },
642
- });
643
- }
644
- const identityActions = opts.allowIdentityMutations ? ["list", "link", "unlink"] : ["list"];
645
- const IdentityParams = Type.Object({
646
- action: StringEnum(identityActions),
647
- channel: Type.Optional(Type.String({ description: "Channel: slack, discord, telegram (for link)" })),
648
- actor_id: Type.Optional(Type.String({ description: "Channel actor ID (for link)" })),
649
- tenant_id: Type.Optional(Type.String({ description: "Channel tenant ID (for link)" })),
650
- role: Type.Optional(Type.String({ description: "Role: operator, contributor, viewer (for link, default operator)" })),
651
- binding_id: Type.Optional(Type.String({ description: "Binding ID (for link/unlink)" })),
652
- actor_binding_id: Type.Optional(Type.String({ description: "Actor binding ID (for unlink, usually same as binding_id)" })),
653
- reason: Type.Optional(Type.String({ description: "Unlink reason (for unlink)" })),
654
- include_inactive: Type.Optional(Type.Boolean({ description: "Include inactive bindings (for list)" })),
655
- });
656
- if (opts.includeIdentityTool) {
657
- pi.registerTool({
658
- name: "mu_identity",
659
- label: "Identity",
660
- description: opts.allowIdentityMutations
661
- ? "Manage identity bindings. Actions: list (enumerate bindings), link (create binding), unlink (self-unlink)."
662
- : "Read identity bindings. Action: list.",
663
- parameters: IdentityParams,
664
- async execute(_toolCallId, params) {
665
- switch (params.action) {
666
- case "list": {
667
- const query = new URLSearchParams();
668
- if (params.include_inactive)
669
- query.set("include_inactive", "true");
670
- const data = await fetchMuJson(`/api/identities${query.size > 0 ? `?${query.toString()}` : ""}`);
671
- const bindings = asArray(data.bindings)
672
- .map((binding) => asRecord(binding))
673
- .filter((binding) => binding != null)
674
- .map((binding) => summarizeBinding(binding));
675
- return textResult(toJsonText({ count: bindings.length, bindings }), {
676
- count: data.count,
677
- bindings: data.bindings,
678
- });
679
- }
680
- case "link": {
681
- if (!opts.allowIdentityMutations) {
682
- return textResult("identity mutations are disabled in query-only mode; use list/get workflows or approved operator mutation flow.", { blocked: true, reason: "identity_query_only_mode" });
683
- }
684
- const channel = trimOrNull(params.channel);
685
- const actorId = trimOrNull(params.actor_id);
686
- const tenantId = trimOrNull(params.tenant_id);
687
- if (!channel)
688
- return textResult("Error: channel required for link");
689
- if (!actorId)
690
- return textResult("Error: actor_id required for link");
691
- if (!tenantId)
692
- return textResult("Error: tenant_id required for link");
693
- const body = {
694
- channel,
695
- actor_id: actorId,
696
- tenant_id: tenantId,
697
- };
698
- const role = trimOrNull(params.role);
699
- if (role)
700
- body.role = role;
701
- const bindingId = trimOrNull(params.binding_id);
702
- if (bindingId)
703
- body.binding_id = bindingId;
704
- const result = await fetchMuJson("/api/identities/link", {
705
- method: "POST",
706
- body,
707
- });
708
- return textResult(toJsonText(result), result);
709
- }
710
- case "unlink": {
711
- if (!opts.allowIdentityMutations) {
712
- return textResult("identity mutations are disabled in query-only mode; use list/get workflows or approved operator mutation flow.", { blocked: true, reason: "identity_query_only_mode" });
713
- }
714
- const bindingId = trimOrNull(params.binding_id);
715
- const actorBindingId = trimOrNull(params.actor_binding_id);
716
- if (!bindingId)
717
- return textResult("Error: binding_id required for unlink");
718
- if (!actorBindingId)
719
- return textResult("Error: actor_binding_id required for unlink");
720
- const body = {
721
- binding_id: bindingId,
722
- actor_binding_id: actorBindingId,
723
- };
724
- const reason = trimOrNull(params.reason);
725
- if (reason)
726
- body.reason = reason;
727
- const result = await fetchMuJson("/api/identities/unlink", {
728
- method: "POST",
729
- body,
730
- });
731
- return textResult(toJsonText(result), result);
732
- }
733
- default:
734
- return textResult(`Unknown action: ${params.action}`);
735
- }
736
- },
737
- });
738
- }
739
- if (opts.includeStatusTool) {
740
- registerMuSubcommand(pi, {
741
- subcommand: "status",
742
- summary: "Show concise mu server status",
743
- usage: "/mu status",
744
- handler: async (_args, ctx) => {
745
- try {
746
- const status = await fetchMuStatus();
747
- ctx.ui.notify(summarizeStatus(status), "info");
748
- }
749
- catch (err) {
750
- ctx.ui.notify(`Failed: ${err instanceof Error ? err.message : String(err)}`, "error");
751
- }
752
- },
753
- });
754
- }
755
- if (opts.includeControlPlaneTool) {
756
- registerMuSubcommand(pi, {
757
- subcommand: "control",
758
- summary: "Show control-plane adapter/runtime status",
759
- usage: "/mu control",
760
- handler: async (_args, ctx) => {
761
- try {
762
- const status = await fetchMuStatus();
763
- const cp = status.control_plane;
764
- const routes = cpRoutesFromStatus(cp.routes, cp.adapters);
765
- const lines = [
766
- `control_plane: ${cp.active ? "active" : "inactive"}`,
767
- `adapters: ${cp.adapters.length > 0 ? cp.adapters.join(", ") : "(none)"}`,
768
- `routes: ${routes.length > 0 ? routes.map((entry) => `${entry.name}:${entry.route}`).join(", ") : "(none)"}`,
769
- generationSummary(cp.generation),
770
- observabilitySummary(cp.observability.counters),
771
- ];
772
- ctx.ui.notify(lines.join("\n"), "info");
773
- }
774
- catch (err) {
775
- ctx.ui.notify(`Failed: ${err instanceof Error ? err.message : String(err)}`, "error");
776
- }
777
- },
778
- });
779
- }
780
- }
781
- export function serverToolsExtension(pi, opts = {}) {
782
- registerServerTools(pi, {
783
- allowForumPost: opts.allowForumPost ?? true,
784
- allowIssueMutations: opts.allowIssueMutations ?? true,
785
- allowIdentityMutations: opts.allowIdentityMutations ?? true,
786
- includeStatusTool: opts.includeStatusTool ?? true,
787
- includeControlPlaneTool: opts.includeControlPlaneTool ?? true,
788
- includeIssuesTool: opts.includeIssuesTool ?? true,
789
- includeForumTool: opts.includeForumTool ?? true,
790
- includeEventsTool: opts.includeEventsTool ?? true,
791
- includeIdentityTool: opts.includeIdentityTool ?? true,
792
- toolIntroLine: opts.toolIntroLine ??
793
- "Tools: mu_status, mu_control_plane, mu_issues, mu_forum, mu_events, mu_runs, mu_activities, mu_heartbeats, mu_cron, mu_identity.",
794
- usageLine: opts.usageLine ??
795
- "Use these tools to inspect repository state and control-plane runtime before advising users.",
796
- extraSystemPromptLines: opts.extraSystemPromptLines ?? [],
797
- });
798
- }
799
- export function serverToolsReadOnlyExtension(pi) {
800
- registerServerTools(pi, {
801
- allowForumPost: false,
802
- allowIssueMutations: false,
803
- allowIdentityMutations: false,
804
- includeStatusTool: true,
805
- includeControlPlaneTool: true,
806
- includeIssuesTool: true,
807
- includeForumTool: true,
808
- includeEventsTool: true,
809
- includeIdentityTool: true,
810
- toolIntroLine: "Tools: mu_status, mu_control_plane, mu_issues, mu_forum(read/topics), mu_events, mu_runs(read), mu_messaging_setup(read), mu_identity(list).",
811
- usageLine: "Use these tools to inspect repository state and control-plane runtime before advising users.",
812
- extraSystemPromptLines: [
813
- "You have Bash, Read, Write, and Edit tools. Use them to run mu CLI commands, edit config files, and complete tasks directly.",
814
- ],
815
- });
816
- }
817
- export function serverToolsIssueForumExtension(pi) {
818
- registerServerTools(pi, {
819
- allowForumPost: true,
820
- allowIssueMutations: true,
821
- allowIdentityMutations: false,
822
- includeStatusTool: false,
823
- includeControlPlaneTool: false,
824
- includeIssuesTool: true,
825
- includeForumTool: true,
826
- includeEventsTool: false,
827
- includeIdentityTool: false,
828
- toolIntroLine: "Tools: mu_issues, mu_forum.",
829
- usageLine: "Use these tools to coordinate issue status and forum updates for your assigned work.",
830
- extraSystemPromptLines: [],
831
- });
832
- }
833
- export default serverToolsExtension;