@stackmemoryai/stackmemory 1.10.2 → 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 +31 -8
- package/dist/src/core/config/config-manager.js +2 -1
- package/dist/src/core/storage/obsidian-vault-adapter.js +410 -0
- package/dist/src/core/wiki/wiki-compiler.js +792 -0
- package/dist/src/integrations/mcp/server.js +2 -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";
|
|
@@ -198,6 +199,8 @@ program.command("status").description("Show current StackMemory status").option(
|
|
|
198
199
|
await UpdateChecker.checkForUpdates(VERSION);
|
|
199
200
|
await sessionManager.initialize();
|
|
200
201
|
await sharedContextLayer.initialize();
|
|
202
|
+
const { initObsidianVault } = await import("../core/storage/obsidian-vault-adapter.js");
|
|
203
|
+
await initObsidianVault();
|
|
201
204
|
const session = await sessionManager.getOrCreateSession({
|
|
202
205
|
projectPath: projectRoot,
|
|
203
206
|
sessionId: options.session
|
|
@@ -484,18 +487,24 @@ program.command("board").description("Open the StackMemory Board (agent kanban +
|
|
|
484
487
|
const { spawn: spawnProc } = await import("child_process");
|
|
485
488
|
const { join: join2 } = await import("path");
|
|
486
489
|
const { existsSync: existsSync2 } = await import("fs");
|
|
490
|
+
const { fileURLToPath: toPath } = await import("url");
|
|
491
|
+
const pkgRoot = join2(
|
|
492
|
+
toPath(import.meta.url),
|
|
493
|
+
"..",
|
|
494
|
+
"..",
|
|
495
|
+
"..",
|
|
496
|
+
"..",
|
|
497
|
+
"tools",
|
|
498
|
+
"agent-viewer",
|
|
499
|
+
"server.cjs"
|
|
500
|
+
);
|
|
487
501
|
const candidates = [
|
|
488
502
|
join2(process.cwd(), "tools", "agent-viewer", "server.js"),
|
|
489
|
-
|
|
490
|
-
process.env.PROVENANTAI_ROOT || "",
|
|
491
|
-
"tools",
|
|
492
|
-
"agent-viewer",
|
|
493
|
-
"server.js"
|
|
494
|
-
),
|
|
503
|
+
pkgRoot,
|
|
495
504
|
join2(
|
|
496
505
|
process.env.HOME || "",
|
|
497
506
|
"Dev",
|
|
498
|
-
"
|
|
507
|
+
"stackmemory",
|
|
499
508
|
"tools",
|
|
500
509
|
"agent-viewer",
|
|
501
510
|
"server.js"
|
|
@@ -504,7 +513,7 @@ program.command("board").description("Open the StackMemory Board (agent kanban +
|
|
|
504
513
|
const serverPath = candidates.find((c) => existsSync2(c));
|
|
505
514
|
if (!serverPath) {
|
|
506
515
|
console.error(
|
|
507
|
-
"Board server not found. Run from
|
|
516
|
+
"Board server not found. Run from the stackmemory repo or install globally."
|
|
508
517
|
);
|
|
509
518
|
process.exit(1);
|
|
510
519
|
}
|
|
@@ -513,6 +522,19 @@ program.command("board").description("Open the StackMemory Board (agent kanban +
|
|
|
513
522
|
stdio: "inherit",
|
|
514
523
|
env: { ...process.env, FORCE_COLOR: "1" }
|
|
515
524
|
});
|
|
525
|
+
if (options.open !== false) {
|
|
526
|
+
setTimeout(async () => {
|
|
527
|
+
const url = `http://localhost:${options.port}`;
|
|
528
|
+
const cp = await import("child_process");
|
|
529
|
+
try {
|
|
530
|
+
cp.execSync(
|
|
531
|
+
process.platform === "darwin" ? `open ${url}` : `xdg-open ${url}`,
|
|
532
|
+
{ stdio: "ignore" }
|
|
533
|
+
);
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
}, 1e3);
|
|
537
|
+
}
|
|
516
538
|
child.on("close", (code) => process.exit(code || 0));
|
|
517
539
|
process.on("SIGINT", () => {
|
|
518
540
|
child.kill("SIGINT");
|
|
@@ -555,6 +577,7 @@ program.addCommand(createDesiresCommands());
|
|
|
555
577
|
program.addCommand(createConductorCommands());
|
|
556
578
|
program.addCommand(createPreflightCommand());
|
|
557
579
|
program.addCommand(createSnapshotCommand());
|
|
580
|
+
program.addCommand(createWikiCommand());
|
|
558
581
|
program.addCommand(createLoopCommand());
|
|
559
582
|
program.addCommand(createRulesCommand());
|
|
560
583
|
program.addCommand(createSkillCommand());
|
|
@@ -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]);
|