@pi-unipi/unipi 2.1.3 → 2.2.0

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 (90) hide show
  1. package/README.md +6 -2
  2. package/package.json +23 -22
  3. package/packages/ask-user/ask-ui.ts +45 -31
  4. package/packages/ask-user/launcher-ui.ts +26 -22
  5. package/packages/ask-user/package.json +2 -2
  6. package/packages/ask-user/settings-tui.ts +22 -9
  7. package/packages/autocomplete/package.json +1 -1
  8. package/packages/autocomplete/src/constants.ts +5 -0
  9. package/packages/btw/package.json +2 -2
  10. package/packages/cocoindex/package.json +2 -2
  11. package/packages/compactor/package.json +3 -3
  12. package/packages/compactor/src/tui/settings-overlay.ts +2 -1
  13. package/packages/core/constants.ts +18 -0
  14. package/packages/core/index.ts +1 -0
  15. package/packages/core/package.json +1 -1
  16. package/packages/core/tui-width.ts +127 -0
  17. package/packages/footer/package.json +2 -2
  18. package/packages/footer/src/tui/settings-tui.ts +2 -1
  19. package/packages/image/README.md +82 -0
  20. package/packages/image/index.ts +1 -0
  21. package/packages/image/package.json +57 -0
  22. package/packages/image/skills/image/SKILL.md +73 -0
  23. package/packages/image/src/commands.ts +17 -0
  24. package/packages/image/src/generate.ts +201 -0
  25. package/packages/image/src/image-source.ts +204 -0
  26. package/packages/image/src/index.ts +99 -0
  27. package/packages/image/src/models.ts +290 -0
  28. package/packages/image/src/recognize.ts +223 -0
  29. package/packages/image/src/settings.ts +149 -0
  30. package/packages/image/src/tools.ts +296 -0
  31. package/packages/image/src/tui/model-selector.ts +279 -0
  32. package/packages/image/src/tui/settings-dialog.ts +236 -0
  33. package/packages/info-screen/package.json +2 -2
  34. package/packages/info-screen/tui/info-overlay.ts +505 -0
  35. package/packages/input-shortcuts/package.json +2 -2
  36. package/packages/kanboard/package.json +2 -2
  37. package/packages/kanboard/tui/kanboard-overlay.ts +300 -0
  38. package/packages/mcp/package.json +2 -2
  39. package/packages/mcp/src/tui/add-overlay.ts +10 -7
  40. package/packages/mcp/src/tui/settings-overlay.ts +10 -7
  41. package/packages/memory/package.json +3 -3
  42. package/packages/memory/tui/settings-tui.ts +281 -0
  43. package/packages/milestone/package.json +2 -2
  44. package/packages/notify/README.md +2 -0
  45. package/packages/notify/events.ts +11 -0
  46. package/packages/notify/package.json +2 -2
  47. package/packages/notify/permission-prompt-message.ts +95 -0
  48. package/packages/notify/platforms/focus-win.ts +123 -0
  49. package/packages/notify/platforms/focus.ts +33 -0
  50. package/packages/notify/platforms/gotify.ts +36 -0
  51. package/packages/notify/platforms/native.ts +79 -0
  52. package/packages/notify/platforms/node-notifier.d.ts +20 -0
  53. package/packages/notify/platforms/ntfy.ts +48 -0
  54. package/packages/notify/platforms/telegram.ts +77 -0
  55. package/packages/notify/settings.ts +1 -0
  56. package/packages/notify/skills/configure-notify/SKILL.md +28 -1
  57. package/packages/notify/src/__tests__/permission-prompt-message.test.ts +172 -0
  58. package/packages/notify/tui/gotify-setup.ts +529 -0
  59. package/packages/notify/tui/ntfy-setup.ts +653 -0
  60. package/packages/notify/tui/recap-model-selector.ts +301 -0
  61. package/packages/notify/tui/settings-overlay.ts +353 -0
  62. package/packages/notify/tui/telegram-setup.ts +302 -0
  63. package/packages/ralph/package.json +3 -3
  64. package/packages/subagents/package.json +4 -4
  65. package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +185 -0
  66. package/packages/subagents/src/index.ts +52 -10
  67. package/packages/unipi/index.ts +2 -0
  68. package/packages/updater/package.json +2 -2
  69. package/packages/updater/src/tui/changelog-overlay.ts +2 -2
  70. package/packages/updater/src/tui/readme-overlay.ts +2 -1
  71. package/packages/updater/src/tui/update-overlay.ts +2 -1
  72. package/packages/utility/package.json +2 -2
  73. package/packages/utility/src/tui/badge-settings-tui.ts +2 -2
  74. package/packages/utility/src/tui/util-settings-tui.ts +2 -2
  75. package/packages/web-api/README.md +28 -11
  76. package/packages/web-api/package.json +8 -2
  77. package/packages/web-api/skills/web/SKILL.md +36 -9
  78. package/packages/web-api/src/index.ts +15 -0
  79. package/packages/web-api/src/providers/duckduckgo.ts +71 -27
  80. package/packages/web-api/src/providers/firecrawl.ts +1 -1
  81. package/packages/web-api/src/providers/jina-reader.ts +1 -1
  82. package/packages/web-api/src/providers/jina-search.ts +1 -1
  83. package/packages/web-api/src/providers/perplexity.ts +2 -2
  84. package/packages/web-api/src/providers/serpapi.ts +1 -1
  85. package/packages/web-api/src/providers/tavily.ts +1 -1
  86. package/packages/web-api/src/providers/wigolo-client.ts +196 -0
  87. package/packages/web-api/src/providers/wigolo.ts +117 -0
  88. package/packages/web-api/src/settings.ts +1 -0
  89. package/packages/web-api/src/tools.ts +93 -36
  90. package/packages/workflow/package.json +2 -2
@@ -0,0 +1,300 @@
1
+ /**
2
+ * @pi-unipi/kanboard — Kanboard TUI Overlay
3
+ *
4
+ * Two tabs: Tasks list and Kanban Board.
5
+ * Uses pi-tui overlay API (same pattern as MCP add overlay).
6
+ */
7
+
8
+ import {
9
+ Key,
10
+ matchesKey,
11
+ truncateToWidth,
12
+ type TUI,
13
+ visibleWidth,
14
+ } from "@earendil-works/pi-tui";
15
+ import type { Theme, KeybindingsManager } from "@earendil-works/pi-coding-agent";
16
+ import type { ParsedDoc, ParsedItem, ItemStatus } from "../types.js";
17
+ import { createDefaultRegistry } from "../parser/index.js";
18
+
19
+ type Tab = "tasks" | "board";
20
+
21
+ interface KanboardState {
22
+ tab: Tab;
23
+ tasks: ParsedItem[];
24
+ taskIndex: number;
25
+ taskScroll: number;
26
+ /** Board columns: index into tasks array grouped by status */
27
+ boardColumns: {
28
+ todo: number[];
29
+ inProgress: number[];
30
+ done: number[];
31
+ };
32
+ boardCol: number; // 0=todo, 1=inProgress, 2=done
33
+ boardIndex: number[];
34
+ pendingG: boolean;
35
+ docsRoot: string;
36
+ }
37
+
38
+ /** Status icon */
39
+ function statusIcon(status: ItemStatus): string {
40
+ switch (status) {
41
+ case "done":
42
+ return "✓";
43
+ case "in-progress":
44
+ return "◐";
45
+ default:
46
+ return "○";
47
+ }
48
+ }
49
+
50
+ /** Pad string to visible width */
51
+ function padVisible(content: string, targetWidth: number): string {
52
+ const pad = Math.max(0, targetWidth - visibleWidth(content));
53
+ return content + " ".repeat(pad);
54
+ }
55
+
56
+ /**
57
+ * Render the kanboard overlay.
58
+ */
59
+ export function renderKanboardOverlay(params?: {
60
+ docsRoot?: string;
61
+ onComplete?: () => void;
62
+ }) {
63
+ return (
64
+ tui: TUI,
65
+ theme: Theme,
66
+ _kb: KeybindingsManager,
67
+ done: (result: { viewed: boolean } | null) => void,
68
+ ) => {
69
+ const state: KanboardState = {
70
+ tab: "tasks",
71
+ tasks: [],
72
+ taskIndex: 0,
73
+ taskScroll: 0,
74
+ boardColumns: { todo: [], inProgress: [], done: [] },
75
+ boardCol: 0,
76
+ boardIndex: [0, 0, 0],
77
+ pendingG: false,
78
+ docsRoot: params?.docsRoot ?? ".unipi/docs",
79
+ };
80
+
81
+ // Load data
82
+ let loaded = false;
83
+ const ensureLoaded = async () => {
84
+ if (loaded) return;
85
+ const registry = await createDefaultRegistry();
86
+ const docs = registry.parseAll(state.docsRoot);
87
+ state.tasks = docs.flatMap((d) => d.items);
88
+
89
+ // Build board columns
90
+ state.boardColumns = { todo: [], inProgress: [], done: [] };
91
+ state.tasks.forEach((item, idx) => {
92
+ if (item.status === "done") state.boardColumns.done.push(idx);
93
+ else if (item.status === "in-progress") state.boardColumns.inProgress.push(idx);
94
+ else state.boardColumns.todo.push(idx);
95
+ });
96
+
97
+ // Init board indices
98
+ state.boardIndex = [
99
+ 0,
100
+ 0,
101
+ 0,
102
+ ];
103
+
104
+ loaded = true;
105
+ };
106
+
107
+ const render = async () => {
108
+ await ensureLoaded();
109
+
110
+ const width = tui.terminal?.columns ?? 80;
111
+ const height = tui.terminal?.rows ?? 24;
112
+ const lines: string[] = [];
113
+
114
+ // Header
115
+ const header = " 📋 Kanboard ";
116
+ const tabLine =
117
+ " [T]asks | [B]oard ".replace(
118
+ state.tab === "tasks" ? "[T]" : "T",
119
+ state.tab === "tasks" ? "▸T" : " T",
120
+ ).replace(
121
+ state.tab === "board" ? "[B]" : "B",
122
+ state.tab === "board" ? "▸B" : " B",
123
+ );
124
+ lines.push(truncateToWidth(header, width));
125
+ lines.push(truncateToWidth(tabLine, width));
126
+ lines.push("─".repeat(width));
127
+
128
+ if (state.tab === "tasks") {
129
+ renderTasksTab(lines, width, height - 5);
130
+ } else {
131
+ renderBoardTab(lines, width, height - 5);
132
+ }
133
+
134
+ // Footer
135
+ lines.push("─".repeat(width));
136
+ const footer = " j/k: navigate Tab: switch tab q/Esc: close ";
137
+ lines.push(truncateToWidth(footer, width));
138
+
139
+ (tui as unknown as { setContent(lines: string[]): void }).setContent(lines);
140
+ };
141
+
142
+ const renderTasksTab = (lines: string[], width: number, maxLines: number) => {
143
+ if (state.tasks.length === 0) {
144
+ lines.push(truncateToWidth(" No tasks found.", width));
145
+ return;
146
+ }
147
+
148
+ // Ensure index in range
149
+ state.taskIndex = Math.min(state.taskIndex, state.tasks.length - 1);
150
+ state.taskIndex = Math.max(0, state.taskIndex);
151
+
152
+ // Scroll to keep selected visible
153
+ if (state.taskIndex < state.taskScroll) state.taskScroll = state.taskIndex;
154
+ if (state.taskIndex >= state.taskScroll + maxLines) {
155
+ state.taskScroll = state.taskIndex - maxLines + 1;
156
+ }
157
+
158
+ const visible = state.tasks.slice(
159
+ state.taskScroll,
160
+ state.taskScroll + maxLines,
161
+ );
162
+
163
+ for (let i = 0; i < visible.length; i++) {
164
+ const item = visible[i];
165
+ const globalIdx = state.taskScroll + i;
166
+ const selected = globalIdx === state.taskIndex;
167
+ const prefix = selected ? " ▸ " : " ";
168
+ const icon = statusIcon(item.status);
169
+ const source = `[${item.sourceFile}]`;
170
+ const line = `${prefix}${icon} ${item.text} ${source}`;
171
+ lines.push(
172
+ truncateToWidth(
173
+ selected ? `\x1b[7m${padVisible(line, width)}\x1b[0m` : line,
174
+ width,
175
+ ),
176
+ );
177
+ }
178
+ };
179
+
180
+ const renderBoardTab = (lines: string[], width: number, maxLines: number) => {
181
+ const colWidth = Math.floor(width / 3);
182
+ const cols: Array<{ title: string; indices: number[]; colIdx: number }> = [
183
+ { title: "To Do", indices: state.boardColumns.todo, colIdx: 0 },
184
+ { title: "In Progress", indices: state.boardColumns.inProgress, colIdx: 1 },
185
+ { title: "Done", indices: state.boardColumns.done, colIdx: 2 },
186
+ ];
187
+
188
+ // Column headers
189
+ let headerLine = "";
190
+ for (const col of cols) {
191
+ const isActive = col.colIdx === state.boardCol;
192
+ const title = isActive ? `▸ ${col.title}` : ` ${col.title}`;
193
+ headerLine += padVisible(` ${title} (${col.indices.length})`, colWidth);
194
+ }
195
+ lines.push(truncateToWidth(headerLine, width));
196
+ lines.push("─".repeat(width));
197
+
198
+ // Column items
199
+ const maxItems = Math.max(
200
+ ...cols.map((c) => c.indices.length),
201
+ maxLines - 2,
202
+ );
203
+ for (let row = 0; row < Math.min(maxItems, maxLines - 2); row++) {
204
+ let rowLine = "";
205
+ for (const col of cols) {
206
+ const active = col.colIdx === state.boardCol;
207
+ const boardIdx = state.boardIndex[col.colIdx] ?? 0;
208
+ if (row < col.indices.length) {
209
+ const item = state.tasks[col.indices[row]];
210
+ const selected = active && row === boardIdx;
211
+ const icon = statusIcon(item.status);
212
+ const text = truncateToWidth(` ${icon} ${item.text}`, colWidth - 1);
213
+ if (selected) {
214
+ rowLine += `\x1b[7m${padVisible(text, colWidth)}\x1b[0m`;
215
+ } else {
216
+ rowLine += padVisible(text, colWidth);
217
+ }
218
+ } else {
219
+ rowLine += padVisible("", colWidth);
220
+ }
221
+ }
222
+ lines.push(truncateToWidth(rowLine, width));
223
+ }
224
+ };
225
+
226
+ const handleKey = async (key: string) => {
227
+ await ensureLoaded();
228
+
229
+ // Close
230
+ if (matchesKey(key, "q") || matchesKey(key, "escape")) {
231
+ done({ viewed: true });
232
+ return;
233
+ }
234
+
235
+ // Tab switching
236
+ if (matchesKey(key, "tab") || matchesKey(key, "b")) {
237
+ state.tab = state.tab === "tasks" ? "board" : "tasks";
238
+ render();
239
+ return;
240
+ }
241
+
242
+ if (matchesKey(key, "t")) {
243
+ state.tab = "tasks";
244
+ render();
245
+ return;
246
+ }
247
+
248
+ // Navigation
249
+ if (state.tab === "tasks") {
250
+ if (matchesKey(key, "j") || matchesKey(key, "down")) {
251
+ state.taskIndex = Math.min(state.taskIndex + 1, state.tasks.length - 1);
252
+ } else if (matchesKey(key, "k") || matchesKey(key, "up")) {
253
+ state.taskIndex = Math.max(state.taskIndex - 1, 0);
254
+ } else if (matchesKey(key, "g")) {
255
+ if (state.pendingG) {
256
+ state.taskIndex = 0;
257
+ state.pendingG = false;
258
+ } else {
259
+ state.pendingG = true;
260
+ setTimeout(() => {
261
+ state.pendingG = false;
262
+ }, 500);
263
+ }
264
+ } else if (matchesKey(key, "shift+g")) {
265
+ state.taskIndex = state.tasks.length - 1;
266
+ }
267
+ } else {
268
+ // Board navigation
269
+ const cols = [
270
+ state.boardColumns.todo,
271
+ state.boardColumns.inProgress,
272
+ state.boardColumns.done,
273
+ ];
274
+
275
+ if (matchesKey(key, "h") || matchesKey(key, "left")) {
276
+ state.boardCol = Math.max(0, state.boardCol - 1);
277
+ } else if (matchesKey(key, "l") || matchesKey(key, "right")) {
278
+ state.boardCol = Math.min(2, state.boardCol + 1);
279
+ } else if (matchesKey(key, "j") || matchesKey(key, "down")) {
280
+ const col = cols[state.boardCol];
281
+ state.boardIndex[state.boardCol] = Math.min(
282
+ state.boardIndex[state.boardCol] + 1,
283
+ col.length - 1,
284
+ );
285
+ } else if (matchesKey(key, "k") || matchesKey(key, "up")) {
286
+ state.boardIndex[state.boardCol] = Math.max(
287
+ state.boardIndex[state.boardCol] - 1,
288
+ 0,
289
+ );
290
+ }
291
+ }
292
+
293
+ render();
294
+ };
295
+
296
+ // Start
297
+ render();
298
+ (tui as unknown as { on(event: string, handler: (data: string) => void): void }).on("key", handleKey);
299
+ };
300
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/mcp",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
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",
@@ -27,7 +27,7 @@
27
27
  "README.md"
28
28
  ],
29
29
  "dependencies": {
30
- "@pi-unipi/core": "2.1.3"
30
+ "@pi-unipi/core": "2.2.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -31,6 +31,7 @@ import {
31
31
  getProjectConfigDir,
32
32
  } from "../config/manager.js";
33
33
  import { validateMcpConfig, DEFAULT_MCP_CONFIG, DEFAULT_METADATA } from "../config/schema.js";
34
+ import { boxInnerWidth, normalizeWidth, WidthKeyedCache } from "@pi-unipi/core";
34
35
 
35
36
  type Mode = "normal" | "search" | "editor";
36
37
  type StatusKind = "info" | "success" | "warn" | "error";
@@ -158,11 +159,12 @@ export function renderMcpAddOverlay(params?: {
158
159
  // Ctrl+S handles saving; no submit path needed.
159
160
  (editor as any).disableSubmit = true;
160
161
 
161
- let cachedLines: string[] | undefined;
162
+ // Width-keyed so a terminal resize can never serve stale, over-wide lines.
163
+ const lineCache = new WidthKeyedCache();
162
164
  let lastListHeight = 12;
163
165
 
164
166
  function refresh() {
165
- cachedLines = undefined;
167
+ lineCache.clear();
166
168
  tui.requestRender();
167
169
  }
168
170
 
@@ -537,11 +539,13 @@ export function renderMcpAddOverlay(params?: {
537
539
 
538
540
  // ─── Rendering ────────────────────────────────────────────────────
539
541
 
540
- function render(width: number): string[] {
541
- if (cachedLines) return cachedLines;
542
+ function render(rawWidth: number): string[] {
543
+ const width = normalizeWidth(rawWidth);
544
+ const cached = lineCache.get(width);
545
+ if (cached) return cached;
542
546
 
543
547
  const lines: string[] = [];
544
- const innerWidth = Math.max(40, width - 2);
548
+ const innerWidth = boxInnerWidth(width);
545
549
 
546
550
  // Pane widths: account for left │, middle │, right │ borders
547
551
  const leftW = Math.floor((innerWidth - 1) / 2);
@@ -776,8 +780,7 @@ export function renderMcpAddOverlay(params?: {
776
780
  );
777
781
  lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
778
782
 
779
- cachedLines = lines;
780
- return lines;
783
+ return lineCache.set(width, lines);
781
784
  }
782
785
 
783
786
  return { render, invalidate: refresh, handleInput };
@@ -16,6 +16,7 @@ import {
16
16
  getGlobalConfigDir,
17
17
  getProjectConfigDir,
18
18
  } from "../config/manager.js";
19
+ import { boxInnerWidth, normalizeWidth, WidthKeyedCache } from "@pi-unipi/core";
19
20
 
20
21
  /** Server display item */
21
22
  interface ServerDisplayItem {
@@ -65,7 +66,8 @@ export function renderMcpSettingsOverlay(params?: {
65
66
  confirmDelete: null,
66
67
  };
67
68
 
68
- let cachedLines: string[] | undefined;
69
+ // Width-keyed so a terminal resize can never serve stale, over-wide lines.
70
+ const lineCache = new WidthKeyedCache();
69
71
 
70
72
  function refreshServers() {
71
73
  const configDir =
@@ -115,7 +117,7 @@ export function renderMcpSettingsOverlay(params?: {
115
117
  refreshServers();
116
118
 
117
119
  function refresh() {
118
- cachedLines = undefined;
120
+ lineCache.clear();
119
121
  tui.requestRender();
120
122
  }
121
123
 
@@ -275,11 +277,13 @@ export function renderMcpSettingsOverlay(params?: {
275
277
  return content + " ".repeat(pad);
276
278
  }
277
279
 
278
- function render(width: number): string[] {
279
- if (cachedLines) return cachedLines;
280
+ function render(rawWidth: number): string[] {
281
+ const width = normalizeWidth(rawWidth);
282
+ const cached = lineCache.get(width);
283
+ if (cached) return cached;
280
284
 
281
285
  const lines: string[] = [];
282
- const innerWidth = Math.max(22, width - 2);
286
+ const innerWidth = boxInnerWidth(width);
283
287
 
284
288
  // ── Header ──────────────────────────────────────────────────────
285
289
  const header = " MCP Settings ";
@@ -356,8 +360,7 @@ export function renderMcpSettingsOverlay(params?: {
356
360
  );
357
361
  lines.push(theme.fg("accent", `╰${"─".repeat(innerWidth)}╯`));
358
362
 
359
- cachedLines = lines;
360
- return lines;
363
+ return lineCache.set(width, lines);
361
364
  }
362
365
 
363
366
  return { render, invalidate: refresh, handleInput };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/memory",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "Persistent cross-session memory with MemPalace backend (auto-installed) and SQLite fallback for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -43,8 +43,8 @@
43
43
  "better-sqlite3": "^12.9.0",
44
44
  "sqlite-vec": "^0.1.9",
45
45
  "js-yaml": "^4.1.0",
46
- "@pi-unipi/core": "2.1.3",
47
- "@pi-unipi/info-screen": "2.1.3"
46
+ "@pi-unipi/core": "2.2.0",
47
+ "@pi-unipi/info-screen": "2.2.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@earendil-works/pi-coding-agent": "^0.80.0",