specmem-hardwicksoftware 3.7.35 → 3.7.38
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/CHANGELOG.md +34 -0
- package/README.md +11 -15
- package/bin/specmem-autoclaude.cjs +12 -1
- package/bin/specmem-cli.cjs +1077 -11
- package/bin/specmem-console.cjs +890 -63
- package/bootstrap.cjs +10 -2
- package/claude-hooks/agent-loading-hook.cjs +16 -16
- package/claude-hooks/agent-loading-hook.js +28 -21
- package/claude-hooks/agent-type-matcher.js +1 -1
- package/claude-hooks/background-completion-silencer.js +1 -1
- package/claude-hooks/file-claim-enforcer.cjs +37 -36
- package/claude-hooks/output-cleaner.cjs +1 -1
- package/claude-hooks/refusal-detector-hook.cjs +53 -0
- package/claude-hooks/settings.json +64 -4
- package/claude-hooks/smart-search-interceptor.js +1 -1
- package/claude-hooks/specmem-search-enforcer.cjs +2 -11
- package/claude-hooks/specmem-team-member-inject.js +1 -1
- package/claude-hooks/specmem-unified-hook.py +1 -1
- package/claude-hooks/subagent-loading-hook.cjs +1 -1
- package/claude-hooks/task-progress-hook.cjs +7 -7
- package/claude-hooks/task-progress-hook.js +3 -3
- package/claude-hooks/team-comms-enforcer.cjs +113 -47
- package/claude-hooks/use-code-pointers.cjs +1 -1
- package/dist/claude-sessions/sessionParser.js +5 -0
- package/dist/cli/deploy-to-claude.js +9 -2
- package/dist/codebase/codebaseIndexer.js +48 -17
- package/dist/codebase/exclusions.js +3 -4
- package/dist/codebase/index.js +4 -0
- package/dist/codebase/pdfExtractor.js +298 -0
- package/dist/dashboard/api/taskTeamMembers.js +2 -2
- package/dist/db/bigBrainMigrations.js +29 -0
- package/dist/hooks/hookManager.js +4 -4
- package/dist/hooks/teamFramingCli.js +1 -1
- package/dist/hooks/teamMemberPrepromptHook.js +5 -5
- package/dist/index.js +49 -12
- package/dist/init/claudeConfigInjector.js +27 -8
- package/dist/installer/autoInstall.js +7 -1
- package/dist/mcp/compactionProxy.js +1052 -192
- package/dist/mcp/compactionProxyDaemon.js +112 -37
- package/dist/mcp/contextVault.js +439 -0
- package/dist/mcp/embeddingServerManager.js +151 -17
- package/dist/mcp/mcpProtocolHandler.js +6 -1
- package/dist/mcp/miniCOTServerManager.js +82 -8
- package/dist/mcp/specMemServer.js +45 -10
- package/dist/mcp/toolRegistry.js +6 -0
- package/dist/startup/startupIndexing.js +14 -0
- package/dist/team-members/taskOrchestrator.js +3 -3
- package/dist/team-members/taskTeamMemberLogger.js +2 -2
- package/dist/tools/goofy/deployTeamMember.js +3 -3
- package/dist/tools/goofy/digInTheVault.js +81 -0
- package/dist/tools/goofy/findCodePointers.js +17 -0
- package/dist/tools/goofy/findWhatISaid.js +19 -0
- package/dist/tools/goofy/stashTheGoods.js +56 -0
- package/dist/tools/teamMemberDeployer.js +2 -2
- package/dist/watcher/changeHandler.js +65 -8
- package/dist/watcher/changeQueue.js +20 -1
- package/embedding-sandbox/frankenstein-embeddings.py +4 -3
- package/embedding-sandbox/mini-cot-service.py +11 -13
- package/embedding-sandbox/pdf-text-extract.py +208 -0
- package/package.json +1 -1
- package/scripts/deploy-hooks.cjs +12 -4
- package/scripts/fast-batch-embedder.cjs +2 -2
- package/scripts/force-retry.cjs +34 -0
- package/scripts/global-postinstall.cjs +97 -4
- package/scripts/poetic-abliteration.cjs +379 -0
- package/scripts/refusal-enforcer.cjs +88 -0
- package/scripts/specmem-init.cjs +222 -41
- package/specmem/model-config.json +6 -6
- package/specmem/supervisord.conf +1 -1
- package/svg-sections/readme-token-compaction.svg +246 -0
- package/claude-hooks/agent-chooser-hook.js +0 -179
package/bin/specmem-cli.cjs
CHANGED
|
@@ -44,6 +44,20 @@ const SPECMEM_PKG = path.dirname(__dirname);
|
|
|
44
44
|
const MODELS_DIR = path.join(SPECMEM_PKG, 'models');
|
|
45
45
|
const OPTIMIZED_DIR = path.join(MODELS_DIR, 'optimized');
|
|
46
46
|
|
|
47
|
+
// Helper: Get compaction proxy port if running
|
|
48
|
+
function getProxyEnv() {
|
|
49
|
+
try {
|
|
50
|
+
const portFile = path.join(os.homedir(), '.claude', '.compaction-proxy-port');
|
|
51
|
+
if (fs.existsSync(portFile)) {
|
|
52
|
+
const port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10);
|
|
53
|
+
if (port) {
|
|
54
|
+
return { ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}` };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {}
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
const args = process.argv.slice(2);
|
|
48
62
|
const command = args[0] || 'help';
|
|
49
63
|
const subArgs = args.slice(1);
|
|
@@ -237,6 +251,20 @@ switch (command) {
|
|
|
237
251
|
case 'proxy':
|
|
238
252
|
manageProxy(subArgs);
|
|
239
253
|
break;
|
|
254
|
+
case 'embed':
|
|
255
|
+
manageEmbed(subArgs);
|
|
256
|
+
break;
|
|
257
|
+
case 'config':
|
|
258
|
+
manageConfig(subArgs);
|
|
259
|
+
break;
|
|
260
|
+
case 'minicot':
|
|
261
|
+
case 'minicott':
|
|
262
|
+
manageMinicot(subArgs);
|
|
263
|
+
break;
|
|
264
|
+
case 'quickstart':
|
|
265
|
+
// Minimal init without database setup
|
|
266
|
+
runQuickstart();
|
|
267
|
+
break;
|
|
240
268
|
case '--version':
|
|
241
269
|
case '-v':
|
|
242
270
|
case 'version':
|
|
@@ -570,11 +598,11 @@ ${CYAN}What's configured:${RESET}
|
|
|
570
598
|
} catch {
|
|
571
599
|
console.log(`${YELLOW}⚠${RESET} Could not install screen, launching directly...`);
|
|
572
600
|
console.log(`${DIM}Run: claude${RESET}\n`);
|
|
573
|
-
// Launch directly without screen
|
|
601
|
+
// Launch directly without screen (add proxy env)
|
|
574
602
|
const child = spawn('claude', [], {
|
|
575
603
|
cwd: projectDir,
|
|
576
604
|
stdio: 'inherit',
|
|
577
|
-
env: { ...process.env, SPECMEM_PROJECT_PATH: projectDir }
|
|
605
|
+
env: { ...process.env, SPECMEM_PROJECT_PATH: projectDir, ...getProxyEnv() }
|
|
578
606
|
});
|
|
579
607
|
return;
|
|
580
608
|
}
|
|
@@ -601,10 +629,12 @@ ${CYAN}What's configured:${RESET}
|
|
|
601
629
|
console.log(`${DIM}───────────────────────────────────────────────${RESET}`);
|
|
602
630
|
|
|
603
631
|
// Use -S to create named session, no -d so it attaches immediately
|
|
632
|
+
// Include proxy env to route through compaction proxy
|
|
633
|
+
const proxyEnv = getProxyEnv();
|
|
604
634
|
const result = spawnSync('screen', ['-S', screenName, 'claude'], {
|
|
605
635
|
cwd: projectDir,
|
|
606
636
|
stdio: 'inherit',
|
|
607
|
-
env: { ...process.env, SPECMEM_PROJECT_PATH: projectDir }
|
|
637
|
+
env: { ...process.env, SPECMEM_PROJECT_PATH: projectDir, ...proxyEnv }
|
|
608
638
|
});
|
|
609
639
|
|
|
610
640
|
// User has detached or exited
|
|
@@ -909,24 +939,1060 @@ ${BOLD}Aliases:${RESET}
|
|
|
909
939
|
}
|
|
910
940
|
}
|
|
911
941
|
|
|
942
|
+
// ============================================================================
|
|
943
|
+
// EMBED COMMAND - Embedding Server Management
|
|
944
|
+
// ============================================================================
|
|
945
|
+
function manageEmbed(args) {
|
|
946
|
+
const sub = (args[0] || 'status').toLowerCase();
|
|
947
|
+
const projectPath = process.cwd();
|
|
948
|
+
|
|
949
|
+
// Get project-specific paths
|
|
950
|
+
const specmemDir = path.join(projectPath, 'specmem');
|
|
951
|
+
const socketDir = path.join(specmemDir, 'run');
|
|
952
|
+
const socketPath = path.join(socketDir, 'embed.sock');
|
|
953
|
+
const pidFilePath = path.join(socketDir, 'embedding.pid');
|
|
954
|
+
|
|
955
|
+
switch (sub) {
|
|
956
|
+
case 'start': {
|
|
957
|
+
// Ensure directories exist
|
|
958
|
+
if (!fs.existsSync(socketDir)) {
|
|
959
|
+
fs.mkdirSync(socketDir, { recursive: true });
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Check if already running
|
|
963
|
+
if (fs.existsSync(pidFilePath)) {
|
|
964
|
+
const pidData = fs.readFileSync(pidFilePath, 'utf8').trim();
|
|
965
|
+
const [pid] = pidData.split(':');
|
|
966
|
+
try {
|
|
967
|
+
process.kill(pid, 0); // Check if process exists
|
|
968
|
+
console.log(`${YELLOW}Embedding server already running (PID: ${pid})${RESET}`);
|
|
969
|
+
process.exit(0);
|
|
970
|
+
} catch (e) {
|
|
971
|
+
// PID file stale, remove it
|
|
972
|
+
fs.unlinkSync(pidFilePath);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Start the embedding server
|
|
977
|
+
console.log(`${CYAN}Starting embedding server...${RESET}`);
|
|
978
|
+
const embedScript = path.join(SPECMEM_PKG, 'embedding-sandbox', 'frankenstein-embeddings.py');
|
|
979
|
+
|
|
980
|
+
if (!fs.existsSync(embedScript)) {
|
|
981
|
+
console.log(`${RED}Embedding script not found: ${embedScript}${RESET}`);
|
|
982
|
+
process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const env = {
|
|
986
|
+
...process.env,
|
|
987
|
+
SPECMEM_PROJECT_PATH: projectPath,
|
|
988
|
+
SPECMEM_EMBEDDING_SOCKET: socketPath,
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
const child = spawn('python3', [embedScript], {
|
|
992
|
+
cwd: projectPath,
|
|
993
|
+
env,
|
|
994
|
+
detached: true,
|
|
995
|
+
stdio: 'ignore',
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
child.unref();
|
|
999
|
+
|
|
1000
|
+
// Write PID file
|
|
1001
|
+
fs.writeFileSync(pidFilePath, `${child.pid}:${Date.now()}`, { mode: 0o644 });
|
|
1002
|
+
|
|
1003
|
+
console.log(`${GREEN}Embedding server started (PID: ${child.pid})${RESET}`);
|
|
1004
|
+
console.log(`${DIM}Socket: ${socketPath}${RESET}`);
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
case 'stop': {
|
|
1009
|
+
if (!fs.existsSync(pidFilePath)) {
|
|
1010
|
+
console.log(`${YELLOW}No PID file found - server may not be running${RESET}`);
|
|
1011
|
+
process.exit(1);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const pidData = fs.readFileSync(pidFilePath, 'utf8').trim();
|
|
1015
|
+
const [pid, timestamp] = pidData.split(':');
|
|
1016
|
+
|
|
1017
|
+
try {
|
|
1018
|
+
process.kill(parseInt(pid), 'SIGTERM');
|
|
1019
|
+
console.log(`${GREEN}Sent SIGTERM to embedding server (PID: ${pid})${RESET}`);
|
|
1020
|
+
|
|
1021
|
+
// Wait a bit then force kill if still alive
|
|
1022
|
+
setTimeout(() => {
|
|
1023
|
+
try {
|
|
1024
|
+
process.kill(parseInt(pid), 0);
|
|
1025
|
+
process.kill(parseInt(pid), 'SIGKILL');
|
|
1026
|
+
console.log(`${YELLOW}Force killed server${RESET}`);
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
// Process already dead
|
|
1029
|
+
}
|
|
1030
|
+
}, 3000);
|
|
1031
|
+
|
|
1032
|
+
// Remove PID file
|
|
1033
|
+
fs.unlinkSync(pidFilePath);
|
|
1034
|
+
console.log(`${GREEN}Embedding server stopped${RESET}`);
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
console.log(`${RED}Failed to stop server: ${e.message}${RESET}`);
|
|
1037
|
+
fs.unlinkSync(pidFilePath);
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
case 'restart': {
|
|
1044
|
+
// First try to stop
|
|
1045
|
+
if (fs.existsSync(pidFilePath)) {
|
|
1046
|
+
const pidData = fs.readFileSync(pidFilePath, 'utf8').trim();
|
|
1047
|
+
const [pid] = pidData.split(':');
|
|
1048
|
+
try {
|
|
1049
|
+
process.kill(parseInt(pid), 'SIGTERM');
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
// Already dead
|
|
1052
|
+
}
|
|
1053
|
+
fs.unlinkSync(pidFilePath);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Small delay then start
|
|
1057
|
+
setTimeout(() => {
|
|
1058
|
+
// Recursive call to start
|
|
1059
|
+
manageEmbed(['start']);
|
|
1060
|
+
}, 1000);
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
case 'force-restart': {
|
|
1065
|
+
// Force kill regardless of state
|
|
1066
|
+
if (fs.existsSync(pidFilePath)) {
|
|
1067
|
+
const pidData = fs.readFileSync(pidFilePath, 'utf8').trim();
|
|
1068
|
+
const [pid] = pidData.split(':');
|
|
1069
|
+
try {
|
|
1070
|
+
process.kill(parseInt(pid), 'SIGKILL');
|
|
1071
|
+
} catch (e) {
|
|
1072
|
+
// Already dead
|
|
1073
|
+
}
|
|
1074
|
+
fs.unlinkSync(pidFilePath);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Also try to kill any process using the socket
|
|
1078
|
+
try {
|
|
1079
|
+
execSync(`lsof -t ${socketPath} 2>/dev/null | xargs -r kill -9 2>/dev/null || true`, { stdio: 'pipe' });
|
|
1080
|
+
} catch (e) {}
|
|
1081
|
+
|
|
1082
|
+
// Start fresh
|
|
1083
|
+
setTimeout(() => {
|
|
1084
|
+
manageEmbed(['start']);
|
|
1085
|
+
}, 500);
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
case 'status': {
|
|
1090
|
+
console.log(`${CYAN}═══ Embedding Server Status ═══${RESET}\n`);
|
|
1091
|
+
|
|
1092
|
+
if (!fs.existsSync(socketDir)) {
|
|
1093
|
+
console.log(`${YELLOW}Project not initialized - no specmem directory${RESET}`);
|
|
1094
|
+
console.log(`${DIM}Run 'specmem init' or 'specmem quickstart' first${RESET}`);
|
|
1095
|
+
process.exit(1);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Check PID file
|
|
1099
|
+
if (fs.existsSync(pidFilePath)) {
|
|
1100
|
+
const pidData = fs.readFileSync(pidFilePath, 'utf8').trim();
|
|
1101
|
+
const [pid, timestamp] = pidData.split(':');
|
|
1102
|
+
console.log(` PID File: ${GREEN}exists${RESET} (PID: ${pid})`);
|
|
1103
|
+
console.log(` Started: ${new Date(parseInt(timestamp)).toLocaleString()}`);
|
|
1104
|
+
|
|
1105
|
+
// Check if process is alive
|
|
1106
|
+
try {
|
|
1107
|
+
process.kill(parseInt(pid), 0);
|
|
1108
|
+
console.log(` Process: ${GREEN}running${RESET}`);
|
|
1109
|
+
} catch (e) {
|
|
1110
|
+
console.log(` Process: ${RED}dead (stale PID file)${RESET}`);
|
|
1111
|
+
}
|
|
1112
|
+
} else {
|
|
1113
|
+
console.log(` PID File: ${YELLOW}not found${RESET}`);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Check socket
|
|
1117
|
+
console.log(`\n Socket: ${socketPath}`);
|
|
1118
|
+
if (fs.existsSync(socketPath)) {
|
|
1119
|
+
console.log(` ${GREEN}exists${RESET}`);
|
|
1120
|
+
} else {
|
|
1121
|
+
console.log(` ${YELLOW}not found${RESET}`);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Check for stopped flag
|
|
1125
|
+
const stoppedFlag = path.join(socketDir, 'embedding.stopped');
|
|
1126
|
+
if (fs.existsSync(stoppedFlag)) {
|
|
1127
|
+
console.log(`\n ${YELLOW}Server was manually stopped by user${RESET}`);
|
|
1128
|
+
console.log(` ${DIM}Use 'specmem embed start' to restart${RESET}`);
|
|
1129
|
+
}
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
case 'list': {
|
|
1134
|
+
console.log(`${CYAN}═══ All Embedding Servers ═══${RESET}\n`);
|
|
1135
|
+
|
|
1136
|
+
// Find all PID files in common locations
|
|
1137
|
+
const searchPaths = [
|
|
1138
|
+
path.join(os.homedir(), '.specmem'),
|
|
1139
|
+
path.join(projectPath, 'specmem'),
|
|
1140
|
+
path.join(projectPath, '.specmem'),
|
|
1141
|
+
];
|
|
1142
|
+
|
|
1143
|
+
let found = false;
|
|
1144
|
+
for (const sp of searchPaths) {
|
|
1145
|
+
if (!fs.existsSync(sp)) continue;
|
|
1146
|
+
|
|
1147
|
+
try {
|
|
1148
|
+
const pidFiles = execSync(`find ${sp} -name "embedding.pid" 2>/dev/null`, { encoding: 'utf8' })
|
|
1149
|
+
.trim()
|
|
1150
|
+
.split('\n')
|
|
1151
|
+
.filter(Boolean);
|
|
1152
|
+
|
|
1153
|
+
for (const pf of pidFiles) {
|
|
1154
|
+
const dir = path.dirname(pf);
|
|
1155
|
+
const socketPath = path.join(dir, 'embed.sock');
|
|
1156
|
+
|
|
1157
|
+
if (fs.existsSync(pf)) {
|
|
1158
|
+
const pidData = fs.readFileSync(pf, 'utf8').trim();
|
|
1159
|
+
const [pid, timestamp] = pidData.split(':');
|
|
1160
|
+
const projName = path.basename(path.dirname(path.dirname(dir)));
|
|
1161
|
+
|
|
1162
|
+
let status = `${RED}dead${RESET}`;
|
|
1163
|
+
try {
|
|
1164
|
+
process.kill(parseInt(pid), 0);
|
|
1165
|
+
status = `${GREEN}running${RESET}`;
|
|
1166
|
+
} catch (e) {}
|
|
1167
|
+
|
|
1168
|
+
console.log(` Project: ${projName}`);
|
|
1169
|
+
console.log(` PID: ${pid} (${status})`);
|
|
1170
|
+
console.log(` Socket: ${socketPath}`);
|
|
1171
|
+
console.log(` Started: ${new Date(parseInt(timestamp)).toLocaleString()}`);
|
|
1172
|
+
console.log('');
|
|
1173
|
+
found = true;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
} catch (e) {}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (!found) {
|
|
1180
|
+
console.log(` ${DIM}No embedding servers found${RESET}`);
|
|
1181
|
+
}
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
case 'diagnose': {
|
|
1186
|
+
console.log(`${CYAN}═══ Embedding Server Diagnostics ═══${RESET}\n`);
|
|
1187
|
+
|
|
1188
|
+
console.log(`Project: ${projectPath}`);
|
|
1189
|
+
console.log(`SpecMem Dir: ${specmemDir}`);
|
|
1190
|
+
console.log(`Socket Dir: ${socketDir}`);
|
|
1191
|
+
console.log(`Socket Path: ${socketPath}`);
|
|
1192
|
+
console.log(`PID File: ${pidFilePath}`);
|
|
1193
|
+
|
|
1194
|
+
// Check if directories exist
|
|
1195
|
+
console.log(`\n${CYAN}Directory Status:${RESET}`);
|
|
1196
|
+
console.log(` specmem/ exists: ${fs.existsSync(specmemDir) ? GREEN + 'yes' + RESET : YELLOW + 'no' + RESET}`);
|
|
1197
|
+
console.log(` run/ exists: ${fs.existsSync(socketDir) ? GREEN + 'yes' + RESET : YELLOW + 'no' + RESET}`);
|
|
1198
|
+
console.log(` socket exists: ${fs.existsSync(socketPath) ? GREEN + 'yes' + RESET : YELLOW + 'no' + RESET}`);
|
|
1199
|
+
console.log(` PID file exists: ${fs.existsSync(pidFilePath) ? GREEN + 'yes' + RESET : YELLOW + 'no' + RESET}`);
|
|
1200
|
+
|
|
1201
|
+
// Check process
|
|
1202
|
+
if (fs.existsSync(pidFilePath)) {
|
|
1203
|
+
const pidData = fs.readFileSync(pidFilePath, 'utf8').trim();
|
|
1204
|
+
const [pid] = pidData.split(':');
|
|
1205
|
+
|
|
1206
|
+
console.log(`\n${CYAN}Process Info:${RESET}`);
|
|
1207
|
+
console.log(` PID: ${pid}`);
|
|
1208
|
+
|
|
1209
|
+
try {
|
|
1210
|
+
const procPath = `/proc/${pid}`;
|
|
1211
|
+
if (fs.existsSync(procPath)) {
|
|
1212
|
+
const cmdline = fs.readFileSync(path.join(procPath, 'cmdline'), 'utf8').replace(/\0/g, ' ');
|
|
1213
|
+
console.log(` Command: ${cmdline.slice(0, 100)}`);
|
|
1214
|
+
|
|
1215
|
+
// Check env
|
|
1216
|
+
try {
|
|
1217
|
+
const envPath = path.join(procPath, 'environ');
|
|
1218
|
+
const envData = fs.readFileSync(envPath);
|
|
1219
|
+
const envStr = envData.toString('utf8');
|
|
1220
|
+
const specmemEnv = envStr.split('\0').find(e => e.startsWith('SPECMEM'));
|
|
1221
|
+
if (specmemEnv) {
|
|
1222
|
+
console.log(` Env: ${specmemEnv.slice(0, 80)}`);
|
|
1223
|
+
}
|
|
1224
|
+
} catch (e) {}
|
|
1225
|
+
} else {
|
|
1226
|
+
console.log(` ${RED}Process does not exist (stale PID)${RESET}`);
|
|
1227
|
+
}
|
|
1228
|
+
} catch (e) {
|
|
1229
|
+
console.log(` ${RED}Error reading process: ${e.message}${RESET}`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Check socket ownership
|
|
1234
|
+
if (fs.existsSync(socketPath)) {
|
|
1235
|
+
console.log(`\n${CYAN}Socket Status:${RESET}`);
|
|
1236
|
+
try {
|
|
1237
|
+
const stats = fs.statSync(socketPath);
|
|
1238
|
+
console.log(` Created: ${stats.birthtime}`);
|
|
1239
|
+
console.log(` Size: ${stats.size} bytes`);
|
|
1240
|
+
} catch (e) {
|
|
1241
|
+
console.log(` Error: ${e.message}`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
case 'cleanup': {
|
|
1248
|
+
console.log(`${CYAN}Cleaning up stale embedding server files...${RESET}`);
|
|
1249
|
+
|
|
1250
|
+
let cleaned = 0;
|
|
1251
|
+
|
|
1252
|
+
// Clean stale PID files
|
|
1253
|
+
const searchPaths = [
|
|
1254
|
+
path.join(os.homedir(), '.specmem'),
|
|
1255
|
+
path.join(projectPath, 'specmem'),
|
|
1256
|
+
path.join(projectPath, '.specmem'),
|
|
1257
|
+
];
|
|
1258
|
+
|
|
1259
|
+
for (const sp of searchPaths) {
|
|
1260
|
+
if (!fs.existsSync(sp)) continue;
|
|
1261
|
+
|
|
1262
|
+
try {
|
|
1263
|
+
const pidFiles = execSync(`find ${sp} -name "embedding.pid" 2>/dev/null`, { encoding: 'utf8' })
|
|
1264
|
+
.trim()
|
|
1265
|
+
.split('\n')
|
|
1266
|
+
.filter(Boolean);
|
|
1267
|
+
|
|
1268
|
+
for (const pf of pidFiles) {
|
|
1269
|
+
try {
|
|
1270
|
+
const pidData = fs.readFileSync(pf, 'utf8').trim();
|
|
1271
|
+
const [pid] = pidData.split(':');
|
|
1272
|
+
try {
|
|
1273
|
+
process.kill(parseInt(pid), 0);
|
|
1274
|
+
// Process still alive, skip
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
// Process dead, remove PID file
|
|
1277
|
+
fs.unlinkSync(pf);
|
|
1278
|
+
console.log(` Removed stale PID file: ${pf}`);
|
|
1279
|
+
cleaned++;
|
|
1280
|
+
}
|
|
1281
|
+
} catch (e) {}
|
|
1282
|
+
}
|
|
1283
|
+
} catch (e) {}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
console.log(`${GREEN}Cleaned ${cleaned} stale PID files${RESET}`);
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
default:
|
|
1291
|
+
console.log(`${BOLD}Usage:${RESET} specmem embed <command>
|
|
1292
|
+
|
|
1293
|
+
${BOLD}Commands:${RESET}
|
|
1294
|
+
${CYAN}start${RESET} Start embedding server
|
|
1295
|
+
${CYAN}stop${RESET} Stop embedding server
|
|
1296
|
+
${CYAN}restart${RESET} Restart embedding server
|
|
1297
|
+
${CYAN}force-restart${RESET} Force restart (kill and start fresh)
|
|
1298
|
+
${CYAN}status${RESET} Show embedding server status
|
|
1299
|
+
${CYAN}list${RESET} List all embedding servers
|
|
1300
|
+
${CYAN}diagnose${RESET} Full diagnostic info
|
|
1301
|
+
${CYAN}cleanup${RESET} Clean up stale PID files
|
|
1302
|
+
`);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// ============================================================================
|
|
1307
|
+
// CONFIG COMMAND - Configuration Management
|
|
1308
|
+
// ============================================================================
|
|
1309
|
+
function manageConfig(args) {
|
|
1310
|
+
const sub = (args[0] || 'list').toLowerCase();
|
|
1311
|
+
const key = args[1];
|
|
1312
|
+
const value = args[2];
|
|
1313
|
+
|
|
1314
|
+
const configDir = path.join(os.homedir(), '.specmem');
|
|
1315
|
+
const configFile = path.join(configDir, 'config.json');
|
|
1316
|
+
|
|
1317
|
+
// Default config values
|
|
1318
|
+
const defaults = {
|
|
1319
|
+
SPECMEM_SEARCH_LIMIT: '5',
|
|
1320
|
+
SPECMEM_THRESHOLD: '0.30',
|
|
1321
|
+
SPECMEM_MAX_CONTENT: '200',
|
|
1322
|
+
SPECMEM_DB_HOST: 'localhost',
|
|
1323
|
+
SPECMEM_DB_PORT: '5432',
|
|
1324
|
+
SPECMEM_EMBEDDING_MODEL: 'BAAI/bge-small-en-v1.5',
|
|
1325
|
+
SPECMEM_EMBEDDING_BATCH_SIZE: '100',
|
|
1326
|
+
SPECMEM_AUTO_CONSOLIDATE: 'true',
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
// Load existing config
|
|
1330
|
+
let config = {};
|
|
1331
|
+
if (fs.existsSync(configFile)) {
|
|
1332
|
+
try {
|
|
1333
|
+
config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
1334
|
+
} catch (e) {}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// Helper to save config
|
|
1338
|
+
const saveConfig = () => {
|
|
1339
|
+
if (!fs.existsSync(configDir)) {
|
|
1340
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1341
|
+
}
|
|
1342
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), { mode: 0o644 });
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
// Helper to get value (config > env > default)
|
|
1346
|
+
const getValue = (k) => {
|
|
1347
|
+
if (config[k] !== undefined) return config[k];
|
|
1348
|
+
if (process.env[k] !== undefined) return process.env[k];
|
|
1349
|
+
return defaults[k];
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
switch (sub) {
|
|
1353
|
+
case 'list': {
|
|
1354
|
+
console.log(`${CYAN}═══ SpecMem Configuration ═══${RESET}\n`);
|
|
1355
|
+
|
|
1356
|
+
// Show source
|
|
1357
|
+
if (fs.existsSync(configFile)) {
|
|
1358
|
+
console.log(`${GREEN}Config file:${RESET} ${configFile}`);
|
|
1359
|
+
} else {
|
|
1360
|
+
console.log(`${YELLOW}Config file:${RESET} not found (using defaults)`);
|
|
1361
|
+
}
|
|
1362
|
+
console.log('');
|
|
1363
|
+
|
|
1364
|
+
// List all settings
|
|
1365
|
+
const allKeys = [...new Set([...Object.keys(defaults), ...Object.keys(config)])];
|
|
1366
|
+
for (const k of allKeys.sort()) {
|
|
1367
|
+
const val = getValue(k);
|
|
1368
|
+
const def = defaults[k];
|
|
1369
|
+
const isDefault = val === def;
|
|
1370
|
+
const isEnvOverride = process.env[k] !== undefined && config[k] === undefined;
|
|
1371
|
+
|
|
1372
|
+
let source = '';
|
|
1373
|
+
if (isEnvOverride) {
|
|
1374
|
+
source = ` ${DIM}(env override)${RESET}`;
|
|
1375
|
+
} else if (!isDefault) {
|
|
1376
|
+
source = ` ${CYAN}(custom)${RESET}`;
|
|
1377
|
+
} else {
|
|
1378
|
+
source = ` ${DIM}(default)${RESET}`;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
console.log(` ${k}: ${val}${source}`);
|
|
1382
|
+
}
|
|
1383
|
+
break;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
case 'get': {
|
|
1387
|
+
if (!key) {
|
|
1388
|
+
console.log(`${RED}Error: Please specify a key to get${RESET}`);
|
|
1389
|
+
console.log(`Usage: specmem config get <key>`);
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const val = getValue(key);
|
|
1394
|
+
console.log(`${key}=${val}`);
|
|
1395
|
+
break;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
case 'set': {
|
|
1399
|
+
if (!key || value === undefined) {
|
|
1400
|
+
console.log(`${RED}Error: Please specify key and value${RESET}`);
|
|
1401
|
+
console.log(`Usage: specmem config set <key> <value>`);
|
|
1402
|
+
process.exit(1);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
config[key] = value;
|
|
1406
|
+
saveConfig();
|
|
1407
|
+
console.log(`${GREEN}Set ${key}=${value}${RESET}`);
|
|
1408
|
+
console.log(`${DIM}This will override environment variable ${key}${RESET}`);
|
|
1409
|
+
console.log(`${DIM}To apply, restart your Claude session or set the env var manually${RESET}`);
|
|
1410
|
+
break;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
case 'unset': {
|
|
1414
|
+
if (!key) {
|
|
1415
|
+
console.log(`${RED}Error: Please specify a key to unset${RESET}`);
|
|
1416
|
+
console.log(`Usage: specmem config unset <key>`);
|
|
1417
|
+
process.exit(1);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (config[key] !== undefined) {
|
|
1421
|
+
delete config[key];
|
|
1422
|
+
saveConfig();
|
|
1423
|
+
console.log(`${GREEN}Unset ${key}${RESET}`);
|
|
1424
|
+
console.log(`${DIM}Now using default: ${defaults[key] || '(none)'}${RESET}`);
|
|
1425
|
+
} else {
|
|
1426
|
+
console.log(`${YELLOW}${key} was not set in config (using default)${RESET}`);
|
|
1427
|
+
}
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
case 'reset': {
|
|
1432
|
+
if (fs.existsSync(configFile)) {
|
|
1433
|
+
fs.unlinkSync(configFile);
|
|
1434
|
+
console.log(`${GREEN}Reset config to defaults${RESET}`);
|
|
1435
|
+
} else {
|
|
1436
|
+
console.log(`${YELLOW}No config file to reset${RESET}`);
|
|
1437
|
+
}
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
case 'edit': {
|
|
1442
|
+
// Open config file in editor
|
|
1443
|
+
if (!fs.existsSync(configDir)) {
|
|
1444
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1445
|
+
}
|
|
1446
|
+
if (!fs.existsSync(configFile)) {
|
|
1447
|
+
saveConfig();
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const editor = process.env.EDITOR || 'nano';
|
|
1451
|
+
try {
|
|
1452
|
+
execSync(`${editor} "${configFile}"`, { stdio: 'inherit' });
|
|
1453
|
+
console.log(`${GREEN}Config file saved${RESET}`);
|
|
1454
|
+
} catch (e) {
|
|
1455
|
+
console.log(`${RED}Failed to open editor: ${e.message}${RESET}`);
|
|
1456
|
+
console.log(`Edit manually: ${configFile}`);
|
|
1457
|
+
}
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
default:
|
|
1462
|
+
console.log(`${BOLD}Usage:${RESET} specmem config <command>
|
|
1463
|
+
|
|
1464
|
+
${BOLD}Commands:${RESET}
|
|
1465
|
+
${CYAN}list${RESET} List all settings with current values
|
|
1466
|
+
${CYAN}get <key>${RESET} Get value of specific setting
|
|
1467
|
+
${CYAN}set <key> <val>${RESET} Set value (persists to config file)
|
|
1468
|
+
${CYAN}unset <key>${RESET} Remove custom value (use default)
|
|
1469
|
+
${CYAN}reset${RESET} Reset all config to defaults
|
|
1470
|
+
${CYAN}edit${RESET} Open config file in editor
|
|
1471
|
+
|
|
1472
|
+
${BOLD}Configurable Settings:${RESET}
|
|
1473
|
+
SPECMEM_SEARCH_LIMIT Memory search limit (default: 5)
|
|
1474
|
+
SPECMEM_THRESHOLD Similarity threshold (default: 0.30)
|
|
1475
|
+
SPECMEM_MAX_CONTENT Max content length (default: 200)
|
|
1476
|
+
SPECMEM_DB_HOST Database host
|
|
1477
|
+
SPECMEM_DB_PORT Database port
|
|
1478
|
+
SPECMEM_EMBEDDING_MODEL Embedding model name
|
|
1479
|
+
SPECMEM_EMBEDDING_BATCH_SIZE Batch size (default: 100)
|
|
1480
|
+
SPECMEM_AUTO_CONSOLIDATE Auto consolidation toggle
|
|
1481
|
+
`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// ============================================================================
|
|
1486
|
+
// MINICOT COMMAND - Mini COT (Pythia) Server Management
|
|
1487
|
+
// ============================================================================
|
|
1488
|
+
function manageMinicot(args) {
|
|
1489
|
+
const sub = (args[0] || 'status').toLowerCase();
|
|
1490
|
+
const projectPath = process.cwd();
|
|
1491
|
+
|
|
1492
|
+
// Get project-specific paths - prefer sockets/, fallback to run/
|
|
1493
|
+
const specmemDir = path.join(projectPath, 'specmem');
|
|
1494
|
+
const socketDir = path.join(specmemDir, 'sockets');
|
|
1495
|
+
const runDir = path.join(specmemDir, 'run');
|
|
1496
|
+
const socketPath = path.join(socketDir, 'minicot.sock');
|
|
1497
|
+
const runSocketPath = path.join(runDir, 'minicot.sock');
|
|
1498
|
+
const pidFilePath = path.join(socketDir, 'minicot.pid');
|
|
1499
|
+
const runPidFilePath = path.join(runDir, 'minicot.pid');
|
|
1500
|
+
const stoppedFlag = path.join(socketDir, 'minicot.stopped');
|
|
1501
|
+
const runStoppedFlag = path.join(runDir, 'minicot.stopped');
|
|
1502
|
+
|
|
1503
|
+
// Helper to find the actual paths in use
|
|
1504
|
+
const getActivePaths = () => {
|
|
1505
|
+
if (fs.existsSync(pidFilePath) || fs.existsSync(socketPath)) {
|
|
1506
|
+
return { socketDir, socketPath, pidFilePath, stoppedFlag };
|
|
1507
|
+
}
|
|
1508
|
+
if (fs.existsSync(runPidFilePath) || fs.existsSync(runSocketPath)) {
|
|
1509
|
+
return { socketDir: runDir, socketPath: runSocketPath, pidFilePath: runPidFilePath, stoppedFlag: runStoppedFlag };
|
|
1510
|
+
}
|
|
1511
|
+
return { socketDir, socketPath, pidFilePath, stoppedFlag };
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
// Helper to get minicot script path
|
|
1515
|
+
const getMinicotScript = () => {
|
|
1516
|
+
const candidates = [
|
|
1517
|
+
path.join(projectPath, 'mini-cot-service.py'),
|
|
1518
|
+
path.join(SPECMEM_PKG, 'mini-cot-service.py'),
|
|
1519
|
+
path.join(projectPath, 'node_modules', 'specmem-hardwicksoftware', 'mini-cot-service.py'),
|
|
1520
|
+
path.join(dirname(process.execPath), 'lib', 'node_modules', 'specmem-hardwicksoftware', 'mini-cot-service.py'),
|
|
1521
|
+
];
|
|
1522
|
+
for (const c of candidates) {
|
|
1523
|
+
if (fs.existsSync(c)) return c;
|
|
1524
|
+
}
|
|
1525
|
+
return candidates[0]; // Return first candidate anyway for error message
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
switch (sub) {
|
|
1529
|
+
case 'start': {
|
|
1530
|
+
// Ensure directories exist - prefer sockets/, create run/ as fallback
|
|
1531
|
+
if (!fs.existsSync(socketDir)) {
|
|
1532
|
+
fs.mkdirSync(socketDir, { recursive: true });
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Check if already running
|
|
1536
|
+
const activePaths = getActivePaths();
|
|
1537
|
+
if (fs.existsSync(activePaths.pidFilePath)) {
|
|
1538
|
+
const pidData = fs.readFileSync(activePaths.pidFilePath, 'utf8').trim();
|
|
1539
|
+
const [pid] = pidData.split(':');
|
|
1540
|
+
try {
|
|
1541
|
+
process.kill(pid, 0);
|
|
1542
|
+
console.log(`${YELLOW}Mini COT server already running (PID: ${pid})${RESET}`);
|
|
1543
|
+
process.exit(0);
|
|
1544
|
+
} catch (e) {
|
|
1545
|
+
fs.unlinkSync(activePaths.pidFilePath);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Start the minicot server
|
|
1550
|
+
console.log(`${CYAN}Starting Mini COT server...${RESET}`);
|
|
1551
|
+
const minicotScript = getMinicotScript();
|
|
1552
|
+
|
|
1553
|
+
if (!fs.existsSync(minicotScript)) {
|
|
1554
|
+
console.log(`${RED}Mini COT script not found: ${minicotScript}${RESET}`);
|
|
1555
|
+
process.exit(1);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const env = {
|
|
1559
|
+
...process.env,
|
|
1560
|
+
SPECMEM_PROJECT_PATH: projectPath,
|
|
1561
|
+
SPECMEM_MINICOT_SOCKET: socketPath,
|
|
1562
|
+
SPECMEM_MINICOT_MODEL: process.env.SPECMEM_MINICOT_MODEL || 'EleutherAI/pythia-410m',
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
const child = spawn('python3', [minicotScript, '--socket', socketPath], {
|
|
1566
|
+
cwd: projectPath,
|
|
1567
|
+
env,
|
|
1568
|
+
detached: true,
|
|
1569
|
+
stdio: 'ignore',
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
child.unref();
|
|
1573
|
+
|
|
1574
|
+
// Write PID file
|
|
1575
|
+
fs.writeFileSync(pidFilePath, `${child.pid}:${Date.now()}`, { mode: 0o644 });
|
|
1576
|
+
|
|
1577
|
+
console.log(`${GREEN}Mini COT server started (PID: ${child.pid})${RESET}`);
|
|
1578
|
+
console.log(`${DIM}Socket: ${socketPath}${RESET}`);
|
|
1579
|
+
console.log(`${DIM}Model: ${env.SPECMEM_MINICOT_MODEL}${RESET}`);
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
case 'stop': {
|
|
1584
|
+
const activePaths = getActivePaths();
|
|
1585
|
+
if (!fs.existsSync(activePaths.pidFilePath)) {
|
|
1586
|
+
console.log(`${YELLOW}No PID file found - server may not be running${RESET}`);
|
|
1587
|
+
process.exit(1);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const pidData = fs.readFileSync(activePaths.pidFilePath, 'utf8').trim();
|
|
1591
|
+
const [pid, timestamp] = pidData.split(':');
|
|
1592
|
+
|
|
1593
|
+
try {
|
|
1594
|
+
process.kill(parseInt(pid), 'SIGTERM');
|
|
1595
|
+
console.log(`${GREEN}Sent SIGTERM to Mini COT server (PID: ${pid})${RESET}`);
|
|
1596
|
+
|
|
1597
|
+
setTimeout(() => {
|
|
1598
|
+
try {
|
|
1599
|
+
process.kill(parseInt(pid), 0);
|
|
1600
|
+
process.kill(parseInt(pid), 'SIGKILL');
|
|
1601
|
+
console.log(`${YELLOW}Force killed server${RESET}`);
|
|
1602
|
+
} catch (e) {}
|
|
1603
|
+
}, 3000);
|
|
1604
|
+
|
|
1605
|
+
fs.unlinkSync(activePaths.pidFilePath);
|
|
1606
|
+
console.log(`${GREEN}Mini COT server stopped${RESET}`);
|
|
1607
|
+
} catch (e) {
|
|
1608
|
+
console.log(`${RED}Failed to stop server: ${e.message}${RESET}`);
|
|
1609
|
+
fs.unlinkSync(activePaths.pidFilePath);
|
|
1610
|
+
process.exit(1);
|
|
1611
|
+
}
|
|
1612
|
+
break;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
case 'restart': {
|
|
1616
|
+
const activePaths = getActivePaths();
|
|
1617
|
+
if (fs.existsSync(activePaths.pidFilePath)) {
|
|
1618
|
+
const pidData = fs.readFileSync(activePaths.pidFilePath, 'utf8').trim();
|
|
1619
|
+
const [pid] = pidData.split(':');
|
|
1620
|
+
try {
|
|
1621
|
+
process.kill(parseInt(pid), 'SIGTERM');
|
|
1622
|
+
} catch (e) {}
|
|
1623
|
+
fs.unlinkSync(activePaths.pidFilePath);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
setTimeout(() => {
|
|
1627
|
+
manageMinicot(['start']);
|
|
1628
|
+
}, 1000);
|
|
1629
|
+
break;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
case 'force-restart': {
|
|
1633
|
+
let activePaths;
|
|
1634
|
+
try {
|
|
1635
|
+
activePaths = getActivePaths();
|
|
1636
|
+
} catch (e) {
|
|
1637
|
+
activePaths = { socketDir, socketPath, pidFilePath, stoppedFlag };
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
if (fs.existsSync(pidFilePath)) {
|
|
1641
|
+
const pidData = fs.readFileSync(pidFilePath, 'utf8').trim();
|
|
1642
|
+
const [pid] = pidData.split(':');
|
|
1643
|
+
try {
|
|
1644
|
+
process.kill(parseInt(pid), 'SIGKILL');
|
|
1645
|
+
} catch (e) {}
|
|
1646
|
+
fs.unlinkSync(pidFilePath);
|
|
1647
|
+
}
|
|
1648
|
+
if (fs.existsSync(runPidFilePath)) {
|
|
1649
|
+
const pidData = fs.readFileSync(runPidFilePath, 'utf8').trim();
|
|
1650
|
+
const [pid] = pidData.split(':');
|
|
1651
|
+
try {
|
|
1652
|
+
process.kill(parseInt(pid), 'SIGKILL');
|
|
1653
|
+
} catch (e) {}
|
|
1654
|
+
fs.unlinkSync(runPidFilePath);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Also try to kill any process using the socket
|
|
1658
|
+
try {
|
|
1659
|
+
execSync(`lsof -t ${socketPath} 2>/dev/null | xargs -r kill -9 2>/dev/null || true`, { stdio: 'pipe' });
|
|
1660
|
+
execSync(`lsof -t ${runSocketPath} 2>/dev/null | xargs -r kill -9 2>/dev/null || true`, { stdio: 'pipe' });
|
|
1661
|
+
} catch (e) {}
|
|
1662
|
+
|
|
1663
|
+
setTimeout(() => {
|
|
1664
|
+
manageMinicot(['start']);
|
|
1665
|
+
}, 500);
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
case 'status': {
|
|
1670
|
+
console.log(`${CYAN}═══ Mini COT Server Status ═══${RESET}\n`);
|
|
1671
|
+
|
|
1672
|
+
const activePaths = getActivePaths();
|
|
1673
|
+
|
|
1674
|
+
if (!fs.existsSync(socketDir) && !fs.existsSync(runDir)) {
|
|
1675
|
+
console.log(`${YELLOW}Project not initialized - no specmem directory${RESET}`);
|
|
1676
|
+
console.log(`${DIM}Run 'specmem init' or 'specmem quickstart' first${RESET}`);
|
|
1677
|
+
process.exit(1);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Check PID file
|
|
1681
|
+
if (fs.existsSync(activePaths.pidFilePath)) {
|
|
1682
|
+
const pidData = fs.readFileSync(activePaths.pidFilePath, 'utf8').trim();
|
|
1683
|
+
const [pid, timestamp] = pidData.split(':');
|
|
1684
|
+
console.log(` PID File: ${GREEN}exists${RESET} (PID: ${pid})`);
|
|
1685
|
+
console.log(` Started: ${new Date(parseInt(timestamp)).toLocaleString()}`);
|
|
1686
|
+
|
|
1687
|
+
try {
|
|
1688
|
+
process.kill(parseInt(pid), 0);
|
|
1689
|
+
console.log(` Process: ${GREEN}running${RESET}`);
|
|
1690
|
+
} catch (e) {
|
|
1691
|
+
console.log(` Process: ${RED}dead (stale PID file)${RESET}`);
|
|
1692
|
+
}
|
|
1693
|
+
} else {
|
|
1694
|
+
console.log(` PID File: ${YELLOW}not found${RESET}`);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// Check socket
|
|
1698
|
+
console.log(`\n Socket: ${activePaths.socketPath}`);
|
|
1699
|
+
if (fs.existsSync(activePaths.socketPath)) {
|
|
1700
|
+
console.log(` ${GREEN}exists${RESET}`);
|
|
1701
|
+
} else {
|
|
1702
|
+
console.log(` ${YELLOW}not found${RESET}`);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// Check stopped flag
|
|
1706
|
+
if (fs.existsSync(stoppedFlag) || fs.existsSync(runStoppedFlag)) {
|
|
1707
|
+
console.log(`\n ${YELLOW}Server was manually stopped by user${RESET}`);
|
|
1708
|
+
console.log(` ${DIM}Use 'specmem minicot start' to restart${RESET}`);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// Show model info
|
|
1712
|
+
console.log(`\n Model: ${process.env.SPECMEM_MINICOT_MODEL || 'EleutherAI/pythia-410m'}${RESET}`);
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
case 'list': {
|
|
1717
|
+
console.log(`${CYAN}═══ All Mini COT Servers ═══${RESET}\n`);
|
|
1718
|
+
|
|
1719
|
+
const searchPaths = [
|
|
1720
|
+
path.join(os.homedir(), '.specmem'),
|
|
1721
|
+
];
|
|
1722
|
+
|
|
1723
|
+
// Add all specmem dirs we can find
|
|
1724
|
+
try {
|
|
1725
|
+
const projDirs = execSync(`find ${os.homedir()} -maxdepth 3 -type d -name "specmem" 2>/dev/null | head -20`, { encoding: 'utf8' })
|
|
1726
|
+
.trim()
|
|
1727
|
+
.split('\n')
|
|
1728
|
+
.filter(Boolean);
|
|
1729
|
+
for (const d of projDirs) {
|
|
1730
|
+
if (!searchPaths.includes(d)) searchPaths.push(d);
|
|
1731
|
+
}
|
|
1732
|
+
} catch (e) {}
|
|
1733
|
+
|
|
1734
|
+
let found = false;
|
|
1735
|
+
for (const sp of searchPaths) {
|
|
1736
|
+
if (!fs.existsSync(sp)) continue;
|
|
1737
|
+
|
|
1738
|
+
for (const subdir of ['sockets', 'run']) {
|
|
1739
|
+
const dir = path.join(sp, subdir);
|
|
1740
|
+
if (!fs.existsSync(dir)) continue;
|
|
1741
|
+
|
|
1742
|
+
const pf = path.join(dir, 'minicot.pid');
|
|
1743
|
+
if (!fs.existsSync(pf)) continue;
|
|
1744
|
+
|
|
1745
|
+
const pidData = fs.readFileSync(pf, 'utf8').trim();
|
|
1746
|
+
const [pid, timestamp] = pidData.split(':');
|
|
1747
|
+
const projName = path.basename(path.dirname(path.dirname(dir)));
|
|
1748
|
+
|
|
1749
|
+
let status = `${RED}dead${RESET}`;
|
|
1750
|
+
try {
|
|
1751
|
+
process.kill(parseInt(pid), 0);
|
|
1752
|
+
status = `${GREEN}running${RESET}`;
|
|
1753
|
+
} catch (e) {}
|
|
1754
|
+
|
|
1755
|
+
console.log(` Project: ${projName}`);
|
|
1756
|
+
console.log(` PID: ${pid} (${status})`);
|
|
1757
|
+
console.log(` Socket: ${path.join(dir, 'minicot.sock')}`);
|
|
1758
|
+
console.log(` Started: ${new Date(parseInt(timestamp)).toLocaleString()}`);
|
|
1759
|
+
console.log('');
|
|
1760
|
+
found = true;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (!found) {
|
|
1765
|
+
console.log(` ${DIM}No Mini COT servers found${RESET}`);
|
|
1766
|
+
}
|
|
1767
|
+
break;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
case 'diagnose': {
|
|
1771
|
+
console.log(`${CYAN}═══ Mini COT Server Diagnostics ═══${RESET}\n`);
|
|
1772
|
+
|
|
1773
|
+
const activePaths = getActivePaths();
|
|
1774
|
+
|
|
1775
|
+
console.log(`Project: ${projectPath}`);
|
|
1776
|
+
console.log(`SpecMem Dir: ${specmemDir}`);
|
|
1777
|
+
console.log(`Socket Dir: ${activePaths.socketDir}`);
|
|
1778
|
+
console.log(`Socket Path: ${activePaths.socketPath}`);
|
|
1779
|
+
console.log(`PID File: ${activePaths.pidFilePath}`);
|
|
1780
|
+
|
|
1781
|
+
// Check if directories exist
|
|
1782
|
+
console.log(`\n${CYAN}Directory Status:${RESET}`);
|
|
1783
|
+
console.log(` specmem/ exists: ${fs.existsSync(specmemDir) ? GREEN + 'yes' + RESET : YELLOW + 'no' + RESET}`);
|
|
1784
|
+
console.log(` sockets/ exists: ${fs.existsSync(socketDir) ? GREEN + 'yes' + RESET : YELLOW + 'no' + RESET}`);
|
|
1785
|
+
console.log(` run/ exists: ${fs.existsSync(runDir) ? GREEN + 'yes' + RESET : YELLOW + 'no' + RESET}`);
|
|
1786
|
+
console.log(` socket exists: ${fs.existsSync(activePaths.socketPath) ? GREEN + 'yes' + RESET : YELLOW + 'no' + RESET}`);
|
|
1787
|
+
console.log(` PID file exists: ${fs.existsSync(activePaths.pidFilePath) ? GREEN + 'yes' + RESET : YELLOW + 'no' + RESET}`);
|
|
1788
|
+
|
|
1789
|
+
// Check process
|
|
1790
|
+
if (fs.existsSync(activePaths.pidFilePath)) {
|
|
1791
|
+
const pidData = fs.readFileSync(activePaths.pidFilePath, 'utf8').trim();
|
|
1792
|
+
const [pid] = pidData.split(':');
|
|
1793
|
+
|
|
1794
|
+
console.log(`\n${CYAN}Process Info:${RESET}`);
|
|
1795
|
+
console.log(` PID: ${pid}`);
|
|
1796
|
+
|
|
1797
|
+
try {
|
|
1798
|
+
const procPath = `/proc/${pid}`;
|
|
1799
|
+
if (fs.existsSync(procPath)) {
|
|
1800
|
+
const cmdline = fs.readFileSync(path.join(procPath, 'cmdline'), 'utf8').replace(/\0/g, ' ');
|
|
1801
|
+
console.log(` Command: ${cmdline.slice(0, 100)}`);
|
|
1802
|
+
|
|
1803
|
+
try {
|
|
1804
|
+
const envPath = path.join(procPath, 'environ');
|
|
1805
|
+
const envData = fs.readFileSync(envPath);
|
|
1806
|
+
const envStr = envData.toString('utf8');
|
|
1807
|
+
const minicotEnv = envStr.split('\0').find(e => e.startsWith('SPECMEM_MINICOT'));
|
|
1808
|
+
if (minicotEnv) {
|
|
1809
|
+
console.log(` Env: ${minicotEnv.slice(0, 80)}`);
|
|
1810
|
+
}
|
|
1811
|
+
} catch (e) {}
|
|
1812
|
+
} else {
|
|
1813
|
+
console.log(` ${RED}Process does not exist (stale PID)${RESET}`);
|
|
1814
|
+
}
|
|
1815
|
+
} catch (e) {
|
|
1816
|
+
console.log(` ${RED}Error reading process: ${e.message}${RESET}`);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// Check for running minicot processes
|
|
1821
|
+
console.log(`\n${CYAN}Running Python Processes:${RESET}`);
|
|
1822
|
+
try {
|
|
1823
|
+
const procs = execSync(`pgrep -af "mini-cot-service.py" 2>/dev/null || echo ""`, { encoding: 'utf8' }).trim();
|
|
1824
|
+
if (procs) {
|
|
1825
|
+
console.log(procs.split('\n').map(l => ` ${l}`).join('\n'));
|
|
1826
|
+
} else {
|
|
1827
|
+
console.log(` ${DIM}None found${RESET}`);
|
|
1828
|
+
}
|
|
1829
|
+
} catch (e) {
|
|
1830
|
+
console.log(` ${DIM}Could not search processes${RESET}`);
|
|
1831
|
+
}
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
case 'cleanup': {
|
|
1836
|
+
console.log(`${CYAN}Cleaning up stale Mini COT server files...${RESET}`);
|
|
1837
|
+
|
|
1838
|
+
let cleaned = 0;
|
|
1839
|
+
|
|
1840
|
+
const searchPaths = [
|
|
1841
|
+
path.join(os.homedir(), '.specmem'),
|
|
1842
|
+
];
|
|
1843
|
+
|
|
1844
|
+
// Find all specmem dirs
|
|
1845
|
+
try {
|
|
1846
|
+
const projDirs = execSync(`find ${os.homedir()} -maxdepth 3 -type d -name "specmem" 2>/dev/null | head -20`, { encoding: 'utf8' })
|
|
1847
|
+
.trim()
|
|
1848
|
+
.split('\n')
|
|
1849
|
+
.filter(Boolean);
|
|
1850
|
+
for (const d of projDirs) {
|
|
1851
|
+
if (!searchPaths.includes(d)) searchPaths.push(d);
|
|
1852
|
+
}
|
|
1853
|
+
} catch (e) {}
|
|
1854
|
+
|
|
1855
|
+
for (const sp of searchPaths) {
|
|
1856
|
+
if (!fs.existsSync(sp)) continue;
|
|
1857
|
+
|
|
1858
|
+
for (const subdir of ['sockets', 'run']) {
|
|
1859
|
+
const dir = path.join(sp, subdir);
|
|
1860
|
+
if (!fs.existsSync(dir)) continue;
|
|
1861
|
+
|
|
1862
|
+
const pf = path.join(dir, 'minicot.pid');
|
|
1863
|
+
if (!fs.existsSync(pf)) continue;
|
|
1864
|
+
|
|
1865
|
+
try {
|
|
1866
|
+
const pidData = fs.readFileSync(pf, 'utf8').trim();
|
|
1867
|
+
const [pid] = pidData.split(':');
|
|
1868
|
+
try {
|
|
1869
|
+
process.kill(parseInt(pid), 0);
|
|
1870
|
+
} catch (e) {
|
|
1871
|
+
fs.unlinkSync(pf);
|
|
1872
|
+
console.log(` Removed stale PID file: ${pf}`);
|
|
1873
|
+
cleaned++;
|
|
1874
|
+
}
|
|
1875
|
+
} catch (e) {}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
console.log(`${GREEN}Cleaned ${cleaned} stale PID files${RESET}`);
|
|
1880
|
+
break;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
default:
|
|
1884
|
+
console.log(`${BOLD}Usage:${RESET} specmem minicot <command>
|
|
1885
|
+
|
|
1886
|
+
${BOLD}Commands:${RESET}
|
|
1887
|
+
${CYAN}start${RESET} Start Mini COT server (Pythia-410M)
|
|
1888
|
+
${CYAN}stop${RESET} Stop Mini COT server
|
|
1889
|
+
${CYAN}restart${RESET} Restart Mini COT server
|
|
1890
|
+
${CYAN}force-restart${RESET} Force restart (kill and start fresh)
|
|
1891
|
+
${CYAN}status${RESET} Show Mini COT server status
|
|
1892
|
+
${CYAN}list${RESET} List all Mini COT servers
|
|
1893
|
+
${CYAN}diagnose${RESET} Full diagnostic info
|
|
1894
|
+
${CYAN}cleanup${RESET} Clean up stale PID files
|
|
1895
|
+
`);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// ============================================================================
|
|
1900
|
+
// QUICKSTART - Minimal init without database
|
|
1901
|
+
// ============================================================================
|
|
1902
|
+
function runQuickstart() {
|
|
1903
|
+
showBanner();
|
|
1904
|
+
console.log(`${CYAN}${BOLD}═══ SpecMem Quickstart ═══${RESET}\n`);
|
|
1905
|
+
console.log(`${DIM}Setting up minimal SpecMem configuration...${RESET}\n`);
|
|
1906
|
+
|
|
1907
|
+
const projectPath = process.cwd();
|
|
1908
|
+
const specmemDir = path.join(projectPath, 'specmem');
|
|
1909
|
+
const configDir = path.join(os.homedir(), '.specmem');
|
|
1910
|
+
|
|
1911
|
+
// Create project directories
|
|
1912
|
+
console.log(`${CYAN}→${RESET} Creating project directories...`);
|
|
1913
|
+
const dirs = ['run', 'sockets', 'logs', 'data'];
|
|
1914
|
+
for (const dir of dirs) {
|
|
1915
|
+
const dirPath = path.join(specmemDir, dir);
|
|
1916
|
+
if (!fs.existsSync(dirPath)) {
|
|
1917
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
console.log(`${GREEN}✓${RESET} Created ${specmemDir}/*`);
|
|
1921
|
+
|
|
1922
|
+
// Create global config if needed
|
|
1923
|
+
console.log(`${CYAN}→${RESET} Setting up global config...`);
|
|
1924
|
+
const configFile = path.join(configDir, 'config.json');
|
|
1925
|
+
if (!fs.existsSync(configFile)) {
|
|
1926
|
+
if (!fs.existsSync(configDir)) {
|
|
1927
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1928
|
+
}
|
|
1929
|
+
const defaultConfig = {
|
|
1930
|
+
SPECMEM_SEARCH_LIMIT: '5',
|
|
1931
|
+
SPECMEM_THRESHOLD: '0.30',
|
|
1932
|
+
SPECMEM_MAX_CONTENT: '200',
|
|
1933
|
+
SPECMEM_AUTO_CONSOLIDATE: 'true',
|
|
1934
|
+
};
|
|
1935
|
+
fs.writeFileSync(configFile, JSON.stringify(defaultConfig, null, 2), { mode: 0o644 });
|
|
1936
|
+
console.log(`${GREEN}✓${RESET} Created default config`);
|
|
1937
|
+
} else {
|
|
1938
|
+
console.log(`${DIM} Config already exists${RESET}`);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// Create .env file if needed
|
|
1942
|
+
const envFile = path.join(projectPath, '.env');
|
|
1943
|
+
if (!fs.existsSync(envFile)) {
|
|
1944
|
+
const envContent = `# SpecMem Configuration
|
|
1945
|
+
# Database (optional - will use defaults if not set)
|
|
1946
|
+
# SPECMEM_DB_HOST=localhost
|
|
1947
|
+
# SPECMEM_DB_PORT=5432
|
|
1948
|
+
# SPECMEM_DB_NAME=specmem
|
|
1949
|
+
|
|
1950
|
+
# Embedding settings
|
|
1951
|
+
# SPECMEM_EMBEDDING_MODEL=BAAI/bge-small-en-v1.5
|
|
1952
|
+
|
|
1953
|
+
# Memory settings
|
|
1954
|
+
# SPECMEM_SEARCH_LIMIT=5
|
|
1955
|
+
# SPECMEM_THRESHOLD=0.30
|
|
1956
|
+
`;
|
|
1957
|
+
fs.writeFileSync(envFile, envContent, { mode: 0o644 });
|
|
1958
|
+
console.log(`${GREEN}✓${RESET} Created .env template`);
|
|
1959
|
+
} else {
|
|
1960
|
+
console.log(`${DIM} .env already exists${RESET}`);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
console.log(`
|
|
1964
|
+
${GREEN}${BOLD}✓ Quickstart complete!${RESET}
|
|
1965
|
+
|
|
1966
|
+
${CYAN}Next steps:${RESET}
|
|
1967
|
+
• Run ${CYAN}specmem embed start${RESET} to start embedding server
|
|
1968
|
+
• Or run ${CYAN}claude${RESET} - SpecMem will auto-start services
|
|
1969
|
+
|
|
1970
|
+
${YELLOW}Note:${RESET} Database not configured. Run ${CYAN}specmem init${RESET} for full setup.
|
|
1971
|
+
`);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
912
1974
|
// Show help
|
|
913
1975
|
function showHelp() {
|
|
914
1976
|
showBanner();
|
|
915
1977
|
console.log(`${BOLD}Usage:${RESET} specmem <command>
|
|
916
1978
|
|
|
917
1979
|
${BOLD}Commands:${RESET}
|
|
918
|
-
${CYAN}setup${RESET}
|
|
919
|
-
${CYAN}init${RESET}
|
|
920
|
-
${CYAN}
|
|
921
|
-
${CYAN}
|
|
1980
|
+
${CYAN}setup${RESET} First-run setup (download + optimize embedding models)
|
|
1981
|
+
${CYAN}init${RESET} Initialize SpecMem for current project
|
|
1982
|
+
${CYAN}quickstart${RESET} Quick setup without database
|
|
1983
|
+
${CYAN}status${RESET} Check SpecMem status and model info
|
|
1984
|
+
${CYAN}proxy${RESET} Manage compaction proxy (enable/disable/status)
|
|
1985
|
+
${CYAN}embed${RESET} Manage embedding server (start/stop/status)
|
|
1986
|
+
${CYAN}minicot${RESET} Manage Mini COT server (Pythia-410M)
|
|
1987
|
+
${CYAN}config${RESET} Manage configuration settings
|
|
922
1988
|
${CYAN}dashboard${RESET} Launch the AEGIS dashboard (alias: dash)
|
|
923
|
-
${CYAN}doctor${RESET}
|
|
1989
|
+
${CYAN}doctor${RESET} Check system requirements
|
|
924
1990
|
|
|
925
1991
|
${BOLD}Quick Start:${RESET}
|
|
926
|
-
${DIM}$${RESET} specmem init
|
|
1992
|
+
${DIM}$${RESET} specmem init ${DIM}# First time only - optimizes models${RESET}
|
|
927
1993
|
${DIM}$${RESET} cd your-project
|
|
928
|
-
${DIM}$${RESET} specmem init
|
|
929
|
-
${DIM}$${RESET} claude
|
|
1994
|
+
${DIM}$${RESET} specmem init ${DIM}# Initialize project${RESET}
|
|
1995
|
+
${DIM}$${RESET} claude ${DIM}# Services auto-start with !${RESET}
|
|
930
1996
|
|
|
931
1997
|
${BOLD}What It Does:${RESET}
|
|
932
1998
|
SpecMem provides semantic memory for Code sessions.
|