@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.
- package/README.md +3 -3
- package/dist/src/cli/commands/daemon.js +306 -2
- package/dist/src/cli/commands/desires.js +117 -0
- package/dist/src/cli/commands/digest.js +73 -0
- package/dist/src/cli/commands/setup.js +369 -39
- package/dist/src/cli/commands/team.js +168 -0
- package/dist/src/cli/index.js +6 -0
- package/dist/src/core/digest/chronological-digest.js +143 -0
- package/dist/src/core/digest/index.js +1 -0
- package/dist/src/integrations/mcp/server.js +622 -251
- package/dist/src/integrations/mcp/tool-definitions.js +607 -50
- package/dist/src/utils/hook-installer.js +35 -0
- package/package.json +2 -2
- package/scripts/install-claude-hooks-auto.js +35 -0
- package/templates/claude-hooks/daemon-auto-start.js +125 -0
- package/templates/claude-hooks/desire-path-trace.js +118 -0
- package/templates/claude-hooks/session-rescue.sh +3 -0
- package/templates/claude-hooks/team-subagent-stop.js +77 -0
- package/templates/claude-hooks/team-task-complete.js +83 -0
- package/templates/claude-hooks/team-teammate-idle.js +96 -0
- /package/templates/claude-hooks/{auto-background-hook.js → archive/auto-background-hook.js} +0 -0
- /package/templates/claude-hooks/{hook-config.json → archive/hook-config.json} +0 -0
- /package/templates/claude-hooks/{hooks.json → archive/hooks.json} +0 -0
- /package/templates/claude-hooks/{on-compact-detected → archive/on-compact-detected} +0 -0
- /package/templates/claude-hooks/{on-exit → archive/on-exit} +0 -0
- /package/templates/claude-hooks/{post-edit-sweep.js → archive/post-edit-sweep.js} +0 -0
- /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.
|
|
4
|
-
"description": "Project-scoped memory for AI coding tools. Durable context across sessions with
|
|
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();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|