@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.
@@ -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";
@@ -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
- join2(
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
- "provenantai",
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 a repo with tools/agent-viewer/server.js"
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]);