@stackmemoryai/stackmemory 1.2.7 → 1.2.9

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.
Files changed (27) hide show
  1. package/README.md +3 -3
  2. package/dist/src/cli/commands/daemon.js +306 -2
  3. package/dist/src/cli/commands/desires.js +117 -0
  4. package/dist/src/cli/commands/digest.js +73 -0
  5. package/dist/src/cli/commands/setup.js +369 -39
  6. package/dist/src/cli/commands/team.js +168 -0
  7. package/dist/src/cli/index.js +6 -0
  8. package/dist/src/core/digest/chronological-digest.js +143 -0
  9. package/dist/src/core/digest/index.js +1 -0
  10. package/dist/src/integrations/mcp/server.js +622 -251
  11. package/dist/src/integrations/mcp/tool-definitions.js +607 -50
  12. package/dist/src/utils/hook-installer.js +35 -0
  13. package/package.json +2 -2
  14. package/scripts/install-claude-hooks-auto.js +35 -0
  15. package/templates/claude-hooks/daemon-auto-start.js +125 -0
  16. package/templates/claude-hooks/desire-path-trace.js +118 -0
  17. package/templates/claude-hooks/session-rescue.sh +3 -0
  18. package/templates/claude-hooks/team-subagent-stop.js +77 -0
  19. package/templates/claude-hooks/team-task-complete.js +83 -0
  20. package/templates/claude-hooks/team-teammate-idle.js +96 -0
  21. /package/templates/claude-hooks/{auto-background-hook.js → archive/auto-background-hook.js} +0 -0
  22. /package/templates/claude-hooks/{hook-config.json → archive/hook-config.json} +0 -0
  23. /package/templates/claude-hooks/{hooks.json → archive/hooks.json} +0 -0
  24. /package/templates/claude-hooks/{on-compact-detected → archive/on-compact-detected} +0 -0
  25. /package/templates/claude-hooks/{on-exit → archive/on-exit} +0 -0
  26. /package/templates/claude-hooks/{post-edit-sweep.js → archive/post-edit-sweep.js} +0 -0
  27. /package/templates/claude-hooks/{pre-tool-use → archive/pre-tool-use} +0 -0
@@ -47,6 +47,41 @@ const CANONICAL_HOOKS = [
47
47
  timeout: 2,
48
48
  commandPrefix: "node",
49
49
  required: false
50
+ },
51
+ {
52
+ scriptName: "team-subagent-stop.js",
53
+ eventType: "SubagentStop",
54
+ timeout: 5,
55
+ commandPrefix: "node",
56
+ required: false
57
+ },
58
+ {
59
+ scriptName: "team-task-complete.js",
60
+ eventType: "TaskCompleted",
61
+ timeout: 5,
62
+ commandPrefix: "node",
63
+ required: false
64
+ },
65
+ {
66
+ scriptName: "team-teammate-idle.js",
67
+ eventType: "TeammateIdle",
68
+ timeout: 3,
69
+ commandPrefix: "node",
70
+ required: false
71
+ },
72
+ {
73
+ scriptName: "desire-path-trace.js",
74
+ eventType: "PostToolUse",
75
+ timeout: 2,
76
+ commandPrefix: "node",
77
+ required: false
78
+ },
79
+ {
80
+ scriptName: "daemon-auto-start.js",
81
+ eventType: "PostToolUse",
82
+ timeout: 2,
83
+ commandPrefix: "node",
84
+ required: false
50
85
  }
51
86
  ];
52
87
  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.7",
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.",
3
+ "version": "1.2.9",
4
+ "description": "Project-scoped memory for AI coding tools. Durable context across sessions with 56 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"
@@ -89,6 +89,41 @@ try {
89
89
  commandPrefix: 'node',
90
90
  required: true,
91
91
  },
92
+ {
93
+ scriptName: 'team-subagent-stop.js',
94
+ eventType: 'SubagentStop',
95
+ timeout: 5,
96
+ commandPrefix: 'node',
97
+ required: false,
98
+ },
99
+ {
100
+ scriptName: 'team-task-complete.js',
101
+ eventType: 'TaskCompleted',
102
+ timeout: 5,
103
+ commandPrefix: 'node',
104
+ required: false,
105
+ },
106
+ {
107
+ scriptName: 'team-teammate-idle.js',
108
+ eventType: 'TeammateIdle',
109
+ timeout: 3,
110
+ commandPrefix: 'node',
111
+ required: false,
112
+ },
113
+ {
114
+ scriptName: 'desire-path-trace.js',
115
+ eventType: 'PostToolUse',
116
+ timeout: 2,
117
+ commandPrefix: 'node',
118
+ required: false,
119
+ },
120
+ {
121
+ scriptName: 'daemon-auto-start.js',
122
+ eventType: 'PostToolUse',
123
+ timeout: 2,
124
+ commandPrefix: 'node',
125
+ required: false,
126
+ },
92
127
  ];
93
128
 
94
129
  const DEAD_HOOKS = ['sms-response-handler.js'];
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Daemon Auto-Start Hook (PostToolUse)
5
+ *
6
+ * Checks if the unified daemon is running on each tool call.
7
+ * If not, spawns it in the background. Runs at most once per session
8
+ * (tracks via env var to avoid repeated checks).
9
+ *
10
+ * Must complete in <50ms — PID file check + optional spawn.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { spawn } = require('child_process');
16
+
17
+ const HOME = process.env.HOME || '/tmp';
18
+ const DAEMON_DIR = path.join(HOME, '.stackmemory', 'daemon');
19
+ const PID_FILE = path.join(DAEMON_DIR, 'daemon.pid');
20
+
21
+ // Track whether we already checked this session (avoid repeated spawns)
22
+ const STATE_KEY = 'STACKMEMORY_DAEMON_CHECKED';
23
+ const SESSION_ID = process.env.CLAUDE_INSTANCE_ID || String(process.ppid);
24
+ const STATE_FILE = path.join(
25
+ HOME,
26
+ '.stackmemory',
27
+ `daemon-check-${SESSION_ID}`
28
+ );
29
+
30
+ function isDaemonRunning() {
31
+ try {
32
+ if (!fs.existsSync(PID_FILE)) return false;
33
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10);
34
+ if (isNaN(pid)) return false;
35
+ process.kill(pid, 0); // signal 0 = check existence
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ function findDaemonScript() {
43
+ // Check common locations for the daemon script
44
+ const candidates = [
45
+ // Global npm install
46
+ path.join(
47
+ __dirname,
48
+ '..',
49
+ '..',
50
+ 'node_modules',
51
+ '@stackmemoryai',
52
+ 'stackmemory',
53
+ 'dist',
54
+ 'daemon',
55
+ 'unified-daemon.js'
56
+ ),
57
+ // Homebrew global
58
+ path.join(
59
+ '/opt/homebrew/lib/node_modules/@stackmemoryai/stackmemory/dist/daemon/unified-daemon.js'
60
+ ),
61
+ // ~/.stackmemory/bin (installed by postinstall)
62
+ path.join(HOME, '.stackmemory', 'bin', 'session-daemon.js'),
63
+ ];
64
+
65
+ for (const candidate of candidates) {
66
+ if (fs.existsSync(candidate)) return candidate;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ function alreadyChecked() {
72
+ try {
73
+ if (fs.existsSync(STATE_FILE)) {
74
+ const age = Date.now() - fs.statSync(STATE_FILE).mtimeMs;
75
+ // Re-check every 30 minutes
76
+ return age < 30 * 60 * 1000;
77
+ }
78
+ } catch {
79
+ // ignore
80
+ }
81
+ return false;
82
+ }
83
+
84
+ function markChecked() {
85
+ try {
86
+ const dir = path.dirname(STATE_FILE);
87
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
88
+ fs.writeFileSync(STATE_FILE, String(Date.now()));
89
+ } catch {
90
+ // best effort
91
+ }
92
+ }
93
+
94
+ async function main() {
95
+ try {
96
+ // Skip if recently checked
97
+ if (alreadyChecked()) return;
98
+ markChecked();
99
+
100
+ // Already running? Done.
101
+ if (isDaemonRunning()) return;
102
+
103
+ // Try to spawn daemon
104
+ const script = findDaemonScript();
105
+ if (!script) return;
106
+
107
+ const child = spawn('node', [script], {
108
+ detached: true,
109
+ stdio: 'ignore',
110
+ env: { ...process.env },
111
+ });
112
+ child.unref();
113
+ } catch {
114
+ // Silent fail -- never block the agent
115
+ }
116
+ }
117
+
118
+ // Read stdin (required by hook protocol) then run
119
+ let input = '';
120
+ process.stdin.on('data', (chunk) => {
121
+ input += chunk;
122
+ });
123
+ process.stdin.on('end', () => {
124
+ main();
125
+ });
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Desire Path Trace Hook (PostToolUse)
5
+ *
6
+ * Detects failed tool calls and logs them to ~/.stackmemory/desire-paths/
7
+ * as JSONL for later analysis. Tracks what agents *want* but can't get:
8
+ * unknown tools, invalid params, handler errors.
9
+ *
10
+ * Must complete in <50ms -- pure file I/O only.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const HOME = process.env.HOME || '/tmp';
17
+ const DESIRE_DIR = path.join(HOME, '.stackmemory', 'desire-paths');
18
+
19
+ function ensureDir(dir) {
20
+ if (!fs.existsSync(dir)) {
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ }
23
+ }
24
+
25
+ function categorize(errorText) {
26
+ if (!errorText) return 'handler_error';
27
+ if (/unknown tool/i.test(errorText)) return 'unknown_tool';
28
+ if (
29
+ /invalid param|missing.*param|required.*param|unexpected.*param/i.test(
30
+ errorText
31
+ )
32
+ )
33
+ return 'invalid_params';
34
+ return 'handler_error';
35
+ }
36
+
37
+ function isFailure(input) {
38
+ // Check is_error flag
39
+ if (input.tool_response && input.tool_response.is_error === true) return true;
40
+
41
+ // Check for error text in response
42
+ const response =
43
+ typeof input.tool_response === 'string'
44
+ ? input.tool_response
45
+ : JSON.stringify(input.tool_response || '');
46
+
47
+ if (/Unknown tool:/i.test(response)) return true;
48
+ if (/^Error:/m.test(response)) return true;
49
+ if (/McpError|MCP error/i.test(response)) return true;
50
+
51
+ return false;
52
+ }
53
+
54
+ function extractError(input) {
55
+ const response = input.tool_response;
56
+ if (!response) return 'unknown error';
57
+
58
+ if (typeof response === 'string') return response.slice(0, 500);
59
+ if (response.error) return String(response.error).slice(0, 500);
60
+ if (response.content && Array.isArray(response.content)) {
61
+ const textBlock = response.content.find((b) => b.type === 'text');
62
+ if (textBlock) return String(textBlock.text).slice(0, 500);
63
+ }
64
+
65
+ return JSON.stringify(response).slice(0, 500);
66
+ }
67
+
68
+ function truncateInput(toolInput) {
69
+ if (!toolInput) return {};
70
+ const result = {};
71
+ for (const [key, value] of Object.entries(toolInput)) {
72
+ if (typeof value === 'string' && value.length > 200) {
73
+ result[key] = value.slice(0, 200) + '...[truncated]';
74
+ } else {
75
+ result[key] = value;
76
+ }
77
+ }
78
+ return result;
79
+ }
80
+
81
+ async function readInput() {
82
+ let input = '';
83
+ for await (const chunk of process.stdin) {
84
+ input += chunk;
85
+ }
86
+ return JSON.parse(input);
87
+ }
88
+
89
+ async function main() {
90
+ try {
91
+ const input = await readInput();
92
+
93
+ if (!isFailure(input)) return;
94
+
95
+ const errorText = extractError(input);
96
+ const date = new Date().toISOString().slice(0, 10);
97
+ const entry = {
98
+ ts: new Date().toISOString(),
99
+ sid:
100
+ input.session_id ||
101
+ process.env.CLAUDE_INSTANCE_ID ||
102
+ String(process.ppid),
103
+ tool: input.tool_name || 'unknown',
104
+ input: truncateInput(input.tool_input),
105
+ error: errorText,
106
+ category: categorize(errorText),
107
+ cwd: input.cwd || process.cwd(),
108
+ };
109
+
110
+ ensureDir(DESIRE_DIR);
111
+ const filePath = path.join(DESIRE_DIR, `desire-${date}.jsonl`);
112
+ fs.appendFileSync(filePath, JSON.stringify(entry) + '\n');
113
+ } catch {
114
+ // Silent fail -- never block the agent
115
+ }
116
+ }
117
+
118
+ main();
@@ -13,3 +13,6 @@ command -v stackmemory >/dev/null 2>&1 || exit 0
13
13
  # Capture without committing, compact format, 10s timeout
14
14
  timeout 10 stackmemory capture --no-commit --format compact \
15
15
  -m "auto-rescue on session close" >/dev/null 2>&1 || true
16
+
17
+ # Generate today's digest (best-effort, 5s timeout)
18
+ timeout 5 stackmemory digest today >/dev/null 2>&1 || true
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Team Subagent Stop Hook (SubagentStop)
5
+ *
6
+ * Fires when a subagent finishes. Captures the last assistant message
7
+ * as shared team context so other agents can see findings without
8
+ * manual team_context_share calls.
9
+ *
10
+ * Fire-and-forget: exits 0 always, never blocks the agent.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { spawn } = require('child_process');
16
+
17
+ const MAX_CONTENT = 500;
18
+
19
+ function hasStackmemory(cwd) {
20
+ let dir = cwd || process.cwd();
21
+ for (let i = 0; i < 20; i++) {
22
+ if (fs.existsSync(path.join(dir, '.stackmemory', 'context.db'))) {
23
+ return true;
24
+ }
25
+ const parent = path.dirname(dir);
26
+ if (parent === dir) break;
27
+ dir = parent;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ async function readInput() {
33
+ let input = '';
34
+ for await (const chunk of process.stdin) {
35
+ input += chunk;
36
+ }
37
+ return JSON.parse(input);
38
+ }
39
+
40
+ async function main() {
41
+ try {
42
+ const input = await readInput();
43
+ const { agent_id, last_assistant_message, cwd } = input;
44
+
45
+ if (!hasStackmemory(cwd || process.cwd())) return;
46
+
47
+ const message = (last_assistant_message || '').slice(0, MAX_CONTENT).trim();
48
+ if (!message) return;
49
+
50
+ const args = [
51
+ 'team',
52
+ 'share',
53
+ '-c',
54
+ message,
55
+ '-t',
56
+ 'FACT',
57
+ '-p',
58
+ '7',
59
+ '--source',
60
+ 'subagent',
61
+ ];
62
+ if (agent_id) {
63
+ args.push('--agent-id', String(agent_id));
64
+ }
65
+
66
+ const child = spawn('stackmemory', args, {
67
+ stdio: 'ignore',
68
+ detached: true,
69
+ cwd: cwd || process.cwd(),
70
+ });
71
+ child.unref();
72
+ } catch {
73
+ // Silent fail — never block the agent
74
+ }
75
+ }
76
+
77
+ main();
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Team Task Complete Hook (TaskCompleted)
5
+ *
6
+ * Fires when a task is marked complete. Shares the completion summary
7
+ * as a DECISION anchor so other agents know what was accomplished.
8
+ *
9
+ * Fire-and-forget: exits 0 always, never blocks the agent.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { spawn } = require('child_process');
15
+
16
+ const MAX_CONTENT = 800;
17
+
18
+ function hasStackmemory(cwd) {
19
+ let dir = cwd || process.cwd();
20
+ for (let i = 0; i < 20; i++) {
21
+ if (fs.existsSync(path.join(dir, '.stackmemory', 'context.db'))) {
22
+ return true;
23
+ }
24
+ const parent = path.dirname(dir);
25
+ if (parent === dir) break;
26
+ dir = parent;
27
+ }
28
+ return false;
29
+ }
30
+
31
+ async function readInput() {
32
+ let input = '';
33
+ for await (const chunk of process.stdin) {
34
+ input += chunk;
35
+ }
36
+ return JSON.parse(input);
37
+ }
38
+
39
+ async function main() {
40
+ try {
41
+ const input = await readInput();
42
+ const { task_id, task_subject, task_description, teammate_name, cwd } =
43
+ input;
44
+
45
+ if (!hasStackmemory(cwd || process.cwd())) return;
46
+
47
+ const who = teammate_name || 'agent';
48
+ const subject = task_subject || 'unknown task';
49
+ const desc = task_description || '';
50
+ const content = `Task completed by ${who}: ${subject}. ${desc}`
51
+ .slice(0, MAX_CONTENT)
52
+ .trim();
53
+
54
+ if (!content) return;
55
+
56
+ const args = [
57
+ 'team',
58
+ 'share',
59
+ '-c',
60
+ content,
61
+ '-t',
62
+ 'DECISION',
63
+ '-p',
64
+ '8',
65
+ '--source',
66
+ 'task-complete',
67
+ ];
68
+ if (task_id) {
69
+ args.push('--task-id', String(task_id));
70
+ }
71
+
72
+ const child = spawn('stackmemory', args, {
73
+ stdio: 'ignore',
74
+ detached: true,
75
+ cwd: cwd || process.cwd(),
76
+ });
77
+ child.unref();
78
+ } catch {
79
+ // Silent fail — never block the agent
80
+ }
81
+ }
82
+
83
+ main();
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Team Teammate Idle Hook (TeammateIdle)
5
+ *
6
+ * Fires when a teammate is about to go idle. Logs the idle event
7
+ * for audit and shares a low-priority FACT anchor.
8
+ *
9
+ * Fire-and-forget: exits 0 always, never blocks the agent.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { spawn } = require('child_process');
15
+
16
+ const HOME = process.env.HOME || '/tmp';
17
+
18
+ function hasStackmemory(cwd) {
19
+ let dir = cwd || process.cwd();
20
+ for (let i = 0; i < 20; i++) {
21
+ if (fs.existsSync(path.join(dir, '.stackmemory', 'context.db'))) {
22
+ return true;
23
+ }
24
+ const parent = path.dirname(dir);
25
+ if (parent === dir) break;
26
+ dir = parent;
27
+ }
28
+ return false;
29
+ }
30
+
31
+ function ensureDir(dir) {
32
+ if (!fs.existsSync(dir)) {
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ }
35
+ }
36
+
37
+ async function readInput() {
38
+ let input = '';
39
+ for await (const chunk of process.stdin) {
40
+ input += chunk;
41
+ }
42
+ return JSON.parse(input);
43
+ }
44
+
45
+ async function main() {
46
+ try {
47
+ const input = await readInput();
48
+ const { teammate_name, team_name, cwd } = input;
49
+
50
+ if (!hasStackmemory(cwd || process.cwd())) return;
51
+
52
+ const name = teammate_name || 'unknown';
53
+
54
+ // Log idle event for audit
55
+ const auditDir = path.join(HOME, '.stackmemory', 'team-events');
56
+ ensureDir(auditDir);
57
+ const entry = {
58
+ event: 'teammate_idle',
59
+ teammate: name,
60
+ team: team_name || 'default',
61
+ ts: new Date().toISOString(),
62
+ cwd: cwd || process.cwd(),
63
+ };
64
+ const auditFile = path.join(
65
+ auditDir,
66
+ `idle-${new Date().toISOString().slice(0, 10)}.jsonl`
67
+ );
68
+ fs.appendFileSync(auditFile, JSON.stringify(entry) + '\n');
69
+
70
+ // Share low-priority anchor
71
+ const content = `Teammate ${name} idle`;
72
+ const args = [
73
+ 'team',
74
+ 'share',
75
+ '-c',
76
+ content,
77
+ '-t',
78
+ 'FACT',
79
+ '-p',
80
+ '4',
81
+ '--source',
82
+ 'teammate-idle',
83
+ ];
84
+
85
+ const child = spawn('stackmemory', args, {
86
+ stdio: 'ignore',
87
+ detached: true,
88
+ cwd: cwd || process.cwd(),
89
+ });
90
+ child.unref();
91
+ } catch {
92
+ // Silent fail — never block the agent
93
+ }
94
+ }
95
+
96
+ main();