@unson/brainbase-mcp 0.41.0 → 0.41.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.41.0",
3
+ "version": "0.41.1",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -481,5 +481,5 @@
481
481
  "engines": {
482
482
  "node": ">=20"
483
483
  },
484
- "gitHead": "c460d39f08bd68e8630fcdd000a934017f41203c"
484
+ "gitHead": "d89de84f6d437c564f843ee06dcdf5836a154d36"
485
485
  }
@@ -16,12 +16,31 @@
16
16
  .bb-minutes-eyebrow,
17
17
  .bb-minutes-label,
18
18
  .bb-minutes-item-meta,
19
+ .bb-minutes-meeting-label,
19
20
  .bb-minutes-rail-title,
20
21
  .bb-minutes-revision {
21
22
  color: var(--bb-color-muted);
22
23
  font-size: 0.82rem;
23
24
  }
24
25
 
26
+ .bb-minutes-meeting-label {
27
+ margin: 0;
28
+ overflow-wrap: anywhere;
29
+ }
30
+
31
+ .bb-minutes-create-guard {
32
+ flex-basis: 100%;
33
+ margin: 0;
34
+ color: var(--bb-color-danger, var(--bb-color-ink));
35
+ }
36
+
37
+ .bb-minutes-edit-guard {
38
+ display: grid;
39
+ gap: var(--bb-space-2);
40
+ margin: 0;
41
+ color: var(--bb-color-danger, var(--bb-color-ink));
42
+ }
43
+
25
44
  .bb-minutes-heading h1,
26
45
  .bb-minutes-heading p,
27
46
  .bb-minutes-pane-head h2,
@@ -16,6 +16,15 @@ function text(value, fallback = '') {
16
16
  return typeof value === 'string' && value.trim() ? value.trim() : fallback;
17
17
  }
18
18
 
19
+ function meetingLabelText(meetingLabel, meeting) {
20
+ if (typeof meetingLabel !== 'function') return '';
21
+ try {
22
+ return text(meetingLabel(meeting));
23
+ } catch {
24
+ return '';
25
+ }
26
+ }
27
+
19
28
  function makeElement(doc, tag, { className, textContent, type, value, disabled, attrs = {} } = {}) {
20
29
  const element = doc.createElement(tag);
21
30
  if (className) element.className = className;
@@ -205,6 +214,9 @@ export function createMeetingMinutesUI({
205
214
  token,
206
215
  basePath = '/api/meeting-minutes',
207
216
  versionActionExtensions = [],
217
+ meetingLabel,
218
+ canCreateMeeting,
219
+ canEditMinutes,
208
220
  } = {}) {
209
221
  if (!root) throw new TypeError('root is required');
210
222
  const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
@@ -213,6 +225,8 @@ export function createMeetingMinutesUI({
213
225
  ? fetcher
214
226
  : typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
215
227
  const base = String(basePath).replace(/\/+$/u, '');
228
+ const createBlockedMessage = 'この画面では会議を作成できません。';
229
+ const editBlockedMessage = 'この画面では議事録を編集できません。';
216
230
  const state = {
217
231
  meetings: [],
218
232
  detail: null,
@@ -223,6 +237,8 @@ export function createMeetingMinutesUI({
223
237
  listError: null,
224
238
  detailError: null,
225
239
  };
240
+ let listRequestGeneration = 0;
241
+ let detailRequestGeneration = 0;
226
242
 
227
243
  const screen = makeElement(doc, 'div', { className: 'bb-minutes-screen', attrs: { 'data-contract-version': MEETING_MINUTES_UI_CONTRACT_VERSION } });
228
244
  const heading = makeElement(doc, 'header', { className: 'bb-minutes-heading' });
@@ -274,6 +290,49 @@ export function createMeetingMinutesUI({
274
290
 
275
291
  function clear(node) { node.replaceChildren(); return node; }
276
292
 
293
+ function canCreate() {
294
+ if (typeof canCreateMeeting === 'function') {
295
+ try {
296
+ return canCreateMeeting() === true;
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+ return canCreateMeeting !== false;
302
+ }
303
+
304
+ function canEdit() {
305
+ if (typeof canEditMinutes === 'function') {
306
+ try {
307
+ return canEditMinutes() === true;
308
+ } catch {
309
+ return false;
310
+ }
311
+ }
312
+ return canEditMinutes !== false;
313
+ }
314
+
315
+ function appendCreateBlockedNotice(parent, { alert = false } = {}) {
316
+ parent.append(makeElement(doc, 'p', {
317
+ className: 'bb-minutes-create-guard',
318
+ textContent: createBlockedMessage,
319
+ attrs: alert ? { role: 'alert' } : {},
320
+ }));
321
+ }
322
+
323
+ function appendEditBlockedNotice(parent, { alert = false } = {}) {
324
+ const existing = [...(parent.children ?? [])].find((child) => child?.className === 'bb-minutes-edit-guard');
325
+ if (existing) {
326
+ if (alert) existing.setAttribute('role', 'alert');
327
+ return existing;
328
+ }
329
+ parent.append(makeElement(doc, 'p', {
330
+ className: 'bb-minutes-edit-guard',
331
+ textContent: editBlockedMessage,
332
+ attrs: alert ? { role: 'alert' } : {},
333
+ }));
334
+ }
335
+
277
336
  function appendLoadError(parent, message, retry) {
278
337
  const notice = makeElement(doc, 'div', {
279
338
  className: 'bb-minutes-load-error',
@@ -288,7 +347,12 @@ export function createMeetingMinutesUI({
288
347
  clear(listPane);
289
348
  const head = makeElement(doc, 'div', { className: 'bb-minutes-pane-head' });
290
349
  head.append(makeElement(doc, 'h2', { textContent: '会議一覧' }));
291
- head.append(button(doc, '会議を作成', () => renderCreateForm(), { className: 'bb-minutes-button is-primary' }));
350
+ const createAllowed = canCreate();
351
+ head.append(button(doc, '会議を作成', () => renderCreateForm(), {
352
+ className: 'bb-minutes-button is-primary',
353
+ disabled: !createAllowed,
354
+ }));
355
+ if (!createAllowed) appendCreateBlockedNotice(head);
292
356
  listPane.append(head);
293
357
  if (state.listError) {
294
358
  appendLoadError(listPane, `会議一覧を読み込めませんでした。理由: ${state.listError}`, () => loadMeetings({ restoreFocus: true }));
@@ -307,8 +371,10 @@ export function createMeetingMinutesUI({
307
371
  type: 'button',
308
372
  attrs: { type: 'button' },
309
373
  });
374
+ const label = meetingLabelText(meetingLabel, meeting);
310
375
  item.append(
311
376
  makeElement(doc, 'strong', { textContent: text(meeting.title, '無題の会議') }),
377
+ ...(label ? [makeElement(doc, 'span', { className: 'bb-minutes-meeting-label', textContent: label })] : []),
312
378
  makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: displayDate(meeting.scheduled_at) }),
313
379
  makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: `${Array.isArray(entry.minutes) ? entry.minutes.length : 0}件の議事録` }),
314
380
  );
@@ -382,8 +448,14 @@ export function createMeetingMinutesUI({
382
448
  section.append(content);
383
449
  if (selected) {
384
450
  const actions = makeElement(doc, 'div', { className: 'bb-minutes-actions' });
451
+ const editAllowed = canEdit();
385
452
  const confirm = button(doc, selected.confirmation ? '確認済み' : 'この版を確認', async () => {
386
453
  if (selected.confirmation) return;
454
+ if (!canEdit()) {
455
+ confirm.disabled = true;
456
+ appendEditBlockedNotice(section, { alert: true });
457
+ return;
458
+ }
387
459
  confirm.disabled = true;
388
460
  try {
389
461
  await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes/${encodeURIComponent(documentRecord.minutes_id)}/confirm`, {
@@ -396,9 +468,10 @@ export function createMeetingMinutesUI({
396
468
  confirm.disabled = false;
397
469
  setStatus(error instanceof Error ? error.message : '確認を保存できませんでした。', 'error');
398
470
  }
399
- }, { className: 'bb-minutes-button is-primary', disabled: Boolean(selected.confirmation) });
471
+ }, { className: 'bb-minutes-button is-primary', disabled: Boolean(selected.confirmation) || !editAllowed });
400
472
  actions.append(confirm);
401
473
  section.append(actions);
474
+ if (!editAllowed) appendEditBlockedNotice(section);
402
475
  }
403
476
  appendVersionActionExtensions(doc, section, {
404
477
  extensions: versionActionExtensions,
@@ -422,6 +495,10 @@ export function createMeetingMinutesUI({
422
495
  }
423
496
 
424
497
  function appendSaveForm(section, detail, documentRecord) {
498
+ if (!canEdit()) {
499
+ appendEditBlockedNotice(section);
500
+ return;
501
+ }
425
502
  const form = makeElement(doc, 'form', { className: 'bb-minutes-save-form' });
426
503
  form.append(makeElement(doc, 'h4', { textContent: '訂正版を保存' }));
427
504
  const field = labelInput(doc, '本文(保存すると新しい版になります)', { type: 'textarea', name: 'body', placeholder: '議事録の本文を入力' });
@@ -430,6 +507,11 @@ export function createMeetingMinutesUI({
430
507
  form.append(field.wrapper, save);
431
508
  form.addEventListener('submit', async (event) => {
432
509
  event.preventDefault();
510
+ if (!canEdit()) {
511
+ save.disabled = false;
512
+ appendEditBlockedNotice(section, { alert: true });
513
+ return;
514
+ }
433
515
  save.disabled = true;
434
516
  try {
435
517
  await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes/${encodeURIComponent(documentRecord.minutes_id)}`, {
@@ -466,6 +548,8 @@ export function createMeetingMinutesUI({
466
548
  const meeting = detail.meeting;
467
549
  const header = makeElement(doc, 'header', { className: 'bb-minutes-detail-head' });
468
550
  header.append(makeElement(doc, 'h2', { textContent: text(meeting.title, '無題の会議') }));
551
+ const label = meetingLabelText(meetingLabel, meeting);
552
+ if (label) header.append(makeElement(doc, 'p', { className: 'bb-minutes-meeting-label', textContent: label }));
469
553
  header.append(makeElement(doc, 'p', { className: 'bb-minutes-item-meta', textContent: `${displayDate(meeting.scheduled_at)} / ${Array.isArray(meeting.participant_ids) && meeting.participant_ids.length ? meeting.participant_ids.join('、') : '参加者未登録'}` }));
470
554
  detailPane.append(header);
471
555
  const documents = Array.isArray(detail.minutes) ? detail.minutes : [];
@@ -477,6 +561,11 @@ export function createMeetingMinutesUI({
477
561
  }
478
562
 
479
563
  function renderCreateMinutesForm(detail) {
564
+ if (!canEdit()) {
565
+ const guard = makeElement(doc, 'section', { className: 'bb-minutes-edit-guard' });
566
+ appendEditBlockedNotice(guard);
567
+ return guard;
568
+ }
480
569
  const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
481
570
  form.append(makeElement(doc, 'h3', { textContent: '議事録文書を追加' }));
482
571
  const title = labelInput(doc, '議事録の名前', { name: 'title', value: '議事録' });
@@ -486,6 +575,11 @@ export function createMeetingMinutesUI({
486
575
  form.append(title.wrapper, body.wrapper, save);
487
576
  form.addEventListener('submit', async (event) => {
488
577
  event.preventDefault();
578
+ if (!canEdit()) {
579
+ save.disabled = false;
580
+ appendEditBlockedNotice(form, { alert: true });
581
+ return;
582
+ }
489
583
  save.disabled = true;
490
584
  try {
491
585
  await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes`, {
@@ -506,6 +600,10 @@ export function createMeetingMinutesUI({
506
600
 
507
601
  function renderCreateForm() {
508
602
  clear(detailPane);
603
+ if (!canCreate()) {
604
+ appendCreateBlockedNotice(detailPane, { alert: true });
605
+ return;
606
+ }
509
607
  const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
510
608
  form.append(makeElement(doc, 'h2', { textContent: '会議を作成' }), makeElement(doc, 'p', { className: 'bb-minutes-item-meta', textContent: '外部サービスを接続しなくても、会議と議事録を保存できます。' }));
511
609
  const title = labelInput(doc, '会議名', { name: 'title', placeholder: '例: 週次定例' });
@@ -518,6 +616,11 @@ export function createMeetingMinutesUI({
518
616
  form.append(title.wrapper, date.wrapper, participants.wrapper, minutesTitle.wrapper, body.wrapper, save);
519
617
  form.addEventListener('submit', async (event) => {
520
618
  event.preventDefault();
619
+ if (!canCreate()) {
620
+ save.disabled = false;
621
+ appendCreateBlockedNotice(detailPane, { alert: true });
622
+ return;
623
+ }
521
624
  save.disabled = true;
522
625
  try {
523
626
  const scheduledAt = date.input.value ? new Date(date.input.value).toISOString() : undefined;
@@ -542,16 +645,20 @@ export function createMeetingMinutesUI({
542
645
  }
543
646
 
544
647
  async function loadMeetings({ restoreFocus = false } = {}) {
648
+ const requestGeneration = ++listRequestGeneration;
649
+ const isCurrentRequest = () => requestGeneration === listRequestGeneration;
545
650
  state.loading = true;
546
651
  setStatus('会議一覧を読み込んでいます。');
547
652
  try {
548
653
  const payload = await call('');
654
+ if (!isCurrentRequest()) return state.meetings;
549
655
  if (!isRecord(payload) || !Array.isArray(payload.meetings) || payload.absence_confirmed !== true) throw new Error('会議一覧の形式を確認できません。');
550
656
  state.meetings = payload.meetings;
551
657
  state.listError = null;
552
658
  const selectedMeetingStillListed = state.meetingId !== null
553
659
  && state.meetings.some((entry) => entry?.meeting?.meeting_id === state.meetingId);
554
660
  if (state.meetingId !== null && !selectedMeetingStillListed) {
661
+ detailRequestGeneration += 1;
555
662
  state.detail = null;
556
663
  state.meetingId = null;
557
664
  state.minutesId = null;
@@ -567,12 +674,14 @@ export function createMeetingMinutesUI({
567
674
  setStatus(state.meetings.length ? `${state.meetings.length}件の会議を読み込みました。` : '保存された会議はありません。');
568
675
  if (restoreFocus && typeof listPane.focus === 'function') listPane.focus({ preventScroll: true });
569
676
  } catch (error) {
677
+ if (!isCurrentRequest()) return state.meetings;
570
678
  state.listError = loadErrorMessage(error, '会議一覧を読み込めませんでした。');
571
679
  const preserve = preserveLoadedStateOnFailure(error);
572
680
  const hadDetail = Boolean(state.detail) || state.meetingId !== null;
573
681
  if (!preserve) {
574
682
  state.meetings = [];
575
683
  if (hadDetail) {
684
+ detailRequestGeneration += 1;
576
685
  state.detail = null;
577
686
  state.meetingId = null;
578
687
  state.minutesId = null;
@@ -584,12 +693,15 @@ export function createMeetingMinutesUI({
584
693
  if (!preserve && hadDetail) renderDetail(null);
585
694
  setStatus(state.listError, 'error');
586
695
  } finally {
587
- state.loading = false;
696
+ if (isCurrentRequest()) state.loading = false;
588
697
  }
589
698
  return state.meetings;
590
699
  }
591
700
 
592
701
  async function loadDetail(meetingId) {
702
+ const requestGeneration = ++detailRequestGeneration;
703
+ const isCurrentRequest = () => requestGeneration === detailRequestGeneration
704
+ && state.meetingId === meetingId;
593
705
  state.meetingId = meetingId;
594
706
  state.minutesId = null;
595
707
  state.detailError = null;
@@ -597,12 +709,14 @@ export function createMeetingMinutesUI({
597
709
  setStatus('会議詳細を読み込んでいます。');
598
710
  try {
599
711
  const detail = await call(`/${encodeURIComponent(meetingId)}`);
712
+ if (!isCurrentRequest()) return state.detail;
600
713
  if (!isRecord(detail) || !isRecord(detail.meeting) || !Array.isArray(detail.minutes) || !Array.isArray(detail.versions)) throw new Error('会議詳細の形式を確認できません。');
601
714
  state.detail = detail;
602
715
  state.detailError = null;
603
716
  renderDetail(detail);
604
717
  setStatus('会議詳細を読み込みました。');
605
718
  } catch (error) {
719
+ if (!isCurrentRequest()) return state.detail;
606
720
  const preserved = preserveLoadedStateOnFailure(error) && state.detail?.meeting?.meeting_id === meetingId
607
721
  ? state.detail
608
722
  : null;