agent-recall-core 3.3.28 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1014 -206
- package/dist/digest/store.d.ts.map +1 -1
- package/dist/digest/store.js +9 -2
- package/dist/digest/store.js.map +1 -1
- package/dist/helpers/normalize.d.ts.map +1 -1
- package/dist/helpers/normalize.js +2 -0
- package/dist/helpers/normalize.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/palace/awareness.d.ts +7 -3
- package/dist/palace/awareness.d.ts.map +1 -1
- package/dist/palace/awareness.js +21 -11
- package/dist/palace/awareness.js.map +1 -1
- package/dist/palace/insights-index.d.ts.map +1 -1
- package/dist/palace/insights-index.js +31 -0
- package/dist/palace/insights-index.js.map +1 -1
- package/dist/palace/rooms.d.ts +1 -1
- package/dist/palace/rooms.d.ts.map +1 -1
- package/dist/palace/rooms.js +30 -5
- package/dist/palace/rooms.js.map +1 -1
- package/dist/supabase/client.d.ts +9 -0
- package/dist/supabase/client.d.ts.map +1 -0
- package/dist/supabase/client.js +22 -0
- package/dist/supabase/client.js.map +1 -0
- package/dist/supabase/config.d.ts +10 -0
- package/dist/supabase/config.d.ts.map +1 -0
- package/dist/supabase/config.js +45 -0
- package/dist/supabase/config.js.map +1 -0
- package/dist/supabase/embedding.d.ts +25 -0
- package/dist/supabase/embedding.d.ts.map +1 -0
- package/dist/supabase/embedding.js +76 -0
- package/dist/supabase/embedding.js.map +1 -0
- package/dist/supabase/recall-backend.d.ts +21 -0
- package/dist/supabase/recall-backend.d.ts.map +1 -0
- package/dist/supabase/recall-backend.js +128 -0
- package/dist/supabase/recall-backend.js.map +1 -0
- package/dist/supabase/sync.d.ts +22 -0
- package/dist/supabase/sync.d.ts.map +1 -0
- package/dist/supabase/sync.js +180 -0
- package/dist/supabase/sync.js.map +1 -0
- package/dist/tools-logic/awareness-update.d.ts +1 -0
- package/dist/tools-logic/awareness-update.d.ts.map +1 -1
- package/dist/tools-logic/awareness-update.js +1 -0
- package/dist/tools-logic/awareness-update.js.map +1 -1
- package/dist/tools-logic/bootstrap.d.ts +64 -0
- package/dist/tools-logic/bootstrap.d.ts.map +1 -0
- package/dist/tools-logic/bootstrap.js +752 -0
- package/dist/tools-logic/bootstrap.js.map +1 -0
- package/dist/tools-logic/insight-promotion.d.ts +19 -0
- package/dist/tools-logic/insight-promotion.d.ts.map +1 -0
- package/dist/tools-logic/insight-promotion.js +61 -0
- package/dist/tools-logic/insight-promotion.js.map +1 -0
- package/dist/tools-logic/journal-cold-start.d.ts +6 -0
- package/dist/tools-logic/journal-cold-start.d.ts.map +1 -1
- package/dist/tools-logic/journal-cold-start.js +38 -7
- package/dist/tools-logic/journal-cold-start.js.map +1 -1
- package/dist/tools-logic/journal-read.d.ts.map +1 -1
- package/dist/tools-logic/journal-read.js +24 -3
- package/dist/tools-logic/journal-read.js.map +1 -1
- package/dist/tools-logic/journal-search.d.ts +2 -0
- package/dist/tools-logic/journal-search.d.ts.map +1 -1
- package/dist/tools-logic/journal-search.js +7 -1
- package/dist/tools-logic/journal-search.js.map +1 -1
- package/dist/tools-logic/journal-write.d.ts +5 -0
- package/dist/tools-logic/journal-write.d.ts.map +1 -1
- package/dist/tools-logic/journal-write.js +47 -1
- package/dist/tools-logic/journal-write.js.map +1 -1
- package/dist/tools-logic/palace-lint.js +3 -3
- package/dist/tools-logic/palace-lint.js.map +1 -1
- package/dist/tools-logic/palace-search.d.ts.map +1 -1
- package/dist/tools-logic/palace-search.js +3 -0
- package/dist/tools-logic/palace-search.js.map +1 -1
- package/dist/tools-logic/palace-write.d.ts.map +1 -1
- package/dist/tools-logic/palace-write.js +19 -6
- package/dist/tools-logic/palace-write.js.map +1 -1
- package/dist/tools-logic/recall-backend.d.ts +29 -0
- package/dist/tools-logic/recall-backend.d.ts.map +1 -0
- package/dist/tools-logic/recall-backend.js +58 -0
- package/dist/tools-logic/recall-backend.js.map +1 -0
- package/dist/tools-logic/session-end.d.ts.map +1 -1
- package/dist/tools-logic/session-end.js +8 -1
- package/dist/tools-logic/session-end.js.map +1 -1
- package/dist/tools-logic/session-start.d.ts +1 -0
- package/dist/tools-logic/session-start.d.ts.map +1 -1
- package/dist/tools-logic/session-start.js +57 -3
- package/dist/tools-logic/session-start.js.map +1 -1
- package/dist/tools-logic/smart-recall.d.ts +10 -0
- package/dist/tools-logic/smart-recall.d.ts.map +1 -1
- package/dist/tools-logic/smart-recall.js +56 -32
- package/dist/tools-logic/smart-recall.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -4
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap — Layered Scan + Selective Import Architecture
|
|
3
|
+
*
|
|
4
|
+
* bootstrapScan() — discovers everything available on the machine (read-only)
|
|
5
|
+
* bootstrapImport() — imports selected items into ~/.agent-recall/
|
|
6
|
+
*
|
|
7
|
+
* This gives the orchestrator/CLI/MCP full control over what gets imported.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
import { getRoot } from "../types.js";
|
|
15
|
+
import { ensurePalaceInitialized } from "../palace/rooms.js";
|
|
16
|
+
import { writeIdentity } from "../palace/identity.js";
|
|
17
|
+
import { todayISO } from "../storage/fs-utils.js";
|
|
18
|
+
import { palaceWrite } from "./palace-write.js";
|
|
19
|
+
import { journalWrite } from "./journal-write.js";
|
|
20
|
+
import { awarenessUpdate } from "./awareness-update.js";
|
|
21
|
+
import { palaceDir } from "../storage/paths.js";
|
|
22
|
+
const execFileAsync = promisify(execFile);
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Constants
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
const DEFAULT_SCAN_DIRS = [
|
|
27
|
+
"Projects",
|
|
28
|
+
"work",
|
|
29
|
+
"code",
|
|
30
|
+
"dev",
|
|
31
|
+
"src",
|
|
32
|
+
"repos",
|
|
33
|
+
"github",
|
|
34
|
+
].map((d) => path.join(os.homedir(), d));
|
|
35
|
+
const SKIP_DIRS = new Set([
|
|
36
|
+
"node_modules",
|
|
37
|
+
".git",
|
|
38
|
+
"vendor",
|
|
39
|
+
"dist",
|
|
40
|
+
"build",
|
|
41
|
+
"__pycache__",
|
|
42
|
+
".venv",
|
|
43
|
+
".next",
|
|
44
|
+
".nuxt",
|
|
45
|
+
"coverage",
|
|
46
|
+
".cache",
|
|
47
|
+
".npm",
|
|
48
|
+
]);
|
|
49
|
+
const SECRET_PATTERNS = [
|
|
50
|
+
/\.env$/i,
|
|
51
|
+
/credentials/i,
|
|
52
|
+
/secrets/i,
|
|
53
|
+
/tokens/i,
|
|
54
|
+
/\.pem$/i,
|
|
55
|
+
/\.key$/i,
|
|
56
|
+
];
|
|
57
|
+
const MAX_FILE_SIZE = 5 * 1024; // 5KB
|
|
58
|
+
const PREVIEW_LEN = 100;
|
|
59
|
+
const GIT_TIMEOUT_MS = 2000;
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
function isSecretFile(filePath) {
|
|
64
|
+
const base = path.basename(filePath);
|
|
65
|
+
return SECRET_PATTERNS.some((re) => re.test(base));
|
|
66
|
+
}
|
|
67
|
+
function toSlug(name) {
|
|
68
|
+
return name
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
71
|
+
.replace(/^-+|-+$/g, "");
|
|
72
|
+
}
|
|
73
|
+
function detectLanguage(dir) {
|
|
74
|
+
const langMap = {
|
|
75
|
+
".ts": "TypeScript",
|
|
76
|
+
".tsx": "TypeScript",
|
|
77
|
+
".js": "JavaScript",
|
|
78
|
+
".jsx": "JavaScript",
|
|
79
|
+
".mjs": "JavaScript",
|
|
80
|
+
".py": "Python",
|
|
81
|
+
".go": "Go",
|
|
82
|
+
".rs": "Rust",
|
|
83
|
+
".rb": "Ruby",
|
|
84
|
+
".java": "Java",
|
|
85
|
+
".kt": "Kotlin",
|
|
86
|
+
".swift": "Swift",
|
|
87
|
+
".cpp": "C++",
|
|
88
|
+
".c": "C",
|
|
89
|
+
".cs": "C#",
|
|
90
|
+
".php": "PHP",
|
|
91
|
+
};
|
|
92
|
+
try {
|
|
93
|
+
const entries = fs.readdirSync(dir);
|
|
94
|
+
const extCounts = {};
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const ext = path.extname(entry).toLowerCase();
|
|
97
|
+
if (ext && langMap[ext]) {
|
|
98
|
+
extCounts[ext] = (extCounts[ext] ?? 0) + 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const topExt = Object.entries(extCounts).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
102
|
+
return topExt ? langMap[topExt] : undefined;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function readPreview(filePath) {
|
|
109
|
+
try {
|
|
110
|
+
if (!fs.existsSync(filePath))
|
|
111
|
+
return "";
|
|
112
|
+
const stat = fs.statSync(filePath);
|
|
113
|
+
if (stat.size > MAX_FILE_SIZE)
|
|
114
|
+
return "[file too large — skipped]";
|
|
115
|
+
const buf = Buffer.alloc(PREVIEW_LEN + 10);
|
|
116
|
+
const fd = fs.openSync(filePath, "r");
|
|
117
|
+
let bytesRead;
|
|
118
|
+
try {
|
|
119
|
+
bytesRead = fs.readSync(fd, buf, 0, PREVIEW_LEN + 10, 0);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
fs.closeSync(fd);
|
|
123
|
+
}
|
|
124
|
+
return buf
|
|
125
|
+
.slice(0, bytesRead)
|
|
126
|
+
.toString("utf-8")
|
|
127
|
+
.slice(0, PREVIEW_LEN)
|
|
128
|
+
.replace(/\n/g, " ")
|
|
129
|
+
.trim();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function fileSizeBytes(filePath) {
|
|
136
|
+
try {
|
|
137
|
+
return fs.statSync(filePath).size;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function gitCmd(dir, args) {
|
|
144
|
+
try {
|
|
145
|
+
const { stdout } = await execFileAsync("git", ["-C", dir, ...args], {
|
|
146
|
+
timeout: GIT_TIMEOUT_MS,
|
|
147
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
148
|
+
});
|
|
149
|
+
return stdout.trim();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function readPackageInfo(dir) {
|
|
156
|
+
const pkgPath = path.join(dir, "package.json");
|
|
157
|
+
if (!fs.existsSync(pkgPath))
|
|
158
|
+
return {};
|
|
159
|
+
try {
|
|
160
|
+
if (fileSizeBytes(pkgPath) > MAX_FILE_SIZE)
|
|
161
|
+
return {};
|
|
162
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
163
|
+
return {
|
|
164
|
+
description: typeof pkg["description"] === "string" ? pkg["description"] : undefined,
|
|
165
|
+
name: typeof pkg["name"] === "string" ? pkg["name"] : undefined,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function readReadmeDescription(dir) {
|
|
173
|
+
for (const name of ["README.md", "readme.md", "README.txt"]) {
|
|
174
|
+
const p = path.join(dir, name);
|
|
175
|
+
if (!fs.existsSync(p))
|
|
176
|
+
continue;
|
|
177
|
+
try {
|
|
178
|
+
if (fileSizeBytes(p) > MAX_FILE_SIZE)
|
|
179
|
+
continue;
|
|
180
|
+
const lines = fs.readFileSync(p, "utf-8").split("\n");
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
const trimmed = line.trim();
|
|
183
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(">"))
|
|
184
|
+
continue;
|
|
185
|
+
return trimmed.slice(0, 200);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// ignore
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
/** Strip YAML frontmatter from markdown content. */
|
|
195
|
+
function stripFrontmatter(content) {
|
|
196
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
197
|
+
if (!match)
|
|
198
|
+
return { body: content, meta: {} };
|
|
199
|
+
const meta = {};
|
|
200
|
+
for (const line of match[1].split("\n")) {
|
|
201
|
+
const kv = line.match(/^(\w+):\s*(.+)$/);
|
|
202
|
+
if (kv)
|
|
203
|
+
meta[kv[1].trim()] = kv[2].trim();
|
|
204
|
+
}
|
|
205
|
+
return { body: match[2].trim(), meta };
|
|
206
|
+
}
|
|
207
|
+
/** Map AutoMemory `type:` frontmatter field to palace room slug. */
|
|
208
|
+
function getTargetRoom(meta) {
|
|
209
|
+
switch (meta["type"]) {
|
|
210
|
+
case "feedback": return "alignment";
|
|
211
|
+
case "project": return "goals";
|
|
212
|
+
default: return "knowledge";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Walk a directory to find git repos up to max_depth. Stops recursing into found repos. */
|
|
216
|
+
function findGitRepos(dir, maxDepth, depth = 0) {
|
|
217
|
+
if (depth > maxDepth)
|
|
218
|
+
return [];
|
|
219
|
+
if (!fs.existsSync(dir))
|
|
220
|
+
return [];
|
|
221
|
+
let entries;
|
|
222
|
+
try {
|
|
223
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
const hasGit = entries.some((e) => e.name === ".git" && e.isDirectory());
|
|
229
|
+
if (hasGit)
|
|
230
|
+
return [dir];
|
|
231
|
+
const results = [];
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
if (!entry.isDirectory())
|
|
234
|
+
continue;
|
|
235
|
+
if (SKIP_DIRS.has(entry.name))
|
|
236
|
+
continue;
|
|
237
|
+
results.push(...findGitRepos(path.join(dir, entry.name), maxDepth, depth + 1));
|
|
238
|
+
}
|
|
239
|
+
return results;
|
|
240
|
+
}
|
|
241
|
+
/** Get slugs of projects already in AR */
|
|
242
|
+
function existingArSlugs() {
|
|
243
|
+
const root = getRoot();
|
|
244
|
+
const projectsDir = path.join(root, "projects");
|
|
245
|
+
if (!fs.existsSync(projectsDir))
|
|
246
|
+
return new Set();
|
|
247
|
+
try {
|
|
248
|
+
return new Set(fs
|
|
249
|
+
.readdirSync(projectsDir, { withFileTypes: true })
|
|
250
|
+
.filter((e) => e.isDirectory())
|
|
251
|
+
.map((e) => e.name));
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return new Set();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// bootstrapScan
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
export async function bootstrapScan(options) {
|
|
261
|
+
const t0 = Date.now();
|
|
262
|
+
const maxDepth = options?.max_depth ?? 3;
|
|
263
|
+
const scanDirs = [...DEFAULT_SCAN_DIRS, ...(options?.scan_dirs ?? []), ...(options?.source_dirs ?? [])];
|
|
264
|
+
const arSlugs = existingArSlugs();
|
|
265
|
+
// Map from slug → DiscoveredProject (for merging multi-source projects)
|
|
266
|
+
const projectMap = new Map();
|
|
267
|
+
// -------------------------------------------------------------------------
|
|
268
|
+
// 1. Git repo discovery
|
|
269
|
+
// -------------------------------------------------------------------------
|
|
270
|
+
const allRepoDirs = [];
|
|
271
|
+
for (const dir of scanDirs) {
|
|
272
|
+
allRepoDirs.push(...findGitRepos(dir, maxDepth));
|
|
273
|
+
}
|
|
274
|
+
const uniqueRepoDirs = [...new Set(allRepoDirs)];
|
|
275
|
+
// Parallel git metadata fetch
|
|
276
|
+
const gitResults = await Promise.allSettled(uniqueRepoDirs.map(async (repoDir) => {
|
|
277
|
+
const [remoteUrl, lastCommitIso, logText] = await Promise.all([
|
|
278
|
+
gitCmd(repoDir, ["config", "--get", "remote.origin.url"]),
|
|
279
|
+
gitCmd(repoDir, ["log", "-1", "--format=%aI"]),
|
|
280
|
+
gitCmd(repoDir, ["log", "--oneline", "-5"]),
|
|
281
|
+
]);
|
|
282
|
+
const pkgInfo = readPackageInfo(repoDir);
|
|
283
|
+
const remoteName = remoteUrl ? path.basename(remoteUrl, ".git") : "";
|
|
284
|
+
const name = pkgInfo.name ?? (remoteName || path.basename(repoDir));
|
|
285
|
+
const slug = toSlug(name);
|
|
286
|
+
const language = detectLanguage(repoDir);
|
|
287
|
+
const lastActivity = lastCommitIso || undefined;
|
|
288
|
+
const description = pkgInfo.description ?? readReadmeDescription(repoDir);
|
|
289
|
+
const langDetail = [
|
|
290
|
+
language,
|
|
291
|
+
lastActivity ? `last commit ${lastActivity.slice(0, 10)}` : "",
|
|
292
|
+
]
|
|
293
|
+
.filter(Boolean)
|
|
294
|
+
.join(", ");
|
|
295
|
+
const importable = [];
|
|
296
|
+
// Identity — always generated from discovered metadata
|
|
297
|
+
importable.push({
|
|
298
|
+
id: "identity",
|
|
299
|
+
type: "identity",
|
|
300
|
+
source_path: repoDir,
|
|
301
|
+
size_bytes: 0,
|
|
302
|
+
preview: `${name} — ${description ?? "no description"}`.slice(0, PREVIEW_LEN),
|
|
303
|
+
});
|
|
304
|
+
// Trajectory — if git log available
|
|
305
|
+
if (logText) {
|
|
306
|
+
importable.push({
|
|
307
|
+
id: "git-trajectory",
|
|
308
|
+
type: "trajectory",
|
|
309
|
+
source_path: repoDir,
|
|
310
|
+
size_bytes: Buffer.byteLength(logText, "utf-8"),
|
|
311
|
+
preview: logText.slice(0, PREVIEW_LEN),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// CLAUDE.md — if exists and not a secret
|
|
315
|
+
const claudemdPath = path.join(repoDir, "CLAUDE.md");
|
|
316
|
+
if (fs.existsSync(claudemdPath) && !isSecretFile(claudemdPath)) {
|
|
317
|
+
importable.push({
|
|
318
|
+
id: "claudemd",
|
|
319
|
+
type: "architecture",
|
|
320
|
+
source_path: claudemdPath,
|
|
321
|
+
size_bytes: fileSizeBytes(claudemdPath),
|
|
322
|
+
preview: readPreview(claudemdPath),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
slug,
|
|
327
|
+
name,
|
|
328
|
+
path: repoDir,
|
|
329
|
+
sources: [
|
|
330
|
+
{
|
|
331
|
+
type: "git",
|
|
332
|
+
path: repoDir,
|
|
333
|
+
detail: langDetail || "git repo",
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
description,
|
|
337
|
+
language,
|
|
338
|
+
last_activity: lastActivity,
|
|
339
|
+
already_in_ar: arSlugs.has(slug),
|
|
340
|
+
importable_items: importable,
|
|
341
|
+
};
|
|
342
|
+
}));
|
|
343
|
+
for (const result of gitResults) {
|
|
344
|
+
if (result.status !== "fulfilled")
|
|
345
|
+
continue;
|
|
346
|
+
const proj = result.value;
|
|
347
|
+
const existing = projectMap.get(proj.slug);
|
|
348
|
+
if (existing) {
|
|
349
|
+
// Same slug, different path → disambiguate by appending parent dir
|
|
350
|
+
if (existing.path !== proj.path) {
|
|
351
|
+
const parentSlug = toSlug(path.basename(path.dirname(proj.path)));
|
|
352
|
+
const disambiguated = `${proj.slug}-${parentSlug}`;
|
|
353
|
+
if (!projectMap.has(disambiguated)) {
|
|
354
|
+
proj.slug = disambiguated;
|
|
355
|
+
projectMap.set(disambiguated, proj);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Same project from different sources → merge
|
|
360
|
+
existing.sources.push(...proj.sources);
|
|
361
|
+
const existingIds = new Set(existing.importable_items.map((i) => i.id));
|
|
362
|
+
for (const item of proj.importable_items) {
|
|
363
|
+
if (!existingIds.has(item.id))
|
|
364
|
+
existing.importable_items.push(item);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
projectMap.set(proj.slug, proj);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// -------------------------------------------------------------------------
|
|
372
|
+
// 2. Claude AutoMemory discovery (~/.claude/projects/)
|
|
373
|
+
// -------------------------------------------------------------------------
|
|
374
|
+
const claudeProjectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
375
|
+
if (fs.existsSync(claudeProjectsDir)) {
|
|
376
|
+
let subdirs = [];
|
|
377
|
+
try {
|
|
378
|
+
subdirs = fs.readdirSync(claudeProjectsDir);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// ignore
|
|
382
|
+
}
|
|
383
|
+
for (const encodedName of subdirs) {
|
|
384
|
+
const memoryDir = path.join(claudeProjectsDir, encodedName, "memory");
|
|
385
|
+
const memoryMdPath = path.join(memoryDir, "MEMORY.md");
|
|
386
|
+
if (!fs.existsSync(memoryMdPath))
|
|
387
|
+
continue;
|
|
388
|
+
// Decode encoded path: "-Users-tongwu-Projects-myapp" → last segment
|
|
389
|
+
const decoded = encodedName.replace(/^-/, "").replace(/-/g, "/");
|
|
390
|
+
const projectName = path.basename(decoded) || encodedName;
|
|
391
|
+
const slug = toSlug(projectName);
|
|
392
|
+
// List .md files in memory/ (skip > 5KB, skip secrets)
|
|
393
|
+
let mdFiles = [];
|
|
394
|
+
try {
|
|
395
|
+
mdFiles = fs
|
|
396
|
+
.readdirSync(memoryDir)
|
|
397
|
+
.filter((f) => f.endsWith(".md") &&
|
|
398
|
+
!isSecretFile(path.join(memoryDir, f)) &&
|
|
399
|
+
fileSizeBytes(path.join(memoryDir, f)) <= MAX_FILE_SIZE);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// ignore
|
|
403
|
+
}
|
|
404
|
+
const importable = mdFiles.map((fname) => {
|
|
405
|
+
const fpath = path.join(memoryDir, fname);
|
|
406
|
+
return {
|
|
407
|
+
id: `claude-memory:${fname}`,
|
|
408
|
+
type: "memory",
|
|
409
|
+
source_path: fpath,
|
|
410
|
+
size_bytes: fileSizeBytes(fpath),
|
|
411
|
+
preview: readPreview(fpath),
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
const sourceDetail = `${mdFiles.length} memory files`;
|
|
415
|
+
const existing = projectMap.get(slug);
|
|
416
|
+
if (existing) {
|
|
417
|
+
existing.sources.push({ type: "claude-memory", path: memoryDir, detail: sourceDetail });
|
|
418
|
+
const existingIds = new Set(existing.importable_items.map((i) => i.id));
|
|
419
|
+
for (const item of importable) {
|
|
420
|
+
if (!existingIds.has(item.id))
|
|
421
|
+
existing.importable_items.push(item);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
projectMap.set(slug, {
|
|
426
|
+
slug,
|
|
427
|
+
name: projectName,
|
|
428
|
+
path: `/${decoded}`,
|
|
429
|
+
sources: [{ type: "claude-memory", path: memoryDir, detail: sourceDetail }],
|
|
430
|
+
already_in_ar: arSlugs.has(slug),
|
|
431
|
+
importable_items: importable,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// -------------------------------------------------------------------------
|
|
437
|
+
// 3. Global items — user profile (~/.claude/projects/-Users-*/memory/user_*.md)
|
|
438
|
+
// -------------------------------------------------------------------------
|
|
439
|
+
const globalItems = [];
|
|
440
|
+
if (fs.existsSync(claudeProjectsDir)) {
|
|
441
|
+
let topDirs = [];
|
|
442
|
+
try {
|
|
443
|
+
topDirs = fs.readdirSync(claudeProjectsDir);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// ignore
|
|
447
|
+
}
|
|
448
|
+
for (const dir of topDirs) {
|
|
449
|
+
if (!dir.startsWith("-Users-"))
|
|
450
|
+
continue;
|
|
451
|
+
const memDir = path.join(claudeProjectsDir, dir, "memory");
|
|
452
|
+
if (!fs.existsSync(memDir))
|
|
453
|
+
continue;
|
|
454
|
+
let memFiles = [];
|
|
455
|
+
try {
|
|
456
|
+
memFiles = fs.readdirSync(memDir);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
for (const fname of memFiles) {
|
|
462
|
+
if (!fname.startsWith("user_") || !fname.endsWith(".md"))
|
|
463
|
+
continue;
|
|
464
|
+
const fpath = path.join(memDir, fname);
|
|
465
|
+
if (isSecretFile(fpath))
|
|
466
|
+
continue;
|
|
467
|
+
const sz = fileSizeBytes(fpath);
|
|
468
|
+
if (sz > MAX_FILE_SIZE)
|
|
469
|
+
continue;
|
|
470
|
+
globalItems.push({
|
|
471
|
+
id: `global:${fname}`,
|
|
472
|
+
type: "memory",
|
|
473
|
+
source_path: fpath,
|
|
474
|
+
size_bytes: sz,
|
|
475
|
+
preview: readPreview(fpath),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// -------------------------------------------------------------------------
|
|
481
|
+
// 4. Stats
|
|
482
|
+
// -------------------------------------------------------------------------
|
|
483
|
+
const projects = [...projectMap.values()];
|
|
484
|
+
const totalImportable = projects.reduce((acc, p) => acc + p.importable_items.length, 0) + globalItems.length;
|
|
485
|
+
const alreadyInAr = projects.filter((p) => p.already_in_ar).length;
|
|
486
|
+
return {
|
|
487
|
+
projects,
|
|
488
|
+
global_items: globalItems,
|
|
489
|
+
stats: {
|
|
490
|
+
total_projects: projects.length,
|
|
491
|
+
total_importable_items: totalImportable,
|
|
492
|
+
total_already_in_ar: alreadyInAr,
|
|
493
|
+
scan_duration_ms: Date.now() - t0,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// bootstrapImport
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
export async function bootstrapImport(scan, selection) {
|
|
501
|
+
const t0 = Date.now();
|
|
502
|
+
const includeGlobal = selection?.include_global ?? true;
|
|
503
|
+
const skipItems = new Set(selection?.skip_items ?? []);
|
|
504
|
+
const allowedTypes = selection?.item_types ? new Set(selection.item_types) : null;
|
|
505
|
+
let projectsCreated = 0;
|
|
506
|
+
let itemsImported = 0;
|
|
507
|
+
let itemsSkipped = 0;
|
|
508
|
+
const errors = [];
|
|
509
|
+
// Determine which projects to process — explicit slugs intersect with already_in_ar guard
|
|
510
|
+
let targetProjects = scan.projects.filter((p) => !p.already_in_ar);
|
|
511
|
+
if (selection?.project_slugs && selection.project_slugs.length > 0) {
|
|
512
|
+
const allowedSlugs = new Set(selection.project_slugs);
|
|
513
|
+
targetProjects = targetProjects.filter((p) => allowedSlugs.has(p.slug));
|
|
514
|
+
}
|
|
515
|
+
for (const proj of targetProjects) {
|
|
516
|
+
let createdThisProject = false;
|
|
517
|
+
for (const item of proj.importable_items) {
|
|
518
|
+
// Skip by item ID
|
|
519
|
+
if (skipItems.has(item.id)) {
|
|
520
|
+
itemsSkipped++;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
// Filter by type
|
|
524
|
+
if (allowedTypes && !allowedTypes.has(item.type)) {
|
|
525
|
+
itemsSkipped++;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
// Ensure palace initialized (idempotent)
|
|
530
|
+
ensurePalaceInitialized(proj.slug);
|
|
531
|
+
if (!createdThisProject) {
|
|
532
|
+
projectsCreated++;
|
|
533
|
+
createdThisProject = true;
|
|
534
|
+
// Problem 3: Populate identity.md from README/package.json if still placeholder
|
|
535
|
+
try {
|
|
536
|
+
const identityPath = path.join(palaceDir(proj.slug), "identity.md");
|
|
537
|
+
if (fs.existsSync(identityPath)) {
|
|
538
|
+
const identityContent = fs.readFileSync(identityPath, "utf-8");
|
|
539
|
+
if (identityContent.includes("_(fill in:")) {
|
|
540
|
+
let description = "";
|
|
541
|
+
const projectSourceDir = proj.path;
|
|
542
|
+
const readmePath = path.join(projectSourceDir, "README.md");
|
|
543
|
+
if (fs.existsSync(readmePath)) {
|
|
544
|
+
const readmeLines = fs.readFileSync(readmePath, "utf-8").split("\n");
|
|
545
|
+
let pastHeading = false;
|
|
546
|
+
for (const line of readmeLines) {
|
|
547
|
+
if (line.startsWith("# ")) {
|
|
548
|
+
pastHeading = true;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (pastHeading && line.trim() && !line.startsWith("#")) {
|
|
552
|
+
description = line.replace(/^[>_*]+/, "").trim().slice(0, 100);
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!description) {
|
|
558
|
+
const pkgPath = path.join(projectSourceDir, "package.json");
|
|
559
|
+
if (fs.existsSync(pkgPath)) {
|
|
560
|
+
try {
|
|
561
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
562
|
+
if (typeof pkg["description"] === "string") {
|
|
563
|
+
description = pkg["description"].slice(0, 100);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch { /* skip */ }
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (description) {
|
|
570
|
+
const updated = identityContent.replace(/>\s*_\(fill in:.*?\)_/, `> ${description}`);
|
|
571
|
+
fs.writeFileSync(identityPath, updated, "utf-8");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch { /* non-fatal: identity population is best-effort */ }
|
|
577
|
+
}
|
|
578
|
+
if (item.id === "identity") {
|
|
579
|
+
// Write identity.md from discovered metadata
|
|
580
|
+
const gitSource = proj.sources.find((s) => s.type === "git");
|
|
581
|
+
const identityContent = [
|
|
582
|
+
`# ${proj.name}`,
|
|
583
|
+
"",
|
|
584
|
+
proj.description ?? "No description available.",
|
|
585
|
+
"",
|
|
586
|
+
`- Language: ${proj.language ?? "unknown"}`,
|
|
587
|
+
`- Source: ${gitSource?.path ?? proj.path}`,
|
|
588
|
+
`- Bootstrapped: ${todayISO()}`,
|
|
589
|
+
].join("\n");
|
|
590
|
+
writeIdentity(proj.slug, identityContent);
|
|
591
|
+
itemsImported++;
|
|
592
|
+
}
|
|
593
|
+
else if (item.id === "git-trajectory") {
|
|
594
|
+
// Write initial journal entry from git log
|
|
595
|
+
const logText = await gitCmd(proj.path, ["log", "--oneline", "-5"]);
|
|
596
|
+
const content = [
|
|
597
|
+
`# Bootstrap — ${todayISO()}`,
|
|
598
|
+
"",
|
|
599
|
+
"## Brief",
|
|
600
|
+
`Auto-imported from git. Recent commits:\n${logText || "(no log available)"}`,
|
|
601
|
+
"",
|
|
602
|
+
"## Next",
|
|
603
|
+
"Continue from last activity.",
|
|
604
|
+
].join("\n");
|
|
605
|
+
await journalWrite({ content, project: proj.slug, saveType: "arsave" });
|
|
606
|
+
itemsImported++;
|
|
607
|
+
}
|
|
608
|
+
else if (item.id === "claudemd") {
|
|
609
|
+
// Read CLAUDE.md first 3KB, write to architecture room
|
|
610
|
+
if (!fs.existsSync(item.source_path) || isSecretFile(item.source_path)) {
|
|
611
|
+
itemsSkipped++;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const raw = fs.readFileSync(item.source_path, "utf-8");
|
|
615
|
+
const claudemdContent = raw.slice(0, 3000);
|
|
616
|
+
await palaceWrite({
|
|
617
|
+
room: "architecture",
|
|
618
|
+
topic: "project-conventions",
|
|
619
|
+
content: claudemdContent,
|
|
620
|
+
project: proj.slug,
|
|
621
|
+
});
|
|
622
|
+
itemsImported++;
|
|
623
|
+
}
|
|
624
|
+
else if (item.id.startsWith("claude-memory:")) {
|
|
625
|
+
// Read memory file, strip frontmatter, route by type
|
|
626
|
+
if (!fs.existsSync(item.source_path) || isSecretFile(item.source_path)) {
|
|
627
|
+
itemsSkipped++;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const sz = fileSizeBytes(item.source_path);
|
|
631
|
+
if (sz > MAX_FILE_SIZE) {
|
|
632
|
+
itemsSkipped++;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const rawContent = fs.readFileSync(item.source_path, "utf-8");
|
|
636
|
+
const { body, meta } = stripFrontmatter(rawContent);
|
|
637
|
+
const topic = (meta["name"] ?? item.id.replace("claude-memory:", "")).replace(/\.md$/, "");
|
|
638
|
+
if (meta["type"] === "user") {
|
|
639
|
+
// Route user-type files to awareness instead of palace
|
|
640
|
+
await awarenessUpdate({
|
|
641
|
+
insights: [
|
|
642
|
+
{
|
|
643
|
+
title: topic,
|
|
644
|
+
evidence: body,
|
|
645
|
+
applies_when: ["always"],
|
|
646
|
+
source: `bootstrap:${item.source_path}`,
|
|
647
|
+
severity: "important",
|
|
648
|
+
},
|
|
649
|
+
],
|
|
650
|
+
project: proj.slug,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
const room = getTargetRoom(meta);
|
|
655
|
+
await palaceWrite({
|
|
656
|
+
room,
|
|
657
|
+
topic,
|
|
658
|
+
content: body,
|
|
659
|
+
project: proj.slug,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
itemsImported++;
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
itemsSkipped++;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
errors.push({
|
|
670
|
+
project: proj.slug,
|
|
671
|
+
item: item.id,
|
|
672
|
+
error: err instanceof Error ? err.message : String(err),
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// -------------------------------------------------------------------------
|
|
678
|
+
// Global items (user profile)
|
|
679
|
+
// -------------------------------------------------------------------------
|
|
680
|
+
if (includeGlobal && scan.global_items.length > 0) {
|
|
681
|
+
// Import global items into a dedicated "_global" project
|
|
682
|
+
const globalSlug = "_global";
|
|
683
|
+
try {
|
|
684
|
+
ensurePalaceInitialized(globalSlug);
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
// ignore init errors for global project
|
|
688
|
+
}
|
|
689
|
+
for (const item of scan.global_items) {
|
|
690
|
+
if (skipItems.has(item.id)) {
|
|
691
|
+
itemsSkipped++;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (allowedTypes && !allowedTypes.has(item.type)) {
|
|
695
|
+
itemsSkipped++;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
if (!fs.existsSync(item.source_path) || isSecretFile(item.source_path)) {
|
|
700
|
+
itemsSkipped++;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const sz = fileSizeBytes(item.source_path);
|
|
704
|
+
if (sz > MAX_FILE_SIZE) {
|
|
705
|
+
itemsSkipped++;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
const rawContent = fs.readFileSync(item.source_path, "utf-8");
|
|
709
|
+
const { body, meta } = stripFrontmatter(rawContent);
|
|
710
|
+
const topic = (meta["name"] ?? item.id.replace("global:", "")).replace(/\.md$/, "");
|
|
711
|
+
if (meta["type"] === "user") {
|
|
712
|
+
await awarenessUpdate({
|
|
713
|
+
insights: [
|
|
714
|
+
{
|
|
715
|
+
title: topic,
|
|
716
|
+
evidence: body,
|
|
717
|
+
applies_when: ["always"],
|
|
718
|
+
source: `bootstrap:${item.source_path}`,
|
|
719
|
+
severity: "important",
|
|
720
|
+
},
|
|
721
|
+
],
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
const room = getTargetRoom(meta);
|
|
726
|
+
await palaceWrite({
|
|
727
|
+
room,
|
|
728
|
+
topic,
|
|
729
|
+
content: body,
|
|
730
|
+
project: globalSlug,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
itemsImported++;
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
errors.push({
|
|
737
|
+
project: globalSlug,
|
|
738
|
+
item: item.id,
|
|
739
|
+
error: err instanceof Error ? err.message : String(err),
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
projects_created: projectsCreated,
|
|
746
|
+
items_imported: itemsImported,
|
|
747
|
+
items_skipped: itemsSkipped,
|
|
748
|
+
errors,
|
|
749
|
+
duration_ms: Date.now() - t0,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
//# sourceMappingURL=bootstrap.js.map
|