@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,1061 @@
|
|
|
1
|
+
import { buildOperationalizationPlan } from './operationalization.js';
|
|
2
|
+
export function buildAgentOnboardingProtocol() {
|
|
3
|
+
return {
|
|
4
|
+
goal: 'Help the user reach a first useful Brainbase answer from local canonical context before asking them to configure source collectors.',
|
|
5
|
+
interviewSections: [
|
|
6
|
+
{
|
|
7
|
+
id: 'value_target',
|
|
8
|
+
title: 'First value target',
|
|
9
|
+
questions: [
|
|
10
|
+
'What do you not want to explain repeatedly to Codex, Claude Code, or CodeCode?',
|
|
11
|
+
'Which real request would prove Brainbase is useful today?',
|
|
12
|
+
'Should the first demo focus on a work premise, key relationship, decision principle, or active project?'
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'hypothesis',
|
|
17
|
+
title: 'Memory hypothesis',
|
|
18
|
+
questions: [
|
|
19
|
+
'Summarize the smallest self, work, and relationship facts needed for that demo.',
|
|
20
|
+
'State what Brainbase should remember in plain language before writing any files.',
|
|
21
|
+
'Name the relationship or project the demo should use.'
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'approval',
|
|
26
|
+
title: 'Approval',
|
|
27
|
+
questions: [
|
|
28
|
+
'Ask the user to approve the exact facts that Brainbase will save locally.',
|
|
29
|
+
'Confirm that the facts are safe to store locally under the Personal OS directory.',
|
|
30
|
+
'Do not show candidate JSON as the first user-facing review surface.'
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'minimum_seed',
|
|
35
|
+
title: 'Minimum canonical seed',
|
|
36
|
+
questions: [
|
|
37
|
+
'Run brainbase onboard:init if the Personal OS directory does not exist.',
|
|
38
|
+
'Run brainbase onboard:seed with the approved name, value, project, relationship, or decision principle.',
|
|
39
|
+
'Keep raw sources and candidates out of the first value demo.'
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'first_value_demo',
|
|
44
|
+
title: 'First value demo',
|
|
45
|
+
questions: [
|
|
46
|
+
'Run brainbase onboard:demo with the real request the user chose.',
|
|
47
|
+
'Show the try-this prompt, sample result, and what the user did not have to explain again.',
|
|
48
|
+
'Treat the useful output, not ready=true or connector readiness, as the onboarding completion signal.',
|
|
49
|
+
'After showing the useful output, report the remaining operationalization actions instead of calling onboarding done.'
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'optional_sources',
|
|
54
|
+
title: 'Optional source setup',
|
|
55
|
+
questions: [
|
|
56
|
+
'Only after the first demo, ask which mail, calendar, drive/docs, or task tools should be diagnosed.',
|
|
57
|
+
'Use source diagnosis to fill gaps exposed by the demo, not as the starting point.',
|
|
58
|
+
'Keep imported material under sources/ and promote only reviewed facts.'
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
safetyRules: [
|
|
63
|
+
'Do not ask the user to paste OAuth tokens, passwords, API keys, or refresh tokens into chat.',
|
|
64
|
+
'Use read-only collection paths where available.',
|
|
65
|
+
'Do not treat raw source diagnosis, connector readiness, or candidate JSON as onboarding completion.',
|
|
66
|
+
'Keep mail, calendar, drive, and task data under sources/ until the user approves candidates.',
|
|
67
|
+
'Prefer metadata-first import. Body excerpts require explicit user approval.',
|
|
68
|
+
'Drive collection must be folder allowlist based.',
|
|
69
|
+
'The first user-facing value explanation should use plain language before internal Brainbase terms.'
|
|
70
|
+
],
|
|
71
|
+
nextCommands: [
|
|
72
|
+
'brainbase onboard:init',
|
|
73
|
+
'brainbase onboard:seed --name "<name>" --value "<what should not be re-explained>" --project "<current project>" --relationship "<person>|<role>|<context>"',
|
|
74
|
+
'brainbase onboard:projects --name "<project>" --goal "<goal>" --status "<status>" --role "<your role>"',
|
|
75
|
+
'brainbase onboard:demo --scenario "<real request that should now work>"',
|
|
76
|
+
'brainbase onboard:skills --target codex',
|
|
77
|
+
'brainbase onboard:routines --target codex --cwd <brainbase-checkout>',
|
|
78
|
+
'brainbase onboard:install --target codex --dry-run',
|
|
79
|
+
'brainbase doctor',
|
|
80
|
+
'brainbase onboard:diagnose-sources --email gmail --calendar google-calendar --drive google-drive --drive-folder "<folder-id>" --tasks notion'
|
|
81
|
+
],
|
|
82
|
+
completionCheck: [
|
|
83
|
+
'Personal OS directory exists.',
|
|
84
|
+
'The approved self, work, and person context are saved locally.',
|
|
85
|
+
'brainbase onboard:demo returns a try-this prompt, sample result, and useful answer for the selected real request.',
|
|
86
|
+
'The user can see what they did not have to explain again.',
|
|
87
|
+
'The completion report lists public skills, ohayo/oyasumi/retro routines, real MCP config merge, source allowlists, and MCP get_context/search verification as unfinished operationalization work.',
|
|
88
|
+
'The selected MCP client has a Brainbase config snippet merged into its real config after user approval.',
|
|
89
|
+
'Source diagnosis is optional follow-up work, but source allowlist/import/candidate review must be explicitly completed or deferred.'
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function buildValueDemo(input) {
|
|
94
|
+
const scenario = input.scenario?.trim() || 'Draft the first message or working note that should use my saved Brainbase context.';
|
|
95
|
+
const seeded = seededAreas(input.os);
|
|
96
|
+
const missing = Object.entries(seeded)
|
|
97
|
+
.filter(([, value]) => !value)
|
|
98
|
+
.map(([key]) => key);
|
|
99
|
+
const self = selfContext(input.os);
|
|
100
|
+
const work = workContext(input.os);
|
|
101
|
+
const relationships = input.os.relationships.relationships.map((relationship) => ({
|
|
102
|
+
person: relationship.person,
|
|
103
|
+
role: relationship.role,
|
|
104
|
+
context: relationship.context
|
|
105
|
+
}));
|
|
106
|
+
const decisions = input.os.decisions.map((decision) => ({
|
|
107
|
+
title: decision.title,
|
|
108
|
+
decision: decision.decision
|
|
109
|
+
}));
|
|
110
|
+
const selectedRelationship = selectRelationship(input.os.relationships.relationships, scenario);
|
|
111
|
+
const ready = missing.length === 0;
|
|
112
|
+
const sampleResult = ready
|
|
113
|
+
? buildReadySampleResult({ scenario, self, work, selectedRelationship, decisions })
|
|
114
|
+
: buildMissingDemoAnswer(missing);
|
|
115
|
+
const valueExplanation = ready
|
|
116
|
+
? buildValueExplanation({ self, work, selectedRelationship, decisions })
|
|
117
|
+
: `まだ最初の価値を見せるための最小メモが足りません: ${missing.map(missingLabel).join(', ')}。`;
|
|
118
|
+
const operationalization = buildOperationalizationPlan({
|
|
119
|
+
dataDir: input.os.dataDir,
|
|
120
|
+
firstValueReady: ready
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
goal: 'Show one useful answer from the context the user just saved before any setup follow-up.',
|
|
124
|
+
ready,
|
|
125
|
+
missing,
|
|
126
|
+
scenario,
|
|
127
|
+
tryPrompt: scenario,
|
|
128
|
+
contextUsed: {
|
|
129
|
+
ownerName: input.os.graph.owner?.name,
|
|
130
|
+
self,
|
|
131
|
+
work,
|
|
132
|
+
relationships,
|
|
133
|
+
selectedRelationship: selectedRelationship ? {
|
|
134
|
+
person: selectedRelationship.person,
|
|
135
|
+
role: selectedRelationship.role,
|
|
136
|
+
context: selectedRelationship.context
|
|
137
|
+
} : undefined,
|
|
138
|
+
decisions
|
|
139
|
+
},
|
|
140
|
+
answer: sampleResult,
|
|
141
|
+
sampleResult,
|
|
142
|
+
valueExplanation,
|
|
143
|
+
operationalization,
|
|
144
|
+
nextStep: ready
|
|
145
|
+
? 'Do not stop here. Use the operationalization checklist below to install skills, register routines with confirmation, merge MCP config, decide source allowlists, and verify MCP get_context/search.'
|
|
146
|
+
: `First save the minimum missing memo: ${missing.map(missingLabel).join(', ')}.`,
|
|
147
|
+
completionSignal: ready ? 'first_value_demo_ready' : 'needs_seed'
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function buildSourceDiagnosis(input) {
|
|
151
|
+
const recommendationSet = buildConnectorRecommendations(input);
|
|
152
|
+
const diagnostics = recommendationSet.recommendations.map((recommendation) => diagnoseRecommendation(recommendation, input));
|
|
153
|
+
return {
|
|
154
|
+
goal: 'Diagnose local source collection readiness before any raw material or candidates are promoted into canonical Brainbase SSOT.',
|
|
155
|
+
diagnostics,
|
|
156
|
+
nextCommands: [
|
|
157
|
+
'brainbase onboard:candidates --write --name "<name>" --project "<current project>"',
|
|
158
|
+
'brainbase onboard:projects --name "<approved project>" --goal "<approved goal>"',
|
|
159
|
+
'brainbase doctor',
|
|
160
|
+
'brainbase onboard:seed --name "<name>" --value "<approved value>" --project "<approved project>"'
|
|
161
|
+
],
|
|
162
|
+
safetyRules: [
|
|
163
|
+
'Do not paste OAuth tokens, passwords, API keys, or refresh tokens into chat.',
|
|
164
|
+
'Use metadata-first source collection before body text or document excerpts.',
|
|
165
|
+
'Keep provider output under sources/ and extracted facts under candidates/.',
|
|
166
|
+
'Promote only reviewed candidate facts into canonical SSOT.',
|
|
167
|
+
'Drive collection must use explicit folder allowlists.'
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function buildCandidateDrafts(input) {
|
|
172
|
+
const now = input.now ?? new Date().toISOString();
|
|
173
|
+
const candidates = [];
|
|
174
|
+
if (input.name) {
|
|
175
|
+
candidates.push(candidate('self', { name: input.name }, now));
|
|
176
|
+
}
|
|
177
|
+
for (const value of input.values ?? []) {
|
|
178
|
+
candidates.push(candidate('value', { text: value, tags: ['onboarding'] }, now));
|
|
179
|
+
}
|
|
180
|
+
for (const project of input.projects ?? []) {
|
|
181
|
+
candidates.push(candidate('project', { name: project, summary: 'Approved during Brainbase agent onboarding.' }, now));
|
|
182
|
+
}
|
|
183
|
+
for (const principle of input.decisionPrinciples ?? []) {
|
|
184
|
+
candidates.push(candidate('decision', {
|
|
185
|
+
title: 'Agent onboarding decision principle',
|
|
186
|
+
decision: principle,
|
|
187
|
+
tags: ['principle', 'onboarding']
|
|
188
|
+
}, now));
|
|
189
|
+
}
|
|
190
|
+
for (const encoded of input.relationships ?? []) {
|
|
191
|
+
const [person, role, context] = encoded.split('|').map((part) => part.trim());
|
|
192
|
+
if (!person || !context) {
|
|
193
|
+
throw new Error('relationship must be "person|role|context" or "person||context"');
|
|
194
|
+
}
|
|
195
|
+
candidates.push(candidate('relationship', { person, role: role || undefined, context }, now));
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
goal: 'Review candidate facts before promoting them into canonical Brainbase SSOT.',
|
|
199
|
+
canonicalWrites: false,
|
|
200
|
+
candidatePath: `${input.dataDir}/candidates/onboarding-candidates-${fileSafeTimestamp(now)}.json`,
|
|
201
|
+
candidates,
|
|
202
|
+
safetyRules: [
|
|
203
|
+
'Candidates are not canonical memory.',
|
|
204
|
+
'Do not promote raw source material without user review.',
|
|
205
|
+
'Only approved facts should be copied into graph.json, personal-kg.jsonl, relationships.json, or decisions.jsonl.'
|
|
206
|
+
],
|
|
207
|
+
nextCommands: [
|
|
208
|
+
'Review the candidate file with the user.',
|
|
209
|
+
'Promote approved self/work/relationship facts with brainbase onboard:seed.',
|
|
210
|
+
'Run brainbase doctor after promotion.'
|
|
211
|
+
]
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
export function buildLocalOnboardingPlan(input) {
|
|
215
|
+
const profile = input.profile || 'google-workspace-local';
|
|
216
|
+
if (profile !== 'google-workspace-local') {
|
|
217
|
+
throw new Error('profile must be google-workspace-local');
|
|
218
|
+
}
|
|
219
|
+
const host = input.host || 'local-machine';
|
|
220
|
+
const email = normalizeInput(input.email || 'google-workspace');
|
|
221
|
+
const calendar = normalizeInput(input.calendar || 'google-calendar');
|
|
222
|
+
const drive = normalizeInput(input.drive || 'google-drive');
|
|
223
|
+
const tasks = normalizeInput(input.tasks || 'scattered-calendar-notes');
|
|
224
|
+
const secondaryEmails = (input.secondaryEmails ?? []).map(normalizeInput).filter((value) => !isNone(value));
|
|
225
|
+
const driveFolders = input.driveFolders ?? [];
|
|
226
|
+
const localFolders = input.localFolders ?? [];
|
|
227
|
+
const inactiveTaskTools = (input.inactiveTaskTools ?? []).map(normalizeInput).filter((value) => !isNone(value));
|
|
228
|
+
const workspaceAccount = accountPlaceholder(email, 'google-workspace-account');
|
|
229
|
+
const secondaryAccounts = secondaryEmails.map((value, index) => accountPlaceholder(value, index === 0 ? 'secondary-gmail-account' : `secondary-email-${index + 1}`));
|
|
230
|
+
const sources = [
|
|
231
|
+
{
|
|
232
|
+
area: 'email',
|
|
233
|
+
input: [email, ...secondaryEmails].join(', '),
|
|
234
|
+
collector: 'gog gmail',
|
|
235
|
+
importMode: 'metadata-first',
|
|
236
|
+
sourcePath: 'sources/gmail/threads.jsonl',
|
|
237
|
+
accounts: [workspaceAccount, ...secondaryAccounts],
|
|
238
|
+
allowlists: [],
|
|
239
|
+
requiredUserInput: ['Workspace mail account', ...(secondaryAccounts.length > 0 ? ['Secondary Gmail account authorization'] : [])],
|
|
240
|
+
notes: [
|
|
241
|
+
'Start with thread metadata, participants, subjects, dates, labels, URLs, and snippets.',
|
|
242
|
+
'Body excerpts require explicit user approval.'
|
|
243
|
+
]
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
area: 'calendar',
|
|
247
|
+
input: calendar,
|
|
248
|
+
collector: 'gog calendar',
|
|
249
|
+
importMode: 'metadata-first',
|
|
250
|
+
sourcePath: 'sources/calendar/events.jsonl',
|
|
251
|
+
accounts: [workspaceAccount],
|
|
252
|
+
allowlists: ['primary', '<additional-allowed-calendar-id>'],
|
|
253
|
+
requiredUserInput: ['Date range', 'Private calendar exclusion decision'],
|
|
254
|
+
notes: [
|
|
255
|
+
'Use Google Calendar as both schedule context and one source of scattered task candidates.',
|
|
256
|
+
'Descriptions and private event details are opt-in.'
|
|
257
|
+
]
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
area: 'drive',
|
|
261
|
+
input: drive,
|
|
262
|
+
collector: 'gog drive',
|
|
263
|
+
importMode: 'metadata-first',
|
|
264
|
+
sourcePath: 'sources/drive/files.jsonl',
|
|
265
|
+
accounts: [workspaceAccount],
|
|
266
|
+
allowlists: driveFolders,
|
|
267
|
+
requiredUserInput: driveFolders.length === 0 ? ['At least one allowed Google Drive folder id'] : [],
|
|
268
|
+
notes: [
|
|
269
|
+
'List metadata for explicitly allowed Drive folders only.',
|
|
270
|
+
'Do not scan the whole Drive.',
|
|
271
|
+
'Downloads are selected-file only after review.'
|
|
272
|
+
]
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
area: 'local_files',
|
|
276
|
+
input: localFolders.length > 0 ? localFolders.join(', ') : 'none',
|
|
277
|
+
collector: 'manual local metadata index',
|
|
278
|
+
importMode: 'manual',
|
|
279
|
+
sourcePath: 'sources/drive/local-files.jsonl',
|
|
280
|
+
accounts: [],
|
|
281
|
+
allowlists: localFolders,
|
|
282
|
+
requiredUserInput: localFolders.length === 0 ? ['At least one allowed local folder for notes or work files'] : [],
|
|
283
|
+
notes: [
|
|
284
|
+
'Use explicit local folder allowlists only.',
|
|
285
|
+
'Do not scan the full home directory.',
|
|
286
|
+
'Local notes are candidate extraction input until reviewed.'
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
area: 'tasks',
|
|
291
|
+
input: tasks,
|
|
292
|
+
collector: 'calendar-and-notes-candidates',
|
|
293
|
+
importMode: 'manual',
|
|
294
|
+
sourcePath: 'sources/tasks/tasks.jsonl',
|
|
295
|
+
accounts: [workspaceAccount],
|
|
296
|
+
allowlists: ['calendar events', ...localFolders],
|
|
297
|
+
requiredUserInput: localFolders.length === 0 ? ['Allowed local notes folder if notes should be included'] : [],
|
|
298
|
+
notes: [
|
|
299
|
+
'Treat Google Calendar entries and local notes as task candidate inputs.',
|
|
300
|
+
'Do not require inactive tools as connectors.',
|
|
301
|
+
...(inactiveTaskTools.length > 0 ? [`Inactive task tools: ${inactiveTaskTools.join(', ')}`] : []),
|
|
302
|
+
'Promote only reviewed tasks, relationships, decisions, and personal KG facts.'
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
];
|
|
306
|
+
const driveFolderArg = driveFolders.length > 0
|
|
307
|
+
? driveFolders.map((folder) => `--drive-folder ${quoteShell(folder)}`).join(' ')
|
|
308
|
+
: '--drive-folder "<allowed-google-drive-folder-id>"';
|
|
309
|
+
return {
|
|
310
|
+
goal: 'Turn a Google Workspace local onboarding interview into a safe Brainbase MCP setup plan.',
|
|
311
|
+
profile,
|
|
312
|
+
canonicalWrites: false,
|
|
313
|
+
host: {
|
|
314
|
+
input: host,
|
|
315
|
+
role: 'local Brainbase MCP runtime host reachable by the owner over SSH',
|
|
316
|
+
boundary: 'This is not a hosted backend, server operations handoff, Lightsail sync, bb.unson.jp sync, or Infisical-managed deployment.'
|
|
317
|
+
},
|
|
318
|
+
sources,
|
|
319
|
+
setupSteps: [
|
|
320
|
+
{
|
|
321
|
+
id: 'runtime-host',
|
|
322
|
+
title: 'Prepare the local runtime host',
|
|
323
|
+
commands: [
|
|
324
|
+
'ssh <mac-mini-host>',
|
|
325
|
+
'git clone https://github.com/Unson-LLC/brainbase.git',
|
|
326
|
+
'cd brainbase && npm install && npm run build'
|
|
327
|
+
],
|
|
328
|
+
notes: [
|
|
329
|
+
'Run Brainbase MCP on the owner-controlled Mac mini or local machine.',
|
|
330
|
+
'Keep the Personal OS under ~/.brainbase/personal-os/.'
|
|
331
|
+
]
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: 'google-workspace-readonly',
|
|
335
|
+
title: 'Authorize Google Workspace read-only collectors',
|
|
336
|
+
commands: [
|
|
337
|
+
`gog auth add ${workspaceAccount} --services gmail,calendar,drive`,
|
|
338
|
+
...secondaryAccounts.map((account) => `gog auth add ${account} --services gmail`),
|
|
339
|
+
`gog gmail search "newer_than:30d" --account ${workspaceAccount} --json`,
|
|
340
|
+
`gog calendar events primary --account ${workspaceAccount} --from <from-date> --to <to-date> --json`,
|
|
341
|
+
...(driveFolders.length > 0
|
|
342
|
+
? driveFolders.map((folder) => `gog drive ls --parent ${folder} --account ${workspaceAccount} --json`)
|
|
343
|
+
: [`gog drive ls --parent <allowed-google-drive-folder-id> --account ${workspaceAccount} --json`])
|
|
344
|
+
],
|
|
345
|
+
notes: [
|
|
346
|
+
'Use metadata-first collection.',
|
|
347
|
+
'Do not paste OAuth credentials or tokens into chat.'
|
|
348
|
+
]
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: 'candidate-extraction',
|
|
352
|
+
title: 'Register projects and draft candidates before canonical promotion',
|
|
353
|
+
commands: [
|
|
354
|
+
'brainbase onboard:projects --name "<project>" --goal "<goal>" --status "<status>" --role "<your role>"',
|
|
355
|
+
'brainbase onboard:candidates --write --name "<name>" --project "<current project>"',
|
|
356
|
+
'brainbase doctor'
|
|
357
|
+
],
|
|
358
|
+
notes: [
|
|
359
|
+
'Review Calendar and local note task candidates with the user.',
|
|
360
|
+
'Use onboard:seed only after approval.'
|
|
361
|
+
]
|
|
362
|
+
}
|
|
363
|
+
],
|
|
364
|
+
safetyRules: [
|
|
365
|
+
'No canonical SSOT writes happen during planning.',
|
|
366
|
+
'Do not paste OAuth tokens, passwords, API keys, or refresh tokens into chat.',
|
|
367
|
+
'Google source collection is metadata-first and read-only in the first pass.',
|
|
368
|
+
'Drive and local file collection are allowlist-first.',
|
|
369
|
+
'Abandoned or inactive task tools are context, not required connectors.',
|
|
370
|
+
'Hosted backend, Infisical, Unson API, Lightsail, and bb.unson.jp sync are out of scope for v1.'
|
|
371
|
+
],
|
|
372
|
+
nextCommands: [
|
|
373
|
+
`brainbase onboard:diagnose-sources --email ${quoteShell(email)} --calendar ${quoteShell(calendar)} --drive ${quoteShell(drive)} ${driveFolderArg} --tasks ${quoteShell(tasks)}`,
|
|
374
|
+
'brainbase onboard:projects --name "<project>" --goal "<goal>" --status "<status>" --role "<your role>"',
|
|
375
|
+
'brainbase onboard:candidates --write --name "<name>" --project "<current project>"',
|
|
376
|
+
'brainbase onboard:install --target codex --dry-run',
|
|
377
|
+
'brainbase doctor'
|
|
378
|
+
]
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
export function buildConnectorRecommendations(input) {
|
|
382
|
+
const recommendations = [
|
|
383
|
+
recommendEmail(input.email),
|
|
384
|
+
recommendCalendar(input.calendar),
|
|
385
|
+
recommendDrive(input.drive),
|
|
386
|
+
recommendTasks(input.tasks)
|
|
387
|
+
];
|
|
388
|
+
return {
|
|
389
|
+
goal: 'Choose a local, read-only source collection path before promoting reviewed candidates into Brainbase canonical SSOT.',
|
|
390
|
+
recommendations,
|
|
391
|
+
canonicalization: [
|
|
392
|
+
'Store imported source material under sources/ by provider.',
|
|
393
|
+
'Extract candidates into candidates/ before canonical writes.',
|
|
394
|
+
'Promote only user-approved people, organizations, projects, relationships, decisions, and personal KG entries.',
|
|
395
|
+
'Normal MCP tools should prefer canonical files over raw source material.'
|
|
396
|
+
],
|
|
397
|
+
nextCommands: [
|
|
398
|
+
'brainbase onboard:init',
|
|
399
|
+
'brainbase onboard:install --target codex --dry-run',
|
|
400
|
+
'brainbase doctor'
|
|
401
|
+
]
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
export function renderAgentProtocol(format) {
|
|
405
|
+
const protocol = buildAgentOnboardingProtocol();
|
|
406
|
+
if (format === 'json') {
|
|
407
|
+
return `${JSON.stringify(protocol, null, 2)}\n`;
|
|
408
|
+
}
|
|
409
|
+
return [
|
|
410
|
+
'# Brainbase Agent Onboarding Protocol',
|
|
411
|
+
'',
|
|
412
|
+
protocol.goal,
|
|
413
|
+
'',
|
|
414
|
+
'## Interview',
|
|
415
|
+
...protocol.interviewSections.flatMap((section) => [
|
|
416
|
+
'',
|
|
417
|
+
`### ${section.title}`,
|
|
418
|
+
...section.questions.map((question) => `- ${question}`)
|
|
419
|
+
]),
|
|
420
|
+
'',
|
|
421
|
+
'## Safety Rules',
|
|
422
|
+
...protocol.safetyRules.map((rule) => `- ${rule}`),
|
|
423
|
+
'',
|
|
424
|
+
'## Next Commands',
|
|
425
|
+
...protocol.nextCommands.map((command) => `- \`${command}\``),
|
|
426
|
+
'',
|
|
427
|
+
'## Completion Check',
|
|
428
|
+
...protocol.completionCheck.map((check) => `- ${check}`),
|
|
429
|
+
''
|
|
430
|
+
].join('\n');
|
|
431
|
+
}
|
|
432
|
+
export function renderConnectorRecommendations(input, format) {
|
|
433
|
+
const recommendationSet = buildConnectorRecommendations(input);
|
|
434
|
+
if (format === 'json') {
|
|
435
|
+
return `${JSON.stringify(recommendationSet, null, 2)}\n`;
|
|
436
|
+
}
|
|
437
|
+
return [
|
|
438
|
+
'# Brainbase Connector Recommendations',
|
|
439
|
+
'',
|
|
440
|
+
recommendationSet.goal,
|
|
441
|
+
'',
|
|
442
|
+
...recommendationSet.recommendations.flatMap((recommendation) => [
|
|
443
|
+
`## ${labelForArea(recommendation.area)}`,
|
|
444
|
+
'',
|
|
445
|
+
`- Input: ${recommendation.input}`,
|
|
446
|
+
`- Recommendation: ${recommendation.recommendation}`,
|
|
447
|
+
`- Source path: \`${recommendation.sourcePath}\``,
|
|
448
|
+
`- Import mode: ${recommendation.importMode}`,
|
|
449
|
+
'- Setup hints:',
|
|
450
|
+
...recommendation.setupHints.map((hint) => ` - ${hint}`),
|
|
451
|
+
'- Safety notes:',
|
|
452
|
+
...recommendation.safetyNotes.map((note) => ` - ${note}`),
|
|
453
|
+
''
|
|
454
|
+
]),
|
|
455
|
+
'## Canonicalization',
|
|
456
|
+
...recommendationSet.canonicalization.map((item) => `- ${item}`),
|
|
457
|
+
'',
|
|
458
|
+
'## Next Commands',
|
|
459
|
+
...recommendationSet.nextCommands.map((command) => `- \`${command}\``),
|
|
460
|
+
''
|
|
461
|
+
].join('\n');
|
|
462
|
+
}
|
|
463
|
+
export function renderSourceDiagnosis(input, format) {
|
|
464
|
+
const diagnosisSet = buildSourceDiagnosis(input);
|
|
465
|
+
if (format === 'json') {
|
|
466
|
+
return `${JSON.stringify(diagnosisSet, null, 2)}\n`;
|
|
467
|
+
}
|
|
468
|
+
return [
|
|
469
|
+
'# Brainbase Source Diagnosis',
|
|
470
|
+
'',
|
|
471
|
+
diagnosisSet.goal,
|
|
472
|
+
'',
|
|
473
|
+
...diagnosisSet.diagnostics.flatMap((diagnosis) => [
|
|
474
|
+
`## ${labelForArea(diagnosis.area)}`,
|
|
475
|
+
'',
|
|
476
|
+
`- Input: ${diagnosis.input}`,
|
|
477
|
+
`- Status: ${diagnosis.status}`,
|
|
478
|
+
`- Collector: ${diagnosis.collector ?? 'none'}`,
|
|
479
|
+
`- Source path: \`${diagnosis.sourcePath}\``,
|
|
480
|
+
`- Write target: \`${diagnosis.writeTarget}\``,
|
|
481
|
+
`- Import mode: ${diagnosis.importMode}`,
|
|
482
|
+
'- Required user input:',
|
|
483
|
+
...(diagnosis.requiredUserInput.length === 0 ? [' - none'] : diagnosis.requiredUserInput.map((item) => ` - ${item}`)),
|
|
484
|
+
'- Setup commands:',
|
|
485
|
+
...(diagnosis.setupCommands.length === 0 ? [' - none'] : diagnosis.setupCommands.map((command) => ` - \`${command}\``)),
|
|
486
|
+
'- Safety notes:',
|
|
487
|
+
...diagnosis.safetyNotes.map((note) => ` - ${note}`),
|
|
488
|
+
''
|
|
489
|
+
]),
|
|
490
|
+
'## Safety Rules',
|
|
491
|
+
...diagnosisSet.safetyRules.map((rule) => `- ${rule}`),
|
|
492
|
+
'',
|
|
493
|
+
'## Next Commands',
|
|
494
|
+
...diagnosisSet.nextCommands.map((command) => `- \`${command}\``),
|
|
495
|
+
''
|
|
496
|
+
].join('\n');
|
|
497
|
+
}
|
|
498
|
+
export function renderValueDemo(input, format) {
|
|
499
|
+
const demo = buildValueDemo(input);
|
|
500
|
+
if (format === 'json') {
|
|
501
|
+
return `${JSON.stringify(demo, null, 2)}\n`;
|
|
502
|
+
}
|
|
503
|
+
return [
|
|
504
|
+
'# Brainbase First Value Demo',
|
|
505
|
+
'',
|
|
506
|
+
demo.goal,
|
|
507
|
+
'',
|
|
508
|
+
`- Ready: ${String(demo.ready)}`,
|
|
509
|
+
`- Completion signal: ${demo.completionSignal}`,
|
|
510
|
+
`- Missing: ${demo.missing.length === 0 ? 'none' : demo.missing.map(missingLabel).join(', ')}`,
|
|
511
|
+
'',
|
|
512
|
+
'## Try This Now',
|
|
513
|
+
demo.tryPrompt,
|
|
514
|
+
'',
|
|
515
|
+
'## Sample Result',
|
|
516
|
+
demo.sampleResult,
|
|
517
|
+
'',
|
|
518
|
+
'## What Changed',
|
|
519
|
+
demo.valueExplanation,
|
|
520
|
+
'',
|
|
521
|
+
'## Operationalization Still Pending',
|
|
522
|
+
demo.operationalization.goal,
|
|
523
|
+
'',
|
|
524
|
+
'### Completed',
|
|
525
|
+
...demo.operationalization.completed.map((item) => `- ${item}`),
|
|
526
|
+
'',
|
|
527
|
+
'### Next Actions',
|
|
528
|
+
...demo.operationalization.pending.map((item) => `- ${item.title}: \`${item.command}\` (${item.safety})`),
|
|
529
|
+
'',
|
|
530
|
+
'### Recommended Order',
|
|
531
|
+
...demo.operationalization.recommendedOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
532
|
+
'',
|
|
533
|
+
'## Next Step',
|
|
534
|
+
demo.nextStep,
|
|
535
|
+
''
|
|
536
|
+
].join('\n');
|
|
537
|
+
}
|
|
538
|
+
export function renderCandidateDrafts(input, format) {
|
|
539
|
+
const candidateSet = buildCandidateDrafts(input);
|
|
540
|
+
if (format === 'json') {
|
|
541
|
+
return `${JSON.stringify(candidateSet, null, 2)}\n`;
|
|
542
|
+
}
|
|
543
|
+
return [
|
|
544
|
+
'# Brainbase Onboarding Candidates',
|
|
545
|
+
'',
|
|
546
|
+
candidateSet.goal,
|
|
547
|
+
'',
|
|
548
|
+
`- Canonical writes: ${String(candidateSet.canonicalWrites)}`,
|
|
549
|
+
`- Candidate path: \`${candidateSet.candidatePath}\``,
|
|
550
|
+
'',
|
|
551
|
+
'## Candidates',
|
|
552
|
+
...(candidateSet.candidates.length === 0
|
|
553
|
+
? ['- none']
|
|
554
|
+
: candidateSet.candidates.map((candidate) => `- ${candidate.kind}: ${JSON.stringify(candidate.payload)}`)),
|
|
555
|
+
'',
|
|
556
|
+
'## Safety Rules',
|
|
557
|
+
...candidateSet.safetyRules.map((rule) => `- ${rule}`),
|
|
558
|
+
'',
|
|
559
|
+
'## Next Commands',
|
|
560
|
+
...candidateSet.nextCommands.map((command) => `- ${command}`),
|
|
561
|
+
''
|
|
562
|
+
].join('\n');
|
|
563
|
+
}
|
|
564
|
+
export function renderLocalOnboardingPlan(input, format) {
|
|
565
|
+
const plan = buildLocalOnboardingPlan(input);
|
|
566
|
+
if (format === 'json') {
|
|
567
|
+
return `${JSON.stringify(plan, null, 2)}\n`;
|
|
568
|
+
}
|
|
569
|
+
return [
|
|
570
|
+
'# Brainbase Local Onboarding Plan',
|
|
571
|
+
'',
|
|
572
|
+
plan.goal,
|
|
573
|
+
'',
|
|
574
|
+
`- Profile: ${plan.profile}`,
|
|
575
|
+
`- Canonical writes: ${String(plan.canonicalWrites)}`,
|
|
576
|
+
'',
|
|
577
|
+
'## Runtime Host',
|
|
578
|
+
`- Input: ${plan.host.input}`,
|
|
579
|
+
`- Role: ${plan.host.role}`,
|
|
580
|
+
`- Boundary: ${plan.host.boundary}`,
|
|
581
|
+
'',
|
|
582
|
+
'## Sources',
|
|
583
|
+
...plan.sources.flatMap((source) => [
|
|
584
|
+
'',
|
|
585
|
+
`### ${labelForPlanArea(source.area)}`,
|
|
586
|
+
`- Input: ${source.input}`,
|
|
587
|
+
`- Collector: ${source.collector}`,
|
|
588
|
+
`- Import mode: ${source.importMode}`,
|
|
589
|
+
`- Source path: \`${source.sourcePath}\``,
|
|
590
|
+
`- Accounts: ${source.accounts.length > 0 ? source.accounts.join(', ') : 'none'}`,
|
|
591
|
+
`- Allowlists: ${source.allowlists.length > 0 ? source.allowlists.join(', ') : 'none'}`,
|
|
592
|
+
'- Required user input:',
|
|
593
|
+
...(source.requiredUserInput.length === 0 ? [' - none'] : source.requiredUserInput.map((item) => ` - ${item}`)),
|
|
594
|
+
'- Notes:',
|
|
595
|
+
...source.notes.map((note) => ` - ${note}`)
|
|
596
|
+
]),
|
|
597
|
+
'',
|
|
598
|
+
'## Setup Steps',
|
|
599
|
+
...plan.setupSteps.flatMap((step) => [
|
|
600
|
+
'',
|
|
601
|
+
`### ${step.title}`,
|
|
602
|
+
'- Commands:',
|
|
603
|
+
...step.commands.map((command) => ` - \`${command}\``),
|
|
604
|
+
'- Notes:',
|
|
605
|
+
...step.notes.map((note) => ` - ${note}`)
|
|
606
|
+
]),
|
|
607
|
+
'',
|
|
608
|
+
'## Safety Rules',
|
|
609
|
+
...plan.safetyRules.map((rule) => `- ${rule}`),
|
|
610
|
+
'',
|
|
611
|
+
'## Next Commands',
|
|
612
|
+
...plan.nextCommands.map((command) => `- \`${command}\``),
|
|
613
|
+
''
|
|
614
|
+
].join('\n');
|
|
615
|
+
}
|
|
616
|
+
export function parseOnboardingFormat(value) {
|
|
617
|
+
if (!value || value === 'markdown') {
|
|
618
|
+
return 'markdown';
|
|
619
|
+
}
|
|
620
|
+
if (value === 'json') {
|
|
621
|
+
return 'json';
|
|
622
|
+
}
|
|
623
|
+
throw new Error('format must be markdown|json');
|
|
624
|
+
}
|
|
625
|
+
function diagnoseRecommendation(recommendation, input) {
|
|
626
|
+
const writeTarget = `${input.dataDir}/${recommendation.sourcePath}`;
|
|
627
|
+
if (recommendation.importMode === 'not-configured') {
|
|
628
|
+
return {
|
|
629
|
+
area: recommendation.area,
|
|
630
|
+
input: recommendation.input,
|
|
631
|
+
status: 'not_configured',
|
|
632
|
+
collector: null,
|
|
633
|
+
sourcePath: recommendation.sourcePath,
|
|
634
|
+
writeTarget,
|
|
635
|
+
importMode: recommendation.importMode,
|
|
636
|
+
requiredUserInput: [],
|
|
637
|
+
setupCommands: [],
|
|
638
|
+
safetyNotes: recommendation.safetyNotes
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
if (isGoogleDiagnosis(recommendation)) {
|
|
642
|
+
const gogCommand = input.gogCommand || 'gog';
|
|
643
|
+
const driveFolders = input.driveFolders ?? [];
|
|
644
|
+
const needsDriveFolder = recommendation.area === 'drive' && driveFolders.length === 0;
|
|
645
|
+
const status = input.gogAvailable
|
|
646
|
+
? (needsDriveFolder ? 'needs_input' : 'ready')
|
|
647
|
+
: 'needs_setup';
|
|
648
|
+
return {
|
|
649
|
+
area: recommendation.area,
|
|
650
|
+
input: recommendation.input,
|
|
651
|
+
status,
|
|
652
|
+
collector: 'gog',
|
|
653
|
+
sourcePath: recommendation.sourcePath,
|
|
654
|
+
writeTarget,
|
|
655
|
+
importMode: 'metadata-first',
|
|
656
|
+
requiredUserInput: [
|
|
657
|
+
...(input.gogAvailable ? [] : [`Install or configure local GoG command: ${gogCommand}`]),
|
|
658
|
+
...(needsDriveFolder ? ['At least one allowed Google Drive folder id'] : []),
|
|
659
|
+
...(recommendation.area === 'calendar' ? ['Date range and private-calendar exclusion decision'] : []),
|
|
660
|
+
...(recommendation.area === 'email' ? ['Mail account and metadata/body-excerpt scope decision'] : [])
|
|
661
|
+
],
|
|
662
|
+
setupCommands: googleSetupCommands(recommendation.area, gogCommand, writeTarget, driveFolders),
|
|
663
|
+
safetyNotes: recommendation.safetyNotes
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
area: recommendation.area,
|
|
668
|
+
input: recommendation.input,
|
|
669
|
+
status: recommendation.importMode === 'manual' ? 'needs_input' : 'needs_setup',
|
|
670
|
+
collector: collectorFor(recommendation),
|
|
671
|
+
sourcePath: recommendation.sourcePath,
|
|
672
|
+
writeTarget,
|
|
673
|
+
importMode: recommendation.importMode,
|
|
674
|
+
requiredUserInput: nonGoogleRequiredInput(recommendation),
|
|
675
|
+
setupCommands: nonGoogleSetupCommands(recommendation, writeTarget),
|
|
676
|
+
safetyNotes: recommendation.safetyNotes
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function seededAreas(os) {
|
|
680
|
+
return {
|
|
681
|
+
self: Boolean(os.graph.owner?.name) || os.personalKg.some((entry) => entry.type === 'self'),
|
|
682
|
+
work: os.graph.entities.some((entity) => entity.type === 'project') || os.personalKg.some((entry) => entry.type === 'work'),
|
|
683
|
+
relationships: os.relationships.relationships.length > 0
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function selfContext(os) {
|
|
687
|
+
return [
|
|
688
|
+
...(os.graph.owner?.name ? [`Owner: ${os.graph.owner.name}`] : []),
|
|
689
|
+
...(os.graph.owner?.summary ? [os.graph.owner.summary] : []),
|
|
690
|
+
...os.personalKg
|
|
691
|
+
.filter((entry) => entry.type === 'self' || entry.type === 'value' || entry.type === 'judgment')
|
|
692
|
+
.map((entry) => entry.text)
|
|
693
|
+
];
|
|
694
|
+
}
|
|
695
|
+
function workContext(os) {
|
|
696
|
+
return [
|
|
697
|
+
...os.graph.entities
|
|
698
|
+
.filter((entity) => entity.type === 'project')
|
|
699
|
+
.map((entity) => entity.summary ? `${entity.name}: ${entity.summary}` : entity.name),
|
|
700
|
+
...os.personalKg
|
|
701
|
+
.filter((entry) => entry.type === 'work' || entry.type === 'experience')
|
|
702
|
+
.map((entry) => entry.text)
|
|
703
|
+
];
|
|
704
|
+
}
|
|
705
|
+
function selectRelationship(relationships, scenario) {
|
|
706
|
+
const normalizedScenario = normalizeForMatch(scenario);
|
|
707
|
+
return relationships.find((relationship) => relationshipTerms(relationship.person)
|
|
708
|
+
.some((term) => normalizedScenario.includes(normalizeForMatch(term))))
|
|
709
|
+
?? relationships[0];
|
|
710
|
+
}
|
|
711
|
+
function relationshipTerms(name) {
|
|
712
|
+
const compact = name.replace(/\s+/g, '');
|
|
713
|
+
const terms = new Set([
|
|
714
|
+
name,
|
|
715
|
+
compact,
|
|
716
|
+
...name.split(/[\s,|/()]+/).filter(Boolean)
|
|
717
|
+
]);
|
|
718
|
+
if (compact.length >= 2) {
|
|
719
|
+
terms.add(compact.slice(0, 2));
|
|
720
|
+
}
|
|
721
|
+
return [...terms].filter((term) => term.length >= 2);
|
|
722
|
+
}
|
|
723
|
+
function normalizeForMatch(value) {
|
|
724
|
+
return value.toLowerCase().replace(/\s+/g, '');
|
|
725
|
+
}
|
|
726
|
+
function buildReadySampleResult(input) {
|
|
727
|
+
const projectLine = input.work[0] ?? '保存済みの仕事メモ';
|
|
728
|
+
const ownerLine = input.self[0] ?? 'あなたの進め方';
|
|
729
|
+
const relationshipLine = input.selectedRelationship
|
|
730
|
+
? `${input.selectedRelationship.person}${input.selectedRelationship.role ? `(${input.selectedRelationship.role})` : ''}には、${input.selectedRelationship.context}を前提に相談します。`
|
|
731
|
+
: '特定の相談相手が未指定なので、まず保存済みの仕事メモから次の一手を整理します。';
|
|
732
|
+
const decisionLine = input.decisions[0]
|
|
733
|
+
? `判断基準: ${input.decisions[0].decision}`
|
|
734
|
+
: '追加の判断基準はまだないので、次に動ける形へ絞ります。';
|
|
735
|
+
return [
|
|
736
|
+
`「${input.scenario}」への回答例:`,
|
|
737
|
+
'',
|
|
738
|
+
`次に進める作業は、${projectLine}を前提に一つのメモへ整理することです。`,
|
|
739
|
+
`進め方は、${ownerLine}を崩さず、説明を足しすぎない形にします。`,
|
|
740
|
+
relationshipLine,
|
|
741
|
+
decisionLine,
|
|
742
|
+
'',
|
|
743
|
+
'そのまま送れる最初のアウトプット:',
|
|
744
|
+
input.selectedRelationship
|
|
745
|
+
? `${input.selectedRelationship.person}さんに確認したい論点を、背景説明ではなく「次に判断したいこと」と「相談したいこと」に分けて出します。`
|
|
746
|
+
: '保存済みの仕事メモから、今日進める作業と追加で確認したい一点だけを出します。'
|
|
747
|
+
].join('\n');
|
|
748
|
+
}
|
|
749
|
+
function buildValueExplanation(input) {
|
|
750
|
+
const remembered = [];
|
|
751
|
+
if (input.self[0])
|
|
752
|
+
remembered.push('あなたの前提');
|
|
753
|
+
if (input.work[0])
|
|
754
|
+
remembered.push('今の仕事');
|
|
755
|
+
if (input.selectedRelationship)
|
|
756
|
+
remembered.push(`${input.selectedRelationship.person}さんの文脈`);
|
|
757
|
+
if (input.decisions[0])
|
|
758
|
+
remembered.push('判断基準');
|
|
759
|
+
return remembered.length === 0
|
|
760
|
+
? 'まだ十分なメモがないため、説明し直しを減らせていません。'
|
|
761
|
+
: `${remembered.join('、')}をもう一度説明しなくても、回答がそこから始まります。`;
|
|
762
|
+
}
|
|
763
|
+
function buildMissingDemoAnswer(missing) {
|
|
764
|
+
return [
|
|
765
|
+
'まだ最初の価値体験はできません。',
|
|
766
|
+
`足りない最小メモ: ${missing.map(missingLabel).join(', ')}。`,
|
|
767
|
+
'まず、あなたが毎回説明したくないことを一つだけ保存してから、同じプロンプトをもう一度試します。'
|
|
768
|
+
].join('\n');
|
|
769
|
+
}
|
|
770
|
+
function missingLabel(value) {
|
|
771
|
+
if (value === 'self')
|
|
772
|
+
return 'あなた自身の前提';
|
|
773
|
+
if (value === 'work')
|
|
774
|
+
return '今の仕事';
|
|
775
|
+
if (value === 'relationships')
|
|
776
|
+
return '相談相手や関係者';
|
|
777
|
+
return value;
|
|
778
|
+
}
|
|
779
|
+
function isGoogleDiagnosis(recommendation) {
|
|
780
|
+
return recommendation.importMode === 'metadata-first'
|
|
781
|
+
&& (recommendation.recommendation.includes('GoG') || recommendation.recommendation.includes('Google'));
|
|
782
|
+
}
|
|
783
|
+
function googleSetupCommands(area, gogCommand, writeTarget, driveFolders) {
|
|
784
|
+
if (area === 'email') {
|
|
785
|
+
return [
|
|
786
|
+
`${gogCommand} gmail auth --readonly`,
|
|
787
|
+
`${gogCommand} gmail threads --metadata --out ${writeTarget}`
|
|
788
|
+
];
|
|
789
|
+
}
|
|
790
|
+
if (area === 'calendar') {
|
|
791
|
+
return [
|
|
792
|
+
`${gogCommand} calendar auth --readonly`,
|
|
793
|
+
`${gogCommand} calendar events --metadata --out ${writeTarget}`
|
|
794
|
+
];
|
|
795
|
+
}
|
|
796
|
+
if (area === 'drive') {
|
|
797
|
+
const folder = driveFolders[0] ?? '<allowed-folder-id>';
|
|
798
|
+
return [
|
|
799
|
+
`${gogCommand} drive auth --readonly`,
|
|
800
|
+
`${gogCommand} drive ls ${folder} --json --out ${writeTarget}`,
|
|
801
|
+
`${gogCommand} drive download <file-id> --out <approved-output-path>`
|
|
802
|
+
];
|
|
803
|
+
}
|
|
804
|
+
return [];
|
|
805
|
+
}
|
|
806
|
+
function collectorFor(recommendation) {
|
|
807
|
+
if (recommendation.area === 'tasks') {
|
|
808
|
+
if (matches(recommendation.input, ['notion']))
|
|
809
|
+
return 'notion-mcp-or-export';
|
|
810
|
+
if (matches(recommendation.input, ['linear']))
|
|
811
|
+
return 'linear-mcp-or-export';
|
|
812
|
+
if (matches(recommendation.input, ['github', 'github issues', 'github-issues', 'issues']))
|
|
813
|
+
return 'github-connector-or-export';
|
|
814
|
+
if (matches(recommendation.input, ['nocodb']))
|
|
815
|
+
return 'nocodb-mcp-or-export';
|
|
816
|
+
}
|
|
817
|
+
return recommendation.importMode === 'manual' ? 'manual-export' : 'external-export';
|
|
818
|
+
}
|
|
819
|
+
function nonGoogleRequiredInput(recommendation) {
|
|
820
|
+
if (recommendation.importMode === 'manual') {
|
|
821
|
+
return ['Explicit local file, folder, CSV, or JSONL export path'];
|
|
822
|
+
}
|
|
823
|
+
if (recommendation.area === 'tasks') {
|
|
824
|
+
return ['Workspace/project/database allowlist', 'Read-only export or connector path'];
|
|
825
|
+
}
|
|
826
|
+
return ['Read-only export path', 'Allowed account or workspace'];
|
|
827
|
+
}
|
|
828
|
+
function nonGoogleSetupCommands(recommendation, writeTarget) {
|
|
829
|
+
if (matches(recommendation.input, ['none', 'no', 'なし'])) {
|
|
830
|
+
return [];
|
|
831
|
+
}
|
|
832
|
+
if (matches(recommendation.input, ['csv', 'spreadsheet', 'excel', 'manual'])) {
|
|
833
|
+
return [`Normalize the approved export into ${writeTarget}`];
|
|
834
|
+
}
|
|
835
|
+
return [`Export or collect read-only data, then normalize JSONL into ${writeTarget}`];
|
|
836
|
+
}
|
|
837
|
+
function candidate(kind, payload, createdAt) {
|
|
838
|
+
return {
|
|
839
|
+
id: `candidate-${kind}-${hash(`${kind}:${JSON.stringify(payload)}`)}`,
|
|
840
|
+
kind,
|
|
841
|
+
payload,
|
|
842
|
+
source: 'agent-interview',
|
|
843
|
+
promoted: false,
|
|
844
|
+
createdAt
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function fileSafeTimestamp(value) {
|
|
848
|
+
return value.replace(/[^0-9A-Za-z-]/g, '-');
|
|
849
|
+
}
|
|
850
|
+
function hash(value) {
|
|
851
|
+
let hashValue = 0;
|
|
852
|
+
for (const char of value) {
|
|
853
|
+
hashValue = ((hashValue << 5) - hashValue + char.charCodeAt(0)) | 0;
|
|
854
|
+
}
|
|
855
|
+
return Math.abs(hashValue).toString(36);
|
|
856
|
+
}
|
|
857
|
+
function recommendEmail(value) {
|
|
858
|
+
const input = normalizeInput(value);
|
|
859
|
+
if (isNone(input))
|
|
860
|
+
return notConfigured('email', input, 'sources/gmail/threads.jsonl');
|
|
861
|
+
if (matches(input, ['gmail', 'google', 'google mail', 'google workspace', 'google-workspace', 'gws', 'workspace'])) {
|
|
862
|
+
return {
|
|
863
|
+
area: 'email',
|
|
864
|
+
input,
|
|
865
|
+
recommendation: 'Use local GoG Gmail collection when available.',
|
|
866
|
+
sourcePath: 'sources/gmail/threads.jsonl',
|
|
867
|
+
importMode: 'metadata-first',
|
|
868
|
+
setupHints: [
|
|
869
|
+
'Run GoG auth for Gmail with a read-only scope.',
|
|
870
|
+
'Start from thread metadata, participants, subject, dates, labels, URLs, and snippets.',
|
|
871
|
+
'Collect body excerpts only after the user approves that scope.'
|
|
872
|
+
],
|
|
873
|
+
safetyNotes: commonSafetyNotes()
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
if (matches(input, ['outlook', 'microsoft', 'microsoft 365', 'office 365', 'exchange'])) {
|
|
877
|
+
return exportFirst('email', input, 'sources/gmail/threads.jsonl', 'Use Microsoft Graph or mailbox export as a future connector path.');
|
|
878
|
+
}
|
|
879
|
+
if (matches(input, ['apple', 'apple mail', 'mail.app'])) {
|
|
880
|
+
return exportFirst('email', input, 'sources/gmail/threads.jsonl', 'Use Apple Mail export or mbox import as a future connector path.');
|
|
881
|
+
}
|
|
882
|
+
return manual('email', input, 'sources/gmail/threads.jsonl');
|
|
883
|
+
}
|
|
884
|
+
function recommendCalendar(value) {
|
|
885
|
+
const input = normalizeInput(value);
|
|
886
|
+
if (isNone(input))
|
|
887
|
+
return notConfigured('calendar', input, 'sources/calendar/events.jsonl');
|
|
888
|
+
if (matches(input, ['google', 'google-calendar', 'google calendar', 'gcal', 'google workspace', 'google-workspace', 'gws', 'workspace'])) {
|
|
889
|
+
return {
|
|
890
|
+
area: 'calendar',
|
|
891
|
+
input,
|
|
892
|
+
recommendation: 'Use local GoG Google Calendar collection when available.',
|
|
893
|
+
sourcePath: 'sources/calendar/events.jsonl',
|
|
894
|
+
importMode: 'metadata-first',
|
|
895
|
+
setupHints: [
|
|
896
|
+
'Run GoG auth for calendar with a read-only scope.',
|
|
897
|
+
'Collect event id, title, start/end, organizer, attendees, recurrence, URL, and selected metadata.',
|
|
898
|
+
'Treat private calendars and descriptions as opt-in.'
|
|
899
|
+
],
|
|
900
|
+
safetyNotes: commonSafetyNotes()
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
if (matches(input, ['outlook', 'microsoft', 'microsoft 365', 'office 365', 'exchange'])) {
|
|
904
|
+
return exportFirst('calendar', input, 'sources/calendar/events.jsonl', 'Use Microsoft Graph or calendar export as a future connector path.');
|
|
905
|
+
}
|
|
906
|
+
if (matches(input, ['apple', 'apple calendar', 'ical', 'icloud'])) {
|
|
907
|
+
return exportFirst('calendar', input, 'sources/calendar/events.jsonl', 'Use iCalendar export as a future connector path.');
|
|
908
|
+
}
|
|
909
|
+
return manual('calendar', input, 'sources/calendar/events.jsonl');
|
|
910
|
+
}
|
|
911
|
+
function recommendDrive(value) {
|
|
912
|
+
const input = normalizeInput(value);
|
|
913
|
+
if (isNone(input))
|
|
914
|
+
return notConfigured('drive', input, 'sources/drive/files.jsonl');
|
|
915
|
+
if (matches(input, ['google', 'google-drive', 'google drive', 'drive', 'google-docs', 'google docs', 'docs', 'google workspace', 'google-workspace', 'gws', 'workspace'])) {
|
|
916
|
+
return {
|
|
917
|
+
area: 'drive',
|
|
918
|
+
input,
|
|
919
|
+
recommendation: 'Use local GoG Drive collection with an explicit folder allowlist.',
|
|
920
|
+
sourcePath: 'sources/drive/files.jsonl',
|
|
921
|
+
importMode: 'metadata-first',
|
|
922
|
+
setupHints: [
|
|
923
|
+
'Run GoG auth for Drive with a read-only scope.',
|
|
924
|
+
'Ask the user for allowed folder ids before collection.',
|
|
925
|
+
'Use metadata first: file id, title, mime type, owner, modified time, folder path, URL.',
|
|
926
|
+
'If downloading is approved, use GoG Drive download with --out for selected files only.'
|
|
927
|
+
],
|
|
928
|
+
safetyNotes: [
|
|
929
|
+
...commonSafetyNotes(),
|
|
930
|
+
'Do not scan the whole Drive by default.',
|
|
931
|
+
'Exclude contracts, medical, finance, and private folders unless explicitly approved.'
|
|
932
|
+
]
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
if (matches(input, ['onedrive', 'one drive', 'sharepoint', 'microsoft'])) {
|
|
936
|
+
return exportFirst('drive', input, 'sources/drive/files.jsonl', 'Use Microsoft Graph or selective export as a future connector path.');
|
|
937
|
+
}
|
|
938
|
+
if (matches(input, ['dropbox'])) {
|
|
939
|
+
return exportFirst('drive', input, 'sources/drive/files.jsonl', 'Use Dropbox export or API collection as a future connector path.');
|
|
940
|
+
}
|
|
941
|
+
if (matches(input, ['local', 'folder', 'local folder', 'filesystem'])) {
|
|
942
|
+
return manual('drive', input, 'sources/drive/files.jsonl', 'Use an explicit local folder allowlist.');
|
|
943
|
+
}
|
|
944
|
+
return manual('drive', input, 'sources/drive/files.jsonl');
|
|
945
|
+
}
|
|
946
|
+
function recommendTasks(value) {
|
|
947
|
+
const input = normalizeInput(value);
|
|
948
|
+
if (isNone(input))
|
|
949
|
+
return notConfigured('tasks', input, 'sources/tasks/tasks.jsonl');
|
|
950
|
+
if (matches(input, ['notion'])) {
|
|
951
|
+
return exportFirst('tasks', input, 'sources/tasks/tasks.jsonl', 'Use Notion MCP, Notion API, or a database export.');
|
|
952
|
+
}
|
|
953
|
+
if (matches(input, ['todoist'])) {
|
|
954
|
+
return exportFirst('tasks', input, 'sources/tasks/tasks.jsonl', 'Use Todoist API or export.');
|
|
955
|
+
}
|
|
956
|
+
if (matches(input, ['linear'])) {
|
|
957
|
+
return exportFirst('tasks', input, 'sources/tasks/tasks.jsonl', 'Use Linear MCP or issue export.');
|
|
958
|
+
}
|
|
959
|
+
if (matches(input, ['github', 'github issues', 'issues'])) {
|
|
960
|
+
return exportFirst('tasks', input, 'sources/tasks/tasks.jsonl', 'Use GitHub Issues through the GitHub connector or export.');
|
|
961
|
+
}
|
|
962
|
+
if (matches(input, ['nocodb'])) {
|
|
963
|
+
return exportFirst('tasks', input, 'sources/tasks/tasks.jsonl', 'Use NocoDB MCP/API or table export.');
|
|
964
|
+
}
|
|
965
|
+
if (matches(input, ['csv', 'spreadsheet', 'excel', 'manual'])) {
|
|
966
|
+
return manual('tasks', input, 'sources/tasks/tasks.jsonl', 'Use CSV or JSONL import from an explicit file.');
|
|
967
|
+
}
|
|
968
|
+
if (matches(input, ['scattered-calendar-notes', 'calendar-notes', 'calendar and notes', 'calendar', 'notes', 'memo', 'memos'])) {
|
|
969
|
+
return manual('tasks', input, 'sources/tasks/tasks.jsonl', 'Use Google Calendar entries and explicit local notes as candidate task inputs.');
|
|
970
|
+
}
|
|
971
|
+
return manual('tasks', input, 'sources/tasks/tasks.jsonl');
|
|
972
|
+
}
|
|
973
|
+
function exportFirst(area, input, sourcePath, recommendation) {
|
|
974
|
+
return {
|
|
975
|
+
area,
|
|
976
|
+
input,
|
|
977
|
+
recommendation,
|
|
978
|
+
sourcePath,
|
|
979
|
+
importMode: 'export-first',
|
|
980
|
+
setupHints: [
|
|
981
|
+
'Prefer an official read-only API or user-generated export.',
|
|
982
|
+
'Normalize into JSONL under the source path before extracting candidates.'
|
|
983
|
+
],
|
|
984
|
+
safetyNotes: commonSafetyNotes()
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function manual(area, input, sourcePath, recommendation = 'Use a manual export or local JSONL file until a connector is implemented.') {
|
|
988
|
+
return {
|
|
989
|
+
area,
|
|
990
|
+
input,
|
|
991
|
+
recommendation,
|
|
992
|
+
sourcePath,
|
|
993
|
+
importMode: 'manual',
|
|
994
|
+
setupHints: [
|
|
995
|
+
'Ask the user for an explicit export file or folder.',
|
|
996
|
+
'Normalize into JSONL under the source path before extracting candidates.'
|
|
997
|
+
],
|
|
998
|
+
safetyNotes: commonSafetyNotes()
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function notConfigured(area, input, sourcePath) {
|
|
1002
|
+
return {
|
|
1003
|
+
area,
|
|
1004
|
+
input,
|
|
1005
|
+
recommendation: 'No connector configured for this area.',
|
|
1006
|
+
sourcePath,
|
|
1007
|
+
importMode: 'not-configured',
|
|
1008
|
+
setupHints: [
|
|
1009
|
+
'Skip this area during onboarding unless the user later selects a tool.'
|
|
1010
|
+
],
|
|
1011
|
+
safetyNotes: commonSafetyNotes()
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
function commonSafetyNotes() {
|
|
1015
|
+
return [
|
|
1016
|
+
'Keep raw source material under sources/ until reviewed.',
|
|
1017
|
+
'Do not paste secrets or OAuth tokens into chat.',
|
|
1018
|
+
'Promote only approved candidates into canonical SSOT.'
|
|
1019
|
+
];
|
|
1020
|
+
}
|
|
1021
|
+
function normalizeInput(value) {
|
|
1022
|
+
return value?.trim().toLowerCase() || 'none';
|
|
1023
|
+
}
|
|
1024
|
+
function accountPlaceholder(input, fallback) {
|
|
1025
|
+
if (input.includes('@')) {
|
|
1026
|
+
return input;
|
|
1027
|
+
}
|
|
1028
|
+
if (matches(input, ['gmail', 'google mail'])) {
|
|
1029
|
+
return '<gmail-account>';
|
|
1030
|
+
}
|
|
1031
|
+
if (matches(input, ['google-workspace', 'google workspace', 'gws', 'workspace'])) {
|
|
1032
|
+
return `<${fallback}>`;
|
|
1033
|
+
}
|
|
1034
|
+
return `<${fallback}>`;
|
|
1035
|
+
}
|
|
1036
|
+
function quoteShell(value) {
|
|
1037
|
+
if (/^[A-Za-z0-9._:@/-]+$/.test(value)) {
|
|
1038
|
+
return value;
|
|
1039
|
+
}
|
|
1040
|
+
return JSON.stringify(value);
|
|
1041
|
+
}
|
|
1042
|
+
function matches(input, aliases) {
|
|
1043
|
+
return aliases.includes(input);
|
|
1044
|
+
}
|
|
1045
|
+
function isNone(input) {
|
|
1046
|
+
return input === 'none' || input === 'no' || input === 'なし';
|
|
1047
|
+
}
|
|
1048
|
+
function labelForArea(area) {
|
|
1049
|
+
if (area === 'email')
|
|
1050
|
+
return 'Email';
|
|
1051
|
+
if (area === 'calendar')
|
|
1052
|
+
return 'Calendar';
|
|
1053
|
+
if (area === 'drive')
|
|
1054
|
+
return 'Drive and Docs';
|
|
1055
|
+
return 'Tasks';
|
|
1056
|
+
}
|
|
1057
|
+
function labelForPlanArea(area) {
|
|
1058
|
+
if (area === 'local_files')
|
|
1059
|
+
return 'Local Files';
|
|
1060
|
+
return labelForArea(area);
|
|
1061
|
+
}
|