@stackmemoryai/stackmemory 1.0.1 → 1.2.0
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/src/cli/commands/audit.js +134 -0
- package/dist/src/cli/commands/bench.js +252 -0
- package/dist/src/cli/commands/dashboard.js +2 -1
- package/dist/src/cli/commands/stats.js +118 -0
- package/dist/src/cli/index.js +6 -0
- package/dist/src/core/config/feature-flags.js +7 -1
- package/dist/src/core/context/enhanced-rehydration.js +24 -5
- package/dist/src/core/extensions/cerebras-adapter.js +28 -0
- package/dist/src/core/extensions/deepinfra-adapter.js +32 -0
- package/dist/src/core/extensions/provider-adapter.js +33 -240
- package/dist/src/core/models/complexity-scorer.js +154 -0
- package/dist/src/core/models/model-router.js +230 -36
- package/dist/src/core/models/provider-pricing.js +63 -0
- package/dist/src/core/models/sensitive-guard.js +112 -0
- package/dist/src/core/monitoring/feedback-loops.js +88 -0
- package/dist/src/hooks/schemas.js +12 -1
- package/dist/src/integrations/anthropic/batch-client.js +256 -0
- package/dist/src/integrations/anthropic/client.js +87 -72
- package/dist/src/integrations/claude-code/subagent-client.js +133 -12
- package/dist/src/integrations/graphiti/client.js +16 -4
- package/dist/src/integrations/mcp/handlers/index.js +25 -1
- package/dist/src/integrations/mcp/handlers/provider-handlers.js +227 -0
- package/dist/src/integrations/mcp/server.js +207 -1
- package/dist/src/integrations/mcp/tool-definitions.js +38 -1
- package/dist/src/orchestrators/multimodal/baselines.js +128 -0
- package/dist/src/orchestrators/multimodal/constants.js +9 -1
- package/dist/src/orchestrators/multimodal/harness.js +86 -6
- package/dist/src/orchestrators/multimodal/providers.js +113 -2
- package/dist/src/skills/recursive-agent-orchestrator.js +15 -10
- package/dist/src/utils/fuzzy-edit.js +162 -0
- package/package.json +5 -1
|
@@ -10,6 +10,40 @@ import {
|
|
|
10
10
|
ModelRouterConfigSchema,
|
|
11
11
|
parseConfigSafe
|
|
12
12
|
} from "../../hooks/schemas.js";
|
|
13
|
+
import { isFeatureEnabled } from "../config/feature-flags.js";
|
|
14
|
+
import { scoreComplexity } from "./complexity-scorer.js";
|
|
15
|
+
import {
|
|
16
|
+
detectSensitiveContent,
|
|
17
|
+
isApprovedProvider
|
|
18
|
+
} from "./sensitive-guard.js";
|
|
19
|
+
const MODEL_TOKEN_LIMITS = {
|
|
20
|
+
// Claude 4.x / 4.5 / 4.6
|
|
21
|
+
"claude-opus-4-6": 2e5,
|
|
22
|
+
"claude-sonnet-4-5-20250929": 2e5,
|
|
23
|
+
"claude-haiku-4-5-20251001": 2e5,
|
|
24
|
+
"claude-sonnet-4-20250514": 2e5,
|
|
25
|
+
// Claude 3.x
|
|
26
|
+
"claude-3-5-sonnet-20241022": 2e5,
|
|
27
|
+
"claude-3-5-haiku-20241022": 2e5,
|
|
28
|
+
"claude-3-opus-20240229": 2e5,
|
|
29
|
+
// OpenAI
|
|
30
|
+
"gpt-4o": 128e3,
|
|
31
|
+
"gpt-4-turbo": 128e3,
|
|
32
|
+
"gpt-4": 8192,
|
|
33
|
+
o1: 2e5,
|
|
34
|
+
"o3-mini": 2e5,
|
|
35
|
+
// Qwen
|
|
36
|
+
"qwen3-max-2025-01-23": 128e3,
|
|
37
|
+
// Cerebras
|
|
38
|
+
"llama-4-scout-17b-16e-instruct": 131072,
|
|
39
|
+
// DeepInfra
|
|
40
|
+
"THUDM/glm-4-9b-chat": 128e3
|
|
41
|
+
};
|
|
42
|
+
const DEFAULT_MODEL_TOKEN_LIMIT = 2e5;
|
|
43
|
+
function getModelTokenLimit(model) {
|
|
44
|
+
if (!model) return DEFAULT_MODEL_TOKEN_LIMIT;
|
|
45
|
+
return MODEL_TOKEN_LIMITS[model] ?? DEFAULT_MODEL_TOKEN_LIMIT;
|
|
46
|
+
}
|
|
13
47
|
const CONFIG_PATH = join(homedir(), ".stackmemory", "model-router.json");
|
|
14
48
|
const DEFAULT_CONFIG = {
|
|
15
49
|
enabled: false,
|
|
@@ -40,6 +74,29 @@ const DEFAULT_CONFIG = {
|
|
|
40
74
|
enable_thinking: true,
|
|
41
75
|
thinking_budget: 1e4
|
|
42
76
|
}
|
|
77
|
+
},
|
|
78
|
+
cerebras: {
|
|
79
|
+
provider: "cerebras",
|
|
80
|
+
model: "llama-4-scout-17b-16e-instruct",
|
|
81
|
+
baseUrl: "https://api.cerebras.ai/v1",
|
|
82
|
+
apiKeyEnv: "CEREBRAS_API_KEY"
|
|
83
|
+
},
|
|
84
|
+
deepinfra: {
|
|
85
|
+
provider: "deepinfra",
|
|
86
|
+
model: "THUDM/glm-4-9b-chat",
|
|
87
|
+
baseUrl: "https://api.deepinfra.com/v1/openai",
|
|
88
|
+
apiKeyEnv: "DEEPINFRA_API_KEY"
|
|
89
|
+
},
|
|
90
|
+
openrouter: {
|
|
91
|
+
provider: "openrouter",
|
|
92
|
+
model: "meta-llama/llama-4-scout",
|
|
93
|
+
baseUrl: "https://openrouter.ai/api",
|
|
94
|
+
apiKeyEnv: "OPENROUTER_API_KEY"
|
|
95
|
+
},
|
|
96
|
+
"anthropic-batch": {
|
|
97
|
+
provider: "anthropic-batch",
|
|
98
|
+
model: "claude-sonnet-4-5-20250929",
|
|
99
|
+
apiKeyEnv: "ANTHROPIC_API_KEY"
|
|
43
100
|
}
|
|
44
101
|
},
|
|
45
102
|
thinkingMode: {
|
|
@@ -49,28 +106,43 @@ const DEFAULT_CONFIG = {
|
|
|
49
106
|
topP: 0.95
|
|
50
107
|
}
|
|
51
108
|
};
|
|
109
|
+
let _configCache = null;
|
|
110
|
+
const CONFIG_CACHE_TTL_MS = 5e3;
|
|
52
111
|
function loadModelRouterConfig() {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (_configCache && now < _configCache.expiresAt) {
|
|
114
|
+
return _configCache.config;
|
|
115
|
+
}
|
|
116
|
+
let config;
|
|
53
117
|
try {
|
|
54
118
|
if (existsSync(CONFIG_PATH)) {
|
|
55
119
|
const data = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
56
|
-
|
|
120
|
+
config = parseConfigSafe(
|
|
57
121
|
ModelRouterConfigSchema,
|
|
58
122
|
{ ...DEFAULT_CONFIG, ...data },
|
|
59
123
|
DEFAULT_CONFIG,
|
|
60
124
|
"model-router"
|
|
61
125
|
);
|
|
126
|
+
} else {
|
|
127
|
+
config = { ...DEFAULT_CONFIG };
|
|
62
128
|
}
|
|
63
129
|
} catch {
|
|
130
|
+
config = { ...DEFAULT_CONFIG };
|
|
64
131
|
}
|
|
65
|
-
|
|
132
|
+
_configCache = { config, expiresAt: now + CONFIG_CACHE_TTL_MS };
|
|
133
|
+
return config;
|
|
66
134
|
}
|
|
67
135
|
function saveModelRouterConfig(config) {
|
|
68
136
|
try {
|
|
69
137
|
ensureSecureDir(join(homedir(), ".stackmemory"));
|
|
70
138
|
writeFileSecure(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
139
|
+
_configCache = null;
|
|
71
140
|
} catch {
|
|
72
141
|
}
|
|
73
142
|
}
|
|
143
|
+
function invalidateConfigCache() {
|
|
144
|
+
_configCache = null;
|
|
145
|
+
}
|
|
74
146
|
function getModelForTask(taskType) {
|
|
75
147
|
const config = loadModelRouterConfig();
|
|
76
148
|
if (!config.enabled) {
|
|
@@ -124,6 +196,156 @@ function requiresDeepThinking(input) {
|
|
|
124
196
|
];
|
|
125
197
|
return thinkPatterns.some((pattern) => pattern.test(input));
|
|
126
198
|
}
|
|
199
|
+
const OPTIMAL_ROUTING = {
|
|
200
|
+
linting: {
|
|
201
|
+
provider: "deepinfra",
|
|
202
|
+
model: "THUDM/glm-4-9b-chat",
|
|
203
|
+
apiKeyEnv: "DEEPINFRA_API_KEY",
|
|
204
|
+
baseUrl: "https://api.deepinfra.com/v1/openai"
|
|
205
|
+
},
|
|
206
|
+
context: {
|
|
207
|
+
provider: "deepinfra",
|
|
208
|
+
model: "THUDM/glm-4-9b-chat",
|
|
209
|
+
apiKeyEnv: "DEEPINFRA_API_KEY",
|
|
210
|
+
baseUrl: "https://api.deepinfra.com/v1/openai"
|
|
211
|
+
},
|
|
212
|
+
code: {
|
|
213
|
+
provider: "cerebras",
|
|
214
|
+
model: "llama-4-scout-17b-16e-instruct",
|
|
215
|
+
apiKeyEnv: "CEREBRAS_API_KEY",
|
|
216
|
+
baseUrl: "https://api.cerebras.ai/v1"
|
|
217
|
+
},
|
|
218
|
+
testing: {
|
|
219
|
+
provider: "cerebras",
|
|
220
|
+
model: "llama-4-scout-17b-16e-instruct",
|
|
221
|
+
apiKeyEnv: "CEREBRAS_API_KEY",
|
|
222
|
+
baseUrl: "https://api.cerebras.ai/v1"
|
|
223
|
+
},
|
|
224
|
+
review: {
|
|
225
|
+
provider: "anthropic",
|
|
226
|
+
model: "claude-sonnet-4-5-20250929",
|
|
227
|
+
apiKeyEnv: "ANTHROPIC_API_KEY"
|
|
228
|
+
},
|
|
229
|
+
plan: {
|
|
230
|
+
provider: "anthropic",
|
|
231
|
+
model: "claude-sonnet-4-5-20250929",
|
|
232
|
+
apiKeyEnv: "ANTHROPIC_API_KEY"
|
|
233
|
+
},
|
|
234
|
+
think: {
|
|
235
|
+
provider: "anthropic",
|
|
236
|
+
model: "claude-sonnet-4-5-20250929",
|
|
237
|
+
apiKeyEnv: "ANTHROPIC_API_KEY"
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const FALLBACK_CHAIN = ["deepinfra", "cerebras", "anthropic"];
|
|
241
|
+
const CHEAP_PROVIDERS = [
|
|
242
|
+
{
|
|
243
|
+
provider: "openrouter",
|
|
244
|
+
model: "meta-llama/llama-4-scout",
|
|
245
|
+
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
246
|
+
baseUrl: "https://openrouter.ai/api"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
provider: "deepinfra",
|
|
250
|
+
model: "THUDM/glm-4-9b-chat",
|
|
251
|
+
apiKeyEnv: "DEEPINFRA_API_KEY",
|
|
252
|
+
baseUrl: "https://api.deepinfra.com/v1/openai"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
provider: "cerebras",
|
|
256
|
+
model: "llama-4-scout-17b-16e-instruct",
|
|
257
|
+
apiKeyEnv: "CEREBRAS_API_KEY",
|
|
258
|
+
baseUrl: "https://api.cerebras.ai/v1"
|
|
259
|
+
}
|
|
260
|
+
];
|
|
261
|
+
function getOptimalProvider(taskType, preference, complexityInput) {
|
|
262
|
+
const defaultResult = {
|
|
263
|
+
provider: "anthropic",
|
|
264
|
+
model: "claude-sonnet-4-5-20250929",
|
|
265
|
+
apiKeyEnv: "ANTHROPIC_API_KEY"
|
|
266
|
+
};
|
|
267
|
+
if (!isFeatureEnabled("multiProvider")) {
|
|
268
|
+
return defaultResult;
|
|
269
|
+
}
|
|
270
|
+
if (complexityInput) {
|
|
271
|
+
const sensitiveCheck = detectSensitiveContent(
|
|
272
|
+
complexityInput.task,
|
|
273
|
+
complexityInput.context
|
|
274
|
+
);
|
|
275
|
+
if (sensitiveCheck.sensitive) {
|
|
276
|
+
return defaultResult;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (preference) {
|
|
280
|
+
if (!isApprovedProvider(preference) && complexityInput) {
|
|
281
|
+
const check = detectSensitiveContent(
|
|
282
|
+
complexityInput.task,
|
|
283
|
+
complexityInput.context
|
|
284
|
+
);
|
|
285
|
+
if (check.sensitive) {
|
|
286
|
+
return defaultResult;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const config = loadModelRouterConfig();
|
|
290
|
+
const providerConfig = config.providers[preference];
|
|
291
|
+
if (providerConfig && process.env[providerConfig.apiKeyEnv]) {
|
|
292
|
+
return {
|
|
293
|
+
provider: preference,
|
|
294
|
+
model: providerConfig.model,
|
|
295
|
+
baseUrl: providerConfig.baseUrl,
|
|
296
|
+
apiKeyEnv: providerConfig.apiKeyEnv
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (complexityInput) {
|
|
301
|
+
const complexity = scoreComplexity(
|
|
302
|
+
complexityInput.task,
|
|
303
|
+
complexityInput.context
|
|
304
|
+
);
|
|
305
|
+
if (complexity.tier === "low") {
|
|
306
|
+
const cheap = findAvailableCheapProvider();
|
|
307
|
+
if (cheap) return cheap;
|
|
308
|
+
}
|
|
309
|
+
if (complexity.tier === "high") {
|
|
310
|
+
return defaultResult;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const route = OPTIMAL_ROUTING[taskType];
|
|
314
|
+
if (route && process.env[route.apiKeyEnv]) {
|
|
315
|
+
return { ...route };
|
|
316
|
+
}
|
|
317
|
+
const fallbackConfig = loadModelRouterConfig();
|
|
318
|
+
for (const provider of FALLBACK_CHAIN) {
|
|
319
|
+
const providerConfig = fallbackConfig.providers[provider];
|
|
320
|
+
if (providerConfig && process.env[providerConfig.apiKeyEnv]) {
|
|
321
|
+
return {
|
|
322
|
+
provider,
|
|
323
|
+
model: providerConfig.model,
|
|
324
|
+
baseUrl: providerConfig.baseUrl,
|
|
325
|
+
apiKeyEnv: providerConfig.apiKeyEnv
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return defaultResult;
|
|
330
|
+
}
|
|
331
|
+
function findAvailableCheapProvider() {
|
|
332
|
+
for (const p of CHEAP_PROVIDERS) {
|
|
333
|
+
if (process.env[p.apiKeyEnv]) {
|
|
334
|
+
return {
|
|
335
|
+
provider: p.provider,
|
|
336
|
+
model: p.model,
|
|
337
|
+
apiKeyEnv: p.apiKeyEnv,
|
|
338
|
+
baseUrl: p.baseUrl
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
function getComplexityRoutedProvider(taskType, task, context) {
|
|
345
|
+
const complexity = scoreComplexity(task, context);
|
|
346
|
+
const provider = getOptimalProvider(taskType, void 0, { task, context });
|
|
347
|
+
return { ...provider, complexity: complexity.tier };
|
|
348
|
+
}
|
|
127
349
|
class ModelRouter {
|
|
128
350
|
config;
|
|
129
351
|
currentProvider;
|
|
@@ -196,28 +418,6 @@ class ModelRouter {
|
|
|
196
418
|
const apiKey = process.env[fallbackProvider.apiKeyEnv];
|
|
197
419
|
return !!apiKey;
|
|
198
420
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Check if error should trigger fallback
|
|
201
|
-
*/
|
|
202
|
-
shouldFallback(error) {
|
|
203
|
-
if (!this.isFallbackEnabled()) return false;
|
|
204
|
-
if (this.inFallbackMode) return false;
|
|
205
|
-
const fallback = this.config.fallback;
|
|
206
|
-
if (fallback.onRateLimit && error.status === 429) {
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
if (fallback.onError && error.status && error.status >= 500) {
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
if (fallback.onTimeout) {
|
|
213
|
-
const isTimeout = error.code === "ETIMEDOUT" || error.code === "ESOCKETTIMEDOUT" || error.message?.toLowerCase().includes("timeout");
|
|
214
|
-
if (isTimeout) return true;
|
|
215
|
-
}
|
|
216
|
-
if (error.message?.toLowerCase().includes("overloaded")) {
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
421
|
/**
|
|
222
422
|
* Activate fallback mode
|
|
223
423
|
*/
|
|
@@ -278,16 +478,6 @@ function getModelRouter() {
|
|
|
278
478
|
}
|
|
279
479
|
return routerInstance;
|
|
280
480
|
}
|
|
281
|
-
function getPlanModeEnv() {
|
|
282
|
-
const router = getModelRouter();
|
|
283
|
-
const result = router.route("plan");
|
|
284
|
-
return result.env;
|
|
285
|
-
}
|
|
286
|
-
function getThinkingModeEnv() {
|
|
287
|
-
const router = getModelRouter();
|
|
288
|
-
const result = router.route("think");
|
|
289
|
-
return result.env;
|
|
290
|
-
}
|
|
291
481
|
function isFallbackAvailable() {
|
|
292
482
|
const router = getModelRouter();
|
|
293
483
|
return router.isFallbackEnabled();
|
|
@@ -322,13 +512,17 @@ function resetFallback() {
|
|
|
322
512
|
router.reset();
|
|
323
513
|
}
|
|
324
514
|
export {
|
|
515
|
+
DEFAULT_MODEL_TOKEN_LIMIT,
|
|
516
|
+
MODEL_TOKEN_LIMITS,
|
|
325
517
|
ModelRouter,
|
|
326
518
|
buildModelEnv,
|
|
519
|
+
getComplexityRoutedProvider,
|
|
327
520
|
getFallbackStatus,
|
|
328
521
|
getModelForTask,
|
|
329
522
|
getModelRouter,
|
|
330
|
-
|
|
331
|
-
|
|
523
|
+
getModelTokenLimit,
|
|
524
|
+
getOptimalProvider,
|
|
525
|
+
invalidateConfigCache,
|
|
332
526
|
isFallbackAvailable,
|
|
333
527
|
isPlanningContext,
|
|
334
528
|
loadModelRouterConfig,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
const MODEL_PRICING = {
|
|
6
|
+
// Anthropic (direct API)
|
|
7
|
+
"anthropic/claude-sonnet-4-5-20250929": {
|
|
8
|
+
inputPer1M: 3,
|
|
9
|
+
outputPer1M: 15,
|
|
10
|
+
source: "anthropic.com"
|
|
11
|
+
},
|
|
12
|
+
"anthropic/claude-sonnet-4-20250514": {
|
|
13
|
+
inputPer1M: 3,
|
|
14
|
+
outputPer1M: 15,
|
|
15
|
+
source: "anthropic.com"
|
|
16
|
+
},
|
|
17
|
+
"anthropic/claude-haiku-4-5-20251001": {
|
|
18
|
+
inputPer1M: 0.8,
|
|
19
|
+
outputPer1M: 4,
|
|
20
|
+
source: "anthropic.com"
|
|
21
|
+
},
|
|
22
|
+
// OpenAI (direct API)
|
|
23
|
+
"openai/gpt-4o": {
|
|
24
|
+
inputPer1M: 2.5,
|
|
25
|
+
outputPer1M: 10,
|
|
26
|
+
source: "openai.com"
|
|
27
|
+
},
|
|
28
|
+
// OpenRouter (aggregated)
|
|
29
|
+
"openrouter/meta-llama/llama-4-scout": {
|
|
30
|
+
inputPer1M: 0.08,
|
|
31
|
+
outputPer1M: 0.3,
|
|
32
|
+
source: "openrouter.ai/api/v1/models"
|
|
33
|
+
},
|
|
34
|
+
// Cerebras (free tier / inference)
|
|
35
|
+
"cerebras/llama-4-scout-17b-16e-instruct": {
|
|
36
|
+
inputPer1M: 0.1,
|
|
37
|
+
outputPer1M: 0.1,
|
|
38
|
+
source: "cerebras.ai"
|
|
39
|
+
},
|
|
40
|
+
// DeepInfra
|
|
41
|
+
"deepinfra/THUDM/glm-4-9b-chat": {
|
|
42
|
+
inputPer1M: 0.065,
|
|
43
|
+
outputPer1M: 0.065,
|
|
44
|
+
source: "deepinfra.com"
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function calculateCost(provider, model, inputTokens, outputTokens) {
|
|
48
|
+
const key = `${provider}/${model}`;
|
|
49
|
+
const pricing = MODEL_PRICING[key];
|
|
50
|
+
if (!pricing) return null;
|
|
51
|
+
const inputCost = inputTokens / 1e6 * pricing.inputPer1M;
|
|
52
|
+
const outputCost = outputTokens / 1e6 * pricing.outputPer1M;
|
|
53
|
+
return { inputCost, outputCost, totalCost: inputCost + outputCost };
|
|
54
|
+
}
|
|
55
|
+
function formatCost(usd) {
|
|
56
|
+
if (usd < 0.01) return `$${usd.toFixed(6)}`;
|
|
57
|
+
return `$${usd.toFixed(4)}`;
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
MODEL_PRICING,
|
|
61
|
+
calculateCost,
|
|
62
|
+
formatCost
|
|
63
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
const SENSITIVE_PATTERNS = [
|
|
6
|
+
// API keys — common prefixes
|
|
7
|
+
{ pattern: /\bsk-[a-zA-Z0-9]{20,}/, label: "API key (sk-)" },
|
|
8
|
+
{ pattern: /\bsk-ant-[a-zA-Z0-9-]{20,}/, label: "Anthropic key" },
|
|
9
|
+
{ pattern: /\bAKIA[A-Z0-9]{12,}/, label: "AWS access key" },
|
|
10
|
+
{ pattern: /\bghp_[a-zA-Z0-9]{30,}/, label: "GitHub PAT" },
|
|
11
|
+
{ pattern: /\bgho_[a-zA-Z0-9]{30,}/, label: "GitHub OAuth" },
|
|
12
|
+
{ pattern: /\bghs_[a-zA-Z0-9]{30,}/, label: "GitHub App" },
|
|
13
|
+
{ pattern: /\bglpat-[a-zA-Z0-9_-]{20,}/, label: "GitLab PAT" },
|
|
14
|
+
{ pattern: /\bnpm_[a-zA-Z0-9]{30,}/, label: "npm token" },
|
|
15
|
+
{ pattern: /\bxox[bpsar]-[a-zA-Z0-9-]{10,}/, label: "Slack token" },
|
|
16
|
+
{ pattern: /\blin_api_[a-zA-Z0-9]{20,}/, label: "Linear API key" },
|
|
17
|
+
{ pattern: /\blin_oauth_[a-zA-Z0-9]{20,}/, label: "Linear OAuth" },
|
|
18
|
+
{ pattern: /\bSG\.[a-zA-Z0-9_-]{20,}/, label: "SendGrid key" },
|
|
19
|
+
{ pattern: /\brk_live_[a-zA-Z0-9]{20,}/, label: "Stripe key" },
|
|
20
|
+
{ pattern: /\bsk_(?:live|test)_[a-zA-Z0-9]{20,}/, label: "Stripe secret" },
|
|
21
|
+
{ pattern: /\bwhsec_[a-zA-Z0-9]{20,}/, label: "Webhook secret" },
|
|
22
|
+
// Private keys and certificates
|
|
23
|
+
{
|
|
24
|
+
pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/,
|
|
25
|
+
label: "Private key (PEM)"
|
|
26
|
+
},
|
|
27
|
+
{ pattern: /-----BEGIN\s+CERTIFICATE-----/, label: "Certificate (PEM)" },
|
|
28
|
+
{
|
|
29
|
+
pattern: /-----BEGIN\s+(?:EC\s+)?PRIVATE\s+KEY-----/,
|
|
30
|
+
label: "EC private key"
|
|
31
|
+
},
|
|
32
|
+
// JWT tokens (header.payload.signature format)
|
|
33
|
+
{
|
|
34
|
+
pattern: /\beyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/,
|
|
35
|
+
label: "JWT token"
|
|
36
|
+
},
|
|
37
|
+
{ pattern: /\bBearer\s+eyJ[a-zA-Z0-9_-]{10,}/, label: "Bearer JWT" },
|
|
38
|
+
// Connection strings with credentials
|
|
39
|
+
{
|
|
40
|
+
pattern: /(?:mysql|postgres|postgresql|mongodb|redis):\/\/[^:]+:[^@]+@/,
|
|
41
|
+
label: "DB connection string"
|
|
42
|
+
},
|
|
43
|
+
// Generic high-entropy secrets (base64 blocks in config-like context)
|
|
44
|
+
{
|
|
45
|
+
pattern: /(?:password|secret|token|api[_-]?key|auth[_-]?token)\s*[:=]\s*["'][^"']{8,}["']/i,
|
|
46
|
+
label: "Credential assignment"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
pattern: /(?:PASSWORD|SECRET|TOKEN|API_KEY|AUTH_TOKEN)\s*=\s*\S{8,}/,
|
|
50
|
+
label: "Env var credential"
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
const APPROVED_PROVIDERS = /* @__PURE__ */ new Set(["anthropic", "anthropic-batch"]);
|
|
54
|
+
function checkString(text) {
|
|
55
|
+
const matches = [];
|
|
56
|
+
for (const { pattern, label } of SENSITIVE_PATTERNS) {
|
|
57
|
+
if (pattern.test(text)) {
|
|
58
|
+
matches.push(label);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { sensitive: matches.length > 0, matches };
|
|
62
|
+
}
|
|
63
|
+
function detectSensitiveContent(task, context) {
|
|
64
|
+
const allMatches = [];
|
|
65
|
+
const taskResult = checkString(task);
|
|
66
|
+
allMatches.push(...taskResult.matches);
|
|
67
|
+
if (context) {
|
|
68
|
+
for (const value of Object.values(context)) {
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
const r = checkString(value);
|
|
71
|
+
allMatches.push(...r.matches);
|
|
72
|
+
} else if (Array.isArray(value)) {
|
|
73
|
+
for (const item of value) {
|
|
74
|
+
if (typeof item === "string") {
|
|
75
|
+
const r = checkString(item);
|
|
76
|
+
allMatches.push(...r.matches);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else if (value && typeof value === "object") {
|
|
80
|
+
for (const nested of Object.values(value)) {
|
|
81
|
+
if (typeof nested === "string") {
|
|
82
|
+
const r = checkString(nested);
|
|
83
|
+
allMatches.push(...r.matches);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const unique = [...new Set(allMatches)];
|
|
90
|
+
return { sensitive: unique.length > 0, matches: unique };
|
|
91
|
+
}
|
|
92
|
+
function isApprovedProvider(provider) {
|
|
93
|
+
return APPROVED_PROVIDERS.has(provider);
|
|
94
|
+
}
|
|
95
|
+
function shouldBlockProvider(provider, task, context) {
|
|
96
|
+
if (isApprovedProvider(provider)) {
|
|
97
|
+
return { blocked: false };
|
|
98
|
+
}
|
|
99
|
+
const check = detectSensitiveContent(task, context);
|
|
100
|
+
if (!check.sensitive) {
|
|
101
|
+
return { blocked: false };
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
blocked: true,
|
|
105
|
+
reason: `Sensitive content detected (${check.matches.join(", ")}). Blocked routing to ${provider}; using approved provider instead.`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
detectSensitiveContent,
|
|
110
|
+
isApprovedProvider,
|
|
111
|
+
shouldBlockProvider
|
|
112
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { EventEmitter } from "events";
|
|
6
|
+
const DEFAULT_CONFIG = {
|
|
7
|
+
contextPressure: { enabled: true, cooldownSec: 60 },
|
|
8
|
+
editRecovery: { enabled: true, cooldownSec: 0 },
|
|
9
|
+
retrievalQuality: { enabled: true, cooldownSec: 300 },
|
|
10
|
+
traceErrorChain: { enabled: true, cooldownSec: 30 },
|
|
11
|
+
harnessRegression: { enabled: true, cooldownSec: 0 },
|
|
12
|
+
sessionDrift: { enabled: true, cooldownSec: 120 }
|
|
13
|
+
};
|
|
14
|
+
class FeedbackLoopEngine extends EventEmitter {
|
|
15
|
+
config;
|
|
16
|
+
lastFired = /* @__PURE__ */ new Map();
|
|
17
|
+
history = [];
|
|
18
|
+
maxHistory = 200;
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
super();
|
|
21
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fire a loop if enabled and not in cooldown.
|
|
25
|
+
* Returns the LoopEvent if fired, null if skipped.
|
|
26
|
+
*/
|
|
27
|
+
fire(loopName, trigger, data, action, outcome = "success") {
|
|
28
|
+
const cfg = this.config[loopName];
|
|
29
|
+
if (!cfg?.enabled) return null;
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const lastTime = this.lastFired.get(loopName) || 0;
|
|
32
|
+
if (now - lastTime < cfg.cooldownSec * 1e3) return null;
|
|
33
|
+
const event = {
|
|
34
|
+
loop: loopName,
|
|
35
|
+
trigger,
|
|
36
|
+
timestamp: now,
|
|
37
|
+
data,
|
|
38
|
+
action,
|
|
39
|
+
outcome
|
|
40
|
+
};
|
|
41
|
+
this.lastFired.set(loopName, now);
|
|
42
|
+
this.history.push(event);
|
|
43
|
+
if (this.history.length > this.maxHistory) {
|
|
44
|
+
this.history = this.history.slice(-this.maxHistory);
|
|
45
|
+
}
|
|
46
|
+
this.emit("loop", event);
|
|
47
|
+
this.emit(`loop:${loopName}`, event);
|
|
48
|
+
return event;
|
|
49
|
+
}
|
|
50
|
+
/** Get recent loop events, optionally filtered by loop name. */
|
|
51
|
+
getHistory(loopName, limit = 50) {
|
|
52
|
+
const filtered = loopName ? this.history.filter((e) => e.loop === loopName) : this.history;
|
|
53
|
+
return filtered.slice(-limit);
|
|
54
|
+
}
|
|
55
|
+
/** Get summary stats per loop. */
|
|
56
|
+
getStats() {
|
|
57
|
+
const stats = {};
|
|
58
|
+
for (const event of this.history) {
|
|
59
|
+
if (!stats[event.loop]) {
|
|
60
|
+
stats[event.loop] = {
|
|
61
|
+
fires: 0,
|
|
62
|
+
successes: 0,
|
|
63
|
+
errors: 0,
|
|
64
|
+
lastFired: null
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
stats[event.loop].fires++;
|
|
68
|
+
if (event.outcome === "success") stats[event.loop].successes++;
|
|
69
|
+
if (event.outcome === "error") stats[event.loop].errors++;
|
|
70
|
+
stats[event.loop].lastFired = event.timestamp;
|
|
71
|
+
}
|
|
72
|
+
return stats;
|
|
73
|
+
}
|
|
74
|
+
/** Update config at runtime. */
|
|
75
|
+
updateConfig(partial) {
|
|
76
|
+
this.config = { ...this.config, ...partial };
|
|
77
|
+
}
|
|
78
|
+
/** Get current config. */
|
|
79
|
+
getConfig() {
|
|
80
|
+
return { ...this.config };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const feedbackLoops = new FeedbackLoopEngine();
|
|
84
|
+
export {
|
|
85
|
+
DEFAULT_CONFIG,
|
|
86
|
+
FeedbackLoopEngine,
|
|
87
|
+
feedbackLoops
|
|
88
|
+
};
|
|
@@ -16,6 +16,10 @@ const ModelProviderSchema = z.enum([
|
|
|
16
16
|
"qwen",
|
|
17
17
|
"openai",
|
|
18
18
|
"ollama",
|
|
19
|
+
"cerebras",
|
|
20
|
+
"deepinfra",
|
|
21
|
+
"openrouter",
|
|
22
|
+
"anthropic-batch",
|
|
19
23
|
"custom"
|
|
20
24
|
]);
|
|
21
25
|
const ModelConfigSchema = z.object({
|
|
@@ -33,7 +37,10 @@ const ModelRouterConfigSchema = z.object({
|
|
|
33
37
|
plan: ModelProviderSchema.optional(),
|
|
34
38
|
think: ModelProviderSchema.optional(),
|
|
35
39
|
code: ModelProviderSchema.optional(),
|
|
36
|
-
review: ModelProviderSchema.optional()
|
|
40
|
+
review: ModelProviderSchema.optional(),
|
|
41
|
+
linting: ModelProviderSchema.optional(),
|
|
42
|
+
context: ModelProviderSchema.optional(),
|
|
43
|
+
testing: ModelProviderSchema.optional()
|
|
37
44
|
}).optional().default({}),
|
|
38
45
|
fallback: z.object({
|
|
39
46
|
enabled: z.boolean(),
|
|
@@ -49,6 +56,10 @@ const ModelRouterConfigSchema = z.object({
|
|
|
49
56
|
qwen: ModelConfigSchema.optional(),
|
|
50
57
|
openai: ModelConfigSchema.optional(),
|
|
51
58
|
ollama: ModelConfigSchema.optional(),
|
|
59
|
+
cerebras: ModelConfigSchema.optional(),
|
|
60
|
+
deepinfra: ModelConfigSchema.optional(),
|
|
61
|
+
openrouter: ModelConfigSchema.optional(),
|
|
62
|
+
"anthropic-batch": ModelConfigSchema.optional(),
|
|
52
63
|
custom: ModelConfigSchema.optional()
|
|
53
64
|
}).optional().default({}),
|
|
54
65
|
thinkingMode: z.object({
|