@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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { DecisionRecord, GraphEntity, PersonalKgEntry, RelationshipRecord } from './types.js';
|
|
2
|
+
export type SourceProvider = 'gmail' | 'calendar' | 'drive' | 'local';
|
|
3
|
+
export interface GmailSourceRecord {
|
|
4
|
+
kind: 'gmail';
|
|
5
|
+
id: string;
|
|
6
|
+
threadId?: string;
|
|
7
|
+
from: Participant;
|
|
8
|
+
to: Participant[];
|
|
9
|
+
cc: Participant[];
|
|
10
|
+
subject: string;
|
|
11
|
+
date?: string;
|
|
12
|
+
labels: string[];
|
|
13
|
+
snippet: string;
|
|
14
|
+
}
|
|
15
|
+
export interface CalendarSourceRecord {
|
|
16
|
+
kind: 'calendar';
|
|
17
|
+
id: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
start?: string;
|
|
20
|
+
end?: string;
|
|
21
|
+
organizer?: Participant;
|
|
22
|
+
attendees: Participant[];
|
|
23
|
+
location?: string;
|
|
24
|
+
status?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DriveSourceRecord {
|
|
28
|
+
kind: 'drive';
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
mimeType?: string;
|
|
32
|
+
modifiedTime?: string;
|
|
33
|
+
owners: Participant[];
|
|
34
|
+
parents: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface LocalSourceRecord {
|
|
37
|
+
kind: 'local';
|
|
38
|
+
path: string;
|
|
39
|
+
title: string;
|
|
40
|
+
modified?: string;
|
|
41
|
+
taskLines: string[];
|
|
42
|
+
}
|
|
43
|
+
export type SourceRecord = GmailSourceRecord | CalendarSourceRecord | DriveSourceRecord | LocalSourceRecord;
|
|
44
|
+
export interface Participant {
|
|
45
|
+
email?: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ImportResult {
|
|
49
|
+
provider: SourceProvider;
|
|
50
|
+
sourcePath: string;
|
|
51
|
+
count: number;
|
|
52
|
+
records: SourceRecord[];
|
|
53
|
+
}
|
|
54
|
+
export declare function sourcePathFor(provider: SourceProvider): string;
|
|
55
|
+
export declare function parseProvider(value: string | undefined): SourceProvider;
|
|
56
|
+
export interface NormalizeOptions {
|
|
57
|
+
includeDescriptions?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare function normalizeSource(provider: SourceProvider, raw: unknown, options?: NormalizeOptions): SourceRecord[];
|
|
60
|
+
export declare function renderSourceJsonl(records: SourceRecord[]): string;
|
|
61
|
+
export type CandidateKind = 'person' | 'org' | 'project' | 'relationship' | 'next_action';
|
|
62
|
+
export interface ExtractedCandidate {
|
|
63
|
+
id: string;
|
|
64
|
+
kind: CandidateKind;
|
|
65
|
+
payload: Record<string, string | string[] | undefined>;
|
|
66
|
+
provenance: {
|
|
67
|
+
count: number;
|
|
68
|
+
sources: string[];
|
|
69
|
+
};
|
|
70
|
+
source: 'source-extraction';
|
|
71
|
+
promoted: false;
|
|
72
|
+
}
|
|
73
|
+
export interface ExtractedCandidateSet {
|
|
74
|
+
goal: string;
|
|
75
|
+
canonicalWrites: false;
|
|
76
|
+
candidatePath: string;
|
|
77
|
+
counts: Record<CandidateKind, number>;
|
|
78
|
+
candidates: ExtractedCandidate[];
|
|
79
|
+
safetyRules: string[];
|
|
80
|
+
nextCommands: string[];
|
|
81
|
+
}
|
|
82
|
+
export interface ExtractOptions {
|
|
83
|
+
selfEmails?: string[];
|
|
84
|
+
topRelationships?: number;
|
|
85
|
+
}
|
|
86
|
+
export declare function extractCandidates(records: SourceRecord[], options?: ExtractOptions): ExtractedCandidate[];
|
|
87
|
+
export declare function buildExtractedCandidateSet(candidates: ExtractedCandidate[], dataDir: string): ExtractedCandidateSet;
|
|
88
|
+
export interface ApplyInput {
|
|
89
|
+
graphEntities: GraphEntity[];
|
|
90
|
+
relationships: RelationshipRecord[];
|
|
91
|
+
personalKg: PersonalKgEntry[];
|
|
92
|
+
decisions: DecisionRecord[];
|
|
93
|
+
ownerName?: string;
|
|
94
|
+
}
|
|
95
|
+
export interface ApplyResult {
|
|
96
|
+
graphEntities: GraphEntity[];
|
|
97
|
+
relationships: RelationshipRecord[];
|
|
98
|
+
personalKgAdditions: PersonalKgEntry[];
|
|
99
|
+
decisionAdditions: DecisionRecord[];
|
|
100
|
+
ownerName?: string;
|
|
101
|
+
applied: {
|
|
102
|
+
id: string;
|
|
103
|
+
kind: string;
|
|
104
|
+
summary: string;
|
|
105
|
+
}[];
|
|
106
|
+
skipped: {
|
|
107
|
+
id: string;
|
|
108
|
+
reason: string;
|
|
109
|
+
}[];
|
|
110
|
+
}
|
|
111
|
+
export interface ApplyCandidate {
|
|
112
|
+
id: string;
|
|
113
|
+
kind: string;
|
|
114
|
+
payload: Record<string, unknown>;
|
|
115
|
+
}
|
|
116
|
+
export declare function loadApplyCandidates(raw: unknown): ApplyCandidate[];
|
|
117
|
+
export declare function planApply(candidates: ApplyCandidate[], selection: {
|
|
118
|
+
ids: Set<string>;
|
|
119
|
+
all: boolean;
|
|
120
|
+
}, base: ApplyInput, now: string): ApplyResult;
|
|
121
|
+
export declare function stableHash(value: string): string;
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
const PROVIDER_SOURCE_PATH = {
|
|
2
|
+
gmail: 'sources/gmail/threads.jsonl',
|
|
3
|
+
calendar: 'sources/calendar/events.jsonl',
|
|
4
|
+
drive: 'sources/drive/files.jsonl',
|
|
5
|
+
local: 'sources/drive/local-files.jsonl'
|
|
6
|
+
};
|
|
7
|
+
const CONSUMER_EMAIL_DOMAINS = new Set([
|
|
8
|
+
'gmail.com',
|
|
9
|
+
'googlemail.com',
|
|
10
|
+
'outlook.com',
|
|
11
|
+
'hotmail.com',
|
|
12
|
+
'live.com',
|
|
13
|
+
'yahoo.com',
|
|
14
|
+
'yahoo.co.jp',
|
|
15
|
+
'icloud.com',
|
|
16
|
+
'me.com',
|
|
17
|
+
'proton.me',
|
|
18
|
+
'protonmail.com',
|
|
19
|
+
'docomo.ne.jp',
|
|
20
|
+
'ezweb.ne.jp',
|
|
21
|
+
'au.com',
|
|
22
|
+
'softbank.ne.jp'
|
|
23
|
+
]);
|
|
24
|
+
const TASK_KEYWORDS = ['todo', 'follow up', 'followup', 'send', 'review', 'draft', 'prepare', 'reply', 'schedule', '対応', '確認', '送付', '返信', '準備', 'タスク', '提出', 'レビュー'];
|
|
25
|
+
export function sourcePathFor(provider) {
|
|
26
|
+
return PROVIDER_SOURCE_PATH[provider];
|
|
27
|
+
}
|
|
28
|
+
export function parseProvider(value) {
|
|
29
|
+
if (value === 'gmail' || value === 'calendar' || value === 'drive' || value === 'local') {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
throw new Error('onboard:import requires --source gmail|calendar|drive|local');
|
|
33
|
+
}
|
|
34
|
+
export function normalizeSource(provider, raw, options = {}) {
|
|
35
|
+
const items = unwrapArray(provider, raw);
|
|
36
|
+
switch (provider) {
|
|
37
|
+
case 'gmail':
|
|
38
|
+
return items.map((item, index) => normalizeGmail(item, index));
|
|
39
|
+
case 'calendar':
|
|
40
|
+
return items.map((item, index) => normalizeCalendar(item, index, options));
|
|
41
|
+
case 'drive':
|
|
42
|
+
return items.map((item, index) => normalizeDrive(item, index));
|
|
43
|
+
case 'local':
|
|
44
|
+
return items.map((item, index) => normalizeLocal(item, index));
|
|
45
|
+
default:
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function renderSourceJsonl(records) {
|
|
50
|
+
if (records.length === 0) {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
return `${records.map((record) => JSON.stringify(record)).join('\n')}\n`;
|
|
54
|
+
}
|
|
55
|
+
function unwrapArray(provider, raw) {
|
|
56
|
+
if (Array.isArray(raw)) {
|
|
57
|
+
return raw.filter(isRecord);
|
|
58
|
+
}
|
|
59
|
+
if (isRecord(raw)) {
|
|
60
|
+
const containerKeys = ['messages', 'threads', 'events', 'items', 'files', 'records', provider, `${provider}s`];
|
|
61
|
+
for (const key of containerKeys) {
|
|
62
|
+
const value = raw[key];
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
return value.filter(isRecord);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Could not find an array of ${provider} records in the imported JSON.`);
|
|
69
|
+
}
|
|
70
|
+
function normalizeGmail(item, index) {
|
|
71
|
+
return {
|
|
72
|
+
kind: 'gmail',
|
|
73
|
+
id: readString(item, ['id', 'messageId', 'threadId']) || `gmail-${index}`,
|
|
74
|
+
threadId: readString(item, ['threadId', 'thread_id']) || undefined,
|
|
75
|
+
from: parseParticipant(firstDefined(item, ['from', 'sender'])),
|
|
76
|
+
to: parseParticipants(firstDefined(item, ['to', 'recipients'])),
|
|
77
|
+
cc: parseParticipants(firstDefined(item, ['cc'])),
|
|
78
|
+
subject: readString(item, ['subject', 'title']),
|
|
79
|
+
date: readString(item, ['date', 'internalDate', 'receivedAt']) || undefined,
|
|
80
|
+
labels: readStringArray(item, ['labels', 'labelIds']),
|
|
81
|
+
snippet: truncate(readString(item, ['snippet', 'preview']), 280)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function normalizeCalendar(item, index, options) {
|
|
85
|
+
const organizer = firstDefined(item, ['organizer', 'creator']);
|
|
86
|
+
const record = {
|
|
87
|
+
kind: 'calendar',
|
|
88
|
+
id: readString(item, ['id', 'iCalUID', 'eventId']) || `calendar-${index}`,
|
|
89
|
+
summary: readString(item, ['summary', 'title', 'subject']),
|
|
90
|
+
start: readDateField(firstDefined(item, ['start', 'startTime'])) || undefined,
|
|
91
|
+
end: readDateField(firstDefined(item, ['end', 'endTime'])) || undefined,
|
|
92
|
+
organizer: organizer ? parseParticipant(organizer) : undefined,
|
|
93
|
+
attendees: parseParticipants(firstDefined(item, ['attendees', 'participants'])),
|
|
94
|
+
location: readString(item, ['location']) || undefined,
|
|
95
|
+
status: readString(item, ['status']) || undefined
|
|
96
|
+
};
|
|
97
|
+
if (options.includeDescriptions) {
|
|
98
|
+
const description = truncate(readString(item, ['description', 'notes']), 280);
|
|
99
|
+
if (description) {
|
|
100
|
+
record.description = description;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return record;
|
|
104
|
+
}
|
|
105
|
+
function normalizeDrive(item, index) {
|
|
106
|
+
return {
|
|
107
|
+
kind: 'drive',
|
|
108
|
+
id: readString(item, ['id', 'fileId']) || `drive-${index}`,
|
|
109
|
+
name: readString(item, ['name', 'title']),
|
|
110
|
+
mimeType: readString(item, ['mimeType', 'mime_type']) || undefined,
|
|
111
|
+
modifiedTime: readString(item, ['modifiedTime', 'modified', 'updatedAt']) || undefined,
|
|
112
|
+
owners: parseParticipants(firstDefined(item, ['owners'])),
|
|
113
|
+
parents: readStringArray(item, ['parents', 'parentIds'])
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function normalizeLocal(item, index) {
|
|
117
|
+
const path = readString(item, ['path', 'file', 'name']) || `local-${index}`;
|
|
118
|
+
const explicitTasks = readStringArray(item, ['taskLines', 'tasks']);
|
|
119
|
+
const excerptTasks = extractTaskLines(readString(item, ['excerpt', 'preview', 'content']));
|
|
120
|
+
return {
|
|
121
|
+
kind: 'local',
|
|
122
|
+
path,
|
|
123
|
+
title: readString(item, ['title']) || basename(path),
|
|
124
|
+
modified: readString(item, ['modified', 'modifiedTime', 'updatedAt']) || undefined,
|
|
125
|
+
taskLines: dedupe([...explicitTasks, ...excerptTasks]).slice(0, 20)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function extractCandidates(records, options = {}) {
|
|
129
|
+
const selfEmails = new Set((options.selfEmails ?? []).map((email) => email.toLowerCase()));
|
|
130
|
+
const topRelationships = options.topRelationships ?? 8;
|
|
131
|
+
const people = new Map();
|
|
132
|
+
const orgs = new Map();
|
|
133
|
+
const projects = new Map();
|
|
134
|
+
const nextActions = new Map();
|
|
135
|
+
const notePerson = (participant, source) => {
|
|
136
|
+
const email = participant.email?.trim().toLowerCase();
|
|
137
|
+
if (!email || selfEmails.has(email)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const existing = people.get(email) ?? { email, name: undefined, count: 0, sources: new Set() };
|
|
141
|
+
existing.count += 1;
|
|
142
|
+
existing.sources.add(source);
|
|
143
|
+
if (!existing.name && participant.name) {
|
|
144
|
+
existing.name = participant.name;
|
|
145
|
+
}
|
|
146
|
+
people.set(email, existing);
|
|
147
|
+
const domain = email.split('@')[1];
|
|
148
|
+
if (domain && !CONSUMER_EMAIL_DOMAINS.has(domain)) {
|
|
149
|
+
const org = orgs.get(domain) ?? { domain, count: 0, sources: new Set() };
|
|
150
|
+
org.count += 1;
|
|
151
|
+
org.sources.add(source);
|
|
152
|
+
orgs.set(domain, org);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const noteProject = (rawName, source) => {
|
|
156
|
+
const name = rawName.trim();
|
|
157
|
+
if (!name) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const key = normalizeTitle(name);
|
|
161
|
+
if (!key) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const existing = projects.get(key) ?? { name, count: 0, sources: new Set() };
|
|
165
|
+
existing.count += 1;
|
|
166
|
+
existing.sources.add(source);
|
|
167
|
+
projects.set(key, existing);
|
|
168
|
+
};
|
|
169
|
+
const noteAction = (text, source) => {
|
|
170
|
+
const cleaned = text.trim();
|
|
171
|
+
if (!cleaned) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const key = normalizeTitle(cleaned);
|
|
175
|
+
const existing = nextActions.get(key) ?? { text: cleaned, sources: new Set() };
|
|
176
|
+
existing.sources.add(source);
|
|
177
|
+
nextActions.set(key, existing);
|
|
178
|
+
};
|
|
179
|
+
for (const record of records) {
|
|
180
|
+
if (record.kind === 'gmail') {
|
|
181
|
+
notePerson(record.from, 'gmail');
|
|
182
|
+
record.to.forEach((participant) => notePerson(participant, 'gmail'));
|
|
183
|
+
record.cc.forEach((participant) => notePerson(participant, 'gmail'));
|
|
184
|
+
if (looksLikeTask(record.subject)) {
|
|
185
|
+
noteAction(record.subject, 'gmail');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (record.kind === 'calendar') {
|
|
189
|
+
if (record.organizer) {
|
|
190
|
+
notePerson(record.organizer, 'calendar');
|
|
191
|
+
}
|
|
192
|
+
record.attendees.forEach((participant) => notePerson(participant, 'calendar'));
|
|
193
|
+
noteProject(record.summary, 'calendar');
|
|
194
|
+
if (looksLikeTask(record.summary)) {
|
|
195
|
+
noteAction(record.summary, 'calendar');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (record.kind === 'drive') {
|
|
199
|
+
record.owners.forEach((participant) => notePerson(participant, 'drive'));
|
|
200
|
+
noteProject(record.name, 'drive');
|
|
201
|
+
}
|
|
202
|
+
else if (record.kind === 'local') {
|
|
203
|
+
record.taskLines.forEach((line) => noteAction(line, 'local'));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const candidates = [];
|
|
207
|
+
const rankedPeople = [...people.values()].sort(byCountThenKey((person) => person.email));
|
|
208
|
+
for (const person of rankedPeople) {
|
|
209
|
+
const displayName = person.name || person.email;
|
|
210
|
+
candidates.push({
|
|
211
|
+
id: `person-${stableHash(person.email)}`,
|
|
212
|
+
kind: 'person',
|
|
213
|
+
payload: { name: displayName, email: person.email },
|
|
214
|
+
provenance: { count: person.count, sources: sortedSources(person.sources) },
|
|
215
|
+
source: 'source-extraction',
|
|
216
|
+
promoted: false
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
for (const org of [...orgs.values()].sort(byCountThenKey((entry) => entry.domain))) {
|
|
220
|
+
candidates.push({
|
|
221
|
+
id: `org-${stableHash(org.domain)}`,
|
|
222
|
+
kind: 'org',
|
|
223
|
+
payload: { name: org.domain, domain: org.domain },
|
|
224
|
+
provenance: { count: org.count, sources: sortedSources(org.sources) },
|
|
225
|
+
source: 'source-extraction',
|
|
226
|
+
promoted: false
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
for (const project of [...projects.values()].filter((entry) => entry.count >= 2).sort(byCountThenKey((entry) => entry.name))) {
|
|
230
|
+
candidates.push({
|
|
231
|
+
id: `project-${stableHash(normalizeTitle(project.name))}`,
|
|
232
|
+
kind: 'project',
|
|
233
|
+
payload: { name: project.name },
|
|
234
|
+
provenance: { count: project.count, sources: sortedSources(project.sources) },
|
|
235
|
+
source: 'source-extraction',
|
|
236
|
+
promoted: false
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
for (const person of rankedPeople.filter((entry) => entry.count >= 2).slice(0, topRelationships)) {
|
|
240
|
+
const displayName = person.name || person.email;
|
|
241
|
+
candidates.push({
|
|
242
|
+
id: `relationship-${stableHash(person.email)}`,
|
|
243
|
+
kind: 'relationship',
|
|
244
|
+
payload: {
|
|
245
|
+
person: displayName,
|
|
246
|
+
email: person.email,
|
|
247
|
+
context: `Frequent contact (${person.count} interactions across ${sortedSources(person.sources).join(', ')}).`
|
|
248
|
+
},
|
|
249
|
+
provenance: { count: person.count, sources: sortedSources(person.sources) },
|
|
250
|
+
source: 'source-extraction',
|
|
251
|
+
promoted: false
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
for (const action of [...nextActions.values()].sort((a, b) => a.text.localeCompare(b.text))) {
|
|
255
|
+
candidates.push({
|
|
256
|
+
id: `next_action-${stableHash(normalizeTitle(action.text))}`,
|
|
257
|
+
kind: 'next_action',
|
|
258
|
+
payload: { text: action.text },
|
|
259
|
+
provenance: { count: 1, sources: sortedSources(action.sources) },
|
|
260
|
+
source: 'source-extraction',
|
|
261
|
+
promoted: false
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return candidates;
|
|
265
|
+
}
|
|
266
|
+
export function buildExtractedCandidateSet(candidates, dataDir) {
|
|
267
|
+
const counts = {
|
|
268
|
+
person: 0,
|
|
269
|
+
org: 0,
|
|
270
|
+
project: 0,
|
|
271
|
+
relationship: 0,
|
|
272
|
+
next_action: 0
|
|
273
|
+
};
|
|
274
|
+
for (const candidate of candidates) {
|
|
275
|
+
counts[candidate.kind] += 1;
|
|
276
|
+
}
|
|
277
|
+
const fingerprint = stableHash(candidates.map((candidate) => candidate.id).join('|') || 'empty');
|
|
278
|
+
return {
|
|
279
|
+
goal: 'Review source-extracted candidates before promoting them into canonical Brainbase SSOT.',
|
|
280
|
+
canonicalWrites: false,
|
|
281
|
+
candidatePath: `${dataDir}/candidates/extracted-${fingerprint}.json`,
|
|
282
|
+
counts,
|
|
283
|
+
candidates,
|
|
284
|
+
safetyRules: [
|
|
285
|
+
'Extracted candidates are derived from secondary source material and are not canonical memory.',
|
|
286
|
+
'Promote candidates only with onboard:apply --write after explicit selection.',
|
|
287
|
+
'Review person, org, project, relationship, and next-action candidates with the user before promotion.'
|
|
288
|
+
],
|
|
289
|
+
nextCommands: [
|
|
290
|
+
'Review the extracted candidate file with the user.',
|
|
291
|
+
'brainbase onboard:apply --from <candidate-file> --select <id> --write',
|
|
292
|
+
'brainbase doctor'
|
|
293
|
+
]
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
export function loadApplyCandidates(raw) {
|
|
297
|
+
const container = isRecord(raw) && Array.isArray(raw.candidates) ? raw.candidates : raw;
|
|
298
|
+
if (!Array.isArray(container)) {
|
|
299
|
+
throw new Error('Candidate file does not contain a candidates array.');
|
|
300
|
+
}
|
|
301
|
+
return container.filter(isRecord).map((item, index) => ({
|
|
302
|
+
id: readString(item, ['id']) || `candidate-${index}`,
|
|
303
|
+
kind: readString(item, ['kind']) || 'unknown',
|
|
304
|
+
payload: isRecord(item.payload) ? item.payload : {}
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
export function planApply(candidates, selection, base, now) {
|
|
308
|
+
const graphEntities = [...base.graphEntities];
|
|
309
|
+
const relationships = [...base.relationships];
|
|
310
|
+
const personalKgAdditions = [];
|
|
311
|
+
const decisionAdditions = [];
|
|
312
|
+
const applied = [];
|
|
313
|
+
const skipped = [];
|
|
314
|
+
let ownerName = base.ownerName;
|
|
315
|
+
for (const candidate of candidates) {
|
|
316
|
+
if (!selection.all && !selection.ids.has(candidate.id)) {
|
|
317
|
+
skipped.push({ id: candidate.id, reason: 'not selected' });
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const payload = candidate.payload;
|
|
321
|
+
switch (candidate.kind) {
|
|
322
|
+
case 'self': {
|
|
323
|
+
const name = payloadString(payload, 'name');
|
|
324
|
+
if (!name) {
|
|
325
|
+
skipped.push({ id: candidate.id, reason: 'self candidate missing name' });
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
ownerName = name;
|
|
329
|
+
upsertEntity(graphEntities, { id: 'self', type: 'person', name, summary: 'Owner of this local Brainbase Personal OS.', tags: ['self'] });
|
|
330
|
+
personalKgAdditions.push({ id: `self-${stableHash(name)}`, type: 'self', text: `I am ${name}.`, tags: ['self'], source: candidate.id, updatedAt: now });
|
|
331
|
+
applied.push({ id: candidate.id, kind: candidate.kind, summary: name });
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case 'person':
|
|
335
|
+
case 'relationship': {
|
|
336
|
+
const person = payloadString(payload, 'person') || payloadString(payload, 'name');
|
|
337
|
+
if (!person) {
|
|
338
|
+
skipped.push({ id: candidate.id, reason: 'person candidate missing name' });
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
const context = payloadString(payload, 'context') || payloadString(payload, 'email') || 'Imported from onboarding sources.';
|
|
342
|
+
const role = payloadString(payload, 'role');
|
|
343
|
+
upsertEntity(graphEntities, { id: `person-${stableHash(person)}`, type: 'person', name: person, summary: context, tags: ['relationship'] });
|
|
344
|
+
if (candidate.kind === 'relationship' && !relationships.some((relationship) => relationship.person === person && relationship.context === context)) {
|
|
345
|
+
relationships.push({ id: `relationship-${stableHash(`${person}|${context}`)}`, person, role: role || undefined, context, tags: ['relationship'], updatedAt: now });
|
|
346
|
+
}
|
|
347
|
+
applied.push({ id: candidate.id, kind: candidate.kind, summary: person });
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case 'org': {
|
|
351
|
+
const name = payloadString(payload, 'name') || payloadString(payload, 'domain');
|
|
352
|
+
if (!name) {
|
|
353
|
+
skipped.push({ id: candidate.id, reason: 'org candidate missing name' });
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
upsertEntity(graphEntities, { id: `org-${stableHash(name)}`, type: 'org', name, summary: 'Imported from onboarding sources.', tags: ['org'] });
|
|
357
|
+
applied.push({ id: candidate.id, kind: candidate.kind, summary: name });
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case 'project': {
|
|
361
|
+
const name = payloadString(payload, 'name');
|
|
362
|
+
if (!name) {
|
|
363
|
+
skipped.push({ id: candidate.id, reason: 'project candidate missing name' });
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
upsertEntity(graphEntities, { id: `project-${stableHash(name)}`, type: 'project', name, summary: 'Imported from onboarding sources.', tags: ['work'] });
|
|
367
|
+
personalKgAdditions.push({ id: `work-${stableHash(name)}`, type: 'work', text: name, tags: ['work'], source: candidate.id, updatedAt: now });
|
|
368
|
+
applied.push({ id: candidate.id, kind: candidate.kind, summary: name });
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
case 'value': {
|
|
372
|
+
const text = payloadString(payload, 'text');
|
|
373
|
+
if (!text) {
|
|
374
|
+
skipped.push({ id: candidate.id, reason: 'value candidate missing text' });
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
personalKgAdditions.push({ id: `value-${stableHash(text)}`, type: 'value', text, tags: ['onboarding'], source: candidate.id, updatedAt: now });
|
|
378
|
+
applied.push({ id: candidate.id, kind: candidate.kind, summary: text });
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case 'next_action': {
|
|
382
|
+
const text = payloadString(payload, 'text');
|
|
383
|
+
if (!text) {
|
|
384
|
+
skipped.push({ id: candidate.id, reason: 'next_action candidate missing text' });
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
personalKgAdditions.push({ id: `next-action-${stableHash(text)}`, type: 'work', text, tags: ['next-action'], source: candidate.id, updatedAt: now });
|
|
388
|
+
applied.push({ id: candidate.id, kind: candidate.kind, summary: text });
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case 'decision': {
|
|
392
|
+
const text = payloadString(payload, 'decision') || payloadString(payload, 'text');
|
|
393
|
+
if (!text) {
|
|
394
|
+
skipped.push({ id: candidate.id, reason: 'decision candidate missing decision text' });
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
decisionAdditions.push({ id: `decision-${stableHash(text)}`, title: 'Promoted decision principle', decision: text, tags: ['principle'], updatedAt: now });
|
|
398
|
+
applied.push({ id: candidate.id, kind: candidate.kind, summary: text });
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
default:
|
|
402
|
+
skipped.push({ id: candidate.id, reason: `unsupported candidate kind: ${candidate.kind}` });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return { graphEntities, relationships, personalKgAdditions, decisionAdditions, ownerName, applied, skipped };
|
|
406
|
+
}
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
// Helpers
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
function byCountThenKey(key) {
|
|
411
|
+
return (a, b) => b.count - a.count || key(a).localeCompare(key(b));
|
|
412
|
+
}
|
|
413
|
+
function sortedSources(sources) {
|
|
414
|
+
return [...sources].sort();
|
|
415
|
+
}
|
|
416
|
+
function looksLikeTask(text) {
|
|
417
|
+
const haystack = text.toLowerCase();
|
|
418
|
+
return TASK_KEYWORDS.some((keyword) => haystack.includes(keyword));
|
|
419
|
+
}
|
|
420
|
+
function extractTaskLines(content) {
|
|
421
|
+
if (!content) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
return content
|
|
425
|
+
.split('\n')
|
|
426
|
+
.map((line) => line.trim())
|
|
427
|
+
.filter((line) => /^(- \[ \]|todo[::]|□|☐|\* \[ \])/i.test(line) || looksLikeTask(line))
|
|
428
|
+
.map((line) => line.replace(/^(- \[ \]|\* \[ \]|todo[::]|□|☐)\s*/i, '').trim())
|
|
429
|
+
.filter(Boolean);
|
|
430
|
+
}
|
|
431
|
+
function normalizeTitle(value) {
|
|
432
|
+
return value
|
|
433
|
+
.toLowerCase()
|
|
434
|
+
.replace(/\b(re|fwd|fw)\s*[::]/gi, '')
|
|
435
|
+
.replace(/\s+/g, ' ')
|
|
436
|
+
.trim();
|
|
437
|
+
}
|
|
438
|
+
function parseParticipants(value) {
|
|
439
|
+
if (Array.isArray(value)) {
|
|
440
|
+
return value.map((entry) => parseParticipant(entry)).filter((participant) => participant.email || participant.name);
|
|
441
|
+
}
|
|
442
|
+
if (typeof value === 'string') {
|
|
443
|
+
return value
|
|
444
|
+
.split(',')
|
|
445
|
+
.map((part) => parseParticipant(part))
|
|
446
|
+
.filter((participant) => participant.email || participant.name);
|
|
447
|
+
}
|
|
448
|
+
if (value === undefined || value === null) {
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
const single = parseParticipant(value);
|
|
452
|
+
return single.email || single.name ? [single] : [];
|
|
453
|
+
}
|
|
454
|
+
function parseParticipant(value) {
|
|
455
|
+
if (isRecord(value)) {
|
|
456
|
+
return {
|
|
457
|
+
email: normalizeEmail(readString(value, ['email', 'emailAddress', 'address'])),
|
|
458
|
+
name: readString(value, ['name', 'displayName']) || undefined
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
if (typeof value === 'string') {
|
|
462
|
+
return parseParticipantString(value);
|
|
463
|
+
}
|
|
464
|
+
return {};
|
|
465
|
+
}
|
|
466
|
+
function parseParticipantString(value) {
|
|
467
|
+
const trimmed = value.trim();
|
|
468
|
+
const angle = trimmed.match(/^(.*?)<([^>]+)>$/);
|
|
469
|
+
if (angle) {
|
|
470
|
+
return {
|
|
471
|
+
name: angle[1].replace(/["']/g, '').trim() || undefined,
|
|
472
|
+
email: normalizeEmail(angle[2])
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (trimmed.includes('@')) {
|
|
476
|
+
return { email: normalizeEmail(trimmed) };
|
|
477
|
+
}
|
|
478
|
+
return trimmed ? { name: trimmed } : {};
|
|
479
|
+
}
|
|
480
|
+
function normalizeEmail(value) {
|
|
481
|
+
const trimmed = value?.trim().toLowerCase();
|
|
482
|
+
return trimmed && trimmed.includes('@') ? trimmed : undefined;
|
|
483
|
+
}
|
|
484
|
+
function readDateField(value) {
|
|
485
|
+
if (isRecord(value)) {
|
|
486
|
+
return readString(value, ['dateTime', 'date']);
|
|
487
|
+
}
|
|
488
|
+
return typeof value === 'string' ? value : '';
|
|
489
|
+
}
|
|
490
|
+
function firstDefined(item, keys) {
|
|
491
|
+
for (const key of keys) {
|
|
492
|
+
if (item[key] !== undefined && item[key] !== null) {
|
|
493
|
+
return item[key];
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
function readString(item, keys) {
|
|
499
|
+
for (const key of keys) {
|
|
500
|
+
const value = item[key];
|
|
501
|
+
if (typeof value === 'string' && value.trim()) {
|
|
502
|
+
return value.trim();
|
|
503
|
+
}
|
|
504
|
+
if (typeof value === 'number') {
|
|
505
|
+
return String(value);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return '';
|
|
509
|
+
}
|
|
510
|
+
function readStringArray(item, keys) {
|
|
511
|
+
for (const key of keys) {
|
|
512
|
+
const value = item[key];
|
|
513
|
+
if (Array.isArray(value)) {
|
|
514
|
+
return value.filter((entry) => typeof entry === 'string' && entry.trim().length > 0).map((entry) => entry.trim());
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
function payloadString(payload, key) {
|
|
520
|
+
const value = payload[key];
|
|
521
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
522
|
+
}
|
|
523
|
+
function upsertEntity(entities, entity) {
|
|
524
|
+
const index = entities.findIndex((candidate) => candidate.id === entity.id);
|
|
525
|
+
if (index >= 0) {
|
|
526
|
+
entities[index] = { ...entities[index], ...entity };
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
entities.push(entity);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function truncate(value, max) {
|
|
533
|
+
if (value.length <= max) {
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
return `${value.slice(0, max - 1)}…`;
|
|
537
|
+
}
|
|
538
|
+
function basename(path) {
|
|
539
|
+
const parts = path.split(/[\\/]/).filter(Boolean);
|
|
540
|
+
return parts[parts.length - 1] ?? path;
|
|
541
|
+
}
|
|
542
|
+
function dedupe(values) {
|
|
543
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
544
|
+
}
|
|
545
|
+
function isRecord(value) {
|
|
546
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
547
|
+
}
|
|
548
|
+
export function stableHash(value) {
|
|
549
|
+
let hashValue = 0;
|
|
550
|
+
for (const char of value) {
|
|
551
|
+
hashValue = ((hashValue << 5) - hashValue + char.charCodeAt(0)) | 0;
|
|
552
|
+
}
|
|
553
|
+
return Math.abs(hashValue).toString(36);
|
|
554
|
+
}
|
package/dist/index.d.ts
ADDED