@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/dist/cli.js ADDED
@@ -0,0 +1,824 @@
1
+ #!/usr/bin/env node
2
+ import { constants } from 'node:fs';
3
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { delimiter, dirname, isAbsolute, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { initializePersonalOs, loadPersonalOs, mutatePersonalOs } from './ssot.js';
7
+ import { resolveDataDir } from './paths.js';
8
+ import { auditPersonalOsDirectory } from './ontology-ssot.js';
9
+ import { portableOntology, resolveOntologyVersion } from './ontology.js';
10
+ import { onboardingStatus } from './tools.js';
11
+ import { buildCandidateDrafts, parseOnboardingFormat, renderAgentProtocol, renderCandidateDrafts, renderConnectorRecommendations, renderLocalOnboardingPlan, renderSourceDiagnosis, renderValueDemo } from './onboarding.js';
12
+ import { buildExtractedCandidateSet, extractCandidates, loadApplyCandidates, normalizeSource, parseProvider, planApply, renderSourceJsonl, sourcePathFor } from './import-extract.js';
13
+ import { buildClaudeScheduledTasks, buildRoutines, parseRoutineKinds, parseRoutineTarget, renderCodexAutomations } from './routines.js';
14
+ import { assertPublicSafeSkillBundle, buildSkillBundle, parseSkillIds, parseSkillTarget, renderSkillsMarkdown } from './skills.js';
15
+ import { buildProjectRegistrationPlan, parseProjectSource, parseProjectStakeholder, renderProjectRegistrationMarkdown } from './projects.js';
16
+ import { renderGuidedFirstRun } from './guided-onboarding.js';
17
+ export async function runCli(argv = process.argv.slice(2), io = process) {
18
+ const parsed = parseArgs(argv);
19
+ try {
20
+ switch (parsed.command) {
21
+ case 'onboard:init':
22
+ return await onboardInit(parsed, io);
23
+ case 'onboard:seed':
24
+ return await onboardSeed(parsed, io);
25
+ case 'onboard:install':
26
+ return await onboardInstall(parsed, io);
27
+ case 'onboard:agent':
28
+ return await onboardAgent(parsed, io);
29
+ case 'onboard:start':
30
+ return await onboardStart(parsed, io);
31
+ case 'onboard:demo':
32
+ return await onboardDemo(parsed, io);
33
+ case 'onboard:recommend':
34
+ return await onboardRecommend(parsed, io);
35
+ case 'onboard:diagnose-sources':
36
+ return await onboardDiagnoseSources(parsed, io);
37
+ case 'onboard:plan':
38
+ return await onboardPlan(parsed, io);
39
+ case 'onboard:projects':
40
+ return await onboardProjects(parsed, io);
41
+ case 'onboard:candidates':
42
+ return await onboardCandidates(parsed, io);
43
+ case 'onboard:import':
44
+ return await onboardImport(parsed, io);
45
+ case 'onboard:extract':
46
+ return await onboardExtract(parsed, io);
47
+ case 'onboard:apply':
48
+ return await onboardApply(parsed, io);
49
+ case 'onboard:routines':
50
+ return await onboardRoutines(parsed, io);
51
+ case 'onboard:skills':
52
+ return await onboardSkills(parsed, io);
53
+ case 'ontology:show':
54
+ write(io, `${JSON.stringify(portableOntology, null, 2)}\n`);
55
+ return 0;
56
+ case 'ontology:audit':
57
+ return await ontologyAudit(parsed, io);
58
+ case 'doctor':
59
+ return await doctor(parsed, io);
60
+ case 'mcp':
61
+ await import('./index.js');
62
+ return 0;
63
+ default:
64
+ write(io, usage());
65
+ return parsed.flags.has('help') || !parsed.command ? 0 : 1;
66
+ }
67
+ }
68
+ catch (error) {
69
+ writeError(io, `${error instanceof Error ? error.message : String(error)}\n`);
70
+ return 1;
71
+ }
72
+ }
73
+ async function onboardInit(parsed, io) {
74
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
75
+ await initializePersonalOs(dataDir);
76
+ write(io, `Initialized Brainbase Personal OS at ${dataDir}\n`);
77
+ return 0;
78
+ }
79
+ async function onboardSeed(parsed, io) {
80
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
81
+ await initializePersonalOs(dataDir);
82
+ const name = first(parsed, 'name');
83
+ const hasSeedValues = Boolean(name)
84
+ || ['value', 'project', 'decision-principle', 'relationship'].some((key) => (parsed.values.get(key)?.length ?? 0) > 0);
85
+ if (!parsed.flags.has('non-interactive') && !hasSeedValues) {
86
+ write(io, 'No seed values provided. Use --name, --value, --project, --decision-principle, or --relationship.\n');
87
+ return 1;
88
+ }
89
+ await mutatePersonalOs(dataDir, (os) => {
90
+ const now = new Date().toISOString();
91
+ const personalEntries = [];
92
+ const decisions = [];
93
+ const relationships = [...os.relationships.relationships];
94
+ const graphEntities = [...os.graph.entities];
95
+ if (name) {
96
+ os.graph.owner = { ...os.graph.owner, name };
97
+ upsertGraphEntity(graphEntities, {
98
+ id: 'self',
99
+ type: 'person',
100
+ name,
101
+ summary: 'Owner of this local Brainbase Personal OS.',
102
+ tags: ['self']
103
+ });
104
+ personalEntries.push({
105
+ id: `self-${Date.now()}`,
106
+ type: 'self',
107
+ text: `I am ${name}.`,
108
+ tags: ['self'],
109
+ updatedAt: now
110
+ });
111
+ }
112
+ for (const value of parsed.values.get('value') ?? []) {
113
+ personalEntries.push({
114
+ id: `value-${hash(value)}`,
115
+ type: 'value',
116
+ text: value,
117
+ tags: ['onboarding'],
118
+ updatedAt: now
119
+ });
120
+ }
121
+ for (const project of parsed.values.get('project') ?? []) {
122
+ upsertGraphEntity(graphEntities, {
123
+ id: `project-${hash(project)}`,
124
+ type: 'project',
125
+ name: project,
126
+ summary: 'Seeded during Brainbase onboarding.',
127
+ tags: ['work']
128
+ });
129
+ personalEntries.push({
130
+ id: `work-${hash(project)}`,
131
+ type: 'work',
132
+ text: project,
133
+ tags: ['work'],
134
+ updatedAt: now
135
+ });
136
+ }
137
+ for (const value of parsed.values.get('decision-principle') ?? []) {
138
+ decisions.push({
139
+ id: `decision-${hash(value)}`,
140
+ title: 'Seeded decision principle',
141
+ decision: value,
142
+ tags: ['principle', 'onboarding'],
143
+ updatedAt: now
144
+ });
145
+ }
146
+ for (const encoded of parsed.values.get('relationship') ?? []) {
147
+ const [person, role, context] = encoded.split('|').map((part) => part.trim());
148
+ if (!person || !context) {
149
+ throw new Error('relationship must be "person|role|context" or "person||context"');
150
+ }
151
+ relationships.push({
152
+ id: `relationship-${hash(encoded)}`,
153
+ person,
154
+ role: role || undefined,
155
+ context,
156
+ tags: ['relationship'],
157
+ updatedAt: now
158
+ });
159
+ upsertGraphEntity(graphEntities, {
160
+ id: `person-${hash(person)}`,
161
+ type: 'person',
162
+ name: person,
163
+ summary: context,
164
+ tags: ['relationship']
165
+ });
166
+ }
167
+ return proposedPersonalOs(os, {
168
+ graph: { ...os.graph, entities: graphEntities },
169
+ relationships: { version: 1, relationships },
170
+ personalKg: [...os.personalKg, ...personalEntries],
171
+ decisions: [...os.decisions, ...decisions]
172
+ });
173
+ });
174
+ write(io, `Seeded Brainbase Personal OS at ${dataDir}\n`);
175
+ return 0;
176
+ }
177
+ async function onboardInstall(parsed, io) {
178
+ const target = first(parsed, 'target');
179
+ if (!isInstallTarget(target)) {
180
+ throw new Error('onboard:install requires --target codex|claude|codecode');
181
+ }
182
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
183
+ const payload = buildInstallPayload(target, dataDir);
184
+ const outputPath = first(parsed, 'output');
185
+ if (parsed.flags.has('dry-run') || !outputPath) {
186
+ write(io, payload);
187
+ return 0;
188
+ }
189
+ await mkdir(dirname(outputPath), { recursive: true });
190
+ await writeConfigSnippet(outputPath, payload);
191
+ write(io, `Wrote ${target} MCP config to ${outputPath}\n`);
192
+ return 0;
193
+ }
194
+ async function onboardAgent(parsed, io) {
195
+ const format = parseOnboardingFormat(first(parsed, 'format'));
196
+ write(io, renderAgentProtocol(format));
197
+ return 0;
198
+ }
199
+ async function onboardStart(parsed, io) {
200
+ const format = parseOnboardingFormat(first(parsed, 'format'));
201
+ const target = parseGuidedTarget(first(parsed, 'target'));
202
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
203
+ await initializePersonalOs(dataDir);
204
+ const os = await loadPersonalOs(dataDir);
205
+ const status = onboardingStatus(os);
206
+ const missing = Array.isArray(status.missing)
207
+ ? status.missing.filter((item) => typeof item === 'string')
208
+ : [];
209
+ const explicitGogCommand = first(parsed, 'gog-command');
210
+ const gogCommand = explicitGogCommand ?? 'gog';
211
+ const gogAvailable = parsed.flags.has('assume-gog') || await commandExists(gogCommand);
212
+ const projectName = first(parsed, 'project') ?? first(parsed, 'project-name');
213
+ const project = projectName ? {
214
+ name: projectName,
215
+ goal: first(parsed, 'goal'),
216
+ status: first(parsed, 'status'),
217
+ role: first(parsed, 'role'),
218
+ stakeholders: (parsed.values.get('stakeholder') ?? []).map(parseProjectStakeholder),
219
+ sources: (parsed.values.get('source') ?? []).map(parseProjectSource),
220
+ taskSources: parsed.values.get('task-source') ?? [],
221
+ decisionPrinciples: parsed.values.get('decision-principle') ?? []
222
+ } : undefined;
223
+ write(io, renderGuidedFirstRun({
224
+ dataDir,
225
+ target,
226
+ profile: first(parsed, 'profile'),
227
+ host: first(parsed, 'host'),
228
+ name: first(parsed, 'name'),
229
+ value: parsed.values.get('value') ?? [],
230
+ email: first(parsed, 'email'),
231
+ secondaryEmails: parsed.values.get('secondary-email') ?? [],
232
+ calendar: first(parsed, 'calendar'),
233
+ drive: first(parsed, 'drive'),
234
+ driveFolders: parsed.values.get('drive-folder') ?? [],
235
+ localFolders: parsed.values.get('local-folder') ?? [],
236
+ tasks: first(parsed, 'tasks'),
237
+ inactiveTaskTools: parsed.values.get('inactive-task-tool') ?? [],
238
+ gogCommand: explicitGogCommand,
239
+ assumeGog: parsed.flags.has('assume-gog'),
240
+ gogAvailable,
241
+ project,
242
+ connected: status.connected === true,
243
+ missing
244
+ }, format));
245
+ return 0;
246
+ }
247
+ async function onboardDemo(parsed, io) {
248
+ const format = parseOnboardingFormat(first(parsed, 'format'));
249
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
250
+ await initializePersonalOs(dataDir);
251
+ const os = await loadPersonalOs(dataDir);
252
+ write(io, renderValueDemo({ os, scenario: first(parsed, 'scenario') }, format));
253
+ return 0;
254
+ }
255
+ async function onboardRecommend(parsed, io) {
256
+ const format = parseOnboardingFormat(first(parsed, 'format'));
257
+ write(io, renderConnectorRecommendations({
258
+ email: first(parsed, 'email'),
259
+ calendar: first(parsed, 'calendar'),
260
+ drive: first(parsed, 'drive'),
261
+ tasks: first(parsed, 'tasks')
262
+ }, format));
263
+ return 0;
264
+ }
265
+ async function onboardDiagnoseSources(parsed, io) {
266
+ const format = parseOnboardingFormat(first(parsed, 'format'));
267
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
268
+ await initializePersonalOs(dataDir);
269
+ const gogCommand = first(parsed, 'gog-command') ?? 'gog';
270
+ const gogAvailable = parsed.flags.has('assume-gog') || await commandExists(gogCommand);
271
+ write(io, renderSourceDiagnosis({
272
+ dataDir,
273
+ email: first(parsed, 'email'),
274
+ calendar: first(parsed, 'calendar'),
275
+ drive: first(parsed, 'drive'),
276
+ tasks: first(parsed, 'tasks'),
277
+ gogCommand,
278
+ gogAvailable,
279
+ driveFolders: parsed.values.get('drive-folder') ?? []
280
+ }, format));
281
+ return 0;
282
+ }
283
+ async function onboardPlan(parsed, io) {
284
+ const format = parseOnboardingFormat(first(parsed, 'format'));
285
+ write(io, renderLocalOnboardingPlan({
286
+ profile: first(parsed, 'profile'),
287
+ host: first(parsed, 'host'),
288
+ email: first(parsed, 'email'),
289
+ secondaryEmails: parsed.values.get('secondary-email') ?? [],
290
+ calendar: first(parsed, 'calendar'),
291
+ drive: first(parsed, 'drive'),
292
+ driveFolders: parsed.values.get('drive-folder') ?? [],
293
+ localFolders: parsed.values.get('local-folder') ?? [],
294
+ tasks: first(parsed, 'tasks'),
295
+ inactiveTaskTools: parsed.values.get('inactive-task-tool') ?? []
296
+ }, format));
297
+ return 0;
298
+ }
299
+ async function onboardCandidates(parsed, io) {
300
+ const format = parseOnboardingFormat(first(parsed, 'format'));
301
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
302
+ await initializePersonalOs(dataDir);
303
+ const input = {
304
+ dataDir,
305
+ name: first(parsed, 'name'),
306
+ values: parsed.values.get('value') ?? [],
307
+ projects: parsed.values.get('project') ?? [],
308
+ relationships: parsed.values.get('relationship') ?? [],
309
+ decisionPrinciples: parsed.values.get('decision-principle') ?? [],
310
+ now: new Date().toISOString()
311
+ };
312
+ const candidateSet = buildCandidateDrafts(input);
313
+ if (candidateSet.candidates.length === 0 && !parsed.flags.has('non-interactive')) {
314
+ write(io, 'No candidate values provided. Use --name, --value, --project, --decision-principle, or --relationship.\n');
315
+ return 1;
316
+ }
317
+ if (parsed.flags.has('write')) {
318
+ await mkdir(dirname(candidateSet.candidatePath), { recursive: true });
319
+ await writeFile(candidateSet.candidatePath, `${JSON.stringify(candidateSet, null, 2)}\n`, { flag: 'wx' });
320
+ }
321
+ write(io, renderCandidateDrafts(input, format));
322
+ if (parsed.flags.has('write')) {
323
+ write(io, format === 'json' ? '' : `Wrote candidate file: ${candidateSet.candidatePath}\n`);
324
+ }
325
+ return 0;
326
+ }
327
+ async function onboardProjects(parsed, io) {
328
+ const format = parseOnboardingFormat(first(parsed, 'format'));
329
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
330
+ await initializePersonalOs(dataDir);
331
+ const name = first(parsed, 'name');
332
+ if (!name) {
333
+ throw new Error('onboard:projects requires --name <project-name>');
334
+ }
335
+ const plan = buildProjectRegistrationPlan({
336
+ name,
337
+ goal: first(parsed, 'goal'),
338
+ status: first(parsed, 'status'),
339
+ role: first(parsed, 'role'),
340
+ stakeholders: (parsed.values.get('stakeholder') ?? []).map(parseProjectStakeholder),
341
+ sources: (parsed.values.get('source') ?? []).map(parseProjectSource),
342
+ taskSources: parsed.values.get('task-source') ?? [],
343
+ decisionPrinciples: parsed.values.get('decision-principle') ?? [],
344
+ now: parsed.flags.has('write') ? new Date().toISOString() : undefined
345
+ });
346
+ const willWrite = parsed.flags.has('write');
347
+ if (willWrite) {
348
+ await applyProjectRegistrationPlan(dataDir, plan);
349
+ }
350
+ if (format === 'json') {
351
+ write(io, `${JSON.stringify({ ...plan, canonicalWrites: willWrite, dataDir }, null, 2)}\n`);
352
+ }
353
+ else {
354
+ write(io, renderProjectRegistrationMarkdown(plan, willWrite, dataDir));
355
+ }
356
+ return 0;
357
+ }
358
+ async function applyProjectRegistrationPlan(dataDir, plan) {
359
+ await mutatePersonalOs(dataDir, (os) => {
360
+ const graphEntities = [...os.graph.entities];
361
+ for (const entity of plan.writes.graphEntities) {
362
+ upsertGraphEntity(graphEntities, entity);
363
+ }
364
+ const relationships = [...os.relationships.relationships];
365
+ for (const relationship of plan.writes.relationships) {
366
+ if (!relationships.some((existing) => existing.id === relationship.id)) {
367
+ relationships.push(relationship);
368
+ }
369
+ }
370
+ return proposedPersonalOs(os, {
371
+ graph: { ...os.graph, entities: graphEntities },
372
+ relationships: { version: 1, relationships },
373
+ personalKg: [...os.personalKg, ...plan.writes.personalKg],
374
+ decisions: [...os.decisions, ...plan.writes.decisions]
375
+ });
376
+ });
377
+ }
378
+ async function onboardImport(parsed, io) {
379
+ const format = parseOnboardingFormat(first(parsed, 'format'));
380
+ const provider = parseProvider(first(parsed, 'source'));
381
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
382
+ await initializePersonalOs(dataDir);
383
+ const fromPath = first(parsed, 'from');
384
+ if (!fromPath) {
385
+ throw new Error('onboard:import requires --from <file|-> with collected provider JSON.');
386
+ }
387
+ const rawText = await readInput(fromPath, io);
388
+ let raw;
389
+ try {
390
+ raw = JSON.parse(rawText);
391
+ }
392
+ catch (error) {
393
+ throw new Error(`onboard:import expected JSON from ${fromPath}: ${error instanceof Error ? error.message : String(error)}`);
394
+ }
395
+ const records = normalizeSource(provider, raw, { includeDescriptions: parsed.flags.has('include-descriptions') });
396
+ const relativePath = first(parsed, 'out') ?? sourcePathFor(provider);
397
+ const outPath = isAbsolute(relativePath) ? relativePath : join(dataDir, relativePath);
398
+ await mkdir(dirname(outPath), { recursive: true });
399
+ await writeFile(outPath, renderSourceJsonl(records));
400
+ if (format === 'json') {
401
+ write(io, `${JSON.stringify({ provider, sourcePath: outPath, count: records.length }, null, 2)}\n`);
402
+ }
403
+ else {
404
+ write(io, `Imported ${records.length} ${provider} record(s) (metadata-first) to ${outPath}\n`);
405
+ }
406
+ return 0;
407
+ }
408
+ async function onboardExtract(parsed, io) {
409
+ const format = parseOnboardingFormat(first(parsed, 'format'));
410
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
411
+ await initializePersonalOs(dataDir);
412
+ const records = await readSourceRecords(dataDir);
413
+ const candidates = extractCandidates(records, {
414
+ selfEmails: parsed.values.get('self-email') ?? [],
415
+ topRelationships: numberOption(first(parsed, 'top-relationships'))
416
+ });
417
+ const candidateSet = buildExtractedCandidateSet(candidates, dataDir);
418
+ if (records.length === 0) {
419
+ write(io, format === 'json'
420
+ ? `${JSON.stringify(candidateSet, null, 2)}\n`
421
+ : 'No source records found. Run onboard:import first.\n');
422
+ return records.length === 0 && !parsed.flags.has('write') ? 1 : 0;
423
+ }
424
+ if (parsed.flags.has('write')) {
425
+ await mkdir(dirname(candidateSet.candidatePath), { recursive: true });
426
+ await writeFile(candidateSet.candidatePath, `${JSON.stringify(candidateSet, null, 2)}\n`);
427
+ }
428
+ write(io, format === 'json' ? `${JSON.stringify(candidateSet, null, 2)}\n` : renderExtractedSet(candidateSet));
429
+ if (parsed.flags.has('write') && format !== 'json') {
430
+ write(io, `Wrote extracted candidate file: ${candidateSet.candidatePath}\n`);
431
+ }
432
+ return 0;
433
+ }
434
+ async function onboardApply(parsed, io) {
435
+ const format = parseOnboardingFormat(first(parsed, 'format'));
436
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
437
+ await initializePersonalOs(dataDir);
438
+ const fromPath = first(parsed, 'from');
439
+ if (!fromPath) {
440
+ throw new Error('onboard:apply requires --from <candidate-file>.');
441
+ }
442
+ const raw = JSON.parse(await readInput(fromPath, io));
443
+ const candidates = loadApplyCandidates(raw);
444
+ const selectedIds = new Set(parsed.values.get('select') ?? []);
445
+ const all = parsed.flags.has('all');
446
+ if (!all && selectedIds.size === 0) {
447
+ throw new Error('onboard:apply requires --select <id> (repeatable) or --all to choose which candidates to promote.');
448
+ }
449
+ const now = new Date().toISOString();
450
+ const willWrite = parsed.flags.has('write');
451
+ let result;
452
+ if (willWrite) {
453
+ await mutatePersonalOs(dataDir, (os) => {
454
+ result = planApply(candidates, { ids: selectedIds, all }, {
455
+ graphEntities: [...os.graph.entities],
456
+ relationships: [...os.relationships.relationships],
457
+ personalKg: os.personalKg,
458
+ decisions: os.decisions,
459
+ ownerName: os.graph.owner?.name
460
+ }, now);
461
+ return proposedPersonalOs(os, {
462
+ graph: { ...os.graph, owner: result.ownerName ? { ...os.graph.owner, name: result.ownerName } : os.graph.owner, entities: result.graphEntities },
463
+ relationships: { version: 1, relationships: result.relationships },
464
+ personalKg: [...os.personalKg, ...result.personalKgAdditions],
465
+ decisions: [...os.decisions, ...result.decisionAdditions]
466
+ });
467
+ });
468
+ }
469
+ else {
470
+ const os = await loadPersonalOs(dataDir);
471
+ result = planApply(candidates, { ids: selectedIds, all }, {
472
+ graphEntities: [...os.graph.entities],
473
+ relationships: [...os.relationships.relationships],
474
+ personalKg: os.personalKg,
475
+ decisions: os.decisions,
476
+ ownerName: os.graph.owner?.name
477
+ }, now);
478
+ }
479
+ if (!result) {
480
+ throw new Error('Failed to compute onboarding apply result.');
481
+ }
482
+ if (format === 'json') {
483
+ write(io, `${JSON.stringify({ applied: result.applied, skipped: result.skipped, canonicalWrites: willWrite, dataDir }, null, 2)}\n`);
484
+ }
485
+ else {
486
+ write(io, renderApplyResult(result, willWrite, dataDir));
487
+ }
488
+ return 0;
489
+ }
490
+ async function readInput(fromPath, io) {
491
+ if (fromPath === '-') {
492
+ return readStdin();
493
+ }
494
+ return readFile(fromPath, 'utf8');
495
+ }
496
+ async function readStdin() {
497
+ const chunks = [];
498
+ for await (const chunk of process.stdin) {
499
+ chunks.push(Buffer.from(chunk));
500
+ }
501
+ return Buffer.concat(chunks).toString('utf8');
502
+ }
503
+ async function readSourceRecords(dataDir) {
504
+ const relativePaths = ['sources/gmail/threads.jsonl', 'sources/calendar/events.jsonl', 'sources/drive/files.jsonl', 'sources/drive/local-files.jsonl'];
505
+ const records = [];
506
+ for (const relativePath of relativePaths) {
507
+ let content = '';
508
+ try {
509
+ content = await readFile(join(dataDir, relativePath), 'utf8');
510
+ }
511
+ catch {
512
+ continue;
513
+ }
514
+ for (const line of content.split('\n').map((row) => row.trim()).filter(Boolean)) {
515
+ try {
516
+ records.push(JSON.parse(line));
517
+ }
518
+ catch {
519
+ // Skip malformed source lines; sources are secondary material.
520
+ }
521
+ }
522
+ }
523
+ return records;
524
+ }
525
+ function renderExtractedSet(set) {
526
+ const lines = ['# Brainbase Extracted Candidates', '', set.goal, ''];
527
+ lines.push(`- Candidate path: \`${set.candidatePath}\``);
528
+ lines.push(`- Counts: person ${set.counts.person}, org ${set.counts.org}, project ${set.counts.project}, relationship ${set.counts.relationship}, next_action ${set.counts.next_action}`);
529
+ lines.push('', '## Candidates');
530
+ for (const candidate of set.candidates) {
531
+ const label = candidate.payload.name ?? candidate.payload.person ?? candidate.payload.text ?? candidate.id;
532
+ lines.push(`- [${candidate.kind}] ${candidate.id}: ${String(label)} (count ${candidate.provenance.count}, sources ${candidate.provenance.sources.join('/')})`);
533
+ }
534
+ lines.push('', '## Safety Rules');
535
+ for (const rule of set.safetyRules) {
536
+ lines.push(`- ${rule}`);
537
+ }
538
+ lines.push('', '## Next Commands');
539
+ for (const command of set.nextCommands) {
540
+ lines.push(`- ${command}`);
541
+ }
542
+ return `${lines.join('\n')}\n`;
543
+ }
544
+ function renderApplyResult(result, wrote, dataDir) {
545
+ const lines = ['# Brainbase Apply', ''];
546
+ lines.push(`- Canonical writes: ${wrote}`);
547
+ lines.push(`- Data dir: ${dataDir}`);
548
+ lines.push('', '## Applied');
549
+ if (result.applied.length === 0) {
550
+ lines.push('- (none)');
551
+ }
552
+ for (const item of result.applied) {
553
+ lines.push(`- [${item.kind}] ${item.id}: ${item.summary}`);
554
+ }
555
+ lines.push('', '## Skipped');
556
+ if (result.skipped.length === 0) {
557
+ lines.push('- (none)');
558
+ }
559
+ for (const item of result.skipped) {
560
+ lines.push(`- ${item.id}: ${item.reason}`);
561
+ }
562
+ if (!wrote) {
563
+ lines.push('', 'Dry-run only. Re-run with --write to promote selected candidates into canonical SSOT.');
564
+ }
565
+ return `${lines.join('\n')}\n`;
566
+ }
567
+ function numberOption(value) {
568
+ if (!value) {
569
+ return undefined;
570
+ }
571
+ const parsed = Number.parseInt(value, 10);
572
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
573
+ }
574
+ async function onboardRoutines(parsed, io) {
575
+ const format = parseOnboardingFormat(first(parsed, 'format'));
576
+ const target = parseRoutineTarget(first(parsed, 'target'));
577
+ const kinds = parseRoutineKinds(first(parsed, 'routines'));
578
+ const cwd = first(parsed, 'cwd') ?? process.cwd();
579
+ const definitions = buildRoutines(kinds, {
580
+ ohayoHour: numberOption(first(parsed, 'ohayo-hour')),
581
+ ohayoMinute: numberOption(first(parsed, 'ohayo-minute')),
582
+ oyasumiHour: numberOption(first(parsed, 'oyasumi-hour')),
583
+ oyasumiMinute: numberOption(first(parsed, 'oyasumi-minute')),
584
+ retroDow: first(parsed, 'retro-dow'),
585
+ retroHour: numberOption(first(parsed, 'retro-hour')),
586
+ retroMinute: numberOption(first(parsed, 'retro-minute'))
587
+ });
588
+ const payload = target === 'codex'
589
+ ? renderCodexAutomations(definitions, cwd, first(parsed, 'model') ?? 'gpt-5')
590
+ : `${JSON.stringify(buildClaudeScheduledTasks(definitions, cwd), null, 2)}\n`;
591
+ const outPath = first(parsed, 'out');
592
+ if (outPath) {
593
+ await mkdir(dirname(outPath), { recursive: true });
594
+ await writeConfigSnippet(outPath, payload);
595
+ }
596
+ if (format === 'json') {
597
+ write(io, `${JSON.stringify({ target, cwd, outPath: outPath ?? null, routines: definitions }, null, 2)}\n`);
598
+ }
599
+ else {
600
+ write(io, renderRoutinesMarkdown(definitions, target, payload, outPath));
601
+ }
602
+ return 0;
603
+ }
604
+ function renderRoutinesMarkdown(definitions, target, payload, outPath) {
605
+ const lines = ['# Brainbase Personal Routines', ''];
606
+ lines.push(`- Target agent: ${target}`);
607
+ lines.push(`- Routines: ${definitions.map((definition) => definition.kind).join(', ')}`);
608
+ lines.push('- Generation only: this prints a definition; it does not register with any live scheduler.');
609
+ lines.push('', '## Schedule');
610
+ for (const definition of definitions) {
611
+ const when = definition.schedule.freq === 'weekly'
612
+ ? `weekly ${definition.schedule.dayOfWeek} ${pad(definition.schedule.hour)}:${pad(definition.schedule.minute)}`
613
+ : `daily ${pad(definition.schedule.hour)}:${pad(definition.schedule.minute)}`;
614
+ lines.push(`- ${definition.kind}: ${when} (rrule \`${definition.rrule}\`, cron \`${definition.cron}\`)`);
615
+ }
616
+ lines.push('', `## ${target === 'codex' ? 'automation.toml' : 'scheduled-tasks.json'}`, '', '```', payload.trimEnd(), '```');
617
+ lines.push('', '## Register');
618
+ if (target === 'codex') {
619
+ lines.push('- Save each per-file TOML document as ~/.codex/automations/<id>/automation.toml on your agent host, or import it through your Codex automation UI.');
620
+ }
621
+ else {
622
+ lines.push('- Register each scheduled task with your Claude Code scheduler (for example the /schedule command) using its cron and prompt.');
623
+ }
624
+ lines.push('- Routines run against your local Brainbase MCP context; keep external side effects confirmation-gated.');
625
+ if (outPath) {
626
+ lines.push(`- Wrote routine definition to ${outPath}`);
627
+ }
628
+ return `${lines.join('\n')}\n`;
629
+ }
630
+ function pad(value) {
631
+ return value.toString().padStart(2, '0');
632
+ }
633
+ async function onboardSkills(parsed, io) {
634
+ const format = parseOnboardingFormat(first(parsed, 'format'));
635
+ const target = parseSkillTarget(first(parsed, 'target'));
636
+ const ids = parseSkillIds(first(parsed, 'skills'));
637
+ const bundle = buildSkillBundle(target, ids);
638
+ assertPublicSafeSkillBundle(bundle);
639
+ const outDir = first(parsed, 'out');
640
+ if (outDir) {
641
+ await writeSkillBundle(outDir, bundle);
642
+ }
643
+ if (format === 'json') {
644
+ write(io, `${JSON.stringify({ ...bundle, outDir: outDir ?? null }, null, 2)}\n`);
645
+ }
646
+ else {
647
+ write(io, renderSkillsMarkdown(bundle, outDir));
648
+ }
649
+ return 0;
650
+ }
651
+ async function writeSkillBundle(outDir, bundle) {
652
+ for (const skill of bundle.skills) {
653
+ const outputPath = join(outDir, skill.relativePath);
654
+ await mkdir(dirname(outputPath), { recursive: true });
655
+ try {
656
+ await writeFile(outputPath, skill.content, { flag: 'wx' });
657
+ }
658
+ catch (error) {
659
+ if (error instanceof Error && 'code' in error && error.code === 'EEXIST') {
660
+ throw new Error(`Refusing to overwrite existing skill file ${outputPath}. Choose a new --out directory or remove the old file first.`);
661
+ }
662
+ throw error;
663
+ }
664
+ }
665
+ }
666
+ function isInstallTarget(value) {
667
+ return value === 'codex' || value === 'claude' || value === 'codecode';
668
+ }
669
+ function parseGuidedTarget(value) {
670
+ if (!value || value === 'codex') {
671
+ return 'codex';
672
+ }
673
+ if (value === 'claude' || value === 'codecode') {
674
+ return value;
675
+ }
676
+ throw new Error('onboard:start requires --target codex|claude|codecode');
677
+ }
678
+ function buildInstallPayload(target, dataDir) {
679
+ const server = {
680
+ command: process.execPath,
681
+ args: [fileURLToPath(new URL('./index.js', import.meta.url))],
682
+ env: {
683
+ BRAINBASE_PERSONAL_OS_DIR: dataDir
684
+ }
685
+ };
686
+ if (target === 'codex') {
687
+ return [
688
+ '[mcp_servers.brainbase]',
689
+ `command = ${tomlString(server.command)}`,
690
+ `args = [${server.args.map(tomlString).join(', ')}]`,
691
+ '',
692
+ '[mcp_servers.brainbase.env]',
693
+ `BRAINBASE_PERSONAL_OS_DIR = ${tomlString(dataDir)}`,
694
+ ''
695
+ ].join('\n');
696
+ }
697
+ return `${JSON.stringify({ mcpServers: { brainbase: server } }, null, 2)}\n`;
698
+ }
699
+ function tomlString(value) {
700
+ return JSON.stringify(value);
701
+ }
702
+ async function writeConfigSnippet(outputPath, payload) {
703
+ try {
704
+ await writeFile(outputPath, payload, { flag: 'wx' });
705
+ }
706
+ catch (error) {
707
+ if (error instanceof Error && 'code' in error && error.code === 'EEXIST') {
708
+ throw new Error(`Refusing to overwrite existing MCP config snippet ${outputPath}. Choose a new --output path or remove the old snippet first.`);
709
+ }
710
+ throw error;
711
+ }
712
+ }
713
+ async function doctor(parsed, io) {
714
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
715
+ const os = await loadPersonalOs(dataDir);
716
+ write(io, `${JSON.stringify(onboardingStatus(os), null, 2)}\n`);
717
+ return 0;
718
+ }
719
+ function parseArgs(argv) {
720
+ const [command, ...rest] = argv;
721
+ const values = new Map();
722
+ const flags = new Set();
723
+ for (let index = 0; index < rest.length; index += 1) {
724
+ const token = rest[index];
725
+ if (!token?.startsWith('--')) {
726
+ continue;
727
+ }
728
+ const key = token.slice(2);
729
+ const next = rest[index + 1];
730
+ if (!next || next.startsWith('--')) {
731
+ flags.add(key);
732
+ continue;
733
+ }
734
+ values.set(key, [...(values.get(key) ?? []), next]);
735
+ index += 1;
736
+ }
737
+ return {
738
+ command,
739
+ values,
740
+ flags
741
+ };
742
+ }
743
+ function first(parsed, key) {
744
+ return parsed.values.get(key)?.[0];
745
+ }
746
+ async function commandExists(command) {
747
+ const candidates = isAbsolute(command)
748
+ ? [command]
749
+ : (process.env.PATH ?? '').split(delimiter).filter(Boolean).map((dir) => join(dir, command));
750
+ for (const candidate of candidates) {
751
+ try {
752
+ await access(candidate, constants.X_OK);
753
+ return true;
754
+ }
755
+ catch {
756
+ // Continue checking PATH candidates.
757
+ }
758
+ }
759
+ return false;
760
+ }
761
+ function upsertGraphEntity(entities, entity) {
762
+ const index = entities.findIndex((candidate) => candidate.id === entity.id);
763
+ if (index >= 0) {
764
+ entities[index] = entity;
765
+ }
766
+ else {
767
+ entities.push(entity);
768
+ }
769
+ }
770
+ function hash(value) {
771
+ let hashValue = 0;
772
+ for (const char of value) {
773
+ hashValue = ((hashValue << 5) - hashValue + char.charCodeAt(0)) | 0;
774
+ }
775
+ return Math.abs(hashValue).toString(36);
776
+ }
777
+ function write(io, text) {
778
+ io.stdout?.write(text);
779
+ }
780
+ function writeError(io, text) {
781
+ io.stderr?.write(text);
782
+ }
783
+ async function ontologyAudit(parsed, io) {
784
+ const dataDir = resolveDataDir(first(parsed, 'dir'));
785
+ const ontologyVersion = resolveOntologyVersion(first(parsed, 'ontology-version'));
786
+ const result = await auditPersonalOsDirectory(dataDir, { ontologyVersion });
787
+ write(io, `${JSON.stringify(result, null, 2)}\n`);
788
+ if (result.status === 'unverified') {
789
+ return 1;
790
+ }
791
+ return result.violations.some((violation) => violation.severity === 'error') ? 1 : 0;
792
+ }
793
+ function proposedPersonalOs(os, proposed) {
794
+ return { ...os, ...proposed };
795
+ }
796
+ function usage() {
797
+ return `Usage:
798
+ brainbase-mcp
799
+ brainbase mcp
800
+ brainbase onboard:init [--dir path]
801
+ brainbase onboard:seed [--dir path] [--name value] [--value value] [--project value] [--decision-principle value] [--relationship "person|role|context"]
802
+ brainbase onboard:install --target codex|claude|codecode [--dir path] [--dry-run] [--output path]
803
+ brainbase onboard:agent [--format markdown|json]
804
+ brainbase onboard:start [--target codex|claude|codecode] [--dir path] [--name value] [--project value] [--goal value] [--status value] [--role value] [--email value] [--calendar value] [--drive value] [--drive-folder id] [--local-folder path] [--tasks value] [--format markdown|json]
805
+ brainbase onboard:demo [--dir path] [--scenario value] [--format markdown|json]
806
+ brainbase onboard:recommend [--email value] [--calendar value] [--drive value] [--tasks value] [--format markdown|json]
807
+ brainbase onboard:diagnose-sources [--dir path] [--email value] [--calendar value] [--drive value] [--drive-folder id] [--tasks value] [--assume-gog] [--gog-command command] [--format markdown|json]
808
+ brainbase onboard:plan [--profile google-workspace-local] [--host value] [--email value] [--secondary-email value] [--calendar value] [--drive value] [--drive-folder id] [--local-folder path] [--tasks value] [--inactive-task-tool value] [--format markdown|json]
809
+ brainbase onboard:projects --name value [--goal value] [--status value] [--role value] [--stakeholder "person|role|context"] [--source "area|label|ref"] [--task-source value] [--decision-principle value] [--write] [--dir path] [--format markdown|json]
810
+ brainbase onboard:candidates [--dir path] [--name value] [--value value] [--project value] [--decision-principle value] [--relationship "person|role|context"] [--write] [--format markdown|json]
811
+ brainbase onboard:import --source gmail|calendar|drive|local --from path|- [--dir path] [--out path] [--include-descriptions] [--format markdown|json]
812
+ brainbase onboard:extract [--dir path] [--self-email value] [--top-relationships n] [--write] [--format markdown|json]
813
+ brainbase onboard:apply --from path [--select id] [--all] [--write] [--dir path] [--format markdown|json]
814
+ brainbase onboard:routines --target codex|claude [--routines ohayo,oyasumi,retro] [--ohayo-hour n] [--oyasumi-hour n] [--retro-dow MON-SUN] [--retro-hour n] [--cwd path] [--out path] [--format markdown|json]
815
+ brainbase onboard:skills --target codex|claude|portable [--skills id,id] [--out dir] [--format markdown|json]
816
+ brainbase ontology:show
817
+ brainbase ontology:audit [--dir path] [--ontology-version 0.0.0|1.0.0]
818
+ brainbase doctor [--dir path]
819
+ `;
820
+ }
821
+ if (import.meta.url === `file://${process.argv[1]}`) {
822
+ const code = await runCli();
823
+ process.exit(code);
824
+ }