@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.
@@ -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
+ };
@@ -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"