@pi-unipi/unipi 2.4.0 → 2.4.2

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 (99) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +26 -22
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +4 -2
  5. package/packages/btw/package.json +2 -2
  6. package/packages/cocoindex/package.json +2 -2
  7. package/packages/cocoindex/tools.ts +0 -8
  8. package/packages/compactor/README.md +18 -22
  9. package/packages/compactor/package.json +3 -3
  10. package/packages/compactor/src/compaction/auto-trigger.ts +0 -0
  11. package/packages/compactor/src/compaction/build-sections.ts +2 -2
  12. package/packages/compactor/src/compaction/format.ts +1 -1
  13. package/packages/compactor/src/config/presets.ts +19 -30
  14. package/packages/compactor/src/config/schema.ts +0 -0
  15. package/packages/compactor/src/index.ts +39 -56
  16. package/packages/compactor/src/session/resume-inject.ts +27 -0
  17. package/packages/compactor/src/tools/ctx-batch-execute.ts +4 -2
  18. package/packages/compactor/src/tools/ctx-execute-file.ts +4 -2
  19. package/packages/compactor/src/tools/ctx-execute.ts +4 -2
  20. package/packages/compactor/src/tools/register.ts +22 -4
  21. package/packages/compactor/src/tui/settings-overlay.ts +9 -22
  22. package/packages/compactor/src/types.ts +24 -2
  23. package/packages/core/events.ts +13 -13
  24. package/packages/core/package.json +3 -2
  25. package/packages/footer/README.md +2 -2
  26. package/packages/footer/package.json +3 -3
  27. package/packages/footer/src/events.ts +0 -10
  28. package/packages/footer/src/segments/core.ts +0 -9
  29. package/packages/image/package.json +2 -2
  30. package/packages/info-screen/core-groups.ts +0 -14
  31. package/packages/info-screen/package.json +2 -2
  32. package/packages/input-shortcuts/package.json +2 -2
  33. package/packages/kanboard/commands.ts +0 -1
  34. package/packages/kanboard/package.json +2 -2
  35. package/packages/mcp/package.json +2 -2
  36. package/packages/memory/package.json +3 -3
  37. package/packages/memory/storage.ts +0 -18
  38. package/packages/memory/tools.ts +0 -1
  39. package/packages/milestone/hooks.ts +9 -15
  40. package/packages/milestone/package.json +2 -2
  41. package/packages/notify/README.md +2 -0
  42. package/packages/notify/events.ts +23 -13
  43. package/packages/notify/package.json +2 -2
  44. package/packages/notify/tools.ts +17 -8
  45. package/packages/notify/types.ts +5 -1
  46. package/packages/ralph/index.ts +10 -5
  47. package/packages/ralph/package.json +3 -3
  48. package/packages/ralph/ralph-loop.ts +10 -0
  49. package/packages/subagents/README.md +3 -1
  50. package/packages/subagents/package.json +10 -6
  51. package/packages/subagents/src/agent-manager.ts +46 -11
  52. package/packages/subagents/src/core-compat.ts +38 -0
  53. package/packages/subagents/src/global.d.ts +7 -0
  54. package/packages/subagents/src/index.ts +6 -3
  55. package/packages/subagents/src/widget.ts +0 -7
  56. package/packages/unipi/bundled.js +371 -438
  57. package/packages/updater/package.json +4 -3
  58. package/packages/utility/README.md +1 -1
  59. package/packages/utility/package.json +7 -4
  60. package/packages/utility/src/commands.ts +2 -10
  61. package/packages/utility/src/diff/theme.ts +0 -3
  62. package/packages/utility/src/tui/name-badge.ts +0 -9
  63. package/packages/web-api/package.json +2 -2
  64. package/packages/web-api/src/engine/extract.ts +0 -26
  65. package/packages/web-api/src/tools.ts +0 -13
  66. package/packages/workflow/commands.ts +13 -1
  67. package/packages/workflow/index.ts +12 -1
  68. package/packages/workflow/lifecycle.ts +43 -0
  69. package/packages/workflow/package.json +3 -2
  70. package/packages/autocomplete/index.ts +0 -7
  71. package/packages/autocomplete/src/__tests__/command-registry.audit.test.ts +0 -120
  72. package/packages/autocomplete/src/__tests__/provider.sorting.test.ts +0 -423
  73. package/packages/compactor/src/compaction/recall-scope.ts +0 -32
  74. package/packages/compactor/src/compaction/sections.ts +0 -15
  75. package/packages/compactor/src/display/bash-display.ts +0 -28
  76. package/packages/compactor/src/display/diff-presentation.ts +0 -25
  77. package/packages/compactor/src/display/pending-diff-preview.ts +0 -51
  78. package/packages/compactor/src/display/user-message-box.ts +0 -16
  79. package/packages/compactor/src/session/analytics.ts +0 -205
  80. package/packages/image/index.ts +0 -1
  81. package/packages/input-shortcuts/index.ts +0 -5
  82. package/packages/input-shortcuts/src/status.ts +0 -35
  83. package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
  84. package/packages/mcp/index.ts +0 -1
  85. package/packages/memory/search.ts +0 -134
  86. package/packages/milestone/coexist.ts +0 -114
  87. package/packages/notify/src/__tests__/ask-user-prompt-message.test.ts +0 -159
  88. package/packages/notify/src/__tests__/event-bus.test.ts +0 -119
  89. package/packages/notify/src/__tests__/permission-prompt-message.test.ts +0 -172
  90. package/packages/subagents/src/__tests__/badge-generation.test.ts +0 -315
  91. package/packages/subagents/src/__tests__/config.test.ts +0 -240
  92. package/packages/subagents/src/__tests__/esc-propagation.test.ts +0 -162
  93. package/packages/subagents/src/__tests__/file-lock.test.ts +0 -244
  94. package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
  95. package/packages/subagents/src/__tests__/workflow-integration.test.ts +0 -334
  96. package/packages/subagents/src/prompts.ts +0 -39
  97. package/packages/utility/index.ts +0 -1
  98. package/packages/web-api/src/tui/progress.ts +0 -168
  99. package/packages/web-api/src/tui/result.ts +0 -173
@@ -1,205 +0,0 @@
1
- /**
2
- * AnalyticsEngine — Runtime savings + session continuity reporting.
3
- *
4
- * Ported from context-mode's AnalyticsEngine, trimmed to budget-focused stats.
5
- * Omits: formatReport(), categoryLabels, categoryHints, ThinkInCodeComparison,
6
- * SandboxIO, dataBar(), visual formatting helpers.
7
- *
8
- * Usage:
9
- * const engine = new AnalyticsEngine(sessionDb);
10
- * const report = engine.queryAll(runtimeStats);
11
- */
12
-
13
- // ─────────────────────────────────────────────────────────
14
- // Types
15
- // ─────────────────────────────────────────────────────────
16
-
17
- /** Database adapter — anything with a prepare() method (better-sqlite3, bun:sqlite, etc.) */
18
- export interface DatabaseAdapter {
19
- prepare(sql: string): {
20
- run(...params: unknown[]): unknown;
21
- get(...params: unknown[]): unknown;
22
- all(...params: unknown[]): unknown[];
23
- };
24
- }
25
-
26
- /** Context savings result */
27
- export interface ContextSavings {
28
- rawBytes: number;
29
- contextBytes: number;
30
- savedBytes: number;
31
- savedPercent: number;
32
- }
33
-
34
- /** Runtime stats tracked during a live session. */
35
- export interface RuntimeStats {
36
- bytesReturned: Record<string, number>;
37
- bytesIndexed: number;
38
- bytesSandboxed: number;
39
- calls: Record<string, number>;
40
- sessionStart: number;
41
- cacheHits: number;
42
- cacheBytesSaved: number;
43
- }
44
-
45
- /** Unified report combining runtime stats, DB analytics, and continuity data. */
46
- export interface FullReport {
47
- /** Runtime context savings */
48
- savings: {
49
- processed_kb: number;
50
- entered_kb: number;
51
- saved_kb: number;
52
- pct: number;
53
- savings_ratio: number;
54
- by_tool: Array<{ tool: string; calls: number; context_kb: number; tokens: number }>;
55
- total_calls: number;
56
- total_bytes_returned: number;
57
- kept_out: number;
58
- total_processed: number;
59
- };
60
- /** Session metadata from SessionDB */
61
- session: {
62
- id: string;
63
- uptime_min: string;
64
- };
65
- /** Session continuity data */
66
- continuity: {
67
- total_events: number;
68
- compact_count: number;
69
- all_time_compact_count: number;
70
- resume_ready: boolean;
71
- };
72
- /** Persistent project memory — all events across all sessions */
73
- projectMemory: {
74
- total_events: number;
75
- session_count: number;
76
- };
77
- }
78
-
79
- // ─────────────────────────────────────────────────────────
80
- // AnalyticsEngine
81
- // ─────────────────────────────────────────────────────────
82
-
83
- export class AnalyticsEngine {
84
- private readonly db: DatabaseAdapter;
85
-
86
- constructor(db: DatabaseAdapter) {
87
- this.db = db;
88
- }
89
-
90
- /**
91
- * Build a FullReport by merging runtime stats (passed in)
92
- * with continuity data from the DB.
93
- */
94
- queryAll(runtimeStats: RuntimeStats): FullReport {
95
- // ── Resolve latest session ID ──
96
- const latestSession = this.db.prepare(
97
- "SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1",
98
- ).get() as { session_id: string } | undefined;
99
- const sid = latestSession?.session_id ?? "";
100
-
101
- // ── Runtime savings ──
102
- const totalBytesReturned = Object.values(runtimeStats.bytesReturned).reduce(
103
- (sum, b) => sum + b, 0,
104
- );
105
- const totalCalls = Object.values(runtimeStats.calls).reduce(
106
- (sum, c) => sum + c, 0,
107
- );
108
- const keptOut = runtimeStats.bytesIndexed + runtimeStats.bytesSandboxed;
109
- const totalProcessed = keptOut + totalBytesReturned;
110
- const savingsRatio = totalProcessed / Math.max(totalBytesReturned, 1);
111
- const reductionPct = totalProcessed > 0
112
- ? Math.round((1 - totalBytesReturned / totalProcessed) * 100)
113
- : 0;
114
-
115
- const toolNames = new Set([
116
- ...Object.keys(runtimeStats.calls),
117
- ...Object.keys(runtimeStats.bytesReturned),
118
- ]);
119
- const byTool = Array.from(toolNames).sort().map((tool) => ({
120
- tool,
121
- calls: runtimeStats.calls[tool] || 0,
122
- context_kb: Math.round((runtimeStats.bytesReturned[tool] || 0) / 1024 * 10) / 10,
123
- tokens: Math.round((runtimeStats.bytesReturned[tool] || 0) / 4),
124
- }));
125
-
126
- const uptimeMs = Date.now() - runtimeStats.sessionStart;
127
- const uptimeMin = (uptimeMs / 60_000).toFixed(1);
128
-
129
- // ── Continuity data (scoped to current session) ──
130
- const eventTotal = (this.db.prepare(
131
- "SELECT COUNT(*) as cnt FROM session_events WHERE session_id = ?",
132
- ).get(sid) as { cnt: number }).cnt;
133
-
134
- const meta = this.db.prepare(
135
- "SELECT compact_count FROM session_meta WHERE session_id = ?",
136
- ).get(sid) as { compact_count: number } | undefined;
137
- const compactCount = meta?.compact_count ?? 0;
138
-
139
- // ── All-time compaction count across all sessions ──
140
- const allTimeCompactions = (this.db.prepare(
141
- "SELECT COALESCE(SUM(compact_count), 0) as total FROM session_meta",
142
- ).get() as { total: number }).total;
143
-
144
- const resume = this.db.prepare(
145
- "SELECT event_count, consumed FROM session_resume WHERE session_id = ? ORDER BY created_at DESC LIMIT 1",
146
- ).get(sid) as { event_count: number; consumed: number } | undefined;
147
- const resumeReady = resume ? !resume.consumed : false;
148
-
149
- // ── Project-wide persistent memory (all sessions, no session_id filter) ──
150
- const projectTotals = this.db.prepare(
151
- "SELECT COUNT(*) as cnt, COUNT(DISTINCT session_id) as sessions FROM session_events",
152
- ).get() as { cnt: number; sessions: number };
153
-
154
- return {
155
- savings: {
156
- processed_kb: Math.round(totalProcessed / 1024 * 10) / 10,
157
- entered_kb: Math.round(totalBytesReturned / 1024 * 10) / 10,
158
- saved_kb: Math.round(keptOut / 1024 * 10) / 10,
159
- pct: reductionPct,
160
- savings_ratio: Math.round(savingsRatio * 10) / 10,
161
- by_tool: byTool,
162
- total_calls: totalCalls,
163
- total_bytes_returned: totalBytesReturned,
164
- kept_out: keptOut,
165
- total_processed: totalProcessed,
166
- },
167
- session: {
168
- id: sid,
169
- uptime_min: uptimeMin,
170
- },
171
- continuity: {
172
- total_events: eventTotal,
173
- compact_count: compactCount,
174
- all_time_compact_count: allTimeCompactions,
175
- resume_ready: resumeReady,
176
- },
177
- projectMemory: {
178
- total_events: projectTotals.cnt,
179
- session_count: projectTotals.sessions,
180
- },
181
- };
182
- }
183
- }
184
-
185
- // ─────────────────────────────────────────────────────────
186
- // createMinimalDb — in-memory SQLite fallback
187
- // ─────────────────────────────────────────────────────────
188
-
189
- /**
190
- * Create a minimal in-memory DatabaseAdapter for when SessionDB is unavailable.
191
- * Returns zeroed/empty results for all queries.
192
- */
193
- export function createMinimalDb(): DatabaseAdapter {
194
- // Use an in-memory SQLite database with the expected schema
195
- // so AnalyticsEngine queries don't fail.
196
- const emptyStmt = {
197
- run: (..._params: unknown[]) => {},
198
- get: (..._params: unknown[]) => ({ cnt: 0, sessions: 0, compact_count: 0, total: 0, session_id: "", event_count: 0, consumed: 1 }),
199
- all: (..._params: unknown[]) => [] as unknown[],
200
- };
201
-
202
- return {
203
- prepare: (_sql: string) => emptyStmt,
204
- };
205
- }
@@ -1 +0,0 @@
1
- export { default } from "./src/index.js";
@@ -1,5 +0,0 @@
1
- /**
2
- * @pi-unipi/input-shortcuts — Re-exports
3
- */
4
-
5
- export { default } from "./src/index.ts";
@@ -1,35 +0,0 @@
1
- /**
2
- * Status bar feedback helper using ctx.ui.setStatus() with auto-clear.
3
- */
4
-
5
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
- import { STATUS_SUCCESS_MS, STATUS_ERROR_MS } from "./types.ts";
7
-
8
- const STATUS_KEY = "input-shortcuts";
9
-
10
- /** Show a success status message with auto-clear. */
11
- export function showSuccess(ctx: ExtensionContext, text: string, durationMs = STATUS_SUCCESS_MS): void {
12
- showStatus(ctx, text, durationMs);
13
- }
14
-
15
- /** Show an error status message with auto-clear. */
16
- export function showError(ctx: ExtensionContext, text: string, durationMs = STATUS_ERROR_MS): void {
17
- showStatus(ctx, text, durationMs);
18
- }
19
-
20
- /** Show status text with auto-clear after duration. */
21
- export function showStatus(ctx: ExtensionContext, text: string, durationMs: number): void {
22
- ctx.ui.setStatus(STATUS_KEY, text);
23
- setTimeout(() => {
24
- try {
25
- ctx.ui.setStatus(STATUS_KEY, undefined);
26
- } catch {
27
- // Context may be gone — ignore
28
- }
29
- }, durationMs);
30
- }
31
-
32
- /** Clear the status bar entry immediately. */
33
- export function clearStatus(ctx: ExtensionContext): void {
34
- ctx.ui.setStatus(STATUS_KEY, undefined);
35
- }
@@ -1,300 +0,0 @@
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 +0,0 @@
1
- export { default } from "./src/index.js";
@@ -1,134 +0,0 @@
1
- /**
2
- * @unipi/memory — Hybrid search algorithm
3
- *
4
- * Combines vector similarity search with fuzzy text matching
5
- * for best recall across semantic and exact matches.
6
- */
7
-
8
- import type { MemoryStorage, MemoryRecord, SearchResult } from "./storage.js";
9
-
10
- /**
11
- * Perform hybrid search combining vector + fuzzy.
12
- */
13
- export function hybridSearch(
14
- storage: MemoryStorage,
15
- query: string,
16
- limit = 10,
17
- embedding?: Float32Array | null
18
- ): SearchResult[] {
19
- // Delegate to storage's search method which already implements hybrid
20
- return storage.search(query, limit, embedding);
21
- }
22
-
23
- /**
24
- * Calculate fuzzy match score between text and query.
25
- * Returns 0-1 score (1 = perfect match).
26
- */
27
- export function fuzzyMatch(text: string, query: string): number {
28
- const lowerText = text.toLowerCase();
29
- const lowerQuery = query.toLowerCase();
30
-
31
- // Exact match
32
- if (lowerText === lowerQuery) return 1.0;
33
-
34
- // Starts with
35
- if (lowerText.startsWith(lowerQuery)) return 0.9;
36
-
37
- // Contains
38
- if (lowerText.includes(lowerQuery)) return 0.7;
39
-
40
- // Word boundary match
41
- const words = lowerQuery.split(/\s+/);
42
- let matchedWords = 0;
43
- for (const word of words) {
44
- if (lowerText.includes(word)) {
45
- matchedWords++;
46
- }
47
- }
48
- if (matchedWords > 0) {
49
- return 0.3 + (matchedWords / words.length) * 0.4;
50
- }
51
-
52
- // Subsequence match
53
- let textIdx = 0;
54
- let queryIdx = 0;
55
- let subsequenceMatches = 0;
56
-
57
- while (textIdx < lowerText.length && queryIdx < lowerQuery.length) {
58
- if (lowerText[textIdx] === lowerQuery[queryIdx]) {
59
- subsequenceMatches++;
60
- queryIdx++;
61
- }
62
- textIdx++;
63
- }
64
-
65
- if (queryIdx === lowerQuery.length) {
66
- // All query chars found in order
67
- return 0.2 + (subsequenceMatches / lowerQuery.length) * 0.2;
68
- }
69
-
70
- return 0;
71
- }
72
-
73
- /**
74
- * Extract a snippet around the query match.
75
- */
76
- export function extractSnippet(
77
- content: string,
78
- query: string,
79
- chars = 150
80
- ): string {
81
- const lowerContent = content.toLowerCase();
82
- const lowerQuery = query.toLowerCase();
83
-
84
- // Find best match position
85
- let bestIdx = -1;
86
- let bestScore = 0;
87
-
88
- const words = lowerQuery.split(/\s+/);
89
- for (const word of words) {
90
- const idx = lowerContent.indexOf(word);
91
- if (idx !== -1 && (bestIdx === -1 || idx < bestIdx)) {
92
- bestIdx = idx;
93
- bestScore = 0.8;
94
- }
95
- }
96
-
97
- if (bestIdx === -1) {
98
- // No match, return beginning
99
- return content.slice(0, chars) + (content.length > chars ? "..." : "");
100
- }
101
-
102
- const start = Math.max(0, bestIdx - chars / 3);
103
- const end = Math.min(content.length, bestIdx + chars * 2 / 3);
104
- let snippet = content.slice(start, end);
105
-
106
- if (start > 0) snippet = "..." + snippet;
107
- if (end < content.length) snippet = snippet + "...";
108
-
109
- return snippet;
110
- }
111
-
112
- /**
113
- * Merge and deduplicate search results from multiple sources.
114
- */
115
- export function mergeResults(
116
- ...resultSets: SearchResult[][]
117
- ): SearchResult[] {
118
- const merged = new Map<string, SearchResult>();
119
-
120
- for (const results of resultSets) {
121
- for (const result of results) {
122
- const existing = merged.get(result.record.id);
123
- if (existing) {
124
- // Boost score if found in multiple sources
125
- existing.score = Math.min(existing.score + result.score * 0.2, 1);
126
- } else {
127
- merged.set(result.record.id, { ...result });
128
- }
129
- }
130
- }
131
-
132
- return Array.from(merged.values())
133
- .sort((a, b) => b.score - a.score);
134
- }