@stackmemoryai/stackmemory 1.8.1 → 1.10.2

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,406 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath as __fileURLToPath } from 'url';
3
+ import { dirname as __pathDirname } from 'path';
4
+ const __filename = __fileURLToPath(import.meta.url);
5
+ const __dirname = __pathDirname(__filename);
6
+ import { Command } from "commander";
7
+ import chalk from "chalk";
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ import * as os from "os";
11
+ function parseFrontmatter(content) {
12
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
13
+ if (!match) {
14
+ throw new Error("No YAML frontmatter found (expected --- delimiters)");
15
+ }
16
+ const yamlBlock = match[1];
17
+ const body = match[2];
18
+ const fm = {};
19
+ let currentKey = null;
20
+ let currentArray = null;
21
+ for (const line of yamlBlock.split("\n")) {
22
+ const trimmed = line.trim();
23
+ if (!trimmed || trimmed.startsWith("#")) continue;
24
+ if (trimmed.startsWith("- ") && currentKey && currentArray !== null) {
25
+ currentArray.push(trimmed.slice(2).trim());
26
+ continue;
27
+ }
28
+ if (currentKey && currentArray !== null) {
29
+ fm[currentKey] = currentArray;
30
+ currentArray = null;
31
+ currentKey = null;
32
+ }
33
+ const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.*)$/);
34
+ if (!kvMatch) continue;
35
+ const key = kvMatch[1];
36
+ const val = kvMatch[2].trim();
37
+ if (val === "" || val === "[]") {
38
+ currentKey = key;
39
+ currentArray = [];
40
+ } else if (val.startsWith("[") && val.endsWith("]")) {
41
+ fm[key] = val.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, ""));
42
+ } else {
43
+ fm[key] = val.replace(/^['"]|['"]$/g, "");
44
+ }
45
+ }
46
+ if (currentKey && currentArray !== null) {
47
+ fm[currentKey] = currentArray;
48
+ }
49
+ return {
50
+ frontmatter: {
51
+ name: fm["name"] || "unknown",
52
+ version: fm["version"] || "0.0.0",
53
+ domain: fm["domain"] || "general",
54
+ expires: fm["expires"] || "",
55
+ activates_on: fm["activates_on"] || [],
56
+ sources: fm["sources"] || [],
57
+ context7: fm["context7"]
58
+ },
59
+ body
60
+ };
61
+ }
62
+ function getSkillDirs() {
63
+ const dirs = [];
64
+ const globalDir = path.join(os.homedir(), ".claude", "skills", "knowledge");
65
+ if (fs.existsSync(globalDir)) {
66
+ dirs.push({ dir: globalDir, scope: "global" });
67
+ }
68
+ const projectDir = path.join(process.cwd(), ".claude", "skills", "knowledge");
69
+ if (fs.existsSync(projectDir)) {
70
+ dirs.push({ dir: projectDir, scope: "project" });
71
+ }
72
+ return dirs;
73
+ }
74
+ function discoverSkills() {
75
+ const skills = [];
76
+ const now = /* @__PURE__ */ new Date();
77
+ for (const { dir, scope } of getSkillDirs()) {
78
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".skill.md"));
79
+ for (const file of files) {
80
+ const filePath = path.join(dir, file);
81
+ try {
82
+ const content = fs.readFileSync(filePath, "utf-8");
83
+ const { frontmatter, body } = parseFrontmatter(content);
84
+ const stale = frontmatter.expires ? new Date(frontmatter.expires) < now : false;
85
+ skills.push({ path: filePath, scope, frontmatter, body, stale });
86
+ } catch {
87
+ }
88
+ }
89
+ }
90
+ return skills;
91
+ }
92
+ function matchSkills(prompt, skills) {
93
+ const words = new Set(prompt.toLowerCase().split(/\s+/));
94
+ const results = [];
95
+ for (const skill of skills) {
96
+ const matched = [];
97
+ for (const keyword of skill.frontmatter.activates_on) {
98
+ const kw = keyword.toLowerCase();
99
+ if (words.has(kw) || prompt.toLowerCase().includes(kw)) {
100
+ matched.push(keyword);
101
+ }
102
+ }
103
+ if (matched.length > 0) {
104
+ const ratio = matched.length / skill.frontmatter.activates_on.length;
105
+ const scopeBoost = skill.scope === "project" ? 0.1 : 0;
106
+ const stalepenalty = skill.stale ? -0.2 : 0;
107
+ results.push({
108
+ skill,
109
+ score: Math.min(1, ratio + scopeBoost + stalepenalty),
110
+ matchedKeywords: matched
111
+ });
112
+ }
113
+ }
114
+ return results.sort((a, b) => b.score - a.score);
115
+ }
116
+ function createSkillCommand() {
117
+ const cmd = new Command("skill").description(
118
+ "Manage knowledge skills (.skill.md) \u2014 domain expertise for AI agents"
119
+ );
120
+ cmd.command("list").description("List all discovered knowledge skills").option("--stale", "Show only stale (expired) skills").option("--json", "Output as JSON").action((options) => {
121
+ let skills = discoverSkills();
122
+ if (options.stale) {
123
+ skills = skills.filter((s) => s.stale);
124
+ }
125
+ if (options.json) {
126
+ const out = skills.map((s) => ({
127
+ name: s.frontmatter.name,
128
+ domain: s.frontmatter.domain,
129
+ version: s.frontmatter.version,
130
+ scope: s.scope,
131
+ expires: s.frontmatter.expires,
132
+ stale: s.stale,
133
+ keywords: s.frontmatter.activates_on.length,
134
+ path: s.path
135
+ }));
136
+ console.log(JSON.stringify(out, null, 2));
137
+ return;
138
+ }
139
+ if (skills.length === 0) {
140
+ console.log(chalk.yellow("No knowledge skills found."));
141
+ console.log(` Global: ~/.claude/skills/knowledge/*.skill.md`);
142
+ console.log(` Project: .claude/skills/knowledge/*.skill.md`);
143
+ return;
144
+ }
145
+ console.log(chalk.bold(`
146
+ Knowledge Skills (${skills.length})
147
+ `));
148
+ for (const s of skills) {
149
+ const status = s.stale ? chalk.red("expired") : chalk.green("active");
150
+ const scope = s.scope === "global" ? chalk.dim("global") : chalk.cyan("project");
151
+ const keywords = chalk.dim(
152
+ `[${s.frontmatter.activates_on.slice(0, 5).join(", ")}${s.frontmatter.activates_on.length > 5 ? "..." : ""}]`
153
+ );
154
+ console.log(
155
+ ` ${chalk.bold(s.frontmatter.name.padEnd(20))} ${s.frontmatter.domain.padEnd(12)} ${s.frontmatter.version.padEnd(14)} ${scope.padEnd(18)} ${status} ${keywords}`
156
+ );
157
+ }
158
+ const staleCount = skills.filter((s) => s.stale).length;
159
+ if (staleCount > 0) {
160
+ console.log(
161
+ chalk.yellow(
162
+ `
163
+ ${staleCount} skill(s) expired \u2014 run ${chalk.bold("stackmemory skill refresh")} to update`
164
+ )
165
+ );
166
+ }
167
+ console.log();
168
+ });
169
+ cmd.command("match <prompt>").description("Find skills matching a prompt (keyword match)").option("-n, --limit <n>", "Max results", "5").option("--json", "Output as JSON").action((prompt, options) => {
170
+ const skills = discoverSkills();
171
+ const matches = matchSkills(prompt, skills).slice(
172
+ 0,
173
+ parseInt(options.limit, 10)
174
+ );
175
+ if (options.json) {
176
+ const out = matches.map((m) => ({
177
+ name: m.skill.frontmatter.name,
178
+ domain: m.skill.frontmatter.domain,
179
+ score: Math.round(m.score * 100),
180
+ stale: m.skill.stale,
181
+ matchedKeywords: m.matchedKeywords,
182
+ path: m.skill.path
183
+ }));
184
+ console.log(JSON.stringify(out, null, 2));
185
+ return;
186
+ }
187
+ if (matches.length === 0) {
188
+ console.log(chalk.yellow(`No skills match: "${prompt}"`));
189
+ return;
190
+ }
191
+ console.log(chalk.bold(`
192
+ Matches for: "${prompt}"
193
+ `));
194
+ for (const m of matches) {
195
+ const pct = Math.round(m.score * 100);
196
+ const bar = pct >= 50 ? chalk.green(`${pct}%`) : chalk.yellow(`${pct}%`);
197
+ const staleTag = m.skill.stale ? chalk.red(" [stale]") : "";
198
+ console.log(
199
+ ` ${bar.padEnd(16)} ${chalk.bold(m.skill.frontmatter.name.padEnd(20))} ${chalk.dim(m.matchedKeywords.join(", "))}${staleTag}`
200
+ );
201
+ }
202
+ console.log();
203
+ });
204
+ cmd.command("show <name>").description("Display a skill's full content").action((name) => {
205
+ const skills = discoverSkills();
206
+ const skill = skills.find((s) => s.frontmatter.name === name);
207
+ if (!skill) {
208
+ console.error(chalk.red(`Skill not found: ${name}`));
209
+ console.log(
210
+ `Available: ${skills.map((s) => s.frontmatter.name).join(", ")}`
211
+ );
212
+ process.exit(1);
213
+ }
214
+ const status = skill.stale ? chalk.red("EXPIRED") : chalk.green("active");
215
+ console.log(
216
+ chalk.bold(`
217
+ ${skill.frontmatter.name}`) + ` (${skill.frontmatter.domain}) ${status}`
218
+ );
219
+ console.log(
220
+ chalk.dim(
221
+ ` v${skill.frontmatter.version} | expires ${skill.frontmatter.expires} | ${skill.scope}`
222
+ )
223
+ );
224
+ console.log(chalk.dim(` ${skill.path}`));
225
+ if (skill.frontmatter.context7) {
226
+ console.log(chalk.dim(` context7: ${skill.frontmatter.context7}`));
227
+ }
228
+ console.log(
229
+ chalk.dim(` keywords: ${skill.frontmatter.activates_on.join(", ")}`)
230
+ );
231
+ console.log(chalk.dim(` sources:`));
232
+ for (const src of skill.frontmatter.sources) {
233
+ console.log(chalk.dim(` - ${src}`));
234
+ }
235
+ console.log();
236
+ console.log(skill.body);
237
+ });
238
+ cmd.command("add <source>").description("Add a knowledge skill from a local file or URL").option(
239
+ "--global",
240
+ "Install to ~/.claude/skills/knowledge/ (default: project)"
241
+ ).option("--name <name>", "Override skill name").action(
242
+ async (source, options) => {
243
+ let content = "";
244
+ if (source.startsWith("http://") || source.startsWith("https://")) {
245
+ try {
246
+ const res = await fetch(source);
247
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
248
+ content = await res.text();
249
+ } catch (err) {
250
+ console.error(chalk.red(`Failed to fetch: ${source}`));
251
+ console.error(err.message);
252
+ process.exit(1);
253
+ return;
254
+ }
255
+ } else {
256
+ const resolved = path.resolve(source);
257
+ if (!fs.existsSync(resolved)) {
258
+ console.error(chalk.red(`File not found: ${resolved}`));
259
+ process.exit(1);
260
+ return;
261
+ }
262
+ content = fs.readFileSync(resolved, "utf-8");
263
+ }
264
+ let parsed;
265
+ try {
266
+ parsed = parseFrontmatter(content);
267
+ } catch (err) {
268
+ console.error(
269
+ chalk.red(`Invalid skill file: ${err.message}`)
270
+ );
271
+ process.exit(1);
272
+ return;
273
+ }
274
+ if (!parsed) return;
275
+ const skillName = options.name ?? parsed.frontmatter.name;
276
+ const targetDir = options.global ? path.join(os.homedir(), ".claude", "skills", "knowledge") : path.join(process.cwd(), ".claude", "skills", "knowledge");
277
+ fs.mkdirSync(targetDir, { recursive: true });
278
+ const targetPath = path.join(targetDir, `${skillName}.skill.md`);
279
+ const exists = fs.existsSync(targetPath);
280
+ fs.writeFileSync(targetPath, content, "utf-8");
281
+ const action = exists ? "Updated" : "Added";
282
+ const scope = options.global ? "global" : "project";
283
+ console.log(
284
+ chalk.green(`${action} ${chalk.bold(skillName)} (${scope})`)
285
+ );
286
+ console.log(chalk.dim(` ${targetPath}`));
287
+ console.log(
288
+ chalk.dim(
289
+ ` domain: ${parsed.frontmatter.domain} | keywords: ${parsed.frontmatter.activates_on.length} | expires: ${parsed.frontmatter.expires}`
290
+ )
291
+ );
292
+ }
293
+ );
294
+ cmd.command("refresh [name]").description("Check freshness and show stale skills that need updating").option("--fetch", "Attempt to fetch source URLs and show diff summary").action(async (name, options) => {
295
+ let skills = discoverSkills();
296
+ if (name) {
297
+ skills = skills.filter((s) => s.frontmatter.name === name);
298
+ if (skills.length === 0) {
299
+ console.error(chalk.red(`Skill not found: ${name}`));
300
+ process.exit(1);
301
+ }
302
+ }
303
+ const stale = skills.filter((s) => s.stale);
304
+ const fresh = skills.filter((s) => !s.stale);
305
+ console.log(chalk.bold(`
306
+ Skill Freshness Report
307
+ `));
308
+ console.log(
309
+ ` ${chalk.green(String(fresh.length))} active ${chalk.red(String(stale.length))} expired ${skills.length} total
310
+ `
311
+ );
312
+ if (stale.length === 0) {
313
+ console.log(chalk.green(" All skills are fresh."));
314
+ console.log();
315
+ return;
316
+ }
317
+ for (const s of stale) {
318
+ const daysPast = Math.floor(
319
+ (Date.now() - new Date(s.frontmatter.expires).getTime()) / 864e5
320
+ );
321
+ console.log(
322
+ ` ${chalk.red("expired")} ${chalk.bold(s.frontmatter.name.padEnd(20))} ${chalk.dim(`${daysPast}d ago`)} ${s.scope}`
323
+ );
324
+ if (s.frontmatter.context7) {
325
+ console.log(
326
+ chalk.dim(` context7: ${s.frontmatter.context7}`)
327
+ );
328
+ }
329
+ for (const src of s.frontmatter.sources) {
330
+ console.log(chalk.dim(` source: ${src}`));
331
+ }
332
+ if (options.fetch && s.frontmatter.sources.length > 0) {
333
+ const url = s.frontmatter.sources[0];
334
+ try {
335
+ const res = await fetch(url, { method: "HEAD" });
336
+ console.log(
337
+ res.ok ? chalk.green(` ${url} \u2014 reachable (${res.status})`) : chalk.yellow(` ${url} \u2014 ${res.status}`)
338
+ );
339
+ } catch {
340
+ console.log(chalk.red(` ${url} \u2014 unreachable`));
341
+ }
342
+ }
343
+ }
344
+ console.log(
345
+ chalk.yellow(
346
+ `
347
+ To update: edit the .skill.md file and bump the version + expires date.`
348
+ )
349
+ );
350
+ if (!options.fetch) {
351
+ console.log(
352
+ chalk.dim(` Run with --fetch to check source URL reachability.`)
353
+ );
354
+ }
355
+ console.log();
356
+ });
357
+ cmd.command("init <name>").description("Scaffold a new .skill.md file").option("--global", "Create in ~/.claude/skills/knowledge/").option("--domain <domain>", "Skill domain", "general").action((name, options) => {
358
+ const targetDir = options.global ? path.join(os.homedir(), ".claude", "skills", "knowledge") : path.join(process.cwd(), ".claude", "skills", "knowledge");
359
+ fs.mkdirSync(targetDir, { recursive: true });
360
+ const targetPath = path.join(targetDir, `${name}.skill.md`);
361
+ if (fs.existsSync(targetPath)) {
362
+ console.error(chalk.red(`Already exists: ${targetPath}`));
363
+ process.exit(1);
364
+ }
365
+ const sixMonths = /* @__PURE__ */ new Date();
366
+ sixMonths.setMonth(sixMonths.getMonth() + 6);
367
+ const expiresDate = sixMonths.toISOString().slice(0, 10);
368
+ const template = `---
369
+ name: ${name}
370
+ version: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, ".")}
371
+ domain: ${options.domain}
372
+ expires: ${expiresDate}
373
+ activates_on: [${name}]
374
+ sources:
375
+ - https://docs.example.com
376
+ context7:
377
+ ---
378
+
379
+ # ${name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, " ")}
380
+
381
+ ## Current SDK & Versions
382
+ - TODO: add current versions
383
+
384
+ ## Preferred Patterns
385
+ - TODO: document the right way to do things
386
+
387
+ ## Gotchas
388
+ - TODO: things LLMs consistently get wrong
389
+
390
+ ## Entry Points
391
+ - TODO: canonical doc URLs
392
+ `;
393
+ fs.writeFileSync(targetPath, template, "utf-8");
394
+ console.log(chalk.green(`Created ${chalk.bold(name)} skill`));
395
+ console.log(chalk.dim(` ${targetPath}`));
396
+ console.log(
397
+ chalk.dim(
398
+ ` Edit the file to add domain knowledge, then update activates_on keywords.`
399
+ )
400
+ );
401
+ });
402
+ return cmd;
403
+ }
404
+ export {
405
+ createSkillCommand
406
+ };
@@ -59,8 +59,10 @@ import { createTeamCommands } from "./commands/team.js";
59
59
  import { createDesiresCommands } from "./commands/desires.js";
60
60
  import { createConductorCommands } from "./commands/orchestrate.js";
61
61
  import { createPreflightCommand } from "./commands/preflight.js";
62
+ import { createRulesCommand } from "./commands/rules.js";
62
63
  import { createSnapshotCommand } from "./commands/snapshot.js";
63
64
  import { createLoopCommand } from "./commands/loop.js";
65
+ import { createSkillCommand } from "./commands/skill.js";
64
66
  import chalk from "chalk";
65
67
  import * as fs from "fs";
66
68
  import * as path from "path";
@@ -478,6 +480,45 @@ program.addCommand(createMemoryCommand());
478
480
  program.addCommand(clearCommand);
479
481
  program.addCommand(serviceCommand);
480
482
  program.addCommand(createHooksCommand());
483
+ program.command("board").description("Open the StackMemory Board (agent kanban + diff viewer)").option("-p, --port <port>", "Port to serve on", "3456").option("--no-open", "Do not auto-open browser").action(async (options) => {
484
+ const { spawn: spawnProc } = await import("child_process");
485
+ const { join: join2 } = await import("path");
486
+ const { existsSync: existsSync2 } = await import("fs");
487
+ const candidates = [
488
+ join2(process.cwd(), "tools", "agent-viewer", "server.js"),
489
+ join2(
490
+ process.env.PROVENANTAI_ROOT || "",
491
+ "tools",
492
+ "agent-viewer",
493
+ "server.js"
494
+ ),
495
+ join2(
496
+ process.env.HOME || "",
497
+ "Dev",
498
+ "provenantai",
499
+ "tools",
500
+ "agent-viewer",
501
+ "server.js"
502
+ )
503
+ ];
504
+ const serverPath = candidates.find((c) => existsSync2(c));
505
+ if (!serverPath) {
506
+ console.error(
507
+ "Board server not found. Run from a repo with tools/agent-viewer/server.js"
508
+ );
509
+ process.exit(1);
510
+ }
511
+ console.log(`Starting StackMemory Board on port ${options.port}...`);
512
+ const child = spawnProc("node", [serverPath, "--port", options.port], {
513
+ stdio: "inherit",
514
+ env: { ...process.env, FORCE_COLOR: "1" }
515
+ });
516
+ child.on("close", (code) => process.exit(code || 0));
517
+ process.on("SIGINT", () => {
518
+ child.kill("SIGINT");
519
+ process.exit(0);
520
+ });
521
+ });
481
522
  const lazyCommands = [];
482
523
  if (isFeatureEnabled("skills")) {
483
524
  lazyCommands.push(
@@ -515,6 +556,8 @@ program.addCommand(createConductorCommands());
515
556
  program.addCommand(createPreflightCommand());
516
557
  program.addCommand(createSnapshotCommand());
517
558
  program.addCommand(createLoopCommand());
559
+ program.addCommand(createRulesCommand());
560
+ program.addCommand(createSkillCommand());
518
561
  registerSetupCommands(program);
519
562
  program.command("mm-spike").description(
520
563
  "Run multi-agent planning/implementation spike (planner/implementer/critic)"