@unson/brainbase-mcp 0.1.0 → 0.2.2

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/server.js CHANGED
@@ -2,6 +2,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { z } from 'zod';
5
+ import { ConnectedOnboardingRuntime } from './connected-onboarding.js';
5
6
  import { resolveDataDir } from './paths.js';
6
7
  import { auditPersonalOsDirectory } from './ontology-ssot.js';
7
8
  import { getOntologyImpact, inferPersonalOs, portableOntology, resolveOntologyVersion } from './ontology.js';
@@ -16,6 +17,71 @@ const argsSchema = z.object({
16
17
  ontologyVersion: z.string().optional(),
17
18
  asOf: z.string().datetime({ offset: true }).optional()
18
19
  });
20
+ const sourceInventorySchema = z.object({
21
+ id: z.string(),
22
+ mode: z.enum(['mcp', 'drive', 'gmail', 'local_folder', 'single_document']),
23
+ status: z.enum(['ready', 'waiting_for_authorization', 'unavailable', 'error', 'unconfirmed']),
24
+ evidencePointer: z.string().optional(),
25
+ permissionScope: z.array(z.string()).optional(),
26
+ detail: z.string().optional()
27
+ }).strict();
28
+ const candidateSchema = z.object({
29
+ kind: z.string(),
30
+ payload: z.record(z.unknown()),
31
+ observationClass: z.enum(['observed', 'inferred']),
32
+ evidenceId: z.string()
33
+ }).strict();
34
+ const reviewActionSchema = z.discriminatedUnion('decision', [
35
+ z.object({ candidateId: z.string(), decision: z.literal('approve'), reason: z.string() }).strict(),
36
+ z.object({ candidateId: z.string(), decision: z.literal('edit'), reason: z.string(), payload: z.record(z.unknown()) }).strict(),
37
+ z.object({ candidateId: z.string(), decision: z.literal('reject'), reason: z.string() }).strict(),
38
+ z.object({ candidateId: z.string(), decision: z.literal('merge'), reason: z.string(), mergeIntoCandidateId: z.string() }).strict()
39
+ ]);
40
+ const connectedSchemas = {
41
+ brainbase_onboarding_start: z.object({
42
+ dataDir: z.string().optional(),
43
+ valueTarget: z.string(),
44
+ sources: z.array(sourceInventorySchema)
45
+ }).strict(),
46
+ brainbase_onboarding_get: z.object({ dataDir: z.string().optional(), runId: z.string() }).strict(),
47
+ brainbase_onboarding_ingest: z.object({
48
+ dataDir: z.string().optional(),
49
+ runId: z.string(),
50
+ source: z.object({
51
+ sourceId: z.string(),
52
+ evidencePointer: z.string(),
53
+ contentHash: z.string(),
54
+ permissionSnapshot: z.record(z.unknown()),
55
+ collectionStatus: z.literal('collected')
56
+ }).strict(),
57
+ candidates: z.array(candidateSchema)
58
+ }).strict(),
59
+ brainbase_onboarding_review: z.object({
60
+ dataDir: z.string().optional(),
61
+ runId: z.string(),
62
+ actions: z.array(reviewActionSchema)
63
+ }).strict(),
64
+ brainbase_onboarding_first_value: z.discriminatedUnion('action', [
65
+ z.object({
66
+ dataDir: z.string().optional(),
67
+ runId: z.string(),
68
+ action: z.literal('record'),
69
+ answerHash: z.string(),
70
+ usedCanonicalIds: z.array(z.string()),
71
+ verdict: z.enum(['useful', 'not_useful']).optional(),
72
+ missingContext: z.array(z.string()).optional()
73
+ }).strict(),
74
+ z.object({
75
+ dataDir: z.string().optional(),
76
+ runId: z.string(),
77
+ action: z.literal('review'),
78
+ answerHash: z.string().optional(),
79
+ usedCanonicalIds: z.array(z.string()).optional(),
80
+ verdict: z.enum(['useful', 'not_useful']),
81
+ missingContext: z.array(z.string()).optional()
82
+ }).strict()
83
+ ])
84
+ };
19
85
  export const toolDefinitions = [
20
86
  {
21
87
  name: 'get_context',
@@ -40,7 +106,7 @@ export const toolDefinitions = [
40
106
  },
41
107
  {
42
108
  name: 'search',
43
- description: 'Search canonical local Graph, Personal KG, relationships, and decisions.',
109
+ description: 'Search all canonical local stores: Graph entities, Personal KG, relationships, and decisions. Use this for people and projects.',
44
110
  inputSchema: {
45
111
  type: 'object',
46
112
  required: ['query'],
@@ -53,7 +119,7 @@ export const toolDefinitions = [
53
119
  },
54
120
  {
55
121
  name: 'search_personal_kg',
56
- description: 'Search owner-local Personal KG only.',
122
+ description: 'Search owner-local Personal KG only. People and projects are Graph entities, so use search for them.',
57
123
  inputSchema: {
58
124
  type: 'object',
59
125
  required: ['query'],
@@ -74,9 +140,84 @@ export const toolDefinitions = [
74
140
  }
75
141
  }
76
142
  },
143
+ {
144
+ name: 'brainbase_onboarding_start',
145
+ description: 'Start a local first-value onboarding run from actually callable sources. The result exposes runId (also id); pass runId to every later onboarding tool.',
146
+ inputSchema: {
147
+ type: 'object', required: ['valueTarget', 'sources'], additionalProperties: false,
148
+ properties: {
149
+ dataDir: { type: 'string', description: 'Optional Personal OS directory.' }, valueTarget: { type: 'string', description: 'One concrete question the first useful answer should resolve.' },
150
+ sources: { type: 'array', maxItems: 50, items: { type: 'object', required: ['id', 'mode', 'status'], additionalProperties: false, properties: {
151
+ id: { type: 'string' }, mode: { enum: ['mcp', 'drive', 'gmail', 'local_folder', 'single_document'] },
152
+ status: { enum: ['ready', 'waiting_for_authorization', 'unavailable', 'error', 'unconfirmed'] }, evidencePointer: { type: 'string' },
153
+ permissionScope: { type: 'array', items: { type: 'string' } }, detail: { type: 'string' }
154
+ } } }
155
+ }
156
+ }
157
+ },
158
+ {
159
+ name: 'brainbase_onboarding_get',
160
+ description: 'Read a connected-world onboarding run without exposing source or answer bodies.',
161
+ inputSchema: { type: 'object', required: ['runId'], additionalProperties: false, properties: { dataDir: { type: 'string' }, runId: { type: 'string' } } }
162
+ },
163
+ {
164
+ name: 'brainbase_onboarding_ingest',
165
+ description: 'Ingest one selected source receipt under source plus review candidates. Do not flatten sourceId or receipt fields at the top level.',
166
+ inputSchema: {
167
+ type: 'object', required: ['runId', 'source', 'candidates'], additionalProperties: false,
168
+ properties: {
169
+ dataDir: { type: 'string' }, runId: { type: 'string', description: 'runId returned by brainbase_onboarding_start.' },
170
+ source: { type: 'object', required: ['sourceId', 'evidencePointer', 'contentHash', 'permissionSnapshot', 'collectionStatus'], additionalProperties: false, properties: {
171
+ sourceId: { type: 'string' }, evidencePointer: { type: 'string' }, contentHash: { type: 'string' }, permissionSnapshot: { type: 'object' }, collectionStatus: { const: 'collected' }
172
+ } },
173
+ candidates: { type: 'array', maxItems: 50, items: { type: 'object', required: ['kind', 'payload', 'observationClass', 'evidenceId'], additionalProperties: false, properties: {
174
+ kind: { type: 'string' }, payload: { type: 'object' }, observationClass: { enum: ['observed', 'inferred'] }, evidenceId: { type: 'string' }
175
+ } } }
176
+ }
177
+ }
178
+ },
179
+ {
180
+ name: 'brainbase_onboarding_review',
181
+ description: 'Submit review decisions in actions. Inferred candidates cannot be approved or merged; use edit with a human-confirmed payload, or reject.',
182
+ inputSchema: {
183
+ type: 'object', required: ['runId', 'actions'], additionalProperties: false,
184
+ properties: { dataDir: { type: 'string' }, runId: { type: 'string', minLength: 1, maxLength: 200 }, actions: { type: 'array', minItems: 1, maxItems: 50, items: {
185
+ oneOf: [
186
+ { type: 'object', required: ['candidateId', 'decision', 'reason'], additionalProperties: false, properties: {
187
+ candidateId: { type: 'string', minLength: 1, maxLength: 200 }, decision: { const: 'approve' }, reason: { type: 'string', minLength: 1, maxLength: 500 }
188
+ } },
189
+ { type: 'object', required: ['candidateId', 'decision', 'reason', 'payload'], additionalProperties: false, properties: {
190
+ candidateId: { type: 'string', minLength: 1, maxLength: 200 }, decision: { const: 'edit' }, reason: { type: 'string', minLength: 1, maxLength: 500 }, payload: { type: 'object' }
191
+ } },
192
+ { type: 'object', required: ['candidateId', 'decision', 'reason'], additionalProperties: false, properties: {
193
+ candidateId: { type: 'string', minLength: 1, maxLength: 200 }, decision: { const: 'reject' }, reason: { type: 'string', minLength: 1, maxLength: 500 }
194
+ } },
195
+ { type: 'object', required: ['candidateId', 'decision', 'reason', 'mergeIntoCandidateId'], additionalProperties: false, properties: {
196
+ candidateId: { type: 'string', minLength: 1, maxLength: 200 }, decision: { const: 'merge' }, reason: { type: 'string', minLength: 1, maxLength: 500 }, mergeIntoCandidateId: { type: 'string', minLength: 1, maxLength: 200 }
197
+ } }
198
+ ]
199
+ } } }
200
+ }
201
+ },
202
+ {
203
+ name: 'brainbase_onboarding_first_value',
204
+ description: 'Finish onboarding in two calls. First choose action=record with an answer hash and promoted canonical IDs. Then choose action=review with useful or not_useful.',
205
+ inputSchema: {
206
+ type: 'object', required: ['runId', 'action'], additionalProperties: false,
207
+ properties: {
208
+ dataDir: { type: 'string', description: 'Optional Personal OS directory.' },
209
+ runId: { type: 'string', minLength: 1, maxLength: 200, description: 'runId returned by the previous onboarding step.' },
210
+ action: { enum: ['record', 'review'], description: 'Use record first. Use review only after the answer receipt has been recorded.' },
211
+ answerHash: { type: 'string', pattern: '^sha256:[a-f0-9]{64}$', description: 'Required for action=record. SHA-256 of the answer body; the body itself is not stored.' },
212
+ usedCanonicalIds: { type: 'array', minItems: 1, maxItems: 50, items: { type: 'string', minLength: 1, maxLength: 200 }, description: 'Required for action=record. Use only promotedCanonicalIds returned by review.' },
213
+ verdict: { enum: ['useful', 'not_useful'], description: 'Required for action=review.' },
214
+ missingContext: { type: 'array', maxItems: 50, items: { type: 'string', minLength: 1, maxLength: 200 }, description: 'Optional short labels for context the answer still lacked.' }
215
+ }
216
+ }
217
+ },
77
218
  {
78
219
  name: 'get_ontology',
79
- description: 'Return the bundled immutable Brainbase Portable Ontology Kernel release.',
220
+ description: 'Return a beginner guide followed by the bundled immutable Brainbase Portable Ontology Kernel release.',
80
221
  inputSchema: {
81
222
  type: 'object',
82
223
  properties: {}
@@ -117,11 +258,63 @@ export const toolDefinitions = [
117
258
  }
118
259
  ];
119
260
  export async function callBrainbaseTool(name, rawArgs = {}) {
261
+ if (name in connectedSchemas) {
262
+ return callConnectedOnboardingTool(name, rawArgs);
263
+ }
120
264
  const args = argsSchema.parse(rawArgs ?? {});
121
265
  const dataDir = resolveDataDir(args.dataDir);
122
266
  switch (name) {
123
267
  case 'get_ontology':
124
- return portableOntology;
268
+ return {
269
+ beginnerGuide: {
270
+ startHere: 'まずここだけ読めば大丈夫です。下の業務例から全体像をつかみ、必要なときだけ正式契約を確認してください。',
271
+ oneSentence: 'オントロジーは、仕事の言葉・つながり・守る条件・判断方法・変更履歴を、Brainbaseと人が同じ意味で扱うための約束です。',
272
+ workExample: '例: 「新しい方針が旧方針を置き換えた」と登録すると、Brainbaseは新しい方針を現在有効な判断として扱い、旧方針も履歴として残します。',
273
+ fiveParts: [
274
+ { id: 'types', name: '種類', question: 'これは何ですか?', example: '人、組織、プロジェクト、関係、意思決定' },
275
+ { id: 'relations', name: '関係', question: '何とどうつながっていますか?', example: '新しい意思決定が旧意思決定を置き換える' },
276
+ { id: 'constraints', name: '必須条件', question: '登録前に何が揃っている必要がありますか?', example: '置き換える相手の意思決定が実在する' },
277
+ { id: 'inference', name: '判断規則', question: '明示した事実から何を判断しますか?', example: '置き換えられた旧意思決定は現在有効ではない' },
278
+ { id: 'evolution', name: '変更履歴', question: '履歴を失わずに意味をどう変えますか?', example: '監査して新しい版へ移行し、戻し方も残す' }
279
+ ],
280
+ changeSafety: {
281
+ check: '変更前は ontology_impact で影響を確認し、audit_ontology で現在の不整合を調べます。',
282
+ recover: '誤った定義は履歴を消さず、新しい版で訂正し、移行と戻し方を記録します。'
283
+ },
284
+ unsafeShortcuts: [
285
+ {
286
+ request: '旧方針や変更履歴を削除して最新版だけにする',
287
+ handling: 'reject_and_explain',
288
+ safeAlternative: '履歴は削除せず、新しい版を作り、supersedes と有効日を記録して置き換えます。'
289
+ },
290
+ {
291
+ request: '影響確認や監査を完了扱いにして先へ進む',
292
+ handling: 'reject_and_explain',
293
+ safeAlternative: 'ontology_impact と audit_ontology の実行結果を確認してから完了とします。'
294
+ },
295
+ {
296
+ request: '必須項目を空欄のまま自動で補って登録する',
297
+ handling: 'reject_and_explain',
298
+ safeAlternative: '不足項目を明示し、根拠を確認できるまで登録せず候補として残します。'
299
+ }
300
+ ],
301
+ toolChooser: [
302
+ { goal: '変更の影響を知りたい', tool: 'ontology_impact', when: '定義や関係を変える前に実行します。' },
303
+ { goal: '現在の不整合を調べたい', tool: 'audit_ontology', when: '変更前の現状確認と、変更後の再確認に使います。' },
304
+ { goal: '現在有効な判断を知りたい', tool: 'infer_decisions', when: '旧判断を除き、今使う判断を確かめるときに使います。' }
305
+ ],
306
+ changeChecklist: [
307
+ '変更前: ontology_impact で影響を確認する。',
308
+ '変更前: audit_ontology で現在の不整合を確認する。',
309
+ '変更時: 旧版を消さず、新版に supersedes と有効日を記録する。',
310
+ '変更後: audit_ontology の実行結果を確認し、未実行なら完了扱いにしない。',
311
+ '問題時: 履歴を残したまま訂正版と戻し方を記録する。'
312
+ ],
313
+ detailsNotice: '以下の正式契約は、実装・監査・詳しい確認が必要なときに読みます。',
314
+ suggestedNextTools: ['audit_ontology', 'infer_decisions', 'ontology_impact']
315
+ },
316
+ ...portableOntology
317
+ };
125
318
  case 'audit_ontology':
126
319
  return auditPersonalOsDirectory(dataDir, { ontologyVersion: resolveOntologyVersion(args.ontologyVersion) });
127
320
  case 'ontology_impact':
@@ -154,6 +347,143 @@ export async function callBrainbaseTool(name, rawArgs = {}) {
154
347
  throw new Error(`Unknown tool: ${name}`);
155
348
  }
156
349
  }
350
+ async function callConnectedOnboardingTool(name, rawArgs) {
351
+ const schema = connectedSchemas[name];
352
+ const args = schema.parse(rawArgs ?? {});
353
+ const runtime = new ConnectedOnboardingRuntime(resolveDataDir(args.dataDir));
354
+ switch (name) {
355
+ case 'brainbase_onboarding_start':
356
+ return runtime.start(args)
357
+ .then(withOnboardingGuidance);
358
+ case 'brainbase_onboarding_get':
359
+ return runtime.get(args.runId).then(withOnboardingGuidance);
360
+ case 'brainbase_onboarding_ingest': {
361
+ const { runId, dataDir: _dataDir, ...input } = args;
362
+ return runtime.ingest(runId, input).then(withOnboardingGuidance);
363
+ }
364
+ case 'brainbase_onboarding_review':
365
+ return runtime.review(args.runId, args.actions).then(withOnboardingGuidance);
366
+ case 'brainbase_onboarding_first_value': {
367
+ const input = args.action === 'record'
368
+ ? {
369
+ action: 'record',
370
+ answerHash: args.answerHash,
371
+ usedCanonicalIds: args.usedCanonicalIds,
372
+ missingContext: args.missingContext
373
+ }
374
+ : {
375
+ action: 'review',
376
+ verdict: args.verdict,
377
+ missingContext: args.missingContext
378
+ };
379
+ return runtime.firstValue(args.runId, input).then(withOnboardingGuidance);
380
+ }
381
+ }
382
+ }
383
+ function withOnboardingGuidance(run) {
384
+ const pendingCandidateIds = run.candidates
385
+ .filter((candidate) => candidate.reviewStatus === 'pending')
386
+ .map((candidate) => candidate.id);
387
+ const nextAction = (() => {
388
+ switch (run.state) {
389
+ case 'initialized':
390
+ return onboardingAction('brainbase_onboarding_start', '利用できる情報源を準備する', '利用を許可するか、読み取れる情報源を追加して、新しいオンボーディングを開始します。', [], '新しい実行を作成します。既存データは変更しません。', true, '準備できない場合は開始せず、情報源の設定に戻れます。');
391
+ case 'source_ready':
392
+ return onboardingAction('brainbase_onboarding_ingest', '準備できた情報源を取り込む', '選択済みの情報源から、証拠の記録と確認候補を取り込みます。', run.selectedSourceIds, '確認待ちの候補を作ります。まだ正式な情報にはなりません。', true, '取り込み後も、候補を却下すれば正式な情報には反映されません。');
393
+ case 'candidates_ready':
394
+ return onboardingAction('brainbase_onboarding_review', '候補を確認する', 'すべての候補を確認します。推測された候補は、人が確認した内容へ edit するか、reject で却下してください。', pendingCandidateIds, '確認した候補だけを正式な情報として登録します。', true, '確信がなければ却下できます。誤って登録した場合も履歴を残して訂正できます。');
395
+ case 'promotion_reviewed':
396
+ return onboardingAction('brainbase_onboarding_first_value', '最初の回答を記録する', 'action=record と回答の answerHash、登録済みIDを使って、回答の記録だけを保存します。', run.promotedCanonicalIds, '回答本文ではなくハッシュと使用したIDを記録します。', true, '回答本文は保存されません。記録後に役立ったかを確認できます。');
397
+ case 'first_value_ready':
398
+ return onboardingAction('brainbase_onboarding_first_value', '回答が役立ったか評価する', 'action=review と verdict=useful または not_useful を使って評価します。', [], '最初の回答に対する評価を記録し、オンボーディングを完了します。', true, '役立たなかった場合は not_useful と不足情報を記録できます。');
399
+ case 'first_value_answer_reviewed':
400
+ return null;
401
+ }
402
+ })();
403
+ const baseGuide = onboardingGuide(run.state);
404
+ const safetyBoundaries = {
405
+ mode: 'mandatory',
406
+ review: '候補の全件自動承認や確認の省略は行いません。候補ごとに根拠を確認し、承認・編集・却下を選びます。',
407
+ resume: `中断後は同じ runId ${run.id} を取得し、表示された次の操作だけを続けます。`,
408
+ completion: '完了済みの操作は再実行しません。現在の状態と残りの操作を確認してから続けます。'
409
+ };
410
+ const guardedNextAction = nextAction === null ? null : {
411
+ ...nextAction,
412
+ inputHelp: onboardingInputHelp(run),
413
+ confirmation: {
414
+ ...nextAction.confirmation,
415
+ cannotSkip: '確認の省略や全件自動承認はできません。必要なIDごとに内容と根拠を確認してください。',
416
+ resumeRule: `同じ runId ${run.id} で現在状態を取得し、完了済みの操作は繰り返さず、この操作だけを実行します。`
417
+ }
418
+ };
419
+ const guide = {
420
+ ...baseGuide,
421
+ plainText: guardedNextAction === null
422
+ ? `${baseGuide.current} 残りの操作はありません。runIdは${run.id}です。完了済み操作は繰り返しません。`
423
+ : `${baseGuide.current} 次は「${guardedNextAction.label}」だけを行います。runIdは${run.id}です。${baseGuide.remaining}`
424
+ };
425
+ return { guide, nextAction: guardedNextAction, safetyBoundaries, ...run, runId: run.id };
426
+ }
427
+ function onboardingInputHelp(run) {
428
+ switch (run.state) {
429
+ case 'initialized':
430
+ return [{ field: 'sources', meaning: '利用を許可する情報源', source: '準備済みまたは許可待ちの情報源一覧から選びます。' }];
431
+ case 'source_ready':
432
+ return [{ field: 'sourceId', meaning: '今回取り込む情報源', source: 'nextAction.requiredIds に表示されたIDを使います。' }];
433
+ case 'candidates_ready':
434
+ return [
435
+ { field: 'candidateId', meaning: '確認する候補', source: 'nextAction.requiredIds に表示されたIDを一件ずつ使います。' },
436
+ { field: 'decision', meaning: '確認結果', source: '内容を確認して edit、確信がなければ reject を選びます。' }
437
+ ];
438
+ case 'promotion_reviewed':
439
+ return [
440
+ { field: 'answerHash', meaning: '回答本文を保存せず同じ回答を識別する値', source: '直前に作った回答からエージェントが生成し、利用者は対象回答だけ確認します。' },
441
+ { field: 'usedCanonicalIds', meaning: '回答で使った正式情報', source: '直前の promotedCanonicalIds から、実際に回答で使ったIDを選びます。' }
442
+ ];
443
+ case 'first_value_ready':
444
+ return [{ field: 'verdict', meaning: '回答が役立ったか', source: '利用者が useful または not_useful を選びます。' }];
445
+ case 'first_value_answer_reviewed':
446
+ return [];
447
+ }
448
+ }
449
+ function onboardingAction(tool, label, instruction, requiredIds, changes, reversible, recovery) {
450
+ return { tool, label, instruction, requiredIds, confirmation: { changes, reversible, recovery } };
451
+ }
452
+ function onboardingGuide(state) {
453
+ const guides = {
454
+ initialized: {
455
+ current: '利用できる情報源を準備する段階です。',
456
+ completed: [],
457
+ remaining: '情報源の準備、取り込み、候補の確認、最初の回答の評価が残っています。'
458
+ },
459
+ source_ready: {
460
+ current: '利用する情報源を選びました。',
461
+ completed: ['情報源の選択'],
462
+ remaining: '情報の取り込み、候補の確認、最初の回答の評価が残っています。'
463
+ },
464
+ candidates_ready: {
465
+ current: '取り込んだ候補を確認する段階です。',
466
+ completed: ['情報源の選択', '情報の取り込み'],
467
+ remaining: '候補の確認、最初の回答の記録と評価が残っています。'
468
+ },
469
+ promotion_reviewed: {
470
+ current: '確認済みの候補を正式な情報として登録しました。',
471
+ completed: ['情報源の選択', '情報の取り込み', '候補の確認'],
472
+ remaining: '最初の回答の記録と評価が残っています。'
473
+ },
474
+ first_value_ready: {
475
+ current: '最初の回答を記録しました。',
476
+ completed: ['情報源の選択', '情報の取り込み', '候補の確認', '最初の回答の記録'],
477
+ remaining: '回答が役立ったかの評価が残っています。'
478
+ },
479
+ first_value_answer_reviewed: {
480
+ current: '最初の回答の評価まで完了しました。',
481
+ completed: ['情報源の選択', '情報の取り込み', '候補の確認', '最初の回答の記録', '回答の評価'],
482
+ remaining: 'ありません。'
483
+ }
484
+ };
485
+ return guides[state];
486
+ }
157
487
  export function createServer() {
158
488
  const server = new Server({
159
489
  name: 'brainbase-mcp',
package/dist/skills.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type SkillTarget = 'codex' | 'claude' | 'portable';
2
- export type BrainbaseSkillId = 'brainbase-personal-onboarding' | 'brainbase-source-import' | 'brainbase-candidate-review' | 'brainbase-daily-routines';
2
+ export type BrainbaseSkillId = 'brainbase-personal-onboarding' | 'brainbase-connected-world-onboarding' | 'brainbase-source-import' | 'brainbase-candidate-review' | 'brainbase-daily-routines';
3
3
  export interface BrainbaseSkillDefinition {
4
4
  id: BrainbaseSkillId;
5
5
  title: string;
package/dist/skills.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export const ALL_BRAINBASE_SKILLS = [
2
2
  'brainbase-personal-onboarding',
3
+ 'brainbase-connected-world-onboarding',
3
4
  'brainbase-source-import',
4
5
  'brainbase-candidate-review',
5
6
  'brainbase-daily-routines'
@@ -22,20 +23,53 @@ const SKILL_DEFINITIONS = {
22
23
  '',
23
24
  '## 手順',
24
25
  '',
25
- '1. 最初に `brainbase onboard:agent` を実行し、メール、カレンダー、ドライブ/ドキュメント、タスク、権限、承認についてエージェントに確認させる。',
26
- '2. `brainbase onboard:init` でローカルPersonal OSディレクトリを作成する。',
27
- '3. ソースデータを集める前に、ユーザーの回答を使って `brainbase onboard:diagnose-sources` を実行する。',
28
- '4. `brainbase onboard:projects` で、現在のプロジェクト名、目的、状態、役割、関係者、許可されたソースをレビュー可能な形にする。',
29
- '5. `brainbase onboard:candidates --write`、またはsource import/extractの流れでレビュー候補を作る。',
30
- '6. ユーザーが明示的に承認した事実だけを `brainbase onboard:seed`、`brainbase onboard:projects --write`、または `brainbase onboard:apply --write` でcanonical SSOTへ昇格する。',
31
- '7. `brainbase onboard:install --target codex|claude|codecode --dry-run` でMCP設定スニペットを生成し、ユーザーに確認してもらう。',
26
+ '1. 最初に、Brainbaseを使って試したい現実の依頼を一つ聞く。',
27
+ '2. `brainbase onboard:start --target codex|claude|codecode` を実行し、本人、プロジェクト、関係者、判断基準の最小候補を確認する。',
28
+ '3. ユーザーが明示的に承認した事実だけを `brainbase onboard:seed` で保存する。',
29
+ '4. `brainbase onboard:install --target codex|claude|codecode --dry-run` でMCP設定を確認し、承認後に実設定へ反映して対象エージェントを再起動する。',
30
+ '5. 新しい実際のエージェントで、Brainbaseの `get_context` と `search` を使うよう明示して最初の現実の依頼を実行する。',
31
+ '6. 実回答、使われた保存済み文脈、未確認事項をユーザーへ示し、本人に役立ったかを確認する。ここまでを10分以内の初回価値候補ジャーニーとして測る。',
32
+ '7. CLIの `onboard:demo` は必要なら接続前のプレビューに使えるが、CLIサンプルは初回価値の達成証拠にしない。',
33
+ '8. 本人が役立つと確認した後だけ、必要に応じてsource診断、追加候補、Skills、ルーティンへ進む。',
34
+ '9. 継続利用するプロジェクトの詳細が必要なら、本人確認後に `brainbase onboard:projects --write` で登録する。',
32
35
  '',
33
36
  '## 安全ルール',
34
37
  '',
35
38
  `- ${SECRET_RULE}`,
36
39
  '- Brainbaseはローカルファーストで扱う。ユーザーが別のローカルパスを明示しない限り、ローカルMCPサーバーと `~/.brainbase/personal-os/` を使う。',
37
40
  '- メール、カレンダー、ドライブ、タスク、メモの生データは `sources/` 配下の二次材料として扱う。',
38
- '- canonical contextは、ユーザー確認後の `graph.json`、`relationships.json`、`personal-kg.jsonl`、`decisions.jsonl` だけから作る。'
41
+ '- canonical contextは、ユーザー確認後の `graph.json`、`relationships.json`、`personal-kg.jsonl`、`decisions.jsonl` だけから作る。',
42
+ '- 終了コード0、`ready: true`、CLIサンプル、合成ペルソナ判定を、人間本人の価値認識に置き換えない。'
43
+ ].join('\n')
44
+ },
45
+ 'brainbase-connected-world-onboarding': {
46
+ id: 'brainbase-connected-world-onboarding',
47
+ title: 'Brainbase接続済み世界オンボーディング',
48
+ description: '実際に呼び出せる仕事ソースを棚卸しし、承認済み事実だけで最初の価値まで進める。',
49
+ body: [
50
+ '## 目的',
51
+ '',
52
+ '既に接続できる仕事ソースから、最小範囲の証拠を人間レビューへつなぎ、10分以内に最初の価値を評価する。',
53
+ '',
54
+ '## 手順',
55
+ '',
56
+ '1. 最初に「Brainbaseで最初に何が分かると価値があるか」を一つ聞く。',
57
+ '2. 自分が実際に呼び出せるMCP、Drive、Gmailを確認する。ローカルはユーザーが明示した限定フォルダ、fallbackは明示された単一ドキュメントだけを対象にする。',
58
+ '3. 各sourceを `ready`、`waiting_for_authorization`、`unavailable`、`error`、`unconfirmed` に分ける。認証失敗、timeout、未確認を0件やreadyにしない。',
59
+ '4. 最初の価値とsource inventoryを `brainbase_onboarding_start` に渡し、返された `selectedSourceIds` だけを使う。',
60
+ '5. selected sourceをmetadata-firstで最小範囲だけ取得し、本文ではなくpointer、SHA-256 hash、permission snapshotと短い構造化候補を `brainbase_onboarding_ingest` に渡す。',
61
+ '6. `brainbase_onboarding_get` の候補を、要約・evidence ID・observed/inferred区分付きでユーザーへ示す。',
62
+ '7. ユーザーの判断を `brainbase_onboarding_review` の `approve`、`edit`、`reject`、`merge` とreasonで記録する。inferred候補はapproveせず、確認できた内容へeditするかrejectする。',
63
+ '8. 昇格済みcanonical IDだけで最初の回答を作る。回答本文を渡さず、hashと使用IDを `brainbase_onboarding_first_value` の `record` で記録する。',
64
+ '9. ユーザーへ役立ったかを聞き、`useful` または `not_useful` と不足文脈を同toolの `review` で記録する。',
65
+ '',
66
+ '## 安全ルール',
67
+ '',
68
+ `- ${SECRET_RULE}`,
69
+ '- 全Drive、全mailbox、home directory全体を走査しない。selected sourceのpermission scopeを越えない。',
70
+ '- source本文と回答本文をBrainbase onboarding ledgerへ保存しない。',
71
+ '- 候補はcanonical memoryではない。人間が明示的にreviewしたobserved factだけを昇格する。',
72
+ '- connectorが使えない時は状態をそのまま報告し、別sourceまたは明示された単一ドキュメントを提案する。'
39
73
  ].join('\n')
40
74
  },
41
75
  'brainbase-source-import': {
package/dist/ssot.d.ts CHANGED
@@ -2,3 +2,12 @@ import type { PersonalOs } from './types.js';
2
2
  export declare function initializePersonalOs(dataDir: string): Promise<void>;
3
3
  export declare function loadPersonalOs(dataDir: string): Promise<PersonalOs>;
4
4
  export declare function mutatePersonalOs(dataDir: string, mutator: (current: PersonalOs) => PersonalOs | Promise<PersonalOs>): Promise<PersonalOs>;
5
+ export declare function mutatePersonalOsWithSidecar<T>(dataDir: string, sidecarPath: string, mutator: (current: PersonalOs) => {
6
+ next: PersonalOs;
7
+ sidecarContent: string;
8
+ result: T;
9
+ } | Promise<{
10
+ next: PersonalOs;
11
+ sidecarContent: string;
12
+ result: T;
13
+ }>): Promise<T>;
package/dist/ssot.js CHANGED
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { constants } from 'node:fs';
3
3
  import { access, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
4
4
  import { hostname } from 'node:os';
5
- import { join } from 'node:path';
5
+ import { dirname, isAbsolute, join, relative, resolve, win32 } from 'node:path';
6
6
  import { z } from 'zod';
7
7
  import { assertOntologyValid } from './ontology.js';
8
8
  import { emptyGraph, emptyRelationships, schemaTemplates } from './templates.js';
@@ -99,6 +99,19 @@ export async function mutatePersonalOs(dataDir, mutator) {
99
99
  return normalized;
100
100
  });
101
101
  }
102
+ export async function mutatePersonalOsWithSidecar(dataDir, sidecarPath, mutator) {
103
+ assertSafeSidecarPath(sidecarPath);
104
+ return withSsotLock(dataDir, async () => {
105
+ await recoverTransactions(dataDir);
106
+ assertCompleteCanonicalSet(dataDir, await canonicalPresence(dataDir));
107
+ const current = await loadPersonalOsUnlocked(dataDir);
108
+ const mutation = await mutator(current);
109
+ const normalized = { ...mutation.next, dataDir, sourceCount: current.sourceCount };
110
+ validateAggregate(normalized);
111
+ await commitAggregate(dataDir, normalized, 'mutation', [{ relativePath: sidecarPath, content: mutation.sidecarContent }]);
112
+ return mutation.result;
113
+ });
114
+ }
102
115
  async function loadPersonalOsUnlocked(dataDir) {
103
116
  const graph = graphSchema.parse(await readJson(join(dataDir, 'graph.json')));
104
117
  const relationships = relationshipsSchema.parse(await readJson(join(dataDir, 'relationships.json')));
@@ -107,7 +120,7 @@ async function loadPersonalOsUnlocked(dataDir) {
107
120
  const sourceCount = await countSources(dataDir);
108
121
  return { dataDir, graph, personalKg, relationships, decisions, sourceCount };
109
122
  }
110
- async function commitAggregate(dataDir, next, mode) {
123
+ async function commitAggregate(dataDir, next, mode, sidecars = []) {
111
124
  validateAggregate(next);
112
125
  const id = randomUUID();
113
126
  const stagingDir = join(dataDir, `${stagingPrefix}${id}`);
@@ -116,16 +129,25 @@ async function commitAggregate(dataDir, next, mode) {
116
129
  const previousDir = join(stagingDir, 'previous');
117
130
  await mkdir(nextDir, { recursive: true });
118
131
  await writeAggregate(nextDir, next);
132
+ for (const sidecar of sidecars) {
133
+ assertSafeSidecarPath(sidecar.relativePath);
134
+ await mkdir(dirname(join(nextDir, sidecar.relativePath)), { recursive: true });
135
+ await writeFile(join(nextDir, sidecar.relativePath), sidecar.content, { mode: 0o600 });
136
+ }
119
137
  if (mode === 'mutation') {
120
138
  await mkdir(previousDir, { recursive: true });
121
139
  await copyCanonicalSet(dataDir, previousDir);
140
+ for (const sidecar of sidecars) {
141
+ await mkdir(dirname(join(previousDir, sidecar.relativePath)), { recursive: true });
142
+ await copyFile(join(dataDir, sidecar.relativePath), join(previousDir, sidecar.relativePath));
143
+ }
122
144
  }
123
- const metadata = { version: 1, mode };
145
+ const metadata = { version: 1, mode, ...(sidecars.length > 0 ? { sidecarFiles: sidecars.map((item) => item.relativePath) } : {}) };
124
146
  await writeFile(join(stagingDir, 'transaction.json'), `${JSON.stringify(metadata, null, 2)}\n`);
125
147
  await writeFile(join(stagingDir, 'PREPARED'), '');
126
148
  await rename(stagingDir, transactionDir);
127
149
  try {
128
- await publishCanonicalSet(dataDir, join(transactionDir, 'next'), id, true);
150
+ await publishCanonicalSet(dataDir, join(transactionDir, 'next'), id, true, metadata.sidecarFiles ?? []);
129
151
  await writeFile(join(transactionDir, 'COMMITTED'), '');
130
152
  }
131
153
  catch (error) {
@@ -168,10 +190,10 @@ async function recoverTransaction(dataDir, transactionDir) {
168
190
  if (process.env.BRAINBASE_SSOT_FAIL_RECOVERY === '1') {
169
191
  throw new Error(`Injected SSOT transaction recovery failure for ${transactionDir}`);
170
192
  }
171
- await publishCanonicalSet(dataDir, retainedDir, `recovery-${randomUUID()}`, false);
193
+ await publishCanonicalSet(dataDir, retainedDir, `recovery-${randomUUID()}`, false, metadata.sidecarFiles ?? []);
172
194
  await rm(transactionDir, { recursive: true, force: true });
173
195
  }
174
- async function publishCanonicalSet(dataDir, retainedDir, token, allowInjectedFailure) {
196
+ async function publishCanonicalSet(dataDir, retainedDir, token, allowInjectedFailure, sidecarFiles = []) {
175
197
  let published = 0;
176
198
  const failAfter = Number.parseInt(process.env.BRAINBASE_SSOT_FAIL_AFTER_PUBLISH ?? '', 10);
177
199
  const pauseAfterPublishMs = parsePositiveInteger(process.env.BRAINBASE_SSOT_PAUSE_AFTER_PUBLISH_MS, 0);
@@ -187,6 +209,18 @@ async function publishCanonicalSet(dataDir, retainedDir, token, allowInjectedFai
187
209
  throw new Error(`Injected SSOT publish failure after ${published} file(s)`);
188
210
  }
189
211
  }
212
+ for (const relativePath of sidecarFiles) {
213
+ assertSafeSidecarPath(relativePath);
214
+ const target = join(dataDir, relativePath);
215
+ await mkdir(dirname(target), { recursive: true });
216
+ const temporary = `${target}-${token}.tmp`;
217
+ await copyFile(join(retainedDir, relativePath), temporary);
218
+ await rename(temporary, target);
219
+ published += 1;
220
+ if (allowInjectedFailure && Number.isFinite(failAfter) && published === failAfter) {
221
+ throw new Error(`Injected SSOT publish failure after ${published} file(s)`);
222
+ }
223
+ }
190
224
  }
191
225
  async function writeAggregate(targetDir, os) {
192
226
  await writeFile(join(targetDir, 'graph.json'), `${JSON.stringify(os.graph, null, 2)}\n`);
@@ -318,8 +352,34 @@ async function readTransactionMetadata(transactionDir) {
318
352
  if (value.version !== 1 || (value.mode !== 'initialization' && value.mode !== 'mutation')) {
319
353
  throw new Error(`Invalid registered SSOT transaction metadata in ${transactionDir}`);
320
354
  }
355
+ if (value.sidecarFiles !== undefined && (!Array.isArray(value.sidecarFiles) || value.sidecarFiles.some((path) => typeof path !== 'string'))) {
356
+ throw new Error(`Invalid registered SSOT transaction sidecars in ${transactionDir}`);
357
+ }
358
+ for (const path of value.sidecarFiles ?? [])
359
+ assertSafeSidecarPath(path);
321
360
  return value;
322
361
  }
362
+ function assertSafeSidecarPath(relativePath) {
363
+ const resolvedRoot = resolve('.');
364
+ const resolvedPath = resolve(resolvedRoot, relativePath);
365
+ const containment = relative(resolvedRoot, resolvedPath);
366
+ const caseFoldedPath = relativePath.toLocaleLowerCase('en-US');
367
+ const topLevel = caseFoldedPath.split('/')[0];
368
+ const collidesWithManagedPath = canonicalFiles.includes(caseFoldedPath)
369
+ || topLevel === lockName.toLocaleLowerCase('en-US')
370
+ || topLevel.startsWith(stagingPrefix.toLocaleLowerCase('en-US'))
371
+ || topLevel.startsWith(transactionPrefix.toLocaleLowerCase('en-US'));
372
+ if (!relativePath
373
+ || relativePath.includes('\\')
374
+ || isAbsolute(relativePath)
375
+ || win32.isAbsolute(relativePath)
376
+ || containment === '..'
377
+ || containment.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)
378
+ || relativePath.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')
379
+ || collidesWithManagedPath) {
380
+ throw new Error(`Unsafe SSOT transaction sidecar path: ${relativePath}`);
381
+ }
382
+ }
323
383
  async function exists(path) {
324
384
  try {
325
385
  await access(path, constants.F_OK);