@unson/brainbase-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +493 -0
- package/SECURITY.md +62 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +824 -0
- package/dist/guided-onboarding.d.ts +97 -0
- package/dist/guided-onboarding.js +490 -0
- package/dist/import-extract.d.ts +121 -0
- package/dist/import-extract.js +554 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/onboarding.d.ts +174 -0
- package/dist/onboarding.js +1061 -0
- package/dist/ontology-ssot.d.ts +4 -0
- package/dist/ontology-ssot.js +33 -0
- package/dist/ontology.d.ts +181 -0
- package/dist/ontology.js +384 -0
- package/dist/operationalization.d.ts +24 -0
- package/dist/operationalization.js +87 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +8 -0
- package/dist/projects.d.ts +51 -0
- package/dist/projects.js +227 -0
- package/dist/routines.d.ts +34 -0
- package/dist/routines.js +136 -0
- package/dist/server.d.ts +127 -0
- package/dist/server.js +185 -0
- package/dist/skills.d.ts +30 -0
- package/dist/skills.js +236 -0
- package/dist/ssot.d.ts +4 -0
- package/dist/ssot.js +387 -0
- package/dist/templates.d.ts +4 -0
- package/dist/templates.js +89 -0
- package/dist/tools.d.ts +6 -0
- package/dist/tools.js +180 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +1 -0
- package/package.json +65 -0
package/dist/ssot.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { access, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { assertOntologyValid } from './ontology.js';
|
|
8
|
+
import { emptyGraph, emptyRelationships, schemaTemplates } from './templates.js';
|
|
9
|
+
const canonicalFiles = ['graph.json', 'relationships.json', 'personal-kg.jsonl', 'decisions.jsonl'];
|
|
10
|
+
const lockName = '.brainbase-ssot.lock';
|
|
11
|
+
const stagingPrefix = '.brainbase-staging-';
|
|
12
|
+
const transactionPrefix = '.brainbase-transaction-';
|
|
13
|
+
const graphEntitySchema = z.object({
|
|
14
|
+
id: z.string().min(1),
|
|
15
|
+
type: z.enum(['person', 'org', 'project', 'relationship']),
|
|
16
|
+
name: z.string().min(1),
|
|
17
|
+
summary: z.string().optional(),
|
|
18
|
+
tags: z.array(z.string()).optional(),
|
|
19
|
+
metadata: z.record(z.unknown()).optional()
|
|
20
|
+
});
|
|
21
|
+
const graphSchema = z.object({
|
|
22
|
+
version: z.literal(1),
|
|
23
|
+
owner: z.object({
|
|
24
|
+
name: z.string().optional(),
|
|
25
|
+
summary: z.string().optional()
|
|
26
|
+
}).optional(),
|
|
27
|
+
entities: z.array(graphEntitySchema)
|
|
28
|
+
});
|
|
29
|
+
const personalKgSchema = z.object({
|
|
30
|
+
id: z.string().min(1),
|
|
31
|
+
type: z.enum(['self', 'work', 'relationship', 'value', 'judgment', 'experience', 'sns_context']),
|
|
32
|
+
text: z.string().min(1),
|
|
33
|
+
tags: z.array(z.string()).optional(),
|
|
34
|
+
source: z.string().optional(),
|
|
35
|
+
updatedAt: z.string().optional()
|
|
36
|
+
});
|
|
37
|
+
const relationshipSchema = z.object({
|
|
38
|
+
id: z.string().min(1),
|
|
39
|
+
person: z.string().min(1),
|
|
40
|
+
role: z.string().optional(),
|
|
41
|
+
context: z.string().min(1),
|
|
42
|
+
tags: z.array(z.string()).optional(),
|
|
43
|
+
updatedAt: z.string().optional()
|
|
44
|
+
});
|
|
45
|
+
const relationshipsSchema = z.object({
|
|
46
|
+
version: z.literal(1),
|
|
47
|
+
relationships: z.array(relationshipSchema)
|
|
48
|
+
});
|
|
49
|
+
const decisionSchema = z.object({
|
|
50
|
+
id: z.string().min(1),
|
|
51
|
+
title: z.string().min(1),
|
|
52
|
+
decision: z.string().min(1),
|
|
53
|
+
topic: z.string().min(1).optional(),
|
|
54
|
+
supersedes: z.array(z.string().min(1)).optional(),
|
|
55
|
+
effectiveAt: z.string().datetime({ offset: true }).optional(),
|
|
56
|
+
rationale: z.string().optional(),
|
|
57
|
+
tags: z.array(z.string()).optional(),
|
|
58
|
+
updatedAt: z.string().optional()
|
|
59
|
+
});
|
|
60
|
+
export async function initializePersonalOs(dataDir) {
|
|
61
|
+
await mkdir(dataDir, { recursive: true });
|
|
62
|
+
await withSsotLock(dataDir, async () => {
|
|
63
|
+
await recoverTransactions(dataDir);
|
|
64
|
+
const present = await canonicalPresence(dataDir);
|
|
65
|
+
if (present.length === canonicalFiles.length) {
|
|
66
|
+
await ensureAncillaryDirectories(dataDir);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (present.length > 0) {
|
|
70
|
+
throw new Error(`Partial canonical SSOT set in ${dataDir}: found ${present.join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
await ensureAncillaryDirectories(dataDir);
|
|
73
|
+
await commitAggregate(dataDir, {
|
|
74
|
+
dataDir,
|
|
75
|
+
graph: emptyGraph,
|
|
76
|
+
relationships: emptyRelationships,
|
|
77
|
+
personalKg: [],
|
|
78
|
+
decisions: [],
|
|
79
|
+
sourceCount: 0
|
|
80
|
+
}, 'initialization');
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function loadPersonalOs(dataDir) {
|
|
84
|
+
return withSsotLock(dataDir, async () => {
|
|
85
|
+
await recoverTransactions(dataDir);
|
|
86
|
+
assertCompleteCanonicalSet(dataDir, await canonicalPresence(dataDir));
|
|
87
|
+
return loadPersonalOsUnlocked(dataDir);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export async function mutatePersonalOs(dataDir, mutator) {
|
|
91
|
+
return withSsotLock(dataDir, async () => {
|
|
92
|
+
await recoverTransactions(dataDir);
|
|
93
|
+
assertCompleteCanonicalSet(dataDir, await canonicalPresence(dataDir));
|
|
94
|
+
const current = await loadPersonalOsUnlocked(dataDir);
|
|
95
|
+
const next = await mutator(current);
|
|
96
|
+
const normalized = { ...next, dataDir, sourceCount: current.sourceCount };
|
|
97
|
+
validateAggregate(normalized);
|
|
98
|
+
await commitAggregate(dataDir, normalized, 'mutation');
|
|
99
|
+
return normalized;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function loadPersonalOsUnlocked(dataDir) {
|
|
103
|
+
const graph = graphSchema.parse(await readJson(join(dataDir, 'graph.json')));
|
|
104
|
+
const relationships = relationshipsSchema.parse(await readJson(join(dataDir, 'relationships.json')));
|
|
105
|
+
const personalKg = await readJsonl(join(dataDir, 'personal-kg.jsonl'), personalKgSchema, 'personal-kg.jsonl');
|
|
106
|
+
const decisions = await readJsonl(join(dataDir, 'decisions.jsonl'), decisionSchema, 'decisions.jsonl');
|
|
107
|
+
const sourceCount = await countSources(dataDir);
|
|
108
|
+
return { dataDir, graph, personalKg, relationships, decisions, sourceCount };
|
|
109
|
+
}
|
|
110
|
+
async function commitAggregate(dataDir, next, mode) {
|
|
111
|
+
validateAggregate(next);
|
|
112
|
+
const id = randomUUID();
|
|
113
|
+
const stagingDir = join(dataDir, `${stagingPrefix}${id}`);
|
|
114
|
+
const transactionDir = join(dataDir, `${transactionPrefix}${id}`);
|
|
115
|
+
const nextDir = join(stagingDir, 'next');
|
|
116
|
+
const previousDir = join(stagingDir, 'previous');
|
|
117
|
+
await mkdir(nextDir, { recursive: true });
|
|
118
|
+
await writeAggregate(nextDir, next);
|
|
119
|
+
if (mode === 'mutation') {
|
|
120
|
+
await mkdir(previousDir, { recursive: true });
|
|
121
|
+
await copyCanonicalSet(dataDir, previousDir);
|
|
122
|
+
}
|
|
123
|
+
const metadata = { version: 1, mode };
|
|
124
|
+
await writeFile(join(stagingDir, 'transaction.json'), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
125
|
+
await writeFile(join(stagingDir, 'PREPARED'), '');
|
|
126
|
+
await rename(stagingDir, transactionDir);
|
|
127
|
+
try {
|
|
128
|
+
await publishCanonicalSet(dataDir, join(transactionDir, 'next'), id, true);
|
|
129
|
+
await writeFile(join(transactionDir, 'COMMITTED'), '');
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await recoverTransaction(dataDir, transactionDir).catch(() => undefined);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
await cleanupCommittedTransaction(transactionDir);
|
|
136
|
+
}
|
|
137
|
+
async function cleanupCommittedTransaction(transactionDir) {
|
|
138
|
+
try {
|
|
139
|
+
if (process.env.BRAINBASE_SSOT_FAIL_COMMITTED_CLEANUP === '1') {
|
|
140
|
+
throw new Error('Injected committed SSOT transaction cleanup failure');
|
|
141
|
+
}
|
|
142
|
+
await rm(transactionDir, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
warnCleanupFailure(`committed SSOT transaction ${transactionDir}`, error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function recoverTransactions(dataDir) {
|
|
149
|
+
const entries = await readdir(dataDir);
|
|
150
|
+
for (const entry of entries.filter((value) => value.startsWith(stagingPrefix)).sort()) {
|
|
151
|
+
await rm(join(dataDir, entry), { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
for (const entry of entries.filter((value) => value.startsWith(transactionPrefix)).sort()) {
|
|
154
|
+
await recoverTransaction(dataDir, join(dataDir, entry));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function recoverTransaction(dataDir, transactionDir) {
|
|
158
|
+
if (await exists(join(transactionDir, 'COMMITTED'))) {
|
|
159
|
+
await rm(transactionDir, { recursive: true, force: true });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!(await exists(join(transactionDir, 'PREPARED')))) {
|
|
163
|
+
throw new Error(`Incomplete registered SSOT transaction ${transactionDir}`);
|
|
164
|
+
}
|
|
165
|
+
const metadata = await readTransactionMetadata(transactionDir);
|
|
166
|
+
const retainedDir = join(transactionDir, metadata.mode === 'initialization' ? 'next' : 'previous');
|
|
167
|
+
assertCompleteCanonicalSet(retainedDir, await canonicalPresence(retainedDir));
|
|
168
|
+
if (process.env.BRAINBASE_SSOT_FAIL_RECOVERY === '1') {
|
|
169
|
+
throw new Error(`Injected SSOT transaction recovery failure for ${transactionDir}`);
|
|
170
|
+
}
|
|
171
|
+
await publishCanonicalSet(dataDir, retainedDir, `recovery-${randomUUID()}`, false);
|
|
172
|
+
await rm(transactionDir, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
async function publishCanonicalSet(dataDir, retainedDir, token, allowInjectedFailure) {
|
|
175
|
+
let published = 0;
|
|
176
|
+
const failAfter = Number.parseInt(process.env.BRAINBASE_SSOT_FAIL_AFTER_PUBLISH ?? '', 10);
|
|
177
|
+
const pauseAfterPublishMs = parsePositiveInteger(process.env.BRAINBASE_SSOT_PAUSE_AFTER_PUBLISH_MS, 0);
|
|
178
|
+
for (const fileName of canonicalFiles) {
|
|
179
|
+
const temporary = join(dataDir, `.brainbase-${fileName}-${token}.tmp`);
|
|
180
|
+
await copyFile(join(retainedDir, fileName), temporary);
|
|
181
|
+
await rename(temporary, join(dataDir, fileName));
|
|
182
|
+
published += 1;
|
|
183
|
+
if (allowInjectedFailure && pauseAfterPublishMs > 0) {
|
|
184
|
+
await delay(pauseAfterPublishMs);
|
|
185
|
+
}
|
|
186
|
+
if (allowInjectedFailure && Number.isFinite(failAfter) && published === failAfter) {
|
|
187
|
+
throw new Error(`Injected SSOT publish failure after ${published} file(s)`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function writeAggregate(targetDir, os) {
|
|
192
|
+
await writeFile(join(targetDir, 'graph.json'), `${JSON.stringify(os.graph, null, 2)}\n`);
|
|
193
|
+
await writeFile(join(targetDir, 'relationships.json'), `${JSON.stringify(os.relationships, null, 2)}\n`);
|
|
194
|
+
await writeFile(join(targetDir, 'personal-kg.jsonl'), serializeJsonl(os.personalKg));
|
|
195
|
+
await writeFile(join(targetDir, 'decisions.jsonl'), serializeJsonl(os.decisions));
|
|
196
|
+
}
|
|
197
|
+
function validateAggregate(os) {
|
|
198
|
+
graphSchema.parse(os.graph);
|
|
199
|
+
relationshipsSchema.parse(os.relationships);
|
|
200
|
+
os.personalKg.forEach((entry) => personalKgSchema.parse(entry));
|
|
201
|
+
os.decisions.forEach((decision) => decisionSchema.parse(decision));
|
|
202
|
+
assertOntologyValid(os);
|
|
203
|
+
}
|
|
204
|
+
function serializeJsonl(values) {
|
|
205
|
+
return values.length === 0 ? '' : `${values.map((value) => JSON.stringify(value)).join('\n')}\n`;
|
|
206
|
+
}
|
|
207
|
+
async function copyCanonicalSet(sourceDir, targetDir) {
|
|
208
|
+
for (const fileName of canonicalFiles) {
|
|
209
|
+
await copyFile(join(sourceDir, fileName), join(targetDir, fileName));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function ensureAncillaryDirectories(dataDir) {
|
|
213
|
+
for (const relative of ['sources', 'sources/gmail', 'sources/calendar', 'sources/drive', 'sources/tasks', 'candidates', 'schemas']) {
|
|
214
|
+
await mkdir(join(dataDir, relative), { recursive: true });
|
|
215
|
+
}
|
|
216
|
+
for (const [fileName, schema] of Object.entries(schemaTemplates)) {
|
|
217
|
+
await writeFile(join(dataDir, 'schemas', fileName), `${JSON.stringify(schema, null, 2)}\n`, { flag: 'wx' }).catch((error) => {
|
|
218
|
+
if (error.code !== 'EEXIST')
|
|
219
|
+
throw error;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function withSsotLock(dataDir, operation) {
|
|
224
|
+
await mkdir(dataDir, { recursive: true });
|
|
225
|
+
const owner = await acquireLock(dataDir);
|
|
226
|
+
try {
|
|
227
|
+
return await operation();
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
await releaseLock(dataDir, owner).catch((error) => {
|
|
231
|
+
warnCleanupFailure(`canonical SSOT lock ${join(dataDir, lockName)}`, error);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function acquireLock(dataDir) {
|
|
236
|
+
const lockDir = join(dataDir, lockName);
|
|
237
|
+
const timeoutMs = parsePositiveInteger(process.env.BRAINBASE_SSOT_LOCK_TIMEOUT_MS, 5_000);
|
|
238
|
+
const retryMs = parsePositiveInteger(process.env.BRAINBASE_SSOT_LOCK_RETRY_MS, 20);
|
|
239
|
+
const deadline = Date.now() + timeoutMs;
|
|
240
|
+
const owner = { token: randomUUID(), pid: process.pid, hostname: hostname() };
|
|
241
|
+
while (true) {
|
|
242
|
+
try {
|
|
243
|
+
await mkdir(lockDir);
|
|
244
|
+
await writeFile(join(lockDir, 'owner.json'), `${JSON.stringify(owner)}\n`, { flag: 'wx' });
|
|
245
|
+
return owner;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
const code = error.code;
|
|
249
|
+
if (code !== 'EEXIST') {
|
|
250
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => undefined);
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
await quarantineDeadSameHostLock(dataDir, lockDir);
|
|
254
|
+
if (Date.now() >= deadline) {
|
|
255
|
+
throw new Error(`Timed out waiting for canonical SSOT lock ${lockDir}`);
|
|
256
|
+
}
|
|
257
|
+
await delay(retryMs);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function quarantineDeadSameHostLock(dataDir, lockDir) {
|
|
262
|
+
const current = await readLockOwner(lockDir);
|
|
263
|
+
if (!current || current.hostname !== hostname() || isProcessAlive(current.pid))
|
|
264
|
+
return;
|
|
265
|
+
const quarantine = join(dataDir, `.brainbase-ssot-lock-stale-${randomUUID()}`);
|
|
266
|
+
try {
|
|
267
|
+
await rename(lockDir, quarantine);
|
|
268
|
+
await rm(quarantine, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (error.code !== 'ENOENT')
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function releaseLock(dataDir, owner) {
|
|
276
|
+
const lockDir = join(dataDir, lockName);
|
|
277
|
+
const current = await readLockOwner(lockDir);
|
|
278
|
+
if (current?.token === owner.token) {
|
|
279
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function readLockOwner(lockDir) {
|
|
283
|
+
try {
|
|
284
|
+
const value = JSON.parse(await readFile(join(lockDir, 'owner.json'), 'utf8'));
|
|
285
|
+
if (typeof value.token === 'string' && typeof value.pid === 'number' && typeof value.hostname === 'string') {
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Missing or malformed owner metadata is never safe to steal.
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
function isProcessAlive(pid) {
|
|
295
|
+
try {
|
|
296
|
+
process.kill(pid, 0);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
return error.code !== 'ESRCH';
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function canonicalPresence(dataDir) {
|
|
304
|
+
const result = [];
|
|
305
|
+
for (const fileName of canonicalFiles) {
|
|
306
|
+
if (await exists(join(dataDir, fileName)))
|
|
307
|
+
result.push(fileName);
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
function assertCompleteCanonicalSet(dataDir, present) {
|
|
312
|
+
if (present.length !== canonicalFiles.length) {
|
|
313
|
+
throw new Error(`Partial canonical SSOT set in ${dataDir}: found ${present.join(', ') || 'none'}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function readTransactionMetadata(transactionDir) {
|
|
317
|
+
const value = await readJson(join(transactionDir, 'transaction.json'));
|
|
318
|
+
if (value.version !== 1 || (value.mode !== 'initialization' && value.mode !== 'mutation')) {
|
|
319
|
+
throw new Error(`Invalid registered SSOT transaction metadata in ${transactionDir}`);
|
|
320
|
+
}
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
async function exists(path) {
|
|
324
|
+
try {
|
|
325
|
+
await access(path, constants.F_OK);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function readJson(filePath) {
|
|
333
|
+
try {
|
|
334
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
throw new Error(`Failed to read canonical SSOT file ${filePath}: ${formatError(error)}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function readJsonl(filePath, schema, label) {
|
|
341
|
+
let content = '';
|
|
342
|
+
try {
|
|
343
|
+
content = await readFile(filePath, 'utf8');
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
throw new Error(`Failed to read canonical SSOT file ${filePath}: ${formatError(error)}`);
|
|
347
|
+
}
|
|
348
|
+
const rows = content.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
349
|
+
return rows.map((line, index) => {
|
|
350
|
+
try {
|
|
351
|
+
return schema.parse(JSON.parse(line));
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
throw new Error(`Invalid ${label} line ${index + 1}: ${formatError(error)}`);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
async function countSources(dataDir) {
|
|
359
|
+
return countSourceFiles(join(dataDir, 'sources'));
|
|
360
|
+
}
|
|
361
|
+
async function countSourceFiles(sourceDir) {
|
|
362
|
+
try {
|
|
363
|
+
const entries = await readdir(sourceDir);
|
|
364
|
+
const counts = await Promise.all(entries.filter((entry) => !entry.startsWith('.')).map(async (entry) => {
|
|
365
|
+
const entryPath = join(sourceDir, entry);
|
|
366
|
+
const entryStat = await stat(entryPath);
|
|
367
|
+
return entryStat.isDirectory() ? countSourceFiles(entryPath) : entryStat.isFile() ? 1 : 0;
|
|
368
|
+
}));
|
|
369
|
+
return counts.reduce((sum, count) => sum + count, 0);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function parsePositiveInteger(value, fallback) {
|
|
376
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
377
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
378
|
+
}
|
|
379
|
+
function delay(milliseconds) {
|
|
380
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
381
|
+
}
|
|
382
|
+
function formatError(error) {
|
|
383
|
+
return error instanceof Error ? error.message : String(error);
|
|
384
|
+
}
|
|
385
|
+
function warnCleanupFailure(resource, error) {
|
|
386
|
+
process.emitWarning(`Failed to clean up ${resource}: ${formatError(error)}`);
|
|
387
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const emptyGraph = {
|
|
2
|
+
version: 1,
|
|
3
|
+
owner: {},
|
|
4
|
+
entities: []
|
|
5
|
+
};
|
|
6
|
+
export const emptyRelationships = {
|
|
7
|
+
version: 1,
|
|
8
|
+
relationships: []
|
|
9
|
+
};
|
|
10
|
+
export const schemaTemplates = {
|
|
11
|
+
'graph.schema.json': {
|
|
12
|
+
type: 'object',
|
|
13
|
+
required: ['version', 'entities'],
|
|
14
|
+
properties: {
|
|
15
|
+
version: { const: 1 },
|
|
16
|
+
owner: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
name: { type: 'string' },
|
|
20
|
+
summary: { type: 'string' }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
entities: {
|
|
24
|
+
type: 'array',
|
|
25
|
+
items: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
required: ['id', 'type', 'name'],
|
|
28
|
+
properties: {
|
|
29
|
+
id: { type: 'string', minLength: 1 },
|
|
30
|
+
type: { enum: ['person', 'org', 'project', 'relationship'] },
|
|
31
|
+
name: { type: 'string', minLength: 1 },
|
|
32
|
+
summary: { type: 'string' },
|
|
33
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
34
|
+
metadata: { type: 'object' }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
'personal-kg.schema.json': {
|
|
41
|
+
type: 'object',
|
|
42
|
+
required: ['id', 'type', 'text'],
|
|
43
|
+
properties: {
|
|
44
|
+
id: { type: 'string', minLength: 1 },
|
|
45
|
+
type: { enum: ['self', 'work', 'relationship', 'value', 'judgment', 'experience', 'sns_context'] },
|
|
46
|
+
text: { type: 'string', minLength: 1 },
|
|
47
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
48
|
+
source: { type: 'string' },
|
|
49
|
+
updatedAt: { type: 'string' }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
'relationships.schema.json': {
|
|
53
|
+
type: 'object',
|
|
54
|
+
required: ['version', 'relationships'],
|
|
55
|
+
properties: {
|
|
56
|
+
version: { const: 1 },
|
|
57
|
+
relationships: {
|
|
58
|
+
type: 'array',
|
|
59
|
+
items: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
required: ['id', 'person', 'context'],
|
|
62
|
+
properties: {
|
|
63
|
+
id: { type: 'string', minLength: 1 },
|
|
64
|
+
person: { type: 'string', minLength: 1 },
|
|
65
|
+
role: { type: 'string' },
|
|
66
|
+
context: { type: 'string', minLength: 1 },
|
|
67
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
68
|
+
updatedAt: { type: 'string' }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
'decisions.schema.json': {
|
|
75
|
+
type: 'object',
|
|
76
|
+
required: ['id', 'title', 'decision'],
|
|
77
|
+
properties: {
|
|
78
|
+
id: { type: 'string', minLength: 1 },
|
|
79
|
+
title: { type: 'string', minLength: 1 },
|
|
80
|
+
decision: { type: 'string', minLength: 1 },
|
|
81
|
+
rationale: { type: 'string' },
|
|
82
|
+
topic: { type: 'string', minLength: 1 },
|
|
83
|
+
supersedes: { type: 'array', items: { type: 'string', minLength: 1 } },
|
|
84
|
+
effectiveAt: { type: 'string', format: 'date-time' },
|
|
85
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
86
|
+
updatedAt: { type: 'string' }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EntityKind, PersonalOs, SearchResult } from './types.js';
|
|
2
|
+
export declare function getContext(os: PersonalOs): Record<string, unknown>;
|
|
3
|
+
export declare function listEntities(os: PersonalOs, type?: EntityKind): Record<string, unknown>;
|
|
4
|
+
export declare function searchPersonalKg(os: PersonalOs, query: string, limit?: number): SearchResult[];
|
|
5
|
+
export declare function searchAll(os: PersonalOs, query: string, limit?: number): SearchResult[];
|
|
6
|
+
export declare function onboardingStatus(os: PersonalOs): Record<string, unknown>;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { buildOperationalizationPlan } from './operationalization.js';
|
|
2
|
+
export function getContext(os) {
|
|
3
|
+
const selfEntries = os.personalKg.filter((entry) => entry.type === 'self' || entry.type === 'value' || entry.type === 'judgment');
|
|
4
|
+
const workEntries = os.personalKg.filter((entry) => entry.type === 'work' || entry.type === 'experience');
|
|
5
|
+
const projects = os.graph.entities.filter((entity) => entity.type === 'project').map((entity) => ({
|
|
6
|
+
id: entity.id,
|
|
7
|
+
name: entity.name,
|
|
8
|
+
summary: entity.summary,
|
|
9
|
+
goal: readMetadataString(entity.metadata, 'goal'),
|
|
10
|
+
status: readMetadataString(entity.metadata, 'status'),
|
|
11
|
+
role: readMetadataString(entity.metadata, 'role'),
|
|
12
|
+
sources: Array.isArray(entity.metadata?.sources) ? entity.metadata.sources : [],
|
|
13
|
+
taskSources: Array.isArray(entity.metadata?.taskSources) ? entity.metadata.taskSources : [],
|
|
14
|
+
decisionPrinciples: Array.isArray(entity.metadata?.decisionPrinciples) ? entity.metadata.decisionPrinciples : []
|
|
15
|
+
}));
|
|
16
|
+
return {
|
|
17
|
+
owner: os.graph.owner ?? {},
|
|
18
|
+
self: selfEntries.map((entry) => entry.text),
|
|
19
|
+
projects,
|
|
20
|
+
work: [
|
|
21
|
+
...projects.map((project) => project.summary ? `${project.name}: ${project.summary}` : project.name),
|
|
22
|
+
...workEntries.map((entry) => entry.text)
|
|
23
|
+
],
|
|
24
|
+
relationships: os.relationships.relationships.map((relationship) => ({
|
|
25
|
+
person: relationship.person,
|
|
26
|
+
role: relationship.role,
|
|
27
|
+
context: relationship.context
|
|
28
|
+
})),
|
|
29
|
+
decisions: os.decisions.map((decision) => ({
|
|
30
|
+
title: decision.title,
|
|
31
|
+
decision: decision.decision,
|
|
32
|
+
rationale: decision.rationale
|
|
33
|
+
})),
|
|
34
|
+
canonicalFiles: ['graph.json', 'personal-kg.jsonl', 'relationships.json', 'decisions.jsonl'],
|
|
35
|
+
note: 'Canonical local SSOT data is preferred over raw sources.'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function listEntities(os, type) {
|
|
39
|
+
const graphEntities = os.graph.entities.filter((entity) => !type || entity.type === type);
|
|
40
|
+
const relationshipEntities = type && type !== 'relationship'
|
|
41
|
+
? []
|
|
42
|
+
: os.relationships.relationships.map((relationship) => ({
|
|
43
|
+
id: relationship.id,
|
|
44
|
+
type: 'relationship',
|
|
45
|
+
name: relationship.person,
|
|
46
|
+
summary: relationship.context,
|
|
47
|
+
tags: relationship.tags
|
|
48
|
+
}));
|
|
49
|
+
const decisionEntities = type && type !== 'decision'
|
|
50
|
+
? []
|
|
51
|
+
: os.decisions.map((decision) => ({
|
|
52
|
+
id: decision.id,
|
|
53
|
+
type: 'decision',
|
|
54
|
+
name: decision.title,
|
|
55
|
+
summary: decision.decision,
|
|
56
|
+
tags: decision.tags
|
|
57
|
+
}));
|
|
58
|
+
return {
|
|
59
|
+
entities: [...graphEntities, ...relationshipEntities, ...decisionEntities]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function searchPersonalKg(os, query, limit = 10) {
|
|
63
|
+
return rank(os.personalKg.map((entry) => ({
|
|
64
|
+
source: 'personal-kg',
|
|
65
|
+
id: entry.id,
|
|
66
|
+
title: entry.type,
|
|
67
|
+
text: entry.text,
|
|
68
|
+
score: scoreText(query, [entry.id, entry.type, entry.text, ...(entry.tags ?? [])])
|
|
69
|
+
})), limit);
|
|
70
|
+
}
|
|
71
|
+
export function searchAll(os, query, limit = 10) {
|
|
72
|
+
const graphResults = os.graph.entities.map((entity) => ({
|
|
73
|
+
source: 'graph',
|
|
74
|
+
id: entity.id,
|
|
75
|
+
title: entity.name,
|
|
76
|
+
text: [entity.summary ?? entity.name, metadataText(entity.metadata)].filter(Boolean).join('\n'),
|
|
77
|
+
score: scoreText(query, [entity.id, entity.type, entity.name, entity.summary ?? '', metadataText(entity.metadata), ...(entity.tags ?? [])])
|
|
78
|
+
}));
|
|
79
|
+
const relationshipResults = os.relationships.relationships.map((relationship) => ({
|
|
80
|
+
source: 'relationships',
|
|
81
|
+
id: relationship.id,
|
|
82
|
+
title: relationship.person,
|
|
83
|
+
text: relationship.context,
|
|
84
|
+
score: scoreText(query, [relationship.id, relationship.person, relationship.role ?? '', relationship.context, ...(relationship.tags ?? [])])
|
|
85
|
+
}));
|
|
86
|
+
const decisionResults = os.decisions.map((decision) => ({
|
|
87
|
+
source: 'decisions',
|
|
88
|
+
id: decision.id,
|
|
89
|
+
title: decision.title,
|
|
90
|
+
text: [decision.decision, decision.rationale ?? ''].filter(Boolean).join('\n'),
|
|
91
|
+
score: scoreText(query, [decision.id, decision.title, decision.decision, decision.rationale ?? '', ...(decision.tags ?? [])])
|
|
92
|
+
}));
|
|
93
|
+
return rank([...graphResults, ...searchPersonalKg(os, query, limit), ...relationshipResults, ...decisionResults], limit);
|
|
94
|
+
}
|
|
95
|
+
export function onboardingStatus(os) {
|
|
96
|
+
const seeded = {
|
|
97
|
+
self: Boolean(os.graph.owner?.name) || os.personalKg.some((entry) => entry.type === 'self'),
|
|
98
|
+
work: os.graph.entities.some((entity) => entity.type === 'project') || os.personalKg.some((entry) => entry.type === 'work'),
|
|
99
|
+
relationships: os.relationships.relationships.length > 0
|
|
100
|
+
};
|
|
101
|
+
const missing = Object.entries(seeded)
|
|
102
|
+
.filter(([, value]) => !value)
|
|
103
|
+
.map(([key]) => key);
|
|
104
|
+
const valueDemoReady = missing.length === 0;
|
|
105
|
+
const operationalization = buildOperationalizationPlan({
|
|
106
|
+
dataDir: os.dataDir,
|
|
107
|
+
firstValueReady: valueDemoReady
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
connected: true,
|
|
111
|
+
dataDir: os.dataDir,
|
|
112
|
+
seeded,
|
|
113
|
+
missing,
|
|
114
|
+
valueDemo: {
|
|
115
|
+
ready: valueDemoReady,
|
|
116
|
+
missing,
|
|
117
|
+
command: 'brainbase onboard:demo --scenario "<real request>"',
|
|
118
|
+
completionSignal: valueDemoReady ? 'first_value_demo_ready' : 'needs_seed'
|
|
119
|
+
},
|
|
120
|
+
operationalization,
|
|
121
|
+
counts: {
|
|
122
|
+
graphEntities: os.graph.entities.length,
|
|
123
|
+
personalKgEntries: os.personalKg.length,
|
|
124
|
+
relationships: os.relationships.relationships.length,
|
|
125
|
+
decisions: os.decisions.length,
|
|
126
|
+
rawSources: os.sourceCount
|
|
127
|
+
},
|
|
128
|
+
backend: 'local'
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function rank(results, limit) {
|
|
132
|
+
return results
|
|
133
|
+
.filter((result) => result.score > 0)
|
|
134
|
+
.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
|
|
135
|
+
.slice(0, Math.max(1, limit));
|
|
136
|
+
}
|
|
137
|
+
function scoreText(query, fields) {
|
|
138
|
+
const terms = searchTokens(query);
|
|
139
|
+
if (terms.length === 0) {
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
const haystack = fields.join('\n').toLowerCase();
|
|
143
|
+
const phrase = query.trim().toLowerCase();
|
|
144
|
+
const matchedTerms = terms.filter((term) => haystack.includes(term));
|
|
145
|
+
if (phrase && haystack.includes(phrase)) {
|
|
146
|
+
return terms.length + 20;
|
|
147
|
+
}
|
|
148
|
+
if (matchedTerms.length === terms.length) {
|
|
149
|
+
return terms.length + 10;
|
|
150
|
+
}
|
|
151
|
+
return matchedTerms.length;
|
|
152
|
+
}
|
|
153
|
+
function searchTokens(query) {
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
return query
|
|
156
|
+
.normalize('NFKC')
|
|
157
|
+
.replace(/[|/\\,;:()[\]{}"'`“”‘’、。・・,.:;()[]{}「」『』【】]/g, ' ')
|
|
158
|
+
.replace(/[-_]+/g, ' ')
|
|
159
|
+
.toLowerCase()
|
|
160
|
+
.split(/\s+/)
|
|
161
|
+
.map((term) => term.trim())
|
|
162
|
+
.filter(Boolean)
|
|
163
|
+
.filter((term) => {
|
|
164
|
+
if (seen.has(term)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
seen.add(term);
|
|
168
|
+
return true;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function metadataText(metadata) {
|
|
172
|
+
if (!metadata) {
|
|
173
|
+
return '';
|
|
174
|
+
}
|
|
175
|
+
return JSON.stringify(metadata);
|
|
176
|
+
}
|
|
177
|
+
function readMetadataString(metadata, key) {
|
|
178
|
+
const value = metadata?.[key];
|
|
179
|
+
return typeof value === 'string' ? value : undefined;
|
|
180
|
+
}
|