cli-jaw 1.3.5 → 1.4.0-preview.20260307160721
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/lib/stt.js +39 -9
- package/dist/lib/stt.js.map +1 -1
- package/dist/server.js +127 -5
- package/dist/server.js.map +1 -1
- package/dist/src/agent/spawn.js +38 -2
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/acp-client.js +5 -3
- package/dist/src/cli/acp-client.js.map +1 -1
- package/dist/src/cli/command-context.js +11 -1
- package/dist/src/cli/command-context.js.map +1 -1
- package/dist/src/cli/handlers.js +72 -0
- package/dist/src/cli/handlers.js.map +1 -1
- package/dist/src/core/config.js +15 -0
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/settings-merge.js +11 -2
- package/dist/src/core/settings-merge.js.map +1 -1
- package/dist/src/memory/advanced.js +1108 -0
- package/dist/src/memory/advanced.js.map +1 -0
- package/dist/src/memory/heartbeat-schedule.js +347 -0
- package/dist/src/memory/heartbeat-schedule.js.map +1 -0
- package/dist/src/memory/heartbeat.js +47 -6
- package/dist/src/memory/heartbeat.js.map +1 -1
- package/dist/src/memory/memory.js +2 -0
- package/dist/src/memory/memory.js.map +1 -1
- package/dist/src/orchestrator/distribute.js +2 -1
- package/dist/src/orchestrator/distribute.js.map +1 -1
- package/dist/src/orchestrator/gateway.js +0 -2
- package/dist/src/orchestrator/gateway.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +85 -7
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/orchestrator/research.js +149 -0
- package/dist/src/orchestrator/research.js.map +1 -0
- package/dist/src/orchestrator/state-machine.js +2 -2
- package/dist/src/orchestrator/state-machine.js.map +1 -1
- package/dist/src/prompt/builder.js +51 -18
- package/dist/src/prompt/builder.js.map +1 -1
- package/dist/src/prompt/templates/a1-system.md +3 -1
- package/dist/src/prompt/templates/a2-default.md +8 -0
- package/dist/src/prompt/templates/research-worker.md +37 -0
- package/dist/src/prompt/templates/worker-context.md +3 -0
- package/package.json +1 -1
- package/public/css/modals.css +46 -1
- package/public/dist/bundle.js +68 -57
- package/public/dist/bundle.js.map +4 -4
- package/public/index.html +142 -3
- package/public/js/constants.ts +1 -0
- package/public/js/features/employees.ts +2 -0
- package/public/js/features/heartbeat.ts +153 -27
- package/public/js/features/memory.ts +364 -52
- package/public/js/main.ts +72 -5
- package/public/js/state.ts +18 -1
- package/public/locales/en.json +14 -1
- package/public/locales/ko.json +14 -1
- package/scripts/release-preview.sh +48 -0
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { join, relative, dirname } from 'path';
|
|
5
|
+
import { createHash, createSign } from 'crypto';
|
|
6
|
+
import { JAW_HOME, settings } from '../core/config.js';
|
|
7
|
+
import { instanceId } from '../core/instance.js';
|
|
8
|
+
import { getMemory } from '../core/db.js';
|
|
9
|
+
const DEFAULT_IMPORTED_COUNTS = { core: 0, markdown: 0, kv: 0, claude: 0 };
|
|
10
|
+
let lastExpansionTerms = [];
|
|
11
|
+
export function getAdvancedMemoryDir() {
|
|
12
|
+
return join(JAW_HOME, 'memory-advanced');
|
|
13
|
+
}
|
|
14
|
+
export function getAdvancedMemoryBackupDir() {
|
|
15
|
+
return join(JAW_HOME, 'backup-memory-v1');
|
|
16
|
+
}
|
|
17
|
+
function getAdvancedIndexDbPath() {
|
|
18
|
+
return join(getAdvancedMemoryDir(), 'index.sqlite');
|
|
19
|
+
}
|
|
20
|
+
export function normalizeOpenAiCompatibleBaseUrl(raw) {
|
|
21
|
+
const value = String(raw || '').trim();
|
|
22
|
+
if (!value)
|
|
23
|
+
return '';
|
|
24
|
+
const trimmed = value.replace(/\/+$/, '');
|
|
25
|
+
return /\/v1$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
|
|
26
|
+
}
|
|
27
|
+
function ensureDir(path) {
|
|
28
|
+
fs.mkdirSync(path, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
function safeReadFile(path) {
|
|
31
|
+
try {
|
|
32
|
+
return fs.readFileSync(path, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function writeText(path, content) {
|
|
39
|
+
ensureDir(dirname(path));
|
|
40
|
+
fs.writeFileSync(path, content);
|
|
41
|
+
}
|
|
42
|
+
function frontmatter(meta) {
|
|
43
|
+
const lines = ['---'];
|
|
44
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
45
|
+
lines.push(`${k}: ${v}`);
|
|
46
|
+
}
|
|
47
|
+
lines.push('---', '');
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
function hashText(text) {
|
|
51
|
+
return createHash('sha1').update(text).digest('hex').slice(0, 12);
|
|
52
|
+
}
|
|
53
|
+
function heuristicKeywords(query) {
|
|
54
|
+
const q = String(query || '').trim();
|
|
55
|
+
if (!q)
|
|
56
|
+
return [];
|
|
57
|
+
const tokens = q.split(/[\s,]+/).map(t => t.trim()).filter(Boolean);
|
|
58
|
+
const out = new Set([q, ...tokens]);
|
|
59
|
+
const lower = q.toLowerCase();
|
|
60
|
+
if (/login|로그인|auth|인증/.test(lower)) {
|
|
61
|
+
out.add('login');
|
|
62
|
+
out.add('auth');
|
|
63
|
+
out.add('인증');
|
|
64
|
+
out.add('401');
|
|
65
|
+
}
|
|
66
|
+
if (/launchd|service|plist|시작 안됨/.test(lower)) {
|
|
67
|
+
out.add('launchd');
|
|
68
|
+
out.add('plist');
|
|
69
|
+
out.add('service');
|
|
70
|
+
}
|
|
71
|
+
return [...out].slice(0, 5);
|
|
72
|
+
}
|
|
73
|
+
function sanitizeKeywords(input) {
|
|
74
|
+
const raw = Array.isArray(input) ? input : [];
|
|
75
|
+
const out = [];
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
for (const item of raw) {
|
|
78
|
+
const value = String(item || '')
|
|
79
|
+
.replace(/[;&|`$><]/g, ' ')
|
|
80
|
+
.replace(/\s+/g, ' ')
|
|
81
|
+
.trim()
|
|
82
|
+
.slice(0, 48);
|
|
83
|
+
if (!value)
|
|
84
|
+
continue;
|
|
85
|
+
const key = value.toLowerCase();
|
|
86
|
+
if (seen.has(key))
|
|
87
|
+
continue;
|
|
88
|
+
seen.add(key);
|
|
89
|
+
out.push(value);
|
|
90
|
+
if (out.length >= 5)
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
function extractJsonArray(text) {
|
|
96
|
+
const trimmed = String(text || '').trim();
|
|
97
|
+
if (!trimmed)
|
|
98
|
+
return [];
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(trimmed);
|
|
101
|
+
return sanitizeKeywords(parsed);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
const match = trimmed.match(/\[[\s\S]*\]/);
|
|
105
|
+
if (match) {
|
|
106
|
+
try {
|
|
107
|
+
return sanitizeKeywords(JSON.parse(match[0]));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function expandViaGemini(query) {
|
|
117
|
+
const apiKey = settings.memoryAdvanced?.apiKey || process.env.GEMINI_API_KEY || '';
|
|
118
|
+
const model = settings.memoryAdvanced?.model || 'gemini-3.1-flash-lite-preview';
|
|
119
|
+
if (!apiKey)
|
|
120
|
+
return [];
|
|
121
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
122
|
+
const body = {
|
|
123
|
+
contents: [{
|
|
124
|
+
parts: [{
|
|
125
|
+
text: `사용자 질문을 바탕으로 로컬 마크다운 기억을 뒤질 검색 키워드 5개를 JSON 배열로만 출력해라.
|
|
126
|
+
- 한국어, 영어, 동의어, 에러코드, 모듈명 포함 가능
|
|
127
|
+
- 예: ["로그인","login","auth","401","인증"]
|
|
128
|
+
|
|
129
|
+
질문: ${query}`,
|
|
130
|
+
}],
|
|
131
|
+
}],
|
|
132
|
+
};
|
|
133
|
+
const res = await fetch(url, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify(body),
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok)
|
|
139
|
+
return [];
|
|
140
|
+
const json = await res.json();
|
|
141
|
+
const text = json?.candidates?.[0]?.content?.parts?.map((p) => p?.text || '').join('\n') || '';
|
|
142
|
+
return extractJsonArray(text);
|
|
143
|
+
}
|
|
144
|
+
async function expandViaOpenAiCompatible(query) {
|
|
145
|
+
const apiKey = settings.memoryAdvanced?.apiKey || '';
|
|
146
|
+
const baseUrl = normalizeOpenAiCompatibleBaseUrl(settings.memoryAdvanced?.baseUrl || '');
|
|
147
|
+
const model = settings.memoryAdvanced?.model || 'gpt-4o-mini';
|
|
148
|
+
if (!apiKey || !baseUrl)
|
|
149
|
+
return [];
|
|
150
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
Authorization: `Bearer ${apiKey}`,
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
model,
|
|
158
|
+
temperature: 0,
|
|
159
|
+
response_format: { type: 'json_object' },
|
|
160
|
+
messages: [
|
|
161
|
+
{
|
|
162
|
+
role: 'system',
|
|
163
|
+
content: 'Return only valid JSON: {"keywords":["k1","k2","k3"]}',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
role: 'user',
|
|
167
|
+
content: `Expand this user query into up to 5 search keywords for local markdown search. Include Korean, English, synonyms, error codes, module names when helpful.\nQuery: ${query}`,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
if (!res.ok)
|
|
173
|
+
return [];
|
|
174
|
+
const json = await res.json();
|
|
175
|
+
const text = json?.choices?.[0]?.message?.content || '';
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(text);
|
|
178
|
+
return sanitizeKeywords(parsed.keywords);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return extractJsonArray(text);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function loadServiceAccount(config) {
|
|
185
|
+
if (config.credentials_json || config.credentialsJson)
|
|
186
|
+
return config.credentials_json || config.credentialsJson;
|
|
187
|
+
const path = config.credentials_path || config.credentialsPath || '';
|
|
188
|
+
if (!path)
|
|
189
|
+
return null;
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function getGoogleAccessToken(sa) {
|
|
198
|
+
const iat = Math.floor(Date.now() / 1000);
|
|
199
|
+
const exp = iat + 3600;
|
|
200
|
+
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
|
|
201
|
+
const claim = Buffer.from(JSON.stringify({
|
|
202
|
+
iss: sa.client_email,
|
|
203
|
+
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
|
204
|
+
aud: sa.token_uri,
|
|
205
|
+
exp,
|
|
206
|
+
iat,
|
|
207
|
+
})).toString('base64url');
|
|
208
|
+
const unsigned = `${header}.${claim}`;
|
|
209
|
+
const signer = createSign('RSA-SHA256');
|
|
210
|
+
signer.update(unsigned);
|
|
211
|
+
signer.end();
|
|
212
|
+
const signature = signer.sign(sa.private_key).toString('base64url');
|
|
213
|
+
const assertion = `${unsigned}.${signature}`;
|
|
214
|
+
const body = new URLSearchParams({
|
|
215
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
216
|
+
assertion,
|
|
217
|
+
});
|
|
218
|
+
const res = await fetch(sa.token_uri, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
221
|
+
body,
|
|
222
|
+
});
|
|
223
|
+
if (!res.ok)
|
|
224
|
+
return '';
|
|
225
|
+
const json = await res.json();
|
|
226
|
+
return json?.access_token || '';
|
|
227
|
+
}
|
|
228
|
+
async function expandViaVertex(query) {
|
|
229
|
+
const cfgRaw = settings.memoryAdvanced?.vertexConfig || '';
|
|
230
|
+
if (!cfgRaw)
|
|
231
|
+
return [];
|
|
232
|
+
let cfg;
|
|
233
|
+
try {
|
|
234
|
+
cfg = JSON.parse(cfgRaw);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
let endpoint = cfg.endpoint || '';
|
|
240
|
+
let token = cfg.token || '';
|
|
241
|
+
const model = settings.memoryAdvanced?.model || cfg.model || 'gemini-3.1-flash-lite-preview';
|
|
242
|
+
if (!endpoint) {
|
|
243
|
+
const sa = loadServiceAccount(cfg);
|
|
244
|
+
const project = cfg.project_id || cfg.projectId || sa?.project_id;
|
|
245
|
+
const location = cfg.location || 'us-central1';
|
|
246
|
+
if (sa && !token)
|
|
247
|
+
token = await getGoogleAccessToken(sa);
|
|
248
|
+
if (project) {
|
|
249
|
+
endpoint = `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google/models/${model}:generateContent`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (!endpoint || !token)
|
|
253
|
+
return [];
|
|
254
|
+
const body = {
|
|
255
|
+
contents: [{
|
|
256
|
+
role: 'user',
|
|
257
|
+
parts: [{
|
|
258
|
+
text: `Return only a JSON array of up to 5 search keywords for local markdown search. Include Korean, English, synonyms, error codes, module names when useful.\nQuery: ${query}`,
|
|
259
|
+
}],
|
|
260
|
+
}],
|
|
261
|
+
};
|
|
262
|
+
const res = await fetch(endpoint, {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: {
|
|
265
|
+
'Content-Type': 'application/json',
|
|
266
|
+
Authorization: `Bearer ${token}`,
|
|
267
|
+
},
|
|
268
|
+
body: JSON.stringify(body),
|
|
269
|
+
});
|
|
270
|
+
if (!res.ok)
|
|
271
|
+
return [];
|
|
272
|
+
const json = await res.json();
|
|
273
|
+
const text = json?.candidates?.[0]?.content?.parts?.map((p) => p?.text || '').join('\n') || '';
|
|
274
|
+
return extractJsonArray(text);
|
|
275
|
+
}
|
|
276
|
+
export async function expandSearchKeywords(query) {
|
|
277
|
+
const q = String(query || '').trim();
|
|
278
|
+
if (!q)
|
|
279
|
+
return [];
|
|
280
|
+
const provider = settings.memoryAdvanced?.provider || 'gemini';
|
|
281
|
+
let expanded = [];
|
|
282
|
+
try {
|
|
283
|
+
if (provider === 'gemini')
|
|
284
|
+
expanded = await expandViaGemini(q);
|
|
285
|
+
else if (provider === 'openai-compatible')
|
|
286
|
+
expanded = await expandViaOpenAiCompatible(q);
|
|
287
|
+
else if (provider === 'vertex')
|
|
288
|
+
expanded = await expandViaVertex(q);
|
|
289
|
+
else
|
|
290
|
+
expanded = [];
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
expanded = [];
|
|
294
|
+
}
|
|
295
|
+
const merged = sanitizeKeywords(expanded.length ? [q, ...expanded] : heuristicKeywords(q));
|
|
296
|
+
lastExpansionTerms = merged;
|
|
297
|
+
return merged;
|
|
298
|
+
}
|
|
299
|
+
function slug(value) {
|
|
300
|
+
return value
|
|
301
|
+
.replace(/\\/g, '/')
|
|
302
|
+
.replace(/[^a-zA-Z0-9._/-]+/g, '-')
|
|
303
|
+
.replace(/-+/g, '-')
|
|
304
|
+
.replace(/^-|-$/g, '');
|
|
305
|
+
}
|
|
306
|
+
function listMarkdownFiles(dir) {
|
|
307
|
+
if (!fs.existsSync(dir))
|
|
308
|
+
return [];
|
|
309
|
+
const out = [];
|
|
310
|
+
const walk = (current) => {
|
|
311
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
312
|
+
if (entry.name.startsWith('.'))
|
|
313
|
+
continue;
|
|
314
|
+
const full = join(current, entry.name);
|
|
315
|
+
if (entry.isDirectory())
|
|
316
|
+
walk(full);
|
|
317
|
+
else if (entry.name.endsWith('.md'))
|
|
318
|
+
out.push(full);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
walk(dir);
|
|
322
|
+
return out.sort();
|
|
323
|
+
}
|
|
324
|
+
function countMarkdownFiles(dir) {
|
|
325
|
+
return listMarkdownFiles(dir).length;
|
|
326
|
+
}
|
|
327
|
+
function countFiles(dir) {
|
|
328
|
+
if (!fs.existsSync(dir))
|
|
329
|
+
return 0;
|
|
330
|
+
let count = 0;
|
|
331
|
+
const walk = (current) => {
|
|
332
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
333
|
+
if (entry.name.startsWith('.'))
|
|
334
|
+
continue;
|
|
335
|
+
const full = join(current, entry.name);
|
|
336
|
+
if (entry.isDirectory())
|
|
337
|
+
walk(full);
|
|
338
|
+
else
|
|
339
|
+
count++;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
walk(dir);
|
|
343
|
+
return count;
|
|
344
|
+
}
|
|
345
|
+
function getMetaPath() {
|
|
346
|
+
return join(getAdvancedMemoryDir(), 'meta.json');
|
|
347
|
+
}
|
|
348
|
+
function readMeta() {
|
|
349
|
+
const metaPath = getMetaPath();
|
|
350
|
+
if (!fs.existsSync(metaPath))
|
|
351
|
+
return null;
|
|
352
|
+
try {
|
|
353
|
+
return JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function writeMeta(patch) {
|
|
360
|
+
const base = readMeta() || {
|
|
361
|
+
schemaVersion: 1,
|
|
362
|
+
phase: '1',
|
|
363
|
+
homeId: instanceId(),
|
|
364
|
+
jawHome: JAW_HOME,
|
|
365
|
+
initializedAt: new Date().toISOString(),
|
|
366
|
+
bootstrapStatus: 'idle',
|
|
367
|
+
importedCounts: { ...DEFAULT_IMPORTED_COUNTS },
|
|
368
|
+
};
|
|
369
|
+
const next = {
|
|
370
|
+
...base,
|
|
371
|
+
...patch,
|
|
372
|
+
importedCounts: {
|
|
373
|
+
...(base.importedCounts || DEFAULT_IMPORTED_COUNTS),
|
|
374
|
+
...(patch.importedCounts || {}),
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
writeText(getMetaPath(), JSON.stringify(next, null, 2));
|
|
378
|
+
return next;
|
|
379
|
+
}
|
|
380
|
+
function parseLegacyMemorySections(content) {
|
|
381
|
+
const sections = {
|
|
382
|
+
userPreferences: '',
|
|
383
|
+
keyDecisions: '',
|
|
384
|
+
activeProjects: '',
|
|
385
|
+
};
|
|
386
|
+
const patterns = [
|
|
387
|
+
{ key: 'userPreferences', re: /## User Preferences([\s\S]*?)(?=\n## |\s*$)/i },
|
|
388
|
+
{ key: 'keyDecisions', re: /## Key Decisions([\s\S]*?)(?=\n## |\s*$)/i },
|
|
389
|
+
{ key: 'activeProjects', re: /## Active Projects([\s\S]*?)(?=\n## |\s*$)/i },
|
|
390
|
+
];
|
|
391
|
+
for (const p of patterns) {
|
|
392
|
+
const match = p.re.exec(content);
|
|
393
|
+
if (match?.[1])
|
|
394
|
+
sections[p.key] = match[1].trim();
|
|
395
|
+
}
|
|
396
|
+
return sections;
|
|
397
|
+
}
|
|
398
|
+
function getLegacyClaudeMemoryDir() {
|
|
399
|
+
const wd = (settings.workingDir || os.homedir()).replace(/^~/, os.homedir());
|
|
400
|
+
const hash = wd.replace(/\//g, '-');
|
|
401
|
+
return join(os.homedir(), '.claude', 'projects', hash, 'memory');
|
|
402
|
+
}
|
|
403
|
+
function importCoreMemory(root) {
|
|
404
|
+
const corePath = join(JAW_HOME, 'memory', 'MEMORY.md');
|
|
405
|
+
if (!fs.existsSync(corePath))
|
|
406
|
+
return 0;
|
|
407
|
+
const profilePath = join(root, 'profile.md');
|
|
408
|
+
if (fs.existsSync(profilePath)) {
|
|
409
|
+
// Profile already exists — preserve user edits, skip overwrite
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
const content = safeReadFile(corePath);
|
|
413
|
+
const parsed = parseLegacyMemorySections(content);
|
|
414
|
+
const body = `# Profile
|
|
415
|
+
|
|
416
|
+
## User Preferences
|
|
417
|
+
${parsed.userPreferences || ''}
|
|
418
|
+
|
|
419
|
+
## Key Decisions
|
|
420
|
+
${parsed.keyDecisions || ''}
|
|
421
|
+
|
|
422
|
+
## Active Projects
|
|
423
|
+
${parsed.activeProjects || ''}
|
|
424
|
+
`;
|
|
425
|
+
const fm = frontmatter({
|
|
426
|
+
id: `profile-${instanceId()}`,
|
|
427
|
+
home_id: instanceId(),
|
|
428
|
+
kind: 'profile',
|
|
429
|
+
source: 'legacy-memory-md',
|
|
430
|
+
trust_level: 'high',
|
|
431
|
+
created_at: new Date().toISOString(),
|
|
432
|
+
updated_at: new Date().toISOString(),
|
|
433
|
+
});
|
|
434
|
+
writeText(profilePath, fm + body);
|
|
435
|
+
return 1;
|
|
436
|
+
}
|
|
437
|
+
function importMarkdownMemory(root) {
|
|
438
|
+
const legacyDir = join(JAW_HOME, 'memory');
|
|
439
|
+
if (!fs.existsSync(legacyDir))
|
|
440
|
+
return 0;
|
|
441
|
+
const files = listMarkdownFiles(legacyDir).filter(f => f !== join(legacyDir, 'MEMORY.md'));
|
|
442
|
+
let imported = 0;
|
|
443
|
+
for (const file of files) {
|
|
444
|
+
const rel = relative(legacyDir, file).replace(/\\/g, '/');
|
|
445
|
+
const body = safeReadFile(file);
|
|
446
|
+
const sourceHash = hashText(body);
|
|
447
|
+
const baseName = rel.split('/').pop() || 'memory.md';
|
|
448
|
+
const isDated = /^\d{4}-\d{2}-\d{2}\.md$/.test(baseName);
|
|
449
|
+
const sectionDir = isDated ? 'episodes/imported' : 'semantic/imported';
|
|
450
|
+
const dest = join(root, sectionDir, rel);
|
|
451
|
+
const fm = frontmatter({
|
|
452
|
+
id: `import-${sourceHash}`,
|
|
453
|
+
home_id: instanceId(),
|
|
454
|
+
kind: isDated ? 'episode' : 'semantic',
|
|
455
|
+
source: 'legacy-markdown',
|
|
456
|
+
trust_level: 'high',
|
|
457
|
+
source_relpath: rel,
|
|
458
|
+
source_hash: sourceHash,
|
|
459
|
+
created_at: new Date().toISOString(),
|
|
460
|
+
updated_at: new Date().toISOString(),
|
|
461
|
+
});
|
|
462
|
+
writeText(dest, fm + body.trim() + '\n');
|
|
463
|
+
imported += 1;
|
|
464
|
+
}
|
|
465
|
+
return imported;
|
|
466
|
+
}
|
|
467
|
+
function importKvMemory(root) {
|
|
468
|
+
const rows = getMemory.all();
|
|
469
|
+
if (!rows.length)
|
|
470
|
+
return 0;
|
|
471
|
+
const lines = rows.map(r => `- \`${r.key}\`: ${r.value} ${r.source ? `(source: ${r.source})` : ''}`);
|
|
472
|
+
const fm = frontmatter({
|
|
473
|
+
id: `kv-${instanceId()}`,
|
|
474
|
+
home_id: instanceId(),
|
|
475
|
+
kind: 'semantic',
|
|
476
|
+
source: 'legacy-kv-table',
|
|
477
|
+
trust_level: 'high',
|
|
478
|
+
created_at: new Date().toISOString(),
|
|
479
|
+
updated_at: new Date().toISOString(),
|
|
480
|
+
});
|
|
481
|
+
writeText(join(root, 'semantic', 'kv-imported.md'), fm + '# Imported KV Memory\n\n' + lines.join('\n') + '\n');
|
|
482
|
+
return rows.length;
|
|
483
|
+
}
|
|
484
|
+
function importClaudeSessionMemory(root) {
|
|
485
|
+
const claudeDir = getLegacyClaudeMemoryDir();
|
|
486
|
+
if (!fs.existsSync(claudeDir))
|
|
487
|
+
return 0;
|
|
488
|
+
const files = listMarkdownFiles(claudeDir);
|
|
489
|
+
let imported = 0;
|
|
490
|
+
for (const file of files) {
|
|
491
|
+
const real = fs.realpathSync(file);
|
|
492
|
+
const body = safeReadFile(file);
|
|
493
|
+
const sourceHash = hashText(body);
|
|
494
|
+
const base = slug(relative(claudeDir, real).replace(/\\/g, '/')) || slug(file.split('/').pop() || 'legacy');
|
|
495
|
+
const dest = join(root, 'episodes', 'legacy', `${base}-${sourceHash}.md`);
|
|
496
|
+
const fm = frontmatter({
|
|
497
|
+
id: `claude-${sourceHash}`,
|
|
498
|
+
home_id: instanceId(),
|
|
499
|
+
kind: 'episode',
|
|
500
|
+
source: 'external-claude-memory',
|
|
501
|
+
trust_level: 'medium',
|
|
502
|
+
source_realpath: real.replace(/\\/g, '/'),
|
|
503
|
+
source_hash: sourceHash,
|
|
504
|
+
created_at: new Date().toISOString(),
|
|
505
|
+
updated_at: new Date().toISOString(),
|
|
506
|
+
});
|
|
507
|
+
writeText(dest, fm + body.trim() + '\n');
|
|
508
|
+
imported += 1;
|
|
509
|
+
}
|
|
510
|
+
return imported;
|
|
511
|
+
}
|
|
512
|
+
function backupLegacyMemory() {
|
|
513
|
+
const backupRoot = getAdvancedMemoryBackupDir();
|
|
514
|
+
ensureDir(backupRoot);
|
|
515
|
+
const legacyMemoryDir = join(JAW_HOME, 'memory');
|
|
516
|
+
const backupMemoryDir = join(backupRoot, 'memory');
|
|
517
|
+
if (fs.existsSync(legacyMemoryDir)) {
|
|
518
|
+
fs.rmSync(backupMemoryDir, { recursive: true, force: true });
|
|
519
|
+
fs.cpSync(legacyMemoryDir, backupMemoryDir, { recursive: true });
|
|
520
|
+
}
|
|
521
|
+
const kvRows = getMemory.all();
|
|
522
|
+
writeText(join(backupRoot, 'memory-kv.json'), JSON.stringify(kvRows, null, 2));
|
|
523
|
+
return backupRoot;
|
|
524
|
+
}
|
|
525
|
+
function parseMarkdownFile(raw) {
|
|
526
|
+
const lines = raw.replace(/\r\n/g, '\n').split('\n');
|
|
527
|
+
if (lines[0] !== '---') {
|
|
528
|
+
return { meta: {}, body: raw, bodyStartLine: 1 };
|
|
529
|
+
}
|
|
530
|
+
const closing = lines.findIndex((line, idx) => idx > 0 && line === '---');
|
|
531
|
+
if (closing === -1) {
|
|
532
|
+
return { meta: {}, body: raw, bodyStartLine: 1 };
|
|
533
|
+
}
|
|
534
|
+
const meta = {};
|
|
535
|
+
for (const line of lines.slice(1, closing)) {
|
|
536
|
+
const idx = line.indexOf(':');
|
|
537
|
+
if (idx === -1)
|
|
538
|
+
continue;
|
|
539
|
+
const key = line.slice(0, idx).trim();
|
|
540
|
+
const value = line.slice(idx + 1).trim();
|
|
541
|
+
if (key)
|
|
542
|
+
meta[key] = value;
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
meta,
|
|
546
|
+
body: lines.slice(closing + 1).join('\n'),
|
|
547
|
+
bodyStartLine: closing + 2,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function buildHeaderPath(stack) {
|
|
551
|
+
return stack.filter(Boolean).join(' > ');
|
|
552
|
+
}
|
|
553
|
+
function chunkMarkdown(absPath, relpath, kind) {
|
|
554
|
+
const raw = safeReadFile(absPath);
|
|
555
|
+
const parsed = parseMarkdownFile(raw);
|
|
556
|
+
const lines = parsed.body.split('\n');
|
|
557
|
+
const chunks = [];
|
|
558
|
+
const headings = [];
|
|
559
|
+
let currentStart = parsed.bodyStartLine;
|
|
560
|
+
let currentBody = [];
|
|
561
|
+
let currentHeader = '';
|
|
562
|
+
const flush = (endLine) => {
|
|
563
|
+
const body = currentBody.join('\n').trim();
|
|
564
|
+
if (!body)
|
|
565
|
+
return;
|
|
566
|
+
const headerPath = buildHeaderPath(headings);
|
|
567
|
+
const prefix = [
|
|
568
|
+
`Source: ${relpath}`,
|
|
569
|
+
`Kind: ${kind}`,
|
|
570
|
+
headerPath ? `Header: ${headerPath}` : '',
|
|
571
|
+
].filter(Boolean).join('\n');
|
|
572
|
+
const content = `${prefix}\n\n${body}`.trim();
|
|
573
|
+
chunks.push({
|
|
574
|
+
relpath,
|
|
575
|
+
path: absPath,
|
|
576
|
+
kind,
|
|
577
|
+
source_start_line: currentStart,
|
|
578
|
+
source_end_line: endLine,
|
|
579
|
+
source_hash: hashText(`${relpath}:${currentStart}:${body}`),
|
|
580
|
+
content,
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
584
|
+
const line = lines[idx] ?? '';
|
|
585
|
+
const actualLine = parsed.bodyStartLine + idx;
|
|
586
|
+
const headerMatch = /^(#{1,3})\s+(.+)$/.exec(line.trim());
|
|
587
|
+
if (headerMatch) {
|
|
588
|
+
flush(actualLine - 1);
|
|
589
|
+
const level = headerMatch[1]?.length || 1;
|
|
590
|
+
headings[level - 1] = headerMatch[2]?.trim() || '';
|
|
591
|
+
headings.length = level;
|
|
592
|
+
currentHeader = headerMatch[2]?.trim() || '';
|
|
593
|
+
currentStart = actualLine;
|
|
594
|
+
currentBody = [line];
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (!currentBody.length) {
|
|
598
|
+
currentStart = actualLine;
|
|
599
|
+
currentBody = currentHeader ? [currentHeader, line] : [line];
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
currentBody.push(line);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
flush(parsed.bodyStartLine + lines.length - 1);
|
|
606
|
+
if (chunks.length === 0 && parsed.body.trim()) {
|
|
607
|
+
chunks.push({
|
|
608
|
+
relpath,
|
|
609
|
+
path: absPath,
|
|
610
|
+
kind,
|
|
611
|
+
source_start_line: parsed.bodyStartLine,
|
|
612
|
+
source_end_line: parsed.bodyStartLine + lines.length - 1,
|
|
613
|
+
source_hash: hashText(`${relpath}:${parsed.body}`),
|
|
614
|
+
content: parsed.body.trim(),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return chunks;
|
|
618
|
+
}
|
|
619
|
+
function getIndexDb() {
|
|
620
|
+
ensureDir(getAdvancedMemoryDir());
|
|
621
|
+
const db = new Database(getAdvancedIndexDbPath());
|
|
622
|
+
db.pragma('journal_mode = WAL');
|
|
623
|
+
db.exec(`
|
|
624
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
625
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
626
|
+
path TEXT NOT NULL,
|
|
627
|
+
relpath TEXT NOT NULL,
|
|
628
|
+
kind TEXT NOT NULL,
|
|
629
|
+
home_id TEXT NOT NULL DEFAULT '',
|
|
630
|
+
source_start_line INTEGER NOT NULL,
|
|
631
|
+
source_end_line INTEGER NOT NULL,
|
|
632
|
+
source_hash TEXT NOT NULL,
|
|
633
|
+
content TEXT NOT NULL,
|
|
634
|
+
content_hash TEXT NOT NULL DEFAULT '',
|
|
635
|
+
created_at TEXT NOT NULL DEFAULT ''
|
|
636
|
+
);
|
|
637
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_relpath ON chunks(relpath);
|
|
638
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_path ON chunks(path);
|
|
639
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
640
|
+
content,
|
|
641
|
+
relpath UNINDEXED,
|
|
642
|
+
kind UNINDEXED,
|
|
643
|
+
tokenize = 'unicode61'
|
|
644
|
+
);
|
|
645
|
+
`);
|
|
646
|
+
return db;
|
|
647
|
+
}
|
|
648
|
+
function clearIndex(db) {
|
|
649
|
+
db.exec('DELETE FROM chunks;');
|
|
650
|
+
db.exec(`DELETE FROM chunks_fts;`);
|
|
651
|
+
}
|
|
652
|
+
function indexedFiles(root) {
|
|
653
|
+
const buckets = [
|
|
654
|
+
join(root, 'profile.md'),
|
|
655
|
+
...listMarkdownFiles(join(root, 'shared')),
|
|
656
|
+
...listMarkdownFiles(join(root, 'episodes')),
|
|
657
|
+
...listMarkdownFiles(join(root, 'semantic')),
|
|
658
|
+
...listMarkdownFiles(join(root, 'procedures')),
|
|
659
|
+
];
|
|
660
|
+
return buckets.filter((value, idx, arr) => value && arr.indexOf(value) === idx && fs.existsSync(value));
|
|
661
|
+
}
|
|
662
|
+
function kindForFile(root, file) {
|
|
663
|
+
const rel = relative(root, file).replace(/\\/g, '/');
|
|
664
|
+
if (rel === 'profile.md')
|
|
665
|
+
return 'profile';
|
|
666
|
+
if (rel.startsWith('shared/'))
|
|
667
|
+
return 'shared';
|
|
668
|
+
if (rel.startsWith('episodes/'))
|
|
669
|
+
return 'episode';
|
|
670
|
+
if (rel.startsWith('semantic/'))
|
|
671
|
+
return 'semantic';
|
|
672
|
+
if (rel.startsWith('procedures/'))
|
|
673
|
+
return 'procedure';
|
|
674
|
+
return 'memory';
|
|
675
|
+
}
|
|
676
|
+
function reindexAll(root) {
|
|
677
|
+
const db = getIndexDb();
|
|
678
|
+
clearIndex(db);
|
|
679
|
+
const now = new Date().toISOString();
|
|
680
|
+
const homeId = instanceId();
|
|
681
|
+
const insertChunk = db.prepare(`
|
|
682
|
+
INSERT INTO chunks (path, relpath, kind, home_id, source_start_line, source_end_line, source_hash, content, content_hash, created_at)
|
|
683
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
684
|
+
`);
|
|
685
|
+
const insertFts = db.prepare(`
|
|
686
|
+
INSERT INTO chunks_fts (rowid, content, relpath, kind)
|
|
687
|
+
VALUES (?, ?, ?, ?)
|
|
688
|
+
`);
|
|
689
|
+
let totalFiles = 0;
|
|
690
|
+
let totalChunks = 0;
|
|
691
|
+
const tx = db.transaction(() => {
|
|
692
|
+
for (const file of indexedFiles(root)) {
|
|
693
|
+
totalFiles += 1;
|
|
694
|
+
const rel = relative(root, file).replace(/\\/g, '/');
|
|
695
|
+
const kind = kindForFile(root, file);
|
|
696
|
+
for (const chunk of chunkMarkdown(file, rel, kind)) {
|
|
697
|
+
const contentHash = hashText(chunk.content);
|
|
698
|
+
const info = insertChunk.run(chunk.path, chunk.relpath, chunk.kind, homeId, chunk.source_start_line, chunk.source_end_line, chunk.source_hash, chunk.content, contentHash, now);
|
|
699
|
+
insertFts.run(Number(info.lastInsertRowid), chunk.content, chunk.relpath, chunk.kind);
|
|
700
|
+
totalChunks += 1;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
tx();
|
|
705
|
+
db.close();
|
|
706
|
+
return { totalFiles, totalChunks };
|
|
707
|
+
}
|
|
708
|
+
function reindexSingleFile(root, file) {
|
|
709
|
+
if (!fs.existsSync(file))
|
|
710
|
+
return 0;
|
|
711
|
+
const db = getIndexDb();
|
|
712
|
+
const rel = relative(root, file).replace(/\\/g, '/');
|
|
713
|
+
const kind = kindForFile(root, file);
|
|
714
|
+
const now = new Date().toISOString();
|
|
715
|
+
const homeId = instanceId();
|
|
716
|
+
// Delete existing chunks for this file
|
|
717
|
+
db.prepare('DELETE FROM chunks_fts WHERE rowid IN (SELECT id FROM chunks WHERE relpath = ?)').run(rel);
|
|
718
|
+
db.prepare('DELETE FROM chunks WHERE relpath = ?').run(rel);
|
|
719
|
+
// Re-chunk and insert
|
|
720
|
+
const insertChunk = db.prepare('INSERT INTO chunks (path, relpath, kind, home_id, source_start_line, source_end_line, source_hash, content, content_hash, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
721
|
+
const insertFts = db.prepare('INSERT INTO chunks_fts (rowid, content, relpath, kind) VALUES (?, ?, ?, ?)');
|
|
722
|
+
let count = 0;
|
|
723
|
+
const tx = db.transaction(() => {
|
|
724
|
+
for (const chunk of chunkMarkdown(file, rel, kind)) {
|
|
725
|
+
const contentHash = hashText(chunk.content);
|
|
726
|
+
const info = insertChunk.run(chunk.path, chunk.relpath, chunk.kind, homeId, chunk.source_start_line, chunk.source_end_line, chunk.source_hash, chunk.content, contentHash, now);
|
|
727
|
+
insertFts.run(Number(info.lastInsertRowid), chunk.content, chunk.relpath, chunk.kind);
|
|
728
|
+
count++;
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
tx();
|
|
732
|
+
db.close();
|
|
733
|
+
return count;
|
|
734
|
+
}
|
|
735
|
+
function buildLikeTerm(term) {
|
|
736
|
+
return `%${term.replace(/[%_]/g, (m) => `\\${m}`)}%`;
|
|
737
|
+
}
|
|
738
|
+
function tokenizeQuery(query) {
|
|
739
|
+
const trimmed = String(query || '').trim();
|
|
740
|
+
if (!trimmed)
|
|
741
|
+
return [];
|
|
742
|
+
const tokens = trimmed
|
|
743
|
+
.split(/[\s,]+/)
|
|
744
|
+
.map(t => t.trim())
|
|
745
|
+
.filter(Boolean);
|
|
746
|
+
return Array.from(new Set([trimmed, ...tokens])).slice(0, 8);
|
|
747
|
+
}
|
|
748
|
+
function tokenizeExpandedQuery(query, expanded) {
|
|
749
|
+
if (expanded?.length)
|
|
750
|
+
return sanitizeKeywords([query, ...expanded]).slice(0, 8);
|
|
751
|
+
return tokenizeQuery(query);
|
|
752
|
+
}
|
|
753
|
+
function formatHits(hits) {
|
|
754
|
+
if (!hits.length)
|
|
755
|
+
return '(no results)';
|
|
756
|
+
return hits.map(hit => {
|
|
757
|
+
const loc = `${hit.relpath}:${hit.source_start_line}-${hit.source_end_line}`;
|
|
758
|
+
return `${loc}\n${hit.snippet}`;
|
|
759
|
+
}).join('\n\n---\n\n');
|
|
760
|
+
}
|
|
761
|
+
function searchIndex(query, expanded) {
|
|
762
|
+
const db = getIndexDb();
|
|
763
|
+
const searchTerms = tokenizeExpandedQuery(query, expanded);
|
|
764
|
+
if (!searchTerms.length) {
|
|
765
|
+
db.close();
|
|
766
|
+
return { hits: [] };
|
|
767
|
+
}
|
|
768
|
+
const byPathLine = new Map();
|
|
769
|
+
const ftsStmt = db.prepare(`
|
|
770
|
+
SELECT
|
|
771
|
+
c.path,
|
|
772
|
+
c.relpath,
|
|
773
|
+
c.kind,
|
|
774
|
+
c.source_start_line,
|
|
775
|
+
c.source_end_line,
|
|
776
|
+
c.content,
|
|
777
|
+
bm25(chunks_fts) AS score
|
|
778
|
+
FROM chunks_fts
|
|
779
|
+
JOIN chunks c ON c.id = chunks_fts.rowid
|
|
780
|
+
WHERE chunks_fts MATCH ?
|
|
781
|
+
ORDER BY score
|
|
782
|
+
LIMIT 16
|
|
783
|
+
`);
|
|
784
|
+
const likeStmt = db.prepare(`
|
|
785
|
+
SELECT path, relpath, kind, source_start_line, source_end_line, content
|
|
786
|
+
FROM chunks
|
|
787
|
+
WHERE content LIKE ? ESCAPE '\\'
|
|
788
|
+
ORDER BY relpath ASC, source_start_line ASC
|
|
789
|
+
LIMIT 16
|
|
790
|
+
`);
|
|
791
|
+
for (const term of searchTerms) {
|
|
792
|
+
const ftsQuery = term.split(/\s+/).map(t => `"${t.replace(/"/g, '')}"`).join(' OR ');
|
|
793
|
+
try {
|
|
794
|
+
const rows = ftsStmt.all(ftsQuery);
|
|
795
|
+
for (const row of rows) {
|
|
796
|
+
const key = `${row.relpath}:${row.source_start_line}:${row.source_end_line}`;
|
|
797
|
+
if (!byPathLine.has(key)) {
|
|
798
|
+
byPathLine.set(key, {
|
|
799
|
+
path: row.path,
|
|
800
|
+
relpath: row.relpath,
|
|
801
|
+
kind: row.kind,
|
|
802
|
+
source_start_line: row.source_start_line,
|
|
803
|
+
source_end_line: row.source_end_line,
|
|
804
|
+
snippet: String(row.content || '').slice(0, 700),
|
|
805
|
+
score: Number(row.score || 0),
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
// ignore FTS parse issues, fallback to LIKE below
|
|
812
|
+
}
|
|
813
|
+
const likeRows = likeStmt.all(buildLikeTerm(term));
|
|
814
|
+
for (const row of likeRows) {
|
|
815
|
+
const key = `${row.relpath}:${row.source_start_line}:${row.source_end_line}`;
|
|
816
|
+
if (!byPathLine.has(key)) {
|
|
817
|
+
byPathLine.set(key, {
|
|
818
|
+
path: row.path,
|
|
819
|
+
relpath: row.relpath,
|
|
820
|
+
kind: row.kind,
|
|
821
|
+
source_start_line: row.source_start_line,
|
|
822
|
+
source_end_line: row.source_end_line,
|
|
823
|
+
snippet: String(row.content || '').slice(0, 700),
|
|
824
|
+
score: 999,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
db.close();
|
|
830
|
+
const hits = [...byPathLine.values()]
|
|
831
|
+
.sort((a, b) => a.score - b.score)
|
|
832
|
+
.slice(0, 8);
|
|
833
|
+
return { hits };
|
|
834
|
+
}
|
|
835
|
+
function updateImportedCount(kind, value) {
|
|
836
|
+
const meta = readMeta();
|
|
837
|
+
writeMeta({
|
|
838
|
+
importedCounts: {
|
|
839
|
+
...(meta?.importedCounts || DEFAULT_IMPORTED_COUNTS),
|
|
840
|
+
[kind]: value,
|
|
841
|
+
},
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
function isAdvancedShadowEnabled() {
|
|
845
|
+
return settings.memoryAdvanced?.enabled === true && fs.existsSync(getMetaPath());
|
|
846
|
+
}
|
|
847
|
+
function importSingleMarkdownFile(root, file) {
|
|
848
|
+
const legacyDir = join(JAW_HOME, 'memory');
|
|
849
|
+
if (!file.startsWith(legacyDir))
|
|
850
|
+
return null;
|
|
851
|
+
const rel = relative(legacyDir, file).replace(/\\/g, '/');
|
|
852
|
+
if (!rel || rel === 'MEMORY.md')
|
|
853
|
+
return null;
|
|
854
|
+
const body = safeReadFile(file);
|
|
855
|
+
const sourceHash = hashText(body);
|
|
856
|
+
const baseName = rel.split('/').pop() || 'memory.md';
|
|
857
|
+
const isDated = /^\d{4}-\d{2}-\d{2}\.md$/.test(baseName);
|
|
858
|
+
const sectionDir = isDated ? 'episodes/imported' : 'semantic/imported';
|
|
859
|
+
const dest = join(root, sectionDir, rel);
|
|
860
|
+
const fm = frontmatter({
|
|
861
|
+
id: `import-${sourceHash}`,
|
|
862
|
+
home_id: instanceId(),
|
|
863
|
+
kind: isDated ? 'episode' : 'semantic',
|
|
864
|
+
source: 'legacy-markdown',
|
|
865
|
+
trust_level: 'high',
|
|
866
|
+
source_relpath: rel,
|
|
867
|
+
source_hash: sourceHash,
|
|
868
|
+
created_at: new Date().toISOString(),
|
|
869
|
+
updated_at: new Date().toISOString(),
|
|
870
|
+
});
|
|
871
|
+
writeText(dest, fm + body.trim() + '\n');
|
|
872
|
+
reindexSingleFile(root, dest);
|
|
873
|
+
return dest;
|
|
874
|
+
}
|
|
875
|
+
export function syncLegacyMarkdownShadowImport(file) {
|
|
876
|
+
if (!isAdvancedShadowEnabled())
|
|
877
|
+
return { ok: false, reason: 'advanced_not_ready' };
|
|
878
|
+
const root = getAdvancedMemoryDir();
|
|
879
|
+
if (file === join(JAW_HOME, 'memory', 'MEMORY.md')) {
|
|
880
|
+
const count = importCoreMemory(root);
|
|
881
|
+
updateImportedCount('core', count);
|
|
882
|
+
if (count > 0)
|
|
883
|
+
reindexSingleFile(root, join(root, 'profile.md'));
|
|
884
|
+
return { ok: true, target: join(root, 'profile.md'), count };
|
|
885
|
+
}
|
|
886
|
+
const target = importSingleMarkdownFile(root, file);
|
|
887
|
+
if (!target)
|
|
888
|
+
return { ok: false, reason: 'not_importable' };
|
|
889
|
+
updateImportedCount('markdown', countMarkdownFiles(join(root, 'semantic')) + countMarkdownFiles(join(root, 'episodes')));
|
|
890
|
+
return { ok: true, target, count: 1 };
|
|
891
|
+
}
|
|
892
|
+
export function syncKvShadowImport() {
|
|
893
|
+
if (!isAdvancedShadowEnabled())
|
|
894
|
+
return { ok: false, reason: 'advanced_not_ready' };
|
|
895
|
+
const root = getAdvancedMemoryDir();
|
|
896
|
+
const count = importKvMemory(root);
|
|
897
|
+
updateImportedCount('kv', count);
|
|
898
|
+
reindexSingleFile(root, join(root, 'semantic', 'kv-imported.md'));
|
|
899
|
+
return { ok: true, target: join(root, 'semantic', 'kv-imported.md'), count };
|
|
900
|
+
}
|
|
901
|
+
export function ensureAdvancedMemoryStructure() {
|
|
902
|
+
const root = getAdvancedMemoryDir();
|
|
903
|
+
const sharedDir = join(root, 'shared');
|
|
904
|
+
const episodesDir = join(root, 'episodes');
|
|
905
|
+
const semanticDir = join(root, 'semantic');
|
|
906
|
+
const proceduresDir = join(root, 'procedures');
|
|
907
|
+
const sessionsDir = join(root, 'sessions');
|
|
908
|
+
const corruptedDir = join(root, 'corrupted');
|
|
909
|
+
const unmappedDir = join(root, 'legacy-unmapped');
|
|
910
|
+
ensureDir(root);
|
|
911
|
+
ensureDir(sharedDir);
|
|
912
|
+
ensureDir(episodesDir);
|
|
913
|
+
ensureDir(semanticDir);
|
|
914
|
+
ensureDir(proceduresDir);
|
|
915
|
+
ensureDir(sessionsDir);
|
|
916
|
+
ensureDir(corruptedDir);
|
|
917
|
+
ensureDir(unmappedDir);
|
|
918
|
+
writeMeta({
|
|
919
|
+
schemaVersion: 1,
|
|
920
|
+
phase: '1',
|
|
921
|
+
homeId: instanceId(),
|
|
922
|
+
jawHome: JAW_HOME,
|
|
923
|
+
initializedAt: readMeta()?.initializedAt || new Date().toISOString(),
|
|
924
|
+
});
|
|
925
|
+
const profilePath = join(root, 'profile.md');
|
|
926
|
+
if (!fs.existsSync(profilePath)) {
|
|
927
|
+
const fm = frontmatter({
|
|
928
|
+
id: `profile-${instanceId()}`,
|
|
929
|
+
home_id: instanceId(),
|
|
930
|
+
kind: 'profile',
|
|
931
|
+
source: 'generated',
|
|
932
|
+
trust_level: 'high',
|
|
933
|
+
created_at: new Date().toISOString(),
|
|
934
|
+
updated_at: new Date().toISOString(),
|
|
935
|
+
});
|
|
936
|
+
writeText(profilePath, fm + `# Profile
|
|
937
|
+
|
|
938
|
+
## User Preferences
|
|
939
|
+
|
|
940
|
+
## Key Decisions
|
|
941
|
+
|
|
942
|
+
## Active Projects
|
|
943
|
+
`);
|
|
944
|
+
}
|
|
945
|
+
return { root, metaPath: getMetaPath(), profilePath };
|
|
946
|
+
}
|
|
947
|
+
export function bootstrapAdvancedMemory(options = {}) {
|
|
948
|
+
const root = getAdvancedMemoryDir();
|
|
949
|
+
ensureAdvancedMemoryStructure();
|
|
950
|
+
writeMeta({
|
|
951
|
+
bootstrapStatus: 'running',
|
|
952
|
+
lastBootstrapAt: new Date().toISOString(),
|
|
953
|
+
lastError: '',
|
|
954
|
+
});
|
|
955
|
+
const resolved = {
|
|
956
|
+
importCore: options.importCore !== false,
|
|
957
|
+
importMarkdown: options.importMarkdown !== false,
|
|
958
|
+
importKv: options.importKv !== false,
|
|
959
|
+
importClaudeSession: options.importClaudeSession !== false,
|
|
960
|
+
};
|
|
961
|
+
try {
|
|
962
|
+
const backupRoot = backupLegacyMemory();
|
|
963
|
+
const counts = {
|
|
964
|
+
core: resolved.importCore ? importCoreMemory(root) : 0,
|
|
965
|
+
markdown: resolved.importMarkdown ? importMarkdownMemory(root) : 0,
|
|
966
|
+
kv: resolved.importKv ? importKvMemory(root) : 0,
|
|
967
|
+
claude: resolved.importClaudeSession ? importClaudeSessionMemory(root) : 0,
|
|
968
|
+
};
|
|
969
|
+
const { totalFiles, totalChunks } = reindexAll(root);
|
|
970
|
+
const meta = writeMeta({
|
|
971
|
+
bootstrapStatus: 'done',
|
|
972
|
+
importedCounts: counts,
|
|
973
|
+
lastBootstrapAt: new Date().toISOString(),
|
|
974
|
+
lastError: '',
|
|
975
|
+
});
|
|
976
|
+
return { root, backupRoot, counts, indexed: { totalFiles, totalChunks }, meta };
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
980
|
+
writeMeta({
|
|
981
|
+
bootstrapStatus: 'failed',
|
|
982
|
+
lastBootstrapAt: new Date().toISOString(),
|
|
983
|
+
lastError: message,
|
|
984
|
+
});
|
|
985
|
+
throw err;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
export function reindexAdvancedMemory() {
|
|
989
|
+
const root = getAdvancedMemoryDir();
|
|
990
|
+
ensureAdvancedMemoryStructure();
|
|
991
|
+
return reindexAll(root);
|
|
992
|
+
}
|
|
993
|
+
export function listAdvancedMemoryFiles() {
|
|
994
|
+
const root = getAdvancedMemoryDir();
|
|
995
|
+
return {
|
|
996
|
+
root,
|
|
997
|
+
sections: {
|
|
998
|
+
profile: fs.existsSync(join(root, 'profile.md')) ? ['profile.md'] : [],
|
|
999
|
+
shared: listMarkdownFiles(join(root, 'shared')).map(f => relative(root, f).replace(/\\/g, '/')),
|
|
1000
|
+
episodes: listMarkdownFiles(join(root, 'episodes')).map(f => relative(root, f).replace(/\\/g, '/')),
|
|
1001
|
+
semantic: listMarkdownFiles(join(root, 'semantic')).map(f => relative(root, f).replace(/\\/g, '/')),
|
|
1002
|
+
procedures: listMarkdownFiles(join(root, 'procedures')).map(f => relative(root, f).replace(/\\/g, '/')),
|
|
1003
|
+
sessions: listMarkdownFiles(join(root, 'sessions')).map(f => relative(root, f).replace(/\\/g, '/')),
|
|
1004
|
+
corrupted: listMarkdownFiles(join(root, 'corrupted')).map(f => relative(root, f).replace(/\\/g, '/')),
|
|
1005
|
+
legacyUnmapped: listMarkdownFiles(join(root, 'legacy-unmapped')).map(f => relative(root, f).replace(/\\/g, '/')),
|
|
1006
|
+
},
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
export function searchAdvancedMemory(query) {
|
|
1010
|
+
const { hits } = searchIndex(query);
|
|
1011
|
+
return formatHits(hits);
|
|
1012
|
+
}
|
|
1013
|
+
export function loadAdvancedProfileSummary(maxChars = 800) {
|
|
1014
|
+
const file = join(getAdvancedMemoryDir(), 'profile.md');
|
|
1015
|
+
if (!fs.existsSync(file))
|
|
1016
|
+
return '';
|
|
1017
|
+
const parsed = parseMarkdownFile(safeReadFile(file));
|
|
1018
|
+
const body = parsed.body.trim();
|
|
1019
|
+
if (!body)
|
|
1020
|
+
return '';
|
|
1021
|
+
return body.length > maxChars ? body.slice(0, maxChars) + '\n...(truncated)' : body;
|
|
1022
|
+
}
|
|
1023
|
+
export function buildTaskSnapshot(query, budget = 2800, expanded) {
|
|
1024
|
+
const cleaned = String(query || '').trim();
|
|
1025
|
+
if (!cleaned)
|
|
1026
|
+
return '';
|
|
1027
|
+
const { hits } = searchIndex(cleaned, expanded);
|
|
1028
|
+
if (!hits.length)
|
|
1029
|
+
return '';
|
|
1030
|
+
const out = [];
|
|
1031
|
+
let remaining = Math.max(0, budget);
|
|
1032
|
+
for (const hit of hits.slice(0, 4)) {
|
|
1033
|
+
if (remaining <= 0)
|
|
1034
|
+
break;
|
|
1035
|
+
const header = `### ${hit.relpath}:${hit.source_start_line}-${hit.source_end_line}`;
|
|
1036
|
+
const snippetBudget = Math.min(700, Math.max(0, remaining - header.length - 4));
|
|
1037
|
+
if (snippetBudget <= 0)
|
|
1038
|
+
break;
|
|
1039
|
+
const snippet = hit.snippet.slice(0, snippetBudget).trim();
|
|
1040
|
+
const block = `${header}\n${snippet}`;
|
|
1041
|
+
out.push(block);
|
|
1042
|
+
remaining -= block.length + 2;
|
|
1043
|
+
}
|
|
1044
|
+
if (!out.length)
|
|
1045
|
+
return '';
|
|
1046
|
+
return `## Task Snapshot\n${out.join('\n\n')}`;
|
|
1047
|
+
}
|
|
1048
|
+
export async function buildTaskSnapshotAsync(query, budget = 2800) {
|
|
1049
|
+
const expanded = await expandSearchKeywords(query);
|
|
1050
|
+
return buildTaskSnapshot(query, budget, expanded);
|
|
1051
|
+
}
|
|
1052
|
+
export function readAdvancedMemorySnippet(relPath, opts = {}) {
|
|
1053
|
+
const root = getAdvancedMemoryDir();
|
|
1054
|
+
const file = join(root, relPath);
|
|
1055
|
+
if (!fs.existsSync(file))
|
|
1056
|
+
return null;
|
|
1057
|
+
const content = safeReadFile(file);
|
|
1058
|
+
if (opts.lines) {
|
|
1059
|
+
const parts = String(opts.lines).split('-').map(Number);
|
|
1060
|
+
const fromRaw = parts[0];
|
|
1061
|
+
const toRaw = parts[1];
|
|
1062
|
+
const from = Number.isFinite(fromRaw) && fromRaw > 0 ? fromRaw : 1;
|
|
1063
|
+
const to = Number.isFinite(toRaw) && toRaw >= from ? toRaw : from;
|
|
1064
|
+
return content.split('\n').slice(from - 1, to).join('\n');
|
|
1065
|
+
}
|
|
1066
|
+
return content;
|
|
1067
|
+
}
|
|
1068
|
+
export function getAdvancedMemoryStatus() {
|
|
1069
|
+
const root = getAdvancedMemoryDir();
|
|
1070
|
+
const meta = readMeta();
|
|
1071
|
+
const initialized = !!meta;
|
|
1072
|
+
const enabled = settings.memoryAdvanced?.enabled === true;
|
|
1073
|
+
const provider = settings.memoryAdvanced?.provider || 'gemini';
|
|
1074
|
+
const corruptedDir = join(root, 'corrupted');
|
|
1075
|
+
const dbPath = getAdvancedIndexDbPath();
|
|
1076
|
+
const indexReady = fs.existsSync(dbPath);
|
|
1077
|
+
const indexed = indexReady ? reindexIndexCounts(dbPath) : { totalFiles: 0, totalChunks: 0 };
|
|
1078
|
+
return {
|
|
1079
|
+
phase: meta?.phase || '0a',
|
|
1080
|
+
enabled,
|
|
1081
|
+
provider,
|
|
1082
|
+
state: !enabled ? 'disabled' : initialized ? 'configured' : 'not_initialized',
|
|
1083
|
+
initialized,
|
|
1084
|
+
storageRoot: root,
|
|
1085
|
+
routing: {
|
|
1086
|
+
searchRead: enabled && indexReady ? 'advanced' : 'basic',
|
|
1087
|
+
save: 'basic',
|
|
1088
|
+
},
|
|
1089
|
+
indexState: initialized ? (indexReady ? 'ready' : 'not_indexed') : 'not_initialized',
|
|
1090
|
+
indexedFiles: indexed.totalFiles,
|
|
1091
|
+
indexedChunks: indexed.totalChunks,
|
|
1092
|
+
lastIndexedAt: fs.existsSync(dbPath) ? fs.statSync(dbPath).mtime.toISOString() : null,
|
|
1093
|
+
importStatus: meta?.bootstrapStatus || (initialized ? 'idle' : 'not_started'),
|
|
1094
|
+
corruptedCount: countFiles(corruptedDir),
|
|
1095
|
+
lastExpansion: lastExpansionTerms,
|
|
1096
|
+
lastError: meta?.lastError || '',
|
|
1097
|
+
importedCounts: meta?.importedCounts || { ...DEFAULT_IMPORTED_COUNTS },
|
|
1098
|
+
backupRoot: getAdvancedMemoryBackupDir(),
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function reindexIndexCounts(dbPath) {
|
|
1102
|
+
const db = new Database(dbPath, { readonly: true });
|
|
1103
|
+
const totalChunks = Number(db.prepare('SELECT COUNT(*) AS c FROM chunks').get()?.c || 0);
|
|
1104
|
+
const totalFiles = Number(db.prepare('SELECT COUNT(DISTINCT relpath) AS c FROM chunks').get()?.c || 0);
|
|
1105
|
+
db.close();
|
|
1106
|
+
return { totalFiles, totalChunks };
|
|
1107
|
+
}
|
|
1108
|
+
//# sourceMappingURL=advanced.js.map
|