memorix 1.1.7 → 1.1.8
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 +13 -0
- package/CLAUDE.md +6 -1
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +730 -150
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/index.html +201 -201
- package/dist/dashboard/static/style.css +3584 -3584
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +13 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +15 -8
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/SETUP.md +10 -0
- package/docs/dev-log/progress.txt +35 -26
- package/package.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/agent-integrations.ts +623 -0
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/cleanup.ts +255 -255
- package/src/cli/commands/doctor.ts +23 -0
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +66 -66
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +73 -73
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +96 -96
- package/src/cli/commands/message.ts +121 -121
- package/src/cli/commands/poll.ts +70 -70
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +132 -132
- package/src/cli/commands/repair.ts +60 -0
- package/src/cli/commands/retention.ts +108 -108
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/skills.ts +123 -123
- package/src/cli/commands/task.ts +192 -192
- package/src/cli/commands/transfer.ts +73 -73
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Panels.tsx +632 -632
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/chat-service.ts +742 -742
- package/src/cli/tui/data.ts +547 -547
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/theme.ts +178 -178
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/context-pack.ts +5 -1
- package/src/codegraph/lite-provider.ts +5 -1
- package/src/codegraph/project-context.ts +5 -1
- package/src/compact/token-budget.ts +74 -74
- package/src/config/behavior.ts +59 -59
- package/src/dashboard/project-classification.ts +64 -64
- package/src/dashboard/static/index.html +201 -201
- package/src/dashboard/static/style.css +3584 -3584
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/git/noise-filter.ts +210 -210
- package/src/hooks/installers/index.ts +4 -4
- package/src/hooks/official-skills.ts +1 -1
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/rules/memorix-agent-rules.md +2 -2
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/auto-relations.ts +107 -107
- package/src/memory/consolidation.ts +302 -302
- package/src/memory/disclosure-policy.ts +141 -141
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/retention.ts +433 -433
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +523 -523
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +380 -380
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/verify-gate.ts +219 -219
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/sdk.ts +327 -327
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/file-lock.ts +100 -100
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +255 -255
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/session-store.ts +259 -259
- package/src/store/sqlite-store.ts +339 -339
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +167 -167
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/wiki/generator.ts +237 -237
- package/src/wiki/knowledge-graph.ts +334 -334
- package/src/wiki/types.ts +85 -85
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- package/src/workspace/workflow-sync.ts +131 -131
package/src/cli/tui/data.ts
CHANGED
|
@@ -1,547 +1,547 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memorix TUI data layer.
|
|
3
|
-
*
|
|
4
|
-
* Pure data functions that return structured results without console output.
|
|
5
|
-
* Used by Ink components to populate panels.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from 'node:fs';
|
|
9
|
-
|
|
10
|
-
export interface ProjectInfo {
|
|
11
|
-
name: string;
|
|
12
|
-
id: string;
|
|
13
|
-
rootPath: string;
|
|
14
|
-
gitRemote: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface HealthInfo {
|
|
18
|
-
embeddingProvider: 'ready' | 'unavailable' | 'disabled';
|
|
19
|
-
embeddingProviderName?: string;
|
|
20
|
-
embeddingLabel: string;
|
|
21
|
-
searchMode: string;
|
|
22
|
-
searchModeLabel: string;
|
|
23
|
-
searchDiagnostic: string;
|
|
24
|
-
backfillPending: number;
|
|
25
|
-
totalMemories: number;
|
|
26
|
-
activeMemories: number;
|
|
27
|
-
sessions: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface MemoryItem {
|
|
31
|
-
id: number;
|
|
32
|
-
title: string;
|
|
33
|
-
type: string;
|
|
34
|
-
createdAt: string;
|
|
35
|
-
entityName: string;
|
|
36
|
-
status?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface BackgroundInfo {
|
|
40
|
-
running: boolean;
|
|
41
|
-
healthy: boolean;
|
|
42
|
-
pid?: number;
|
|
43
|
-
port?: number;
|
|
44
|
-
startedAt?: string;
|
|
45
|
-
dashboard?: string;
|
|
46
|
-
mcp?: string;
|
|
47
|
-
agents?: number;
|
|
48
|
-
sessions?: number;
|
|
49
|
-
uptime?: string;
|
|
50
|
-
message?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface SearchResult {
|
|
54
|
-
id: number;
|
|
55
|
-
title: string;
|
|
56
|
-
type: string;
|
|
57
|
-
score: number;
|
|
58
|
-
entityName: string;
|
|
59
|
-
icon: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface DoctorResult {
|
|
63
|
-
sections: DoctorSection[];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface DoctorSection {
|
|
67
|
-
title: string;
|
|
68
|
-
items: { label: string; value: string; status: 'ok' | 'warn' | 'error' | 'info' }[];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function formatSearchModeLabel(mode: string): string {
|
|
72
|
-
const normalized = (mode || '').toLowerCase();
|
|
73
|
-
if (!normalized) return 'Unknown';
|
|
74
|
-
if (normalized.includes('hybrid') && normalized.includes('rerank')) return 'Hybrid + rerank';
|
|
75
|
-
if (normalized.includes('hybrid')) return 'Hybrid';
|
|
76
|
-
if (normalized.includes('vector-only')) return 'Vector fallback';
|
|
77
|
-
if (normalized.includes('vector')) return 'Vector';
|
|
78
|
-
if (normalized.includes('fulltext')) return 'BM25 full-text';
|
|
79
|
-
return mode;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function formatEmbeddingLabel(
|
|
83
|
-
status: HealthInfo['embeddingProvider'],
|
|
84
|
-
providerName?: string,
|
|
85
|
-
): string {
|
|
86
|
-
if (status === 'disabled') return 'Disabled';
|
|
87
|
-
if (status === 'unavailable') return 'Unavailable';
|
|
88
|
-
if ((providerName || '').startsWith('api-')) return 'API ready';
|
|
89
|
-
if (providerName) return 'Local ready';
|
|
90
|
-
return 'Ready';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function truncate(text: string, max = 60): string {
|
|
94
|
-
if (text.length <= max) return text;
|
|
95
|
-
return `${text.slice(0, max)}...`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function getProjectInfo(): Promise<ProjectInfo | null> {
|
|
99
|
-
try {
|
|
100
|
-
const { detectProject } = await import('../../project/detector.js');
|
|
101
|
-
const proj = detectProject(process.cwd());
|
|
102
|
-
if (!proj) return null;
|
|
103
|
-
return {
|
|
104
|
-
name: proj.name,
|
|
105
|
-
id: proj.id,
|
|
106
|
-
rootPath: proj.rootPath,
|
|
107
|
-
gitRemote: proj.gitRemote || 'none',
|
|
108
|
-
};
|
|
109
|
-
} catch {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export async function getHealthInfo(projectId?: string): Promise<HealthInfo> {
|
|
115
|
-
const defaults: HealthInfo = {
|
|
116
|
-
embeddingProvider: 'disabled',
|
|
117
|
-
embeddingProviderName: undefined,
|
|
118
|
-
embeddingLabel: 'Disabled',
|
|
119
|
-
searchMode: 'fulltext',
|
|
120
|
-
searchModeLabel: 'BM25 full-text',
|
|
121
|
-
searchDiagnostic: '',
|
|
122
|
-
backfillPending: 0,
|
|
123
|
-
totalMemories: 0,
|
|
124
|
-
activeMemories: 0,
|
|
125
|
-
sessions: 0,
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const { detectProject } = await import('../../project/detector.js');
|
|
130
|
-
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
131
|
-
const { initObservationStore: initStore, getObservationStore: getStore } = await import('../../store/obs-store.js');
|
|
132
|
-
|
|
133
|
-
const proj = detectProject(process.cwd());
|
|
134
|
-
if (!proj) return defaults;
|
|
135
|
-
|
|
136
|
-
// Load .env BEFORE any process.env reads or provider initialization (#46)
|
|
137
|
-
try {
|
|
138
|
-
const { loadDotenv } = await import('../../config/dotenv-loader.js');
|
|
139
|
-
loadDotenv(proj.rootPath);
|
|
140
|
-
} catch { /* best-effort */ }
|
|
141
|
-
|
|
142
|
-
const effectiveProjectId = projectId || proj.id;
|
|
143
|
-
const dataDir = await getProjectDataDir(effectiveProjectId);
|
|
144
|
-
await initStore(dataDir);
|
|
145
|
-
const allObs = (await getStore().loadAll()) as any[];
|
|
146
|
-
// Filter by project — flat storage shares one observations.json across projects
|
|
147
|
-
const obs = allObs.filter((o: any) => o.projectId === effectiveProjectId);
|
|
148
|
-
const active = obs.filter((o: any) => (o.status ?? 'active') === 'active');
|
|
149
|
-
|
|
150
|
-
defaults.totalMemories = obs.length;
|
|
151
|
-
defaults.activeMemories = active.length;
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const { initSessionStore, getSessionStore } = await import('../../store/session-store.js');
|
|
155
|
-
await initSessionStore(dataDir);
|
|
156
|
-
const sessStore = getSessionStore();
|
|
157
|
-
const allSessions = await sessStore.loadAll() as any[];
|
|
158
|
-
defaults.sessions = allSessions.filter((s: any) => s.projectId === effectiveProjectId).length;
|
|
159
|
-
} catch {
|
|
160
|
-
// Ignore unreadable session data and keep defaults.
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const { getEmbeddingProvider, isEmbeddingExplicitlyDisabled } = await import('../../embedding/provider.js');
|
|
165
|
-
if (isEmbeddingExplicitlyDisabled()) {
|
|
166
|
-
defaults.embeddingProvider = 'disabled';
|
|
167
|
-
} else {
|
|
168
|
-
const provider = await getEmbeddingProvider();
|
|
169
|
-
defaults.embeddingProvider = provider ? 'ready' : 'unavailable';
|
|
170
|
-
defaults.embeddingProviderName = provider?.name;
|
|
171
|
-
}
|
|
172
|
-
defaults.embeddingLabel = formatEmbeddingLabel(
|
|
173
|
-
defaults.embeddingProvider,
|
|
174
|
-
defaults.embeddingProviderName,
|
|
175
|
-
);
|
|
176
|
-
} catch {
|
|
177
|
-
defaults.embeddingProvider = 'unavailable';
|
|
178
|
-
defaults.embeddingLabel = 'Unavailable';
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
const { getLastSearchMode, isEmbeddingEnabled } = await import('../../store/orama-store.js');
|
|
183
|
-
const vectorActive = isEmbeddingEnabled();
|
|
184
|
-
defaults.searchMode = getLastSearchMode(effectiveProjectId);
|
|
185
|
-
defaults.searchModeLabel = formatSearchModeLabel(defaults.searchMode);
|
|
186
|
-
|
|
187
|
-
// Build diagnostic explanation
|
|
188
|
-
if (defaults.searchMode.includes('hybrid')) {
|
|
189
|
-
defaults.searchDiagnostic = 'Hybrid search active';
|
|
190
|
-
} else if (defaults.searchMode.includes('vector')) {
|
|
191
|
-
defaults.searchDiagnostic = 'Vector fallback (BM25 empty)';
|
|
192
|
-
} else if (defaults.searchMode.includes('rerank')) {
|
|
193
|
-
defaults.searchDiagnostic = 'LLM reranking active';
|
|
194
|
-
} else if (defaults.searchMode.includes('embedding unavailable')) {
|
|
195
|
-
defaults.searchDiagnostic = 'Embedding unavailable';
|
|
196
|
-
} else if (defaults.embeddingProvider === 'ready' && vectorActive) {
|
|
197
|
-
defaults.searchDiagnostic = 'Vector index ready';
|
|
198
|
-
} else if (defaults.embeddingProvider === 'ready' && !vectorActive) {
|
|
199
|
-
defaults.searchDiagnostic = 'Provider ready, no index yet';
|
|
200
|
-
} else if (defaults.embeddingProvider === 'unavailable') {
|
|
201
|
-
defaults.searchDiagnostic = 'BM25 only (no provider)';
|
|
202
|
-
} else {
|
|
203
|
-
defaults.searchDiagnostic = 'BM25 only (disabled)';
|
|
204
|
-
}
|
|
205
|
-
} catch {
|
|
206
|
-
defaults.searchMode = 'fulltext';
|
|
207
|
-
defaults.searchModeLabel = 'BM25 full-text';
|
|
208
|
-
defaults.searchDiagnostic = 'Could not determine search mode';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return defaults;
|
|
212
|
-
} catch {
|
|
213
|
-
return defaults;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export async function getRecentMemories(limit = 8, projectId?: string): Promise<MemoryItem[]> {
|
|
218
|
-
try {
|
|
219
|
-
const { detectProject } = await import('../../project/detector.js');
|
|
220
|
-
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
221
|
-
const { initObservationStore: initStore, getObservationStore: getStore } = await import('../../store/obs-store.js');
|
|
222
|
-
|
|
223
|
-
const proj = detectProject(process.cwd());
|
|
224
|
-
if (!proj) return [];
|
|
225
|
-
|
|
226
|
-
const effectiveProjectId = projectId || proj.id;
|
|
227
|
-
const dataDir = await getProjectDataDir(effectiveProjectId);
|
|
228
|
-
await initStore(dataDir);
|
|
229
|
-
const allObs = (await getStore().loadAll()) as any[];
|
|
230
|
-
// Filter by project — flat storage shares one observations.json
|
|
231
|
-
const projectObs = allObs.filter((o: any) => o.projectId === effectiveProjectId);
|
|
232
|
-
const active = projectObs.filter((o: any) => (o.status ?? 'active') === 'active');
|
|
233
|
-
const filtered = active.filter((o: any) => !/^(Ran:|Command:|Executed:)\s/i.test(o.title || ''));
|
|
234
|
-
|
|
235
|
-
return filtered.slice(-limit).reverse().map((o: any) => ({
|
|
236
|
-
id: o.id,
|
|
237
|
-
title: o.title || '(untitled)',
|
|
238
|
-
type: o.type || 'discovery',
|
|
239
|
-
createdAt: o.createdAt || '',
|
|
240
|
-
entityName: o.entityName || '',
|
|
241
|
-
status: o.status,
|
|
242
|
-
}));
|
|
243
|
-
} catch {
|
|
244
|
-
return [];
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export async function searchMemories(query: string, limit = 10): Promise<SearchResult[]> {
|
|
249
|
-
try {
|
|
250
|
-
const { searchObservations, getDb, hydrateIndex } = await import('../../store/orama-store.js');
|
|
251
|
-
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
252
|
-
const { detectProject } = await import('../../project/detector.js');
|
|
253
|
-
const { initObservations } = await import('../../memory/observations.js');
|
|
254
|
-
const { initObservationStore, getObservationStore: getStore } = await import('../../store/obs-store.js');
|
|
255
|
-
|
|
256
|
-
const proj = detectProject(process.cwd());
|
|
257
|
-
if (!proj) return [];
|
|
258
|
-
|
|
259
|
-
const dataDir = await getProjectDataDir(proj.id);
|
|
260
|
-
await initObservationStore(dataDir);
|
|
261
|
-
await initObservations(dataDir);
|
|
262
|
-
await getDb();
|
|
263
|
-
|
|
264
|
-
const allObs = (await getStore().loadAll()) as any[];
|
|
265
|
-
await hydrateIndex(allObs);
|
|
266
|
-
|
|
267
|
-
const results = await searchObservations({ query, limit, projectId: proj.id });
|
|
268
|
-
|
|
269
|
-
const typeIcons: Record<string, string> = {
|
|
270
|
-
gotcha: '!',
|
|
271
|
-
decision: 'D',
|
|
272
|
-
'problem-solution': 'S',
|
|
273
|
-
discovery: '?',
|
|
274
|
-
'how-it-works': 'H',
|
|
275
|
-
'what-changed': 'C',
|
|
276
|
-
'trade-off': 'T',
|
|
277
|
-
reasoning: 'R',
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
return results.map((r: any) => ({
|
|
281
|
-
id: r.id,
|
|
282
|
-
title: r.title || '(untitled)',
|
|
283
|
-
type: r.type || 'discovery',
|
|
284
|
-
score: r.score ?? 0,
|
|
285
|
-
entityName: r.entityName || '',
|
|
286
|
-
icon: typeIcons[r.type] || '?',
|
|
287
|
-
}));
|
|
288
|
-
} catch {
|
|
289
|
-
return [];
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export async function storeQuickMemory(text: string): Promise<{ id: number; title: string } | null> {
|
|
294
|
-
try {
|
|
295
|
-
const { detectProject } = await import('../../project/detector.js');
|
|
296
|
-
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
297
|
-
const { initObservations, storeObservation } = await import('../../memory/observations.js');
|
|
298
|
-
const { initObservationStore } = await import('../../store/obs-store.js');
|
|
299
|
-
|
|
300
|
-
const proj = detectProject(process.cwd());
|
|
301
|
-
if (!proj) return null;
|
|
302
|
-
|
|
303
|
-
const dataDir = await getProjectDataDir(proj.id);
|
|
304
|
-
await initObservationStore(dataDir);
|
|
305
|
-
await initObservations(dataDir);
|
|
306
|
-
|
|
307
|
-
const result = await storeObservation({
|
|
308
|
-
entityName: 'quick-note',
|
|
309
|
-
type: 'discovery',
|
|
310
|
-
title: text.slice(0, 100),
|
|
311
|
-
narrative: text,
|
|
312
|
-
facts: [],
|
|
313
|
-
projectId: proj.id,
|
|
314
|
-
sourceDetail: 'explicit',
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
return { id: result.observation.id, title: text.slice(0, 100) };
|
|
318
|
-
} catch {
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export async function getBackgroundStatus(): Promise<BackgroundInfo> {
|
|
324
|
-
const result: BackgroundInfo = { running: false, healthy: false };
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
328
|
-
const statePath = `${home.replace(/\\/g, '/')}/.memorix/background.json`;
|
|
329
|
-
|
|
330
|
-
if (!fs.existsSync(statePath)) {
|
|
331
|
-
try {
|
|
332
|
-
const controller = new AbortController();
|
|
333
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
334
|
-
const res = await fetch('http://127.0.0.1:3211/api/team', { signal: controller.signal });
|
|
335
|
-
clearTimeout(timer);
|
|
336
|
-
if (res.ok) {
|
|
337
|
-
const data = (await res.json()) as any;
|
|
338
|
-
result.running = true;
|
|
339
|
-
result.healthy = true;
|
|
340
|
-
result.port = 3211;
|
|
341
|
-
result.dashboard = 'http://127.0.0.1:3211/';
|
|
342
|
-
result.mcp = 'http://127.0.0.1:3211/mcp';
|
|
343
|
-
result.agents = data.agents?.length ?? 0;
|
|
344
|
-
result.sessions = data.sessions ?? 0;
|
|
345
|
-
result.message = 'Foreground instance detected';
|
|
346
|
-
}
|
|
347
|
-
} catch {
|
|
348
|
-
// Not running.
|
|
349
|
-
}
|
|
350
|
-
return result;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
354
|
-
result.pid = state.pid;
|
|
355
|
-
result.port = state.port;
|
|
356
|
-
result.startedAt = state.startedAt;
|
|
357
|
-
result.dashboard = `http://127.0.0.1:${state.port}/`;
|
|
358
|
-
result.mcp = `http://127.0.0.1:${state.port}/mcp`;
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
process.kill(state.pid, 0);
|
|
362
|
-
result.running = true;
|
|
363
|
-
} catch {
|
|
364
|
-
result.message = 'Process exited';
|
|
365
|
-
return result;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
try {
|
|
369
|
-
const controller = new AbortController();
|
|
370
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
371
|
-
const res = await fetch(`http://127.0.0.1:${state.port}/api/team`, { signal: controller.signal });
|
|
372
|
-
clearTimeout(timer);
|
|
373
|
-
if (res.ok) {
|
|
374
|
-
const data = (await res.json()) as any;
|
|
375
|
-
result.healthy = true;
|
|
376
|
-
result.agents = data.agents?.length ?? 0;
|
|
377
|
-
result.sessions = data.sessions ?? 0;
|
|
378
|
-
result.uptime = data.uptime;
|
|
379
|
-
}
|
|
380
|
-
} catch {
|
|
381
|
-
result.message = 'Running but unhealthy';
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return result;
|
|
385
|
-
} catch {
|
|
386
|
-
return result;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export async function getDoctorSummary(): Promise<DoctorResult> {
|
|
391
|
-
const result: DoctorResult = { sections: [] };
|
|
392
|
-
|
|
393
|
-
try {
|
|
394
|
-
const proj = await getProjectInfo();
|
|
395
|
-
const projectItems: DoctorSection['items'] = [];
|
|
396
|
-
if (proj) {
|
|
397
|
-
projectItems.push({ label: 'Name', value: proj.name, status: 'ok' });
|
|
398
|
-
projectItems.push({ label: 'ID', value: truncate(proj.id, 16), status: 'info' });
|
|
399
|
-
projectItems.push({
|
|
400
|
-
label: 'Remote',
|
|
401
|
-
value: proj.gitRemote,
|
|
402
|
-
status: proj.gitRemote !== 'none' ? 'ok' : 'warn',
|
|
403
|
-
});
|
|
404
|
-
} else {
|
|
405
|
-
projectItems.push({ label: 'Project', value: 'Not detected (no .git)', status: 'error' });
|
|
406
|
-
}
|
|
407
|
-
result.sections.push({ title: 'Project', items: projectItems });
|
|
408
|
-
|
|
409
|
-
const health = await getHealthInfo(proj?.id);
|
|
410
|
-
result.sections.push({
|
|
411
|
-
title: 'Data',
|
|
412
|
-
items: [
|
|
413
|
-
{ label: 'Total', value: `${health.totalMemories}`, status: 'info' },
|
|
414
|
-
{ label: 'Active', value: `${health.activeMemories}`, status: health.activeMemories > 0 ? 'ok' : 'warn' },
|
|
415
|
-
{ label: 'Sessions', value: `${health.sessions}`, status: 'info' },
|
|
416
|
-
],
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
const searchItems: DoctorSection['items'] = [
|
|
420
|
-
{ label: 'Search Mode', value: health.searchModeLabel, status: 'info' },
|
|
421
|
-
{ label: 'Embedding', value: health.embeddingLabel, status: health.embeddingProvider === 'ready' ? 'ok' : 'info' },
|
|
422
|
-
];
|
|
423
|
-
if (health.embeddingProviderName) {
|
|
424
|
-
searchItems.push({ label: 'Provider', value: health.embeddingProviderName, status: 'info' });
|
|
425
|
-
}
|
|
426
|
-
result.sections.push({ title: 'Search', items: searchItems });
|
|
427
|
-
|
|
428
|
-
const bg = await getBackgroundStatus();
|
|
429
|
-
result.sections.push({
|
|
430
|
-
title: 'Background',
|
|
431
|
-
items: [
|
|
432
|
-
{
|
|
433
|
-
label: 'Status',
|
|
434
|
-
value: bg.healthy ? 'Running & healthy' : bg.running ? 'Running (unhealthy)' : 'Not running',
|
|
435
|
-
status: bg.healthy ? 'ok' : bg.running ? 'warn' : 'info',
|
|
436
|
-
},
|
|
437
|
-
...(bg.port ? [{ label: 'Port', value: `${bg.port}`, status: 'info' as const }] : []),
|
|
438
|
-
],
|
|
439
|
-
});
|
|
440
|
-
} catch (err) {
|
|
441
|
-
result.sections.push({
|
|
442
|
-
title: 'Error',
|
|
443
|
-
items: [{ label: 'Diagnostics failed', value: err instanceof Error ? err.message : String(err), status: 'error' }],
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return result;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
export function detectMode(): { mode: string; detail: string } {
|
|
451
|
-
try {
|
|
452
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
453
|
-
const statePath = `${home.replace(/\\/g, '/')}/.memorix/background.json`;
|
|
454
|
-
if (fs.existsSync(statePath)) {
|
|
455
|
-
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
456
|
-
try {
|
|
457
|
-
process.kill(state.pid, 0);
|
|
458
|
-
return { mode: 'Background', detail: `port ${state.port}` };
|
|
459
|
-
} catch {
|
|
460
|
-
// Ignore dead process and fall back to CLI.
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
} catch {
|
|
464
|
-
// Ignore state read failures and fall back to CLI.
|
|
465
|
-
}
|
|
466
|
-
return { mode: 'CLI', detail: 'Quick mode' };
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/** Session state for the Workbench view. */
|
|
470
|
-
export interface SessionState {
|
|
471
|
-
status: 'unbound' | 'binding' | 'bound' | 'error';
|
|
472
|
-
sessionId?: string;
|
|
473
|
-
startedAt?: string;
|
|
474
|
-
agent?: string;
|
|
475
|
-
context?: string;
|
|
476
|
-
error?: string;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
export {
|
|
480
|
-
getSessionState,
|
|
481
|
-
bindSession,
|
|
482
|
-
unbindSession,
|
|
483
|
-
} from './session-service.js';
|
|
484
|
-
|
|
485
|
-
export async function getKnowledgeGraph(
|
|
486
|
-
projectId?: string,
|
|
487
|
-
): Promise<import('../../wiki/types.js').ProjectKnowledgeGraph | null> {
|
|
488
|
-
try {
|
|
489
|
-
const { detectProject } = await import('../../project/detector.js');
|
|
490
|
-
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
491
|
-
const { initObservationStore } = await import('../../store/obs-store.js');
|
|
492
|
-
const { initObservations, getAllObservations } = await import('../../memory/observations.js');
|
|
493
|
-
const { initMiniSkillStore, getMiniSkillStore } = await import('../../store/mini-skill-store.js');
|
|
494
|
-
const { generateKnowledgeGraph } = await import('../../wiki/knowledge-graph.js');
|
|
495
|
-
|
|
496
|
-
const proj = detectProject(process.cwd());
|
|
497
|
-
if (!proj) return null;
|
|
498
|
-
|
|
499
|
-
const effectiveProjectId = projectId || proj.id;
|
|
500
|
-
const dataDir = await getProjectDataDir(effectiveProjectId);
|
|
501
|
-
await initObservationStore(dataDir);
|
|
502
|
-
await initObservations(dataDir);
|
|
503
|
-
await initMiniSkillStore(dataDir);
|
|
504
|
-
|
|
505
|
-
const observations = getAllObservations();
|
|
506
|
-
const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
|
|
507
|
-
|
|
508
|
-
return generateKnowledgeGraph({
|
|
509
|
-
projectId: effectiveProjectId,
|
|
510
|
-
observations,
|
|
511
|
-
miniSkills: skills,
|
|
512
|
-
});
|
|
513
|
-
} catch {
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
export async function getKnowledgeBase(projectId?: string): Promise<import('../../wiki/types.js').ProjectKnowledgeOverview | null> {
|
|
519
|
-
try {
|
|
520
|
-
const { detectProject } = await import('../../project/detector.js');
|
|
521
|
-
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
522
|
-
const { initObservationStore } = await import('../../store/obs-store.js');
|
|
523
|
-
const { initObservations, getAllObservations } = await import('../../memory/observations.js');
|
|
524
|
-
const { initMiniSkillStore, getMiniSkillStore } = await import('../../store/mini-skill-store.js');
|
|
525
|
-
const { generateKnowledgeBase } = await import('../../wiki/generator.js');
|
|
526
|
-
|
|
527
|
-
const proj = detectProject(process.cwd());
|
|
528
|
-
if (!proj) return null;
|
|
529
|
-
|
|
530
|
-
const effectiveProjectId = projectId || proj.id;
|
|
531
|
-
const dataDir = await getProjectDataDir(effectiveProjectId);
|
|
532
|
-
await initObservationStore(dataDir);
|
|
533
|
-
await initObservations(dataDir);
|
|
534
|
-
await initMiniSkillStore(dataDir);
|
|
535
|
-
|
|
536
|
-
const observations = getAllObservations();
|
|
537
|
-
const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
|
|
538
|
-
|
|
539
|
-
return generateKnowledgeBase({
|
|
540
|
-
projectId: effectiveProjectId,
|
|
541
|
-
observations,
|
|
542
|
-
miniSkills: skills,
|
|
543
|
-
});
|
|
544
|
-
} catch {
|
|
545
|
-
return null;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Memorix TUI data layer.
|
|
3
|
+
*
|
|
4
|
+
* Pure data functions that return structured results without console output.
|
|
5
|
+
* Used by Ink components to populate panels.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
|
|
10
|
+
export interface ProjectInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
id: string;
|
|
13
|
+
rootPath: string;
|
|
14
|
+
gitRemote: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HealthInfo {
|
|
18
|
+
embeddingProvider: 'ready' | 'unavailable' | 'disabled';
|
|
19
|
+
embeddingProviderName?: string;
|
|
20
|
+
embeddingLabel: string;
|
|
21
|
+
searchMode: string;
|
|
22
|
+
searchModeLabel: string;
|
|
23
|
+
searchDiagnostic: string;
|
|
24
|
+
backfillPending: number;
|
|
25
|
+
totalMemories: number;
|
|
26
|
+
activeMemories: number;
|
|
27
|
+
sessions: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MemoryItem {
|
|
31
|
+
id: number;
|
|
32
|
+
title: string;
|
|
33
|
+
type: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
entityName: string;
|
|
36
|
+
status?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BackgroundInfo {
|
|
40
|
+
running: boolean;
|
|
41
|
+
healthy: boolean;
|
|
42
|
+
pid?: number;
|
|
43
|
+
port?: number;
|
|
44
|
+
startedAt?: string;
|
|
45
|
+
dashboard?: string;
|
|
46
|
+
mcp?: string;
|
|
47
|
+
agents?: number;
|
|
48
|
+
sessions?: number;
|
|
49
|
+
uptime?: string;
|
|
50
|
+
message?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SearchResult {
|
|
54
|
+
id: number;
|
|
55
|
+
title: string;
|
|
56
|
+
type: string;
|
|
57
|
+
score: number;
|
|
58
|
+
entityName: string;
|
|
59
|
+
icon: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DoctorResult {
|
|
63
|
+
sections: DoctorSection[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DoctorSection {
|
|
67
|
+
title: string;
|
|
68
|
+
items: { label: string; value: string; status: 'ok' | 'warn' | 'error' | 'info' }[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatSearchModeLabel(mode: string): string {
|
|
72
|
+
const normalized = (mode || '').toLowerCase();
|
|
73
|
+
if (!normalized) return 'Unknown';
|
|
74
|
+
if (normalized.includes('hybrid') && normalized.includes('rerank')) return 'Hybrid + rerank';
|
|
75
|
+
if (normalized.includes('hybrid')) return 'Hybrid';
|
|
76
|
+
if (normalized.includes('vector-only')) return 'Vector fallback';
|
|
77
|
+
if (normalized.includes('vector')) return 'Vector';
|
|
78
|
+
if (normalized.includes('fulltext')) return 'BM25 full-text';
|
|
79
|
+
return mode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatEmbeddingLabel(
|
|
83
|
+
status: HealthInfo['embeddingProvider'],
|
|
84
|
+
providerName?: string,
|
|
85
|
+
): string {
|
|
86
|
+
if (status === 'disabled') return 'Disabled';
|
|
87
|
+
if (status === 'unavailable') return 'Unavailable';
|
|
88
|
+
if ((providerName || '').startsWith('api-')) return 'API ready';
|
|
89
|
+
if (providerName) return 'Local ready';
|
|
90
|
+
return 'Ready';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function truncate(text: string, max = 60): string {
|
|
94
|
+
if (text.length <= max) return text;
|
|
95
|
+
return `${text.slice(0, max)}...`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function getProjectInfo(): Promise<ProjectInfo | null> {
|
|
99
|
+
try {
|
|
100
|
+
const { detectProject } = await import('../../project/detector.js');
|
|
101
|
+
const proj = detectProject(process.cwd());
|
|
102
|
+
if (!proj) return null;
|
|
103
|
+
return {
|
|
104
|
+
name: proj.name,
|
|
105
|
+
id: proj.id,
|
|
106
|
+
rootPath: proj.rootPath,
|
|
107
|
+
gitRemote: proj.gitRemote || 'none',
|
|
108
|
+
};
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getHealthInfo(projectId?: string): Promise<HealthInfo> {
|
|
115
|
+
const defaults: HealthInfo = {
|
|
116
|
+
embeddingProvider: 'disabled',
|
|
117
|
+
embeddingProviderName: undefined,
|
|
118
|
+
embeddingLabel: 'Disabled',
|
|
119
|
+
searchMode: 'fulltext',
|
|
120
|
+
searchModeLabel: 'BM25 full-text',
|
|
121
|
+
searchDiagnostic: '',
|
|
122
|
+
backfillPending: 0,
|
|
123
|
+
totalMemories: 0,
|
|
124
|
+
activeMemories: 0,
|
|
125
|
+
sessions: 0,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const { detectProject } = await import('../../project/detector.js');
|
|
130
|
+
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
131
|
+
const { initObservationStore: initStore, getObservationStore: getStore } = await import('../../store/obs-store.js');
|
|
132
|
+
|
|
133
|
+
const proj = detectProject(process.cwd());
|
|
134
|
+
if (!proj) return defaults;
|
|
135
|
+
|
|
136
|
+
// Load .env BEFORE any process.env reads or provider initialization (#46)
|
|
137
|
+
try {
|
|
138
|
+
const { loadDotenv } = await import('../../config/dotenv-loader.js');
|
|
139
|
+
loadDotenv(proj.rootPath);
|
|
140
|
+
} catch { /* best-effort */ }
|
|
141
|
+
|
|
142
|
+
const effectiveProjectId = projectId || proj.id;
|
|
143
|
+
const dataDir = await getProjectDataDir(effectiveProjectId);
|
|
144
|
+
await initStore(dataDir);
|
|
145
|
+
const allObs = (await getStore().loadAll()) as any[];
|
|
146
|
+
// Filter by project — flat storage shares one observations.json across projects
|
|
147
|
+
const obs = allObs.filter((o: any) => o.projectId === effectiveProjectId);
|
|
148
|
+
const active = obs.filter((o: any) => (o.status ?? 'active') === 'active');
|
|
149
|
+
|
|
150
|
+
defaults.totalMemories = obs.length;
|
|
151
|
+
defaults.activeMemories = active.length;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const { initSessionStore, getSessionStore } = await import('../../store/session-store.js');
|
|
155
|
+
await initSessionStore(dataDir);
|
|
156
|
+
const sessStore = getSessionStore();
|
|
157
|
+
const allSessions = await sessStore.loadAll() as any[];
|
|
158
|
+
defaults.sessions = allSessions.filter((s: any) => s.projectId === effectiveProjectId).length;
|
|
159
|
+
} catch {
|
|
160
|
+
// Ignore unreadable session data and keep defaults.
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const { getEmbeddingProvider, isEmbeddingExplicitlyDisabled } = await import('../../embedding/provider.js');
|
|
165
|
+
if (isEmbeddingExplicitlyDisabled()) {
|
|
166
|
+
defaults.embeddingProvider = 'disabled';
|
|
167
|
+
} else {
|
|
168
|
+
const provider = await getEmbeddingProvider();
|
|
169
|
+
defaults.embeddingProvider = provider ? 'ready' : 'unavailable';
|
|
170
|
+
defaults.embeddingProviderName = provider?.name;
|
|
171
|
+
}
|
|
172
|
+
defaults.embeddingLabel = formatEmbeddingLabel(
|
|
173
|
+
defaults.embeddingProvider,
|
|
174
|
+
defaults.embeddingProviderName,
|
|
175
|
+
);
|
|
176
|
+
} catch {
|
|
177
|
+
defaults.embeddingProvider = 'unavailable';
|
|
178
|
+
defaults.embeddingLabel = 'Unavailable';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const { getLastSearchMode, isEmbeddingEnabled } = await import('../../store/orama-store.js');
|
|
183
|
+
const vectorActive = isEmbeddingEnabled();
|
|
184
|
+
defaults.searchMode = getLastSearchMode(effectiveProjectId);
|
|
185
|
+
defaults.searchModeLabel = formatSearchModeLabel(defaults.searchMode);
|
|
186
|
+
|
|
187
|
+
// Build diagnostic explanation
|
|
188
|
+
if (defaults.searchMode.includes('hybrid')) {
|
|
189
|
+
defaults.searchDiagnostic = 'Hybrid search active';
|
|
190
|
+
} else if (defaults.searchMode.includes('vector')) {
|
|
191
|
+
defaults.searchDiagnostic = 'Vector fallback (BM25 empty)';
|
|
192
|
+
} else if (defaults.searchMode.includes('rerank')) {
|
|
193
|
+
defaults.searchDiagnostic = 'LLM reranking active';
|
|
194
|
+
} else if (defaults.searchMode.includes('embedding unavailable')) {
|
|
195
|
+
defaults.searchDiagnostic = 'Embedding unavailable';
|
|
196
|
+
} else if (defaults.embeddingProvider === 'ready' && vectorActive) {
|
|
197
|
+
defaults.searchDiagnostic = 'Vector index ready';
|
|
198
|
+
} else if (defaults.embeddingProvider === 'ready' && !vectorActive) {
|
|
199
|
+
defaults.searchDiagnostic = 'Provider ready, no index yet';
|
|
200
|
+
} else if (defaults.embeddingProvider === 'unavailable') {
|
|
201
|
+
defaults.searchDiagnostic = 'BM25 only (no provider)';
|
|
202
|
+
} else {
|
|
203
|
+
defaults.searchDiagnostic = 'BM25 only (disabled)';
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
defaults.searchMode = 'fulltext';
|
|
207
|
+
defaults.searchModeLabel = 'BM25 full-text';
|
|
208
|
+
defaults.searchDiagnostic = 'Could not determine search mode';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return defaults;
|
|
212
|
+
} catch {
|
|
213
|
+
return defaults;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function getRecentMemories(limit = 8, projectId?: string): Promise<MemoryItem[]> {
|
|
218
|
+
try {
|
|
219
|
+
const { detectProject } = await import('../../project/detector.js');
|
|
220
|
+
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
221
|
+
const { initObservationStore: initStore, getObservationStore: getStore } = await import('../../store/obs-store.js');
|
|
222
|
+
|
|
223
|
+
const proj = detectProject(process.cwd());
|
|
224
|
+
if (!proj) return [];
|
|
225
|
+
|
|
226
|
+
const effectiveProjectId = projectId || proj.id;
|
|
227
|
+
const dataDir = await getProjectDataDir(effectiveProjectId);
|
|
228
|
+
await initStore(dataDir);
|
|
229
|
+
const allObs = (await getStore().loadAll()) as any[];
|
|
230
|
+
// Filter by project — flat storage shares one observations.json
|
|
231
|
+
const projectObs = allObs.filter((o: any) => o.projectId === effectiveProjectId);
|
|
232
|
+
const active = projectObs.filter((o: any) => (o.status ?? 'active') === 'active');
|
|
233
|
+
const filtered = active.filter((o: any) => !/^(Ran:|Command:|Executed:)\s/i.test(o.title || ''));
|
|
234
|
+
|
|
235
|
+
return filtered.slice(-limit).reverse().map((o: any) => ({
|
|
236
|
+
id: o.id,
|
|
237
|
+
title: o.title || '(untitled)',
|
|
238
|
+
type: o.type || 'discovery',
|
|
239
|
+
createdAt: o.createdAt || '',
|
|
240
|
+
entityName: o.entityName || '',
|
|
241
|
+
status: o.status,
|
|
242
|
+
}));
|
|
243
|
+
} catch {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function searchMemories(query: string, limit = 10): Promise<SearchResult[]> {
|
|
249
|
+
try {
|
|
250
|
+
const { searchObservations, getDb, hydrateIndex } = await import('../../store/orama-store.js');
|
|
251
|
+
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
252
|
+
const { detectProject } = await import('../../project/detector.js');
|
|
253
|
+
const { initObservations } = await import('../../memory/observations.js');
|
|
254
|
+
const { initObservationStore, getObservationStore: getStore } = await import('../../store/obs-store.js');
|
|
255
|
+
|
|
256
|
+
const proj = detectProject(process.cwd());
|
|
257
|
+
if (!proj) return [];
|
|
258
|
+
|
|
259
|
+
const dataDir = await getProjectDataDir(proj.id);
|
|
260
|
+
await initObservationStore(dataDir);
|
|
261
|
+
await initObservations(dataDir);
|
|
262
|
+
await getDb();
|
|
263
|
+
|
|
264
|
+
const allObs = (await getStore().loadAll()) as any[];
|
|
265
|
+
await hydrateIndex(allObs);
|
|
266
|
+
|
|
267
|
+
const results = await searchObservations({ query, limit, projectId: proj.id });
|
|
268
|
+
|
|
269
|
+
const typeIcons: Record<string, string> = {
|
|
270
|
+
gotcha: '!',
|
|
271
|
+
decision: 'D',
|
|
272
|
+
'problem-solution': 'S',
|
|
273
|
+
discovery: '?',
|
|
274
|
+
'how-it-works': 'H',
|
|
275
|
+
'what-changed': 'C',
|
|
276
|
+
'trade-off': 'T',
|
|
277
|
+
reasoning: 'R',
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return results.map((r: any) => ({
|
|
281
|
+
id: r.id,
|
|
282
|
+
title: r.title || '(untitled)',
|
|
283
|
+
type: r.type || 'discovery',
|
|
284
|
+
score: r.score ?? 0,
|
|
285
|
+
entityName: r.entityName || '',
|
|
286
|
+
icon: typeIcons[r.type] || '?',
|
|
287
|
+
}));
|
|
288
|
+
} catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function storeQuickMemory(text: string): Promise<{ id: number; title: string } | null> {
|
|
294
|
+
try {
|
|
295
|
+
const { detectProject } = await import('../../project/detector.js');
|
|
296
|
+
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
297
|
+
const { initObservations, storeObservation } = await import('../../memory/observations.js');
|
|
298
|
+
const { initObservationStore } = await import('../../store/obs-store.js');
|
|
299
|
+
|
|
300
|
+
const proj = detectProject(process.cwd());
|
|
301
|
+
if (!proj) return null;
|
|
302
|
+
|
|
303
|
+
const dataDir = await getProjectDataDir(proj.id);
|
|
304
|
+
await initObservationStore(dataDir);
|
|
305
|
+
await initObservations(dataDir);
|
|
306
|
+
|
|
307
|
+
const result = await storeObservation({
|
|
308
|
+
entityName: 'quick-note',
|
|
309
|
+
type: 'discovery',
|
|
310
|
+
title: text.slice(0, 100),
|
|
311
|
+
narrative: text,
|
|
312
|
+
facts: [],
|
|
313
|
+
projectId: proj.id,
|
|
314
|
+
sourceDetail: 'explicit',
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return { id: result.observation.id, title: text.slice(0, 100) };
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function getBackgroundStatus(): Promise<BackgroundInfo> {
|
|
324
|
+
const result: BackgroundInfo = { running: false, healthy: false };
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
328
|
+
const statePath = `${home.replace(/\\/g, '/')}/.memorix/background.json`;
|
|
329
|
+
|
|
330
|
+
if (!fs.existsSync(statePath)) {
|
|
331
|
+
try {
|
|
332
|
+
const controller = new AbortController();
|
|
333
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
334
|
+
const res = await fetch('http://127.0.0.1:3211/api/team', { signal: controller.signal });
|
|
335
|
+
clearTimeout(timer);
|
|
336
|
+
if (res.ok) {
|
|
337
|
+
const data = (await res.json()) as any;
|
|
338
|
+
result.running = true;
|
|
339
|
+
result.healthy = true;
|
|
340
|
+
result.port = 3211;
|
|
341
|
+
result.dashboard = 'http://127.0.0.1:3211/';
|
|
342
|
+
result.mcp = 'http://127.0.0.1:3211/mcp';
|
|
343
|
+
result.agents = data.agents?.length ?? 0;
|
|
344
|
+
result.sessions = data.sessions ?? 0;
|
|
345
|
+
result.message = 'Foreground instance detected';
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
// Not running.
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
354
|
+
result.pid = state.pid;
|
|
355
|
+
result.port = state.port;
|
|
356
|
+
result.startedAt = state.startedAt;
|
|
357
|
+
result.dashboard = `http://127.0.0.1:${state.port}/`;
|
|
358
|
+
result.mcp = `http://127.0.0.1:${state.port}/mcp`;
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
process.kill(state.pid, 0);
|
|
362
|
+
result.running = true;
|
|
363
|
+
} catch {
|
|
364
|
+
result.message = 'Process exited';
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const controller = new AbortController();
|
|
370
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
371
|
+
const res = await fetch(`http://127.0.0.1:${state.port}/api/team`, { signal: controller.signal });
|
|
372
|
+
clearTimeout(timer);
|
|
373
|
+
if (res.ok) {
|
|
374
|
+
const data = (await res.json()) as any;
|
|
375
|
+
result.healthy = true;
|
|
376
|
+
result.agents = data.agents?.length ?? 0;
|
|
377
|
+
result.sessions = data.sessions ?? 0;
|
|
378
|
+
result.uptime = data.uptime;
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
result.message = 'Running but unhealthy';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return result;
|
|
385
|
+
} catch {
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function getDoctorSummary(): Promise<DoctorResult> {
|
|
391
|
+
const result: DoctorResult = { sections: [] };
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const proj = await getProjectInfo();
|
|
395
|
+
const projectItems: DoctorSection['items'] = [];
|
|
396
|
+
if (proj) {
|
|
397
|
+
projectItems.push({ label: 'Name', value: proj.name, status: 'ok' });
|
|
398
|
+
projectItems.push({ label: 'ID', value: truncate(proj.id, 16), status: 'info' });
|
|
399
|
+
projectItems.push({
|
|
400
|
+
label: 'Remote',
|
|
401
|
+
value: proj.gitRemote,
|
|
402
|
+
status: proj.gitRemote !== 'none' ? 'ok' : 'warn',
|
|
403
|
+
});
|
|
404
|
+
} else {
|
|
405
|
+
projectItems.push({ label: 'Project', value: 'Not detected (no .git)', status: 'error' });
|
|
406
|
+
}
|
|
407
|
+
result.sections.push({ title: 'Project', items: projectItems });
|
|
408
|
+
|
|
409
|
+
const health = await getHealthInfo(proj?.id);
|
|
410
|
+
result.sections.push({
|
|
411
|
+
title: 'Data',
|
|
412
|
+
items: [
|
|
413
|
+
{ label: 'Total', value: `${health.totalMemories}`, status: 'info' },
|
|
414
|
+
{ label: 'Active', value: `${health.activeMemories}`, status: health.activeMemories > 0 ? 'ok' : 'warn' },
|
|
415
|
+
{ label: 'Sessions', value: `${health.sessions}`, status: 'info' },
|
|
416
|
+
],
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const searchItems: DoctorSection['items'] = [
|
|
420
|
+
{ label: 'Search Mode', value: health.searchModeLabel, status: 'info' },
|
|
421
|
+
{ label: 'Embedding', value: health.embeddingLabel, status: health.embeddingProvider === 'ready' ? 'ok' : 'info' },
|
|
422
|
+
];
|
|
423
|
+
if (health.embeddingProviderName) {
|
|
424
|
+
searchItems.push({ label: 'Provider', value: health.embeddingProviderName, status: 'info' });
|
|
425
|
+
}
|
|
426
|
+
result.sections.push({ title: 'Search', items: searchItems });
|
|
427
|
+
|
|
428
|
+
const bg = await getBackgroundStatus();
|
|
429
|
+
result.sections.push({
|
|
430
|
+
title: 'Background',
|
|
431
|
+
items: [
|
|
432
|
+
{
|
|
433
|
+
label: 'Status',
|
|
434
|
+
value: bg.healthy ? 'Running & healthy' : bg.running ? 'Running (unhealthy)' : 'Not running',
|
|
435
|
+
status: bg.healthy ? 'ok' : bg.running ? 'warn' : 'info',
|
|
436
|
+
},
|
|
437
|
+
...(bg.port ? [{ label: 'Port', value: `${bg.port}`, status: 'info' as const }] : []),
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
} catch (err) {
|
|
441
|
+
result.sections.push({
|
|
442
|
+
title: 'Error',
|
|
443
|
+
items: [{ label: 'Diagnostics failed', value: err instanceof Error ? err.message : String(err), status: 'error' }],
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function detectMode(): { mode: string; detail: string } {
|
|
451
|
+
try {
|
|
452
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
453
|
+
const statePath = `${home.replace(/\\/g, '/')}/.memorix/background.json`;
|
|
454
|
+
if (fs.existsSync(statePath)) {
|
|
455
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
456
|
+
try {
|
|
457
|
+
process.kill(state.pid, 0);
|
|
458
|
+
return { mode: 'Background', detail: `port ${state.port}` };
|
|
459
|
+
} catch {
|
|
460
|
+
// Ignore dead process and fall back to CLI.
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
// Ignore state read failures and fall back to CLI.
|
|
465
|
+
}
|
|
466
|
+
return { mode: 'CLI', detail: 'Quick mode' };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Session state for the Workbench view. */
|
|
470
|
+
export interface SessionState {
|
|
471
|
+
status: 'unbound' | 'binding' | 'bound' | 'error';
|
|
472
|
+
sessionId?: string;
|
|
473
|
+
startedAt?: string;
|
|
474
|
+
agent?: string;
|
|
475
|
+
context?: string;
|
|
476
|
+
error?: string;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export {
|
|
480
|
+
getSessionState,
|
|
481
|
+
bindSession,
|
|
482
|
+
unbindSession,
|
|
483
|
+
} from './session-service.js';
|
|
484
|
+
|
|
485
|
+
export async function getKnowledgeGraph(
|
|
486
|
+
projectId?: string,
|
|
487
|
+
): Promise<import('../../wiki/types.js').ProjectKnowledgeGraph | null> {
|
|
488
|
+
try {
|
|
489
|
+
const { detectProject } = await import('../../project/detector.js');
|
|
490
|
+
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
491
|
+
const { initObservationStore } = await import('../../store/obs-store.js');
|
|
492
|
+
const { initObservations, getAllObservations } = await import('../../memory/observations.js');
|
|
493
|
+
const { initMiniSkillStore, getMiniSkillStore } = await import('../../store/mini-skill-store.js');
|
|
494
|
+
const { generateKnowledgeGraph } = await import('../../wiki/knowledge-graph.js');
|
|
495
|
+
|
|
496
|
+
const proj = detectProject(process.cwd());
|
|
497
|
+
if (!proj) return null;
|
|
498
|
+
|
|
499
|
+
const effectiveProjectId = projectId || proj.id;
|
|
500
|
+
const dataDir = await getProjectDataDir(effectiveProjectId);
|
|
501
|
+
await initObservationStore(dataDir);
|
|
502
|
+
await initObservations(dataDir);
|
|
503
|
+
await initMiniSkillStore(dataDir);
|
|
504
|
+
|
|
505
|
+
const observations = getAllObservations();
|
|
506
|
+
const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
|
|
507
|
+
|
|
508
|
+
return generateKnowledgeGraph({
|
|
509
|
+
projectId: effectiveProjectId,
|
|
510
|
+
observations,
|
|
511
|
+
miniSkills: skills,
|
|
512
|
+
});
|
|
513
|
+
} catch {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export async function getKnowledgeBase(projectId?: string): Promise<import('../../wiki/types.js').ProjectKnowledgeOverview | null> {
|
|
519
|
+
try {
|
|
520
|
+
const { detectProject } = await import('../../project/detector.js');
|
|
521
|
+
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
522
|
+
const { initObservationStore } = await import('../../store/obs-store.js');
|
|
523
|
+
const { initObservations, getAllObservations } = await import('../../memory/observations.js');
|
|
524
|
+
const { initMiniSkillStore, getMiniSkillStore } = await import('../../store/mini-skill-store.js');
|
|
525
|
+
const { generateKnowledgeBase } = await import('../../wiki/generator.js');
|
|
526
|
+
|
|
527
|
+
const proj = detectProject(process.cwd());
|
|
528
|
+
if (!proj) return null;
|
|
529
|
+
|
|
530
|
+
const effectiveProjectId = projectId || proj.id;
|
|
531
|
+
const dataDir = await getProjectDataDir(effectiveProjectId);
|
|
532
|
+
await initObservationStore(dataDir);
|
|
533
|
+
await initObservations(dataDir);
|
|
534
|
+
await initMiniSkillStore(dataDir);
|
|
535
|
+
|
|
536
|
+
const observations = getAllObservations();
|
|
537
|
+
const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
|
|
538
|
+
|
|
539
|
+
return generateKnowledgeBase({
|
|
540
|
+
projectId: effectiveProjectId,
|
|
541
|
+
observations,
|
|
542
|
+
miniSkills: skills,
|
|
543
|
+
});
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|