@neurcode-ai/cli 0.9.8 → 0.9.10
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/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +37 -0
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/brain.d.ts.map +1 -1
- package/dist/commands/brain.js +195 -11
- package/dist/commands/brain.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +308 -72
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +44 -1
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +6 -2
- package/dist/commands/watch.js.map +1 -1
- package/dist/services/security/SecurityGuard.js +6 -6
- package/dist/services/security/SecurityGuard.js.map +1 -1
- package/dist/services/watch/Sentinel.d.ts +2 -1
- package/dist/services/watch/Sentinel.d.ts.map +1 -1
- package/dist/services/watch/Sentinel.js +31 -4
- package/dist/services/watch/Sentinel.js.map +1 -1
- package/dist/utils/brain-context.d.ts +78 -0
- package/dist/utils/brain-context.d.ts.map +1 -0
- package/dist/utils/brain-context.js +646 -0
- package/dist/utils/brain-context.js.map +1 -0
- package/dist/utils/plan-cache.d.ts +72 -13
- package/dist/utils/plan-cache.d.ts.map +1 -1
- package/dist/utils/plan-cache.js +1086 -152
- package/dist/utils/plan-cache.js.map +1 -1
- package/package.json +4 -3
package/dist/utils/plan-cache.js
CHANGED
|
@@ -1,164 +1,718 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.normalizeIntent = normalizeIntent;
|
|
4
|
-
exports.
|
|
5
|
-
exports.computePlanCacheKey = computePlanCacheKey;
|
|
4
|
+
exports.getRepoIdentity = getRepoIdentity;
|
|
6
5
|
exports.getGitRepoFingerprint = getGitRepoFingerprint;
|
|
7
6
|
exports.getFilesystemFingerprintFromTree = getFilesystemFingerprintFromTree;
|
|
7
|
+
exports.computePromptHash = computePromptHash;
|
|
8
|
+
exports.computePolicyVersionHash = computePolicyVersionHash;
|
|
9
|
+
exports.getNeurcodeVersion = getNeurcodeVersion;
|
|
10
|
+
exports.computePlanCacheKey = computePlanCacheKey;
|
|
11
|
+
exports.getBrainDbPath = getBrainDbPath;
|
|
12
|
+
exports.getBrainPointerPath = getBrainPointerPath;
|
|
13
|
+
exports.getBrainFallbackCachePath = getBrainFallbackCachePath;
|
|
14
|
+
exports.getPlanCachePath = getPlanCachePath;
|
|
15
|
+
exports.getBrainStorageMode = getBrainStorageMode;
|
|
16
|
+
exports.setNoCodeStorageMode = setNoCodeStorageMode;
|
|
17
|
+
exports.isNoCodeStorageMode = isNoCodeStorageMode;
|
|
8
18
|
exports.readCachedPlan = readCachedPlan;
|
|
9
19
|
exports.peekCachedPlan = peekCachedPlan;
|
|
10
20
|
exports.writeCachedPlan = writeCachedPlan;
|
|
11
21
|
exports.listCachedPlans = listCachedPlans;
|
|
12
22
|
exports.deleteCachedPlans = deleteCachedPlans;
|
|
13
23
|
exports.findSimilarCachedPlans = findSimilarCachedPlans;
|
|
24
|
+
exports.findNearCachedPlan = findNearCachedPlan;
|
|
25
|
+
exports.diagnosePlanCacheMiss = diagnosePlanCacheMiss;
|
|
26
|
+
exports.getBrainDbSizeBytes = getBrainDbSizeBytes;
|
|
27
|
+
exports.getBrainStoreBackend = getBrainStoreBackend;
|
|
28
|
+
exports.closeBrainStore = closeBrainStore;
|
|
14
29
|
const child_process_1 = require("child_process");
|
|
15
30
|
const crypto_1 = require("crypto");
|
|
16
31
|
const fs_1 = require("fs");
|
|
17
32
|
const path_1 = require("path");
|
|
18
33
|
const secret_masking_1 = require("./secret-masking");
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
let sqliteCtor = null;
|
|
35
|
+
function getSqliteCtor() {
|
|
36
|
+
if (sqliteCtor)
|
|
37
|
+
return sqliteCtor;
|
|
38
|
+
try {
|
|
39
|
+
sqliteCtor = require('better-sqlite3');
|
|
40
|
+
return sqliteCtor;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const CACHE_SCHEMA_VERSION = 2;
|
|
47
|
+
const BRAIN_DB_FILE_NAME = 'brain.db';
|
|
48
|
+
const BRAIN_POINTER_FILE_NAME = 'brain.json';
|
|
49
|
+
const LEGACY_CACHE_FILE_NAME = 'plan-cache.json';
|
|
50
|
+
const FALLBACK_CACHE_FILE_NAME = 'plan-cache.json';
|
|
51
|
+
const MAX_ENTRIES = 500;
|
|
52
|
+
const NON_SEMANTIC_GIT_PATH_MARKERS = [
|
|
53
|
+
'.neurcode/',
|
|
54
|
+
'.gitignore',
|
|
55
|
+
];
|
|
56
|
+
const dbConnections = new Map();
|
|
57
|
+
let cachedCliVersion = null;
|
|
22
58
|
function sha256Hex(input) {
|
|
23
59
|
return (0, crypto_1.createHash)('sha256').update(input).digest('hex');
|
|
24
60
|
}
|
|
25
|
-
function sanitizeGitStatusPorcelain(status) {
|
|
26
|
-
// Ignore Neurcode CLI state changes from cache fingerprinting.
|
|
27
|
-
// `.neurcode/config.json` is intentionally *not* gitignored so it can be committed if desired,
|
|
28
|
-
// but it should never affect plan cache hits/misses.
|
|
29
|
-
const lines = status.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
30
|
-
const filtered = lines.filter((l) => !l.includes('.neurcode/config.json'));
|
|
31
|
-
return filtered.join('\n');
|
|
32
|
-
}
|
|
33
61
|
function normalizeIntent(intent) {
|
|
34
62
|
return intent
|
|
35
63
|
.trim()
|
|
36
64
|
.replace(/\s+/g, ' ')
|
|
37
65
|
.toLowerCase();
|
|
38
66
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
67
|
+
function sanitizeGitStatusPorcelain(status) {
|
|
68
|
+
const lines = status.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
69
|
+
const filtered = lines.filter((line) => {
|
|
70
|
+
for (const marker of NON_SEMANTIC_GIT_PATH_MARKERS) {
|
|
71
|
+
if (line.includes(marker))
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
});
|
|
76
|
+
return filtered.join('\n');
|
|
41
77
|
}
|
|
42
|
-
function
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
45
|
-
|
|
78
|
+
function normalizeRepoIdentity(raw) {
|
|
79
|
+
const trimmed = raw.trim();
|
|
80
|
+
if (!trimmed)
|
|
81
|
+
return '';
|
|
82
|
+
// Convert SCP-like git syntax to URI form: git@host:owner/repo -> ssh://host/owner/repo
|
|
83
|
+
let normalized = trimmed;
|
|
84
|
+
const scpLike = /^[^@]+@[^:]+:.+$/;
|
|
85
|
+
if (scpLike.test(trimmed)) {
|
|
86
|
+
const at = trimmed.indexOf('@');
|
|
87
|
+
const colon = trimmed.indexOf(':', at);
|
|
88
|
+
const host = trimmed.slice(at + 1, colon);
|
|
89
|
+
const repoPath = trimmed.slice(colon + 1);
|
|
90
|
+
normalized = `ssh://${host}/${repoPath}`;
|
|
46
91
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
92
|
+
try {
|
|
93
|
+
const parsed = new URL(normalized);
|
|
94
|
+
parsed.username = '';
|
|
95
|
+
parsed.password = '';
|
|
96
|
+
const noAuth = `${parsed.protocol}//${parsed.host}${parsed.pathname}`.replace(/\/+$/, '');
|
|
97
|
+
return noAuth.replace(/\.git$/i, '').toLowerCase();
|
|
51
98
|
}
|
|
99
|
+
catch {
|
|
100
|
+
return normalized.replace(/\/+$/, '').replace(/\.git$/i, '').toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function fallbackRepoIdentity(cwd) {
|
|
104
|
+
return `local:${sha256Hex((0, path_1.resolve)(cwd))}`;
|
|
105
|
+
}
|
|
106
|
+
function getRepoIdentity(cwd) {
|
|
52
107
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
108
|
+
const inside = (0, child_process_1.execSync)('git rev-parse --is-inside-work-tree', {
|
|
109
|
+
cwd,
|
|
110
|
+
encoding: 'utf-8',
|
|
111
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
112
|
+
}).trim().toLowerCase();
|
|
113
|
+
if (inside === 'true') {
|
|
114
|
+
const remote = (0, child_process_1.execSync)('git config --get remote.origin.url', {
|
|
115
|
+
cwd,
|
|
116
|
+
encoding: 'utf-8',
|
|
117
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
118
|
+
}).trim();
|
|
119
|
+
const normalizedRemote = normalizeRepoIdentity(remote);
|
|
120
|
+
if (normalizedRemote)
|
|
121
|
+
return normalizedRemote;
|
|
57
122
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// ignore
|
|
126
|
+
}
|
|
127
|
+
return fallbackRepoIdentity(cwd);
|
|
128
|
+
}
|
|
129
|
+
function getGitRepoFingerprint(cwd) {
|
|
130
|
+
try {
|
|
131
|
+
const inside = (0, child_process_1.execSync)('git rev-parse --is-inside-work-tree', {
|
|
132
|
+
cwd,
|
|
133
|
+
encoding: 'utf-8',
|
|
134
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
135
|
+
}).trim().toLowerCase();
|
|
136
|
+
if (inside !== 'true')
|
|
137
|
+
return null;
|
|
138
|
+
const repoIdentity = getRepoIdentity(cwd);
|
|
139
|
+
const headSha = (0, child_process_1.execSync)('git rev-parse HEAD', {
|
|
140
|
+
cwd,
|
|
141
|
+
encoding: 'utf-8',
|
|
142
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
143
|
+
}).trim();
|
|
144
|
+
const headTreeSha = (0, child_process_1.execSync)('git rev-parse HEAD^{tree}', {
|
|
145
|
+
cwd,
|
|
146
|
+
encoding: 'utf-8',
|
|
147
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
148
|
+
}).trim();
|
|
149
|
+
const status = (0, child_process_1.execSync)('git status --porcelain', {
|
|
150
|
+
cwd,
|
|
151
|
+
encoding: 'utf-8',
|
|
152
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
153
|
+
});
|
|
154
|
+
const workingTreeHash = sha256Hex(sanitizeGitStatusPorcelain(status));
|
|
155
|
+
return { kind: 'git', repoIdentity, headSha, headTreeSha, workingTreeHash };
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function getFilesystemFingerprintFromTree(fileTree, cwd = process.cwd()) {
|
|
162
|
+
const normalized = [...fileTree].sort().join('\n');
|
|
163
|
+
return {
|
|
164
|
+
kind: 'filesystem',
|
|
165
|
+
repoIdentity: fallbackRepoIdentity(cwd),
|
|
166
|
+
fileTreeHash: sha256Hex(normalized),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function computePromptHash(input) {
|
|
170
|
+
const normalized = normalizeIntent(input.intent);
|
|
171
|
+
const safeIntent = (0, secret_masking_1.maskSecretsInText)(normalized).masked;
|
|
172
|
+
const payload = [
|
|
173
|
+
`intent=${safeIntent}`,
|
|
174
|
+
`ticketRef=${input.ticketRef || ''}`,
|
|
175
|
+
`contextHash=${input.contextHash || ''}`,
|
|
176
|
+
].join('\n');
|
|
177
|
+
return sha256Hex(payload);
|
|
178
|
+
}
|
|
179
|
+
function collectPolicyFiles(cwd) {
|
|
180
|
+
const out = [];
|
|
181
|
+
const candidates = [
|
|
182
|
+
(0, path_1.join)(cwd, 'neurcode.policy.json'),
|
|
183
|
+
(0, path_1.join)(cwd, 'neurcode.rules.json'),
|
|
184
|
+
(0, path_1.join)(cwd, '.neurcode', 'policy.json'),
|
|
185
|
+
(0, path_1.join)(cwd, '.neurcode', 'rules.json'),
|
|
186
|
+
];
|
|
187
|
+
for (const filePath of candidates) {
|
|
188
|
+
if ((0, fs_1.existsSync)(filePath))
|
|
189
|
+
out.push(filePath);
|
|
190
|
+
}
|
|
191
|
+
const policyDir = (0, path_1.join)(cwd, '.neurcode', 'policies');
|
|
192
|
+
if ((0, fs_1.existsSync)(policyDir)) {
|
|
193
|
+
const walk = (dir) => {
|
|
194
|
+
const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
const full = (0, path_1.join)(dir, entry.name);
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
walk(full);
|
|
199
|
+
}
|
|
200
|
+
else if (entry.isFile()) {
|
|
201
|
+
out.push(full);
|
|
202
|
+
}
|
|
66
203
|
}
|
|
204
|
+
};
|
|
205
|
+
try {
|
|
206
|
+
walk(policyDir);
|
|
67
207
|
}
|
|
68
208
|
catch {
|
|
69
|
-
// ignore
|
|
209
|
+
// ignore unreadable dirs
|
|
70
210
|
}
|
|
71
|
-
return cache;
|
|
72
211
|
}
|
|
73
|
-
|
|
74
|
-
|
|
212
|
+
return [...new Set(out)].sort();
|
|
213
|
+
}
|
|
214
|
+
function computePolicyVersionHash(cwd) {
|
|
215
|
+
const lines = [];
|
|
216
|
+
const envPolicyVersion = process.env.NEURCODE_POLICY_VERSION;
|
|
217
|
+
if (envPolicyVersion && envPolicyVersion.trim()) {
|
|
218
|
+
lines.push(`env:${envPolicyVersion.trim()}`);
|
|
219
|
+
}
|
|
220
|
+
for (const policyFile of collectPolicyFiles(cwd)) {
|
|
75
221
|
try {
|
|
76
|
-
const
|
|
77
|
-
|
|
222
|
+
const content = (0, fs_1.readFileSync)(policyFile, 'utf-8');
|
|
223
|
+
lines.push(`${policyFile}:${sha256Hex(content)}`);
|
|
78
224
|
}
|
|
79
225
|
catch {
|
|
80
|
-
// ignore
|
|
226
|
+
// ignore unreadable files
|
|
81
227
|
}
|
|
82
|
-
return { schemaVersion: CACHE_SCHEMA_VERSION, entries: {} };
|
|
83
228
|
}
|
|
229
|
+
if (lines.length === 0) {
|
|
230
|
+
lines.push('default-policy');
|
|
231
|
+
}
|
|
232
|
+
return sha256Hex(lines.join('\n'));
|
|
84
233
|
}
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (keys.length <= MAX_ENTRIES)
|
|
93
|
-
return;
|
|
94
|
-
const sorted = keys
|
|
95
|
-
.map((k) => cache.entries[k])
|
|
96
|
-
.filter(Boolean)
|
|
97
|
-
.sort((a, b) => {
|
|
98
|
-
const aTime = Date.parse(a.lastUsedAt) || 0;
|
|
99
|
-
const bTime = Date.parse(b.lastUsedAt) || 0;
|
|
100
|
-
return aTime - bTime;
|
|
101
|
-
});
|
|
102
|
-
const toRemove = sorted.slice(0, Math.max(0, sorted.length - MAX_ENTRIES));
|
|
103
|
-
for (const entry of toRemove) {
|
|
104
|
-
delete cache.entries[entry.key];
|
|
234
|
+
function getNeurcodeVersion() {
|
|
235
|
+
if (cachedCliVersion)
|
|
236
|
+
return cachedCliVersion;
|
|
237
|
+
const envVersion = process.env.npm_package_version;
|
|
238
|
+
if (envVersion && envVersion.trim()) {
|
|
239
|
+
cachedCliVersion = envVersion.trim();
|
|
240
|
+
return cachedCliVersion;
|
|
105
241
|
}
|
|
242
|
+
const candidates = [
|
|
243
|
+
(0, path_1.join)(__dirname, '../../package.json'),
|
|
244
|
+
(0, path_1.join)(process.cwd(), 'packages/cli/package.json'),
|
|
245
|
+
(0, path_1.join)(process.cwd(), 'package.json'),
|
|
246
|
+
];
|
|
247
|
+
for (const path of candidates) {
|
|
248
|
+
try {
|
|
249
|
+
if (!(0, fs_1.existsSync)(path))
|
|
250
|
+
continue;
|
|
251
|
+
const raw = (0, fs_1.readFileSync)(path, 'utf-8');
|
|
252
|
+
const parsed = JSON.parse(raw);
|
|
253
|
+
if (parsed.version && parsed.version.trim()) {
|
|
254
|
+
cachedCliVersion = parsed.version.trim();
|
|
255
|
+
return cachedCliVersion;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// ignore parse/read errors
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
cachedCliVersion = 'unknown';
|
|
263
|
+
return cachedCliVersion;
|
|
106
264
|
}
|
|
107
265
|
function computePlanCacheKey(input) {
|
|
108
|
-
const safeIntent = (0, secret_masking_1.maskSecretsInText)(input.intent).masked;
|
|
109
|
-
// Use an explicit, stable string rather than JSON.stringify of arbitrary objects.
|
|
110
266
|
const payload = [
|
|
111
267
|
`v=${input.schemaVersion}`,
|
|
112
|
-
`apiUrl=${input.apiUrl}`,
|
|
113
268
|
`orgId=${input.orgId}`,
|
|
114
269
|
`projectId=${input.projectId}`,
|
|
115
|
-
`
|
|
116
|
-
`
|
|
117
|
-
`contextHash=${input.contextHash || ''}`,
|
|
118
|
-
`repo.kind=${input.repo.kind}`,
|
|
270
|
+
`repoIdentity=${input.repo.repoIdentity}`,
|
|
271
|
+
`repoKind=${input.repo.kind}`,
|
|
119
272
|
input.repo.kind === 'git'
|
|
120
|
-
?
|
|
121
|
-
|
|
273
|
+
? [
|
|
274
|
+
`headSha=${input.repo.headSha}`,
|
|
275
|
+
`headTreeSha=${input.repo.headTreeSha}`,
|
|
276
|
+
`workingTreeHash=${input.repo.workingTreeHash}`,
|
|
277
|
+
].join(';')
|
|
278
|
+
: `fileTreeHash=${input.repo.fileTreeHash}`,
|
|
279
|
+
`promptHash=${input.promptHash}`,
|
|
280
|
+
`policyVersionHash=${input.policyVersionHash}`,
|
|
281
|
+
`neurcodeVersion=${input.neurcodeVersion}`,
|
|
122
282
|
].join('\n');
|
|
123
283
|
return sha256Hex(payload);
|
|
124
284
|
}
|
|
125
|
-
function
|
|
285
|
+
function getBrainDbPath(cwd) {
|
|
286
|
+
return (0, path_1.join)(cwd, '.neurcode', BRAIN_DB_FILE_NAME);
|
|
287
|
+
}
|
|
288
|
+
function getBrainPointerPath(cwd) {
|
|
289
|
+
return (0, path_1.join)(cwd, '.neurcode', BRAIN_POINTER_FILE_NAME);
|
|
290
|
+
}
|
|
291
|
+
function getFallbackCachePath(cwd) {
|
|
292
|
+
return (0, path_1.join)(cwd, '.neurcode', FALLBACK_CACHE_FILE_NAME);
|
|
293
|
+
}
|
|
294
|
+
function getBrainFallbackCachePath(cwd) {
|
|
295
|
+
return getFallbackCachePath(cwd);
|
|
296
|
+
}
|
|
297
|
+
// Backward-compatible helper name retained for callers.
|
|
298
|
+
function getPlanCachePath(cwd) {
|
|
299
|
+
return getBrainDbPath(cwd);
|
|
300
|
+
}
|
|
301
|
+
function ensureNeurcodeDir(cwd) {
|
|
302
|
+
const dir = (0, path_1.join)(cwd, '.neurcode');
|
|
303
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
304
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function readFallbackCache(cwd) {
|
|
126
308
|
try {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
309
|
+
const path = getFallbackCachePath(cwd);
|
|
310
|
+
if (!(0, fs_1.existsSync)(path)) {
|
|
311
|
+
return { schemaVersion: CACHE_SCHEMA_VERSION, entries: {} };
|
|
312
|
+
}
|
|
313
|
+
const raw = (0, fs_1.readFileSync)(path, 'utf-8');
|
|
314
|
+
const parsed = JSON.parse(raw);
|
|
315
|
+
if (parsed.schemaVersion !== CACHE_SCHEMA_VERSION || !parsed.entries || typeof parsed.entries !== 'object') {
|
|
316
|
+
throw new Error('Invalid fallback cache schema');
|
|
317
|
+
}
|
|
318
|
+
return parsed;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return { schemaVersion: CACHE_SCHEMA_VERSION, entries: {} };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function writeFallbackCache(cwd, cache) {
|
|
325
|
+
try {
|
|
326
|
+
ensureNeurcodeDir(cwd);
|
|
327
|
+
const path = getFallbackCachePath(cwd);
|
|
328
|
+
const tmp = `${path}.tmp`;
|
|
329
|
+
(0, fs_1.writeFileSync)(tmp, JSON.stringify(cache, null, 2) + '\n', 'utf-8');
|
|
330
|
+
(0, fs_1.renameSync)(tmp, path);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// ignore
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function pruneFallback(cache) {
|
|
337
|
+
const entries = Object.values(cache.entries);
|
|
338
|
+
if (entries.length <= MAX_ENTRIES)
|
|
339
|
+
return;
|
|
340
|
+
entries.sort((a, b) => {
|
|
341
|
+
const aTime = Date.parse(a.lastUsedAt) || 0;
|
|
342
|
+
const bTime = Date.parse(b.lastUsedAt) || 0;
|
|
343
|
+
return aTime - bTime;
|
|
344
|
+
});
|
|
345
|
+
const toDelete = entries.slice(0, Math.max(0, entries.length - MAX_ENTRIES));
|
|
346
|
+
for (const entry of toDelete) {
|
|
347
|
+
delete cache.entries[entry.key];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function readJsonSafe(path) {
|
|
351
|
+
try {
|
|
352
|
+
if (!(0, fs_1.existsSync)(path))
|
|
131
353
|
return null;
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const status = (0, child_process_1.execSync)('git status --porcelain', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
135
|
-
const statusHash = sha256Hex(sanitizeGitStatusPorcelain(status));
|
|
136
|
-
return { kind: 'git', headSha, headTreeSha, statusHash };
|
|
354
|
+
const raw = (0, fs_1.readFileSync)(path, 'utf-8');
|
|
355
|
+
return JSON.parse(raw);
|
|
137
356
|
}
|
|
138
357
|
catch {
|
|
139
358
|
return null;
|
|
140
359
|
}
|
|
141
360
|
}
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
361
|
+
function writePointer(cwd, pointer) {
|
|
362
|
+
try {
|
|
363
|
+
ensureNeurcodeDir(cwd);
|
|
364
|
+
const pointerPath = getBrainPointerPath(cwd);
|
|
365
|
+
(0, fs_1.writeFileSync)(pointerPath, JSON.stringify(pointer, null, 2) + '\n', 'utf-8');
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// ignore
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function updatePointer(cwd, patch) {
|
|
372
|
+
const existing = readJsonSafe(getBrainPointerPath(cwd));
|
|
373
|
+
const next = {
|
|
374
|
+
schemaVersion: 1,
|
|
375
|
+
dbPath: '.neurcode/brain.db',
|
|
376
|
+
repoIdentity: patch.repoIdentity ?? existing?.repoIdentity,
|
|
377
|
+
settings: {
|
|
378
|
+
noCodeStorage: patch.settings?.noCodeStorage ?? existing?.settings?.noCodeStorage ?? false,
|
|
379
|
+
},
|
|
380
|
+
updatedAt: new Date().toISOString(),
|
|
381
|
+
};
|
|
382
|
+
writePointer(cwd, next);
|
|
383
|
+
}
|
|
384
|
+
function getBrainStorageMode(cwd) {
|
|
385
|
+
const env = process.env.NEURCODE_BRAIN_NO_CODE_STORAGE;
|
|
386
|
+
if (typeof env === 'string' && env.trim()) {
|
|
387
|
+
const normalized = env.trim().toLowerCase();
|
|
388
|
+
const enabled = normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
389
|
+
return { noCodeStorage: enabled, source: 'env' };
|
|
390
|
+
}
|
|
391
|
+
const pointer = readJsonSafe(getBrainPointerPath(cwd));
|
|
392
|
+
if (pointer?.settings && typeof pointer.settings.noCodeStorage === 'boolean') {
|
|
393
|
+
return { noCodeStorage: pointer.settings.noCodeStorage, source: 'pointer' };
|
|
394
|
+
}
|
|
395
|
+
return { noCodeStorage: false, source: 'default' };
|
|
396
|
+
}
|
|
397
|
+
function setNoCodeStorageMode(cwd, enabled) {
|
|
398
|
+
updatePointer(cwd, { settings: { noCodeStorage: enabled } });
|
|
399
|
+
}
|
|
400
|
+
function isNoCodeStorageMode(cwd) {
|
|
401
|
+
return getBrainStorageMode(cwd).noCodeStorage;
|
|
402
|
+
}
|
|
403
|
+
function initDbSchema(db) {
|
|
404
|
+
db.exec(`
|
|
405
|
+
CREATE TABLE IF NOT EXISTS brain_meta (
|
|
406
|
+
key TEXT PRIMARY KEY,
|
|
407
|
+
value TEXT NOT NULL
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
CREATE TABLE IF NOT EXISTS plan_cache (
|
|
411
|
+
key TEXT PRIMARY KEY,
|
|
412
|
+
created_at TEXT NOT NULL,
|
|
413
|
+
last_used_at TEXT NOT NULL,
|
|
414
|
+
use_count INTEGER NOT NULL DEFAULT 1,
|
|
415
|
+
org_id TEXT NOT NULL,
|
|
416
|
+
project_id TEXT NOT NULL,
|
|
417
|
+
repo_kind TEXT NOT NULL,
|
|
418
|
+
repo_identity TEXT NOT NULL,
|
|
419
|
+
head_sha TEXT,
|
|
420
|
+
head_tree_sha TEXT,
|
|
421
|
+
working_tree_hash TEXT,
|
|
422
|
+
file_tree_hash TEXT,
|
|
423
|
+
prompt_hash TEXT NOT NULL,
|
|
424
|
+
policy_version_hash TEXT NOT NULL,
|
|
425
|
+
neurcode_version TEXT NOT NULL,
|
|
426
|
+
intent_norm TEXT NOT NULL,
|
|
427
|
+
intent_hash TEXT NOT NULL,
|
|
428
|
+
ticket_ref TEXT,
|
|
429
|
+
context_hash TEXT,
|
|
430
|
+
response_json TEXT NOT NULL,
|
|
431
|
+
no_code_storage INTEGER NOT NULL DEFAULT 0
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
CREATE INDEX IF NOT EXISTS idx_plan_cache_scope ON plan_cache(org_id, project_id);
|
|
435
|
+
CREATE INDEX IF NOT EXISTS idx_plan_cache_lru ON plan_cache(last_used_at);
|
|
436
|
+
CREATE INDEX IF NOT EXISTS idx_plan_cache_repo ON plan_cache(repo_identity);
|
|
437
|
+
`);
|
|
438
|
+
}
|
|
439
|
+
function getMeta(db, key) {
|
|
440
|
+
try {
|
|
441
|
+
const row = db.prepare('SELECT value FROM brain_meta WHERE key = ?').get(key);
|
|
442
|
+
return row?.value || null;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function setMeta(db, key, value) {
|
|
449
|
+
try {
|
|
450
|
+
db.prepare(`
|
|
451
|
+
INSERT INTO brain_meta (key, value) VALUES (?, ?)
|
|
452
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
453
|
+
`).run(key, value);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
// ignore
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function sanitizeCachedResponseForStorage(response, noCodeStorage) {
|
|
460
|
+
if (!noCodeStorage) {
|
|
461
|
+
return response;
|
|
462
|
+
}
|
|
463
|
+
const summaryMasked = (0, secret_masking_1.maskSecretsInText)(response.plan.summary || '').masked;
|
|
464
|
+
const summaryHash = sha256Hex(summaryMasked);
|
|
465
|
+
const files = (response.plan.files || []).slice(0, 80).map((file) => ({
|
|
466
|
+
path: file.path,
|
|
467
|
+
action: file.action,
|
|
468
|
+
reason: [
|
|
469
|
+
file.reason ? `reasonHash=${sha256Hex((0, secret_masking_1.maskSecretsInText)(file.reason).masked)}` : null,
|
|
470
|
+
file.suggestion ? `suggestionHash=${sha256Hex((0, secret_masking_1.maskSecretsInText)(file.suggestion).masked)}` : null,
|
|
471
|
+
].filter(Boolean).join(' | ') || undefined,
|
|
472
|
+
suggestion: undefined,
|
|
473
|
+
}));
|
|
474
|
+
const recommendations = (response.plan.recommendations || []).slice(0, 20).map((r) => {
|
|
475
|
+
const masked = (0, secret_masking_1.maskSecretsInText)(r).masked;
|
|
476
|
+
return `recHash=${sha256Hex(masked)}`;
|
|
477
|
+
});
|
|
478
|
+
return {
|
|
479
|
+
...response,
|
|
480
|
+
plan: {
|
|
481
|
+
...response.plan,
|
|
482
|
+
summary: `no-code-storage summaryHash=${summaryHash} files=${response.plan.files?.length || 0} recommendations=${response.plan.recommendations?.length || 0}`,
|
|
483
|
+
files,
|
|
484
|
+
recommendations,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function migrateLegacyJsonCache(cwd, db) {
|
|
489
|
+
const migrationKey = 'legacy_plan_cache_migrated_v1';
|
|
490
|
+
if (getMeta(db, migrationKey) === '1')
|
|
491
|
+
return;
|
|
492
|
+
const legacyPath = (0, path_1.join)(cwd, '.neurcode', LEGACY_CACHE_FILE_NAME);
|
|
493
|
+
if (!(0, fs_1.existsSync)(legacyPath)) {
|
|
494
|
+
setMeta(db, migrationKey, '1');
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
const raw = (0, fs_1.readFileSync)(legacyPath, 'utf-8');
|
|
499
|
+
const parsed = JSON.parse(raw);
|
|
500
|
+
const entries = parsed?.entries && typeof parsed.entries === 'object' ? Object.values(parsed.entries) : [];
|
|
501
|
+
const insertStmt = db.prepare(`
|
|
502
|
+
INSERT OR IGNORE INTO plan_cache (
|
|
503
|
+
key,
|
|
504
|
+
created_at,
|
|
505
|
+
last_used_at,
|
|
506
|
+
use_count,
|
|
507
|
+
org_id,
|
|
508
|
+
project_id,
|
|
509
|
+
repo_kind,
|
|
510
|
+
repo_identity,
|
|
511
|
+
head_sha,
|
|
512
|
+
head_tree_sha,
|
|
513
|
+
working_tree_hash,
|
|
514
|
+
file_tree_hash,
|
|
515
|
+
prompt_hash,
|
|
516
|
+
policy_version_hash,
|
|
517
|
+
neurcode_version,
|
|
518
|
+
intent_norm,
|
|
519
|
+
intent_hash,
|
|
520
|
+
ticket_ref,
|
|
521
|
+
context_hash,
|
|
522
|
+
response_json,
|
|
523
|
+
no_code_storage
|
|
524
|
+
) VALUES (
|
|
525
|
+
@key,
|
|
526
|
+
@created_at,
|
|
527
|
+
@last_used_at,
|
|
528
|
+
@use_count,
|
|
529
|
+
@org_id,
|
|
530
|
+
@project_id,
|
|
531
|
+
@repo_kind,
|
|
532
|
+
@repo_identity,
|
|
533
|
+
@head_sha,
|
|
534
|
+
@head_tree_sha,
|
|
535
|
+
@working_tree_hash,
|
|
536
|
+
@file_tree_hash,
|
|
537
|
+
@prompt_hash,
|
|
538
|
+
@policy_version_hash,
|
|
539
|
+
@neurcode_version,
|
|
540
|
+
@intent_norm,
|
|
541
|
+
@intent_hash,
|
|
542
|
+
@ticket_ref,
|
|
543
|
+
@context_hash,
|
|
544
|
+
@response_json,
|
|
545
|
+
@no_code_storage
|
|
546
|
+
)
|
|
547
|
+
`);
|
|
548
|
+
for (const item of entries) {
|
|
549
|
+
const legacy = item;
|
|
550
|
+
const input = legacy?.input || {};
|
|
551
|
+
const response = legacy?.response || null;
|
|
552
|
+
if (!legacy?.key || !input?.orgId || !input?.projectId || !response)
|
|
553
|
+
continue;
|
|
554
|
+
const intentNorm = normalizeIntent(String(input.intent || ''));
|
|
555
|
+
const intentHash = sha256Hex(intentNorm);
|
|
556
|
+
const repoIdentity = getRepoIdentity(cwd);
|
|
557
|
+
const repoKind = input.repo?.kind === 'git' ? 'git' : 'filesystem';
|
|
558
|
+
const promptHash = computePromptHash({
|
|
559
|
+
intent: intentNorm,
|
|
560
|
+
ticketRef: input.ticketRef,
|
|
561
|
+
contextHash: input.contextHash,
|
|
562
|
+
});
|
|
563
|
+
insertStmt.run({
|
|
564
|
+
key: String(legacy.key),
|
|
565
|
+
created_at: String(legacy.createdAt || new Date().toISOString()),
|
|
566
|
+
last_used_at: String(legacy.lastUsedAt || legacy.createdAt || new Date().toISOString()),
|
|
567
|
+
use_count: Number(legacy.useCount || 1),
|
|
568
|
+
org_id: String(input.orgId),
|
|
569
|
+
project_id: String(input.projectId),
|
|
570
|
+
repo_kind: repoKind,
|
|
571
|
+
repo_identity: repoIdentity,
|
|
572
|
+
head_sha: repoKind === 'git' ? String(input.repo?.headSha || '') || null : null,
|
|
573
|
+
head_tree_sha: repoKind === 'git' ? String(input.repo?.headTreeSha || '') || null : null,
|
|
574
|
+
working_tree_hash: repoKind === 'git' ? String(input.repo?.statusHash || '') || null : null,
|
|
575
|
+
file_tree_hash: repoKind === 'filesystem' ? String(input.repo?.fileTreeHash || '') || null : null,
|
|
576
|
+
prompt_hash: promptHash,
|
|
577
|
+
policy_version_hash: 'legacy',
|
|
578
|
+
neurcode_version: 'legacy',
|
|
579
|
+
intent_norm: intentNorm,
|
|
580
|
+
intent_hash: intentHash,
|
|
581
|
+
ticket_ref: input.ticketRef ? String(input.ticketRef) : null,
|
|
582
|
+
context_hash: input.contextHash ? String(input.contextHash) : null,
|
|
583
|
+
response_json: JSON.stringify(response),
|
|
584
|
+
no_code_storage: 0,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
(0, fs_1.renameSync)(legacyPath, legacyPath.replace(/\.json$/, `.migrated-${Date.now()}.json`));
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// ignore rename failures
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
// ignore invalid legacy cache
|
|
596
|
+
}
|
|
597
|
+
setMeta(db, migrationKey, '1');
|
|
598
|
+
}
|
|
599
|
+
function getDb(cwd) {
|
|
600
|
+
const dbPath = getBrainDbPath(cwd);
|
|
601
|
+
const existing = dbConnections.get(dbPath);
|
|
602
|
+
if (existing)
|
|
603
|
+
return existing;
|
|
604
|
+
const Ctor = getSqliteCtor();
|
|
605
|
+
if (!Ctor)
|
|
606
|
+
return null;
|
|
607
|
+
try {
|
|
608
|
+
ensureNeurcodeDir(cwd);
|
|
609
|
+
const db = new Ctor(dbPath);
|
|
610
|
+
db.pragma('journal_mode = WAL');
|
|
611
|
+
db.pragma('synchronous = NORMAL');
|
|
612
|
+
initDbSchema(db);
|
|
613
|
+
migrateLegacyJsonCache(cwd, db);
|
|
614
|
+
dbConnections.set(dbPath, db);
|
|
615
|
+
return db;
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function prune(db) {
|
|
622
|
+
try {
|
|
623
|
+
const row = db.prepare('SELECT COUNT(*) as count FROM plan_cache').get();
|
|
624
|
+
const total = Number(row?.count || 0);
|
|
625
|
+
if (total <= MAX_ENTRIES)
|
|
626
|
+
return;
|
|
627
|
+
const toDelete = total - MAX_ENTRIES;
|
|
628
|
+
db.prepare(`
|
|
629
|
+
DELETE FROM plan_cache
|
|
630
|
+
WHERE key IN (
|
|
631
|
+
SELECT key
|
|
632
|
+
FROM plan_cache
|
|
633
|
+
ORDER BY last_used_at ASC
|
|
634
|
+
LIMIT ?
|
|
635
|
+
)
|
|
636
|
+
`).run(toDelete);
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// ignore
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function toEntry(row) {
|
|
643
|
+
if (!row)
|
|
644
|
+
return null;
|
|
645
|
+
try {
|
|
646
|
+
const response = JSON.parse(row.response_json);
|
|
647
|
+
const repo = row.repo_kind === 'git'
|
|
648
|
+
? {
|
|
649
|
+
kind: 'git',
|
|
650
|
+
repoIdentity: row.repo_identity,
|
|
651
|
+
headSha: row.head_sha || '',
|
|
652
|
+
headTreeSha: row.head_tree_sha || '',
|
|
653
|
+
workingTreeHash: row.working_tree_hash || '',
|
|
654
|
+
}
|
|
655
|
+
: {
|
|
656
|
+
kind: 'filesystem',
|
|
657
|
+
repoIdentity: row.repo_identity,
|
|
658
|
+
fileTreeHash: row.file_tree_hash || '',
|
|
659
|
+
};
|
|
660
|
+
return {
|
|
661
|
+
key: row.key,
|
|
662
|
+
createdAt: row.created_at,
|
|
663
|
+
lastUsedAt: row.last_used_at,
|
|
664
|
+
useCount: Number(row.use_count || 0),
|
|
665
|
+
input: {
|
|
666
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
667
|
+
orgId: row.org_id,
|
|
668
|
+
projectId: row.project_id,
|
|
669
|
+
repo,
|
|
670
|
+
promptHash: row.prompt_hash,
|
|
671
|
+
policyVersionHash: row.policy_version_hash,
|
|
672
|
+
neurcodeVersion: row.neurcode_version,
|
|
673
|
+
intent: row.intent_norm || '',
|
|
674
|
+
intentHash: row.intent_hash,
|
|
675
|
+
ticketRef: row.ticket_ref || undefined,
|
|
676
|
+
contextHash: row.context_hash || undefined,
|
|
677
|
+
},
|
|
678
|
+
response,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
146
684
|
}
|
|
147
685
|
function readCachedPlan(cwd, key) {
|
|
148
686
|
try {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
687
|
+
const db = getDb(cwd);
|
|
688
|
+
if (!db) {
|
|
689
|
+
const cache = readFallbackCache(cwd);
|
|
690
|
+
const existing = cache.entries[key];
|
|
691
|
+
if (!existing)
|
|
692
|
+
return null;
|
|
693
|
+
const now = new Date().toISOString();
|
|
694
|
+
const next = {
|
|
695
|
+
...existing,
|
|
696
|
+
lastUsedAt: now,
|
|
697
|
+
useCount: Number(existing.useCount || 0) + 1,
|
|
698
|
+
};
|
|
699
|
+
cache.entries[key] = next;
|
|
700
|
+
pruneFallback(cache);
|
|
701
|
+
writeFallbackCache(cwd, cache);
|
|
702
|
+
return next;
|
|
703
|
+
}
|
|
704
|
+
const row = db.prepare('SELECT * FROM plan_cache WHERE key = ?').get(key);
|
|
705
|
+
if (!row)
|
|
153
706
|
return null;
|
|
154
|
-
// Update LRU metadata.
|
|
155
707
|
const now = new Date().toISOString();
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
708
|
+
db.prepare(`
|
|
709
|
+
UPDATE plan_cache
|
|
710
|
+
SET last_used_at = ?, use_count = use_count + 1
|
|
711
|
+
WHERE key = ?
|
|
712
|
+
`).run(now, key);
|
|
713
|
+
row.last_used_at = now;
|
|
714
|
+
row.use_count = Number(row.use_count || 0) + 1;
|
|
715
|
+
return toEntry(row);
|
|
162
716
|
}
|
|
163
717
|
catch {
|
|
164
718
|
return null;
|
|
@@ -166,9 +720,13 @@ function readCachedPlan(cwd, key) {
|
|
|
166
720
|
}
|
|
167
721
|
function peekCachedPlan(cwd, key) {
|
|
168
722
|
try {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
723
|
+
const db = getDb(cwd);
|
|
724
|
+
if (!db) {
|
|
725
|
+
const cache = readFallbackCache(cwd);
|
|
726
|
+
return cache.entries[key] || null;
|
|
727
|
+
}
|
|
728
|
+
const row = db.prepare('SELECT * FROM plan_cache WHERE key = ?').get(key);
|
|
729
|
+
return toEntry(row);
|
|
172
730
|
}
|
|
173
731
|
catch {
|
|
174
732
|
return null;
|
|
@@ -176,29 +734,140 @@ function peekCachedPlan(cwd, key) {
|
|
|
176
734
|
}
|
|
177
735
|
function writeCachedPlan(cwd, entry) {
|
|
178
736
|
try {
|
|
179
|
-
ensureNeurcodeDir(cwd);
|
|
180
|
-
const cachePath = getPlanCachePath(cwd);
|
|
181
|
-
const cache = safeReadCacheFile(cachePath);
|
|
182
737
|
const now = new Date().toISOString();
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
738
|
+
const noCodeStorage = isNoCodeStorageMode(cwd);
|
|
739
|
+
const normalizedIntent = normalizeIntent(entry.input.intent || '');
|
|
740
|
+
const safeIntent = (0, secret_masking_1.maskSecretsInText)(normalizedIntent).masked;
|
|
741
|
+
const intentForStorage = noCodeStorage ? '' : safeIntent;
|
|
742
|
+
const intentHash = sha256Hex(safeIntent);
|
|
743
|
+
const responseToStore = sanitizeCachedResponseForStorage(entry.response, noCodeStorage);
|
|
744
|
+
const repo = entry.input.repo;
|
|
745
|
+
const repoIdentity = repo.repoIdentity || fallbackRepoIdentity(cwd);
|
|
746
|
+
const db = getDb(cwd);
|
|
747
|
+
if (!db) {
|
|
748
|
+
const cache = readFallbackCache(cwd);
|
|
749
|
+
const existing = cache.entries[entry.key];
|
|
750
|
+
const next = {
|
|
751
|
+
key: entry.key,
|
|
752
|
+
createdAt: existing?.createdAt || now,
|
|
753
|
+
lastUsedAt: now,
|
|
754
|
+
useCount: Number(existing?.useCount || 0) + 1,
|
|
755
|
+
input: {
|
|
756
|
+
...entry.input,
|
|
757
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
758
|
+
repo: {
|
|
759
|
+
...repo,
|
|
760
|
+
repoIdentity,
|
|
761
|
+
},
|
|
762
|
+
intent: intentForStorage,
|
|
763
|
+
intentHash,
|
|
764
|
+
ticketRef: entry.input.ticketRef,
|
|
765
|
+
contextHash: entry.input.contextHash,
|
|
766
|
+
},
|
|
767
|
+
response: responseToStore,
|
|
768
|
+
};
|
|
769
|
+
cache.entries[entry.key] = next;
|
|
770
|
+
pruneFallback(cache);
|
|
771
|
+
writeFallbackCache(cwd, cache);
|
|
772
|
+
updatePointer(cwd, {
|
|
773
|
+
repoIdentity,
|
|
774
|
+
settings: { noCodeStorage },
|
|
775
|
+
});
|
|
776
|
+
return;
|
|
198
777
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
778
|
+
db.prepare(`
|
|
779
|
+
INSERT INTO plan_cache (
|
|
780
|
+
key,
|
|
781
|
+
created_at,
|
|
782
|
+
last_used_at,
|
|
783
|
+
use_count,
|
|
784
|
+
org_id,
|
|
785
|
+
project_id,
|
|
786
|
+
repo_kind,
|
|
787
|
+
repo_identity,
|
|
788
|
+
head_sha,
|
|
789
|
+
head_tree_sha,
|
|
790
|
+
working_tree_hash,
|
|
791
|
+
file_tree_hash,
|
|
792
|
+
prompt_hash,
|
|
793
|
+
policy_version_hash,
|
|
794
|
+
neurcode_version,
|
|
795
|
+
intent_norm,
|
|
796
|
+
intent_hash,
|
|
797
|
+
ticket_ref,
|
|
798
|
+
context_hash,
|
|
799
|
+
response_json,
|
|
800
|
+
no_code_storage
|
|
801
|
+
) VALUES (
|
|
802
|
+
@key,
|
|
803
|
+
@created_at,
|
|
804
|
+
@last_used_at,
|
|
805
|
+
1,
|
|
806
|
+
@org_id,
|
|
807
|
+
@project_id,
|
|
808
|
+
@repo_kind,
|
|
809
|
+
@repo_identity,
|
|
810
|
+
@head_sha,
|
|
811
|
+
@head_tree_sha,
|
|
812
|
+
@working_tree_hash,
|
|
813
|
+
@file_tree_hash,
|
|
814
|
+
@prompt_hash,
|
|
815
|
+
@policy_version_hash,
|
|
816
|
+
@neurcode_version,
|
|
817
|
+
@intent_norm,
|
|
818
|
+
@intent_hash,
|
|
819
|
+
@ticket_ref,
|
|
820
|
+
@context_hash,
|
|
821
|
+
@response_json,
|
|
822
|
+
@no_code_storage
|
|
823
|
+
)
|
|
824
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
825
|
+
last_used_at = excluded.last_used_at,
|
|
826
|
+
use_count = plan_cache.use_count + 1,
|
|
827
|
+
org_id = excluded.org_id,
|
|
828
|
+
project_id = excluded.project_id,
|
|
829
|
+
repo_kind = excluded.repo_kind,
|
|
830
|
+
repo_identity = excluded.repo_identity,
|
|
831
|
+
head_sha = excluded.head_sha,
|
|
832
|
+
head_tree_sha = excluded.head_tree_sha,
|
|
833
|
+
working_tree_hash = excluded.working_tree_hash,
|
|
834
|
+
file_tree_hash = excluded.file_tree_hash,
|
|
835
|
+
prompt_hash = excluded.prompt_hash,
|
|
836
|
+
policy_version_hash = excluded.policy_version_hash,
|
|
837
|
+
neurcode_version = excluded.neurcode_version,
|
|
838
|
+
intent_norm = excluded.intent_norm,
|
|
839
|
+
intent_hash = excluded.intent_hash,
|
|
840
|
+
ticket_ref = excluded.ticket_ref,
|
|
841
|
+
context_hash = excluded.context_hash,
|
|
842
|
+
response_json = excluded.response_json,
|
|
843
|
+
no_code_storage = excluded.no_code_storage
|
|
844
|
+
`).run({
|
|
845
|
+
key: entry.key,
|
|
846
|
+
created_at: now,
|
|
847
|
+
last_used_at: now,
|
|
848
|
+
org_id: entry.input.orgId,
|
|
849
|
+
project_id: entry.input.projectId,
|
|
850
|
+
repo_kind: repo.kind,
|
|
851
|
+
repo_identity: repoIdentity,
|
|
852
|
+
head_sha: repo.kind === 'git' ? repo.headSha : null,
|
|
853
|
+
head_tree_sha: repo.kind === 'git' ? repo.headTreeSha : null,
|
|
854
|
+
working_tree_hash: repo.kind === 'git' ? repo.workingTreeHash : null,
|
|
855
|
+
file_tree_hash: repo.kind === 'filesystem' ? repo.fileTreeHash : null,
|
|
856
|
+
prompt_hash: entry.input.promptHash,
|
|
857
|
+
policy_version_hash: entry.input.policyVersionHash,
|
|
858
|
+
neurcode_version: entry.input.neurcodeVersion,
|
|
859
|
+
intent_norm: intentForStorage,
|
|
860
|
+
intent_hash: intentHash,
|
|
861
|
+
ticket_ref: entry.input.ticketRef || null,
|
|
862
|
+
context_hash: entry.input.contextHash || null,
|
|
863
|
+
response_json: JSON.stringify(responseToStore),
|
|
864
|
+
no_code_storage: noCodeStorage ? 1 : 0,
|
|
865
|
+
});
|
|
866
|
+
prune(db);
|
|
867
|
+
updatePointer(cwd, {
|
|
868
|
+
repoIdentity,
|
|
869
|
+
settings: { noCodeStorage },
|
|
870
|
+
});
|
|
202
871
|
}
|
|
203
872
|
catch {
|
|
204
873
|
// Cache failures should never block plan generation.
|
|
@@ -206,9 +875,17 @@ function writeCachedPlan(cwd, entry) {
|
|
|
206
875
|
}
|
|
207
876
|
function listCachedPlans(cwd) {
|
|
208
877
|
try {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
878
|
+
const db = getDb(cwd);
|
|
879
|
+
if (!db) {
|
|
880
|
+
const cache = readFallbackCache(cwd);
|
|
881
|
+
return Object.values(cache.entries).sort((a, b) => {
|
|
882
|
+
const aTime = Date.parse(a.lastUsedAt) || 0;
|
|
883
|
+
const bTime = Date.parse(b.lastUsedAt) || 0;
|
|
884
|
+
return bTime - aTime;
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
const rows = db.prepare('SELECT * FROM plan_cache ORDER BY last_used_at DESC').all();
|
|
888
|
+
return rows.map((row) => toEntry(row)).filter(Boolean);
|
|
212
889
|
}
|
|
213
890
|
catch {
|
|
214
891
|
return [];
|
|
@@ -216,21 +893,31 @@ function listCachedPlans(cwd) {
|
|
|
216
893
|
}
|
|
217
894
|
function deleteCachedPlans(cwd, shouldDelete) {
|
|
218
895
|
try {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
896
|
+
const db = getDb(cwd);
|
|
897
|
+
if (!db) {
|
|
898
|
+
const cache = readFallbackCache(cwd);
|
|
899
|
+
let deleted = 0;
|
|
900
|
+
for (const [key, entry] of Object.entries(cache.entries)) {
|
|
901
|
+
if (!entry)
|
|
902
|
+
continue;
|
|
903
|
+
if (shouldDelete(entry)) {
|
|
904
|
+
delete cache.entries[key];
|
|
905
|
+
deleted++;
|
|
906
|
+
}
|
|
229
907
|
}
|
|
908
|
+
pruneFallback(cache);
|
|
909
|
+
writeFallbackCache(cwd, cache);
|
|
910
|
+
return { deleted, remaining: Object.keys(cache.entries).length };
|
|
230
911
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
912
|
+
const rows = db.prepare('SELECT * FROM plan_cache').all();
|
|
913
|
+
const entries = rows.map((row) => toEntry(row)).filter(Boolean);
|
|
914
|
+
const keysToDelete = entries.filter((entry) => shouldDelete(entry)).map((entry) => entry.key);
|
|
915
|
+
const stmt = db.prepare('DELETE FROM plan_cache WHERE key = ?');
|
|
916
|
+
for (const key of keysToDelete) {
|
|
917
|
+
stmt.run(key);
|
|
918
|
+
}
|
|
919
|
+
const remainingRow = db.prepare('SELECT COUNT(*) as count FROM plan_cache').get();
|
|
920
|
+
return { deleted: keysToDelete.length, remaining: Number(remainingRow?.count || 0) };
|
|
234
921
|
}
|
|
235
922
|
catch {
|
|
236
923
|
return { deleted: 0, remaining: 0 };
|
|
@@ -247,34 +934,281 @@ function jaccard(a, b) {
|
|
|
247
934
|
if (a.size === 0 || b.size === 0)
|
|
248
935
|
return 0;
|
|
249
936
|
let inter = 0;
|
|
250
|
-
for (const
|
|
251
|
-
if (b.has(
|
|
937
|
+
for (const token of a) {
|
|
938
|
+
if (b.has(token))
|
|
252
939
|
inter++;
|
|
253
940
|
}
|
|
254
941
|
const union = a.size + b.size - inter;
|
|
255
942
|
return union === 0 ? 0 : inter / union;
|
|
256
943
|
}
|
|
944
|
+
function tokenOverlap(a, b) {
|
|
945
|
+
if (a.size === 0 || b.size === 0)
|
|
946
|
+
return 0;
|
|
947
|
+
let inter = 0;
|
|
948
|
+
for (const token of a) {
|
|
949
|
+
if (b.has(token))
|
|
950
|
+
inter++;
|
|
951
|
+
}
|
|
952
|
+
return inter / Math.max(1, Math.min(a.size, b.size));
|
|
953
|
+
}
|
|
954
|
+
function intentSimilarityScore(aIntent, bIntent) {
|
|
955
|
+
const a = new Set(tokenize(normalizeIntent(aIntent)));
|
|
956
|
+
const b = new Set(tokenize(normalizeIntent(bIntent)));
|
|
957
|
+
if (a.size === 0 || b.size === 0)
|
|
958
|
+
return 0;
|
|
959
|
+
const jac = jaccard(a, b);
|
|
960
|
+
const overlap = tokenOverlap(a, b);
|
|
961
|
+
return jac * 0.7 + overlap * 0.3;
|
|
962
|
+
}
|
|
963
|
+
function sameRepoSnapshot(current, cached) {
|
|
964
|
+
if (current.kind !== cached.kind)
|
|
965
|
+
return false;
|
|
966
|
+
if (current.repoIdentity !== cached.repoIdentity)
|
|
967
|
+
return false;
|
|
968
|
+
if (current.kind === 'git' && cached.kind === 'git') {
|
|
969
|
+
return current.headTreeSha === cached.headTreeSha;
|
|
970
|
+
}
|
|
971
|
+
if (current.kind === 'filesystem' && cached.kind === 'filesystem') {
|
|
972
|
+
return current.fileTreeHash === cached.fileTreeHash;
|
|
973
|
+
}
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
function listScopeEntries(cwd, scope) {
|
|
977
|
+
try {
|
|
978
|
+
const db = getDb(cwd);
|
|
979
|
+
if (!db) {
|
|
980
|
+
const cache = readFallbackCache(cwd);
|
|
981
|
+
return Object.values(cache.entries).filter((entry) => entry.input.orgId === scope.orgId && entry.input.projectId === scope.projectId);
|
|
982
|
+
}
|
|
983
|
+
const rows = db.prepare(`
|
|
984
|
+
SELECT *
|
|
985
|
+
FROM plan_cache
|
|
986
|
+
WHERE org_id = ? AND project_id = ?
|
|
987
|
+
ORDER BY last_used_at DESC
|
|
988
|
+
LIMIT 220
|
|
989
|
+
`).all(scope.orgId, scope.projectId);
|
|
990
|
+
return rows.map((row) => toEntry(row)).filter(Boolean);
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
return [];
|
|
994
|
+
}
|
|
995
|
+
}
|
|
257
996
|
function findSimilarCachedPlans(cwd, filter, intent, k = 3) {
|
|
258
997
|
try {
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
998
|
+
const db = getDb(cwd);
|
|
999
|
+
const normalizedIntent = normalizeIntent(intent);
|
|
1000
|
+
const queryTokens = new Set(tokenize(normalizedIntent));
|
|
1001
|
+
if (queryTokens.size === 0)
|
|
1002
|
+
return [];
|
|
1003
|
+
if (!db) {
|
|
1004
|
+
const cache = readFallbackCache(cwd);
|
|
1005
|
+
const entries = Object.values(cache.entries)
|
|
1006
|
+
.filter((entry) => entry.input.orgId === filter.orgId && entry.input.projectId === filter.projectId)
|
|
1007
|
+
.filter((entry) => !filter.repoIdentity || entry.input.repo.repoIdentity === filter.repoIdentity)
|
|
1008
|
+
.filter((entry) => Boolean(entry.input.intent));
|
|
1009
|
+
const scored = entries
|
|
1010
|
+
.map((entry) => {
|
|
1011
|
+
const tokens = new Set(tokenize(entry.input.intent || ''));
|
|
1012
|
+
const score = jaccard(queryTokens, tokens);
|
|
1013
|
+
if (score <= 0)
|
|
1014
|
+
return null;
|
|
1015
|
+
return { entry, score };
|
|
1016
|
+
})
|
|
1017
|
+
.filter(Boolean);
|
|
1018
|
+
return scored
|
|
1019
|
+
.sort((a, b) => b.score - a.score)
|
|
1020
|
+
.slice(0, k)
|
|
1021
|
+
.map((x) => x.entry);
|
|
1022
|
+
}
|
|
1023
|
+
const rows = filter.repoIdentity
|
|
1024
|
+
? db.prepare(`
|
|
1025
|
+
SELECT *
|
|
1026
|
+
FROM plan_cache
|
|
1027
|
+
WHERE org_id = ? AND project_id = ? AND repo_identity = ? AND intent_norm <> ''
|
|
1028
|
+
ORDER BY last_used_at DESC
|
|
1029
|
+
LIMIT 120
|
|
1030
|
+
`).all(filter.orgId, filter.projectId, filter.repoIdentity)
|
|
1031
|
+
: db.prepare(`
|
|
1032
|
+
SELECT *
|
|
1033
|
+
FROM plan_cache
|
|
1034
|
+
WHERE org_id = ? AND project_id = ? AND intent_norm <> ''
|
|
1035
|
+
ORDER BY last_used_at DESC
|
|
1036
|
+
LIMIT 120
|
|
1037
|
+
`).all(filter.orgId, filter.projectId);
|
|
1038
|
+
const scored = rows
|
|
1039
|
+
.map((row) => {
|
|
1040
|
+
const entry = toEntry(row);
|
|
1041
|
+
if (!entry)
|
|
1042
|
+
return null;
|
|
1043
|
+
if (!entry.input.intent)
|
|
1044
|
+
return null;
|
|
1045
|
+
const tokens = new Set(tokenize(entry.input.intent));
|
|
1046
|
+
const score = jaccard(queryTokens, tokens);
|
|
1047
|
+
if (score <= 0)
|
|
1048
|
+
return null;
|
|
1049
|
+
return { entry, score };
|
|
269
1050
|
})
|
|
270
|
-
.filter(
|
|
1051
|
+
.filter(Boolean);
|
|
1052
|
+
return scored
|
|
271
1053
|
.sort((a, b) => b.score - a.score)
|
|
272
1054
|
.slice(0, k)
|
|
273
|
-
.map((
|
|
274
|
-
return scored;
|
|
1055
|
+
.map((x) => x.entry);
|
|
275
1056
|
}
|
|
276
1057
|
catch {
|
|
277
1058
|
return [];
|
|
278
1059
|
}
|
|
279
1060
|
}
|
|
1061
|
+
function findNearCachedPlan(cwd, input) {
|
|
1062
|
+
try {
|
|
1063
|
+
const normalizedIntent = normalizeIntent(input.intent);
|
|
1064
|
+
if (!normalizedIntent)
|
|
1065
|
+
return null;
|
|
1066
|
+
const queryTokens = new Set(tokenize(normalizedIntent));
|
|
1067
|
+
if (queryTokens.size === 0)
|
|
1068
|
+
return null;
|
|
1069
|
+
const thresholdBase = Math.max(0.68, Math.min(input.minIntentSimilarity ?? 0.74, 0.99));
|
|
1070
|
+
const threshold = queryTokens.size <= 4
|
|
1071
|
+
? Math.max(thresholdBase, 0.9)
|
|
1072
|
+
: queryTokens.size <= 8
|
|
1073
|
+
? Math.max(thresholdBase, 0.74)
|
|
1074
|
+
: thresholdBase;
|
|
1075
|
+
const candidates = listScopeEntries(cwd, {
|
|
1076
|
+
orgId: input.orgId,
|
|
1077
|
+
projectId: input.projectId,
|
|
1078
|
+
});
|
|
1079
|
+
let best = null;
|
|
1080
|
+
for (const candidate of candidates) {
|
|
1081
|
+
if (!candidate.input.intent)
|
|
1082
|
+
continue;
|
|
1083
|
+
if (!sameRepoSnapshot(input.repo, candidate.input.repo))
|
|
1084
|
+
continue;
|
|
1085
|
+
if (candidate.input.policyVersionHash !== input.policyVersionHash)
|
|
1086
|
+
continue;
|
|
1087
|
+
if (candidate.input.neurcodeVersion !== input.neurcodeVersion)
|
|
1088
|
+
continue;
|
|
1089
|
+
if (input.ticketRef && candidate.input.ticketRef && candidate.input.ticketRef !== input.ticketRef)
|
|
1090
|
+
continue;
|
|
1091
|
+
if (input.contextHash && candidate.input.contextHash && candidate.input.contextHash !== input.contextHash)
|
|
1092
|
+
continue;
|
|
1093
|
+
const score = intentSimilarityScore(normalizedIntent, candidate.input.intent);
|
|
1094
|
+
if (score < threshold)
|
|
1095
|
+
continue;
|
|
1096
|
+
const result = {
|
|
1097
|
+
entry: candidate,
|
|
1098
|
+
intentSimilarity: score,
|
|
1099
|
+
reason: 'same_snapshot_similar_intent',
|
|
1100
|
+
};
|
|
1101
|
+
if (!best) {
|
|
1102
|
+
best = result;
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
if (result.intentSimilarity > best.intentSimilarity) {
|
|
1106
|
+
best = result;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (result.intentSimilarity === best.intentSimilarity) {
|
|
1110
|
+
const bestTime = Date.parse(best.entry.lastUsedAt || best.entry.createdAt) || 0;
|
|
1111
|
+
const nextTime = Date.parse(result.entry.lastUsedAt || result.entry.createdAt) || 0;
|
|
1112
|
+
if (nextTime > bestTime)
|
|
1113
|
+
best = result;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return best;
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function diagnosePlanCacheMiss(cwd, input) {
|
|
1123
|
+
const scopeEntries = listScopeEntries(cwd, {
|
|
1124
|
+
orgId: input.orgId,
|
|
1125
|
+
projectId: input.projectId,
|
|
1126
|
+
});
|
|
1127
|
+
const repoEntries = scopeEntries.filter((entry) => entry.input.repo.repoIdentity === input.repo.repoIdentity);
|
|
1128
|
+
const snapshotEntries = repoEntries.filter((entry) => sameRepoSnapshot(input.repo, entry.input.repo));
|
|
1129
|
+
const policyEntries = snapshotEntries.filter((entry) => entry.input.policyVersionHash === input.policyVersionHash);
|
|
1130
|
+
const versionEntries = policyEntries.filter((entry) => entry.input.neurcodeVersion === input.neurcodeVersion);
|
|
1131
|
+
let bestIntentSimilarity = 0;
|
|
1132
|
+
let bestIntent;
|
|
1133
|
+
const normalizedIntent = normalizeIntent(input.intent);
|
|
1134
|
+
if (normalizedIntent) {
|
|
1135
|
+
for (const entry of versionEntries) {
|
|
1136
|
+
if (!entry.input.intent)
|
|
1137
|
+
continue;
|
|
1138
|
+
const score = intentSimilarityScore(normalizedIntent, entry.input.intent);
|
|
1139
|
+
if (score > bestIntentSimilarity) {
|
|
1140
|
+
bestIntentSimilarity = score;
|
|
1141
|
+
bestIntent = entry.input.intent;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
let reason;
|
|
1146
|
+
if (scopeEntries.length === 0)
|
|
1147
|
+
reason = 'no_scope_entries';
|
|
1148
|
+
else if (repoEntries.length === 0)
|
|
1149
|
+
reason = 'repo_identity_changed';
|
|
1150
|
+
else if (snapshotEntries.length === 0)
|
|
1151
|
+
reason = 'repo_snapshot_changed';
|
|
1152
|
+
else if (policyEntries.length === 0)
|
|
1153
|
+
reason = 'policy_changed';
|
|
1154
|
+
else if (versionEntries.length === 0)
|
|
1155
|
+
reason = 'neurcode_version_changed';
|
|
1156
|
+
else
|
|
1157
|
+
reason = 'prompt_changed';
|
|
1158
|
+
return {
|
|
1159
|
+
reason,
|
|
1160
|
+
scopedEntries: scopeEntries.length,
|
|
1161
|
+
repoEntries: repoEntries.length,
|
|
1162
|
+
comparableSnapshotEntries: snapshotEntries.length,
|
|
1163
|
+
policyMatchedEntries: policyEntries.length,
|
|
1164
|
+
versionMatchedEntries: versionEntries.length,
|
|
1165
|
+
bestIntentSimilarity,
|
|
1166
|
+
bestIntent,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function getBrainDbSizeBytes(cwd) {
|
|
1170
|
+
try {
|
|
1171
|
+
const dbPath = getBrainDbPath(cwd);
|
|
1172
|
+
if (!(0, fs_1.existsSync)(dbPath))
|
|
1173
|
+
return null;
|
|
1174
|
+
return (0, fs_1.statSync)(dbPath).size;
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function getBrainStoreBackend(cwd) {
|
|
1181
|
+
const db = getDb(cwd);
|
|
1182
|
+
return db ? 'sqlite' : 'json-fallback';
|
|
1183
|
+
}
|
|
1184
|
+
function closeBrainStore(cwd) {
|
|
1185
|
+
try {
|
|
1186
|
+
if (cwd) {
|
|
1187
|
+
const dbPath = getBrainDbPath(cwd);
|
|
1188
|
+
const db = dbConnections.get(dbPath);
|
|
1189
|
+
if (db) {
|
|
1190
|
+
try {
|
|
1191
|
+
db.close();
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
// ignore
|
|
1195
|
+
}
|
|
1196
|
+
dbConnections.delete(dbPath);
|
|
1197
|
+
}
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
for (const [dbPath, db] of dbConnections.entries()) {
|
|
1201
|
+
try {
|
|
1202
|
+
db.close();
|
|
1203
|
+
}
|
|
1204
|
+
catch {
|
|
1205
|
+
// ignore
|
|
1206
|
+
}
|
|
1207
|
+
dbConnections.delete(dbPath);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
catch {
|
|
1211
|
+
// ignore
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
280
1214
|
//# sourceMappingURL=plan-cache.js.map
|