@pi-unipi/unipi 3.0.0-alpha.42 → 3.0.0-alpha.43

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 (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +26 -24
  3. package/packages/app-bridge/package.json +2 -2
  4. package/packages/app-bridge/src/bridge.ts +66 -4
  5. package/packages/app-bridge/src/wire.ts +9 -0
  6. package/packages/ask-user/package.json +2 -2
  7. package/packages/autocomplete/package.json +1 -1
  8. package/packages/autocomplete/src/constants.ts +2 -0
  9. package/packages/background-tasks/package.json +2 -2
  10. package/packages/btw/package.json +2 -2
  11. package/packages/compactor/package.json +3 -3
  12. package/packages/core/command-echo.ts +47 -1
  13. package/packages/core/constants.ts +1 -0
  14. package/packages/core/package.json +1 -1
  15. package/packages/dream/package.json +2 -2
  16. package/packages/footer/package.json +3 -3
  17. package/packages/fusion/package.json +4 -4
  18. package/packages/info-screen/package.json +2 -2
  19. package/packages/input-shortcuts/package.json +2 -2
  20. package/packages/kanboard/package.json +7 -7
  21. package/packages/long-horizon/package.json +2 -2
  22. package/packages/mcp/package.json +2 -2
  23. package/packages/memory/package.json +3 -3
  24. package/packages/notify/package.json +2 -2
  25. package/packages/skill-registry/package.json +2 -2
  26. package/packages/subagents/package.json +3 -3
  27. package/packages/unipi/bundled.js +531 -85
  28. package/packages/updater/package.json +2 -2
  29. package/packages/utility/package.json +2 -2
  30. package/packages/utility/skills/cron/SKILL.md +131 -0
  31. package/packages/utility/src/cron/index.ts +323 -0
  32. package/packages/utility/src/cron/settings.ts +185 -0
  33. package/packages/utility/src/index.ts +18 -1
  34. package/packages/watchdog/package.json +3 -3
  35. package/packages/web-api/package.json +2 -2
  36. package/packages/workflow/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.0.0-alpha.43] — 2026-10-16
10
+
11
+ ### Added
12
+ - Cron: scheduled, unattended pi runs on your PC, run by the UniPi host (needs UniPi host 0.5.1). `skill:cron` sets jobs up from a plain request ("every weekday at 18:30 summarize my inbox"), with plain-language schedules, per-job notifications (ntfy, Gotify, Telegram) and an optional wake-up message to the session that set it. `/unipi:cron` lists jobs, their runs and results, and runs, stops, enables or pauses them. Cron settings are in `/unipi:settings`. (UNI-288, UNI-314–318)
13
+ - `/skill:craft-skill` and `/skill:web` are listed as normal skills. (UNI-307)
14
+
15
+ ### Fixed
16
+ - The UniPi app now gets argument suggestions for every extension's commands (e.g. `/usage `), not only UniPi's own. (UNI-304)
17
+ - The app's model list stayed partial (only openrouter) when a provider such as OmniRoute registered after the app connected; the list is now pushed again when it changes.
18
+ - Removing a steered message from the app no longer stops the run. (UNI-305)
19
+ - `/reload` from the app reloads pi instead of being sent as a message. (UNI-308)
20
+
9
21
  ## [3.0.0-alpha.42] — 2026-10-16
10
22
 
11
23
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -70,7 +70,9 @@
70
70
  "packages/notify/skills",
71
71
  "packages/kanboard/skills",
72
72
  "packages/updater/skills",
73
- "packages/subagents/skills"
73
+ "packages/subagents/skills",
74
+ "packages/web-api/skills",
75
+ "packages/dream/skills"
74
76
  ]
75
77
  },
76
78
  "peerDependencies": {
@@ -80,28 +82,28 @@
80
82
  "typebox": "^1.1.38"
81
83
  },
82
84
  "dependencies": {
83
- "@pi-unipi/app-bridge": "3.0.0-alpha.42",
84
- "@pi-unipi/ask-user": "3.0.0-alpha.42",
85
- "@pi-unipi/background-tasks": "3.0.0-alpha.42",
86
- "@pi-unipi/btw": "3.0.0-alpha.42",
87
- "@pi-unipi/command-enchantment": "3.0.0-alpha.42",
88
- "@pi-unipi/compactor": "3.0.0-alpha.42",
89
- "@pi-unipi/core": "3.0.0-alpha.42",
90
- "@pi-unipi/dream": "3.0.0-alpha.42",
91
- "@pi-unipi/footer": "3.0.0-alpha.42",
92
- "@pi-unipi/fusion": "3.0.0-alpha.42",
93
- "@pi-unipi/info-screen": "3.0.0-alpha.42",
94
- "@pi-unipi/input-shortcuts": "3.0.0-alpha.42",
95
- "@pi-unipi/kanboard": "3.0.0-alpha.42",
96
- "@pi-unipi/mcp": "3.0.0-alpha.42",
97
- "@pi-unipi/memory": "3.0.0-alpha.42",
98
- "@pi-unipi/notify": "3.0.0-alpha.42",
99
- "@pi-unipi/skill-registry": "3.0.0-alpha.42",
100
- "@pi-unipi/subagents": "3.0.0-alpha.42",
101
- "@pi-unipi/updater": "3.0.0-alpha.42",
102
- "@pi-unipi/utility": "3.0.0-alpha.42",
103
- "@pi-unipi/web-api": "3.0.0-alpha.42",
104
- "@pi-unipi/workflow": "3.0.0-alpha.42",
85
+ "@pi-unipi/app-bridge": "3.0.0-alpha.43",
86
+ "@pi-unipi/ask-user": "3.0.0-alpha.43",
87
+ "@pi-unipi/background-tasks": "3.0.0-alpha.43",
88
+ "@pi-unipi/btw": "3.0.0-alpha.43",
89
+ "@pi-unipi/command-enchantment": "3.0.0-alpha.43",
90
+ "@pi-unipi/compactor": "3.0.0-alpha.43",
91
+ "@pi-unipi/core": "3.0.0-alpha.43",
92
+ "@pi-unipi/dream": "3.0.0-alpha.43",
93
+ "@pi-unipi/footer": "3.0.0-alpha.43",
94
+ "@pi-unipi/fusion": "3.0.0-alpha.43",
95
+ "@pi-unipi/info-screen": "3.0.0-alpha.43",
96
+ "@pi-unipi/input-shortcuts": "3.0.0-alpha.43",
97
+ "@pi-unipi/kanboard": "3.0.0-alpha.43",
98
+ "@pi-unipi/mcp": "3.0.0-alpha.43",
99
+ "@pi-unipi/memory": "3.0.0-alpha.43",
100
+ "@pi-unipi/notify": "3.0.0-alpha.43",
101
+ "@pi-unipi/skill-registry": "3.0.0-alpha.43",
102
+ "@pi-unipi/subagents": "3.0.0-alpha.43",
103
+ "@pi-unipi/updater": "3.0.0-alpha.43",
104
+ "@pi-unipi/utility": "3.0.0-alpha.43",
105
+ "@pi-unipi/web-api": "3.0.0-alpha.43",
106
+ "@pi-unipi/workflow": "3.0.0-alpha.43",
105
107
  "@resvg/resvg-wasm": "^2.6.2",
106
108
  "beautiful-mermaid": "^1.1.3"
107
109
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/app-bridge",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Live bridge between a pi TUI session and the UniPi phone app — streaming chat, prompts, steering and dialogs from the phone",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@pi-unipi/core": "3.0.0-alpha.42"
34
+ "@pi-unipi/core": "3.0.0-alpha.43"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -658,6 +658,23 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
658
658
  }
659
659
  };
660
660
 
661
+ // The model list changes after `hello`: providers that register late
662
+ // (pi-omniroute-bridge refreshes its provider in session_start, after this
663
+ // bridge's hello) or a /model in the TUI that loads more. A phone that
664
+ // connected early kept the first, partial list (only openrouter) until
665
+ // something forced a new hello. Push a `models` line whenever the list
666
+ // differs from the last one sent, and check a few times after start.
667
+ let lastModelsKey = "";
668
+ const modelsKey = (list: ModelInfo[]) => list.map((m) => `${m.provider}/${m.id}`).join("\n");
669
+ const pushModelsIfChanged = () => {
670
+ if (!ctx || clients.size === 0) return;
671
+ const list = models();
672
+ const key = modelsKey(list);
673
+ if (key === lastModelsKey) return;
674
+ lastModelsKey = key;
675
+ send({ t: "models", models: list } as OutMsg);
676
+ };
677
+
661
678
  // UNI-222: long-horizon progress (sticky LH_PROGRESS on the core bus).
662
679
  const lhProgress = (): LhProgressInfo | undefined => bus.get(UNIPI_EVENTS.LH_PROGRESS) as LhProgressInfo | undefined;
663
680
  // UNI-269: the working mode (sticky LH_STATE — the footer's "Graph Mode").
@@ -675,7 +692,11 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
675
692
  streaming: streaming ? { id: streaming.id, role: "assistant" as const, content: phoneSafe(streaming.content) as unknown[] } : undefined,
676
693
  tools: [...tools.values()].map((t) => ({ ...t, text: t.text ? clipText(t.text, 8 * 1024) : t.text })),
677
694
  commands: commands().map((x) => ({ ...x, description: x.description ? clipText(x.description, 160) : x.description })),
678
- models: models(),
695
+ models: (() => {
696
+ const list = models();
697
+ lastModelsKey = modelsKey(list);
698
+ return list;
699
+ })(),
679
700
  dialogs: hub.list(),
680
701
  queue: queueView(),
681
702
  fusion: fusionPreset(),
@@ -1121,12 +1142,32 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
1121
1142
  if (!c) return fail("pi is gone.");
1122
1143
  const i = tuiQueue.findIndex((q) => q.id === id);
1123
1144
  if (i < 0) return fail("That queued message is gone.");
1124
- const [target] = tuiQueue.splice(i, 1);
1145
+ // UNI-305: pi has no API to drop ONE item from its own live steer/followUp
1146
+ // queue — the only way to edit it is to abort the whole run and re-send
1147
+ // what's left (see the class comment above `tuiQueue`). That's fine
1148
+ // between tool calls (there is nothing running to lose), but aborting
1149
+ // WHILE a tool is executing kills it mid-flight — real, unsaved work —
1150
+ // and if the removed item was the queue's only entry nothing is left to
1151
+ // re-send: the run just dies instead of continuing. So this waits a
1152
+ // short beat for the in-flight tool to reach its own end (the next safe
1153
+ // boundary) before touching the queue; a tool still running past that
1154
+ // is left alone and the edit is refused instead of destroying it.
1155
+ if (tools.size > 0) {
1156
+ const deadline = Date.now() + 4_000;
1157
+ while (tools.size > 0 && Date.now() < deadline) await sleep(100);
1158
+ if (tools.size > 0) return fail("That message was already handed to pi — it can't be taken back while a tool is running.");
1159
+ }
1160
+ // The wait above may have let pi drain this very item into a real turn
1161
+ // (the message_start handler below removes it from tuiQueue once pi
1162
+ // actually sends it) — re-check instead of operating on a stale index.
1163
+ const liveIndex = tuiQueue.findIndex((q) => q.id === id);
1164
+ if (liveIndex < 0) return fail("That message was already handed to pi — it can't be taken back now.");
1165
+ const [target] = tuiQueue.splice(liveIndex, 1);
1125
1166
  const action = apply(target!) ?? undefined;
1126
1167
  // Keep the edited/kept target in its original slot (promote: first).
1127
1168
  const rest = tuiQueue.splice(0, tuiQueue.length);
1128
1169
  if (action === "promote") rest.unshift(target!);
1129
- else if (action !== "drop") rest.splice(i, 0, target!);
1170
+ else if (action !== "drop") rest.splice(liveIndex, 0, target!);
1130
1171
  if (!c.isIdle()) {
1131
1172
  await abortKeepingEditor(c);
1132
1173
  const deadline = Date.now() + 10_000;
@@ -1507,6 +1548,21 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
1507
1548
  }
1508
1549
  return;
1509
1550
  }
1551
+ case "reload": {
1552
+ // UNI-308: `/reload` must work from the app, not fall through to a plain
1553
+ // prompt. `ctx.reload()` (ExtensionCommandContext) is the same call the
1554
+ // terminal's built-in `/reload` makes.
1555
+ if (!(await ensureIdle(msg.force))) return write(sock, { t: "error", message: "pi is busy. Stop the current run first.", code: "busy", ref: msg.ref });
1556
+ try {
1557
+ await runCommandOp(async (cctx) => {
1558
+ await cctx.reload();
1559
+ });
1560
+ ack();
1561
+ } catch (error) {
1562
+ fail(error instanceof Error ? error.message : String(error));
1563
+ }
1564
+ return;
1565
+ }
1510
1566
  case "tree_go": {
1511
1567
  if (!(await ensureIdle(msg.force))) return write(sock, { t: "error", message: "pi is busy. Stop the current run first.", code: "busy", ref: msg.ref });
1512
1568
  try {
@@ -1822,6 +1878,8 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
1822
1878
  setRemoteDialogs(hub);
1823
1879
  listen();
1824
1880
  writeRecord();
1881
+ // Late providers (registered after this hello): re-check the model list.
1882
+ for (const ms of [1500, 5000, 15000]) setTimeout(() => safe(pushModelsIfChanged)(), ms).unref?.();
1825
1883
  if (event?.reason && event.reason !== "startup") {
1826
1884
  streaming = undefined;
1827
1885
  tools.clear();
@@ -1988,6 +2046,7 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
1988
2046
  });
1989
2047
 
1990
2048
  on("agent_start", safe(() => {
2049
+ pushModelsIfChanged();
1991
2050
  // A fresh run after a phone Stop: the queue delivers after THIS one.
1992
2051
  releaseHold();
1993
2052
  running = true;
@@ -2155,7 +2214,10 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
2155
2214
 
2156
2215
  // The preset's `active` changes with the model (Fusion leaves fusion mode
2157
2216
  // on a /model switch), so it rides along (UNI-212).
2158
- on("model_select", safe(() => stateWithFusion()));
2217
+ on("model_select", safe(() => {
2218
+ stateWithFusion();
2219
+ pushModelsIfChanged();
2220
+ }));
2159
2221
  on("thinking_level_select", safe(() => send({ t: "state", ...runState() })));
2160
2222
  on("session_compact", safe(() => {
2161
2223
  if (!ctx) return;
@@ -251,6 +251,8 @@ export type OutMsg =
251
251
  dream?: DreamStatusInfo;
252
252
  }
253
253
  | ({ t: "state" } & Partial<RunState>)
254
+ /** The model list changed after `hello` (a late provider, a /model in the TUI). */
255
+ | { t: "models"; models: ModelInfo[] }
254
256
  | ({ t: "session"; reason: string } & SessionInfo)
255
257
  | { t: "entry"; entry: unknown }
256
258
  | { t: "msg_start"; id: string; role: string }
@@ -413,6 +415,10 @@ export type InMsg =
413
415
  | { t: "tree"; ref?: string }
414
416
  /** Navigate to a different point in the tree. `force`: abort the current run first. */
415
417
  | { t: "tree_go"; id: string; summarize?: boolean; force?: boolean; ref?: string }
418
+ /** UNI-308: `/reload` sent from the app — re-reads extensions, skills, prompts, context files
419
+ * via `ctx.reload()` (the same thing the terminal's `/reload` built-in does). `force`: abort
420
+ * the current run first, like the other session ops. */
421
+ | { t: "reload"; force?: boolean; ref?: string }
416
422
  /** Fetches the full bytes behind an image `mediaRef` the bridge sent in a
417
423
  * `media`-omitted placeholder (see `phoneSafe`): replies with one or more
418
424
  * `media_chunk` (≤ 700 KB of base64 per chunk) ending in `done:true`, or a
@@ -501,6 +507,7 @@ const IN_TYPES = new Set([
501
507
  "session_fork",
502
508
  "tree",
503
509
  "tree_go",
510
+ "reload",
504
511
  "session_rename",
505
512
  "media",
506
513
  "file_share",
@@ -594,6 +601,8 @@ export function parseIn(line: string): InMsg | { bad: string; ref?: string } | u
594
601
  case "tree_go":
595
602
  if (typeof m.id !== "string" || !m.id) return { bad: "tree_go.id must be a string", ref };
596
603
  return { t: "tree_go", id: m.id, summarize: m.summarize === true, force: m.force === true, ref };
604
+ case "reload":
605
+ return { t: "reload", force: m.force === true, ref };
597
606
  case "session_rename":
598
607
  if (typeof m.name !== "string") return { bad: "session_rename.name must be a string", ref };
599
608
  return { t: "session_rename", name: m.name.slice(0, 200), ref };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -42,7 +42,7 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
- "@pi-unipi/core": "3.0.0-alpha.42"
45
+ "@pi-unipi/core": "3.0.0-alpha.43"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Enhanced TUI autocomplete for /unipi:* commands — colored, sorted, and grouped by package",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -77,6 +77,7 @@ export const COMMAND_REGISTRY: Record<string, string> = {
77
77
  "unipi:dream": "utility",
78
78
  "unipi:answer": "utility",
79
79
  "unipi:summarize": "utility",
80
+ "unipi:cron": "utility",
80
81
  "unipi:move": "utility",
81
82
 
82
83
  // skill-registry
@@ -165,6 +166,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
165
166
  "unipi:doctor": "Check UniPi's runtime: config, model cache, Decision Model, skills",
166
167
  "unipi:answer": "Answer the last reply — fixed input under the scrollable reply, per question, or web form",
167
168
  "unipi:summarize": "Summarize your last reply again (answer first, then findings, open items, questions). Add text to ask about it",
169
+ "unipi:cron": "Scheduled pi runs on this PC — jobs, next runs, results; run, stop, enable, pause",
168
170
  "unipi:skills": "Manage skills — on/off per project, listed or not, the skill vault",
169
171
  "unipi:move": "Re-link sessions, trust, worktrees, boards and memories after moving this project",
170
172
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/background-tasks",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Background tasks for UniPi — durable shell jobs and delegated background agents with a footer dock",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@earendil-works/pi-ai": "^0.87.1",
13
- "@pi-unipi/core": "3.0.0-alpha.42",
13
+ "@pi-unipi/core": "3.0.0-alpha.43",
14
14
  "turndown": "^7.2.4"
15
15
  },
16
16
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
5
5
  "type": "module",
6
6
  "main": "extensions/btw.ts",
@@ -37,7 +37,7 @@
37
37
  "@earendil-works/pi-tui": "^0.87.1"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "3.0.0-alpha.42"
40
+ "@pi-unipi/core": "3.0.0-alpha.43"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Compaction for Pi — lossless zero-LLM or model summaries, rebuilt from full history, with session recall",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,8 +33,8 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "3.0.0-alpha.42",
37
- "@pi-unipi/info-screen": "3.0.0-alpha.42",
36
+ "@pi-unipi/core": "3.0.0-alpha.43",
37
+ "@pi-unipi/info-screen": "3.0.0-alpha.43",
38
38
  "@earendil-works/pi-agent-core": "^0.87.1"
39
39
  },
40
40
  "peerDependencies": {
@@ -12,7 +12,7 @@
12
12
  * failures never block the command itself.
13
13
  */
14
14
 
15
- import type { ExtensionAPI, ExtensionCommandContext, RegisteredCommand } from "@earendil-works/pi-coding-agent";
15
+ import { ExtensionRunner, type ExtensionAPI, type ExtensionCommandContext, type RegisteredCommand } from "@earendil-works/pi-coding-agent";
16
16
  import { Text } from "@earendil-works/pi-tui";
17
17
 
18
18
  export const COMMAND_ECHO_TYPE = "unipi-command-echo";
@@ -96,6 +96,48 @@ export function commandArgCompleters(): Map<string, ArgCompleter> {
96
96
  return (g[COMPLETERS_KEY] ??= new Map<string, ArgCompleter>());
97
97
  }
98
98
 
99
+ /**
100
+ * UNI-304: `commandArgCompleters()` used to only ever hold the commands
101
+ * this module's own `withCommandEcho` wrapper saw (UniPi's commands). Other
102
+ * extensions' commands — pi-omniroute-bridge's `/usage`, a future `/skill:*`
103
+ * completer, anything not registered through `withCommandEcho` — never got
104
+ * an entry, so the app (which can only ask the bridge, never pi's live
105
+ * in-memory command registry) got nothing for them.
106
+ *
107
+ * `ExtensionRunner.resolveRegisteredCommands()` is where pi itself resolves
108
+ * every extension's registered command (with its real `getArgumentCompletions`,
109
+ * see `RegisteredCommand`) into the de-duplicated list the TUI's own
110
+ * autocomplete reads (`interactive-mode.js` calls `getRegisteredCommands()`,
111
+ * which calls this). It runs on every keystroke in a command's argument area,
112
+ * so it is already a safe, repeatedly-called hook. Patching it here — once,
113
+ * idempotently, on the shared class — is the least invasive way to see every
114
+ * extension's completer: no extension author needs to go through
115
+ * `withCommandEcho`, and pi needs no API change. UniPi's `unipi/index.ts`
116
+ * calls `withCommandEcho` (which calls this) before any other module loads,
117
+ * so the patch is in place before any extension — ours or a third party's —
118
+ * registers a single command.
119
+ */
120
+ let runnerPatched = false;
121
+ export function patchExtensionRunnerCommandCompleters(): void {
122
+ if (runnerPatched) return;
123
+ runnerPatched = true;
124
+ const proto = ExtensionRunner.prototype as unknown as {
125
+ resolveRegisteredCommands: () => Array<{ invocationName: string; name: string; getArgumentCompletions?: ArgCompleter }>;
126
+ };
127
+ const original = proto.resolveRegisteredCommands;
128
+ if (typeof original !== "function") return; // Unknown/future pi build: no-op rather than throw.
129
+ proto.resolveRegisteredCommands = function (this: unknown, ...args: unknown[]) {
130
+ const resolved = original.apply(this, args as []);
131
+ const map = commandArgCompleters();
132
+ for (const command of resolved) {
133
+ if (typeof command.getArgumentCompletions === "function") {
134
+ map.set(command.invocationName, command.getArgumentCompletions.bind(command));
135
+ }
136
+ }
137
+ return resolved;
138
+ } as typeof original;
139
+ }
140
+
99
141
  /** Never echo `/name` (internal, programmatic commands). Call before or after registering. */
100
142
  export function silenceCommandEcho(name: string): void {
101
143
  silentSet().add(name.replace(/^\//, ""));
@@ -114,6 +156,10 @@ export function isSilentEcho(text: unknown): boolean {
114
156
  }
115
157
 
116
158
  export function withCommandEcho(pi: ExtensionAPI): ExtensionAPI {
159
+ // UNI-304: patch once, before any command is registered, so every
160
+ // extension's own completer (not just ones that happen to go through this
161
+ // wrapper) ends up in commandArgCompleters().
162
+ patchExtensionRunnerCommandCompleters();
117
163
  try {
118
164
  pi.registerEntryRenderer<{ text?: string }>(
119
165
  COMMAND_ECHO_TYPE,
@@ -110,6 +110,7 @@ export const UTILITY_COMMANDS = {
110
110
  ANSWER: "answer",
111
111
  SUMMARIZE: "summarize",
112
112
  MOVE: "move",
113
+ CRON: "cron",
113
114
  } as const;
114
115
 
115
116
  /** Ask-user tool names */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/dream",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "A pi extension that dreams: reviews past session struggles in the background, turns repeated false paths into memory lessons, and crafts skills from errors — part of the Unipi suite",
5
5
  "type": "module",
6
6
  "main": "extensions/dream.ts",
@@ -39,7 +39,7 @@
39
39
  "@earendil-works/pi-tui": "^0.87.1"
40
40
  },
41
41
  "dependencies": {
42
- "@pi-unipi/core": "3.0.0-alpha.42"
42
+ "@pi-unipi/core": "3.0.0-alpha.43"
43
43
  },
44
44
  "pi": {
45
45
  "extensions": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "3.0.0-alpha.42",
36
- "@pi-unipi/background-tasks": "3.0.0-alpha.42"
35
+ "@pi-unipi/core": "3.0.0-alpha.43",
36
+ "@pi-unipi/background-tasks": "3.0.0-alpha.43"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/fusion",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Devin-style model picker, fusion presets (lead + sidekick), and Local Fusion runtime for UniPi",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -32,9 +32,9 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "3.0.0-alpha.42",
36
- "@pi-unipi/subagents": "3.0.0-alpha.42",
37
- "@pi-unipi/utility": "3.0.0-alpha.42"
35
+ "@pi-unipi/core": "3.0.0-alpha.43",
36
+ "@pi-unipi/subagents": "3.0.0-alpha.43",
37
+ "@pi-unipi/utility": "3.0.0-alpha.43"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-ai": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Unicrab startup splash and the /unipi:info dashboard — this session, context, usage, compaction savings",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "3.0.0-alpha.42"
36
+ "@pi-unipi/core": "3.0.0-alpha.43"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/input-shortcuts",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "3.0.0-alpha.42"
36
+ "@pi-unipi/core": "3.0.0-alpha.43"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Kanboard v3 — deferred-work board: CLI/daemon bridge, task runner and skill",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@pi-unipi/core": "3.0.0-alpha.42"
34
+ "@pi-unipi/core": "3.0.0-alpha.43"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -48,10 +48,10 @@
48
48
  "themes": []
49
49
  },
50
50
  "optionalDependencies": {
51
- "@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.42",
52
- "@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.42",
53
- "@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.42",
54
- "@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.42",
55
- "@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.42"
51
+ "@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.43",
52
+ "@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.43",
53
+ "@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.43",
54
+ "@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.43",
55
+ "@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.43"
56
56
  }
57
57
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/long-horizon",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Long-horizon execution for Pi — mode-gated /goal /ralph /swarm /graph orchestration with a TypeSafe prompt judge",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -37,7 +37,7 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "3.0.0-alpha.42",
40
+ "@pi-unipi/core": "3.0.0-alpha.43",
41
41
  "@resvg/resvg-wasm": "^2.6.2",
42
42
  "beautiful-mermaid": "^1.1.3"
43
43
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/mcp",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "MCP server management extension for Pi coding agent — browse, add, configure, and use MCP servers",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -30,7 +30,7 @@
30
30
  "test": "npx tsx --test tests/**/*.test.ts"
31
31
  },
32
32
  "dependencies": {
33
- "@pi-unipi/core": "3.0.0-alpha.42"
33
+ "@pi-unipi/core": "3.0.0-alpha.43"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/memory",
3
- "version": "3.0.0-alpha.42",
3
+ "version": "3.0.0-alpha.43",
4
4
  "description": "Persistent cross-session memory on MemPalace (auto-installed): daemon-routed writes, shared palace with other agents, for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -44,8 +44,8 @@
44
44
  "README.md"
45
45
  ],
46
46
  "dependencies": {
47
- "@pi-unipi/core": "3.0.0-alpha.42",
48
- "@pi-unipi/info-screen": "3.0.0-alpha.42",
47
+ "@pi-unipi/core": "3.0.0-alpha.43",
48
+ "@pi-unipi/info-screen": "3.0.0-alpha.43",
49
49
  "js-yaml": "^4.1.0"
50
50
  },
51
51
  "peerDependencies": {