@femtomc/mu-agent 26.2.56 → 26.2.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,16 +54,16 @@ Current stack:
54
54
 
55
55
  ## Slash commands (operator-facing)
56
56
 
57
- - `/mu-status` — concise server status
58
- - `/mu-control` — active control-plane adapters and webhook routes
59
- - `/mu-setup` — adapter preflight
60
- - `/mu-setup plan <adapter>` — actionable wiring plan
61
- - `/mu-setup apply <adapter>` — guided config apply + control-plane reload
62
- - `/mu-setup verify [adapter]` — runtime verification for mounted routes
63
- - `/mu-setup <adapter>` — sends adapter setup brief to mu agent (`--no-agent` prints local guide)
64
- - `/mu-events [n]` / `/mu-events tail [n]` — event log tail
65
- - `/mu-events watch on|off` — toggle event watch widget
66
- - `/mu-brand on|off|toggle` — enable/disable UI branding
57
+ - `/mu status` — concise server status
58
+ - `/mu control` — active control-plane adapters and webhook routes
59
+ - `/mu setup` — adapter preflight
60
+ - `/mu setup plan <adapter>` — actionable wiring plan
61
+ - `/mu setup apply <adapter>` — guided config apply + control-plane reload
62
+ - `/mu setup verify [adapter]` — runtime verification for mounted routes
63
+ - `/mu setup <adapter>` — sends adapter setup brief to mu agent (`--no-agent` prints local guide)
64
+ - `/mu events [n]` / `/mu events tail [n]` — event log tail
65
+ - `/mu events watch on|off` — toggle event watch widget
66
+ - `/mu brand on|off|toggle` — enable/disable UI branding
67
67
 
68
68
  ## Tools (agent/operator-facing)
69
69
 
@@ -1 +1 @@
1
- {"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../src/extensions/branding.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AA+CpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QA4NjD;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../src/extensions/branding.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAgDpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QA8NjD;AAED,eAAe,iBAAiB,CAAC"}
@@ -8,6 +8,7 @@
8
8
  * - Lightweight periodic status refresh (open/ready/control-plane)
9
9
  */
10
10
  import { basename } from "node:path";
11
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
11
12
  import { fetchMuStatus, muServerUrl } from "./shared.js";
12
13
  const EMPTY_SNAPSHOT = {
13
14
  openCount: 0,
@@ -61,7 +62,7 @@ export function brandingExtension(pi) {
61
62
  : snapshot.controlPlaneActive
62
63
  ? theme.fg("success", `cp ${snapshot.adapters.join(",") || "on"}`)
63
64
  : theme.fg("muted", "cp off");
64
- const line1 = `${theme.fg("accent", "μ")}${theme.fg("dim", " quick actions")}: ${theme.fg("muted", "/mu-status /mu-control /mu-setup /mu-events")}`;
65
+ const line1 = `${theme.fg("accent", "μ")}${theme.fg("dim", " quick actions")}: ${theme.fg("muted", "/mu status /mu control /mu setup /mu events")}`;
65
66
  const line2 = `${theme.fg("dim", `open ${snapshot.openCount} · ready ${snapshot.readyCount}`)} · ${cpState}`;
66
67
  return [truncateToWidth(line1, width), truncateToWidth(line2, width)];
67
68
  },
@@ -220,8 +221,10 @@ export function brandingExtension(pi) {
220
221
  footerRequestRender = null;
221
222
  activeCtx = null;
222
223
  });
223
- pi.registerCommand("mu-brand", {
224
- description: "Toggle mu TUI branding (`/mu-brand on|off|toggle`)",
224
+ registerMuSubcommand(pi, {
225
+ subcommand: "brand",
226
+ summary: "Toggle mu TUI branding",
227
+ usage: "/mu brand [on|off|toggle]",
225
228
  handler: async (args, ctx) => {
226
229
  const mode = args.trim().toLowerCase();
227
230
  if (mode === "on") {
@@ -0,0 +1,4 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export declare function cronExtension(pi: ExtensionAPI): void;
3
+ export default cronExtension;
4
+ //# sourceMappingURL=cron.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../../src/extensions/cron.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAiBlE,wBAAgB,aAAa,CAAC,EAAE,EAAE,YAAY,QAoK7C;AAED,eAAe,aAAa,CAAC"}
@@ -0,0 +1,187 @@
1
+ import { StringEnum } from "@mariozechner/pi-ai";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { clampInt, fetchMuJson, textResult, toJsonText } from "./shared.js";
4
+ function trimOrNull(value) {
5
+ if (value == null)
6
+ return null;
7
+ const trimmed = value.trim();
8
+ return trimmed.length > 0 ? trimmed : null;
9
+ }
10
+ function normalizedNumber(value) {
11
+ if (value == null || !Number.isFinite(value)) {
12
+ return undefined;
13
+ }
14
+ return Math.trunc(value);
15
+ }
16
+ export function cronExtension(pi) {
17
+ const Params = Type.Object({
18
+ action: StringEnum(["status", "list", "get", "create", "update", "delete", "trigger", "enable", "disable"]),
19
+ program_id: Type.Optional(Type.String({ description: "Cron program ID" })),
20
+ title: Type.Optional(Type.String({ description: "Program title" })),
21
+ target_kind: Type.Optional(Type.String({ description: "Target kind (run|activity)" })),
22
+ run_job_id: Type.Optional(Type.String({ description: "Run job ID target" })),
23
+ run_root_issue_id: Type.Optional(Type.String({ description: "Run root issue ID target" })),
24
+ activity_id: Type.Optional(Type.String({ description: "Activity ID target" })),
25
+ schedule_kind: Type.Optional(Type.String({ description: "Schedule kind (at|every|cron)" })),
26
+ at_ms: Type.Optional(Type.Number({ description: "One-shot timestamp in epoch ms" })),
27
+ at: Type.Optional(Type.String({ description: "One-shot timestamp (ISO-8601)" })),
28
+ every_ms: Type.Optional(Type.Number({ description: "Fixed interval in ms" })),
29
+ anchor_ms: Type.Optional(Type.Number({ description: "Anchor timestamp for every schedules" })),
30
+ expr: Type.Optional(Type.String({ description: "Cron expression (5-field)" })),
31
+ tz: Type.Optional(Type.String({ description: "Optional IANA timezone for cron expressions" })),
32
+ reason: Type.Optional(Type.String({ description: "Execution reason" })),
33
+ enabled: Type.Optional(Type.Boolean({ description: "Enabled state" })),
34
+ schedule_filter: Type.Optional(Type.String({ description: "Filter list by schedule kind" })),
35
+ limit: Type.Optional(Type.Number({ description: "Max returned items for list" })),
36
+ });
37
+ pi.registerTool({
38
+ name: "mu_cron",
39
+ label: "Cron",
40
+ description: "Manage persistent cron programs. Actions: status, list, get, create, update, delete, trigger, enable, disable.",
41
+ parameters: Params,
42
+ async execute(_toolCallId, params) {
43
+ switch (params.action) {
44
+ case "status": {
45
+ const payload = await fetchMuJson("/api/cron/status");
46
+ return textResult(toJsonText(payload), { action: "status" });
47
+ }
48
+ case "list": {
49
+ const query = new URLSearchParams();
50
+ const targetKind = trimOrNull(params.target_kind);
51
+ if (targetKind) {
52
+ query.set("target_kind", targetKind);
53
+ }
54
+ if (typeof params.enabled === "boolean") {
55
+ query.set("enabled", params.enabled ? "true" : "false");
56
+ }
57
+ const scheduleFilter = trimOrNull(params.schedule_filter) ?? trimOrNull(params.schedule_kind);
58
+ if (scheduleFilter) {
59
+ query.set("schedule_kind", scheduleFilter);
60
+ }
61
+ query.set("limit", String(clampInt(params.limit, 50, 1, 500)));
62
+ const payload = await fetchMuJson(`/api/cron?${query.toString()}`);
63
+ return textResult(toJsonText(payload), {
64
+ action: "list",
65
+ targetKind,
66
+ enabled: params.enabled,
67
+ scheduleFilter,
68
+ });
69
+ }
70
+ case "get": {
71
+ const programId = trimOrNull(params.program_id);
72
+ if (!programId)
73
+ return textResult("get requires program_id");
74
+ const payload = await fetchMuJson(`/api/cron/${encodeURIComponent(programId)}`);
75
+ return textResult(toJsonText(payload), { action: "get", programId });
76
+ }
77
+ case "create": {
78
+ const title = trimOrNull(params.title);
79
+ const targetKind = trimOrNull(params.target_kind);
80
+ const scheduleKind = trimOrNull(params.schedule_kind);
81
+ if (!title)
82
+ return textResult("create requires title");
83
+ if (!targetKind)
84
+ return textResult("create requires target_kind (run|activity)");
85
+ if (!scheduleKind)
86
+ return textResult("create requires schedule_kind (at|every|cron)");
87
+ const payload = await fetchMuJson("/api/cron/create", {
88
+ method: "POST",
89
+ body: {
90
+ title,
91
+ target_kind: targetKind,
92
+ run_job_id: trimOrNull(params.run_job_id),
93
+ run_root_issue_id: trimOrNull(params.run_root_issue_id),
94
+ activity_id: trimOrNull(params.activity_id),
95
+ schedule_kind: scheduleKind,
96
+ at_ms: normalizedNumber(params.at_ms),
97
+ at: trimOrNull(params.at),
98
+ every_ms: normalizedNumber(params.every_ms),
99
+ anchor_ms: normalizedNumber(params.anchor_ms),
100
+ expr: trimOrNull(params.expr),
101
+ tz: trimOrNull(params.tz),
102
+ reason: trimOrNull(params.reason),
103
+ enabled: typeof params.enabled === "boolean" ? params.enabled : undefined,
104
+ },
105
+ });
106
+ return textResult(toJsonText(payload), {
107
+ action: "create",
108
+ title,
109
+ targetKind,
110
+ scheduleKind,
111
+ });
112
+ }
113
+ case "update": {
114
+ const programId = trimOrNull(params.program_id);
115
+ if (!programId)
116
+ return textResult("update requires program_id");
117
+ const payload = await fetchMuJson("/api/cron/update", {
118
+ method: "POST",
119
+ body: {
120
+ program_id: programId,
121
+ title: trimOrNull(params.title),
122
+ target_kind: trimOrNull(params.target_kind),
123
+ run_job_id: trimOrNull(params.run_job_id),
124
+ run_root_issue_id: trimOrNull(params.run_root_issue_id),
125
+ activity_id: trimOrNull(params.activity_id),
126
+ schedule_kind: trimOrNull(params.schedule_kind),
127
+ at_ms: normalizedNumber(params.at_ms),
128
+ at: trimOrNull(params.at),
129
+ every_ms: normalizedNumber(params.every_ms),
130
+ anchor_ms: normalizedNumber(params.anchor_ms),
131
+ expr: trimOrNull(params.expr),
132
+ tz: trimOrNull(params.tz),
133
+ reason: trimOrNull(params.reason),
134
+ enabled: typeof params.enabled === "boolean" ? params.enabled : undefined,
135
+ },
136
+ });
137
+ return textResult(toJsonText(payload), { action: "update", programId });
138
+ }
139
+ case "delete": {
140
+ const programId = trimOrNull(params.program_id);
141
+ if (!programId)
142
+ return textResult("delete requires program_id");
143
+ const payload = await fetchMuJson("/api/cron/delete", {
144
+ method: "POST",
145
+ body: {
146
+ program_id: programId,
147
+ },
148
+ });
149
+ return textResult(toJsonText(payload), { action: "delete", programId });
150
+ }
151
+ case "trigger": {
152
+ const programId = trimOrNull(params.program_id);
153
+ if (!programId)
154
+ return textResult("trigger requires program_id");
155
+ const payload = await fetchMuJson("/api/cron/trigger", {
156
+ method: "POST",
157
+ body: {
158
+ program_id: programId,
159
+ reason: trimOrNull(params.reason),
160
+ },
161
+ });
162
+ return textResult(toJsonText(payload), { action: "trigger", programId });
163
+ }
164
+ case "enable":
165
+ case "disable": {
166
+ const programId = trimOrNull(params.program_id);
167
+ if (!programId)
168
+ return textResult(`${params.action} requires program_id`);
169
+ const payload = await fetchMuJson("/api/cron/update", {
170
+ method: "POST",
171
+ body: {
172
+ program_id: programId,
173
+ enabled: params.action === "enable",
174
+ },
175
+ });
176
+ return textResult(toJsonText(payload), {
177
+ action: params.action,
178
+ programId,
179
+ });
180
+ }
181
+ default:
182
+ return textResult(`unknown action: ${params.action}`);
183
+ }
184
+ },
185
+ });
186
+ }
187
+ export default cronExtension;
@@ -2,8 +2,8 @@
2
2
  * mu-event-log — Event stream helper for mu serve.
3
3
  *
4
4
  * - Status line with last event type and tail count
5
- * - Optional watch widget below editor (`/mu-events watch on|off`)
6
- * - Command for quick tail inspection (`/mu-events [n]` or `/mu-events tail [n]`)
5
+ * - Optional watch widget below editor (`/mu events watch on|off`)
6
+ * - Command for quick tail inspection (`/mu events [n]` or `/mu events tail [n]`)
7
7
  */
8
8
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
9
  export declare function eventLogExtension(pi: ExtensionAPI): void;
@@ -1 +1 @@
1
- {"version":3,"file":"event-log.d.ts","sourceRoot":"","sources":["../../src/extensions/event-log.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAgCpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QAgHjD;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"event-log.d.ts","sourceRoot":"","sources":["../../src/extensions/event-log.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAiCpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QAkHjD;AAED,eAAe,iBAAiB,CAAC"}
@@ -2,9 +2,10 @@
2
2
  * mu-event-log — Event stream helper for mu serve.
3
3
  *
4
4
  * - Status line with last event type and tail count
5
- * - Optional watch widget below editor (`/mu-events watch on|off`)
6
- * - Command for quick tail inspection (`/mu-events [n]` or `/mu-events tail [n]`)
5
+ * - Optional watch widget below editor (`/mu events watch on|off`)
6
+ * - Command for quick tail inspection (`/mu events [n]` or `/mu events tail [n]`)
7
7
  */
8
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
8
9
  import { clampInt, fetchMuJson, muServerUrl } from "./shared.js";
9
10
  function eventTime(tsMs) {
10
11
  return new Date(tsMs).toLocaleTimeString();
@@ -96,8 +97,10 @@ export function eventLogExtension(pi) {
96
97
  stopPolling();
97
98
  activeCtx = null;
98
99
  });
99
- pi.registerCommand("mu-events", {
100
- description: "Inspect events (`/mu-events [n]`, `/mu-events tail [n]`, `/mu-events watch on|off`)",
100
+ registerMuSubcommand(pi, {
101
+ subcommand: "events",
102
+ summary: "Inspect event tails and toggle the watch widget",
103
+ usage: "/mu events [n] | /mu events tail [n] | /mu events watch on|off",
101
104
  handler: async (args, ctx) => {
102
105
  const tokens = args
103
106
  .trim()
@@ -1,5 +1,6 @@
1
1
  export { activitiesExtension } from "./activities.js";
2
2
  export { brandingExtension } from "./branding.js";
3
+ export { cronExtension } from "./cron.js";
3
4
  export { eventLogExtension } from "./event-log.js";
4
5
  export { heartbeatsExtension } from "./heartbeats.js";
5
6
  export { messagingSetupExtension } from "./messaging-setup.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,kCAAkC,EAAE,MAAM,kCAAkC,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AA0B1E;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,UAE/B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,UAElC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,kCAAkC,EAAE,MAAM,kCAAkC,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AA2B1E;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,UAE/B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,UAElC,CAAC"}
@@ -1,5 +1,6 @@
1
1
  export { activitiesExtension } from "./activities.js";
2
2
  export { brandingExtension } from "./branding.js";
3
+ export { cronExtension } from "./cron.js";
3
4
  export { eventLogExtension } from "./event-log.js";
4
5
  export { heartbeatsExtension } from "./heartbeats.js";
5
6
  export { messagingSetupExtension } from "./messaging-setup.js";
@@ -16,6 +17,7 @@ const SERVE_EXTENSION_MODULE_BASENAMES = [
16
17
  "orchestration-runs",
17
18
  "activities",
18
19
  "heartbeats",
20
+ "cron",
19
21
  ];
20
22
  const OPERATOR_EXTENSION_MODULE_BASENAMES = [
21
23
  "branding",
@@ -2,7 +2,7 @@
2
2
  * mu-messaging-setup — Adapter configuration diagnostics + guided setup.
3
3
  *
4
4
  * Goals:
5
- * - Make `/mu-setup <adapter>` hand setup context to the active mu agent.
5
+ * - Make `/mu setup <adapter>` hand setup context to the active mu agent.
6
6
  * - Keep configuration in `.mu/config.json` (no process.env mutations).
7
7
  * - Support plan/apply/verify workflow with in-process control-plane reload.
8
8
  */
@@ -1 +1 @@
1
- {"version":3,"file":"messaging-setup.d.ts","sourceRoot":"","sources":["../../src/extensions/messaging-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAA6C,MAAM,+BAA+B,CAAC;AAwpC7G,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,YAAY,QAwPvD;AAED,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"messaging-setup.d.ts","sourceRoot":"","sources":["../../src/extensions/messaging-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAA6C,MAAM,+BAA+B,CAAC;AAypC7G,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,YAAY,QAyPvD;AAED,eAAe,uBAAuB,CAAC"}
@@ -2,13 +2,14 @@
2
2
  * mu-messaging-setup — Adapter configuration diagnostics + guided setup.
3
3
  *
4
4
  * Goals:
5
- * - Make `/mu-setup <adapter>` hand setup context to the active mu agent.
5
+ * - Make `/mu setup <adapter>` hand setup context to the active mu agent.
6
6
  * - Keep configuration in `.mu/config.json` (no process.env mutations).
7
7
  * - Support plan/apply/verify workflow with in-process control-plane reload.
8
8
  */
9
9
  import { StringEnum } from "@mariozechner/pi-ai";
10
10
  import { Type } from "@sinclair/typebox";
11
11
  import { loadBundledPrompt } from "../default_prompts.js";
12
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
12
13
  import { fetchMuJson, fetchMuStatus, muServerUrl, textResult, toJsonText } from "./shared.js";
13
14
  const MESSAGING_SETUP_BRIEF_TEMPLATE = loadBundledPrompt("skills/messaging-setup-brief.md");
14
15
  function interpolateTemplate(template, vars) {
@@ -31,7 +32,7 @@ const ADAPTERS = [
31
32
  "Copy Signing Secret into .mu/config.json → control_plane.adapters.slack.signing_secret.",
32
33
  "Create a Slash Command (e.g. /mu) with Request URL <public-base-url>/webhooks/slack.",
33
34
  "Install/reinstall app after command changes.",
34
- "Run /mu in Slack, then /mu-setup verify slack.",
35
+ "Run /mu in Slack, then /mu setup verify slack.",
35
36
  ],
36
37
  },
37
38
  {
@@ -49,7 +50,7 @@ const ADAPTERS = [
49
50
  "Create/open app in Discord Developer Portal.",
50
51
  "Copy Interaction Public Key into .mu/config.json → control_plane.adapters.discord.signing_secret.",
51
52
  "Set Interactions Endpoint URL to <public-base-url>/webhooks/discord.",
52
- "Run a Discord command interaction, then /mu-setup verify discord.",
53
+ "Run a Discord command interaction, then /mu setup verify discord.",
53
54
  ],
54
55
  },
55
56
  {
@@ -79,7 +80,7 @@ const ADAPTERS = [
79
80
  "Call Telegram setWebhook using URL <public-base-url>/webhooks/telegram and matching secret_token.",
80
81
  "Link your Telegram identity to control-plane policy (mu control link --channel telegram --actor-id <telegram-user-id> --tenant-id telegram-bot --role <viewer|contributor|operator>).",
81
82
  "Optionally set control_plane.adapters.telegram.bot_username.",
82
- "Send /mu in Telegram chat, then /mu-setup verify telegram.",
83
+ "Send /mu in Telegram chat, then /mu setup verify telegram.",
83
84
  ],
84
85
  },
85
86
  {
@@ -251,7 +252,7 @@ function nextStepForState(opts) {
251
252
  case "active":
252
253
  return "No action needed. Adapter is mounted and receiving webhooks.";
253
254
  case "configured_not_active":
254
- return "Run `/mu-setup apply <adapter>` to trigger in-process control-plane reload.";
255
+ return "Run `/mu setup apply <adapter>` to trigger in-process control-plane reload.";
255
256
  case "missing_config":
256
257
  return `Set required config fields: ${opts.missing.join(", ")}.`;
257
258
  case "planned":
@@ -383,7 +384,7 @@ function setupGuide(checks, adapterId) {
383
384
  return [
384
385
  "# Messaging Integration Setup",
385
386
  "",
386
- "Use `/mu-setup <adapter>` to hand setup context to mu agent.",
387
+ "Use `/mu setup <adapter>` to hand setup context to mu agent.",
387
388
  "Config source of truth is `.mu/config.json`.",
388
389
  "",
389
390
  ...sections,
@@ -402,15 +403,15 @@ function buildPlan(check, publicBaseUrl) {
402
403
  else {
403
404
  if (check.missing.length > 0) {
404
405
  steps.push(`Set required config fields: ${check.missing.join(", ")}.`);
405
- steps.push(`Run /mu-setup apply ${check.id} to write config and reload control-plane.`);
406
+ steps.push(`Run /mu setup apply ${check.id} to write config and reload control-plane.`);
406
407
  }
407
408
  if (check.state === "configured_not_active") {
408
- steps.push(`Run /mu-setup apply ${check.id} to trigger control-plane reload.`);
409
+ steps.push(`Run /mu setup apply ${check.id} to trigger control-plane reload.`);
409
410
  }
410
411
  if (webhookUrl) {
411
412
  steps.push(`Configure provider webhook/inbound URL to: ${webhookUrl}`);
412
413
  }
413
- steps.push(`Run verification: /mu-setup verify ${check.id}${normalizedBase ? ` --public-base-url ${normalizedBase}` : ""}`);
414
+ steps.push(`Run verification: /mu setup verify ${check.id}${normalizedBase ? ` --public-base-url ${normalizedBase}` : ""}`);
414
415
  }
415
416
  return {
416
417
  id: check.id,
@@ -423,8 +424,8 @@ function buildPlan(check, publicBaseUrl) {
423
424
  missing_required_fields: check.missing,
424
425
  steps,
425
426
  commands: {
426
- apply: `/mu-setup apply ${check.id}`,
427
- verify: `/mu-setup verify ${check.id}`,
427
+ apply: `/mu setup apply ${check.id}`,
428
+ verify: `/mu setup verify ${check.id}`,
428
429
  },
429
430
  };
430
431
  }
@@ -702,7 +703,7 @@ function verifyText(result) {
702
703
  lines.push(` next: ${check.next_step}`);
703
704
  }
704
705
  if (!result.ok) {
705
- lines.push("", "Tip: run `/mu-setup plan <adapter>` for exact remediation steps.");
706
+ lines.push("", "Tip: run `/mu setup plan <adapter>` for exact remediation steps.");
706
707
  }
707
708
  return lines.join("\n");
708
709
  }
@@ -853,7 +854,7 @@ function buildAgentSetupPrompt(opts) {
853
854
  missing_fields: opts.check.missing.join(", ") || "(none)",
854
855
  provider_steps: adapter.providerSetupSteps.map((step, index) => `${index + 1}. ${step}`).join("\n"),
855
856
  field_status: adapterFieldStatusLines(adapter, opts.check).join("\n"),
856
- verify_command: `/mu-setup verify ${adapter.id}${verifyFlag}`,
857
+ verify_command: `/mu setup verify ${adapter.id}${verifyFlag}`,
857
858
  });
858
859
  }
859
860
  function dispatchSetupPromptToAgent(pi, ctx, prompt) {
@@ -1023,7 +1024,7 @@ export function messagingSetupExtension(pi) {
1023
1024
  const overrides = params.fields ?? {};
1024
1025
  const stillMissing = check.missing.filter((field) => !(field in overrides));
1025
1026
  if (stillMissing.length > 0) {
1026
- return textResult(`Cannot apply ${adapterId}: missing required config fields (${stillMissing.join(", ")}). Pass them via the fields parameter or use /mu-setup apply ${adapterId} for guided input.`, { adapter: adapterId, missing_required_fields: stillMissing });
1027
+ return textResult(`Cannot apply ${adapterId}: missing required config fields (${stillMissing.join(", ")}). Pass them via the fields parameter or use /mu setup apply ${adapterId} for guided input.`, { adapter: adapterId, missing_required_fields: stillMissing });
1027
1028
  }
1028
1029
  const outcome = await applyAdapterConfig({
1029
1030
  adapterId,
@@ -1057,12 +1058,14 @@ export function messagingSetupExtension(pi) {
1057
1058
  }
1058
1059
  },
1059
1060
  });
1060
- pi.registerCommand("mu-setup", {
1061
- description: "Messaging setup workflow (`/mu-setup slack`, `/mu-setup plan <adapter>`, `/mu-setup apply <adapter>`, `/mu-setup verify [adapter]`)",
1061
+ registerMuSubcommand(pi, {
1062
+ subcommand: "setup",
1063
+ summary: "Messaging adapter setup workflow (preflight/guide/plan/apply/verify)",
1064
+ usage: "/mu setup [preflight|guide|plan|apply|verify] [adapter] [--public-base-url URL] [--agent|--no-agent]",
1062
1065
  handler: async (args, ctx) => {
1063
1066
  const parsed = parseSetupCommandArgs(args);
1064
1067
  if (parsed.error) {
1065
- ctx.ui.notify(`${parsed.error}. Usage: /mu-setup [preflight|guide|plan|apply|verify] [adapter] [--public-base-url URL] [--agent|--no-agent]`, "error");
1068
+ ctx.ui.notify(`${parsed.error}. Usage: /mu setup [preflight|guide|plan|apply|verify] [adapter] [--public-base-url URL] [--agent|--no-agent]`, "error");
1066
1069
  return;
1067
1070
  }
1068
1071
  switch (parsed.action) {
@@ -1121,7 +1124,7 @@ export function messagingSetupExtension(pi) {
1121
1124
  }
1122
1125
  case "apply": {
1123
1126
  if (!parsed.adapterId) {
1124
- ctx.ui.notify("apply requires adapter. Example: /mu-setup apply slack", "error");
1127
+ ctx.ui.notify("apply requires adapter. Example: /mu setup apply slack", "error");
1125
1128
  return;
1126
1129
  }
1127
1130
  const text = await runInteractiveApply(ctx, parsed.adapterId);
@@ -0,0 +1,11 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ export type MuSubcommandHandler = (args: string, ctx: ExtensionCommandContext) => Promise<void> | void;
3
+ export type MuSubcommandRegistration = {
4
+ subcommand: string;
5
+ summary: string;
6
+ usage: string;
7
+ aliases?: string[];
8
+ handler: MuSubcommandHandler;
9
+ };
10
+ export declare function registerMuSubcommand(pi: ExtensionAPI, registration: MuSubcommandRegistration): void;
11
+ //# sourceMappingURL=mu-command-dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mu-command-dispatcher.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-command-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE3F,MAAM,MAAM,mBAAmB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEvG,MAAM,MAAM,wBAAwB,GAAG;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAiIF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAsDnG"}
@@ -0,0 +1,143 @@
1
+ const DISPATCHER_STATES = new WeakMap();
2
+ const RESERVED_SUBCOMMANDS = new Set(["help", "?"]);
3
+ function normalizeSubcommand(value) {
4
+ return value.trim().toLowerCase();
5
+ }
6
+ function isValidSubcommandToken(value) {
7
+ return /^[a-z][a-z0-9_-]*$/.test(value);
8
+ }
9
+ function subcommandUsageSummary(entry) {
10
+ const aliasSuffix = entry.aliases && entry.aliases.length > 0 ? ` (aliases: ${entry.aliases.join(", ")})` : "";
11
+ return `- ${entry.usage} — ${entry.summary}${aliasSuffix}`;
12
+ }
13
+ function renderSubcommandCatalog(state) {
14
+ if (state.entries.size === 0) {
15
+ return ["No /mu subcommands are currently registered.", "", "Try again after extensions finish loading."].join("\n");
16
+ }
17
+ const entries = [...state.entries.values()].sort((left, right) => left.normalizedSubcommand.localeCompare(right.normalizedSubcommand));
18
+ const lines = ["Usage: /mu <subcommand> [args]", "", "Subcommands:"];
19
+ for (const entry of entries) {
20
+ lines.push(subcommandUsageSummary(entry));
21
+ }
22
+ lines.push("", "Run `/mu help <subcommand>` for focused usage.");
23
+ return lines.join("\n");
24
+ }
25
+ function renderSubcommandHelp(entry) {
26
+ const lines = [entry.summary, "", `Usage: ${entry.usage}`];
27
+ if (entry.aliases && entry.aliases.length > 0) {
28
+ lines.push(`Aliases: ${entry.aliases.map((alias) => `/mu ${alias}`).join(", ")}`);
29
+ }
30
+ return lines.join("\n");
31
+ }
32
+ function parseInvocation(args) {
33
+ const trimmed = args.trim();
34
+ if (trimmed.length === 0) {
35
+ return { subcommand: "", remainder: "" };
36
+ }
37
+ const boundary = trimmed.search(/\s/);
38
+ if (boundary === -1) {
39
+ return { subcommand: trimmed, remainder: "" };
40
+ }
41
+ return {
42
+ subcommand: trimmed.slice(0, boundary),
43
+ remainder: trimmed.slice(boundary + 1).trim(),
44
+ };
45
+ }
46
+ function resolveEntry(state, token) {
47
+ const normalized = normalizeSubcommand(token);
48
+ if (!normalized)
49
+ return null;
50
+ const canonical = state.aliases.get(normalized) ?? normalized;
51
+ return state.entries.get(canonical) ?? null;
52
+ }
53
+ function ensureDispatcher(pi) {
54
+ const existing = DISPATCHER_STATES.get(pi);
55
+ if (existing) {
56
+ return existing;
57
+ }
58
+ const state = {
59
+ entries: new Map(),
60
+ aliases: new Map(),
61
+ };
62
+ DISPATCHER_STATES.set(pi, state);
63
+ pi.registerCommand("mu", {
64
+ description: "mu command dispatcher (`/mu <subcommand> ...`)",
65
+ handler: async (args, ctx) => {
66
+ const parsed = parseInvocation(args);
67
+ if (!parsed.subcommand) {
68
+ ctx.ui.notify(renderSubcommandCatalog(state), "info");
69
+ return;
70
+ }
71
+ const normalized = normalizeSubcommand(parsed.subcommand);
72
+ if (normalized === "help" || normalized === "?") {
73
+ if (!parsed.remainder) {
74
+ ctx.ui.notify(renderSubcommandCatalog(state), "info");
75
+ return;
76
+ }
77
+ const detail = resolveEntry(state, parsed.remainder.split(/\s+/)[0] ?? "");
78
+ if (!detail) {
79
+ ctx.ui.notify(`Unknown mu subcommand: ${parsed.remainder}\n\n${renderSubcommandCatalog(state)}`, "error");
80
+ return;
81
+ }
82
+ ctx.ui.notify(renderSubcommandHelp(detail), "info");
83
+ return;
84
+ }
85
+ const entry = resolveEntry(state, parsed.subcommand);
86
+ if (!entry) {
87
+ ctx.ui.notify(`Unknown mu subcommand: ${parsed.subcommand}\n\n${renderSubcommandCatalog(state)}`, "error");
88
+ return;
89
+ }
90
+ await entry.handler(parsed.remainder, ctx);
91
+ },
92
+ });
93
+ return state;
94
+ }
95
+ export function registerMuSubcommand(pi, registration) {
96
+ const state = ensureDispatcher(pi);
97
+ const normalizedSubcommand = normalizeSubcommand(registration.subcommand);
98
+ if (!isValidSubcommandToken(normalizedSubcommand)) {
99
+ throw new Error(`Invalid mu subcommand: ${registration.subcommand}`);
100
+ }
101
+ if (RESERVED_SUBCOMMANDS.has(normalizedSubcommand)) {
102
+ throw new Error(`Reserved mu subcommand: ${registration.subcommand}`);
103
+ }
104
+ if (!registration.usage.startsWith("/mu ")) {
105
+ throw new Error(`mu subcommand usage must start with '/mu ': ${registration.usage}`);
106
+ }
107
+ const normalizedAliases = (registration.aliases ?? [])
108
+ .map((alias) => normalizeSubcommand(alias))
109
+ .filter((alias) => alias.length > 0 && alias !== normalizedSubcommand);
110
+ for (const alias of normalizedAliases) {
111
+ if (!isValidSubcommandToken(alias)) {
112
+ throw new Error(`Invalid mu subcommand alias: ${alias}`);
113
+ }
114
+ if (RESERVED_SUBCOMMANDS.has(alias)) {
115
+ throw new Error(`Reserved mu subcommand alias: ${alias}`);
116
+ }
117
+ }
118
+ const existing = state.entries.get(normalizedSubcommand);
119
+ if (existing) {
120
+ for (const alias of existing.normalizedAliases) {
121
+ state.aliases.delete(alias);
122
+ }
123
+ }
124
+ for (const alias of normalizedAliases) {
125
+ const occupiedBy = state.aliases.get(alias);
126
+ if (occupiedBy && occupiedBy !== normalizedSubcommand) {
127
+ throw new Error(`mu subcommand alias '${alias}' is already registered by '${occupiedBy}'`);
128
+ }
129
+ if (state.entries.has(alias) && alias !== normalizedSubcommand) {
130
+ throw new Error(`mu subcommand alias '${alias}' conflicts with existing subcommand`);
131
+ }
132
+ }
133
+ const entry = {
134
+ ...registration,
135
+ normalizedSubcommand,
136
+ normalizedAliases,
137
+ };
138
+ state.entries.set(normalizedSubcommand, entry);
139
+ state.aliases.set(normalizedSubcommand, normalizedSubcommand);
140
+ for (const alias of normalizedAliases) {
141
+ state.aliases.set(alias, normalizedSubcommand);
142
+ }
143
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"server-tools.d.ts","sourceRoot":"","sources":["../../src/extensions/server-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAoFlE,MAAM,MAAM,wBAAwB,GAAG;IACtC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;CAClC,CAAC;AAuXF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,GAAE,wBAA6B,QAQzF;AAED,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,YAAY,QAS5D;AAED,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"server-tools.d.ts","sourceRoot":"","sources":["../../src/extensions/server-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAqFlE,MAAM,MAAM,wBAAwB,GAAG;IACtC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;CAClC,CAAC;AA2XF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,GAAE,wBAA6B,QAQzF;AAED,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,YAAY,QAS5D;AAED,eAAe,oBAAoB,CAAC"}
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { StringEnum } from "@mariozechner/pi-ai";
7
7
  import { Type } from "@sinclair/typebox";
8
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
8
9
  import { clampInt, fetchMuJson, fetchMuStatus, muServerUrl, textResult, toJsonText, } from "./shared.js";
9
10
  function trimOrNull(value) {
10
11
  if (value == null)
@@ -376,8 +377,10 @@ function registerServerTools(pi, opts) {
376
377
  }
377
378
  },
378
379
  });
379
- pi.registerCommand("mu-status", {
380
- description: "Show concise mu server status",
380
+ registerMuSubcommand(pi, {
381
+ subcommand: "status",
382
+ summary: "Show concise mu server status",
383
+ usage: "/mu status",
381
384
  handler: async (_args, ctx) => {
382
385
  try {
383
386
  const status = await fetchMuStatus();
@@ -388,8 +391,10 @@ function registerServerTools(pi, opts) {
388
391
  }
389
392
  },
390
393
  });
391
- pi.registerCommand("mu-control", {
392
- description: "Show control-plane adapter/runtime status",
394
+ registerMuSubcommand(pi, {
395
+ subcommand: "control",
396
+ summary: "Show control-plane adapter/runtime status",
397
+ usage: "/mu control",
393
398
  handler: async (_args, ctx) => {
394
399
  try {
395
400
  const status = await fetchMuStatus();
@@ -414,7 +419,7 @@ export function serverToolsExtension(pi, opts = {}) {
414
419
  registerServerTools(pi, {
415
420
  allowForumPost: opts.allowForumPost ?? true,
416
421
  toolIntroLine: opts.toolIntroLine ??
417
- "Tools: mu_status, mu_control_plane, mu_issues, mu_forum, mu_events, mu_runs, mu_activities, mu_heartbeats, mu_identity.",
422
+ "Tools: mu_status, mu_control_plane, mu_issues, mu_forum, mu_events, mu_runs, mu_activities, mu_heartbeats, mu_cron, mu_identity.",
418
423
  extraSystemPromptLines: opts.extraSystemPromptLines ?? [],
419
424
  });
420
425
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.56",
3
+ "version": "26.2.57",
4
4
  "description": "Shared agent runtime for mu chat, orchestration roles, and serve extensions.",
5
5
  "keywords": [
6
6
  "mu",
@@ -23,7 +23,7 @@
23
23
  "prompts/**"
24
24
  ],
25
25
  "dependencies": {
26
- "@femtomc/mu-core": "26.2.54",
26
+ "@femtomc/mu-core": "workspace:*",
27
27
  "@mariozechner/pi-agent-core": "^0.52.12",
28
28
  "@mariozechner/pi-ai": "^0.52.12",
29
29
  "@mariozechner/pi-coding-agent": "^0.52.12",
@@ -22,6 +22,7 @@ You also have access to specialized read/diagnostic tools:
22
22
  - `mu_runs`
23
23
  - `mu_activities`
24
24
  - `mu_heartbeats`
25
+ - `mu_cron`
25
26
  - `mu_messaging_setup`
26
27
 
27
28
  Hard Constraints: