@stackmemoryai/stackmemory 1.2.4 → 1.2.6

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.
@@ -889,6 +889,25 @@ class ClaudeSkillsManager {
889
889
  };
890
890
  }
891
891
  }
892
+ case "theory": {
893
+ const { TheorySkill } = await import("./theory-skill.js");
894
+ const theorySkill = new TheorySkill(this.context);
895
+ switch (args[0]) {
896
+ case "show":
897
+ return theorySkill.show();
898
+ case "update":
899
+ return theorySkill.update(args.slice(1).join(" "));
900
+ case "init":
901
+ return theorySkill.init(args.slice(1).join(" "));
902
+ case "status":
903
+ return theorySkill.status();
904
+ default:
905
+ return {
906
+ success: false,
907
+ message: "Usage: theory show|init|update|status"
908
+ };
909
+ }
910
+ }
892
911
  case "linear-run": {
893
912
  if (!this.linearTaskRunner) {
894
913
  return {
@@ -931,7 +950,8 @@ class ClaudeSkillsManager {
931
950
  "dashboard",
932
951
  "api",
933
952
  "spec",
934
- "agent"
953
+ "agent",
954
+ "theory"
935
955
  ];
936
956
  if (this.rlmOrchestrator) {
937
957
  skills.push("rlm", "lint");
@@ -1088,6 +1108,22 @@ Options:
1088
1108
  Each agent runs in a disposable /tmp workspace (git clone --depth=1).
1089
1109
  Patches: git apply .stackmemory/patches/<file>.patch
1090
1110
  Spec branches: cd /tmp/sm-spec-* && git log --oneline
1111
+ `;
1112
+ case "theory":
1113
+ return `
1114
+ /theory show
1115
+ /theory init "<problem statement>"
1116
+ /theory update "<full content>"
1117
+ /theory status
1118
+
1119
+ Maintain a living THEORY.MD at repo root \u2014 a narrative operating theory:
1120
+ show \u2014 Display current THEORY.MD content
1121
+ init \u2014 Create THEORY.MD with scaffold sections
1122
+ update \u2014 Overwrite THEORY.MD (validates length, warns on anti-patterns)
1123
+ status \u2014 Show metadata: exists, line count, sections, last modified
1124
+
1125
+ Sections: Problem, Operating Theory, Strategy, Key Discoveries, Open Questions
1126
+ Based on Theorist by @blader (MIT).
1091
1127
  `;
1092
1128
  case "linear-run":
1093
1129
  return `
@@ -0,0 +1,191 @@
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 * as fs from "fs";
6
+ import * as path from "path";
7
+ import { execSync } from "child_process";
8
+ import { logger } from "../core/monitoring/logger.js";
9
+ const THEORY_FILE = "THEORY.MD";
10
+ const MIN_CONTENT_LENGTH = 100;
11
+ const MAX_LINE_WARNING = 200;
12
+ const TEMPLATE_SECTIONS = [
13
+ "## Problem",
14
+ "## Operating Theory",
15
+ "## Strategy",
16
+ "## Key Discoveries",
17
+ "## Open Questions"
18
+ ];
19
+ function getGitRoot() {
20
+ try {
21
+ return execSync("git rev-parse --show-toplevel", {
22
+ encoding: "utf-8",
23
+ timeout: 5e3
24
+ }).trim();
25
+ } catch {
26
+ return void 0;
27
+ }
28
+ }
29
+ function generateTemplate(problemStatement) {
30
+ return `# THEORY.MD
31
+
32
+ ## Problem
33
+
34
+ ${problemStatement}
35
+
36
+ ## Operating Theory
37
+
38
+ _What is your current mental model of how this system works or should work?_
39
+
40
+ ## Strategy
41
+
42
+ _What approach are you taking and why?_
43
+
44
+ ## Key Discoveries
45
+
46
+ _What have you learned that changed your thinking?_
47
+
48
+ ## Open Questions
49
+
50
+ _What don't you know yet? What assumptions need validation?_
51
+ `;
52
+ }
53
+ class TheorySkill {
54
+ constructor(context) {
55
+ this.context = context;
56
+ this.rootDir = getGitRoot() || process.cwd();
57
+ }
58
+ rootDir;
59
+ get theoryPath() {
60
+ return path.join(this.rootDir, THEORY_FILE);
61
+ }
62
+ show() {
63
+ if (!fs.existsSync(this.theoryPath)) {
64
+ return {
65
+ success: false,
66
+ message: `No ${THEORY_FILE} found. Run \`theory init "<problem>"\` to create one.`
67
+ };
68
+ }
69
+ const content = fs.readFileSync(this.theoryPath, "utf-8");
70
+ return {
71
+ success: true,
72
+ message: content,
73
+ data: { path: this.theoryPath, length: content.length }
74
+ };
75
+ }
76
+ init(problemStatement) {
77
+ if (!problemStatement || problemStatement.trim().length === 0) {
78
+ return {
79
+ success: false,
80
+ message: "A problem statement is required to initialize THEORY.MD."
81
+ };
82
+ }
83
+ if (fs.existsSync(this.theoryPath)) {
84
+ return {
85
+ success: false,
86
+ message: `${THEORY_FILE} already exists at ${this.theoryPath}. Use \`theory update\` to modify it.`
87
+ };
88
+ }
89
+ const content = generateTemplate(problemStatement.trim());
90
+ fs.writeFileSync(this.theoryPath, content, "utf-8");
91
+ logger.info("Created THEORY.MD", { path: this.theoryPath });
92
+ return {
93
+ success: true,
94
+ message: `Created ${THEORY_FILE} at ${this.theoryPath}`,
95
+ data: { path: this.theoryPath, sections: TEMPLATE_SECTIONS }
96
+ };
97
+ }
98
+ update(content) {
99
+ if (!content || content.trim().length === 0) {
100
+ return { success: false, message: "Content is required for update." };
101
+ }
102
+ if (content.trim().length < MIN_CONTENT_LENGTH) {
103
+ return {
104
+ success: false,
105
+ message: `Content too short (${content.trim().length} chars). THEORY.MD should be at least ${MIN_CONTENT_LENGTH} characters to be meaningful.`
106
+ };
107
+ }
108
+ const warnings = [];
109
+ if (/\[[ x]\]/i.test(content)) {
110
+ warnings.push(
111
+ "Contains checkboxes \u2014 THEORY.MD is narrative, not a checklist."
112
+ );
113
+ }
114
+ if (/\d{4}-\d{2}-\d{2}/.test(content)) {
115
+ warnings.push(
116
+ "Contains dates \u2014 THEORY.MD captures current thinking, not a changelog."
117
+ );
118
+ }
119
+ const lineCount = content.split("\n").length;
120
+ if (lineCount > MAX_LINE_WARNING) {
121
+ warnings.push(
122
+ `${lineCount} lines is long. Consider distilling to keep THEORY.MD focused.`
123
+ );
124
+ }
125
+ fs.writeFileSync(this.theoryPath, content, "utf-8");
126
+ if (this.context.frameManager) {
127
+ try {
128
+ const frameId = this.context.frameManager.createFrame(
129
+ "write",
130
+ "theory-update",
131
+ { source: THEORY_FILE, length: content.length }
132
+ );
133
+ this.context.frameManager.addEvent(
134
+ "artifact",
135
+ {
136
+ type: "theory-update",
137
+ path: this.theoryPath,
138
+ length: content.length,
139
+ lineCount
140
+ },
141
+ frameId
142
+ );
143
+ this.context.frameManager.closeFrame(frameId, {
144
+ theory_updated: true
145
+ });
146
+ } catch (err) {
147
+ logger.warn("Failed to record theory update frame", { error: err });
148
+ }
149
+ }
150
+ logger.info("Updated THEORY.MD", {
151
+ path: this.theoryPath,
152
+ length: content.length
153
+ });
154
+ const message = warnings.length > 0 ? `Updated ${THEORY_FILE}. Warnings:
155
+ ${warnings.map((w) => ` - ${w}`).join("\n")}` : `Updated ${THEORY_FILE} (${lineCount} lines)`;
156
+ return {
157
+ success: true,
158
+ message,
159
+ data: { path: this.theoryPath, lineCount, warnings }
160
+ };
161
+ }
162
+ status() {
163
+ if (!fs.existsSync(this.theoryPath)) {
164
+ return {
165
+ success: true,
166
+ message: `No ${THEORY_FILE} found.`,
167
+ data: { exists: false }
168
+ };
169
+ }
170
+ const content = fs.readFileSync(this.theoryPath, "utf-8");
171
+ const lines = content.split("\n");
172
+ const stat = fs.statSync(this.theoryPath);
173
+ const sections = TEMPLATE_SECTIONS.filter((s) => content.includes(s));
174
+ return {
175
+ success: true,
176
+ message: `${THEORY_FILE}: ${lines.length} lines, ${sections.length}/${TEMPLATE_SECTIONS.length} sections`,
177
+ data: {
178
+ exists: true,
179
+ path: this.theoryPath,
180
+ lineCount: lines.length,
181
+ charCount: content.length,
182
+ sections,
183
+ totalSections: TEMPLATE_SECTIONS.length,
184
+ lastModified: stat.mtime.toISOString()
185
+ }
186
+ };
187
+ }
188
+ }
189
+ export {
190
+ TheorySkill
191
+ };
@@ -39,6 +39,14 @@ const CANONICAL_HOOKS = [
39
39
  timeout: 2,
40
40
  commandPrefix: "node",
41
41
  required: true
42
+ },
43
+ {
44
+ scriptName: "theory-capture.js",
45
+ eventType: "PostToolUse",
46
+ matcher: "Edit|Write|MultiEdit",
47
+ timeout: 2,
48
+ commandPrefix: "node",
49
+ required: false
42
50
  }
43
51
  ];
44
52
  const DEAD_HOOKS = ["sms-response-handler.js"];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.2.4",
4
- "description": "Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.",
3
+ "version": "1.2.6",
4
+ "description": "Project-scoped memory for AI coding tools. Durable context across sessions with 32 MCP tools, FTS5 search, Claude/Codex/OpenCode wrappers, Linear sync, automatic hooks, and log analysis.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=10.0.0"
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Theory Capture Hook (PostToolUse)
5
+ *
6
+ * Detects writes to THEORY.MD and:
7
+ * 1. Caches metadata to .stackmemory/theory-cache.json
8
+ * 2. Fire-and-forget: records a StackMemory frame via CLI
9
+ *
10
+ * Must complete in <50ms -- file I/O only, CLI exec is detached.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const crypto = require('crypto');
16
+ const { spawn } = require('child_process');
17
+
18
+ const WRITE_TOOLS = ['Edit', 'Write', 'MultiEdit'];
19
+
20
+ function ensureDir(dir) {
21
+ if (!fs.existsSync(dir)) {
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ }
24
+ }
25
+
26
+ function safeWriteFile(filePath, content) {
27
+ const tmp = filePath + '.tmp';
28
+ fs.writeFileSync(tmp, content);
29
+ fs.renameSync(tmp, filePath);
30
+ }
31
+
32
+ function isTheoryPath(filePath) {
33
+ if (!filePath) return false;
34
+ const lower = filePath.toLowerCase();
35
+ return (
36
+ lower.endsWith('/theory.md') ||
37
+ lower.endsWith('\\theory.md') ||
38
+ lower === 'theory.md'
39
+ );
40
+ }
41
+
42
+ async function readInput() {
43
+ let input = '';
44
+ for await (const chunk of process.stdin) {
45
+ input += chunk;
46
+ }
47
+ return JSON.parse(input);
48
+ }
49
+
50
+ async function main() {
51
+ try {
52
+ const input = await readInput();
53
+ const { tool_name, tool_input } = input;
54
+
55
+ // Only care about file-write tools
56
+ if (!WRITE_TOOLS.includes(tool_name)) return;
57
+
58
+ const fp = tool_input?.file_path || '';
59
+ if (!isTheoryPath(fp)) return;
60
+
61
+ // Read current content
62
+ let content;
63
+ try {
64
+ content = fs.readFileSync(fp, 'utf-8');
65
+ } catch {
66
+ return; // File doesn't exist or unreadable
67
+ }
68
+
69
+ // Cache metadata for next session start
70
+ const cacheDir = path.join(process.cwd(), '.stackmemory');
71
+ ensureDir(cacheDir);
72
+
73
+ const lineCount = content.split('\n').length;
74
+ safeWriteFile(
75
+ path.join(cacheDir, 'theory-cache.json'),
76
+ JSON.stringify({
77
+ path: fp,
78
+ timestamp: new Date().toISOString(),
79
+ hash: crypto.createHash('md5').update(content).digest('hex'),
80
+ lineCount,
81
+ })
82
+ );
83
+
84
+ // Fire-and-forget: record as frame via CLI
85
+ const child = spawn(
86
+ 'stackmemory',
87
+ ['context', 'add', 'artifact', `THEORY.MD updated (${lineCount} lines)`],
88
+ {
89
+ cwd: process.cwd(),
90
+ stdio: 'ignore',
91
+ detached: true,
92
+ }
93
+ );
94
+ child.unref();
95
+ } catch {
96
+ // Silent fail -- never block the agent
97
+ }
98
+ }
99
+
100
+ main();
@@ -1,253 +0,0 @@
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 { logger } from "../core/monitoring/logger.js";
6
- import { GraphitiClient } from "../integrations/graphiti/client.js";
7
- import { DEFAULT_GRAPHITI_CONFIG } from "../integrations/graphiti/config.js";
8
- class GraphitiHooks {
9
- client;
10
- config;
11
- constructor(config = {}) {
12
- this.config = { ...DEFAULT_GRAPHITI_CONFIG, ...config };
13
- this.client = new GraphitiClient(this.config);
14
- }
15
- register(emitter) {
16
- if (!this.config.enabled) {
17
- logger.debug("Graphiti hooks disabled");
18
- return;
19
- }
20
- emitter.registerHandler("session_start", this.onSessionStart.bind(this));
21
- emitter.registerHandler("file_change", this.onFileChange.bind(this));
22
- emitter.registerHandler("session_end", this.onSessionEnd.bind(this));
23
- emitter.registerHandler("input_idle", this.onInputIdle.bind(this));
24
- emitter.registerHandler("context_switch", this.onContextSwitch.bind(this));
25
- emitter.registerHandler("prompt_submit", this.onPromptSubmit.bind(this));
26
- emitter.registerHandler("tool_use", this.onToolUse.bind(this));
27
- emitter.registerHandler(
28
- "suggestion_ready",
29
- this.onSuggestionReady.bind(this)
30
- );
31
- emitter.registerHandler("agent_start", this.onAgentStart.bind(this));
32
- emitter.registerHandler("agent_complete", this.onAgentComplete.bind(this));
33
- emitter.registerHandler("agent_error", this.onAgentError.bind(this));
34
- logger.info("Graphiti hooks registered", {
35
- endpoint: this.config.endpoint,
36
- backend: this.config.backend,
37
- maxHops: this.config.maxHops
38
- });
39
- }
40
- async onSessionStart(event) {
41
- try {
42
- const status = await this.client.getStatus();
43
- if (!status.connected) {
44
- logger.warn("Graphiti not available - operating in degraded mode");
45
- return;
46
- }
47
- const episode = {
48
- type: "session_start",
49
- content: event.data || {},
50
- timestamp: Date.now(),
51
- source: "stackmemory",
52
- metadata: { severity: "info" }
53
- };
54
- await this.client.upsertEpisode(episode);
55
- } catch (error) {
56
- logger.debug("Graphiti session_start failed", {
57
- error: error instanceof Error ? error.message : String(error)
58
- });
59
- }
60
- }
61
- async onFileChange(event) {
62
- const fileEvent = event;
63
- try {
64
- const episode = {
65
- type: "file_change",
66
- content: {
67
- path: fileEvent.data.path,
68
- changeType: fileEvent.data.changeType,
69
- size: typeof fileEvent.data.content === "string" ? fileEvent.data.content.length : void 0
70
- },
71
- timestamp: Date.now(),
72
- source: "stackmemory"
73
- };
74
- await this.client.upsertEpisode(episode);
75
- } catch (error) {
76
- logger.debug("Graphiti file_change episode failed", {
77
- error: error instanceof Error ? error.message : String(error)
78
- });
79
- }
80
- }
81
- async onSessionEnd(event) {
82
- try {
83
- const episode = {
84
- type: "session_end",
85
- content: event.data || {},
86
- timestamp: Date.now(),
87
- source: "stackmemory"
88
- };
89
- await this.client.upsertEpisode(episode);
90
- } catch (error) {
91
- logger.debug("Graphiti session_end failed", {
92
- error: error instanceof Error ? error.message : String(error)
93
- });
94
- }
95
- }
96
- async onInputIdle(event) {
97
- const idle = event;
98
- try {
99
- const episode = {
100
- type: "input_idle",
101
- content: {
102
- idleDuration: idle.data.idleDuration,
103
- lastInput: idle.data.lastInput
104
- },
105
- timestamp: Date.now(),
106
- source: "stackmemory"
107
- };
108
- await this.client.upsertEpisode(episode);
109
- } catch (error) {
110
- logger.debug("Graphiti input_idle episode failed", {
111
- error: error instanceof Error ? error.message : String(error)
112
- });
113
- }
114
- }
115
- async onContextSwitch(event) {
116
- const ctx = event;
117
- try {
118
- const episode = {
119
- type: "context_switch",
120
- content: {
121
- fromBranch: ctx.data.fromBranch,
122
- toBranch: ctx.data.toBranch,
123
- fromProject: ctx.data.fromProject,
124
- toProject: ctx.data.toProject
125
- },
126
- timestamp: Date.now(),
127
- source: "stackmemory"
128
- };
129
- await this.client.upsertEpisode(episode);
130
- } catch (error) {
131
- logger.debug("Graphiti context_switch episode failed", {
132
- error: error instanceof Error ? error.message : String(error)
133
- });
134
- }
135
- }
136
- async onPromptSubmit(event) {
137
- try {
138
- const episode = {
139
- type: "prompt_submit",
140
- content: event.data || {},
141
- timestamp: Date.now(),
142
- source: "stackmemory"
143
- };
144
- await this.client.upsertEpisode(episode);
145
- } catch (error) {
146
- logger.debug("Graphiti prompt_submit episode failed", {
147
- error: error instanceof Error ? error.message : String(error)
148
- });
149
- }
150
- }
151
- async onToolUse(event) {
152
- try {
153
- const episode = {
154
- type: "tool_use",
155
- content: event.data || {},
156
- timestamp: Date.now(),
157
- source: "stackmemory"
158
- };
159
- await this.client.upsertEpisode(episode);
160
- } catch (error) {
161
- logger.debug("Graphiti tool_use episode failed", {
162
- error: error instanceof Error ? error.message : String(error)
163
- });
164
- }
165
- }
166
- async onSuggestionReady(event) {
167
- const suggestion = event;
168
- try {
169
- const episode = {
170
- type: "suggestion_ready",
171
- content: {
172
- source: suggestion.data.source,
173
- confidence: suggestion.data.confidence,
174
- preview: suggestion.data.preview
175
- },
176
- timestamp: Date.now(),
177
- source: "stackmemory"
178
- };
179
- await this.client.upsertEpisode(episode);
180
- } catch (error) {
181
- logger.debug("Graphiti suggestion_ready episode failed", {
182
- error: error instanceof Error ? error.message : String(error)
183
- });
184
- }
185
- }
186
- async onAgentStart(event) {
187
- const e = event;
188
- try {
189
- const episode = {
190
- type: "agent_start",
191
- content: { agentType: e.data.agentType, task: e.data.task },
192
- timestamp: Date.now(),
193
- source: "stackmemory"
194
- };
195
- await this.client.upsertEpisode(episode);
196
- } catch (error) {
197
- logger.debug("Graphiti agent_start episode failed", {
198
- error: error instanceof Error ? error.message : String(error)
199
- });
200
- }
201
- }
202
- async onAgentComplete(event) {
203
- const e = event;
204
- try {
205
- const episode = {
206
- type: "agent_complete",
207
- content: { ...e.data },
208
- timestamp: Date.now(),
209
- source: "stackmemory"
210
- };
211
- await this.client.upsertEpisode(episode);
212
- } catch (error) {
213
- logger.debug("Graphiti agent_complete episode failed", {
214
- error: error instanceof Error ? error.message : String(error)
215
- });
216
- }
217
- }
218
- async onAgentError(event) {
219
- const e = event;
220
- try {
221
- const episode = {
222
- type: "agent_error",
223
- content: { agentType: e.data.agentType, error: e.data.error },
224
- timestamp: Date.now(),
225
- source: "stackmemory"
226
- };
227
- await this.client.upsertEpisode(episode);
228
- } catch (error) {
229
- logger.debug("Graphiti agent_error episode failed", {
230
- error: error instanceof Error ? error.message : String(error)
231
- });
232
- }
233
- }
234
- // Expose a simple temporal query helper for future MCP tooling
235
- async buildTemporalContext(query = {}) {
236
- const now = Date.now();
237
- const q = {
238
- query: query.query || void 0,
239
- entityTypes: query.entityTypes || void 0,
240
- relationTypes: query.relationTypes || void 0,
241
- validFrom: query.validFrom ?? now - 1e3 * 60 * 60 * 24 * 30,
242
- // 30d default
243
- validTo: query.validTo ?? now,
244
- maxHops: query.maxHops ?? this.config.maxHops,
245
- k: query.k ?? 20,
246
- rerank: query.rerank ?? true
247
- };
248
- return this.client.queryTemporal(q);
249
- }
250
- }
251
- export {
252
- GraphitiHooks
253
- };