@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.
- package/CHANGELOG.md +43 -0
- package/package.json +26 -22
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +4 -2
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/package.json +2 -2
- package/packages/cocoindex/tools.ts +0 -8
- package/packages/compactor/README.md +18 -22
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/compaction/auto-trigger.ts +0 -0
- package/packages/compactor/src/compaction/build-sections.ts +2 -2
- package/packages/compactor/src/compaction/format.ts +1 -1
- package/packages/compactor/src/config/presets.ts +19 -30
- package/packages/compactor/src/config/schema.ts +0 -0
- package/packages/compactor/src/index.ts +39 -56
- package/packages/compactor/src/session/resume-inject.ts +27 -0
- package/packages/compactor/src/tools/ctx-batch-execute.ts +4 -2
- package/packages/compactor/src/tools/ctx-execute-file.ts +4 -2
- package/packages/compactor/src/tools/ctx-execute.ts +4 -2
- package/packages/compactor/src/tools/register.ts +22 -4
- package/packages/compactor/src/tui/settings-overlay.ts +9 -22
- package/packages/compactor/src/types.ts +24 -2
- package/packages/core/events.ts +13 -13
- package/packages/core/package.json +3 -2
- package/packages/footer/README.md +2 -2
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/events.ts +0 -10
- package/packages/footer/src/segments/core.ts +0 -9
- package/packages/image/package.json +2 -2
- package/packages/info-screen/core-groups.ts +0 -14
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/commands.ts +0 -1
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/memory/storage.ts +0 -18
- package/packages/memory/tools.ts +0 -1
- package/packages/milestone/hooks.ts +9 -15
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +2 -0
- package/packages/notify/events.ts +23 -13
- package/packages/notify/package.json +2 -2
- package/packages/notify/tools.ts +17 -8
- package/packages/notify/types.ts +5 -1
- package/packages/ralph/index.ts +10 -5
- package/packages/ralph/package.json +3 -3
- package/packages/ralph/ralph-loop.ts +10 -0
- package/packages/subagents/README.md +3 -1
- package/packages/subagents/package.json +10 -6
- package/packages/subagents/src/agent-manager.ts +46 -11
- package/packages/subagents/src/core-compat.ts +38 -0
- package/packages/subagents/src/global.d.ts +7 -0
- package/packages/subagents/src/index.ts +6 -3
- package/packages/subagents/src/widget.ts +0 -7
- package/packages/unipi/bundled.js +371 -438
- package/packages/updater/package.json +4 -3
- package/packages/utility/README.md +1 -1
- package/packages/utility/package.json +7 -4
- package/packages/utility/src/commands.ts +2 -10
- package/packages/utility/src/diff/theme.ts +0 -3
- package/packages/utility/src/tui/name-badge.ts +0 -9
- package/packages/web-api/package.json +2 -2
- package/packages/web-api/src/engine/extract.ts +0 -26
- package/packages/web-api/src/tools.ts +0 -13
- package/packages/workflow/commands.ts +13 -1
- package/packages/workflow/index.ts +12 -1
- package/packages/workflow/lifecycle.ts +43 -0
- package/packages/workflow/package.json +3 -2
- package/packages/autocomplete/index.ts +0 -7
- package/packages/autocomplete/src/__tests__/command-registry.audit.test.ts +0 -120
- package/packages/autocomplete/src/__tests__/provider.sorting.test.ts +0 -423
- package/packages/compactor/src/compaction/recall-scope.ts +0 -32
- package/packages/compactor/src/compaction/sections.ts +0 -15
- package/packages/compactor/src/display/bash-display.ts +0 -28
- package/packages/compactor/src/display/diff-presentation.ts +0 -25
- package/packages/compactor/src/display/pending-diff-preview.ts +0 -51
- package/packages/compactor/src/display/user-message-box.ts +0 -16
- package/packages/compactor/src/session/analytics.ts +0 -205
- package/packages/image/index.ts +0 -1
- package/packages/input-shortcuts/index.ts +0 -5
- package/packages/input-shortcuts/src/status.ts +0 -35
- package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
- package/packages/mcp/index.ts +0 -1
- package/packages/memory/search.ts +0 -134
- package/packages/milestone/coexist.ts +0 -114
- package/packages/notify/src/__tests__/ask-user-prompt-message.test.ts +0 -159
- package/packages/notify/src/__tests__/event-bus.test.ts +0 -119
- package/packages/notify/src/__tests__/permission-prompt-message.test.ts +0 -172
- package/packages/subagents/src/__tests__/badge-generation.test.ts +0 -315
- package/packages/subagents/src/__tests__/config.test.ts +0 -240
- package/packages/subagents/src/__tests__/esc-propagation.test.ts +0 -162
- package/packages/subagents/src/__tests__/file-lock.test.ts +0 -244
- package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
- package/packages/subagents/src/__tests__/workflow-integration.test.ts +0 -334
- package/packages/subagents/src/prompts.ts +0 -39
- package/packages/utility/index.ts +0 -1
- package/packages/web-api/src/tui/progress.ts +0 -168
- 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
|
-
}
|
package/packages/image/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default } from "./src/index.js";
|
|
@@ -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
|
-
}
|
package/packages/mcp/index.ts
DELETED
|
@@ -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
|
-
}
|