@stackmemoryai/stackmemory 1.10.2 → 1.10.3
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/dist/src/cli/index.js
CHANGED
|
@@ -198,6 +198,8 @@ program.command("status").description("Show current StackMemory status").option(
|
|
|
198
198
|
await UpdateChecker.checkForUpdates(VERSION);
|
|
199
199
|
await sessionManager.initialize();
|
|
200
200
|
await sharedContextLayer.initialize();
|
|
201
|
+
const { initObsidianVault } = await import("../core/storage/obsidian-vault-adapter.js");
|
|
202
|
+
await initObsidianVault();
|
|
201
203
|
const session = await sessionManager.getOrCreateSession({
|
|
202
204
|
projectPath: projectRoot,
|
|
203
205
|
sessionId: options.session
|
|
@@ -484,18 +486,24 @@ program.command("board").description("Open the StackMemory Board (agent kanban +
|
|
|
484
486
|
const { spawn: spawnProc } = await import("child_process");
|
|
485
487
|
const { join: join2 } = await import("path");
|
|
486
488
|
const { existsSync: existsSync2 } = await import("fs");
|
|
489
|
+
const { fileURLToPath: toPath } = await import("url");
|
|
490
|
+
const pkgRoot = join2(
|
|
491
|
+
toPath(import.meta.url),
|
|
492
|
+
"..",
|
|
493
|
+
"..",
|
|
494
|
+
"..",
|
|
495
|
+
"..",
|
|
496
|
+
"tools",
|
|
497
|
+
"agent-viewer",
|
|
498
|
+
"server.cjs"
|
|
499
|
+
);
|
|
487
500
|
const candidates = [
|
|
488
501
|
join2(process.cwd(), "tools", "agent-viewer", "server.js"),
|
|
489
|
-
|
|
490
|
-
process.env.PROVENANTAI_ROOT || "",
|
|
491
|
-
"tools",
|
|
492
|
-
"agent-viewer",
|
|
493
|
-
"server.js"
|
|
494
|
-
),
|
|
502
|
+
pkgRoot,
|
|
495
503
|
join2(
|
|
496
504
|
process.env.HOME || "",
|
|
497
505
|
"Dev",
|
|
498
|
-
"
|
|
506
|
+
"stackmemory",
|
|
499
507
|
"tools",
|
|
500
508
|
"agent-viewer",
|
|
501
509
|
"server.js"
|
|
@@ -504,7 +512,7 @@ program.command("board").description("Open the StackMemory Board (agent kanban +
|
|
|
504
512
|
const serverPath = candidates.find((c) => existsSync2(c));
|
|
505
513
|
if (!serverPath) {
|
|
506
514
|
console.error(
|
|
507
|
-
"Board server not found. Run from
|
|
515
|
+
"Board server not found. Run from the stackmemory repo or install globally."
|
|
508
516
|
);
|
|
509
517
|
process.exit(1);
|
|
510
518
|
}
|
|
@@ -513,6 +521,19 @@ program.command("board").description("Open the StackMemory Board (agent kanban +
|
|
|
513
521
|
stdio: "inherit",
|
|
514
522
|
env: { ...process.env, FORCE_COLOR: "1" }
|
|
515
523
|
});
|
|
524
|
+
if (options.open !== false) {
|
|
525
|
+
setTimeout(async () => {
|
|
526
|
+
const url = `http://localhost:${options.port}`;
|
|
527
|
+
const cp = await import("child_process");
|
|
528
|
+
try {
|
|
529
|
+
cp.execSync(
|
|
530
|
+
process.platform === "darwin" ? `open ${url}` : `xdg-open ${url}`,
|
|
531
|
+
{ stdio: "ignore" }
|
|
532
|
+
);
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
}, 1e3);
|
|
536
|
+
}
|
|
516
537
|
child.on("close", (code) => process.exit(code || 0));
|
|
517
538
|
process.on("SIGINT", () => {
|
|
518
539
|
child.kill("SIGINT");
|
|
@@ -77,7 +77,8 @@ class ConfigManager {
|
|
|
77
77
|
},
|
|
78
78
|
performance: { ...DEFAULT_CONFIG.performance, ...loaded.performance },
|
|
79
79
|
enrichment: { ...DEFAULT_ENRICHMENT, ...loaded.enrichment },
|
|
80
|
-
profiles: { ...PRESET_PROFILES, ...loaded.profiles }
|
|
80
|
+
profiles: { ...PRESET_PROFILES, ...loaded.profiles },
|
|
81
|
+
obsidian: loaded.obsidian
|
|
81
82
|
};
|
|
82
83
|
if (config.profile && config.profiles?.[config.profile]) {
|
|
83
84
|
this.applyProfile(config, config.profiles[config.profile]);
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
watch
|
|
12
|
+
} from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { logger } from "../monitoring/logger.js";
|
|
15
|
+
import { frameLifecycleHooks } from "../context/frame-lifecycle-hooks.js";
|
|
16
|
+
class ObsidianVaultAdapter {
|
|
17
|
+
config;
|
|
18
|
+
dirs;
|
|
19
|
+
watcher = null;
|
|
20
|
+
ingestCallback = null;
|
|
21
|
+
seenRawFiles = /* @__PURE__ */ new Set();
|
|
22
|
+
unregisterCreate = null;
|
|
23
|
+
unregisterClose = null;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = {
|
|
26
|
+
vaultPath: config.vaultPath,
|
|
27
|
+
subdir: config.subdir ?? "stackmemory",
|
|
28
|
+
watchRaw: config.watchRaw ?? true,
|
|
29
|
+
autoIndex: config.autoIndex ?? true,
|
|
30
|
+
includeEvents: config.includeEvents ?? false
|
|
31
|
+
};
|
|
32
|
+
const root = join(this.config.vaultPath, this.config.subdir);
|
|
33
|
+
this.dirs = {
|
|
34
|
+
root,
|
|
35
|
+
frames: join(root, "frames"),
|
|
36
|
+
raw: join(root, "raw"),
|
|
37
|
+
sessions: join(root, "sessions")
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Initialize vault directory structure and register lifecycle hooks */
|
|
41
|
+
async initialize() {
|
|
42
|
+
for (const dir of Object.values(this.dirs)) {
|
|
43
|
+
if (!existsSync(dir)) {
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const rawReadme = join(this.dirs.raw, "README.md");
|
|
48
|
+
if (!existsSync(rawReadme)) {
|
|
49
|
+
writeFileSync(
|
|
50
|
+
rawReadme,
|
|
51
|
+
[
|
|
52
|
+
"# Raw Ingest",
|
|
53
|
+
"",
|
|
54
|
+
"Drop .md files here (e.g., from Obsidian Web Clipper).",
|
|
55
|
+
"StackMemory will auto-ingest them as frames.",
|
|
56
|
+
"",
|
|
57
|
+
"## Setup",
|
|
58
|
+
"",
|
|
59
|
+
"1. Install [Obsidian Web Clipper](https://obsidian.md/clipper)",
|
|
60
|
+
"2. Set the clip destination to this folder",
|
|
61
|
+
"3. StackMemory watches for new files and ingests them"
|
|
62
|
+
].join("\n")
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
this.unregisterCreate = frameLifecycleHooks.onFrameCreated(
|
|
66
|
+
"obsidian-vault",
|
|
67
|
+
async (frame) => {
|
|
68
|
+
await this.writeFrame(frame);
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
this.unregisterClose = frameLifecycleHooks.onFrameClosed(
|
|
72
|
+
"obsidian-vault",
|
|
73
|
+
async (data) => {
|
|
74
|
+
await this.writeFrame(data.frame, data.events, data.anchors);
|
|
75
|
+
if (this.config.autoIndex) {
|
|
76
|
+
await this.updateIndex();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
if (this.config.watchRaw) {
|
|
81
|
+
this.startWatching();
|
|
82
|
+
}
|
|
83
|
+
if (this.config.autoIndex) {
|
|
84
|
+
await this.updateIndex();
|
|
85
|
+
}
|
|
86
|
+
logger.info("Obsidian vault adapter initialized", {
|
|
87
|
+
vault: this.config.vaultPath,
|
|
88
|
+
subdir: this.config.subdir
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Set callback for when raw files are ingested */
|
|
92
|
+
onIngest(callback) {
|
|
93
|
+
this.ingestCallback = callback;
|
|
94
|
+
}
|
|
95
|
+
/** Write a frame as a .md file */
|
|
96
|
+
async writeFrame(frame, events, anchors) {
|
|
97
|
+
const filename = this.frameFilename(frame);
|
|
98
|
+
const filepath = join(this.dirs.frames, filename);
|
|
99
|
+
const content = this.serializeFrame(frame, events, anchors);
|
|
100
|
+
writeFileSync(filepath, content);
|
|
101
|
+
logger.debug("Wrote frame to vault", {
|
|
102
|
+
frameId: frame.frame_id,
|
|
103
|
+
file: filename
|
|
104
|
+
});
|
|
105
|
+
return filepath;
|
|
106
|
+
}
|
|
107
|
+
/** Write a session summary */
|
|
108
|
+
async writeSessionSummary(sessionId, summary, frames) {
|
|
109
|
+
const filename = `session-${sessionId.slice(0, 12)}.md`;
|
|
110
|
+
const filepath = join(this.dirs.sessions, filename);
|
|
111
|
+
const frameLinks = frames.map(
|
|
112
|
+
(f) => `- [[${this.frameFilename(f).replace(".md", "")}|${f.name}]] (${f.type})`
|
|
113
|
+
).join("\n");
|
|
114
|
+
const content = [
|
|
115
|
+
"---",
|
|
116
|
+
`session_id: "${sessionId}"`,
|
|
117
|
+
`created: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
118
|
+
`frame_count: ${frames.length}`,
|
|
119
|
+
"---",
|
|
120
|
+
"",
|
|
121
|
+
`# Session ${sessionId.slice(0, 12)}`,
|
|
122
|
+
"",
|
|
123
|
+
summary,
|
|
124
|
+
"",
|
|
125
|
+
"## Frames",
|
|
126
|
+
"",
|
|
127
|
+
frameLinks
|
|
128
|
+
].join("\n");
|
|
129
|
+
writeFileSync(filepath, content);
|
|
130
|
+
return filepath;
|
|
131
|
+
}
|
|
132
|
+
/** Update the auto-maintained index.md */
|
|
133
|
+
async updateIndex() {
|
|
134
|
+
const framesDir = this.dirs.frames;
|
|
135
|
+
if (!existsSync(framesDir)) return;
|
|
136
|
+
const files = readdirSync(framesDir).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
137
|
+
const frameLinks = files.slice(0, 100).map((f) => {
|
|
138
|
+
const name = f.replace(".md", "");
|
|
139
|
+
return `- [[frames/${name}]]`;
|
|
140
|
+
});
|
|
141
|
+
const typeCounts = {};
|
|
142
|
+
for (const f of files) {
|
|
143
|
+
const match = f.match(/^(\w+)-/);
|
|
144
|
+
if (match) {
|
|
145
|
+
typeCounts[match[1]] = (typeCounts[match[1]] || 0) + 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const typeStats = Object.entries(typeCounts).map(([type, count]) => `| ${type} | ${count} |`).join("\n");
|
|
149
|
+
const sessionsDir = this.dirs.sessions;
|
|
150
|
+
const sessionFiles = existsSync(sessionsDir) ? readdirSync(sessionsDir).filter(
|
|
151
|
+
(f) => f.endsWith(".md") && f !== "README.md"
|
|
152
|
+
) : [];
|
|
153
|
+
const sessionLinks = sessionFiles.slice(0, 20).map((f) => `- [[sessions/${f.replace(".md", "")}]]`);
|
|
154
|
+
const content = [
|
|
155
|
+
"---",
|
|
156
|
+
`updated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
157
|
+
`total_frames: ${files.length}`,
|
|
158
|
+
"---",
|
|
159
|
+
"",
|
|
160
|
+
"# StackMemory Index",
|
|
161
|
+
"",
|
|
162
|
+
`> Auto-maintained by StackMemory. ${files.length} frames indexed.`,
|
|
163
|
+
"",
|
|
164
|
+
"## Frame Types",
|
|
165
|
+
"",
|
|
166
|
+
"| Type | Count |",
|
|
167
|
+
"|------|-------|",
|
|
168
|
+
typeStats,
|
|
169
|
+
"",
|
|
170
|
+
"## Recent Frames",
|
|
171
|
+
"",
|
|
172
|
+
...frameLinks.slice(0, 30),
|
|
173
|
+
files.length > 30 ? `
|
|
174
|
+
_...and ${files.length - 30} more_` : "",
|
|
175
|
+
"",
|
|
176
|
+
sessionLinks.length > 0 ? "## Sessions\n" : "",
|
|
177
|
+
...sessionLinks,
|
|
178
|
+
"",
|
|
179
|
+
"## Raw Ingest",
|
|
180
|
+
"",
|
|
181
|
+
"[[raw/README|Drop web clipper files in raw/]]"
|
|
182
|
+
].join("\n");
|
|
183
|
+
writeFileSync(join(this.dirs.root, "index.md"), content);
|
|
184
|
+
}
|
|
185
|
+
/** Serialize a Frame to Obsidian-flavored markdown */
|
|
186
|
+
serializeFrame(frame, events, anchors) {
|
|
187
|
+
const lines = [];
|
|
188
|
+
lines.push("---");
|
|
189
|
+
lines.push(`frame_id: "${frame.frame_id}"`);
|
|
190
|
+
lines.push(`type: "${frame.type}"`);
|
|
191
|
+
lines.push(`name: "${this.escapeYaml(frame.name)}"`);
|
|
192
|
+
lines.push(`state: "${frame.state}"`);
|
|
193
|
+
lines.push(`depth: ${frame.depth}`);
|
|
194
|
+
lines.push(`project_id: "${frame.project_id}"`);
|
|
195
|
+
lines.push(`run_id: "${frame.run_id}"`);
|
|
196
|
+
lines.push(`created_at: ${frame.created_at}`);
|
|
197
|
+
if (frame.closed_at) lines.push(`closed_at: ${frame.closed_at}`);
|
|
198
|
+
if (frame.parent_frame_id)
|
|
199
|
+
lines.push(`parent_frame_id: "${frame.parent_frame_id}"`);
|
|
200
|
+
lines.push(`tags: [stackmemory, ${frame.type}]`);
|
|
201
|
+
lines.push("---");
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push(`# ${frame.name}`);
|
|
204
|
+
lines.push("");
|
|
205
|
+
if (frame.parent_frame_id) {
|
|
206
|
+
lines.push(
|
|
207
|
+
`Parent: [[${frame.type}-${frame.parent_frame_id.slice(0, 8)}]]`
|
|
208
|
+
);
|
|
209
|
+
lines.push("");
|
|
210
|
+
}
|
|
211
|
+
lines.push(
|
|
212
|
+
`**Type:** \`${frame.type}\` | **State:** \`${frame.state}\` | **Depth:** ${frame.depth}`
|
|
213
|
+
);
|
|
214
|
+
lines.push(`**Created:** ${new Date(frame.created_at).toISOString()}`);
|
|
215
|
+
if (frame.closed_at) {
|
|
216
|
+
const duration = Math.round((frame.closed_at - frame.created_at) / 1e3);
|
|
217
|
+
lines.push(
|
|
218
|
+
`**Closed:** ${new Date(frame.closed_at).toISOString()} (${duration}s)`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
lines.push("");
|
|
222
|
+
if (frame.digest_text) {
|
|
223
|
+
lines.push("## Digest");
|
|
224
|
+
lines.push("");
|
|
225
|
+
lines.push(frame.digest_text);
|
|
226
|
+
lines.push("");
|
|
227
|
+
}
|
|
228
|
+
if (frame.inputs && Object.keys(frame.inputs).length > 0) {
|
|
229
|
+
lines.push("## Inputs");
|
|
230
|
+
lines.push("");
|
|
231
|
+
lines.push("```json");
|
|
232
|
+
lines.push(JSON.stringify(frame.inputs, null, 2));
|
|
233
|
+
lines.push("```");
|
|
234
|
+
lines.push("");
|
|
235
|
+
}
|
|
236
|
+
if (frame.outputs && Object.keys(frame.outputs).length > 0) {
|
|
237
|
+
lines.push("## Outputs");
|
|
238
|
+
lines.push("");
|
|
239
|
+
lines.push("```json");
|
|
240
|
+
lines.push(JSON.stringify(frame.outputs, null, 2));
|
|
241
|
+
lines.push("```");
|
|
242
|
+
lines.push("");
|
|
243
|
+
}
|
|
244
|
+
if (anchors && anchors.length > 0) {
|
|
245
|
+
lines.push("## Anchors");
|
|
246
|
+
lines.push("");
|
|
247
|
+
for (const anchor of anchors) {
|
|
248
|
+
lines.push(`### ${anchor.type}: ${this.escapeYaml(anchor.content)}`);
|
|
249
|
+
lines.push("");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (this.config.includeEvents && events && events.length > 0) {
|
|
253
|
+
lines.push("## Events");
|
|
254
|
+
lines.push("");
|
|
255
|
+
for (const event of events.slice(-20)) {
|
|
256
|
+
const ts = new Date(event.ts).toISOString().substring(11, 19);
|
|
257
|
+
lines.push(
|
|
258
|
+
`- \`${ts}\` **${event.event_type}** ${this.summarizePayload(event.payload)}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
lines.push("");
|
|
262
|
+
}
|
|
263
|
+
return lines.join("\n");
|
|
264
|
+
}
|
|
265
|
+
/** Generate a filename for a frame */
|
|
266
|
+
frameFilename(frame) {
|
|
267
|
+
const id = frame.frame_id.slice(0, 8);
|
|
268
|
+
const name = frame.name.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/-+/g, "-").slice(0, 40);
|
|
269
|
+
return `${frame.type}-${id}-${name}.md`;
|
|
270
|
+
}
|
|
271
|
+
/** Escape special YAML characters */
|
|
272
|
+
escapeYaml(s) {
|
|
273
|
+
return s.replace(/"/g, '\\"').replace(/\n/g, " ");
|
|
274
|
+
}
|
|
275
|
+
/** Summarize event payload for compact display */
|
|
276
|
+
summarizePayload(payload) {
|
|
277
|
+
const str = JSON.stringify(payload);
|
|
278
|
+
return str.length > 120 ? str.slice(0, 117) + "..." : str;
|
|
279
|
+
}
|
|
280
|
+
// ── Raw File Watcher ──
|
|
281
|
+
/** Watch raw/ directory for new .md files from web clipper */
|
|
282
|
+
startWatching() {
|
|
283
|
+
if (!existsSync(this.dirs.raw)) return;
|
|
284
|
+
for (const f of readdirSync(this.dirs.raw)) {
|
|
285
|
+
this.seenRawFiles.add(f);
|
|
286
|
+
}
|
|
287
|
+
this.watcher = watch(this.dirs.raw, async (eventType, filename) => {
|
|
288
|
+
if (!filename || !filename.endsWith(".md")) return;
|
|
289
|
+
if (filename === "README.md") return;
|
|
290
|
+
if (this.seenRawFiles.has(filename)) return;
|
|
291
|
+
const filepath = join(this.dirs.raw, filename);
|
|
292
|
+
if (!existsSync(filepath)) return;
|
|
293
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
294
|
+
if (!existsSync(filepath)) return;
|
|
295
|
+
this.seenRawFiles.add(filename);
|
|
296
|
+
logger.info("Raw file detected for ingest", { filename });
|
|
297
|
+
try {
|
|
298
|
+
const content = readFileSync(filepath, "utf-8");
|
|
299
|
+
const metadata = this.parseClipperMetadata(content);
|
|
300
|
+
if (this.ingestCallback) {
|
|
301
|
+
await this.ingestCallback(filename, content, metadata);
|
|
302
|
+
}
|
|
303
|
+
logger.info("Ingested raw file", {
|
|
304
|
+
filename,
|
|
305
|
+
source: metadata.source || "unknown"
|
|
306
|
+
});
|
|
307
|
+
} catch (err) {
|
|
308
|
+
logger.error("Failed to ingest raw file", {
|
|
309
|
+
filename,
|
|
310
|
+
error: err.message
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
logger.info("Watching raw/ for web clipper files", { dir: this.dirs.raw });
|
|
315
|
+
}
|
|
316
|
+
/** Parse Obsidian Web Clipper YAML frontmatter */
|
|
317
|
+
parseClipperMetadata(content) {
|
|
318
|
+
const metadata = {};
|
|
319
|
+
if (!content.startsWith("---")) return metadata;
|
|
320
|
+
const end = content.indexOf("---", 3);
|
|
321
|
+
if (end === -1) return metadata;
|
|
322
|
+
const yaml = content.slice(3, end).trim();
|
|
323
|
+
for (const line of yaml.split("\n")) {
|
|
324
|
+
const match = line.match(/^(\w+):\s*(.+)/);
|
|
325
|
+
if (match) {
|
|
326
|
+
metadata[match[1]] = match[2].replace(/^["']|["']$/g, "");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return metadata;
|
|
330
|
+
}
|
|
331
|
+
/** Stop watching and clean up */
|
|
332
|
+
async cleanup() {
|
|
333
|
+
if (this.watcher) {
|
|
334
|
+
this.watcher.close();
|
|
335
|
+
this.watcher = null;
|
|
336
|
+
}
|
|
337
|
+
if (this.unregisterCreate) this.unregisterCreate();
|
|
338
|
+
if (this.unregisterClose) this.unregisterClose();
|
|
339
|
+
logger.info("Obsidian vault adapter cleaned up");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
let _instance = null;
|
|
343
|
+
async function initObsidianVault() {
|
|
344
|
+
if (_instance) return _instance;
|
|
345
|
+
try {
|
|
346
|
+
const configPath = join(process.cwd(), ".stackmemory", "config.yaml");
|
|
347
|
+
if (!existsSync(configPath)) return null;
|
|
348
|
+
const content = readFileSync(configPath, "utf-8");
|
|
349
|
+
const vaultMatch = content.match(
|
|
350
|
+
/obsidian:\s*\n\s+vaultPath:\s*["']?([^\n"']+)/
|
|
351
|
+
);
|
|
352
|
+
if (!vaultMatch) return null;
|
|
353
|
+
const vaultPath = vaultMatch[1].trim();
|
|
354
|
+
if (!vaultPath || !existsSync(vaultPath)) {
|
|
355
|
+
logger.warn("Obsidian vaultPath configured but directory not found", {
|
|
356
|
+
vaultPath
|
|
357
|
+
});
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const subdirMatch = content.match(
|
|
361
|
+
/obsidian:\s*\n(?:\s+\w+:.*\n)*\s+subdir:\s*["']?([^\n"']+)/
|
|
362
|
+
);
|
|
363
|
+
const watchRawMatch = content.match(
|
|
364
|
+
/obsidian:\s*\n(?:\s+\w+:.*\n)*\s+watchRaw:\s*(true|false)/
|
|
365
|
+
);
|
|
366
|
+
const autoIndexMatch = content.match(
|
|
367
|
+
/obsidian:\s*\n(?:\s+\w+:.*\n)*\s+autoIndex:\s*(true|false)/
|
|
368
|
+
);
|
|
369
|
+
_instance = new ObsidianVaultAdapter({
|
|
370
|
+
vaultPath,
|
|
371
|
+
subdir: subdirMatch?.[1]?.trim(),
|
|
372
|
+
watchRaw: watchRawMatch ? watchRawMatch[1] === "true" : void 0,
|
|
373
|
+
autoIndex: autoIndexMatch ? autoIndexMatch[1] === "true" : void 0
|
|
374
|
+
});
|
|
375
|
+
await _instance.initialize();
|
|
376
|
+
logger.info("Obsidian vault adapter auto-initialized", { vaultPath });
|
|
377
|
+
return _instance;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
logger.debug("Obsidian vault adapter not initialized", {
|
|
380
|
+
error: err.message
|
|
381
|
+
});
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function getObsidianVault() {
|
|
386
|
+
return _instance;
|
|
387
|
+
}
|
|
388
|
+
export {
|
|
389
|
+
ObsidianVaultAdapter,
|
|
390
|
+
getObsidianVault,
|
|
391
|
+
initObsidianVault
|
|
392
|
+
};
|
|
@@ -159,6 +159,8 @@ class LocalStackMemoryMCP {
|
|
|
159
159
|
this.browserMCP.initialize(this.server).catch((error) => {
|
|
160
160
|
logger.error("Failed to initialize Browser MCP", error);
|
|
161
161
|
});
|
|
162
|
+
import("../../core/storage/obsidian-vault-adapter.js").then(({ initObsidianVault }) => initObsidianVault()).catch(() => {
|
|
163
|
+
});
|
|
162
164
|
logger.info("StackMemory MCP Server initialized", {
|
|
163
165
|
projectRoot: this.projectRoot,
|
|
164
166
|
projectId: this.projectId
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.3",
|
|
4
4
|
"description": "Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, and automatic hooks.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|