@stackmemoryai/stackmemory 0.5.0 → 0.5.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.
- package/dist/cli/commands/config.js +81 -0
- package/dist/cli/commands/config.js.map +2 -2
- package/dist/cli/commands/decision.js +262 -0
- package/dist/cli/commands/decision.js.map +7 -0
- package/dist/cli/commands/handoff.js +87 -24
- package/dist/cli/commands/handoff.js.map +3 -3
- package/dist/cli/commands/service.js +684 -0
- package/dist/cli/commands/service.js.map +7 -0
- package/dist/cli/commands/sweep.js +311 -0
- package/dist/cli/commands/sweep.js.map +7 -0
- package/dist/cli/index.js +98 -4
- package/dist/cli/index.js.map +2 -2
- package/dist/cli/streamlined-cli.js +144 -0
- package/dist/cli/streamlined-cli.js.map +7 -0
- package/dist/core/config/storage-config.js +111 -0
- package/dist/core/config/storage-config.js.map +7 -0
- package/dist/core/events/event-bus.js +110 -0
- package/dist/core/events/event-bus.js.map +7 -0
- package/dist/core/plugins/plugin-interface.js +87 -0
- package/dist/core/plugins/plugin-interface.js.map +7 -0
- package/dist/core/session/enhanced-handoff.js +654 -0
- package/dist/core/session/enhanced-handoff.js.map +7 -0
- package/dist/core/storage/simplified-storage.js +328 -0
- package/dist/core/storage/simplified-storage.js.map +7 -0
- package/dist/daemon/session-daemon.js +308 -0
- package/dist/daemon/session-daemon.js.map +7 -0
- package/dist/plugins/linear/index.js +166 -0
- package/dist/plugins/linear/index.js.map +7 -0
- package/dist/plugins/loader.js +57 -0
- package/dist/plugins/loader.js.map +7 -0
- package/dist/plugins/plugin-interface.js +67 -0
- package/dist/plugins/plugin-interface.js.map +7 -0
- package/dist/plugins/ralph/simple-ralph-plugin.js +305 -0
- package/dist/plugins/ralph/simple-ralph-plugin.js.map +7 -0
- package/dist/plugins/ralph/use-cases/code-generator.js +151 -0
- package/dist/plugins/ralph/use-cases/code-generator.js.map +7 -0
- package/dist/plugins/ralph/use-cases/test-generator.js +201 -0
- package/dist/plugins/ralph/use-cases/test-generator.js.map +7 -0
- package/dist/skills/repo-ingestion-skill.js +54 -10
- package/dist/skills/repo-ingestion-skill.js.map +2 -2
- package/package.json +4 -8
- package/scripts/archive/check-all-duplicates.ts +2 -2
- package/scripts/archive/merge-linear-duplicates.ts +6 -4
- package/scripts/install-claude-hooks-auto.js +72 -15
- package/scripts/measure-handoff-impact.mjs +395 -0
- package/scripts/measure-handoff-impact.ts +450 -0
- package/templates/claude-hooks/on-startup.js +200 -19
- package/templates/services/com.stackmemory.guardian.plist +59 -0
- package/templates/services/stackmemory-guardian.service +41 -0
- package/scripts/testing/results/real-performance-results.json +0 -90
- package/scripts/testing/test-tier-migration.js +0 -100
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import * as fs from "fs/promises";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { spawn, execSync } from "child_process";
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
function getServiceConfig() {
|
|
9
|
+
const home = process.env.HOME || "";
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
if (platform === "darwin") {
|
|
12
|
+
return {
|
|
13
|
+
platform: "darwin",
|
|
14
|
+
serviceDir: path.join(home, "Library", "LaunchAgents"),
|
|
15
|
+
serviceName: "com.stackmemory.guardian",
|
|
16
|
+
serviceFile: path.join(
|
|
17
|
+
home,
|
|
18
|
+
"Library",
|
|
19
|
+
"LaunchAgents",
|
|
20
|
+
"com.stackmemory.guardian.plist"
|
|
21
|
+
),
|
|
22
|
+
logDir: path.join(home, ".stackmemory", "logs")
|
|
23
|
+
};
|
|
24
|
+
} else if (platform === "linux") {
|
|
25
|
+
return {
|
|
26
|
+
platform: "linux",
|
|
27
|
+
serviceDir: path.join(home, ".config", "systemd", "user"),
|
|
28
|
+
serviceName: "stackmemory-guardian",
|
|
29
|
+
serviceFile: path.join(
|
|
30
|
+
home,
|
|
31
|
+
".config",
|
|
32
|
+
"systemd",
|
|
33
|
+
"user",
|
|
34
|
+
"stackmemory-guardian.service"
|
|
35
|
+
),
|
|
36
|
+
logDir: path.join(home, ".stackmemory", "logs")
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
platform: "unsupported",
|
|
41
|
+
serviceDir: "",
|
|
42
|
+
serviceName: "",
|
|
43
|
+
serviceFile: "",
|
|
44
|
+
logDir: path.join(home, ".stackmemory", "logs")
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function _getStackMemoryBinPath() {
|
|
48
|
+
const localBin = path.join(process.cwd(), "dist", "cli", "index.js");
|
|
49
|
+
if (existsSync(localBin)) {
|
|
50
|
+
return localBin;
|
|
51
|
+
}
|
|
52
|
+
const globalBin = path.join(
|
|
53
|
+
process.env.HOME || "",
|
|
54
|
+
".stackmemory",
|
|
55
|
+
"bin",
|
|
56
|
+
"stackmemory"
|
|
57
|
+
);
|
|
58
|
+
if (existsSync(globalBin)) {
|
|
59
|
+
return globalBin;
|
|
60
|
+
}
|
|
61
|
+
return "npx stackmemory";
|
|
62
|
+
}
|
|
63
|
+
void _getStackMemoryBinPath;
|
|
64
|
+
function getNodePath() {
|
|
65
|
+
try {
|
|
66
|
+
const nodePath = execSync("which node", { encoding: "utf-8" }).trim();
|
|
67
|
+
return nodePath;
|
|
68
|
+
} catch {
|
|
69
|
+
return "/usr/local/bin/node";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function generateMacOSPlist(config) {
|
|
73
|
+
const home = process.env.HOME || "";
|
|
74
|
+
const nodePath = getNodePath();
|
|
75
|
+
const guardianScript = path.join(home, ".stackmemory", "guardian.js");
|
|
76
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
77
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
78
|
+
<plist version="1.0">
|
|
79
|
+
<dict>
|
|
80
|
+
<key>Label</key>
|
|
81
|
+
<string>${config.serviceName}</string>
|
|
82
|
+
|
|
83
|
+
<key>ProgramArguments</key>
|
|
84
|
+
<array>
|
|
85
|
+
<string>${nodePath}</string>
|
|
86
|
+
<string>${guardianScript}</string>
|
|
87
|
+
</array>
|
|
88
|
+
|
|
89
|
+
<key>RunAtLoad</key>
|
|
90
|
+
<true/>
|
|
91
|
+
|
|
92
|
+
<key>KeepAlive</key>
|
|
93
|
+
<dict>
|
|
94
|
+
<key>SuccessfulExit</key>
|
|
95
|
+
<false/>
|
|
96
|
+
</dict>
|
|
97
|
+
|
|
98
|
+
<key>WorkingDirectory</key>
|
|
99
|
+
<string>${home}/.stackmemory</string>
|
|
100
|
+
|
|
101
|
+
<key>StandardOutPath</key>
|
|
102
|
+
<string>${config.logDir}/guardian.log</string>
|
|
103
|
+
|
|
104
|
+
<key>StandardErrorPath</key>
|
|
105
|
+
<string>${config.logDir}/guardian.error.log</string>
|
|
106
|
+
|
|
107
|
+
<key>EnvironmentVariables</key>
|
|
108
|
+
<dict>
|
|
109
|
+
<key>HOME</key>
|
|
110
|
+
<string>${home}</string>
|
|
111
|
+
<key>PATH</key>
|
|
112
|
+
<string>/usr/local/bin:/usr/bin:/bin</string>
|
|
113
|
+
</dict>
|
|
114
|
+
|
|
115
|
+
<key>ThrottleInterval</key>
|
|
116
|
+
<integer>30</integer>
|
|
117
|
+
</dict>
|
|
118
|
+
</plist>`;
|
|
119
|
+
}
|
|
120
|
+
function generateLinuxSystemdService(config) {
|
|
121
|
+
const home = process.env.HOME || "";
|
|
122
|
+
const nodePath = getNodePath();
|
|
123
|
+
const guardianScript = path.join(home, ".stackmemory", "guardian.js");
|
|
124
|
+
return `[Unit]
|
|
125
|
+
Description=StackMemory Guardian Service
|
|
126
|
+
Documentation=https://github.com/stackmemoryai/stackmemory
|
|
127
|
+
After=network.target
|
|
128
|
+
|
|
129
|
+
[Service]
|
|
130
|
+
Type=simple
|
|
131
|
+
ExecStart=${nodePath} ${guardianScript}
|
|
132
|
+
Restart=on-failure
|
|
133
|
+
RestartSec=30
|
|
134
|
+
WorkingDirectory=${home}/.stackmemory
|
|
135
|
+
|
|
136
|
+
Environment=HOME=${home}
|
|
137
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
138
|
+
|
|
139
|
+
StandardOutput=append:${config.logDir}/guardian.log
|
|
140
|
+
StandardError=append:${config.logDir}/guardian.error.log
|
|
141
|
+
|
|
142
|
+
[Install]
|
|
143
|
+
WantedBy=default.target`;
|
|
144
|
+
}
|
|
145
|
+
function generateGuardianScript() {
|
|
146
|
+
return `#!/usr/bin/env node
|
|
147
|
+
/**
|
|
148
|
+
* StackMemory Guardian Service
|
|
149
|
+
* Monitors ~/.stackmemory/sessions/ for active sessions
|
|
150
|
+
* and manages context sync accordingly.
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
const fs = require('fs');
|
|
154
|
+
const path = require('path');
|
|
155
|
+
const { spawn } = require('child_process');
|
|
156
|
+
|
|
157
|
+
const HOME = process.env.HOME || '';
|
|
158
|
+
const SESSIONS_DIR = path.join(HOME, '.stackmemory', 'sessions');
|
|
159
|
+
const STATE_FILE = path.join(HOME, '.stackmemory', 'guardian.state');
|
|
160
|
+
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
161
|
+
|
|
162
|
+
class Guardian {
|
|
163
|
+
constructor() {
|
|
164
|
+
this.syncProcess = null;
|
|
165
|
+
this.lastActivityTime = Date.now();
|
|
166
|
+
this.activeSessions = new Set();
|
|
167
|
+
this.checkInterval = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
log(message, level = 'INFO') {
|
|
171
|
+
const timestamp = new Date().toISOString();
|
|
172
|
+
console.log('[' + timestamp + '] [' + level + '] ' + message);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async getActiveSessions() {
|
|
176
|
+
const sessions = new Set();
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
180
|
+
return sessions;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const files = fs.readdirSync(SESSIONS_DIR);
|
|
184
|
+
|
|
185
|
+
for (const file of files) {
|
|
186
|
+
if (!file.endsWith('.json')) continue;
|
|
187
|
+
|
|
188
|
+
const filePath = path.join(SESSIONS_DIR, file);
|
|
189
|
+
try {
|
|
190
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
191
|
+
const session = JSON.parse(content);
|
|
192
|
+
|
|
193
|
+
// Check if session is active (updated within last 5 minutes)
|
|
194
|
+
const lastUpdate = new Date(session.lastActiveAt || session.startedAt).getTime();
|
|
195
|
+
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
|
|
196
|
+
|
|
197
|
+
if (session.state === 'active' && lastUpdate > fiveMinutesAgo) {
|
|
198
|
+
sessions.add(session.sessionId);
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
// Skip invalid session files
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
this.log('Error reading sessions: ' + err.message, 'ERROR');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return sessions;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
startContextSync() {
|
|
212
|
+
if (this.syncProcess) {
|
|
213
|
+
this.log('Context sync already running');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.log('Starting context sync...');
|
|
218
|
+
|
|
219
|
+
// Find stackmemory binary
|
|
220
|
+
const stackmemoryPaths = [
|
|
221
|
+
path.join(HOME, '.stackmemory', 'bin', 'stackmemory'),
|
|
222
|
+
'npx'
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
let binPath = null;
|
|
226
|
+
for (const p of stackmemoryPaths) {
|
|
227
|
+
if (p === 'npx' || fs.existsSync(p)) {
|
|
228
|
+
binPath = p;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!binPath) {
|
|
234
|
+
this.log('Cannot find stackmemory binary', 'ERROR');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const args = binPath === 'npx'
|
|
239
|
+
? ['stackmemory', 'monitor', '--daemon']
|
|
240
|
+
: ['monitor', '--daemon'];
|
|
241
|
+
|
|
242
|
+
this.syncProcess = spawn(binPath, args, {
|
|
243
|
+
detached: true,
|
|
244
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
this.syncProcess.stdout.on('data', (data) => {
|
|
248
|
+
this.log('sync: ' + data.toString().trim());
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
this.syncProcess.stderr.on('data', (data) => {
|
|
252
|
+
this.log('sync error: ' + data.toString().trim(), 'WARN');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
this.syncProcess.on('exit', (code) => {
|
|
256
|
+
this.log('Context sync exited with code: ' + code);
|
|
257
|
+
this.syncProcess = null;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
this.log('Context sync started');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
stopContextSync() {
|
|
264
|
+
if (!this.syncProcess) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.log('Stopping context sync...');
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
this.syncProcess.kill('SIGTERM');
|
|
272
|
+
this.syncProcess = null;
|
|
273
|
+
this.log('Context sync stopped');
|
|
274
|
+
} catch (err) {
|
|
275
|
+
this.log('Error stopping sync: ' + err.message, 'ERROR');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
saveState() {
|
|
280
|
+
const state = {
|
|
281
|
+
lastCheck: new Date().toISOString(),
|
|
282
|
+
activeSessions: Array.from(this.activeSessions),
|
|
283
|
+
syncRunning: this.syncProcess !== null,
|
|
284
|
+
lastActivity: new Date(this.lastActivityTime).toISOString()
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
289
|
+
} catch (err) {
|
|
290
|
+
this.log('Error saving state: ' + err.message, 'ERROR');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async check() {
|
|
295
|
+
const currentSessions = await this.getActiveSessions();
|
|
296
|
+
const hadActivity = currentSessions.size > 0;
|
|
297
|
+
|
|
298
|
+
if (hadActivity) {
|
|
299
|
+
this.lastActivityTime = Date.now();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Detect session changes
|
|
303
|
+
const newSessions = [...currentSessions].filter(s => !this.activeSessions.has(s));
|
|
304
|
+
const closedSessions = [...this.activeSessions].filter(s => !currentSessions.has(s));
|
|
305
|
+
|
|
306
|
+
if (newSessions.length > 0) {
|
|
307
|
+
this.log('New sessions detected: ' + newSessions.join(', '));
|
|
308
|
+
if (!this.syncProcess) {
|
|
309
|
+
this.startContextSync();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (closedSessions.length > 0) {
|
|
314
|
+
this.log('Sessions closed: ' + closedSessions.join(', '));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.activeSessions = currentSessions;
|
|
318
|
+
|
|
319
|
+
// Check idle timeout
|
|
320
|
+
const idleTime = Date.now() - this.lastActivityTime;
|
|
321
|
+
if (this.syncProcess && currentSessions.size === 0 && idleTime > IDLE_TIMEOUT_MS) {
|
|
322
|
+
this.log('No activity for 30 minutes, stopping sync');
|
|
323
|
+
this.stopContextSync();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.saveState();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async start() {
|
|
330
|
+
this.log('StackMemory Guardian starting...');
|
|
331
|
+
this.log('Monitoring: ' + SESSIONS_DIR);
|
|
332
|
+
|
|
333
|
+
// Ensure directories exist
|
|
334
|
+
const dirs = [
|
|
335
|
+
SESSIONS_DIR,
|
|
336
|
+
path.join(HOME, '.stackmemory', 'logs')
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
for (const dir of dirs) {
|
|
340
|
+
if (!fs.existsSync(dir)) {
|
|
341
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Initial check
|
|
346
|
+
await this.check();
|
|
347
|
+
|
|
348
|
+
// Start monitoring loop (every 30 seconds)
|
|
349
|
+
this.checkInterval = setInterval(() => this.check(), 30 * 1000);
|
|
350
|
+
|
|
351
|
+
this.log('Guardian started successfully');
|
|
352
|
+
|
|
353
|
+
// Handle shutdown signals
|
|
354
|
+
process.on('SIGTERM', () => this.stop());
|
|
355
|
+
process.on('SIGINT', () => this.stop());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
stop() {
|
|
359
|
+
this.log('Guardian stopping...');
|
|
360
|
+
|
|
361
|
+
if (this.checkInterval) {
|
|
362
|
+
clearInterval(this.checkInterval);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
this.stopContextSync();
|
|
366
|
+
|
|
367
|
+
// Clean up state file
|
|
368
|
+
try {
|
|
369
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
370
|
+
fs.unlinkSync(STATE_FILE);
|
|
371
|
+
}
|
|
372
|
+
} catch (err) {
|
|
373
|
+
// Ignore
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.log('Guardian stopped');
|
|
377
|
+
process.exit(0);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const guardian = new Guardian();
|
|
382
|
+
guardian.start().catch(err => {
|
|
383
|
+
console.error('Guardian failed to start:', err);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
});
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
async function installService(config, spinner) {
|
|
389
|
+
const home = process.env.HOME || "";
|
|
390
|
+
await fs.mkdir(config.serviceDir, { recursive: true });
|
|
391
|
+
await fs.mkdir(config.logDir, { recursive: true });
|
|
392
|
+
const guardianPath = path.join(home, ".stackmemory", "guardian.js");
|
|
393
|
+
await fs.writeFile(guardianPath, generateGuardianScript(), "utf-8");
|
|
394
|
+
await fs.chmod(guardianPath, 493);
|
|
395
|
+
if (config.platform === "darwin") {
|
|
396
|
+
const plistContent = generateMacOSPlist(config);
|
|
397
|
+
await fs.writeFile(config.serviceFile, plistContent, "utf-8");
|
|
398
|
+
spinner.text = "Loading service...";
|
|
399
|
+
try {
|
|
400
|
+
execSync(`launchctl load -w "${config.serviceFile}"`, { stdio: "pipe" });
|
|
401
|
+
} catch {
|
|
402
|
+
try {
|
|
403
|
+
execSync(`launchctl unload "${config.serviceFile}"`, { stdio: "pipe" });
|
|
404
|
+
execSync(`launchctl load -w "${config.serviceFile}"`, {
|
|
405
|
+
stdio: "pipe"
|
|
406
|
+
});
|
|
407
|
+
} catch {
|
|
408
|
+
throw new Error("Failed to load launchd service");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
spinner.succeed(chalk.green("Guardian service installed and started"));
|
|
412
|
+
console.log(chalk.gray(`Service file: ${config.serviceFile}`));
|
|
413
|
+
console.log(chalk.gray(`Guardian script: ${guardianPath}`));
|
|
414
|
+
console.log(chalk.gray(`Logs: ${config.logDir}/guardian.log`));
|
|
415
|
+
} else if (config.platform === "linux") {
|
|
416
|
+
const serviceContent = generateLinuxSystemdService(config);
|
|
417
|
+
await fs.writeFile(config.serviceFile, serviceContent, "utf-8");
|
|
418
|
+
spinner.text = "Enabling service...";
|
|
419
|
+
try {
|
|
420
|
+
execSync("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
421
|
+
execSync(`systemctl --user enable ${config.serviceName}`, {
|
|
422
|
+
stdio: "pipe"
|
|
423
|
+
});
|
|
424
|
+
execSync(`systemctl --user start ${config.serviceName}`, {
|
|
425
|
+
stdio: "pipe"
|
|
426
|
+
});
|
|
427
|
+
} catch {
|
|
428
|
+
throw new Error(
|
|
429
|
+
"Failed to enable systemd service. Make sure systemd user session is available."
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
spinner.succeed(chalk.green("Guardian service installed and started"));
|
|
433
|
+
console.log(chalk.gray(`Service file: ${config.serviceFile}`));
|
|
434
|
+
console.log(chalk.gray(`Guardian script: ${guardianPath}`));
|
|
435
|
+
console.log(chalk.gray(`Logs: ${config.logDir}/guardian.log`));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function uninstallService(config, spinner) {
|
|
439
|
+
const home = process.env.HOME || "";
|
|
440
|
+
const guardianPath = path.join(home, ".stackmemory", "guardian.js");
|
|
441
|
+
if (config.platform === "darwin") {
|
|
442
|
+
spinner.text = "Unloading service...";
|
|
443
|
+
try {
|
|
444
|
+
execSync(`launchctl unload "${config.serviceFile}"`, { stdio: "pipe" });
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
await fs.unlink(config.serviceFile);
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
await fs.unlink(guardianPath);
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
spinner.succeed(chalk.green("Guardian service uninstalled"));
|
|
456
|
+
} else if (config.platform === "linux") {
|
|
457
|
+
spinner.text = "Stopping service...";
|
|
458
|
+
try {
|
|
459
|
+
execSync(`systemctl --user stop ${config.serviceName}`, {
|
|
460
|
+
stdio: "pipe"
|
|
461
|
+
});
|
|
462
|
+
execSync(`systemctl --user disable ${config.serviceName}`, {
|
|
463
|
+
stdio: "pipe"
|
|
464
|
+
});
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
await fs.unlink(config.serviceFile);
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
await fs.unlink(guardianPath);
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
execSync("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
spinner.succeed(chalk.green("Guardian service uninstalled"));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function showServiceStatus(config) {
|
|
483
|
+
const home = process.env.HOME || "";
|
|
484
|
+
const stateFile = path.join(home, ".stackmemory", "guardian.state");
|
|
485
|
+
console.log(chalk.bold("\nStackMemory Guardian Service Status\n"));
|
|
486
|
+
if (config.platform === "unsupported") {
|
|
487
|
+
console.log(chalk.red("Platform not supported for service installation"));
|
|
488
|
+
console.log(
|
|
489
|
+
chalk.gray("Supported platforms: macOS (launchd), Linux (systemd)")
|
|
490
|
+
);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (!existsSync(config.serviceFile)) {
|
|
494
|
+
console.log(chalk.yellow("Service not installed"));
|
|
495
|
+
console.log(chalk.gray("Install with: stackmemory service install"));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
let isRunning = false;
|
|
499
|
+
let serviceOutput = "";
|
|
500
|
+
if (config.platform === "darwin") {
|
|
501
|
+
try {
|
|
502
|
+
serviceOutput = execSync(`launchctl list | grep ${config.serviceName}`, {
|
|
503
|
+
encoding: "utf-8",
|
|
504
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
505
|
+
});
|
|
506
|
+
isRunning = serviceOutput.includes(config.serviceName);
|
|
507
|
+
} catch {
|
|
508
|
+
isRunning = false;
|
|
509
|
+
}
|
|
510
|
+
} else if (config.platform === "linux") {
|
|
511
|
+
try {
|
|
512
|
+
serviceOutput = execSync(
|
|
513
|
+
`systemctl --user is-active ${config.serviceName}`,
|
|
514
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
515
|
+
).trim();
|
|
516
|
+
isRunning = serviceOutput === "active";
|
|
517
|
+
} catch {
|
|
518
|
+
isRunning = false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (isRunning) {
|
|
522
|
+
console.log(chalk.green("Status: Running"));
|
|
523
|
+
} else {
|
|
524
|
+
console.log(chalk.yellow("Status: Stopped"));
|
|
525
|
+
}
|
|
526
|
+
console.log(chalk.gray(`Platform: ${config.platform}`));
|
|
527
|
+
console.log(chalk.gray(`Service: ${config.serviceName}`));
|
|
528
|
+
console.log(chalk.gray(`Config: ${config.serviceFile}`));
|
|
529
|
+
if (existsSync(stateFile)) {
|
|
530
|
+
try {
|
|
531
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
532
|
+
console.log(chalk.bold("\nGuardian State:"));
|
|
533
|
+
console.log(` Last check: ${state.lastCheck}`);
|
|
534
|
+
console.log(` Active sessions: ${state.activeSessions?.length || 0}`);
|
|
535
|
+
console.log(` Sync running: ${state.syncRunning ? "Yes" : "No"}`);
|
|
536
|
+
console.log(` Last activity: ${state.lastActivity}`);
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async function showServiceLogs(config, lines) {
|
|
542
|
+
console.log(
|
|
543
|
+
chalk.bold(`
|
|
544
|
+
StackMemory Guardian Logs (last ${lines} lines)
|
|
545
|
+
`)
|
|
546
|
+
);
|
|
547
|
+
const logFile = path.join(config.logDir, "guardian.log");
|
|
548
|
+
if (!existsSync(logFile)) {
|
|
549
|
+
console.log(chalk.yellow("No logs found"));
|
|
550
|
+
console.log(chalk.gray(`Expected at: ${logFile}`));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
const content = readFileSync(logFile, "utf-8");
|
|
555
|
+
const logLines = content.split("\n").filter(Boolean);
|
|
556
|
+
const lastLines = logLines.slice(-lines);
|
|
557
|
+
lastLines.forEach((line) => {
|
|
558
|
+
if (line.includes("[ERROR]")) {
|
|
559
|
+
console.log(chalk.red(line));
|
|
560
|
+
} else if (line.includes("[WARN]")) {
|
|
561
|
+
console.log(chalk.yellow(line));
|
|
562
|
+
} else {
|
|
563
|
+
console.log(chalk.gray(line));
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
console.log(chalk.gray(`
|
|
567
|
+
Full log: ${logFile}`));
|
|
568
|
+
} catch (err) {
|
|
569
|
+
console.log(chalk.red(`Failed to read logs: ${err.message}`));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function createServiceCommand() {
|
|
573
|
+
const cmd = new Command("service").description("Manage StackMemory guardian OS service (auto-start on login)").addHelpText(
|
|
574
|
+
"after",
|
|
575
|
+
`
|
|
576
|
+
Examples:
|
|
577
|
+
stackmemory service install Install and start the guardian service
|
|
578
|
+
stackmemory service uninstall Remove the guardian service
|
|
579
|
+
stackmemory service status Show service status
|
|
580
|
+
stackmemory service logs Show recent service logs
|
|
581
|
+
stackmemory service logs -n 50 Show last 50 log lines
|
|
582
|
+
|
|
583
|
+
The guardian service:
|
|
584
|
+
- Monitors ~/.stackmemory/sessions/ for active sessions
|
|
585
|
+
- Starts context sync when an active session is detected
|
|
586
|
+
- Stops gracefully after 30 minutes of inactivity
|
|
587
|
+
- Runs automatically on system login (opt-in)
|
|
588
|
+
`
|
|
589
|
+
);
|
|
590
|
+
cmd.command("install").description("Install the guardian service (starts on login)").action(async () => {
|
|
591
|
+
const spinner = ora("Installing guardian service...").start();
|
|
592
|
+
try {
|
|
593
|
+
const config = getServiceConfig();
|
|
594
|
+
if (config.platform === "unsupported") {
|
|
595
|
+
spinner.fail(chalk.red("Platform not supported"));
|
|
596
|
+
console.log(
|
|
597
|
+
chalk.gray("Supported: macOS (launchd), Linux (systemd)")
|
|
598
|
+
);
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
await installService(config, spinner);
|
|
602
|
+
console.log(chalk.bold("\nGuardian service will:"));
|
|
603
|
+
console.log(" - Start automatically on login");
|
|
604
|
+
console.log(" - Monitor for active StackMemory sessions");
|
|
605
|
+
console.log(" - Manage context sync based on activity");
|
|
606
|
+
console.log(" - Stop gracefully after 30 min idle");
|
|
607
|
+
} catch (err) {
|
|
608
|
+
spinner.fail(
|
|
609
|
+
chalk.red(`Installation failed: ${err.message}`)
|
|
610
|
+
);
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
cmd.command("uninstall").description("Remove the guardian service").action(async () => {
|
|
615
|
+
const spinner = ora("Uninstalling guardian service...").start();
|
|
616
|
+
try {
|
|
617
|
+
const config = getServiceConfig();
|
|
618
|
+
if (config.platform === "unsupported") {
|
|
619
|
+
spinner.fail(chalk.red("Platform not supported"));
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
await uninstallService(config, spinner);
|
|
623
|
+
} catch (err) {
|
|
624
|
+
spinner.fail(
|
|
625
|
+
chalk.red(`Uninstallation failed: ${err.message}`)
|
|
626
|
+
);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
cmd.command("status").description("Show guardian service status").action(async () => {
|
|
631
|
+
try {
|
|
632
|
+
const config = getServiceConfig();
|
|
633
|
+
await showServiceStatus(config);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
console.error(
|
|
636
|
+
chalk.red(`Status check failed: ${err.message}`)
|
|
637
|
+
);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
cmd.command("logs").description("Show recent guardian service logs").option("-n, --lines <number>", "Number of log lines to show", "20").option("-f, --follow", "Follow log output (tail -f style)").action(async (options) => {
|
|
642
|
+
try {
|
|
643
|
+
const config = getServiceConfig();
|
|
644
|
+
const lines = parseInt(options.lines) || 20;
|
|
645
|
+
if (options.follow) {
|
|
646
|
+
const logFile = path.join(config.logDir, "guardian.log");
|
|
647
|
+
console.log(chalk.bold(`Following ${logFile} (Ctrl+C to stop)
|
|
648
|
+
`));
|
|
649
|
+
const tail = spawn("tail", ["-f", "-n", lines.toString(), logFile], {
|
|
650
|
+
stdio: "inherit"
|
|
651
|
+
});
|
|
652
|
+
process.on("SIGINT", () => {
|
|
653
|
+
tail.kill();
|
|
654
|
+
process.exit(0);
|
|
655
|
+
});
|
|
656
|
+
} else {
|
|
657
|
+
await showServiceLogs(config, lines);
|
|
658
|
+
}
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.error(
|
|
661
|
+
chalk.red(`Failed to show logs: ${err.message}`)
|
|
662
|
+
);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
cmd.action(async () => {
|
|
667
|
+
try {
|
|
668
|
+
const config = getServiceConfig();
|
|
669
|
+
await showServiceStatus(config);
|
|
670
|
+
} catch (err) {
|
|
671
|
+
console.error(
|
|
672
|
+
chalk.red(`Status check failed: ${err.message}`)
|
|
673
|
+
);
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
return cmd;
|
|
678
|
+
}
|
|
679
|
+
var service_default = createServiceCommand();
|
|
680
|
+
export {
|
|
681
|
+
createServiceCommand,
|
|
682
|
+
service_default as default
|
|
683
|
+
};
|
|
684
|
+
//# sourceMappingURL=service.js.map
|