@unson/brainbase-mcp 0.41.13 → 0.42.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.
@@ -26,6 +26,8 @@ import { createMeetingMinutesUI } from './meeting-minutes.js';
26
26
  import { createMeetingMinutesStorageExtension } from './meeting-minutes-storage.js';
27
27
  import { createMeetingMinutesLineageExtension } from './meeting-minutes-lineage-http.js';
28
28
  import { createWorldModelView } from './world-model-view.js';
29
+ import { createPersonalMemoryUI, parsePersonalMemoryRoute, personalMemoryRouteTarget } from './personal-memory.js';
30
+ import { createPersonalMemoryHttpPort } from './personal-memory-http-port.js';
29
31
  import { createGraphClient } from './graph-view-shared.js';
30
32
  import { installNativeScrollbars } from './native-scrollbars.js';
31
33
  import { browserAreaStorage, createAreaMemory, createAreaNav } from './area-nav.js';
@@ -53,6 +55,8 @@ const NAV_ICON_PATHS = Object.freeze({
53
55
  minutes: ['M6 3.5h9l3 3V20.5H6z', 'M15 3.5v4h3', 'M9 12h6m-6 3h6'],
54
56
  // Two people (the organization edition's 人と会社 uses the same shape).
55
57
  people: ['M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z', 'M2.5 20a6.5 6.5 0 0 1 13 0', 'M16 4.5a3.5 3.5 0 0 1 0 6.5', 'M18 14a6.5 6.5 0 0 1 3.5 6'],
58
+ // あなたの記憶 (ledger D15): one person and a lock, seen only by the owner.
59
+ memory: ['M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z', 'M3.5 20a6.5 6.5 0 0 1 10-5.5', 'M15 15h6v5h-6z', 'M16.5 15v-1.5a1.5 1.5 0 0 1 3 0V15'],
56
60
  // Kinds of information and how they connect (settings, ledger D14): three linked boxes.
57
61
  ontology: ['M4 4h6v6H4z', 'M14 14h6v6h-6z', 'M14 4h6v6h-6z', 'M10 7h4', 'M17 10v4'],
58
62
  // World view (extension id `world`, ledger P17).
@@ -75,7 +79,7 @@ const NAV_AREAS = Object.freeze([
75
79
  Object.freeze({ id: 'purpose', label: null, order: ['objectives'], icon: 'objectives' }),
76
80
  Object.freeze({ id: 'work', label: '仕事', order: ['projects', 'minutes'], defaultScreen: 'projects', icon: 'area:work' }),
77
81
  Object.freeze({ id: 'outcomes', label: '成果', order: ['value-proofs'], icon: 'value-proofs' }),
78
- Object.freeze({ id: 'information', label: '情報と関係', order: ['graph', 'people'], defaultScreen: 'graph', icon: 'graph' }),
82
+ Object.freeze({ id: 'information', label: '情報と関係', order: ['graph', 'people', 'memory'], defaultScreen: 'graph', icon: 'graph' }),
79
83
  Object.freeze({ id: 'settings', label: '設定', order: ['ontology'], defaultScreen: 'ontology', icon: 'area:settings' }),
80
84
  ]);
81
85
 
@@ -163,11 +167,14 @@ export function parseLocalWebTarget(target) {
163
167
  // プロジェクトと関係者 one project (`project` = Graph project id); other screens ignore them.
164
168
  const decisionId = screenId === 'value-proofs' ? params.get('decision')?.trim() || null : null;
165
169
  const projectId = screenId === 'projects' ? params.get('project')?.trim() || null : null;
170
+ // あなたの記憶 keeps its filters and the opened memory (never the search words).
171
+ const memoryRoute = screenId === 'memory' ? parsePersonalMemoryRoute(params) : null;
166
172
  return {
167
173
  screenId,
168
174
  entityId: screenId === 'graph' ? params.get('entity_id')?.trim() || null : null,
169
175
  ...(decisionId ? { decisionId } : {}),
170
176
  ...(projectId ? { projectId } : {}),
177
+ ...(memoryRoute ? { memoryRoute } : {}),
171
178
  };
172
179
  }
173
180
 
@@ -228,6 +235,26 @@ function judgmentReferenceLabelReader(context) {
228
235
  };
229
236
  }
230
237
 
238
+ function personalMemoryPort(context) {
239
+ return createPersonalMemoryHttpPort({ fetcher: context.fetcher, token: context.token });
240
+ }
241
+
242
+ /** The opening of each used memory, for 判断の履歴's 本人の考え; the local host has one owner. */
243
+ function personalMemoryResolver(context) {
244
+ const port = personalMemoryPort(context);
245
+ return async (refs) => {
246
+ const list = await port.listMemories({});
247
+ if (list.status !== 'ok') throw new Error(list.reason);
248
+ const wanted = new Set(refs);
249
+ const found = new Map();
250
+ for (const item of list.items) {
251
+ if (!wanted.has(item.id)) continue;
252
+ found.set(item.id, { excerpt: item.text, href: `#${personalMemoryRouteTarget({ memoryId: item.id })}` });
253
+ }
254
+ return found;
255
+ };
256
+ }
257
+
231
258
  function mountToday(container, context) {
232
259
  return createJudgmentHistoryUI({
233
260
  root: makePage(container, context),
@@ -238,6 +265,23 @@ function mountToday(container, context) {
238
265
  token: context.token,
239
266
  basePath: '/api/judgment-history',
240
267
  resolveReferenceLabels: judgmentReferenceLabelReader(context),
268
+ resolvePersonalMemory: personalMemoryResolver(context),
269
+ });
270
+ }
271
+
272
+ function mountPersonalMemory(container, context) {
273
+ return createPersonalMemoryUI({
274
+ root: makePage(container, context),
275
+ rail: context.rail,
276
+ page: context.page,
277
+ document: context.document,
278
+ port: personalMemoryPort(context),
279
+ judgmentHref: (decisionId) => `#value-proofs?decision=${encodeURIComponent(decisionId)}`,
280
+ candidatesCommand: 'brainbase onboard:candidates',
281
+ route: context.initialEntityId ?? undefined,
282
+ onRouteChange: context.navigate
283
+ ? (route, options) => context.navigate(personalMemoryRouteTarget(route), options)
284
+ : undefined,
241
285
  });
242
286
  }
243
287
 
@@ -341,6 +385,8 @@ export const LOCAL_WEB_SCREENS = Object.freeze([
341
385
  onRouteChange: (entityId, options) => context.navigate?.(entityId ? `graph?entity_id=${encodeURIComponent(entityId)}` : 'graph', options),
342
386
  })) }),
343
387
  Object.freeze({ id: 'people', label: '人と会社', area: 'information', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('people', createPeopleAndCompaniesView, { canCorrect: true }) }),
388
+ // Only the owner sees it (ledger D15); the local host has one owner, so it is always listed here.
389
+ Object.freeze({ id: 'memory', label: 'あなたの記憶', area: 'information', usesGraph: false, rail: true, ownerOnly: true, source: '手元の個人KG', mount: mountPersonalMemory }),
344
390
  // The definitions behind 情報と関係: infrequent management work, so under settings (ledger D14).
345
391
  Object.freeze({ id: 'ontology', label: '情報の種類とつながり方', area: 'settings', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('ontology', createGraphOntologyView, { canCorrect: true }) }),
346
392
  ]);
@@ -428,10 +474,14 @@ export function createLocalWebShell({
428
474
  meetingMinutesLineage = null,
429
475
  areaMemory = createAreaMemory(browserAreaStorage()),
430
476
  onNavigate,
477
+ // A host that serves more than one person passes false for anyone but the owner:
478
+ // owner-only screens (あなたの記憶) are then neither listed nor mounted.
479
+ isOwner = true,
431
480
  } = {}) {
432
481
  if (!root) throw new TypeError('root is required');
433
482
  const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
434
483
  if (!doc || typeof doc.createElement !== 'function') throw new Error('document_unavailable');
484
+ screens = screens.filter((screen) => isOwner === true || screen.ownerOnly !== true);
435
485
  const request = typeof fetcher === 'function'
436
486
  ? fetcher
437
487
  : typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
@@ -522,6 +572,7 @@ export function createLocalWebShell({
522
572
  if (!entityId && screen.id === 'graph' && view?.state?.detail && typeof view.closeEntity === 'function') view.closeEntity();
523
573
  if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
524
574
  if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
575
+ if (entityId && screen.id === 'memory' && typeof view?.applyRoute === 'function') view.applyRoute(entityId);
525
576
  return;
526
577
  }
527
578
  const slot = slots.get(screen.id);
@@ -535,6 +586,7 @@ export function createLocalWebShell({
535
586
  const page = Object.freeze({ crumbs: Object.freeze(['あなたのBrainbase', screen.label]), source: screen.source ?? null });
536
587
  const view = screen.mount(slot, Object.freeze({ ...context, rail, page, initialEntityId: entityId })) ?? true;
537
588
  mounted.set(screen.id, view);
589
+ if (screen.id === 'memory' && typeof view?.load === 'function') void view.load();
538
590
  if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
539
591
  if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
540
592
  updateRail();
@@ -550,7 +602,9 @@ export function createLocalWebShell({
550
602
  const screen = screens.find((candidate) => candidate.id === parsed.screenId) ?? screens[0];
551
603
  pendingEntityIds.set(screen.id, screen.id === 'graph' ? parsed.entityId
552
604
  : screen.id === 'value-proofs' ? parsed.decisionId ?? null
553
- : screen.id === 'projects' ? parsed.projectId ?? null : null);
605
+ : screen.id === 'projects' ? parsed.projectId ?? null
606
+ // あなたの記憶 receives its whole route (filters and the opened memory).
607
+ : screen.id === 'memory' ? parsed.memoryRoute ?? null : null);
554
608
  active = screen.id;
555
609
  for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
556
610
  areaNav.show(screen.id);
@@ -0,0 +1,72 @@
1
+ /*
2
+ * The local Web host's port for 「あなたの記憶」 (ui/personal-memory.js).
3
+ *
4
+ * Every read carries the launch token: the memory bodies are private, so the
5
+ * host guards these reads as it guards writes. The list is read whole and
6
+ * filtered here, so the words being searched for never go into a request URL.
7
+ * The local host has a single owner, so `isOwner` is always true.
8
+ */
9
+
10
+ import { filterPersonalMemories } from './personal-memory.js';
11
+
12
+ export function createPersonalMemoryHttpPort({
13
+ fetcher,
14
+ token,
15
+ basePath = '/api/personal-memory',
16
+ tokenHeader = 'x-brainbase-review-token',
17
+ } = {}) {
18
+ const request = typeof fetcher === 'function'
19
+ ? fetcher
20
+ : typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
21
+ const base = String(basePath).replace(/\/+$/u, '');
22
+
23
+ async function read(path) {
24
+ if (!request) return { status: 0, body: null, failure: 'fetch_unavailable' };
25
+ try {
26
+ const response = await request(path, { method: 'GET', headers: token ? { [tokenHeader]: token } : {} });
27
+ let body = null;
28
+ try {
29
+ body = await response.json();
30
+ } catch {
31
+ body = null;
32
+ }
33
+ return { status: response.status, ok: response.ok === true, body };
34
+ } catch (error) {
35
+ return { status: 0, body: null, failure: error instanceof Error ? error.message : 'request_failed' };
36
+ }
37
+ }
38
+
39
+ function unavailable(result) {
40
+ return {
41
+ status: 'unavailable',
42
+ reason: result.failure ?? result.body?.reason ?? result.body?.error?.code ?? `http_${result.status}`,
43
+ };
44
+ }
45
+
46
+ return {
47
+ isOwner: () => true,
48
+ async listMemories(filter = {}) {
49
+ const result = await read(base);
50
+ if (!result.ok || result.body?.status !== 'ok' || !Array.isArray(result.body.items)) return unavailable(result);
51
+ return { status: 'ok', items: filterPersonalMemories(result.body.items, filter), nextCursor: null };
52
+ },
53
+ async readMemory(id) {
54
+ const result = await read(`${base}/${encodeURIComponent(id)}`);
55
+ if (result.status === 404 && result.body?.status === 'not_found') return { status: 'not_found' };
56
+ if (!result.ok || result.body?.status !== 'ok' || !result.body.item) return unavailable(result);
57
+ return { status: 'ok', item: result.body.item };
58
+ },
59
+ async listJudgmentsUsing(id, limit) {
60
+ const result = await read(`${base}/${encodeURIComponent(id)}/judgments?limit=${encodeURIComponent(String(limit))}`);
61
+ if (!result.ok || result.body?.status !== 'ok' || !Array.isArray(result.body.items)) return unavailable(result);
62
+ return { status: 'ok', items: result.body.items, total: result.body.total };
63
+ },
64
+ async countJudgmentsUsing() {
65
+ const result = await read(`${base}?view=judgment-counts`);
66
+ if (!result.ok || result.body?.status !== 'ok' || !result.body.counts) return unavailable(result);
67
+ return { status: 'ok', counts: result.body.counts };
68
+ },
69
+ };
70
+ }
71
+
72
+ export default createPersonalMemoryHttpPort;
@@ -0,0 +1,254 @@
1
+ /*
2
+ * 「あなたの記憶」. Load after workspace-kit.css.
3
+ * The privacy line, the one-line summary, the filter chips, the ledger's
4
+ * columns (the memory opening wraps to two lines) and the detail's parts.
5
+ * States are told apart by words and a quiet tone, never by red: a superseded
6
+ * or retracted memory is not an error.
7
+ * Uses only the shared tokens in brainbase-tokens.css.
8
+ */
9
+ .bb-pm-privacy {
10
+ margin: -4px 0 12px;
11
+ color: var(--bb-color-muted);
12
+ font-size: 12px;
13
+ }
14
+
15
+ .bb-pm-summary {
16
+ margin: 0 0 12px;
17
+ color: var(--bb-color-ink);
18
+ font-size: 13px;
19
+ }
20
+
21
+ .bb-pm-loading {
22
+ height: 2px;
23
+ margin: 0 0 10px;
24
+ overflow: hidden;
25
+ background: var(--bb-color-accent-soft);
26
+ color: transparent;
27
+ font-size: 0;
28
+ }
29
+
30
+ .bb-pm-filter-bar {
31
+ margin: 0 0 14px;
32
+ }
33
+
34
+ .bb-pm-filters {
35
+ display: flex;
36
+ flex-wrap: wrap;
37
+ align-items: center;
38
+ gap: 8px;
39
+ }
40
+
41
+ .bb-pm-chip {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ border: 1px solid var(--bb-color-control-border);
45
+ border-radius: 999px;
46
+ background: var(--bb-color-paper);
47
+ }
48
+
49
+ .bb-pm-chip.is-active {
50
+ border-color: var(--bb-color-accent);
51
+ background: var(--bb-color-accent-soft);
52
+ }
53
+
54
+ .bb-pm-chip-button,
55
+ .bb-pm-chip-clear,
56
+ .bb-pm-option,
57
+ .bb-pm-clear-all {
58
+ border: 0;
59
+ background: transparent;
60
+ color: var(--bb-color-ink);
61
+ font: inherit;
62
+ font-size: 13px;
63
+ cursor: pointer;
64
+ }
65
+
66
+ .bb-pm-chip-button {
67
+ padding: 5px 12px;
68
+ }
69
+
70
+ .bb-pm-chip-clear {
71
+ padding: 5px 10px 5px 0;
72
+ color: var(--bb-color-muted);
73
+ }
74
+
75
+ .bb-pm-clear-all {
76
+ padding: 5px 4px;
77
+ color: var(--bb-color-accent-strong);
78
+ }
79
+
80
+ .bb-pm-clear-all:disabled {
81
+ color: var(--bb-color-quiet);
82
+ cursor: default;
83
+ }
84
+
85
+ .bb-pm-search {
86
+ min-width: 200px;
87
+ flex: 1 1 200px;
88
+ max-width: 320px;
89
+ padding: 6px 10px;
90
+ border: 1px solid var(--bb-color-control-border);
91
+ border-radius: 6px;
92
+ background: var(--bb-color-paper);
93
+ color: var(--bb-color-ink);
94
+ font: inherit;
95
+ font-size: 13px;
96
+ }
97
+
98
+ .bb-pm-filter-options {
99
+ display: flex;
100
+ flex-wrap: wrap;
101
+ gap: 6px;
102
+ margin-top: 8px;
103
+ padding: 8px;
104
+ border: 1px solid var(--bb-color-border);
105
+ border-radius: 8px;
106
+ background: var(--bb-color-surface);
107
+ }
108
+
109
+ .bb-pm-option {
110
+ padding: 4px 10px;
111
+ border: 1px solid var(--bb-color-border);
112
+ border-radius: 999px;
113
+ }
114
+
115
+ .bb-pm-option.is-selected {
116
+ border-color: var(--bb-color-accent);
117
+ background: var(--bb-color-accent-soft);
118
+ }
119
+
120
+ .bb-pm-ledger .bb-ws-ledger-row {
121
+ grid-template-columns: 112px minmax(0, 1fr) minmax(0, 168px) 132px 72px;
122
+ }
123
+
124
+ .bb-pm-ledger .bb-ws-ledger-row > span.bb-pm-kind,
125
+ .bb-pm-ledger .bb-ws-ledger-row > span:first-child {
126
+ color: var(--bb-color-muted);
127
+ font-size: 12px;
128
+ }
129
+
130
+ .bb-pm-ledger .bb-ws-ledger-row > span:nth-child(2) {
131
+ white-space: normal;
132
+ }
133
+
134
+ .bb-pm-excerpt {
135
+ display: -webkit-box;
136
+ overflow: hidden;
137
+ -webkit-box-orient: vertical;
138
+ -webkit-line-clamp: 2;
139
+ line-height: 1.5;
140
+ }
141
+
142
+ .bb-pm-ledger .bb-ws-ledger-row.is-past .bb-pm-excerpt > span:last-child {
143
+ color: var(--bb-color-muted);
144
+ }
145
+
146
+ .bb-pm-ledger .bb-ws-ledger-row > span.is-none,
147
+ .bb-pm-ledger .bb-ws-ledger-row > span.is-unknown,
148
+ .bb-pm-ledger .bb-ws-ledger-row > span.is-loading,
149
+ .bb-pm-updated {
150
+ color: var(--bb-color-muted);
151
+ font-size: 12px;
152
+ }
153
+
154
+ .bb-pm-state,
155
+ .bb-pm-inferred {
156
+ display: inline-block;
157
+ margin-right: 6px;
158
+ padding: 0 6px;
159
+ border: 1px solid var(--bb-color-border-strong);
160
+ border-radius: 4px;
161
+ color: var(--bb-color-muted);
162
+ font-size: 11px;
163
+ line-height: 18px;
164
+ vertical-align: 1px;
165
+ }
166
+
167
+ .bb-pm-state.is-active {
168
+ border-color: var(--bb-color-success);
169
+ color: var(--bb-color-success);
170
+ }
171
+
172
+ .bb-pm-inferred {
173
+ margin: 0 0 0 6px;
174
+ }
175
+
176
+ .bb-pm-origin {
177
+ font-size: 12px;
178
+ }
179
+
180
+ .bb-pm-empty {
181
+ padding: 28px 16px;
182
+ border: 1px dashed var(--bb-color-border-strong);
183
+ border-radius: 8px;
184
+ color: var(--bb-color-ink);
185
+ }
186
+
187
+ .bb-pm-empty p {
188
+ margin: 6px 0 0;
189
+ color: var(--bb-color-muted);
190
+ font-size: 13px;
191
+ }
192
+
193
+ .bb-pm-muted {
194
+ margin: 0;
195
+ color: var(--bb-color-muted);
196
+ font-size: 12px;
197
+ }
198
+
199
+ .bb-pm-status > .bb-pm-state {
200
+ margin-bottom: 6px;
201
+ }
202
+
203
+ .bb-pm-history,
204
+ .bb-pm-judgment-list {
205
+ margin: 6px 0 0;
206
+ padding-left: 18px;
207
+ font-size: 13px;
208
+ }
209
+
210
+ .bb-pm-judgment-list li {
211
+ margin: 0 0 6px;
212
+ }
213
+
214
+ .bb-pm-judgment-list li > .bb-pm-muted {
215
+ margin-left: 8px;
216
+ }
217
+
218
+ .bb-pm-record {
219
+ margin-top: 16px;
220
+ font-size: 12px;
221
+ }
222
+
223
+ .bb-pm-record summary {
224
+ cursor: pointer;
225
+ color: var(--bb-color-muted);
226
+ }
227
+
228
+ .bb-pm-detail-inline {
229
+ margin-top: 20px;
230
+ }
231
+
232
+ @media (max-width: 760px) {
233
+ .bb-pm-ledger .bb-ws-ledger-row {
234
+ grid-template-columns: minmax(0, 1fr) 124px;
235
+ }
236
+
237
+ .bb-pm-ledger .bb-ws-ledger-row > span:nth-child(4) {
238
+ white-space: normal;
239
+ }
240
+
241
+ .bb-pm-ledger .bb-ws-ledger-row > span:nth-child(1),
242
+ .bb-pm-ledger .bb-ws-ledger-row > span:nth-child(3),
243
+ .bb-pm-ledger .bb-ws-ledger-row > span:nth-child(5) {
244
+ display: none;
245
+ }
246
+ }
247
+
248
+ /* The detail's head is the memory itself: body-sized, not a page title. */
249
+ .bb-pm-detail .bb-ws-rail-head h2 {
250
+ font-size: 16px;
251
+ font-weight: 600;
252
+ line-height: 1.6;
253
+ white-space: pre-wrap;
254
+ }