memorix 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/dist/cli/index.js +299 -51
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +50 -1
- package/dist/dashboard/static/index.html +5 -0
- package/dist/dashboard/static/style.css +47 -0
- package/dist/index.js +281 -49
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -241,9 +241,15 @@ document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
|
241
241
|
// API Client
|
|
242
242
|
// ============================================================
|
|
243
243
|
|
|
244
|
+
let selectedProject = ''; // empty = current project (default)
|
|
245
|
+
|
|
244
246
|
async function api(endpoint) {
|
|
245
247
|
try {
|
|
246
|
-
const
|
|
248
|
+
const sep = endpoint.includes('?') ? '&' : '?';
|
|
249
|
+
const url = selectedProject
|
|
250
|
+
? `/api/${endpoint}${sep}project=${encodeURIComponent(selectedProject)}`
|
|
251
|
+
: `/api/${endpoint}`;
|
|
252
|
+
const res = await fetch(url);
|
|
247
253
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
248
254
|
return await res.json();
|
|
249
255
|
} catch (err) {
|
|
@@ -252,6 +258,49 @@ async function api(endpoint) {
|
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
260
|
|
|
261
|
+
// ============================================================
|
|
262
|
+
// Project Switcher
|
|
263
|
+
// ============================================================
|
|
264
|
+
|
|
265
|
+
async function initProjectSwitcher() {
|
|
266
|
+
const select = document.getElementById('project-select');
|
|
267
|
+
if (!select) return;
|
|
268
|
+
|
|
269
|
+
// Fetch project list
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch('/api/projects');
|
|
272
|
+
const projects = await res.json();
|
|
273
|
+
if (!Array.isArray(projects) || projects.length === 0) {
|
|
274
|
+
select.innerHTML = '<option value="">No projects</option>';
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
select.innerHTML = '';
|
|
279
|
+
for (const p of projects) {
|
|
280
|
+
const opt = document.createElement('option');
|
|
281
|
+
opt.value = p.isCurrent ? '' : p.id;
|
|
282
|
+
opt.textContent = p.name + (p.isCurrent ? ' ●' : '');
|
|
283
|
+
opt.title = p.id;
|
|
284
|
+
if (p.isCurrent) opt.selected = true;
|
|
285
|
+
select.appendChild(opt);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
select.innerHTML = '<option value="">Error</option>';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Switch handler
|
|
292
|
+
select.addEventListener('change', () => {
|
|
293
|
+
selectedProject = select.value;
|
|
294
|
+
// Clear all cached pages and reload current
|
|
295
|
+
Object.keys(loaded).forEach(k => delete loaded[k]);
|
|
296
|
+
loadPage(currentPage);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
301
|
+
initProjectSwitcher();
|
|
302
|
+
});
|
|
303
|
+
|
|
255
304
|
// ============================================================
|
|
256
305
|
// Page Loaders
|
|
257
306
|
// ============================================================
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
<div class="sidebar-brand">
|
|
19
19
|
<img src="/logo.png" alt="Memorix" class="brand-logo" />
|
|
20
20
|
</div>
|
|
21
|
+
<div class="project-switcher">
|
|
22
|
+
<select id="project-select" title="Switch project / 切换项目">
|
|
23
|
+
<option value="">Loading...</option>
|
|
24
|
+
</select>
|
|
25
|
+
</div>
|
|
21
26
|
<div class="sidebar-nav">
|
|
22
27
|
<button class="nav-btn active" data-page="dashboard" title="Dashboard">
|
|
23
28
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -1045,4 +1045,51 @@ body {
|
|
|
1045
1045
|
.page {
|
|
1046
1046
|
padding: 16px;
|
|
1047
1047
|
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/* ============================================================
|
|
1051
|
+
* Project Switcher
|
|
1052
|
+
* ============================================================ */
|
|
1053
|
+
|
|
1054
|
+
.project-switcher {
|
|
1055
|
+
padding: 4px 8px 8px;
|
|
1056
|
+
display: flex;
|
|
1057
|
+
justify-content: center;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.project-switcher select {
|
|
1061
|
+
width: 44px;
|
|
1062
|
+
height: 28px;
|
|
1063
|
+
background: var(--bg-card);
|
|
1064
|
+
color: var(--text-primary);
|
|
1065
|
+
border: 1px solid var(--border-subtle);
|
|
1066
|
+
border-radius: 6px;
|
|
1067
|
+
font-size: 10px;
|
|
1068
|
+
font-family: var(--font-mono);
|
|
1069
|
+
cursor: pointer;
|
|
1070
|
+
padding: 0 2px;
|
|
1071
|
+
text-align: center;
|
|
1072
|
+
-webkit-appearance: none;
|
|
1073
|
+
-moz-appearance: none;
|
|
1074
|
+
appearance: none;
|
|
1075
|
+
transition: var(--transition-fast);
|
|
1076
|
+
overflow: hidden;
|
|
1077
|
+
text-overflow: ellipsis;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.project-switcher select:hover {
|
|
1081
|
+
border-color: var(--accent-cyan);
|
|
1082
|
+
box-shadow: var(--glow-cyan);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.project-switcher select:focus {
|
|
1086
|
+
outline: none;
|
|
1087
|
+
border-color: var(--accent-cyan);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.project-switcher select option {
|
|
1091
|
+
background: var(--bg-surface);
|
|
1092
|
+
color: var(--text-primary);
|
|
1093
|
+
font-size: 12px;
|
|
1094
|
+
padding: 4px 8px;
|
|
1048
1095
|
}
|
package/dist/index.js
CHANGED
|
@@ -29,17 +29,138 @@ var init_esm_shims = __esm({
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
// src/store/persistence.ts
|
|
32
|
+
var persistence_exports = {};
|
|
33
|
+
__export(persistence_exports, {
|
|
34
|
+
getBaseDataDir: () => getBaseDataDir,
|
|
35
|
+
getDbFilePath: () => getDbFilePath,
|
|
36
|
+
getGraphFilePath: () => getGraphFilePath,
|
|
37
|
+
getProjectDataDir: () => getProjectDataDir,
|
|
38
|
+
hasExistingData: () => hasExistingData,
|
|
39
|
+
listProjectDirs: () => listProjectDirs,
|
|
40
|
+
loadGraphJsonl: () => loadGraphJsonl,
|
|
41
|
+
loadIdCounter: () => loadIdCounter,
|
|
42
|
+
loadObservationsJson: () => loadObservationsJson,
|
|
43
|
+
migrateGlobalData: () => migrateGlobalData,
|
|
44
|
+
saveGraphJsonl: () => saveGraphJsonl,
|
|
45
|
+
saveIdCounter: () => saveIdCounter,
|
|
46
|
+
saveObservationsJson: () => saveObservationsJson
|
|
47
|
+
});
|
|
32
48
|
import { promises as fs } from "fs";
|
|
33
49
|
import path2 from "path";
|
|
34
50
|
import os from "os";
|
|
51
|
+
function sanitizeProjectId(projectId) {
|
|
52
|
+
return projectId.replace(/\//g, "--").replace(/[<>:"|?*\\]/g, "_");
|
|
53
|
+
}
|
|
35
54
|
async function getProjectDataDir(projectId, baseDir) {
|
|
36
|
-
const
|
|
55
|
+
const base = baseDir ?? DEFAULT_DATA_DIR;
|
|
56
|
+
const dirName = sanitizeProjectId(projectId);
|
|
57
|
+
const dataDir = path2.join(base, dirName);
|
|
37
58
|
await fs.mkdir(dataDir, { recursive: true });
|
|
38
59
|
return dataDir;
|
|
39
60
|
}
|
|
61
|
+
function getBaseDataDir(baseDir) {
|
|
62
|
+
return baseDir ?? DEFAULT_DATA_DIR;
|
|
63
|
+
}
|
|
64
|
+
async function listProjectDirs(baseDir) {
|
|
65
|
+
const base = baseDir ?? DEFAULT_DATA_DIR;
|
|
66
|
+
try {
|
|
67
|
+
const entries = await fs.readdir(base, { withFileTypes: true });
|
|
68
|
+
return entries.filter((e) => e.isDirectory()).map((e) => path2.join(base, e.name));
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function migrateGlobalData(projectId, baseDir) {
|
|
74
|
+
const base = baseDir ?? DEFAULT_DATA_DIR;
|
|
75
|
+
const globalObsPath = path2.join(base, "observations.json");
|
|
76
|
+
const migratedObsPath = path2.join(base, "observations.json.migrated");
|
|
77
|
+
let sourceObsPath = null;
|
|
78
|
+
try {
|
|
79
|
+
await fs.access(globalObsPath);
|
|
80
|
+
sourceObsPath = globalObsPath;
|
|
81
|
+
} catch {
|
|
82
|
+
try {
|
|
83
|
+
await fs.access(migratedObsPath);
|
|
84
|
+
sourceObsPath = migratedObsPath;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let globalObs = [];
|
|
90
|
+
try {
|
|
91
|
+
const data = await fs.readFile(sourceObsPath, "utf-8");
|
|
92
|
+
globalObs = JSON.parse(data);
|
|
93
|
+
if (!Array.isArray(globalObs) || globalObs.length === 0) return false;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const projectDir2 = await getProjectDataDir(projectId, baseDir);
|
|
98
|
+
const projectObsPath = path2.join(projectDir2, "observations.json");
|
|
99
|
+
let projectObs = [];
|
|
100
|
+
try {
|
|
101
|
+
const data = await fs.readFile(projectObsPath, "utf-8");
|
|
102
|
+
projectObs = JSON.parse(data);
|
|
103
|
+
if (!Array.isArray(projectObs)) projectObs = [];
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
if (projectObs.length >= globalObs.length) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const existingIds = new Set(projectObs.map((o) => o.id));
|
|
110
|
+
const merged = [...projectObs];
|
|
111
|
+
for (const obs of globalObs) {
|
|
112
|
+
if (!existingIds.has(obs.id)) {
|
|
113
|
+
merged.push(obs);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
merged.sort((a, b) => (a.id ?? 0) - (b.id ?? 0));
|
|
117
|
+
for (const obs of merged) {
|
|
118
|
+
obs.projectId = projectId;
|
|
119
|
+
}
|
|
120
|
+
await fs.writeFile(projectObsPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
121
|
+
for (const file of ["graph.jsonl", "counter.json"]) {
|
|
122
|
+
const src = path2.join(base, file);
|
|
123
|
+
const srcMigrated = path2.join(base, file + ".migrated");
|
|
124
|
+
const dst = path2.join(projectDir2, file);
|
|
125
|
+
for (const source of [src, srcMigrated]) {
|
|
126
|
+
try {
|
|
127
|
+
await fs.access(source);
|
|
128
|
+
await fs.copyFile(source, dst);
|
|
129
|
+
break;
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const maxId = merged.reduce((max, o) => Math.max(max, o.id ?? 0), 0);
|
|
135
|
+
await fs.writeFile(
|
|
136
|
+
path2.join(projectDir2, "counter.json"),
|
|
137
|
+
JSON.stringify({ nextId: maxId + 1 }),
|
|
138
|
+
"utf-8"
|
|
139
|
+
);
|
|
140
|
+
for (const file of ["observations.json", "graph.jsonl", "counter.json"]) {
|
|
141
|
+
const src = path2.join(base, file);
|
|
142
|
+
try {
|
|
143
|
+
await fs.access(src);
|
|
144
|
+
await fs.rename(src, src + ".migrated");
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function getDbFilePath(projectDir2) {
|
|
151
|
+
return path2.join(projectDir2, "memorix.msp");
|
|
152
|
+
}
|
|
40
153
|
function getGraphFilePath(projectDir2) {
|
|
41
154
|
return path2.join(projectDir2, "graph.jsonl");
|
|
42
155
|
}
|
|
156
|
+
async function hasExistingData(projectDir2) {
|
|
157
|
+
try {
|
|
158
|
+
await fs.access(getDbFilePath(projectDir2));
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
43
164
|
async function saveGraphJsonl(projectDir2, entities, relations) {
|
|
44
165
|
const lines = [
|
|
45
166
|
...entities.map(
|
|
@@ -645,6 +766,18 @@ async function detectInstalledAgents() {
|
|
|
645
766
|
agents.push("kiro");
|
|
646
767
|
} catch {
|
|
647
768
|
}
|
|
769
|
+
const codexDir = path5.join(home, ".codex");
|
|
770
|
+
try {
|
|
771
|
+
await fs3.access(codexDir);
|
|
772
|
+
agents.push("codex");
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
const antigravityDir = path5.join(home, ".gemini", "antigravity");
|
|
776
|
+
try {
|
|
777
|
+
await fs3.access(antigravityDir);
|
|
778
|
+
agents.push("antigravity");
|
|
779
|
+
} catch {
|
|
780
|
+
}
|
|
648
781
|
return agents;
|
|
649
782
|
}
|
|
650
783
|
async function installHooks(agent, projectRoot, global = false) {
|
|
@@ -721,15 +854,34 @@ async function installAgentRules(agent, projectRoot) {
|
|
|
721
854
|
case "copilot":
|
|
722
855
|
rulesPath = path5.join(projectRoot, ".github", "copilot-instructions.md");
|
|
723
856
|
break;
|
|
857
|
+
case "codex":
|
|
858
|
+
rulesPath = path5.join(projectRoot, "AGENTS.md");
|
|
859
|
+
break;
|
|
860
|
+
case "kiro":
|
|
861
|
+
rulesPath = path5.join(projectRoot, ".kiro", "rules", "memorix.md");
|
|
862
|
+
break;
|
|
724
863
|
default:
|
|
725
|
-
|
|
864
|
+
rulesPath = path5.join(projectRoot, ".agent", "rules", "memorix.md");
|
|
865
|
+
break;
|
|
726
866
|
}
|
|
727
867
|
try {
|
|
728
868
|
await fs3.mkdir(path5.dirname(rulesPath), { recursive: true });
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
869
|
+
if (agent === "codex") {
|
|
870
|
+
try {
|
|
871
|
+
const existing = await fs3.readFile(rulesPath, "utf-8");
|
|
872
|
+
if (existing.includes("Memorix")) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
await fs3.writeFile(rulesPath, existing + "\n\n" + rulesContent, "utf-8");
|
|
876
|
+
} catch {
|
|
877
|
+
await fs3.writeFile(rulesPath, rulesContent, "utf-8");
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
try {
|
|
881
|
+
await fs3.access(rulesPath);
|
|
882
|
+
} catch {
|
|
883
|
+
await fs3.writeFile(rulesPath, rulesContent, "utf-8");
|
|
884
|
+
}
|
|
733
885
|
}
|
|
734
886
|
} catch {
|
|
735
887
|
}
|
|
@@ -743,22 +895,50 @@ You have access to Memorix memory tools. Follow these rules to maintain persiste
|
|
|
743
895
|
|
|
744
896
|
At the **beginning of every conversation**, before responding to the user:
|
|
745
897
|
|
|
746
|
-
1. Call \`memorix_search\` with query related to the user's first message or the current project
|
|
747
|
-
2. If results are found, use
|
|
748
|
-
3. Reference relevant memories naturally in your response
|
|
898
|
+
1. Call \`memorix_search\` with a query related to the user's first message or the current project
|
|
899
|
+
2. If results are found, use \`memorix_detail\` to fetch the most relevant ones
|
|
900
|
+
3. Reference relevant memories naturally in your response \u2014 the user should feel you "remember" them
|
|
749
901
|
|
|
750
902
|
This ensures you already know the project context without the user re-explaining.
|
|
751
903
|
|
|
752
904
|
## During Session \u2014 Capture Important Context
|
|
753
905
|
|
|
754
|
-
Proactively call \`memorix_store\`
|
|
906
|
+
**Proactively** call \`memorix_store\` whenever any of the following happen:
|
|
907
|
+
|
|
908
|
+
### Architecture & Decisions
|
|
909
|
+
- Technology choice, framework selection, or design pattern adopted
|
|
910
|
+
- Trade-off discussion with a clear conclusion
|
|
911
|
+
- API design, database schema, or project structure decisions
|
|
912
|
+
|
|
913
|
+
### Bug Fixes & Problem Solving
|
|
914
|
+
- A bug is identified and resolved \u2014 store root cause + fix
|
|
915
|
+
- Workaround applied for a known issue
|
|
916
|
+
- Performance issue diagnosed and optimized
|
|
755
917
|
|
|
756
|
-
|
|
757
|
-
-
|
|
758
|
-
-
|
|
759
|
-
-
|
|
918
|
+
### Gotchas & Pitfalls
|
|
919
|
+
- Something unexpected or tricky is discovered
|
|
920
|
+
- A common mistake is identified and corrected
|
|
921
|
+
- Platform-specific behavior that caused issues
|
|
760
922
|
|
|
761
|
-
|
|
923
|
+
### Configuration & Environment
|
|
924
|
+
- Environment variables, port numbers, paths changed
|
|
925
|
+
- Docker, nginx, Caddy, or reverse proxy config modified
|
|
926
|
+
- Package dependencies added, removed, or version-pinned
|
|
927
|
+
|
|
928
|
+
### Deployment & Operations
|
|
929
|
+
- Server deployment steps (Docker, VPS, cloud)
|
|
930
|
+
- DNS, SSL/TLS certificate, domain configuration
|
|
931
|
+
- CI/CD pipeline setup or changes
|
|
932
|
+
- Database migration or data transfer procedures
|
|
933
|
+
- Server topology (ports, services, reverse proxy chain)
|
|
934
|
+
- SSH keys, access credentials setup (store pattern, NOT secrets)
|
|
935
|
+
|
|
936
|
+
### Project Milestones
|
|
937
|
+
- Feature completed or shipped
|
|
938
|
+
- Version released or published to npm/PyPI/etc.
|
|
939
|
+
- Repository made public, README updated, PR submitted
|
|
940
|
+
|
|
941
|
+
Use appropriate types: \`decision\`, \`problem-solution\`, \`gotcha\`, \`what-changed\`, \`discovery\`, \`how-it-works\`.
|
|
762
942
|
|
|
763
943
|
## Session End \u2014 Store Summary
|
|
764
944
|
|
|
@@ -766,18 +946,21 @@ When the conversation is ending or the user says goodbye:
|
|
|
766
946
|
|
|
767
947
|
1. Call \`memorix_store\` with type \`session-request\` to record:
|
|
768
948
|
- What was accomplished in this session
|
|
769
|
-
- Current project state
|
|
949
|
+
- Current project state and any blockers
|
|
770
950
|
- Pending tasks or next steps
|
|
771
|
-
-
|
|
951
|
+
- Key files modified
|
|
772
952
|
|
|
773
|
-
This creates a "handoff note" for the next session.
|
|
953
|
+
This creates a "handoff note" for the next session (or for another AI agent).
|
|
774
954
|
|
|
775
955
|
## Guidelines
|
|
776
956
|
|
|
777
|
-
- **Don't store trivial information** (greetings, acknowledgments, simple file reads)
|
|
957
|
+
- **Don't store trivial information** (greetings, acknowledgments, simple file reads, ls/dir output)
|
|
778
958
|
- **Do store anything you'd want to know if you lost all context**
|
|
779
|
-
- **
|
|
959
|
+
- **Do store anything a different AI agent would need to continue this work**
|
|
960
|
+
- **Use concise titles** (~5-10 words) and structured facts
|
|
780
961
|
- **Include file paths** in filesModified when relevant
|
|
962
|
+
- **Include related concepts** for better searchability
|
|
963
|
+
- **Prefer storing too much over too little** \u2014 the retention system will auto-decay stale memories
|
|
781
964
|
`;
|
|
782
965
|
}
|
|
783
966
|
async function uninstallHooks(agent, projectRoot, global = false) {
|
|
@@ -802,7 +985,7 @@ async function uninstallHooks(agent, projectRoot, global = false) {
|
|
|
802
985
|
}
|
|
803
986
|
async function getHookStatus(projectRoot) {
|
|
804
987
|
const results = [];
|
|
805
|
-
const agents = ["claude", "copilot", "windsurf", "cursor", "kiro", "codex"];
|
|
988
|
+
const agents = ["claude", "copilot", "windsurf", "cursor", "kiro", "codex", "antigravity"];
|
|
806
989
|
for (const agent of agents) {
|
|
807
990
|
const projectPath = getProjectConfigPath(agent, projectRoot);
|
|
808
991
|
const globalPath = getGlobalConfigPath(agent);
|
|
@@ -967,31 +1150,65 @@ function sendError(res, message, status = 500) {
|
|
|
967
1150
|
function filterByProject(items, projectId) {
|
|
968
1151
|
return items.filter((item) => item.projectId === projectId);
|
|
969
1152
|
}
|
|
970
|
-
async function handleApi(req, res, dataDir, projectId, projectName) {
|
|
1153
|
+
async function handleApi(req, res, dataDir, projectId, projectName, baseDir) {
|
|
971
1154
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
972
1155
|
const apiPath = url.pathname.replace("/api", "");
|
|
1156
|
+
const requestedProject = url.searchParams.get("project");
|
|
1157
|
+
let effectiveDataDir = dataDir;
|
|
1158
|
+
let effectiveProjectId = projectId;
|
|
1159
|
+
let effectiveProjectName = projectName;
|
|
1160
|
+
if (requestedProject && requestedProject !== projectId) {
|
|
1161
|
+
const sanitized = requestedProject.replace(/\//g, "--").replace(/[<>:"|?*\\]/g, "_");
|
|
1162
|
+
const candidateDir = path6.join(baseDir, sanitized);
|
|
1163
|
+
try {
|
|
1164
|
+
await fs4.access(candidateDir);
|
|
1165
|
+
effectiveDataDir = candidateDir;
|
|
1166
|
+
effectiveProjectId = requestedProject;
|
|
1167
|
+
effectiveProjectName = requestedProject.split("/").pop() || requestedProject;
|
|
1168
|
+
} catch {
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
973
1171
|
try {
|
|
974
1172
|
switch (apiPath) {
|
|
1173
|
+
case "/projects": {
|
|
1174
|
+
try {
|
|
1175
|
+
const entries = await fs4.readdir(baseDir, { withFileTypes: true });
|
|
1176
|
+
const projects = entries.filter((e) => e.isDirectory() && e.name.includes("--")).map((e) => {
|
|
1177
|
+
const dirName = e.name;
|
|
1178
|
+
const id = dirName.replace(/--/g, "/");
|
|
1179
|
+
return {
|
|
1180
|
+
id,
|
|
1181
|
+
name: id.split("/").pop() || id,
|
|
1182
|
+
dirName,
|
|
1183
|
+
isCurrent: id === projectId
|
|
1184
|
+
};
|
|
1185
|
+
});
|
|
1186
|
+
sendJson(res, projects);
|
|
1187
|
+
} catch {
|
|
1188
|
+
sendJson(res, []);
|
|
1189
|
+
}
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
975
1192
|
case "/project": {
|
|
976
|
-
sendJson(res, { id:
|
|
1193
|
+
sendJson(res, { id: effectiveProjectId, name: effectiveProjectName });
|
|
977
1194
|
break;
|
|
978
1195
|
}
|
|
979
1196
|
case "/graph": {
|
|
980
|
-
const graph = await loadGraphJsonl(
|
|
1197
|
+
const graph = await loadGraphJsonl(effectiveDataDir);
|
|
981
1198
|
sendJson(res, graph);
|
|
982
1199
|
break;
|
|
983
1200
|
}
|
|
984
1201
|
case "/observations": {
|
|
985
|
-
const allObs = await loadObservationsJson(
|
|
986
|
-
const observations2 = filterByProject(allObs,
|
|
1202
|
+
const allObs = await loadObservationsJson(effectiveDataDir);
|
|
1203
|
+
const observations2 = filterByProject(allObs, effectiveProjectId);
|
|
987
1204
|
sendJson(res, observations2);
|
|
988
1205
|
break;
|
|
989
1206
|
}
|
|
990
1207
|
case "/stats": {
|
|
991
|
-
const graph = await loadGraphJsonl(
|
|
992
|
-
const allObs = await loadObservationsJson(
|
|
993
|
-
const observations2 = filterByProject(allObs,
|
|
994
|
-
const nextId2 = await loadIdCounter(
|
|
1208
|
+
const graph = await loadGraphJsonl(effectiveDataDir);
|
|
1209
|
+
const allObs = await loadObservationsJson(effectiveDataDir);
|
|
1210
|
+
const observations2 = filterByProject(allObs, effectiveProjectId);
|
|
1211
|
+
const nextId2 = await loadIdCounter(effectiveDataDir);
|
|
995
1212
|
const typeCounts = {};
|
|
996
1213
|
for (const obs of observations2) {
|
|
997
1214
|
const t = obs.type || "unknown";
|
|
@@ -1009,8 +1226,8 @@ async function handleApi(req, res, dataDir, projectId, projectName) {
|
|
|
1009
1226
|
break;
|
|
1010
1227
|
}
|
|
1011
1228
|
case "/retention": {
|
|
1012
|
-
const allObs = await loadObservationsJson(
|
|
1013
|
-
const observations2 = filterByProject(allObs,
|
|
1229
|
+
const allObs = await loadObservationsJson(effectiveDataDir);
|
|
1230
|
+
const observations2 = filterByProject(allObs, effectiveProjectId);
|
|
1014
1231
|
const now = Date.now();
|
|
1015
1232
|
const scored = observations2.map((obs) => {
|
|
1016
1233
|
const age = now - new Date(obs.createdAt || now).getTime();
|
|
@@ -1087,10 +1304,11 @@ function openBrowser(url) {
|
|
|
1087
1304
|
}
|
|
1088
1305
|
async function startDashboard(dataDir, port, staticDir, projectId, projectName, autoOpen = true) {
|
|
1089
1306
|
const resolvedStaticDir = staticDir;
|
|
1307
|
+
const baseDir = getBaseDataDir();
|
|
1090
1308
|
const server = createServer(async (req, res) => {
|
|
1091
1309
|
const url = req.url || "/";
|
|
1092
1310
|
if (url.startsWith("/api/")) {
|
|
1093
|
-
await handleApi(req, res, dataDir, projectId, projectName);
|
|
1311
|
+
await handleApi(req, res, dataDir, projectId, projectName, baseDir);
|
|
1094
1312
|
} else {
|
|
1095
1313
|
await serveStatic(req, res, resolvedStaticDir);
|
|
1096
1314
|
}
|
|
@@ -1719,12 +1937,14 @@ function detectProject(cwd) {
|
|
|
1719
1937
|
const rootPath = getGitRoot(basePath) ?? findPackageRoot(basePath) ?? basePath;
|
|
1720
1938
|
const gitRemote = getGitRemote(rootPath);
|
|
1721
1939
|
if (gitRemote) {
|
|
1722
|
-
const
|
|
1723
|
-
const name2 =
|
|
1724
|
-
return { id, name: name2, gitRemote, rootPath };
|
|
1940
|
+
const id2 = normalizeGitRemote(gitRemote);
|
|
1941
|
+
const name2 = id2.split("/").pop() ?? path3.basename(rootPath);
|
|
1942
|
+
return { id: id2, name: name2, gitRemote, rootPath };
|
|
1725
1943
|
}
|
|
1726
1944
|
const name = path3.basename(rootPath);
|
|
1727
|
-
|
|
1945
|
+
const id = `local/${name}`;
|
|
1946
|
+
console.error(`[memorix] Warning: no git remote found at ${rootPath}, using fallback projectId: ${id}`);
|
|
1947
|
+
return { id, name, rootPath };
|
|
1728
1948
|
}
|
|
1729
1949
|
function findPackageRoot(cwd) {
|
|
1730
1950
|
let dir = path3.resolve(cwd);
|
|
@@ -3384,6 +3604,14 @@ var OBSERVATION_TYPES = [
|
|
|
3384
3604
|
];
|
|
3385
3605
|
async function createMemorixServer(cwd) {
|
|
3386
3606
|
const project = detectProject(cwd);
|
|
3607
|
+
try {
|
|
3608
|
+
const { migrateGlobalData: migrateGlobalData2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
|
|
3609
|
+
const migrated = await migrateGlobalData2(project.id);
|
|
3610
|
+
if (migrated) {
|
|
3611
|
+
console.error(`[memorix] Migrated legacy data to project directory: ${project.id}`);
|
|
3612
|
+
}
|
|
3613
|
+
} catch {
|
|
3614
|
+
}
|
|
3387
3615
|
const projectDir2 = await getProjectDataDir(project.id);
|
|
3388
3616
|
const graphManager = new KnowledgeGraphManager(projectDir2);
|
|
3389
3617
|
await graphManager.init();
|
|
@@ -3398,15 +3626,14 @@ async function createMemorixServer(cwd) {
|
|
|
3398
3626
|
const { getHookStatus: getHookStatus2, installHooks: installHooks2, detectInstalledAgents: detectInstalledAgents2 } = await Promise.resolve().then(() => (init_installers(), installers_exports));
|
|
3399
3627
|
const workDir = cwd ?? process.cwd();
|
|
3400
3628
|
const statuses = await getHookStatus2(workDir);
|
|
3401
|
-
const
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
}
|
|
3629
|
+
const installedAgents = new Set(statuses.filter((s) => s.installed).map((s) => s.agent));
|
|
3630
|
+
const detectedAgents = await detectInstalledAgents2();
|
|
3631
|
+
for (const agent of detectedAgents) {
|
|
3632
|
+
if (installedAgents.has(agent)) continue;
|
|
3633
|
+
try {
|
|
3634
|
+
const config = await installHooks2(agent, workDir);
|
|
3635
|
+
console.error(`[memorix] Auto-installed hooks for ${agent} \u2192 ${config.configPath}`);
|
|
3636
|
+
} catch {
|
|
3410
3637
|
}
|
|
3411
3638
|
}
|
|
3412
3639
|
} catch {
|
|
@@ -3543,15 +3770,20 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
|
|
|
3543
3770
|
query: z.string().describe("Search query (natural language or keywords)"),
|
|
3544
3771
|
limit: z.number().optional().describe("Max results (default: 20)"),
|
|
3545
3772
|
type: z.enum(OBSERVATION_TYPES).optional().describe("Filter by observation type"),
|
|
3546
|
-
maxTokens: z.number().optional().describe("Token budget \u2014 trim results to fit (0 = unlimited)")
|
|
3773
|
+
maxTokens: z.number().optional().describe("Token budget \u2014 trim results to fit (0 = unlimited)"),
|
|
3774
|
+
scope: z.enum(["project", "global"]).optional().describe(
|
|
3775
|
+
'Search scope: "project" (default) only searches current project, "global" searches all projects'
|
|
3776
|
+
)
|
|
3547
3777
|
}
|
|
3548
3778
|
},
|
|
3549
|
-
async ({ query, limit, type, maxTokens }) => {
|
|
3779
|
+
async ({ query, limit, type, maxTokens, scope }) => {
|
|
3550
3780
|
const result = await compactSearch({
|
|
3551
3781
|
query,
|
|
3552
3782
|
limit,
|
|
3553
3783
|
type,
|
|
3554
|
-
maxTokens
|
|
3784
|
+
maxTokens,
|
|
3785
|
+
// Default to current project scope; 'global' removes the project filter
|
|
3786
|
+
projectId: scope === "global" ? void 0 : project.id
|
|
3555
3787
|
});
|
|
3556
3788
|
let text = result.formatted;
|
|
3557
3789
|
if (!syncAdvisoryShown && syncAdvisory) {
|