@stackmemoryai/stackmemory 1.3.0 → 1.3.1
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.
|
@@ -292,11 +292,16 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
292
292
|
logger.info("FTS5 index rebuilt");
|
|
293
293
|
}
|
|
294
294
|
/**
|
|
295
|
-
* Incremental garbage collection
|
|
296
|
-
*
|
|
295
|
+
* Incremental garbage collection with generational compression.
|
|
296
|
+
*
|
|
297
|
+
* Phase 1 — Compress: Apply generational strategies to mature/old frames.
|
|
298
|
+
* - 'digest_only': Strip inputs/outputs/events, keep digest + anchors
|
|
299
|
+
* - 'anchors_only': Strip inputs/outputs/events/digest_json, keep digest_text + anchors
|
|
300
|
+
* - 'keep_all': No compression
|
|
301
|
+
*
|
|
302
|
+
* Phase 2 — Delete: Remove frames past retention cutoffs.
|
|
297
303
|
* - 'keep_forever': never deleted
|
|
298
|
-
* - 'default': deleted after retentionDays (default 90)
|
|
299
|
-
* - 'archive': same as default
|
|
304
|
+
* - 'default'/'archive': deleted after retentionDays (default 90)
|
|
300
305
|
* - 'ttl_30d': deleted after 30 days
|
|
301
306
|
* - 'ttl_7d': deleted after 7 days
|
|
302
307
|
*/
|
|
@@ -314,6 +319,62 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
314
319
|
const defaultCutoff = nowSec - retentionDays * 86400;
|
|
315
320
|
const ttl30dCutoff = nowSec - 30 * 86400;
|
|
316
321
|
const ttl7dCutoff = nowSec - 7 * 86400;
|
|
322
|
+
let framesCompressed = 0;
|
|
323
|
+
const gc = options.generationalGc;
|
|
324
|
+
if (gc && !dryRun) {
|
|
325
|
+
const youngDays = gc.youngCutoffDays ?? 1;
|
|
326
|
+
const matureDays = gc.matureCutoffDays ?? 7;
|
|
327
|
+
const oldDays = gc.oldCutoffDays ?? 30;
|
|
328
|
+
const youngCutoff = nowSec - youngDays * 86400;
|
|
329
|
+
const matureCutoff = nowSec - matureDays * 86400;
|
|
330
|
+
const matureStrategy = gc.mature_strategy ?? "digest_only";
|
|
331
|
+
if (matureStrategy !== "keep_all") {
|
|
332
|
+
const matureFrames = this.db.prepare(
|
|
333
|
+
`SELECT frame_id FROM frames
|
|
334
|
+
WHERE created_at < ? AND created_at >= ?
|
|
335
|
+
AND state = 'closed'
|
|
336
|
+
AND retention_policy != 'keep_forever'
|
|
337
|
+
AND inputs != '{}'
|
|
338
|
+
AND run_id NOT IN (SELECT value FROM json_each(?))
|
|
339
|
+
LIMIT ?`
|
|
340
|
+
).all(
|
|
341
|
+
youngCutoff,
|
|
342
|
+
matureCutoff,
|
|
343
|
+
JSON.stringify(protectedRunIds),
|
|
344
|
+
batchSize
|
|
345
|
+
);
|
|
346
|
+
if (matureFrames.length > 0) {
|
|
347
|
+
framesCompressed += this.compressFrames(
|
|
348
|
+
matureFrames.map((f) => f.frame_id),
|
|
349
|
+
matureStrategy
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const oldStrategy = gc.old_strategy ?? "anchors_only";
|
|
354
|
+
if (oldStrategy !== "keep_all") {
|
|
355
|
+
const oldCutoff = nowSec - oldDays * 86400;
|
|
356
|
+
const oldFrames = this.db.prepare(
|
|
357
|
+
`SELECT frame_id FROM frames
|
|
358
|
+
WHERE created_at < ? AND created_at >= ?
|
|
359
|
+
AND state = 'closed'
|
|
360
|
+
AND retention_policy != 'keep_forever'
|
|
361
|
+
AND inputs != '{}'
|
|
362
|
+
AND run_id NOT IN (SELECT value FROM json_each(?))
|
|
363
|
+
LIMIT ?`
|
|
364
|
+
).all(
|
|
365
|
+
matureCutoff,
|
|
366
|
+
oldCutoff,
|
|
367
|
+
JSON.stringify(protectedRunIds),
|
|
368
|
+
batchSize
|
|
369
|
+
);
|
|
370
|
+
if (oldFrames.length > 0) {
|
|
371
|
+
framesCompressed += this.compressFrames(
|
|
372
|
+
oldFrames.map((f) => f.frame_id),
|
|
373
|
+
oldStrategy
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
317
378
|
const candidates = this.db.prepare(
|
|
318
379
|
`SELECT frame_id FROM frames
|
|
319
380
|
WHERE (
|
|
@@ -340,7 +401,8 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
340
401
|
eventsDeleted: 0,
|
|
341
402
|
anchorsDeleted: 0,
|
|
342
403
|
embeddingsDeleted: 0,
|
|
343
|
-
ftsEntriesDeleted: 0
|
|
404
|
+
ftsEntriesDeleted: 0,
|
|
405
|
+
framesCompressed
|
|
344
406
|
};
|
|
345
407
|
}
|
|
346
408
|
if (dryRun) {
|
|
@@ -362,8 +424,9 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
362
424
|
eventsDeleted: eventsCount,
|
|
363
425
|
anchorsDeleted: anchorsCount,
|
|
364
426
|
embeddingsDeleted: embeddingsCount,
|
|
365
|
-
ftsEntriesDeleted: frameIds.length
|
|
427
|
+
ftsEntriesDeleted: frameIds.length,
|
|
366
428
|
// FTS has one entry per frame
|
|
429
|
+
framesCompressed
|
|
367
430
|
};
|
|
368
431
|
}
|
|
369
432
|
const placeholders = frameIds.map(() => "?").join(",");
|
|
@@ -392,14 +455,16 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
392
455
|
framesDeleted: frameIds.length,
|
|
393
456
|
eventsDeleted,
|
|
394
457
|
anchorsDeleted,
|
|
395
|
-
embeddingsDeleted
|
|
458
|
+
embeddingsDeleted,
|
|
459
|
+
framesCompressed
|
|
396
460
|
});
|
|
397
461
|
return {
|
|
398
462
|
framesDeleted: frameIds.length,
|
|
399
463
|
eventsDeleted,
|
|
400
464
|
anchorsDeleted,
|
|
401
465
|
embeddingsDeleted,
|
|
402
|
-
ftsEntriesDeleted: frameIds.length
|
|
466
|
+
ftsEntriesDeleted: frameIds.length,
|
|
467
|
+
framesCompressed
|
|
403
468
|
};
|
|
404
469
|
}
|
|
405
470
|
/**
|
|
@@ -432,6 +497,85 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
432
497
|
if (nowSec - frame.created_at < 86400) score += 0.1;
|
|
433
498
|
return Math.round(Math.min(score, 1) * 100) / 100;
|
|
434
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Compress a frame by stripping data according to strategy.
|
|
502
|
+
*
|
|
503
|
+
* - 'digest_only': Remove inputs, outputs, events. Keep digest_text, digest_json, anchors.
|
|
504
|
+
* - 'anchors_only': Remove inputs, outputs, events, digest_json. Keep digest_text, anchors.
|
|
505
|
+
*
|
|
506
|
+
* Returns true if the frame was compressed, false if not found.
|
|
507
|
+
*/
|
|
508
|
+
compressFrame(frameId, strategy) {
|
|
509
|
+
if (!this.db)
|
|
510
|
+
throw new DatabaseError(
|
|
511
|
+
"Database not connected",
|
|
512
|
+
ErrorCode.DB_CONNECTION_FAILED
|
|
513
|
+
);
|
|
514
|
+
const frame = this.db.prepare("SELECT frame_id FROM frames WHERE frame_id = ?").get(frameId);
|
|
515
|
+
if (!frame) return false;
|
|
516
|
+
this.db.prepare("BEGIN").run();
|
|
517
|
+
try {
|
|
518
|
+
if (strategy === "digest_only") {
|
|
519
|
+
this.db.prepare(
|
|
520
|
+
"UPDATE frames SET inputs = '{}', outputs = '{}' WHERE frame_id = ?"
|
|
521
|
+
).run(frameId);
|
|
522
|
+
} else {
|
|
523
|
+
this.db.prepare(
|
|
524
|
+
"UPDATE frames SET inputs = '{}', outputs = '{}', digest_json = '{}' WHERE frame_id = ?"
|
|
525
|
+
).run(frameId);
|
|
526
|
+
}
|
|
527
|
+
this.db.prepare("DELETE FROM events WHERE frame_id = ?").run(frameId);
|
|
528
|
+
if (this.vecEnabled) {
|
|
529
|
+
this.db.prepare("DELETE FROM frame_embeddings WHERE frame_id = ?").run(frameId);
|
|
530
|
+
}
|
|
531
|
+
this.db.prepare("COMMIT").run();
|
|
532
|
+
return true;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
this.db.prepare("ROLLBACK").run();
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Compress multiple frames in a single transaction.
|
|
540
|
+
* Returns count of frames compressed.
|
|
541
|
+
*/
|
|
542
|
+
compressFrames(frameIds, strategy) {
|
|
543
|
+
if (!this.db || frameIds.length === 0) return 0;
|
|
544
|
+
const placeholders = frameIds.map(() => "?").join(",");
|
|
545
|
+
this.db.prepare("BEGIN").run();
|
|
546
|
+
try {
|
|
547
|
+
if (strategy === "digest_only") {
|
|
548
|
+
this.db.prepare(
|
|
549
|
+
`UPDATE frames SET inputs = '{}', outputs = '{}' WHERE frame_id IN (${placeholders})`
|
|
550
|
+
).run(...frameIds);
|
|
551
|
+
} else {
|
|
552
|
+
this.db.prepare(
|
|
553
|
+
`UPDATE frames SET inputs = '{}', outputs = '{}', digest_json = '{}' WHERE frame_id IN (${placeholders})`
|
|
554
|
+
).run(...frameIds);
|
|
555
|
+
}
|
|
556
|
+
this.db.prepare(`DELETE FROM events WHERE frame_id IN (${placeholders})`).run(...frameIds);
|
|
557
|
+
if (this.vecEnabled) {
|
|
558
|
+
this.db.prepare(
|
|
559
|
+
`DELETE FROM frame_embeddings WHERE frame_id IN (${placeholders})`
|
|
560
|
+
).run(...frameIds);
|
|
561
|
+
}
|
|
562
|
+
this.db.prepare("COMMIT").run();
|
|
563
|
+
return frameIds.length;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
this.db.prepare("ROLLBACK").run();
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Get database file size in bytes.
|
|
571
|
+
*/
|
|
572
|
+
getDatabaseSize() {
|
|
573
|
+
if (!this.db) return 0;
|
|
574
|
+
const result = this.db.prepare(
|
|
575
|
+
"SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"
|
|
576
|
+
).get();
|
|
577
|
+
return result?.size ?? 0;
|
|
578
|
+
}
|
|
435
579
|
/**
|
|
436
580
|
* Recompute importance scores for frames still at default score (0.5).
|
|
437
581
|
* Processes oldest frames first in batches.
|
|
@@ -4,8 +4,7 @@ const __filename = __fileURLToPath(import.meta.url);
|
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { logger } from "../../core/monitoring/logger.js";
|
|
6
6
|
import { STRUCTURED_RESPONSE_SUFFIX } from "../../orchestrators/multimodal/constants.js";
|
|
7
|
-
import {
|
|
8
|
-
import { promisify } from "util";
|
|
7
|
+
import { spawn } from "child_process";
|
|
9
8
|
import * as fs from "fs";
|
|
10
9
|
import * as path from "path";
|
|
11
10
|
import * as os from "os";
|
|
@@ -18,12 +17,11 @@ import {
|
|
|
18
17
|
createProvider
|
|
19
18
|
} from "../../core/extensions/provider-adapter.js";
|
|
20
19
|
import { AnthropicBatchClient } from "../anthropic/batch-client.js";
|
|
21
|
-
const execAsync = promisify(exec);
|
|
22
20
|
class ClaudeCodeSubagentClient {
|
|
23
21
|
tempDir;
|
|
24
22
|
activeSubagents = /* @__PURE__ */ new Map();
|
|
25
23
|
mockMode;
|
|
26
|
-
constructor(mockMode =
|
|
24
|
+
constructor(mockMode = false) {
|
|
27
25
|
this.mockMode = mockMode;
|
|
28
26
|
this.tempDir = path.join(os.tmpdir(), "stackmemory-rlm");
|
|
29
27
|
if (!fs.existsSync(this.tempDir)) {
|
|
@@ -148,7 +146,8 @@ class ClaudeCodeSubagentClient {
|
|
|
148
146
|
}
|
|
149
147
|
}
|
|
150
148
|
/**
|
|
151
|
-
*
|
|
149
|
+
* Execute subagent via Claude Code CLI (`claude -p --output-format stream-json`).
|
|
150
|
+
* Spawns a real Claude Code process with full tool use.
|
|
152
151
|
*/
|
|
153
152
|
async executeSubagentViaCLI(request, startTime, subagentId) {
|
|
154
153
|
try {
|
|
@@ -158,31 +157,24 @@ class ClaudeCodeSubagentClient {
|
|
|
158
157
|
contextFile,
|
|
159
158
|
JSON.stringify(request.context, null, 2)
|
|
160
159
|
);
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
contextFile,
|
|
166
|
-
resultFile
|
|
167
|
-
);
|
|
168
|
-
const result = await this.executeTaskTool(taskCommand, request.timeout);
|
|
169
|
-
let subagentResult = {};
|
|
170
|
-
if (fs.existsSync(resultFile)) {
|
|
171
|
-
const resultContent = await fs.promises.readFile(resultFile, "utf-8");
|
|
172
|
-
try {
|
|
173
|
-
subagentResult = JSON.parse(resultContent);
|
|
174
|
-
} catch {
|
|
175
|
-
subagentResult = { rawOutput: resultContent };
|
|
176
|
-
}
|
|
177
|
-
}
|
|
160
|
+
const fullPrompt = `${prompt}
|
|
161
|
+
|
|
162
|
+
Context (JSON): ${JSON.stringify(request.context)}`;
|
|
163
|
+
const result = await this.spawnClaude(fullPrompt, request.timeout);
|
|
178
164
|
this.cleanup(subagentId);
|
|
165
|
+
let parsed;
|
|
166
|
+
try {
|
|
167
|
+
parsed = JSON.parse(result.text);
|
|
168
|
+
} catch {
|
|
169
|
+
parsed = { rawOutput: result.text };
|
|
170
|
+
}
|
|
179
171
|
return {
|
|
180
172
|
success: true,
|
|
181
|
-
result:
|
|
182
|
-
output: result.
|
|
173
|
+
result: parsed,
|
|
174
|
+
output: result.text,
|
|
183
175
|
duration: Date.now() - startTime,
|
|
184
176
|
subagentType: request.type,
|
|
185
|
-
tokens: this.estimateTokens(
|
|
177
|
+
tokens: this.estimateTokens(fullPrompt + result.text)
|
|
186
178
|
};
|
|
187
179
|
} catch (error) {
|
|
188
180
|
logger.error(`Subagent CLI execution failed: ${request.type}`, {
|
|
@@ -347,66 +339,94 @@ class ClaudeCodeSubagentClient {
|
|
|
347
339
|
return (request.systemPrompt || prompts[request.type] || prompts.planning) + STRUCTURED_RESPONSE_SUFFIX;
|
|
348
340
|
}
|
|
349
341
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
342
|
+
* Spawn `claude -p --output-format stream-json` and collect the result.
|
|
343
|
+
* Parses stream-json events to extract the final assistant text.
|
|
352
344
|
*/
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
# The subagent will generate test files and run them
|
|
367
|
-
;;
|
|
368
|
-
"linting")
|
|
369
|
-
# For linting subagent, run actual linters
|
|
370
|
-
echo "Running linters..."
|
|
371
|
-
npm run lint || true
|
|
372
|
-
;;
|
|
373
|
-
"code")
|
|
374
|
-
# For code generation, create implementation files
|
|
375
|
-
echo "Generating implementation..."
|
|
376
|
-
;;
|
|
377
|
-
*)
|
|
378
|
-
# Default behavior
|
|
379
|
-
echo "Executing ${request.type} task..."
|
|
380
|
-
;;
|
|
381
|
-
esac
|
|
382
|
-
|
|
383
|
-
# Write result
|
|
384
|
-
echo '{"status": "completed", "type": "${request.type}"}' > "${resultFile}"
|
|
385
|
-
`;
|
|
386
|
-
const scriptFile = path.join(this.tempDir, `${request.type}-script.sh`);
|
|
387
|
-
fs.writeFileSync(scriptFile, scriptContent);
|
|
388
|
-
fs.chmodSync(scriptFile, "755");
|
|
389
|
-
return scriptFile;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Execute via Task tool (simulated for now)
|
|
393
|
-
* In production, this would use Claude Code's actual Task tool API
|
|
394
|
-
*/
|
|
395
|
-
async executeTaskTool(command, timeout) {
|
|
396
|
-
try {
|
|
397
|
-
const result = await execAsync(command, {
|
|
398
|
-
timeout: timeout || 3e5,
|
|
399
|
-
// 5 minutes default
|
|
400
|
-
maxBuffer: 10 * 1024 * 1024
|
|
401
|
-
// 10MB buffer
|
|
345
|
+
spawnClaude(prompt, timeout) {
|
|
346
|
+
return new Promise((resolve, reject) => {
|
|
347
|
+
const args = [
|
|
348
|
+
"-p",
|
|
349
|
+
"--output-format",
|
|
350
|
+
"stream-json",
|
|
351
|
+
"--dangerously-skip-permissions",
|
|
352
|
+
prompt
|
|
353
|
+
];
|
|
354
|
+
const claude = spawn("claude", args, {
|
|
355
|
+
cwd: process.cwd(),
|
|
356
|
+
env: { ...process.env },
|
|
357
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
402
358
|
});
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
359
|
+
const timeoutMs = timeout || 3e5;
|
|
360
|
+
const timer = setTimeout(() => {
|
|
361
|
+
claude.kill("SIGTERM");
|
|
362
|
+
reject(new Error(`Subagent timeout after ${timeoutMs}ms`));
|
|
363
|
+
}, timeoutMs);
|
|
364
|
+
let lastAssistantText = "";
|
|
365
|
+
let toolUseCount = 0;
|
|
366
|
+
let lineBuffer = "";
|
|
367
|
+
let stderr = "";
|
|
368
|
+
claude.stdout.on("data", (chunk) => {
|
|
369
|
+
lineBuffer += chunk.toString();
|
|
370
|
+
const lines = lineBuffer.split("\n");
|
|
371
|
+
lineBuffer = lines.pop() || "";
|
|
372
|
+
for (const line of lines) {
|
|
373
|
+
if (!line.trim()) continue;
|
|
374
|
+
try {
|
|
375
|
+
const event = JSON.parse(line);
|
|
376
|
+
if (event.type === "assistant" && event.message) {
|
|
377
|
+
const textBlocks = (event.message.content || []).filter((b) => b.type === "text").map((b) => b.text);
|
|
378
|
+
if (textBlocks.length > 0) {
|
|
379
|
+
lastAssistantText = textBlocks.join("\n");
|
|
380
|
+
}
|
|
381
|
+
const toolBlocks = (event.message.content || []).filter(
|
|
382
|
+
(b) => b.type === "tool_use"
|
|
383
|
+
);
|
|
384
|
+
toolUseCount += toolBlocks.length;
|
|
385
|
+
}
|
|
386
|
+
if (event.type === "result" && event.result) {
|
|
387
|
+
lastAssistantText = event.result;
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
claude.stderr.on("data", (data) => {
|
|
394
|
+
stderr += data.toString();
|
|
395
|
+
});
|
|
396
|
+
claude.on("close", (code) => {
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
if (lineBuffer.trim()) {
|
|
399
|
+
try {
|
|
400
|
+
const event = JSON.parse(lineBuffer);
|
|
401
|
+
if (event.type === "result" && event.result) {
|
|
402
|
+
lastAssistantText = event.result;
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
logger.info("Claude subagent completed", {
|
|
408
|
+
code,
|
|
409
|
+
toolUseCount,
|
|
410
|
+
outputLength: lastAssistantText.length
|
|
411
|
+
});
|
|
412
|
+
if (code === 0 && lastAssistantText) {
|
|
413
|
+
resolve({ text: lastAssistantText, toolUseCount });
|
|
414
|
+
} else if (code === 0) {
|
|
415
|
+
resolve({
|
|
416
|
+
text: "(Claude completed but produced no text output)",
|
|
417
|
+
toolUseCount
|
|
418
|
+
});
|
|
419
|
+
} else {
|
|
420
|
+
reject(
|
|
421
|
+
new Error(`Claude exited code ${code}: ${stderr.slice(0, 500)}`)
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
claude.on("error", (err) => {
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
428
|
+
});
|
|
429
|
+
});
|
|
410
430
|
}
|
|
411
431
|
/**
|
|
412
432
|
* Get mock response for testing
|
|
@@ -3,6 +3,7 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { v4 as uuidv4 } from "uuid";
|
|
6
|
+
import { spawn } from "child_process";
|
|
6
7
|
import { logger } from "../../core/monitoring/logger.js";
|
|
7
8
|
class ClaudeCodeTaskCoordinator {
|
|
8
9
|
activeTasks = /* @__PURE__ */ new Map();
|
|
@@ -205,26 +206,68 @@ class ClaudeCodeTaskCoordinator {
|
|
|
205
206
|
agentType: agentConfig.type,
|
|
206
207
|
promptTokens: this.estimateTokenUsage(prompt, "")
|
|
207
208
|
});
|
|
208
|
-
return this.
|
|
209
|
+
return this.spawnClaudeCode(agentName, prompt, agentConfig);
|
|
209
210
|
}
|
|
210
211
|
/**
|
|
211
|
-
*
|
|
212
|
+
* Spawn Claude Code CLI as a subprocess.
|
|
213
|
+
* Uses `claude --print` for non-interactive execution with stdout capture.
|
|
214
|
+
* Workspace cwd is inherited from the coordinator's process.
|
|
212
215
|
*/
|
|
213
|
-
|
|
214
|
-
return new Promise((resolve) => {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
216
|
+
spawnClaudeCode(agentName, prompt, agentConfig) {
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const args = ["--print"];
|
|
219
|
+
if (agentConfig.type === "oracle") {
|
|
220
|
+
args.push("--model", "opus");
|
|
221
|
+
}
|
|
222
|
+
if (agentConfig.capabilities.includes("code_implementation")) {
|
|
223
|
+
args.push("--allowedTools", "Edit,Write,Bash,Read,Glob,Grep");
|
|
224
|
+
} else {
|
|
225
|
+
args.push("--allowedTools", "Read,Glob,Grep,Bash");
|
|
226
|
+
}
|
|
227
|
+
args.push(prompt);
|
|
228
|
+
logger.info("Spawning claude CLI", {
|
|
229
|
+
agentName,
|
|
230
|
+
agentType: agentConfig.type,
|
|
231
|
+
cwd: process.cwd(),
|
|
232
|
+
promptLength: prompt.length
|
|
233
|
+
});
|
|
234
|
+
const proc = spawn("claude", args, {
|
|
235
|
+
cwd: process.cwd(),
|
|
236
|
+
env: { ...process.env },
|
|
237
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
238
|
+
});
|
|
239
|
+
let stdout = "";
|
|
240
|
+
let stderr = "";
|
|
241
|
+
proc.stdout.on("data", (data) => {
|
|
242
|
+
stdout += data.toString();
|
|
243
|
+
});
|
|
244
|
+
proc.stderr.on("data", (data) => {
|
|
245
|
+
stderr += data.toString();
|
|
246
|
+
});
|
|
247
|
+
proc.on("error", (err) => {
|
|
248
|
+
logger.error("Failed to spawn claude CLI", {
|
|
249
|
+
agentName,
|
|
250
|
+
error: err.message
|
|
251
|
+
});
|
|
252
|
+
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
253
|
+
});
|
|
254
|
+
proc.on("close", (code) => {
|
|
255
|
+
if (code === 0) {
|
|
256
|
+
logger.debug("Claude CLI completed", {
|
|
257
|
+
agentName,
|
|
258
|
+
outputLength: stdout.length
|
|
259
|
+
});
|
|
260
|
+
resolve(stdout.trim());
|
|
261
|
+
} else {
|
|
262
|
+
const errMsg = stderr.slice(0, 500) || `exit code ${code}`;
|
|
263
|
+
logger.warn("Claude CLI failed", {
|
|
264
|
+
agentName,
|
|
265
|
+
exitCode: code,
|
|
266
|
+
stderr: errMsg
|
|
267
|
+
});
|
|
268
|
+
reject(new Error(`Claude CLI exited with code ${code}: ${errMsg}`));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
228
271
|
});
|
|
229
272
|
}
|
|
230
273
|
/**
|
|
@@ -2,11 +2,19 @@ import { fileURLToPath as __fileURLToPath } from 'url';
|
|
|
2
2
|
import { dirname as __pathDirname } from 'path';
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { LinearClient } from "../../linear/client.js";
|
|
5
6
|
import { logger } from "../../../core/monitoring/logger.js";
|
|
6
7
|
class LinearHandlers {
|
|
7
8
|
constructor(deps) {
|
|
8
9
|
this.deps = deps;
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Create an authenticated LinearClient from the auth manager token
|
|
13
|
+
*/
|
|
14
|
+
async getClient() {
|
|
15
|
+
const token = await this.deps.linearAuthManager.getValidToken();
|
|
16
|
+
return new LinearClient({ apiKey: token, useBearer: true });
|
|
17
|
+
}
|
|
10
18
|
/**
|
|
11
19
|
* Sync tasks with Linear
|
|
12
20
|
*/
|
|
@@ -67,7 +75,7 @@ class LinearHandlers {
|
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
/**
|
|
70
|
-
* Update Linear
|
|
78
|
+
* Update Linear issue directly via GraphQL API
|
|
71
79
|
*/
|
|
72
80
|
async handleLinearUpdateTask(args) {
|
|
73
81
|
try {
|
|
@@ -75,27 +83,30 @@ class LinearHandlers {
|
|
|
75
83
|
if (!linear_id) {
|
|
76
84
|
throw new Error("Linear ID is required");
|
|
77
85
|
}
|
|
78
|
-
|
|
79
|
-
await this.deps.linearAuthManager.getValidToken();
|
|
80
|
-
} catch {
|
|
81
|
-
throw new Error("Linear authentication required");
|
|
82
|
-
}
|
|
86
|
+
const client = await this.getClient();
|
|
83
87
|
const updateData = {};
|
|
84
|
-
if (status)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (assignee_id) {
|
|
88
|
-
updateData.assigneeId = assignee_id;
|
|
89
|
-
}
|
|
90
|
-
if (priority) {
|
|
91
|
-
updateData.priority = priority;
|
|
92
|
-
}
|
|
88
|
+
if (status) updateData.stateId = status;
|
|
89
|
+
if (assignee_id) updateData.assigneeId = assignee_id;
|
|
90
|
+
if (priority !== void 0) updateData.priority = priority;
|
|
93
91
|
if (labels) {
|
|
94
|
-
updateData.
|
|
92
|
+
updateData.labelIds = Array.isArray(labels) ? labels : [labels];
|
|
95
93
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
const issue = await client.updateIssue(linear_id, updateData);
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: `Updated ${issue.identifier}: ${issue.title}
|
|
100
|
+
Status: ${issue.state.name} | Priority: ${issue.priority}`
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
metadata: {
|
|
104
|
+
id: issue.id,
|
|
105
|
+
identifier: issue.identifier,
|
|
106
|
+
state: issue.state.name,
|
|
107
|
+
url: issue.url
|
|
108
|
+
}
|
|
109
|
+
};
|
|
99
110
|
} catch (error) {
|
|
100
111
|
logger.error(
|
|
101
112
|
"Error updating Linear task",
|
|
@@ -105,40 +116,43 @@ class LinearHandlers {
|
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
/**
|
|
108
|
-
* Get
|
|
119
|
+
* Get issues from Linear via GraphQL API
|
|
109
120
|
*/
|
|
110
121
|
async handleLinearGetTasks(args) {
|
|
111
122
|
try {
|
|
112
|
-
const {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} = args;
|
|
119
|
-
try {
|
|
120
|
-
await this.deps.linearAuthManager.getValidToken();
|
|
121
|
-
} catch {
|
|
122
|
-
throw new Error("Linear authentication required");
|
|
123
|
-
}
|
|
124
|
-
const filters = {
|
|
125
|
-
limit
|
|
123
|
+
const { team_id, assignee_id, state = "active", limit = 20 } = args;
|
|
124
|
+
const client = await this.getClient();
|
|
125
|
+
const stateTypeMap = {
|
|
126
|
+
active: "started",
|
|
127
|
+
closed: "completed",
|
|
128
|
+
all: void 0
|
|
126
129
|
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
if (search) {
|
|
137
|
-
filters.search = search;
|
|
138
|
-
}
|
|
139
|
-
throw new Error(
|
|
140
|
-
"Linear issue listing via MCP is not yet implemented. Use `stackmemory linear sync` instead."
|
|
130
|
+
const issues = await client.getIssues({
|
|
131
|
+
teamId: team_id,
|
|
132
|
+
assigneeId: assignee_id,
|
|
133
|
+
stateType: stateTypeMap[state],
|
|
134
|
+
limit
|
|
135
|
+
});
|
|
136
|
+
const issueLines = issues.map(
|
|
137
|
+
(i) => `${i.identifier} [${i.state.name}] ${i.title}${i.assignee ? ` (@${i.assignee.name})` : ""}`
|
|
141
138
|
);
|
|
139
|
+
const text = issues.length > 0 ? `Found ${issues.length} issues:
|
|
140
|
+
${issueLines.join("\n")}` : "No issues found matching filters.";
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text }],
|
|
143
|
+
metadata: {
|
|
144
|
+
count: issues.length,
|
|
145
|
+
issues: issues.map((i) => ({
|
|
146
|
+
id: i.id,
|
|
147
|
+
identifier: i.identifier,
|
|
148
|
+
title: i.title,
|
|
149
|
+
state: i.state.name,
|
|
150
|
+
priority: i.priority,
|
|
151
|
+
assignee: i.assignee?.name,
|
|
152
|
+
url: i.url
|
|
153
|
+
}))
|
|
154
|
+
}
|
|
155
|
+
};
|
|
142
156
|
} catch (error) {
|
|
143
157
|
logger.error(
|
|
144
158
|
"Error getting Linear tasks",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, Claude/Codex/OpenCode wrappers, Linear sync, automatic hooks, and log analysis.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|