specmem-hardwicksoftware 3.7.29 → 3.7.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/compactionProxy.js +52 -17
- package/package.json +1 -1
- package/scripts/specmem-init.cjs +86 -111
- package/specmem/supervisord.conf +1 -1
|
@@ -1254,25 +1254,57 @@ async function handleRequest(req, res) {
|
|
|
1254
1254
|
|
|
1255
1255
|
pushEvent('info', `POST /v1/messages model=${body.model || '?'} msgs=${messageCount} size=${(originalSize / 1024).toFixed(0)}KB`);
|
|
1256
1256
|
|
|
1257
|
+
const isCompaction = isCompactionRequest(body);
|
|
1258
|
+
const isPassthrough = !isCompaction && (dontCompress || messageCount <= liveConfig.PRESERVE_RECENT_MESSAGES);
|
|
1259
|
+
let sysPromptModified = false;
|
|
1260
|
+
|
|
1257
1261
|
// === SYSTEM PROMPT COMPRESSION ===
|
|
1262
|
+
// Always compress system prompt if not dontCompress — cache makes repeat calls free.
|
|
1263
|
+
// Cache-miss: fire-and-forget on passthrough (don't block forwarding), await on compaction/live paths.
|
|
1258
1264
|
if (!dontCompress && body.system) {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1265
|
+
// Build hash to check cache without calling async function
|
|
1266
|
+
const _sysKey = typeof body.system === 'string' ? body.system
|
|
1267
|
+
: Array.isArray(body.system) ? body.system.map(b => typeof b === 'string' ? b : (b?.text || '')).join('')
|
|
1268
|
+
: JSON.stringify(body.system);
|
|
1269
|
+
const _sysHash = require('crypto').createHash('md5').update(_sysKey).digest('hex');
|
|
1270
|
+
const _sysCached = _sysPromptCache.get(_sysHash);
|
|
1271
|
+
|
|
1272
|
+
if (_sysCached) {
|
|
1273
|
+
// Cache hit — zero latency, always apply
|
|
1274
|
+
if (_sysCached.charsSaved > 0) {
|
|
1275
|
+
body.system = _sysCached.system;
|
|
1276
|
+
sysPromptModified = true;
|
|
1277
|
+
stats.sysPromptCharsSaved += _sysCached.charsSaved;
|
|
1264
1278
|
stats.sysPromptCompressed++;
|
|
1265
|
-
stats.tokensStripped += Math.floor(
|
|
1266
|
-
stats.bytesStripped +=
|
|
1267
|
-
log('compress', `SYSPROMPT: ${
|
|
1268
|
-
pushEvent('compress', `System prompt: -${
|
|
1279
|
+
stats.tokensStripped += Math.floor(_sysCached.charsSaved / 4);
|
|
1280
|
+
stats.bytesStripped += _sysCached.charsSaved;
|
|
1281
|
+
log('compress', `SYSPROMPT (cache hit): ${_sysCached.charsSaved} chars saved`);
|
|
1282
|
+
pushEvent('compress', `System prompt (cached): -${_sysCached.charsSaved} chars`);
|
|
1283
|
+
}
|
|
1284
|
+
} else if (isPassthrough) {
|
|
1285
|
+
// Cache miss + passthrough: fire-and-forget on new thread — populates cache for next request
|
|
1286
|
+
compressSystemPrompt(body.system).catch(() => {});
|
|
1287
|
+
} else {
|
|
1288
|
+
// Cache miss + compaction/live: must await (need compressed body)
|
|
1289
|
+
try {
|
|
1290
|
+
const sysResult = await compressSystemPrompt(body.system);
|
|
1291
|
+
if (sysResult.charsSaved > 0) {
|
|
1292
|
+
body.system = sysResult.system;
|
|
1293
|
+
sysPromptModified = true;
|
|
1294
|
+
stats.sysPromptCharsSaved += sysResult.charsSaved;
|
|
1295
|
+
stats.sysPromptCompressed++;
|
|
1296
|
+
stats.tokensStripped += Math.floor(sysResult.charsSaved / 4);
|
|
1297
|
+
stats.bytesStripped += sysResult.charsSaved;
|
|
1298
|
+
log('compress', `SYSPROMPT: ${sysResult.charsSaved} chars saved`);
|
|
1299
|
+
pushEvent('compress', `System prompt: -${sysResult.charsSaved} chars`);
|
|
1300
|
+
}
|
|
1301
|
+
} catch (e) {
|
|
1302
|
+
log('warn', `System prompt compression failed: ${e.message}`);
|
|
1269
1303
|
}
|
|
1270
|
-
} catch (e) {
|
|
1271
|
-
log('warn', `System prompt compression failed: ${e.message}`);
|
|
1272
1304
|
}
|
|
1273
1305
|
}
|
|
1274
1306
|
|
|
1275
|
-
if (
|
|
1307
|
+
if (isCompaction) {
|
|
1276
1308
|
// === COMPACTION DETECTED — strip tool bodies ===
|
|
1277
1309
|
stats.compactionRequests++;
|
|
1278
1310
|
stats.lastCompaction = new Date().toISOString();
|
|
@@ -1284,7 +1316,7 @@ async function handleRequest(req, res) {
|
|
|
1284
1316
|
const { strippedMessages, strippingStats } = stripMessages(body.messages);
|
|
1285
1317
|
body.messages = strippedMessages;
|
|
1286
1318
|
|
|
1287
|
-
//
|
|
1319
|
+
// Run steno+MT compression in parallel (independent of strip)
|
|
1288
1320
|
if (!dontCompress) {
|
|
1289
1321
|
const { messages: compressed, blocksCompressed, charsCompressed, verifiedCount = 0, stenoOnlyCount = 0, tmHits: hits = 0, samples: compSamples = [] } = await compressMessagesLive(body.messages);
|
|
1290
1322
|
body.messages = compressed;
|
|
@@ -1294,7 +1326,6 @@ async function handleRequest(req, res) {
|
|
|
1294
1326
|
stats.zhRejected += stenoOnlyCount;
|
|
1295
1327
|
stats.stenoOnly += (blocksCompressed - verifiedCount - stenoOnlyCount);
|
|
1296
1328
|
stats.tmHits += hits;
|
|
1297
|
-
// Store translation samples for preview
|
|
1298
1329
|
if (compSamples.length > 0) stats._lastSamples = compSamples;
|
|
1299
1330
|
if (blocksCompressed > 0) {
|
|
1300
1331
|
pushEvent('compress', `${blocksCompressed} blocks, ${charsCompressed} chars (${verifiedCount} zh, ${stenoOnlyCount} steno, ${hits} TM)`);
|
|
@@ -1318,11 +1349,15 @@ async function handleRequest(req, res) {
|
|
|
1318
1349
|
return;
|
|
1319
1350
|
}
|
|
1320
1351
|
|
|
1321
|
-
// === NON-COMPACTION —
|
|
1322
|
-
if (
|
|
1352
|
+
// === NON-COMPACTION — passthrough if below threshold ===
|
|
1353
|
+
if (isPassthrough) {
|
|
1323
1354
|
stats.passthrough++;
|
|
1324
1355
|
pushEvent('pass', `msgs=${messageCount} (below threshold ${liveConfig.PRESERVE_RECENT_MESSAGES})`);
|
|
1325
|
-
|
|
1356
|
+
// Use modified body if sys prompt was compressed (cache hit), else rawBody
|
|
1357
|
+
const passthroughBody = sysPromptModified
|
|
1358
|
+
? Buffer.from(JSON.stringify(body), 'utf8')
|
|
1359
|
+
: rawBody;
|
|
1360
|
+
forwardRequest(req, res, passthroughBody);
|
|
1326
1361
|
return;
|
|
1327
1362
|
}
|
|
1328
1363
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specmem-hardwicksoftware",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Your Claude Code sessions don't have to start from scratch anymore — SpecMem gives your AI real memory. It won't forget your conversations, your code, or your architecture decisions between sessions. That's the whole point. Semantic code indexing that actually works: TypeScript, JavaScript, Python, Go, Rust, Java, Kotlin, C, C++, HTML and more. It doesn't just track functions — it gets classes, methods, fields, constants, enums, macros, imports, structs, the whole codebase graph. There's chat memory too, powered by pgvector embeddings. You've also got token compression, team coordination, multi-agent comms, and file watching built in. 74+ MCP tools. Runs on PostgreSQL + Docker. It's kind of a big deal. justcalljon.pro",
|
|
6
6
|
"main": "dist/index.js",
|
package/scripts/specmem-init.cjs
CHANGED
|
@@ -3202,11 +3202,11 @@ const TIER_CONFIG = {
|
|
|
3202
3202
|
const MAX_EMBED_CHARS = 8000;
|
|
3203
3203
|
|
|
3204
3204
|
// ============================================================================
|
|
3205
|
-
// STAGE
|
|
3205
|
+
// STAGE 2: PROJECT ANALYSIS
|
|
3206
3206
|
// ============================================================================
|
|
3207
3207
|
|
|
3208
3208
|
async function analyzeProject(projectPath, ui) {
|
|
3209
|
-
ui.setStage(
|
|
3209
|
+
ui.setStage(2, 'PROJECT ANALYSIS');
|
|
3210
3210
|
|
|
3211
3211
|
const results = {
|
|
3212
3212
|
tier: 'small',
|
|
@@ -3386,11 +3386,11 @@ async function analyzeProject(projectPath, ui) {
|
|
|
3386
3386
|
}
|
|
3387
3387
|
|
|
3388
3388
|
// ============================================================================
|
|
3389
|
-
// STAGE
|
|
3389
|
+
// STAGE 3: SCORCHED EARTH - Wipe everything and rebuild fresh
|
|
3390
3390
|
// ============================================================================
|
|
3391
3391
|
|
|
3392
3392
|
async function scorchedEarth(projectPath, ui) {
|
|
3393
|
-
ui.setStage(
|
|
3393
|
+
ui.setStage(3, 'CLEANUP');
|
|
3394
3394
|
|
|
3395
3395
|
const specmemDir = path.join(projectPath, 'specmem');
|
|
3396
3396
|
const projectDir = path.join(projectPath, '.claude');
|
|
@@ -3539,11 +3539,11 @@ async function scorchedEarth(projectPath, ui) {
|
|
|
3539
3539
|
}
|
|
3540
3540
|
|
|
3541
3541
|
// ============================================================================
|
|
3542
|
-
// STAGE
|
|
3542
|
+
// STAGE 4: MODEL OPTIMIZATION
|
|
3543
3543
|
// ============================================================================
|
|
3544
3544
|
|
|
3545
3545
|
async function optimizeModel(projectPath, analysis, ui) {
|
|
3546
|
-
ui.setStage(
|
|
3546
|
+
ui.setStage(4, 'BLAST OFF 🚀');
|
|
3547
3547
|
|
|
3548
3548
|
const recommended = TIER_CONFIG[analysis.tier];
|
|
3549
3549
|
|
|
@@ -3750,7 +3750,7 @@ async function optimizeModel(projectPath, analysis, ui) {
|
|
|
3750
3750
|
* This ensures the overflow queue is pre-populated before launches.
|
|
3751
3751
|
*/
|
|
3752
3752
|
async function coldStartEmbeddingDocker(projectPath, modelConfig, ui, codebaseResult) {
|
|
3753
|
-
ui.setStage(
|
|
3753
|
+
ui.setStage(5, 'EMBEDDING DOCKER');
|
|
3754
3754
|
|
|
3755
3755
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3756
3756
|
// 🔒 PRE-FLIGHT ACK CHECK - Verify optimizations before starting embedding 🔒
|
|
@@ -4129,7 +4129,7 @@ async function waitForEmbeddingReady(sockPath, opts = {}) {
|
|
|
4129
4129
|
}
|
|
4130
4130
|
|
|
4131
4131
|
async function indexCodebase(projectPath, ui, embeddingResult) {
|
|
4132
|
-
ui.setStage(
|
|
4132
|
+
ui.setStage(6, 'CODEBASE INDEXING');
|
|
4133
4133
|
|
|
4134
4134
|
const { Pool } = require('pg');
|
|
4135
4135
|
const crypto = require('crypto');
|
|
@@ -5982,11 +5982,11 @@ async function extractDefinitions(content, filePath, language, fileId) {
|
|
|
5982
5982
|
}
|
|
5983
5983
|
|
|
5984
5984
|
// ============================================================================
|
|
5985
|
-
// STAGE
|
|
5985
|
+
// STAGE 7: TOKEN COMPRESSION
|
|
5986
5986
|
// ============================================================================
|
|
5987
5987
|
|
|
5988
5988
|
async function compressTokens(projectPath, ui) {
|
|
5989
|
-
ui.setStage(
|
|
5989
|
+
ui.setStage(8, 'TOKEN COMPRESSION');
|
|
5990
5990
|
|
|
5991
5991
|
// Import compressor inline
|
|
5992
5992
|
let compress;
|
|
@@ -6085,11 +6085,11 @@ async function compressTokens(projectPath, ui) {
|
|
|
6085
6085
|
|
|
6086
6086
|
|
|
6087
6087
|
// ============================================================================
|
|
6088
|
-
// STAGE
|
|
6088
|
+
// STAGE 8: COMMAND DEPLOYMENT
|
|
6089
6089
|
// ============================================================================
|
|
6090
6090
|
|
|
6091
6091
|
async function deployCommands(projectPath, ui) {
|
|
6092
|
-
ui.setStage(
|
|
6092
|
+
ui.setStage(9, 'COMMAND DEPLOYMENT');
|
|
6093
6093
|
|
|
6094
6094
|
const globalCmdsDir = path.join(os.homedir(), '.claude', 'commands');
|
|
6095
6095
|
const projectCmdsDir = path.join(projectPath, '.claude', 'commands');
|
|
@@ -6167,7 +6167,7 @@ async function deployCommands(projectPath, ui) {
|
|
|
6167
6167
|
* so they're searchable via find_memory from the start.
|
|
6168
6168
|
*/
|
|
6169
6169
|
async function extractSessions(projectPath, ui, embeddingResult = null) {
|
|
6170
|
-
ui.setStage(
|
|
6170
|
+
ui.setStage(7, 'SESSION EXTRACTION');
|
|
6171
6171
|
|
|
6172
6172
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
6173
6173
|
const projectsDir = path.join(claudeDir, 'projects');
|
|
@@ -6812,11 +6812,11 @@ async function extractSessions(projectPath, ui, embeddingResult = null) {
|
|
|
6812
6812
|
}
|
|
6813
6813
|
|
|
6814
6814
|
// ============================================================================
|
|
6815
|
-
// STAGE
|
|
6815
|
+
// STAGE 10: FINAL VERIFICATION
|
|
6816
6816
|
// ============================================================================
|
|
6817
6817
|
|
|
6818
6818
|
async function finalVerification(projectPath, analysis, modelConfig, ui) {
|
|
6819
|
-
ui.setStage(
|
|
6819
|
+
ui.setStage(10, 'FINAL VERIFICATION');
|
|
6820
6820
|
|
|
6821
6821
|
const checks = {
|
|
6822
6822
|
modelConfig: false,
|
|
@@ -8955,32 +8955,44 @@ function registerProjectInRegistry(projectPath, globalDir) {
|
|
|
8955
8955
|
}
|
|
8956
8956
|
|
|
8957
8957
|
// ============================================================================
|
|
8958
|
-
// MODEL DOWNLOAD — ensures ML models exist before init proceeds
|
|
8958
|
+
// MODEL DOWNLOAD — Stage 1: ensures all ML models exist before init proceeds
|
|
8959
8959
|
// ============================================================================
|
|
8960
8960
|
|
|
8961
8961
|
/**
|
|
8962
|
-
*
|
|
8963
|
-
*
|
|
8964
|
-
*
|
|
8962
|
+
* Stage 1: MODEL DOWNLOAD
|
|
8963
|
+
* Checks for all required ML models. If any are missing, auto-downloads
|
|
8964
|
+
* the combined models tarball from GitHub release — no prompting.
|
|
8965
|
+
*
|
|
8966
|
+
* Models bundled in npm (always present after install):
|
|
8967
|
+
* - all-MiniLM-L6-v2 (embedding)
|
|
8968
|
+
* - minisbd (sentence boundary)
|
|
8969
|
+
*
|
|
8970
|
+
* Models NOT in npm (LFS-only, downloaded via release tarball):
|
|
8971
|
+
* - pythia-410m-onnx-quant (mini-COT)
|
|
8972
|
+
* - argos-translate (translation en↔zh/zt)
|
|
8965
8973
|
*/
|
|
8966
|
-
async function ensureModels() {
|
|
8974
|
+
async function ensureModels(ui) {
|
|
8967
8975
|
const specmemRoot = path.resolve(__dirname, '..');
|
|
8968
|
-
const
|
|
8976
|
+
const modelsDir = path.join(specmemRoot, 'embedding-sandbox', 'models');
|
|
8977
|
+
const version = SPECMEM_VERSION;
|
|
8969
8978
|
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8979
|
+
const sentinels = [
|
|
8980
|
+
{ name: 'Mini-COT (Pythia)', path: path.join(modelsDir, 'pythia-410m-onnx-quant', 'model_quantized.onnx') },
|
|
8981
|
+
{ name: 'Translation (Argos)', path: path.join(modelsDir, 'argos-translate', 'translate-en_zh-1_9', 'model', 'model.bin') },
|
|
8982
|
+
];
|
|
8983
|
+
|
|
8984
|
+
const missing = sentinels.filter(s => !fs.existsSync(s.path));
|
|
8985
|
+
|
|
8986
|
+
if (missing.length === 0) {
|
|
8987
|
+
initLog('[MODELS] All models present — skipping download');
|
|
8988
|
+
if (ui) ui.setSubStatus('✓ All ML models present');
|
|
8973
8989
|
return;
|
|
8974
8990
|
}
|
|
8975
8991
|
|
|
8976
|
-
initLog(
|
|
8977
|
-
|
|
8978
|
-
const nohup = process.argv.includes('--nohup') || process.env.SPECMEM_NOHUP === '1';
|
|
8979
|
-
const modelsDir = path.join(specmemRoot, 'embedding-sandbox', 'models');
|
|
8980
|
-
const version = SPECMEM_VERSION;
|
|
8981
|
-
const releaseUrl = `https://github.com/jonhardwick-spec/specmem/releases/download/v${version}/specmem-models-${version}.tar.gz`;
|
|
8992
|
+
initLog(`[MODELS] Missing: ${missing.map(s => s.name).join(', ')}`);
|
|
8993
|
+
if (ui) ui.setSubStatus(`↓ Downloading ML models (~570MB)...`);
|
|
8982
8994
|
|
|
8983
|
-
// Check
|
|
8995
|
+
// Check write perms
|
|
8984
8996
|
let needsSudo = false;
|
|
8985
8997
|
try {
|
|
8986
8998
|
fs.accessSync(path.join(specmemRoot, 'embedding-sandbox'), fs.constants.W_OK);
|
|
@@ -8988,102 +9000,62 @@ async function ensureModels() {
|
|
|
8988
9000
|
needsSudo = true;
|
|
8989
9001
|
}
|
|
8990
9002
|
|
|
8991
|
-
if (!nohup) {
|
|
8992
|
-
console.log('');
|
|
8993
|
-
console.log(drawBox([
|
|
8994
|
-
`${c.bold}${c.yellow}ML Models Required${c.reset}`,
|
|
8995
|
-
'',
|
|
8996
|
-
`SpecMem needs to download ML models (~500MB).`,
|
|
8997
|
-
`These are stripped from npm to keep installs fast.`,
|
|
8998
|
-
'',
|
|
8999
|
-
`${c.dim}Source: GitHub release v${version}${c.reset}`,
|
|
9000
|
-
needsSudo ? `${c.yellow}Note: Install path requires sudo for extraction${c.reset}` : '',
|
|
9001
|
-
].filter(Boolean), { borderColor: c.yellow }));
|
|
9002
|
-
|
|
9003
|
-
const readline = require('readline');
|
|
9004
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
9005
|
-
|
|
9006
|
-
const answer = await new Promise((resolve) => {
|
|
9007
|
-
rl.question(` ${c.bold}Download models?${c.reset} [Y/n] `, (ans) => {
|
|
9008
|
-
rl.close();
|
|
9009
|
-
resolve(ans.trim().toLowerCase());
|
|
9010
|
-
});
|
|
9011
|
-
});
|
|
9012
|
-
|
|
9013
|
-
if (answer === 'n' || answer === 'no') {
|
|
9014
|
-
console.log(` ${c.yellow}⚠${c.reset} Skipped model download — embedding features will not work.`);
|
|
9015
|
-
initLog('[MODELS] User declined model download');
|
|
9016
|
-
return;
|
|
9017
|
-
}
|
|
9018
|
-
} else {
|
|
9019
|
-
console.log(` ${c.dim}[nohup] Auto-downloading ML models (~500MB)...${c.reset}`);
|
|
9020
|
-
}
|
|
9021
|
-
|
|
9022
|
-
// Download
|
|
9023
|
-
const tmpTarball = path.join(os.tmpdir(), `specmem-models-${version}.tar.gz`);
|
|
9024
|
-
|
|
9025
|
-
// Prefer curl, fall back to wget
|
|
9026
9003
|
const hasCurl = (() => { try { execSync('which curl', { stdio: 'pipe' }); return true; } catch { return false; } })();
|
|
9027
9004
|
const hasWget = (() => { try { execSync('which wget', { stdio: 'pipe' }); return true; } catch { return false; } })();
|
|
9028
9005
|
|
|
9029
9006
|
if (!hasCurl && !hasWget) {
|
|
9030
|
-
console.log(` ${c.red}✗${c.reset} Neither curl nor wget found — cannot download models.`);
|
|
9031
|
-
console.log(` ${c.dim}Install curl or wget and re-run specmem init.${c.reset}`);
|
|
9032
9007
|
initLog('[MODELS] ERROR: no curl or wget');
|
|
9008
|
+
if (ui) ui.setSubStatus('⚠ curl/wget not found — ML models unavailable (mini-COT/translation disabled)');
|
|
9033
9009
|
return;
|
|
9034
9010
|
}
|
|
9035
9011
|
|
|
9036
|
-
|
|
9037
|
-
|
|
9012
|
+
const releaseUrl = `https://github.com/jonhardwick-spec/specmem/releases/download/v${version}/specmem-models-${version}.tar.gz`;
|
|
9013
|
+
const tmpTarball = path.join(os.tmpdir(), `specmem-models-${version}.tar.gz`);
|
|
9014
|
+
|
|
9015
|
+
initLog(`[MODELS] Downloading: ${releaseUrl}`);
|
|
9038
9016
|
|
|
9039
9017
|
try {
|
|
9040
9018
|
const dlCmd = hasCurl
|
|
9041
9019
|
? `curl -fSL --progress-bar -o "${tmpTarball}" "${releaseUrl}"`
|
|
9042
9020
|
: `wget --progress=bar:force -O "${tmpTarball}" "${releaseUrl}"`;
|
|
9043
|
-
|
|
9044
|
-
execSync(dlCmd, { stdio: 'inherit', timeout: 600000 });
|
|
9021
|
+
execSync(dlCmd, { stdio: 'pipe', timeout: 900000 }); // 15 min timeout for ~570MB
|
|
9045
9022
|
} catch (e) {
|
|
9046
|
-
|
|
9047
|
-
|
|
9023
|
+
initLog(`[MODELS] Download failed: ${e.message}`);
|
|
9024
|
+
if (ui) ui.setSubStatus(`⚠ Model download failed — mini-COT/translation disabled`);
|
|
9048
9025
|
try { fs.unlinkSync(tmpTarball); } catch {}
|
|
9049
9026
|
return;
|
|
9050
9027
|
}
|
|
9051
9028
|
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
initLog('[MODELS] Extracting to: ' + modelsDir);
|
|
9029
|
+
if (ui) ui.setSubStatus('⤳ Extracting ML models...');
|
|
9030
|
+
initLog(`[MODELS] Extracting to: ${modelsDir}`);
|
|
9055
9031
|
|
|
9056
9032
|
try {
|
|
9057
|
-
// Ensure models directory exists
|
|
9058
9033
|
const mkdirCmd = `mkdir -p "${modelsDir}"`;
|
|
9059
9034
|
const extractCmd = `tar xzf "${tmpTarball}" -C "${modelsDir}"`;
|
|
9060
|
-
|
|
9061
9035
|
if (needsSudo) {
|
|
9062
9036
|
execSync(`sudo ${mkdirCmd}`, { stdio: 'pipe' });
|
|
9063
|
-
execSync(`sudo ${extractCmd}`, { stdio: '
|
|
9064
|
-
// Fix ownership so specmem can read
|
|
9037
|
+
execSync(`sudo ${extractCmd}`, { stdio: 'pipe', timeout: 180000 });
|
|
9065
9038
|
execSync(`sudo chmod -R a+rX "${modelsDir}"`, { stdio: 'pipe' });
|
|
9066
9039
|
} else {
|
|
9067
9040
|
execSync(mkdirCmd, { stdio: 'pipe' });
|
|
9068
|
-
execSync(extractCmd, { stdio: '
|
|
9041
|
+
execSync(extractCmd, { stdio: 'pipe', timeout: 180000 });
|
|
9069
9042
|
}
|
|
9070
9043
|
} catch (e) {
|
|
9071
|
-
|
|
9072
|
-
|
|
9044
|
+
initLog(`[MODELS] Extraction failed: ${e.message}`);
|
|
9045
|
+
if (ui) ui.setSubStatus('⚠ Model extraction failed');
|
|
9073
9046
|
try { fs.unlinkSync(tmpTarball); } catch {}
|
|
9074
9047
|
return;
|
|
9075
9048
|
}
|
|
9076
9049
|
|
|
9077
|
-
// Cleanup tarball
|
|
9078
9050
|
try { fs.unlinkSync(tmpTarball); } catch {}
|
|
9079
9051
|
|
|
9080
|
-
|
|
9081
|
-
if (
|
|
9082
|
-
|
|
9083
|
-
|
|
9052
|
+
const stillMissing = sentinels.filter(s => !fs.existsSync(s.path));
|
|
9053
|
+
if (stillMissing.length === 0) {
|
|
9054
|
+
initLog('[MODELS] All models OK after download');
|
|
9055
|
+
if (ui) ui.setSubStatus('✓ ML models downloaded and ready');
|
|
9084
9056
|
} else {
|
|
9085
|
-
|
|
9086
|
-
|
|
9057
|
+
initLog(`[MODELS] Still missing after download: ${stillMissing.map(s => s.name).join(', ')}`);
|
|
9058
|
+
if (ui) ui.setSubStatus(`⚠ Still missing: ${stillMissing.map(s => s.name).join(', ')}`);
|
|
9087
9059
|
}
|
|
9088
9060
|
}
|
|
9089
9061
|
|
|
@@ -9193,9 +9165,6 @@ async function main() {
|
|
|
9193
9165
|
}
|
|
9194
9166
|
}
|
|
9195
9167
|
|
|
9196
|
-
// ========== ENSURE ML MODELS ==========
|
|
9197
|
-
await ensureModels();
|
|
9198
|
-
|
|
9199
9168
|
// Animated banner with sliding red highlight (screen already cleared at startup)
|
|
9200
9169
|
const _dbg = (m) => { try { fs.appendFileSync('/tmp/init-trace.log', `${Date.now()} ${m}\n`); } catch {} };
|
|
9201
9170
|
_dbg('PRE-BANNER');
|
|
@@ -9858,16 +9827,22 @@ ${lastOutput}
|
|
|
9858
9827
|
|
|
9859
9828
|
// Adjust total stages based on mode
|
|
9860
9829
|
if (!launchScreens) {
|
|
9861
|
-
ui.totalStages =
|
|
9830
|
+
ui.totalStages = 10; // No screen sessions stage
|
|
9862
9831
|
} else if (skipScorchedEarth) {
|
|
9863
|
-
ui.totalStages =
|
|
9832
|
+
ui.totalStages = 3; // Quick mode: models + analyze + screens
|
|
9864
9833
|
} else {
|
|
9865
|
-
ui.totalStages =
|
|
9834
|
+
ui.totalStages = 11; // Full mode with screen sessions
|
|
9866
9835
|
}
|
|
9867
9836
|
|
|
9868
9837
|
ui.start();
|
|
9869
9838
|
_dbg('POST-UI-START');
|
|
9870
9839
|
|
|
9840
|
+
// ==========================================================================
|
|
9841
|
+
// STAGE 1: MODEL DOWNLOAD — auto-fetch any missing ML models
|
|
9842
|
+
// ==========================================================================
|
|
9843
|
+
ui.setStage(1, 'MODEL DOWNLOAD');
|
|
9844
|
+
await ensureModels(ui);
|
|
9845
|
+
|
|
9871
9846
|
// ==========================================================================
|
|
9872
9847
|
// CONTAINER FAST-PATH: If podman/docker is available, use container mode
|
|
9873
9848
|
// This replaces stages 2-9 with: pull image → start container → deploy hooks → verify
|
|
@@ -9953,14 +9928,14 @@ ${lastOutput}
|
|
|
9953
9928
|
}
|
|
9954
9929
|
|
|
9955
9930
|
if (useContainerMode && !skipScorchedEarth) {
|
|
9956
|
-
// Container mode:
|
|
9957
|
-
ui.totalStages = launchScreens ?
|
|
9931
|
+
// Container mode: Stage 1 = models, then container stages
|
|
9932
|
+
ui.totalStages = launchScreens ? 8 : 7;
|
|
9958
9933
|
|
|
9959
|
-
// Stage
|
|
9934
|
+
// Stage 2: Analyze project
|
|
9960
9935
|
const analysis = await analyzeProject(projectPath, ui);
|
|
9961
9936
|
|
|
9962
|
-
// Stage
|
|
9963
|
-
ui.setStage(
|
|
9937
|
+
// Stage 3: Container engine — pull image if needed
|
|
9938
|
+
ui.setStage(3, 'CONTAINER ENGINE');
|
|
9964
9939
|
ui.setStatus('Pulling specmem-brain image...');
|
|
9965
9940
|
const containerImage = process.env.SPECMEM_CONTAINER_IMAGE || 'ghcr.io/hardwicksoftware/specmem-brain:latest';
|
|
9966
9941
|
const rtCmd = (() => {
|
|
@@ -10012,8 +9987,8 @@ ${lastOutput}
|
|
|
10012
9987
|
console.log('');
|
|
10013
9988
|
process.exit(1);
|
|
10014
9989
|
} else {
|
|
10015
|
-
// Stage
|
|
10016
|
-
ui.setStage(
|
|
9990
|
+
// Stage 4: Start container
|
|
9991
|
+
ui.setStage(4, 'CONTAINER START');
|
|
10017
9992
|
ui.setStatus('Starting specmem-brain...');
|
|
10018
9993
|
const dirName = path.basename(projectPath).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
10019
9994
|
const containerName = `specmem-brain-${dirName}`;
|
|
@@ -10412,8 +10387,8 @@ priority=999
|
|
|
10412
10387
|
process.env.SPECMEM_TRANSLATE_SOCKET = path.join(runDir, 'translate.sock');
|
|
10413
10388
|
process.env.SPECMEM_MINICOT_SOCKET = path.join(runDir, 'minicot.sock');
|
|
10414
10389
|
|
|
10415
|
-
// Stage
|
|
10416
|
-
ui.setStage(
|
|
10390
|
+
// Stage 5: PostgreSQL client tools (psql) — hooks shell out to psql for DB queries
|
|
10391
|
+
ui.setStage(5, 'PSQL CLIENT');
|
|
10417
10392
|
ui.setStatus('Checking psql...');
|
|
10418
10393
|
|
|
10419
10394
|
let hasPsqlClient = false;
|
|
@@ -10512,8 +10487,8 @@ priority=999
|
|
|
10512
10487
|
console.log(` ${c.dim}Hooks that query the DB will fail without it.${c.reset}`);
|
|
10513
10488
|
}
|
|
10514
10489
|
|
|
10515
|
-
// Stage
|
|
10516
|
-
ui.setStage(
|
|
10490
|
+
// Stage 6: Deploy hooks + MCP config + verify
|
|
10491
|
+
ui.setStage(6, 'HOOKS & CONFIG');
|
|
10517
10492
|
ui.setStatus('Deploying Claude hooks...');
|
|
10518
10493
|
syncHooksAndSettings();
|
|
10519
10494
|
ui.setStatus('Hooks deployed');
|
|
@@ -10619,9 +10594,9 @@ priority=999
|
|
|
10619
10594
|
}, null, 2));
|
|
10620
10595
|
} catch {}
|
|
10621
10596
|
|
|
10622
|
-
// Stage
|
|
10623
|
-
_dbg('STAGE-
|
|
10624
|
-
ui.setStage(
|
|
10597
|
+
// Stage 7: CODEBASE INDEXING
|
|
10598
|
+
_dbg('STAGE-7-CODEBASE-INDEXING');
|
|
10599
|
+
ui.setStage(7, 'CODEBASE INDEXING');
|
|
10625
10600
|
ui.enableFileFeed(true);
|
|
10626
10601
|
ui.setStatus('Waiting for container services...');
|
|
10627
10602
|
|
package/specmem/supervisord.conf
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
; ============================================
|
|
2
2
|
; SPECMEM BRAIN CONTAINER - DYNAMIC SUPERVISORD CONFIG
|
|
3
|
-
; Generated by specmem-init at 2026-02-
|
|
3
|
+
; Generated by specmem-init at 2026-02-22T17:39:56.598Z
|
|
4
4
|
; Thread counts from model-config.json resourcePool
|
|
5
5
|
; ============================================
|
|
6
6
|
|