@stackmemoryai/stackmemory 1.10.3 → 1.10.4
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/commands/wiki.js +265 -0
- package/dist/src/cli/index.js +2 -0
- package/dist/src/core/storage/obsidian-vault-adapter.js +19 -1
- package/dist/src/core/wiki/wiki-compiler.js +792 -0
- package/dist/src/utils/hook-installer.js +7 -0
- package/package.json +1 -1
- package/templates/claude-hooks/wiki-update.js +373 -0
|
@@ -0,0 +1,265 @@
|
|
|
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 { Command } from "commander";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import {
|
|
10
|
+
WikiCompiler
|
|
11
|
+
} from "../../core/wiki/wiki-compiler.js";
|
|
12
|
+
import {
|
|
13
|
+
generateChronologicalDigest
|
|
14
|
+
} from "../../core/digest/chronological-digest.js";
|
|
15
|
+
function resolveWikiDir() {
|
|
16
|
+
const configPath = join(process.cwd(), ".stackmemory", "config.yaml");
|
|
17
|
+
if (!existsSync(configPath)) return null;
|
|
18
|
+
const content = readFileSync(configPath, "utf-8");
|
|
19
|
+
const vaultMatch = content.match(
|
|
20
|
+
/obsidian:\s*\n\s+vaultPath:\s*["']?([^\n"']+)/
|
|
21
|
+
);
|
|
22
|
+
if (!vaultMatch) return null;
|
|
23
|
+
const vaultPath = (vaultMatch[1] ?? "").trim();
|
|
24
|
+
const subdirMatch = content.match(
|
|
25
|
+
/obsidian:\s*\n(?:\s+\w+:.*\n)*\s+subdir:\s*["']?([^\n"']+)/
|
|
26
|
+
);
|
|
27
|
+
const subdir = subdirMatch?.[1]?.trim() || "stackmemory";
|
|
28
|
+
return join(vaultPath, subdir, "wiki");
|
|
29
|
+
}
|
|
30
|
+
function getCompiler(wikiDirOverride) {
|
|
31
|
+
const wikiDir = wikiDirOverride || resolveWikiDir();
|
|
32
|
+
if (!wikiDir) {
|
|
33
|
+
console.error(
|
|
34
|
+
chalk.red(
|
|
35
|
+
"Wiki not configured. Set obsidian.vaultPath in .stackmemory/config.yaml"
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
console.error(chalk.gray(" Or pass --wiki-dir <path>"));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
return new WikiCompiler({ wikiDir });
|
|
42
|
+
}
|
|
43
|
+
async function openDb() {
|
|
44
|
+
const dbPath = join(process.cwd(), ".stackmemory", "context.db");
|
|
45
|
+
if (!existsSync(dbPath)) {
|
|
46
|
+
console.error(
|
|
47
|
+
chalk.red('StackMemory not initialized. Run "stackmemory init" first.')
|
|
48
|
+
);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const { default: Database } = await import("better-sqlite3");
|
|
52
|
+
return new Database(dbPath);
|
|
53
|
+
}
|
|
54
|
+
function queryContext(db, opts) {
|
|
55
|
+
const since = opts.sinceEpoch ?? 0;
|
|
56
|
+
const limit = opts.limit ?? 1e4;
|
|
57
|
+
const digests = db.prepare(
|
|
58
|
+
`SELECT frame_id, name as frame_name, type as frame_type,
|
|
59
|
+
digest_text, created_at, closed_at
|
|
60
|
+
FROM frames
|
|
61
|
+
WHERE state = 'closed' AND digest_text IS NOT NULL
|
|
62
|
+
AND created_at >= ?
|
|
63
|
+
ORDER BY created_at DESC
|
|
64
|
+
LIMIT ?`
|
|
65
|
+
).all(since, limit);
|
|
66
|
+
const entities = db.prepare(
|
|
67
|
+
`SELECT entity_name, relation, value, context,
|
|
68
|
+
source_frame_id, valid_from, superseded_at
|
|
69
|
+
FROM entity_states
|
|
70
|
+
WHERE valid_from >= ?
|
|
71
|
+
ORDER BY valid_from DESC
|
|
72
|
+
LIMIT ?`
|
|
73
|
+
).all(since, limit);
|
|
74
|
+
const anchors = db.prepare(
|
|
75
|
+
`SELECT a.anchor_id, a.frame_id, f.name as frame_name,
|
|
76
|
+
a.type, a.text, a.priority, a.created_at
|
|
77
|
+
FROM anchors a
|
|
78
|
+
JOIN frames f ON f.frame_id = a.frame_id
|
|
79
|
+
WHERE a.created_at >= ?
|
|
80
|
+
ORDER BY a.created_at DESC
|
|
81
|
+
LIMIT ?`
|
|
82
|
+
).all(since, limit);
|
|
83
|
+
return { digests, entities, anchors };
|
|
84
|
+
}
|
|
85
|
+
function createWikiCommand() {
|
|
86
|
+
const cmd = new Command("wiki").description(
|
|
87
|
+
"LLM knowledge base \u2014 compile context into a persistent wiki"
|
|
88
|
+
);
|
|
89
|
+
cmd.command("create").description("Generate wiki from all existing context").option("--wiki-dir <path>", "Override wiki directory").option(
|
|
90
|
+
"--period <period>",
|
|
91
|
+
"Include session digest (today|yesterday|week)",
|
|
92
|
+
"week"
|
|
93
|
+
).option("--json", "Output as JSON").action(async (options) => {
|
|
94
|
+
const compiler = getCompiler(options.wikiDir);
|
|
95
|
+
await compiler.initialize();
|
|
96
|
+
const db = await openDb();
|
|
97
|
+
const ctx = queryContext(db, {});
|
|
98
|
+
let sessionDigest;
|
|
99
|
+
try {
|
|
100
|
+
const projectId = db.prepare(
|
|
101
|
+
`SELECT project_id FROM frames ORDER BY created_at DESC LIMIT 1`
|
|
102
|
+
).get()?.project_id || "default";
|
|
103
|
+
const content = generateChronologicalDigest(
|
|
104
|
+
db,
|
|
105
|
+
options.period,
|
|
106
|
+
projectId
|
|
107
|
+
);
|
|
108
|
+
sessionDigest = {
|
|
109
|
+
period: options.period,
|
|
110
|
+
content,
|
|
111
|
+
generatedAt: Date.now()
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
db.close();
|
|
116
|
+
const result = await compiler.create({
|
|
117
|
+
...ctx,
|
|
118
|
+
sessionDigest
|
|
119
|
+
});
|
|
120
|
+
if (options.json) {
|
|
121
|
+
console.log(JSON.stringify(result, null, 2));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
console.log(chalk.green("\nWiki created."));
|
|
125
|
+
console.log(chalk.gray(` Digests compiled: ${ctx.digests.length}`));
|
|
126
|
+
console.log(chalk.gray(` Entity states: ${ctx.entities.length}`));
|
|
127
|
+
console.log(chalk.gray(` Anchors processed: ${ctx.anchors.length}`));
|
|
128
|
+
console.log(chalk.gray(` Articles created: ${result.created.length}`));
|
|
129
|
+
console.log(chalk.gray(` Total articles: ${result.totalArticles}`));
|
|
130
|
+
});
|
|
131
|
+
cmd.command("update").description("Incrementally update wiki with new context").option("--wiki-dir <path>", "Override wiki directory").option(
|
|
132
|
+
"--since <date>",
|
|
133
|
+
"Update from this date (ISO format, default: last compile)"
|
|
134
|
+
).option("--json", "Output as JSON").action(async (options) => {
|
|
135
|
+
const compiler = getCompiler(options.wikiDir);
|
|
136
|
+
await compiler.initialize();
|
|
137
|
+
const db = await openDb();
|
|
138
|
+
let sinceEpoch;
|
|
139
|
+
if (options.since) {
|
|
140
|
+
sinceEpoch = Math.floor(new Date(options.since).getTime() / 1e3);
|
|
141
|
+
} else {
|
|
142
|
+
const lastCompile = compiler.getLastCompileTime();
|
|
143
|
+
sinceEpoch = lastCompile ?? 0;
|
|
144
|
+
}
|
|
145
|
+
if (sinceEpoch === 0) {
|
|
146
|
+
console.log(
|
|
147
|
+
chalk.yellow(
|
|
148
|
+
"No previous compile found. Running full create instead."
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
db.close();
|
|
152
|
+
cmd.commands.find((c) => c.name() === "create")?.parseAsync(["create", "--wiki-dir", compiler["config"].wikiDir], {
|
|
153
|
+
from: "user"
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const ctx = queryContext(db, { sinceEpoch });
|
|
158
|
+
db.close();
|
|
159
|
+
if (ctx.digests.length === 0 && ctx.entities.length === 0 && ctx.anchors.length === 0) {
|
|
160
|
+
console.log(chalk.yellow("No new context since last compile."));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const result = await compiler.update(ctx);
|
|
164
|
+
if (options.json) {
|
|
165
|
+
console.log(JSON.stringify(result, null, 2));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
console.log(chalk.green("\nWiki updated."));
|
|
169
|
+
console.log(chalk.gray(` New digests: ${ctx.digests.length}`));
|
|
170
|
+
console.log(chalk.gray(` New entity states: ${ctx.entities.length}`));
|
|
171
|
+
console.log(chalk.gray(` New anchors: ${ctx.anchors.length}`));
|
|
172
|
+
console.log(chalk.gray(` Articles created: ${result.created.length}`));
|
|
173
|
+
console.log(chalk.gray(` Articles updated: ${result.updated.length}`));
|
|
174
|
+
console.log(chalk.gray(` Total articles: ${result.totalArticles}`));
|
|
175
|
+
});
|
|
176
|
+
cmd.command("lint").description("Health check the wiki for issues").option("--wiki-dir <path>", "Override wiki directory").option("--json", "Output as JSON").action(async (options) => {
|
|
177
|
+
const compiler = getCompiler(options.wikiDir);
|
|
178
|
+
await compiler.initialize();
|
|
179
|
+
const result = await compiler.lint();
|
|
180
|
+
if (options.json) {
|
|
181
|
+
console.log(JSON.stringify(result, null, 2));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
console.log(chalk.cyan("\nWiki Lint Report"));
|
|
185
|
+
console.log(chalk.gray(` Total articles: ${result.totalArticles}`));
|
|
186
|
+
if (result.orphans.length > 0) {
|
|
187
|
+
console.log(
|
|
188
|
+
chalk.yellow(
|
|
189
|
+
`
|
|
190
|
+
Orphan pages (no inbound links): ${result.orphans.length}`
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
result.orphans.slice(0, 10).forEach((o) => console.log(chalk.gray(` - ${o}`)));
|
|
194
|
+
if (result.orphans.length > 10) {
|
|
195
|
+
console.log(
|
|
196
|
+
chalk.gray(` ...and ${result.orphans.length - 10} more`)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (result.brokenLinks.length > 0) {
|
|
201
|
+
console.log(
|
|
202
|
+
chalk.red(`
|
|
203
|
+
Broken links: ${result.brokenLinks.length}`)
|
|
204
|
+
);
|
|
205
|
+
result.brokenLinks.slice(0, 10).forEach(
|
|
206
|
+
(l) => console.log(chalk.gray(` - ${l.source} -> ${l.target}`))
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (result.stale.length > 0) {
|
|
210
|
+
console.log(
|
|
211
|
+
chalk.yellow(`
|
|
212
|
+
Stale articles (>30 days): ${result.stale.length}`)
|
|
213
|
+
);
|
|
214
|
+
result.stale.slice(0, 10).forEach((s) => console.log(chalk.gray(` - ${s}`)));
|
|
215
|
+
}
|
|
216
|
+
if (result.orphans.length === 0 && result.brokenLinks.length === 0 && result.stale.length === 0) {
|
|
217
|
+
console.log(chalk.green("\n No issues found."));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
cmd.command("search <query>").description("Search wiki articles by keyword").option("--wiki-dir <path>", "Override wiki directory").option("-n, --limit <n>", "Max results", "20").option("--json", "Output as JSON").action(async (query, options) => {
|
|
221
|
+
const compiler = getCompiler(options.wikiDir);
|
|
222
|
+
await compiler.initialize();
|
|
223
|
+
const results = compiler.search(query).slice(0, parseInt(options.limit));
|
|
224
|
+
if (options.json) {
|
|
225
|
+
console.log(JSON.stringify(results, null, 2));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (results.length === 0) {
|
|
229
|
+
console.log(chalk.yellow(`No results for "${query}".`));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
console.log(
|
|
233
|
+
chalk.cyan(`
|
|
234
|
+
Search: "${query}" \u2014 ${results.length} results
|
|
235
|
+
`)
|
|
236
|
+
);
|
|
237
|
+
for (const r of results) {
|
|
238
|
+
console.log(` ${chalk.white(r.title)}`);
|
|
239
|
+
console.log(chalk.gray(` ${r.path} (${r.matches} matches)`));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
cmd.command("status").description("Show wiki statistics").option("--wiki-dir <path>", "Override wiki directory").option("--json", "Output as JSON").action(async (options) => {
|
|
243
|
+
const compiler = getCompiler(options.wikiDir);
|
|
244
|
+
await compiler.initialize();
|
|
245
|
+
const status = compiler.getStatus();
|
|
246
|
+
if (options.json) {
|
|
247
|
+
console.log(JSON.stringify(status, null, 2));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
console.log(chalk.cyan("\nWiki Status"));
|
|
251
|
+
console.log(chalk.gray(` Total articles: ${status.totalArticles}`));
|
|
252
|
+
for (const [cat, count] of Object.entries(status.byCategory)) {
|
|
253
|
+
console.log(chalk.gray(` ${cat}: ${count}`));
|
|
254
|
+
}
|
|
255
|
+
if (status.lastCompile) {
|
|
256
|
+
console.log(chalk.gray(` Last compile: ${status.lastCompile}`));
|
|
257
|
+
} else {
|
|
258
|
+
console.log(chalk.gray(` Last compile: never`));
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
return cmd;
|
|
262
|
+
}
|
|
263
|
+
export {
|
|
264
|
+
createWikiCommand
|
|
265
|
+
};
|
package/dist/src/cli/index.js
CHANGED
|
@@ -61,6 +61,7 @@ import { createConductorCommands } from "./commands/orchestrate.js";
|
|
|
61
61
|
import { createPreflightCommand } from "./commands/preflight.js";
|
|
62
62
|
import { createRulesCommand } from "./commands/rules.js";
|
|
63
63
|
import { createSnapshotCommand } from "./commands/snapshot.js";
|
|
64
|
+
import { createWikiCommand } from "./commands/wiki.js";
|
|
64
65
|
import { createLoopCommand } from "./commands/loop.js";
|
|
65
66
|
import { createSkillCommand } from "./commands/skill.js";
|
|
66
67
|
import chalk from "chalk";
|
|
@@ -576,6 +577,7 @@ program.addCommand(createDesiresCommands());
|
|
|
576
577
|
program.addCommand(createConductorCommands());
|
|
577
578
|
program.addCommand(createPreflightCommand());
|
|
578
579
|
program.addCommand(createSnapshotCommand());
|
|
580
|
+
program.addCommand(createWikiCommand());
|
|
579
581
|
program.addCommand(createLoopCommand());
|
|
580
582
|
program.addCommand(createRulesCommand());
|
|
581
583
|
program.addCommand(createSkillCommand());
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { join } from "path";
|
|
14
14
|
import { logger } from "../monitoring/logger.js";
|
|
15
15
|
import { frameLifecycleHooks } from "../context/frame-lifecycle-hooks.js";
|
|
16
|
+
import { WikiCompiler } from "../wiki/wiki-compiler.js";
|
|
16
17
|
class ObsidianVaultAdapter {
|
|
17
18
|
config;
|
|
18
19
|
dirs;
|
|
@@ -21,6 +22,7 @@ class ObsidianVaultAdapter {
|
|
|
21
22
|
seenRawFiles = /* @__PURE__ */ new Set();
|
|
22
23
|
unregisterCreate = null;
|
|
23
24
|
unregisterClose = null;
|
|
25
|
+
wikiCompiler = null;
|
|
24
26
|
constructor(config) {
|
|
25
27
|
this.config = {
|
|
26
28
|
vaultPath: config.vaultPath,
|
|
@@ -34,7 +36,8 @@ class ObsidianVaultAdapter {
|
|
|
34
36
|
root,
|
|
35
37
|
frames: join(root, "frames"),
|
|
36
38
|
raw: join(root, "raw"),
|
|
37
|
-
sessions: join(root, "sessions")
|
|
39
|
+
sessions: join(root, "sessions"),
|
|
40
|
+
wiki: join(root, "wiki")
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
43
|
/** Initialize vault directory structure and register lifecycle hooks */
|
|
@@ -62,6 +65,8 @@ class ObsidianVaultAdapter {
|
|
|
62
65
|
].join("\n")
|
|
63
66
|
);
|
|
64
67
|
}
|
|
68
|
+
this.wikiCompiler = new WikiCompiler({ wikiDir: this.dirs.wiki });
|
|
69
|
+
await this.wikiCompiler.initialize();
|
|
65
70
|
this.unregisterCreate = frameLifecycleHooks.onFrameCreated(
|
|
66
71
|
"obsidian-vault",
|
|
67
72
|
async (frame) => {
|
|
@@ -300,6 +305,19 @@ _...and ${files.length - 30} more_` : "",
|
|
|
300
305
|
if (this.ingestCallback) {
|
|
301
306
|
await this.ingestCallback(filename, content, metadata);
|
|
302
307
|
}
|
|
308
|
+
if (this.wikiCompiler) {
|
|
309
|
+
try {
|
|
310
|
+
await this.wikiCompiler.compileRawIngest(
|
|
311
|
+
filename,
|
|
312
|
+
content,
|
|
313
|
+
metadata
|
|
314
|
+
);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
logger.debug("Wiki compile on raw ingest failed", {
|
|
317
|
+
error: err.message
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
303
321
|
logger.info("Ingested raw file", {
|
|
304
322
|
filename,
|
|
305
323
|
source: metadata.source || "unknown"
|