@stackmemoryai/stackmemory 0.5.49 → 0.5.51
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 +17 -3
- package/dist/cli/claude-sm.js +246 -5
- package/dist/cli/claude-sm.js.map +3 -3
- package/dist/cli/commands/sweep.js +190 -421
- package/dist/cli/commands/sweep.js.map +3 -3
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +2 -2
- package/dist/core/config/feature-flags.js +7 -1
- package/dist/core/config/feature-flags.js.map +2 -2
- package/dist/core/context/enhanced-rehydration.js +355 -9
- package/dist/core/context/enhanced-rehydration.js.map +3 -3
- package/dist/core/context/shared-context-layer.js +229 -0
- package/dist/core/context/shared-context-layer.js.map +2 -2
- package/dist/features/sweep/index.js +20 -0
- package/dist/features/sweep/index.js.map +7 -0
- package/dist/features/sweep/prediction-client.js +155 -0
- package/dist/features/sweep/prediction-client.js.map +7 -0
- package/dist/features/sweep/prompt-builder.js +85 -0
- package/dist/features/sweep/prompt-builder.js.map +7 -0
- package/dist/features/sweep/pty-wrapper.js +171 -0
- package/dist/features/sweep/pty-wrapper.js.map +7 -0
- package/dist/features/sweep/state-watcher.js +87 -0
- package/dist/features/sweep/state-watcher.js.map +7 -0
- package/dist/features/sweep/status-bar.js +88 -0
- package/dist/features/sweep/status-bar.js.map +7 -0
- package/dist/features/sweep/sweep-server-manager.js +226 -0
- package/dist/features/sweep/sweep-server-manager.js.map +7 -0
- package/dist/features/sweep/tab-interceptor.js +38 -0
- package/dist/features/sweep/tab-interceptor.js.map +7 -0
- package/dist/features/sweep/types.js +18 -0
- package/dist/features/sweep/types.js.map +7 -0
- package/package.json +1 -1
- package/scripts/test-setup-e2e.sh +154 -0
|
@@ -0,0 +1,171 @@
|
|
|
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 { join } from "path";
|
|
6
|
+
import { writeFileSync, existsSync, mkdirSync } from "fs";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import { SweepStateWatcher } from "./state-watcher.js";
|
|
9
|
+
import { StatusBar } from "./status-bar.js";
|
|
10
|
+
import { TabInterceptor } from "./tab-interceptor.js";
|
|
11
|
+
const HOME = process.env["HOME"] || "/tmp";
|
|
12
|
+
const PENDING_FILE = join(HOME, ".stackmemory", "sweep-pending.json");
|
|
13
|
+
const ALT_SCREEN_ENTER = "\x1B[?1049h";
|
|
14
|
+
const ALT_SCREEN_EXIT = "\x1B[?1049l";
|
|
15
|
+
class PtyWrapper {
|
|
16
|
+
config;
|
|
17
|
+
stateWatcher;
|
|
18
|
+
statusBar;
|
|
19
|
+
tabInterceptor;
|
|
20
|
+
currentPrediction = null;
|
|
21
|
+
inAltScreen = false;
|
|
22
|
+
ptyProcess = null;
|
|
23
|
+
constructor(config = {}) {
|
|
24
|
+
this.config = {
|
|
25
|
+
claudeBin: config.claudeBin || this.findClaude(),
|
|
26
|
+
claudeArgs: config.claudeArgs || [],
|
|
27
|
+
stateFile: config.stateFile || join(HOME, ".stackmemory", "sweep-state.json")
|
|
28
|
+
};
|
|
29
|
+
this.stateWatcher = new SweepStateWatcher(this.config.stateFile);
|
|
30
|
+
this.statusBar = new StatusBar();
|
|
31
|
+
this.tabInterceptor = new TabInterceptor({
|
|
32
|
+
onAccept: () => this.acceptPrediction(),
|
|
33
|
+
onDismiss: () => this.dismissPrediction(),
|
|
34
|
+
onPassthrough: (data) => this.ptyProcess?.write(data.toString("utf-8"))
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async start() {
|
|
38
|
+
let pty;
|
|
39
|
+
try {
|
|
40
|
+
pty = await import("node-pty");
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"node-pty is required for the PTY wrapper.\nInstall with: npm install node-pty"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const cols = process.stdout.columns || 80;
|
|
47
|
+
const rows = process.stdout.rows || 24;
|
|
48
|
+
const env = {};
|
|
49
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
50
|
+
if (v !== void 0) env[k] = v;
|
|
51
|
+
}
|
|
52
|
+
this.ptyProcess = pty.spawn(this.config.claudeBin, this.config.claudeArgs, {
|
|
53
|
+
name: process.env["TERM"] || "xterm-256color",
|
|
54
|
+
cols,
|
|
55
|
+
rows: rows - 1,
|
|
56
|
+
cwd: process.cwd(),
|
|
57
|
+
env
|
|
58
|
+
});
|
|
59
|
+
if (process.stdin.isTTY) {
|
|
60
|
+
process.stdin.setRawMode(true);
|
|
61
|
+
}
|
|
62
|
+
process.stdin.resume();
|
|
63
|
+
this.ptyProcess.onData((data) => {
|
|
64
|
+
if (data.includes(ALT_SCREEN_ENTER)) {
|
|
65
|
+
this.inAltScreen = true;
|
|
66
|
+
this.statusBar.hide();
|
|
67
|
+
}
|
|
68
|
+
if (data.includes(ALT_SCREEN_EXIT)) {
|
|
69
|
+
this.inAltScreen = false;
|
|
70
|
+
}
|
|
71
|
+
process.stdout.write(data);
|
|
72
|
+
});
|
|
73
|
+
process.stdin.on("data", (data) => {
|
|
74
|
+
this.tabInterceptor.process(data);
|
|
75
|
+
});
|
|
76
|
+
this.stateWatcher.on("loading", () => {
|
|
77
|
+
if (!this.inAltScreen) {
|
|
78
|
+
this.statusBar.showLoading();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
this.stateWatcher.on("prediction", (event) => {
|
|
82
|
+
this.currentPrediction = event;
|
|
83
|
+
this.tabInterceptor.setPredictionActive(true);
|
|
84
|
+
if (!this.inAltScreen) {
|
|
85
|
+
this.statusBar.show(
|
|
86
|
+
event.prediction,
|
|
87
|
+
event.file_path,
|
|
88
|
+
event.latency_ms
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
this.stateWatcher.start();
|
|
93
|
+
process.stdout.on("resize", () => {
|
|
94
|
+
const newCols = process.stdout.columns || 80;
|
|
95
|
+
const newRows = process.stdout.rows || 24;
|
|
96
|
+
this.ptyProcess?.resize(newCols, newRows - 1);
|
|
97
|
+
this.statusBar.resize(newRows, newCols);
|
|
98
|
+
});
|
|
99
|
+
this.ptyProcess.onExit(({ exitCode }) => {
|
|
100
|
+
this.cleanup();
|
|
101
|
+
process.exit(exitCode);
|
|
102
|
+
});
|
|
103
|
+
const onSignal = () => {
|
|
104
|
+
this.cleanup();
|
|
105
|
+
process.exit(0);
|
|
106
|
+
};
|
|
107
|
+
process.on("SIGINT", onSignal);
|
|
108
|
+
process.on("SIGTERM", onSignal);
|
|
109
|
+
}
|
|
110
|
+
acceptPrediction() {
|
|
111
|
+
if (!this.currentPrediction || !this.ptyProcess) return;
|
|
112
|
+
const dir = join(HOME, ".stackmemory");
|
|
113
|
+
if (!existsSync(dir)) {
|
|
114
|
+
mkdirSync(dir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
writeFileSync(
|
|
117
|
+
PENDING_FILE,
|
|
118
|
+
JSON.stringify(
|
|
119
|
+
{
|
|
120
|
+
file_path: this.currentPrediction.file_path,
|
|
121
|
+
predicted_content: this.currentPrediction.prediction,
|
|
122
|
+
timestamp: Date.now()
|
|
123
|
+
},
|
|
124
|
+
null,
|
|
125
|
+
2
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
const prompt = `Apply the Sweep prediction from ${PENDING_FILE}
|
|
129
|
+
`;
|
|
130
|
+
this.ptyProcess.write(prompt);
|
|
131
|
+
this.dismissPrediction();
|
|
132
|
+
}
|
|
133
|
+
dismissPrediction() {
|
|
134
|
+
this.currentPrediction = null;
|
|
135
|
+
this.tabInterceptor.setPredictionActive(false);
|
|
136
|
+
this.statusBar.hide();
|
|
137
|
+
}
|
|
138
|
+
cleanup() {
|
|
139
|
+
this.stateWatcher.stop();
|
|
140
|
+
this.statusBar.hide();
|
|
141
|
+
if (process.stdin.isTTY) {
|
|
142
|
+
process.stdin.setRawMode(false);
|
|
143
|
+
}
|
|
144
|
+
process.stdin.pause();
|
|
145
|
+
}
|
|
146
|
+
findClaude() {
|
|
147
|
+
try {
|
|
148
|
+
const resolved = execSync("which claude", { encoding: "utf-8" }).trim();
|
|
149
|
+
if (resolved) return resolved;
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
const candidates = [
|
|
153
|
+
join(HOME, ".bun", "bin", "claude"),
|
|
154
|
+
"/usr/local/bin/claude",
|
|
155
|
+
"/opt/homebrew/bin/claude"
|
|
156
|
+
];
|
|
157
|
+
for (const c of candidates) {
|
|
158
|
+
if (existsSync(c)) return c;
|
|
159
|
+
}
|
|
160
|
+
return "claude";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function launchWrapper(config) {
|
|
164
|
+
const wrapper = new PtyWrapper(config);
|
|
165
|
+
await wrapper.start();
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
PtyWrapper,
|
|
169
|
+
launchWrapper
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=pty-wrapper.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/features/sweep/pty-wrapper.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Sweep PTY Wrapper\n *\n * Wraps Claude Code in a pseudo-terminal to add a Sweep prediction\n * status bar at the bottom of the terminal. Predictions from the\n * PostToolUse hook are displayed via the status bar. Tab to accept,\n * Esc to dismiss.\n */\n\nimport { join } from 'path';\nimport { writeFileSync, existsSync, mkdirSync } from 'fs';\nimport { execSync } from 'child_process';\nimport { SweepStateWatcher, type PredictionEvent } from './state-watcher.js';\nimport { StatusBar } from './status-bar.js';\nimport { TabInterceptor } from './tab-interceptor.js';\n\nconst HOME = process.env['HOME'] || '/tmp';\nconst PENDING_FILE = join(HOME, '.stackmemory', 'sweep-pending.json');\n\n// Alt screen buffer detection\nconst ALT_SCREEN_ENTER = '\\x1b[?1049h';\nconst ALT_SCREEN_EXIT = '\\x1b[?1049l';\n\nexport interface PtyWrapperConfig {\n claudeBin?: string;\n claudeArgs?: string[];\n stateFile?: string;\n}\n\n// Minimal interface for node-pty process to avoid compile-time dep\ninterface PtyProcess {\n write(data: string): void;\n resize(cols: number, rows: number): void;\n onData(cb: (data: string) => void): void;\n onExit(cb: (e: { exitCode: number }) => void): void;\n kill(): void;\n}\n\nexport class PtyWrapper {\n private config: Required<PtyWrapperConfig>;\n private stateWatcher: SweepStateWatcher;\n private statusBar: StatusBar;\n private tabInterceptor: TabInterceptor;\n private currentPrediction: PredictionEvent | null = null;\n private inAltScreen = false;\n private ptyProcess: PtyProcess | null = null;\n\n constructor(config: PtyWrapperConfig = {}) {\n this.config = {\n claudeBin: config.claudeBin || this.findClaude(),\n claudeArgs: config.claudeArgs || [],\n stateFile:\n config.stateFile || join(HOME, '.stackmemory', 'sweep-state.json'),\n };\n\n this.stateWatcher = new SweepStateWatcher(this.config.stateFile);\n this.statusBar = new StatusBar();\n this.tabInterceptor = new TabInterceptor({\n onAccept: () => this.acceptPrediction(),\n onDismiss: () => this.dismissPrediction(),\n onPassthrough: (data) => this.ptyProcess?.write(data.toString('utf-8')),\n });\n }\n\n async start(): Promise<void> {\n // Dynamic import for optional dependency\n let pty: typeof import('node-pty');\n try {\n pty = await import('node-pty');\n } catch {\n throw new Error(\n 'node-pty is required for the PTY wrapper.\\n' +\n 'Install with: npm install node-pty'\n );\n }\n\n const cols = process.stdout.columns || 80;\n const rows = process.stdout.rows || 24;\n\n // Filter undefined values from env\n const env: Record<string, string> = {};\n for (const [k, v] of Object.entries(process.env)) {\n if (v !== undefined) env[k] = v;\n }\n\n // Spawn Claude Code in a PTY with 1 row reserved for status bar\n this.ptyProcess = pty.spawn(this.config.claudeBin, this.config.claudeArgs, {\n name: process.env['TERM'] || 'xterm-256color',\n cols,\n rows: rows - 1,\n cwd: process.cwd(),\n env,\n }) as PtyProcess;\n\n // Set raw mode on stdin\n if (process.stdin.isTTY) {\n process.stdin.setRawMode(true);\n }\n process.stdin.resume();\n\n // PTY stdout -> parent stdout (transparent passthrough)\n this.ptyProcess.onData((data: string) => {\n // Detect alt screen buffer transitions\n if (data.includes(ALT_SCREEN_ENTER)) {\n this.inAltScreen = true;\n this.statusBar.hide();\n }\n if (data.includes(ALT_SCREEN_EXIT)) {\n this.inAltScreen = false;\n }\n\n process.stdout.write(data);\n });\n\n // Parent stdin -> tab interceptor -> PTY\n process.stdin.on('data', (data: Buffer) => {\n this.tabInterceptor.process(data);\n });\n\n // State watcher -> status bar\n this.stateWatcher.on('loading', () => {\n if (!this.inAltScreen) {\n this.statusBar.showLoading();\n }\n });\n\n this.stateWatcher.on('prediction', (event: PredictionEvent) => {\n this.currentPrediction = event;\n this.tabInterceptor.setPredictionActive(true);\n if (!this.inAltScreen) {\n this.statusBar.show(\n event.prediction,\n event.file_path,\n event.latency_ms\n );\n }\n });\n\n this.stateWatcher.start();\n\n // Handle terminal resize\n process.stdout.on('resize', () => {\n const newCols = process.stdout.columns || 80;\n const newRows = process.stdout.rows || 24;\n this.ptyProcess?.resize(newCols, newRows - 1);\n this.statusBar.resize(newRows, newCols);\n });\n\n // Handle PTY exit\n this.ptyProcess.onExit(({ exitCode }) => {\n this.cleanup();\n process.exit(exitCode);\n });\n\n // Handle signals\n const onSignal = () => {\n this.cleanup();\n process.exit(0);\n };\n process.on('SIGINT', onSignal);\n process.on('SIGTERM', onSignal);\n }\n\n private acceptPrediction(): void {\n if (!this.currentPrediction || !this.ptyProcess) return;\n\n // Write prediction to pending file for Claude to read\n const dir = join(HOME, '.stackmemory');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(\n PENDING_FILE,\n JSON.stringify(\n {\n file_path: this.currentPrediction.file_path,\n predicted_content: this.currentPrediction.prediction,\n timestamp: Date.now(),\n },\n null,\n 2\n )\n );\n\n // Inject acceptance prompt into PTY stdin.\n // SAFETY: PENDING_FILE is a constant path, not user-controlled.\n // The prompt is written to Claude Code's input, which interprets\n // it as a user message, not as a shell command.\n const prompt = `Apply the Sweep prediction from ${PENDING_FILE}\\n`;\n this.ptyProcess.write(prompt);\n\n this.dismissPrediction();\n }\n\n private dismissPrediction(): void {\n this.currentPrediction = null;\n this.tabInterceptor.setPredictionActive(false);\n this.statusBar.hide();\n }\n\n private cleanup(): void {\n this.stateWatcher.stop();\n this.statusBar.hide();\n\n if (process.stdin.isTTY) {\n process.stdin.setRawMode(false);\n }\n process.stdin.pause();\n }\n\n private findClaude(): string {\n // Check PATH first via which\n try {\n const resolved = execSync('which claude', { encoding: 'utf-8' }).trim();\n if (resolved) return resolved;\n } catch {\n // Not on PATH\n }\n\n // Check known locations\n const candidates = [\n join(HOME, '.bun', 'bin', 'claude'),\n '/usr/local/bin/claude',\n '/opt/homebrew/bin/claude',\n ];\n\n for (const c of candidates) {\n if (existsSync(c)) return c;\n }\n\n return 'claude';\n }\n}\n\n/**\n * Launch the PTY wrapper\n */\nexport async function launchWrapper(config?: PtyWrapperConfig): Promise<void> {\n const wrapper = new PtyWrapper(config);\n await wrapper.start();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { EventEmitter } from "events";
|
|
6
|
+
import { watch, readFileSync, existsSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
const HOME = process.env["HOME"] || "/tmp";
|
|
9
|
+
const DEFAULT_STATE_FILE = join(HOME, ".stackmemory", "sweep-state.json");
|
|
10
|
+
class SweepStateWatcher extends EventEmitter {
|
|
11
|
+
stateFile;
|
|
12
|
+
lastPredictionTs = 0;
|
|
13
|
+
lastPendingTs = 0;
|
|
14
|
+
watcher = null;
|
|
15
|
+
debounceTimer = null;
|
|
16
|
+
pollTimer = null;
|
|
17
|
+
constructor(stateFile) {
|
|
18
|
+
super();
|
|
19
|
+
this.stateFile = stateFile || DEFAULT_STATE_FILE;
|
|
20
|
+
}
|
|
21
|
+
start() {
|
|
22
|
+
if (this.watcher) return;
|
|
23
|
+
if (!existsSync(this.stateFile)) {
|
|
24
|
+
this.pollTimer = setInterval(() => {
|
|
25
|
+
if (existsSync(this.stateFile)) {
|
|
26
|
+
clearInterval(this.pollTimer);
|
|
27
|
+
this.pollTimer = null;
|
|
28
|
+
this.startWatching();
|
|
29
|
+
}
|
|
30
|
+
}, 1e3);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.startWatching();
|
|
34
|
+
}
|
|
35
|
+
startWatching() {
|
|
36
|
+
this.readState();
|
|
37
|
+
this.watcher = watch(this.stateFile, () => {
|
|
38
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
39
|
+
this.debounceTimer = setTimeout(() => this.readState(), 100);
|
|
40
|
+
});
|
|
41
|
+
this.watcher.on("error", () => {
|
|
42
|
+
this.watcher?.close();
|
|
43
|
+
this.watcher = null;
|
|
44
|
+
setTimeout(() => this.start(), 1e3);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
readState() {
|
|
48
|
+
try {
|
|
49
|
+
if (!existsSync(this.stateFile)) return;
|
|
50
|
+
const raw = readFileSync(this.stateFile, "utf-8");
|
|
51
|
+
const state = JSON.parse(raw);
|
|
52
|
+
if (state.pendingPrediction && state.pendingPrediction !== this.lastPendingTs) {
|
|
53
|
+
this.lastPendingTs = state.pendingPrediction;
|
|
54
|
+
this.emit("loading");
|
|
55
|
+
}
|
|
56
|
+
if (state.lastPrediction && state.lastPrediction.timestamp > this.lastPredictionTs) {
|
|
57
|
+
this.lastPredictionTs = state.lastPrediction.timestamp;
|
|
58
|
+
const event = {
|
|
59
|
+
file_path: state.lastPrediction.file_path,
|
|
60
|
+
prediction: state.lastPrediction.prediction,
|
|
61
|
+
latency_ms: state.lastPrediction.latency_ms,
|
|
62
|
+
timestamp: state.lastPrediction.timestamp
|
|
63
|
+
};
|
|
64
|
+
this.emit("prediction", event);
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
stop() {
|
|
70
|
+
if (this.pollTimer) {
|
|
71
|
+
clearInterval(this.pollTimer);
|
|
72
|
+
this.pollTimer = null;
|
|
73
|
+
}
|
|
74
|
+
if (this.debounceTimer) {
|
|
75
|
+
clearTimeout(this.debounceTimer);
|
|
76
|
+
this.debounceTimer = null;
|
|
77
|
+
}
|
|
78
|
+
if (this.watcher) {
|
|
79
|
+
this.watcher.close();
|
|
80
|
+
this.watcher = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
SweepStateWatcher
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=state-watcher.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/features/sweep/state-watcher.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Sweep State Watcher\n *\n * Watches sweep-state.json for new predictions via fs.watch.\n * Emits events when predictions arrive or loading begins.\n */\n\nimport { EventEmitter } from 'events';\nimport { watch, readFileSync, existsSync, type FSWatcher } from 'fs';\nimport { join } from 'path';\n\nconst HOME = process.env['HOME'] || '/tmp';\nconst DEFAULT_STATE_FILE = join(HOME, '.stackmemory', 'sweep-state.json');\n\nexport interface PredictionEvent {\n file_path: string;\n prediction: string;\n latency_ms: number;\n timestamp: number;\n}\n\ninterface SweepState {\n recentDiffs: Array<{\n file_path: string;\n original: string;\n updated: string;\n timestamp: number;\n }>;\n lastPrediction: {\n file_path: string;\n prediction: string;\n latency_ms: number;\n timestamp: number;\n } | null;\n pendingPrediction: number | null;\n fileContents: Record<string, unknown>;\n}\n\nexport class SweepStateWatcher extends EventEmitter {\n private stateFile: string;\n private lastPredictionTs = 0;\n private lastPendingTs = 0;\n private watcher: FSWatcher | null = null;\n private debounceTimer: ReturnType<typeof setTimeout> | null = null;\n private pollTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(stateFile?: string) {\n super();\n this.stateFile = stateFile || DEFAULT_STATE_FILE;\n }\n\n start(): void {\n if (this.watcher) return;\n\n if (!existsSync(this.stateFile)) {\n // Poll until file appears\n this.pollTimer = setInterval(() => {\n if (existsSync(this.stateFile)) {\n clearInterval(this.pollTimer!);\n this.pollTimer = null;\n this.startWatching();\n }\n }, 1000);\n return;\n }\n\n this.startWatching();\n }\n\n private startWatching(): void {\n // Read initial state\n this.readState();\n\n this.watcher = watch(this.stateFile, () => {\n // Debounce rapid writes\n if (this.debounceTimer) clearTimeout(this.debounceTimer);\n this.debounceTimer = setTimeout(() => this.readState(), 100);\n });\n\n this.watcher.on('error', () => {\n // File may have been deleted, try to re-watch\n this.watcher?.close();\n this.watcher = null;\n setTimeout(() => this.start(), 1000);\n });\n }\n\n private readState(): void {\n try {\n if (!existsSync(this.stateFile)) return;\n\n const raw = readFileSync(this.stateFile, 'utf-8');\n const state: SweepState = JSON.parse(raw);\n\n // Check for new pending prediction (loading state)\n if (\n state.pendingPrediction &&\n state.pendingPrediction !== this.lastPendingTs\n ) {\n this.lastPendingTs = state.pendingPrediction;\n this.emit('loading');\n }\n\n // Check for new completed prediction\n if (\n state.lastPrediction &&\n state.lastPrediction.timestamp > this.lastPredictionTs\n ) {\n this.lastPredictionTs = state.lastPrediction.timestamp;\n const event: PredictionEvent = {\n file_path: state.lastPrediction.file_path,\n prediction: state.lastPrediction.prediction,\n latency_ms: state.lastPrediction.latency_ms,\n timestamp: state.lastPrediction.timestamp,\n };\n this.emit('prediction', event);\n }\n } catch {\n // Ignore parse errors from partial writes\n }\n }\n\n stop(): void {\n if (this.pollTimer) {\n clearInterval(this.pollTimer);\n this.pollTimer = null;\n }\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = null;\n }\n if (this.watcher) {\n this.watcher.close();\n this.watcher = null;\n }\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;AAOA,SAAS,oBAAoB;AAC7B,SAAS,OAAO,cAAc,kBAAkC;AAChE,SAAS,YAAY;AAErB,MAAM,OAAO,QAAQ,IAAI,MAAM,KAAK;AACpC,MAAM,qBAAqB,KAAK,MAAM,gBAAgB,kBAAkB;AA0BjE,MAAM,0BAA0B,aAAa;AAAA,EAC1C;AAAA,EACA,mBAAmB;AAAA,EACnB,gBAAgB;AAAA,EAChB,UAA4B;AAAA,EAC5B,gBAAsD;AAAA,EACtD,YAAmD;AAAA,EAE3D,YAAY,WAAoB;AAC9B,UAAM;AACN,SAAK,YAAY,aAAa;AAAA,EAChC;AAAA,EAEA,QAAc;AACZ,QAAI,KAAK,QAAS;AAElB,QAAI,CAAC,WAAW,KAAK,SAAS,GAAG;AAE/B,WAAK,YAAY,YAAY,MAAM;AACjC,YAAI,WAAW,KAAK,SAAS,GAAG;AAC9B,wBAAc,KAAK,SAAU;AAC7B,eAAK,YAAY;AACjB,eAAK,cAAc;AAAA,QACrB;AAAA,MACF,GAAG,GAAI;AACP;AAAA,IACF;AAEA,SAAK,cAAc;AAAA,EACrB;AAAA,EAEQ,gBAAsB;AAE5B,SAAK,UAAU;AAEf,SAAK,UAAU,MAAM,KAAK,WAAW,MAAM;AAEzC,UAAI,KAAK,cAAe,cAAa,KAAK,aAAa;AACvD,WAAK,gBAAgB,WAAW,MAAM,KAAK,UAAU,GAAG,GAAG;AAAA,IAC7D,CAAC;AAED,SAAK,QAAQ,GAAG,SAAS,MAAM;AAE7B,WAAK,SAAS,MAAM;AACpB,WAAK,UAAU;AACf,iBAAW,MAAM,KAAK,MAAM,GAAG,GAAI;AAAA,IACrC,CAAC;AAAA,EACH;AAAA,EAEQ,YAAkB;AACxB,QAAI;AACF,UAAI,CAAC,WAAW,KAAK,SAAS,EAAG;AAEjC,YAAM,MAAM,aAAa,KAAK,WAAW,OAAO;AAChD,YAAM,QAAoB,KAAK,MAAM,GAAG;AAGxC,UACE,MAAM,qBACN,MAAM,sBAAsB,KAAK,eACjC;AACA,aAAK,gBAAgB,MAAM;AAC3B,aAAK,KAAK,SAAS;AAAA,MACrB;AAGA,UACE,MAAM,kBACN,MAAM,eAAe,YAAY,KAAK,kBACtC;AACA,aAAK,mBAAmB,MAAM,eAAe;AAC7C,cAAM,QAAyB;AAAA,UAC7B,WAAW,MAAM,eAAe;AAAA,UAChC,YAAY,MAAM,eAAe;AAAA,UACjC,YAAY,MAAM,eAAe;AAAA,UACjC,WAAW,MAAM,eAAe;AAAA,QAClC;AACA,aAAK,KAAK,cAAc,KAAK;AAAA,MAC/B;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,OAAa;AACX,QAAI,KAAK,WAAW;AAClB,oBAAc,KAAK,SAAS;AAC5B,WAAK,YAAY;AAAA,IACnB;AACA,QAAI,KAAK,eAAe;AACtB,mBAAa,KAAK,aAAa;AAC/B,WAAK,gBAAgB;AAAA,IACvB;AACA,QAAI,KAAK,SAAS;AAChB,WAAK,QAAQ,MAAM;AACnB,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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 { basename } from "path";
|
|
6
|
+
const ESC = "\x1B";
|
|
7
|
+
const SAVE_CURSOR = `${ESC}7`;
|
|
8
|
+
const RESTORE_CURSOR = `${ESC}8`;
|
|
9
|
+
const CLEAR_LINE = `${ESC}[2K`;
|
|
10
|
+
const RESET = `${ESC}[0m`;
|
|
11
|
+
const BG_DARK = `${ESC}[48;5;236m`;
|
|
12
|
+
const FG_GRAY = `${ESC}[38;5;244m`;
|
|
13
|
+
const FG_CYAN = `${ESC}[38;5;37m`;
|
|
14
|
+
const FG_DIM = `${ESC}[2m`;
|
|
15
|
+
const BOLD = `${ESC}[1m`;
|
|
16
|
+
function moveTo(row, col) {
|
|
17
|
+
return `${ESC}[${row};${col}H`;
|
|
18
|
+
}
|
|
19
|
+
class StatusBar {
|
|
20
|
+
rows;
|
|
21
|
+
cols;
|
|
22
|
+
visible = false;
|
|
23
|
+
currentPrediction = "";
|
|
24
|
+
currentFile = "";
|
|
25
|
+
constructor() {
|
|
26
|
+
this.rows = process.stdout.rows || 24;
|
|
27
|
+
this.cols = process.stdout.columns || 80;
|
|
28
|
+
}
|
|
29
|
+
show(prediction, filePath, latencyMs) {
|
|
30
|
+
this.currentPrediction = prediction;
|
|
31
|
+
this.currentFile = filePath;
|
|
32
|
+
this.visible = true;
|
|
33
|
+
const file = basename(filePath);
|
|
34
|
+
const preview = this.truncatePreview(prediction);
|
|
35
|
+
const latency = `${latencyMs}ms`;
|
|
36
|
+
const label = `${FG_CYAN}${BOLD}[Sweep]${RESET}${BG_DARK}`;
|
|
37
|
+
const fileInfo = `${FG_GRAY} ${file}${RESET}${BG_DARK}`;
|
|
38
|
+
const content = `${FG_GRAY} ${preview}${RESET}${BG_DARK}`;
|
|
39
|
+
const time = `${FG_DIM}${BG_DARK} ${latency}${RESET}${BG_DARK}`;
|
|
40
|
+
const keys = `${BOLD}${BG_DARK} [Tab]${RESET}${BG_DARK}${FG_GRAY} Accept ${BOLD}${BG_DARK}[Esc]${RESET}${BG_DARK}${FG_GRAY} Dismiss${RESET}`;
|
|
41
|
+
const bar = `${BG_DARK}${label}${fileInfo}${content}${time}${keys}${RESET}`;
|
|
42
|
+
this.render(bar);
|
|
43
|
+
}
|
|
44
|
+
showLoading() {
|
|
45
|
+
this.visible = true;
|
|
46
|
+
const bar = `${BG_DARK}${FG_CYAN}${BOLD}[Sweep]${RESET}${BG_DARK}${FG_GRAY} Predicting next edit...${RESET}`;
|
|
47
|
+
this.render(bar);
|
|
48
|
+
}
|
|
49
|
+
hide() {
|
|
50
|
+
if (!this.visible) return;
|
|
51
|
+
this.visible = false;
|
|
52
|
+
this.currentPrediction = "";
|
|
53
|
+
this.currentFile = "";
|
|
54
|
+
const output = SAVE_CURSOR + moveTo(this.rows, 1) + CLEAR_LINE + RESTORE_CURSOR;
|
|
55
|
+
process.stdout.write(output);
|
|
56
|
+
}
|
|
57
|
+
resize(rows, cols) {
|
|
58
|
+
this.rows = rows;
|
|
59
|
+
this.cols = cols;
|
|
60
|
+
if (this.visible && this.currentPrediction) {
|
|
61
|
+
this.show(this.currentPrediction, this.currentFile, 0);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
isVisible() {
|
|
65
|
+
return this.visible;
|
|
66
|
+
}
|
|
67
|
+
render(content) {
|
|
68
|
+
if (!process.stdout.isTTY) return;
|
|
69
|
+
const output = SAVE_CURSOR + moveTo(this.rows, 1) + CLEAR_LINE + content + RESTORE_CURSOR;
|
|
70
|
+
process.stdout.write(output);
|
|
71
|
+
}
|
|
72
|
+
truncatePreview(prediction) {
|
|
73
|
+
const lines = prediction.trim().split("\n");
|
|
74
|
+
let preview = lines[0] || "";
|
|
75
|
+
const maxLen = Math.max(10, this.cols - 55);
|
|
76
|
+
if (preview.length > maxLen) {
|
|
77
|
+
preview = preview.slice(0, maxLen - 3) + "...";
|
|
78
|
+
}
|
|
79
|
+
if (lines.length > 1) {
|
|
80
|
+
preview += ` (+${lines.length - 1} lines)`;
|
|
81
|
+
}
|
|
82
|
+
return preview;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
StatusBar
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=status-bar.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/features/sweep/status-bar.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Sweep Status Bar\n *\n * Renders a 1-row prediction status bar at the bottom of the terminal\n * using ANSI escape sequences. Preserves cursor position.\n */\n\nimport { basename } from 'path';\n\n// ANSI escape sequences\nconst ESC = '\\x1b';\nconst SAVE_CURSOR = `${ESC}7`;\nconst RESTORE_CURSOR = `${ESC}8`;\nconst CLEAR_LINE = `${ESC}[2K`;\nconst RESET = `${ESC}[0m`;\nconst BG_DARK = `${ESC}[48;5;236m`; // dark gray bg\nconst FG_GRAY = `${ESC}[38;5;244m`; // gray text\nconst FG_CYAN = `${ESC}[38;5;37m`; // cyan for label\nconst FG_DIM = `${ESC}[2m`; // dim\nconst BOLD = `${ESC}[1m`;\n\nfunction moveTo(row: number, col: number): string {\n return `${ESC}[${row};${col}H`;\n}\n\nexport class StatusBar {\n private rows: number;\n private cols: number;\n private visible = false;\n private currentPrediction = '';\n private currentFile = '';\n\n constructor() {\n this.rows = process.stdout.rows || 24;\n this.cols = process.stdout.columns || 80;\n }\n\n show(prediction: string, filePath: string, latencyMs: number): void {\n this.currentPrediction = prediction;\n this.currentFile = filePath;\n this.visible = true;\n\n const file = basename(filePath);\n const preview = this.truncatePreview(prediction);\n const latency = `${latencyMs}ms`;\n\n // Build status bar content\n const label = `${FG_CYAN}${BOLD}[Sweep]${RESET}${BG_DARK}`;\n const fileInfo = `${FG_GRAY} ${file}${RESET}${BG_DARK}`;\n const content = `${FG_GRAY} ${preview}${RESET}${BG_DARK}`;\n const time = `${FG_DIM}${BG_DARK} ${latency}${RESET}${BG_DARK}`;\n const keys = `${BOLD}${BG_DARK} [Tab]${RESET}${BG_DARK}${FG_GRAY} Accept ${BOLD}${BG_DARK}[Esc]${RESET}${BG_DARK}${FG_GRAY} Dismiss${RESET}`;\n\n const bar = `${BG_DARK}${label}${fileInfo}${content}${time}${keys}${RESET}`;\n\n this.render(bar);\n }\n\n showLoading(): void {\n this.visible = true;\n const bar = `${BG_DARK}${FG_CYAN}${BOLD}[Sweep]${RESET}${BG_DARK}${FG_GRAY} Predicting next edit...${RESET}`;\n this.render(bar);\n }\n\n hide(): void {\n if (!this.visible) return;\n this.visible = false;\n this.currentPrediction = '';\n this.currentFile = '';\n\n const output =\n SAVE_CURSOR + moveTo(this.rows, 1) + CLEAR_LINE + RESTORE_CURSOR;\n\n process.stdout.write(output);\n }\n\n resize(rows: number, cols: number): void {\n this.rows = rows;\n this.cols = cols;\n if (this.visible && this.currentPrediction) {\n this.show(this.currentPrediction, this.currentFile, 0);\n }\n }\n\n isVisible(): boolean {\n return this.visible;\n }\n\n private render(content: string): void {\n if (!process.stdout.isTTY) return;\n\n const output =\n SAVE_CURSOR +\n moveTo(this.rows, 1) +\n CLEAR_LINE +\n content +\n RESTORE_CURSOR;\n\n process.stdout.write(output);\n }\n\n private truncatePreview(prediction: string): string {\n // Take first non-empty line\n const lines = prediction.trim().split('\\n');\n let preview = lines[0] || '';\n\n // Max preview length: cols minus label/keys overhead (~50 chars)\n const maxLen = Math.max(10, this.cols - 55);\n if (preview.length > maxLen) {\n preview = preview.slice(0, maxLen - 3) + '...';\n }\n\n // Indicate more lines\n if (lines.length > 1) {\n preview += ` (+${lines.length - 1} lines)`;\n }\n\n return preview;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
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 { spawn, execSync } from "child_process";
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_SERVER_CONFIG
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
import { createPredictionClient } from "./prediction-client.js";
|
|
12
|
+
import { logger } from "../../core/monitoring/logger.js";
|
|
13
|
+
const HOME = process.env["HOME"] || "/tmp";
|
|
14
|
+
const PID_FILE = join(HOME, ".stackmemory", "sweep", "server.pid");
|
|
15
|
+
const LOG_FILE = join(HOME, ".stackmemory", "sweep", "server.log");
|
|
16
|
+
class SweepServerManager {
|
|
17
|
+
config;
|
|
18
|
+
process = null;
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this.config = { ...DEFAULT_SERVER_CONFIG, ...config };
|
|
21
|
+
if (!this.config.modelPath) {
|
|
22
|
+
this.config.modelPath = join(
|
|
23
|
+
HOME,
|
|
24
|
+
".stackmemory",
|
|
25
|
+
"models",
|
|
26
|
+
"sweep",
|
|
27
|
+
"sweep-next-edit-1.5b.q8_0.v2.gguf"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Find llama-server executable
|
|
33
|
+
*/
|
|
34
|
+
findLlamaServer() {
|
|
35
|
+
const candidates = [
|
|
36
|
+
"llama-server",
|
|
37
|
+
"llama.cpp/llama-server",
|
|
38
|
+
"/usr/local/bin/llama-server",
|
|
39
|
+
"/opt/homebrew/bin/llama-server",
|
|
40
|
+
join(HOME, ".local", "bin", "llama-server")
|
|
41
|
+
];
|
|
42
|
+
for (const cmd of candidates) {
|
|
43
|
+
try {
|
|
44
|
+
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
45
|
+
return cmd;
|
|
46
|
+
} catch {
|
|
47
|
+
if (existsSync(cmd)) {
|
|
48
|
+
return cmd;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Start the llama-server
|
|
56
|
+
*/
|
|
57
|
+
async startServer() {
|
|
58
|
+
const status = await this.getStatus();
|
|
59
|
+
if (status.running) {
|
|
60
|
+
return status;
|
|
61
|
+
}
|
|
62
|
+
if (!existsSync(this.config.modelPath)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Model not found: ${this.config.modelPath}
|
|
65
|
+
Download with: huggingface-cli download sweepai/sweep-next-edit-1.5B sweep-next-edit-1.5b.q8_0.v2.gguf --local-dir ~/.stackmemory/models/sweep`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const llamaServer = this.findLlamaServer();
|
|
69
|
+
if (!llamaServer) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"llama-server not found. Install with:\n brew install llama.cpp\nor build from source: https://github.com/ggerganov/llama.cpp"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const logDir = dirname(LOG_FILE);
|
|
75
|
+
if (!existsSync(logDir)) {
|
|
76
|
+
mkdirSync(logDir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
const args = [
|
|
79
|
+
"-m",
|
|
80
|
+
this.config.modelPath,
|
|
81
|
+
"--port",
|
|
82
|
+
String(this.config.port),
|
|
83
|
+
"--host",
|
|
84
|
+
this.config.host,
|
|
85
|
+
"-c",
|
|
86
|
+
String(this.config.contextSize)
|
|
87
|
+
];
|
|
88
|
+
if (this.config.threads) {
|
|
89
|
+
args.push("-t", String(this.config.threads));
|
|
90
|
+
}
|
|
91
|
+
if (this.config.gpuLayers && this.config.gpuLayers > 0) {
|
|
92
|
+
args.push("-ngl", String(this.config.gpuLayers));
|
|
93
|
+
}
|
|
94
|
+
logger.info("Starting Sweep server", { llamaServer, args });
|
|
95
|
+
this.process = spawn(llamaServer, args, {
|
|
96
|
+
detached: true,
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
98
|
+
});
|
|
99
|
+
if (this.process.pid) {
|
|
100
|
+
const pidDir = dirname(PID_FILE);
|
|
101
|
+
if (!existsSync(pidDir)) {
|
|
102
|
+
mkdirSync(pidDir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
writeFileSync(
|
|
105
|
+
PID_FILE,
|
|
106
|
+
JSON.stringify({
|
|
107
|
+
pid: this.process.pid,
|
|
108
|
+
port: this.config.port,
|
|
109
|
+
host: this.config.host,
|
|
110
|
+
modelPath: this.config.modelPath,
|
|
111
|
+
startedAt: Date.now()
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
this.process.unref();
|
|
116
|
+
const ready = await this.waitForReady(1e4);
|
|
117
|
+
if (!ready) {
|
|
118
|
+
await this.stopServer();
|
|
119
|
+
throw new Error("Server failed to start within timeout");
|
|
120
|
+
}
|
|
121
|
+
return this.getStatus();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Wait for server to be ready
|
|
125
|
+
*/
|
|
126
|
+
async waitForReady(timeoutMs) {
|
|
127
|
+
const client = createPredictionClient({
|
|
128
|
+
port: this.config.port,
|
|
129
|
+
host: this.config.host
|
|
130
|
+
});
|
|
131
|
+
const startTime = Date.now();
|
|
132
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
133
|
+
if (await client.checkHealth()) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Stop the server
|
|
142
|
+
*/
|
|
143
|
+
async stopServer() {
|
|
144
|
+
const status = await this.getStatus();
|
|
145
|
+
if (!status.running || !status.pid) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
process.kill(status.pid, "SIGTERM");
|
|
150
|
+
await new Promise((resolve) => {
|
|
151
|
+
const checkInterval = setInterval(() => {
|
|
152
|
+
try {
|
|
153
|
+
process.kill(status.pid, 0);
|
|
154
|
+
} catch {
|
|
155
|
+
clearInterval(checkInterval);
|
|
156
|
+
resolve();
|
|
157
|
+
}
|
|
158
|
+
}, 100);
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
clearInterval(checkInterval);
|
|
161
|
+
try {
|
|
162
|
+
process.kill(status.pid, "SIGKILL");
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
resolve();
|
|
166
|
+
}, 5e3);
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logger.warn("Error stopping server", { error });
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
if (existsSync(PID_FILE)) {
|
|
173
|
+
const { unlinkSync } = await import("fs");
|
|
174
|
+
unlinkSync(PID_FILE);
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get server status
|
|
181
|
+
*/
|
|
182
|
+
async getStatus() {
|
|
183
|
+
if (!existsSync(PID_FILE)) {
|
|
184
|
+
return { running: false };
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const data = JSON.parse(readFileSync(PID_FILE, "utf-8"));
|
|
188
|
+
const { pid, port, host, modelPath, startedAt } = data;
|
|
189
|
+
try {
|
|
190
|
+
process.kill(pid, 0);
|
|
191
|
+
} catch {
|
|
192
|
+
return { running: false };
|
|
193
|
+
}
|
|
194
|
+
const client = createPredictionClient({ port, host });
|
|
195
|
+
const healthy = await client.checkHealth();
|
|
196
|
+
return {
|
|
197
|
+
running: healthy,
|
|
198
|
+
pid,
|
|
199
|
+
port,
|
|
200
|
+
host,
|
|
201
|
+
modelPath,
|
|
202
|
+
startedAt
|
|
203
|
+
};
|
|
204
|
+
} catch {
|
|
205
|
+
return { running: false };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Check server health
|
|
210
|
+
*/
|
|
211
|
+
async checkHealth() {
|
|
212
|
+
const client = createPredictionClient({
|
|
213
|
+
port: this.config.port,
|
|
214
|
+
host: this.config.host
|
|
215
|
+
});
|
|
216
|
+
return client.checkHealth();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function createServerManager(config) {
|
|
220
|
+
return new SweepServerManager(config);
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
SweepServerManager,
|
|
224
|
+
createServerManager
|
|
225
|
+
};
|
|
226
|
+
//# sourceMappingURL=sweep-server-manager.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/features/sweep/sweep-server-manager.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Sweep Server Manager\n *\n * Manages the llama-server process for Sweep predictions.\n */\n\nimport { spawn, ChildProcess, execSync } from 'child_process';\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join, dirname } from 'path';\nimport {\n SweepServerConfig,\n SweepServerStatus,\n DEFAULT_SERVER_CONFIG,\n} from './types.js';\nimport { createPredictionClient } from './prediction-client.js';\nimport { logger } from '../../core/monitoring/logger.js';\n\nconst HOME = process.env['HOME'] || '/tmp';\nconst PID_FILE = join(HOME, '.stackmemory', 'sweep', 'server.pid');\nconst LOG_FILE = join(HOME, '.stackmemory', 'sweep', 'server.log');\n\nexport class SweepServerManager {\n private config: SweepServerConfig;\n private process: ChildProcess | null = null;\n\n constructor(config: Partial<SweepServerConfig> = {}) {\n this.config = { ...DEFAULT_SERVER_CONFIG, ...config };\n\n // Set default model path if not provided\n if (!this.config.modelPath) {\n this.config.modelPath = join(\n HOME,\n '.stackmemory',\n 'models',\n 'sweep',\n 'sweep-next-edit-1.5b.q8_0.v2.gguf'\n );\n }\n }\n\n /**\n * Find llama-server executable\n */\n private findLlamaServer(): string | null {\n const candidates = [\n 'llama-server',\n 'llama.cpp/llama-server',\n '/usr/local/bin/llama-server',\n '/opt/homebrew/bin/llama-server',\n join(HOME, '.local', 'bin', 'llama-server'),\n ];\n\n for (const cmd of candidates) {\n try {\n execSync(`which ${cmd}`, { stdio: 'ignore' });\n return cmd;\n } catch {\n if (existsSync(cmd)) {\n return cmd;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Start the llama-server\n */\n async startServer(): Promise<SweepServerStatus> {\n // Check if already running\n const status = await this.getStatus();\n if (status.running) {\n return status;\n }\n\n // Check model exists\n if (!existsSync(this.config.modelPath)) {\n throw new Error(\n `Model not found: ${this.config.modelPath}\\n` +\n 'Download with: huggingface-cli download sweepai/sweep-next-edit-1.5B sweep-next-edit-1.5b.q8_0.v2.gguf --local-dir ~/.stackmemory/models/sweep'\n );\n }\n\n // Find llama-server\n const llamaServer = this.findLlamaServer();\n if (!llamaServer) {\n throw new Error(\n 'llama-server not found. Install with:\\n' +\n ' brew install llama.cpp\\n' +\n 'or build from source: https://github.com/ggerganov/llama.cpp'\n );\n }\n\n // Ensure log directory exists\n const logDir = dirname(LOG_FILE);\n if (!existsSync(logDir)) {\n mkdirSync(logDir, { recursive: true });\n }\n\n // Build command args\n const args = [\n '-m',\n this.config.modelPath,\n '--port',\n String(this.config.port),\n '--host',\n this.config.host,\n '-c',\n String(this.config.contextSize),\n ];\n\n if (this.config.threads) {\n args.push('-t', String(this.config.threads));\n }\n\n if (this.config.gpuLayers && this.config.gpuLayers > 0) {\n args.push('-ngl', String(this.config.gpuLayers));\n }\n\n logger.info('Starting Sweep server', { llamaServer, args });\n\n // Start the process\n this.process = spawn(llamaServer, args, {\n detached: true,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n // Write PID file\n if (this.process.pid) {\n const pidDir = dirname(PID_FILE);\n if (!existsSync(pidDir)) {\n mkdirSync(pidDir, { recursive: true });\n }\n writeFileSync(\n PID_FILE,\n JSON.stringify({\n pid: this.process.pid,\n port: this.config.port,\n host: this.config.host,\n modelPath: this.config.modelPath,\n startedAt: Date.now(),\n })\n );\n }\n\n // Unref to allow parent to exit\n this.process.unref();\n\n // Wait for server to be ready\n const ready = await this.waitForReady(10000);\n if (!ready) {\n await this.stopServer();\n throw new Error('Server failed to start within timeout');\n }\n\n return this.getStatus();\n }\n\n /**\n * Wait for server to be ready\n */\n private async waitForReady(timeoutMs: number): Promise<boolean> {\n const client = createPredictionClient({\n port: this.config.port,\n host: this.config.host,\n });\n\n const startTime = Date.now();\n while (Date.now() - startTime < timeoutMs) {\n if (await client.checkHealth()) {\n return true;\n }\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n return false;\n }\n\n /**\n * Stop the server\n */\n async stopServer(): Promise<void> {\n const status = await this.getStatus();\n\n if (!status.running || !status.pid) {\n return;\n }\n\n try {\n process.kill(status.pid, 'SIGTERM');\n\n // Wait for process to exit\n await new Promise<void>((resolve) => {\n const checkInterval = setInterval(() => {\n try {\n process.kill(status.pid!, 0); // Check if still running\n } catch {\n clearInterval(checkInterval);\n resolve();\n }\n }, 100);\n\n // Force kill after 5 seconds\n setTimeout(() => {\n clearInterval(checkInterval);\n try {\n process.kill(status.pid!, 'SIGKILL');\n } catch {\n // Already dead\n }\n resolve();\n }, 5000);\n });\n } catch (error) {\n // Process may already be dead\n logger.warn('Error stopping server', { error });\n }\n\n // Clean up PID file\n try {\n if (existsSync(PID_FILE)) {\n const { unlinkSync } = await import('fs');\n unlinkSync(PID_FILE);\n }\n } catch {\n // Ignore\n }\n }\n\n /**\n * Get server status\n */\n async getStatus(): Promise<SweepServerStatus> {\n // Check PID file\n if (!existsSync(PID_FILE)) {\n return { running: false };\n }\n\n try {\n const data = JSON.parse(readFileSync(PID_FILE, 'utf-8'));\n const { pid, port, host, modelPath, startedAt } = data;\n\n // Check if process is still running\n try {\n process.kill(pid, 0);\n } catch {\n // Process not running\n return { running: false };\n }\n\n // Verify server is responsive\n const client = createPredictionClient({ port, host });\n const healthy = await client.checkHealth();\n\n return {\n running: healthy,\n pid,\n port,\n host,\n modelPath,\n startedAt,\n };\n } catch {\n return { running: false };\n }\n }\n\n /**\n * Check server health\n */\n async checkHealth(): Promise<boolean> {\n const client = createPredictionClient({\n port: this.config.port,\n host: this.config.host,\n });\n return client.checkHealth();\n }\n}\n\n/**\n * Create a server manager with default config\n */\nexport function createServerManager(\n config?: Partial<SweepServerConfig>\n): SweepServerManager {\n return new SweepServerManager(config);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|