@unson/brainbase-mcp 0.41.11 → 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.
- package/README.md +1 -1
- package/contracts/judgment-dag/digest.json +2 -2
- package/dist/local-web-host.d.ts +6 -0
- package/dist/local-web-host.js +54 -3
- package/dist/personal-memory.d.ts +101 -0
- package/dist/personal-memory.js +204 -0
- package/dist/ssot.d.ts +3 -1
- package/dist/ssot.js +4 -0
- package/package.json +13 -2
- package/ui/area-nav.css +79 -0
- package/ui/area-nav.js +249 -0
- package/ui/foundation-overview.css +9 -0
- package/ui/foundation-overview.js +75 -5
- package/ui/graph-ontology-view.css +88 -0
- package/ui/graph-ontology-view.js +248 -0
- package/ui/graph-own-share.js +167 -0
- package/ui/graph-registry-view.css +272 -50
- package/ui/graph-registry-view.js +539 -456
- package/ui/graph-view-shared.css +32 -0
- package/ui/graph-view-shared.js +191 -12
- package/ui/judgment-history.js +74 -3
- package/ui/local-web-shell.css +1 -13
- package/ui/local-web-shell.js +119 -41
- package/ui/outcome-knowledge.js +6 -2
- package/ui/people-and-companies-view.css +166 -15
- package/ui/people-and-companies-view.js +307 -93
- package/ui/personal-memory-http-port.js +72 -0
- package/ui/personal-memory.css +254 -0
- package/ui/personal-memory.js +677 -0
- package/ui/world/world-canvas-notes.js +3 -2
- package/ui/world/world-view.css +19 -0
- package/ui/world/world-view.js +52 -10
- package/ui/world/world-work-rail.js +114 -8
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ brainbase onboard:candidates
|
|
|
105
105
|
|
|
106
106
|
### Google Workspace local-first adopter
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
常時稼働のローカルホスト(例: Mac mini)、Google Workspace、必要なら補助Gmail、許可したDrive/local folder、Calendar/notesに散在するタスクを想定した例です。使わないタスクツール(例: Notion)は`--inactive-task-tool`で非アクティブ扱いにできます。全Driveやhome directory全体は走査しません。
|
|
109
109
|
|
|
110
110
|
```bash
|
|
111
111
|
brainbase onboard:plan --profile google-workspace-local --host mac-mini --email google-workspace --secondary-email gmail --calendar google-calendar --drive google-drive --drive-folder "<folder-id>" --local-folder "/Users/owner/Notes" --tasks scattered-calendar-notes --inactive-task-tool notion
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "README.md",
|
|
28
|
-
"sha256": "
|
|
28
|
+
"sha256": "26eacd2a057380b4fd386b2176bd6e790a41c0bb8d684857c53bb5cd947e2d28"
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
|
-
"digest": "
|
|
31
|
+
"digest": "38bf5aff53993b1ee737071ca7952d44e36ac2e5ed3a7adcac65fdd86e8954af"
|
|
32
32
|
}
|
package/dist/local-web-host.d.ts
CHANGED
|
@@ -195,6 +195,12 @@ export declare function createWorldModelReadModule(context: LocalWebModuleContex
|
|
|
195
195
|
* correction is never saved through a path that skipped the host check.
|
|
196
196
|
*/
|
|
197
197
|
export declare function createGraphWebModule(context: LocalWebModuleContext): LocalWebModule;
|
|
198
|
+
/**
|
|
199
|
+
* 「あなたの記憶」: the owner's `personal-kg.jsonl`, read only. The memory
|
|
200
|
+
* bodies are private, so these reads require the launch token and the same
|
|
201
|
+
* origin like every write on this host.
|
|
202
|
+
*/
|
|
203
|
+
export declare function createPersonalMemoryModule(context: LocalWebModuleContext): LocalWebModule;
|
|
198
204
|
/** 今日 (value proofs), 目的と現状 (objectives + world model), プロジェクトと関係者 and 情報と関係 (Graph). */
|
|
199
205
|
export declare function defaultLocalWebModules(context: LocalWebModuleContext): LocalWebModule[];
|
|
200
206
|
/** Local single-owner host. The caller owns `listen()` and must bind to a loopback address. */
|
package/dist/local-web-host.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createJudgmentHistoryHttpHandler, JUDGMENT_HISTORY_HTTP_PREFIX } from '
|
|
|
12
12
|
import { createJudgmentHistoryReader, createLocalJudgmentHistoryFeedbackWriter, createLocalJudgmentHistorySource } from './judgment-history.js';
|
|
13
13
|
import { defaultJudgmentJournalRoot, JudgmentValueProofJournalCache } from './judgment-value-proof-review.js';
|
|
14
14
|
import { createMeetingMinutesHttpHandler } from './meeting-minutes-http.js';
|
|
15
|
+
import { createPersonalMemoryHttpHandler, createPersonalMemoryJudgmentIndex, readLocalPersonalMemories } from './personal-memory.js';
|
|
15
16
|
import { createMeetingMinutesLineageHttpHandler } from './meeting-minutes-lineage-http.js';
|
|
16
17
|
import { createMeetingMinutesLineageCorrectionHook, } from './meeting-minutes-lineage-lifecycle.js';
|
|
17
18
|
import { createMeetingMinutesStore } from './meeting-minutes.js';
|
|
@@ -52,7 +53,7 @@ export const LOCAL_WEB_JUDGMENT_HISTORY_PREFIX = JUDGMENT_HISTORY_HTTP_PREFIX;
|
|
|
52
53
|
export const LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES = 64 * 1024;
|
|
53
54
|
export const LOCAL_WEB_PROVENANCE_SOURCE_ID = 'brainbase-local-web';
|
|
54
55
|
const CANONICAL_FILES = ['graph.json', 'relationships.json', 'personal-kg.jsonl', 'decisions.jsonl'];
|
|
55
|
-
const BASE_UI_FILES = ['brainbase-tokens.css', 'local-web-shell.js', 'local-web-shell.css', 'workspace-kit.js', 'workspace-kit.css', 'native-scrollbars.js', 'native-scrollbars.css'];
|
|
56
|
+
const BASE_UI_FILES = ['brainbase-tokens.css', 'local-web-shell.js', 'local-web-shell.css', 'workspace-kit.js', 'workspace-kit.css', 'native-scrollbars.js', 'native-scrollbars.css', 'area-nav.js', 'area-nav.css'];
|
|
56
57
|
const CONTENT_TYPES = Object.freeze({
|
|
57
58
|
'.js': 'text/javascript; charset=utf-8',
|
|
58
59
|
'.css': 'text/css; charset=utf-8'
|
|
@@ -883,12 +884,46 @@ export function createGraphWebModule(context) {
|
|
|
883
884
|
'project-graph-vendor.js',
|
|
884
885
|
'graph-registry-view.js',
|
|
885
886
|
'graph-registry-view.css',
|
|
887
|
+
'graph-ontology-view.js',
|
|
888
|
+
'graph-ontology-view.css',
|
|
886
889
|
'people-and-companies-view.js',
|
|
887
890
|
'people-and-companies-view.css'
|
|
888
891
|
],
|
|
889
892
|
handle: handler
|
|
890
893
|
};
|
|
891
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* 「あなたの記憶」: the owner's `personal-kg.jsonl`, read only. The memory
|
|
897
|
+
* bodies are private, so these reads require the launch token and the same
|
|
898
|
+
* origin like every write on this host.
|
|
899
|
+
*/
|
|
900
|
+
export function createPersonalMemoryModule(context) {
|
|
901
|
+
const readMemories = () => readLocalPersonalMemories(context.dataDir);
|
|
902
|
+
const index = createPersonalMemoryJudgmentIndex({
|
|
903
|
+
source: createLocalJudgmentHistorySource({
|
|
904
|
+
root: context.journalRoot,
|
|
905
|
+
entrypoint: context.judgmentHistoryEntrypoint ?? 'codex'
|
|
906
|
+
}),
|
|
907
|
+
memoryIds: async () => {
|
|
908
|
+
const list = await readMemories();
|
|
909
|
+
return list.status === 'ok' ? list.items.map((item) => item.id) : null;
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
return {
|
|
913
|
+
id: 'personal-memory',
|
|
914
|
+
uiFiles: ['personal-memory.js', 'personal-memory-http-port.js', 'personal-memory.css'],
|
|
915
|
+
handle: createPersonalMemoryHttpHandler({
|
|
916
|
+
readMemories,
|
|
917
|
+
index,
|
|
918
|
+
authorize(request) {
|
|
919
|
+
if (isTrustedLocalWrite(request, context.token))
|
|
920
|
+
return;
|
|
921
|
+
throw rejectUntrustedWrite(request, context.token)
|
|
922
|
+
?? new LocalWebHttpError(403, 'web_token_required', 'A valid launch token is required');
|
|
923
|
+
}
|
|
924
|
+
})
|
|
925
|
+
};
|
|
926
|
+
}
|
|
892
927
|
/** 今日 (value proofs), 目的と現状 (objectives + world model), プロジェクトと関係者 and 情報と関係 (Graph). */
|
|
893
928
|
export function defaultLocalWebModules(context) {
|
|
894
929
|
return [
|
|
@@ -898,7 +933,8 @@ export function defaultLocalWebModules(context) {
|
|
|
898
933
|
createMeetingMinutesModule(context),
|
|
899
934
|
createObjectiveFoundationModule(context),
|
|
900
935
|
createWorldModelReadModule(context),
|
|
901
|
-
createGraphWebModule(context)
|
|
936
|
+
createGraphWebModule(context),
|
|
937
|
+
createPersonalMemoryModule(context)
|
|
902
938
|
];
|
|
903
939
|
}
|
|
904
940
|
function escapeAttribute(value) {
|
|
@@ -938,10 +974,25 @@ const meta = (name) => document.querySelector(\`meta[name="\${name}"]\`)?.getAtt
|
|
|
938
974
|
// Keep the query portion intact; the shell parses the Graph entity query and
|
|
939
975
|
// only treats the entity id as a target of the Graph screen.
|
|
940
976
|
const screenFromHash = () => location.hash.replace(/^#/, '') || undefined;
|
|
941
|
-
|
|
977
|
+
// A screen that opened its own address (a record of 情報と関係) goes back with the
|
|
978
|
+
// browser's history, so 「一覧に戻る」 and the back button land on the same list.
|
|
979
|
+
// Each entry remembers how many records deep it is, which stays right when the
|
|
980
|
+
// owner also uses the browser's back and forward.
|
|
981
|
+
const depth = () => (Number.isInteger(history.state?.bbDepth) ? history.state.bbDepth : 0);
|
|
982
|
+
let shell = null;
|
|
983
|
+
const navigate = (target, { back = false } = {}) => {
|
|
984
|
+
if (back && depth() > 0) {
|
|
985
|
+
history.go(-depth());
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
history.pushState({ bbDepth: back ? 0 : depth() + 1 }, '', \`#\${target}\`);
|
|
989
|
+
shell?.show(target);
|
|
990
|
+
};
|
|
991
|
+
shell = createLocalWebShell({
|
|
942
992
|
root: document.getElementById('brainbase-local-web'),
|
|
943
993
|
token: meta('brainbase-web-token'),
|
|
944
994
|
initialScreen: screenFromHash(),
|
|
995
|
+
onNavigate: navigate,
|
|
945
996
|
meetingMinutesStorage: ${JSON.stringify(meetingMinutesStorage ?? null)},
|
|
946
997
|
meetingMinutesLineage: ${JSON.stringify(meetingMinutesLineage ?? null)},
|
|
947
998
|
screens: ${leading ? `[${leading}, ...LOCAL_WEB_SCREENS${trailing ? `, ${trailing}` : ''}]` : '[...LOCAL_WEB_SCREENS, ...extensionScreens]'}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { JudgmentHistorySource } from './judgment-history.js';
|
|
3
|
+
import type { PersonalKgEntry } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* 「あなたの記憶」 (docs/specs/personal-memory-viewer.md): the owner's own
|
|
6
|
+
* memories read from the local `personal-kg.jsonl`, and the judgments that
|
|
7
|
+
* used each one, found back from the judgment journal's `personal_knowledge`
|
|
8
|
+
* references. Read only. A memory body travels in the response body only,
|
|
9
|
+
* never in a URL or a log line.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PERSONAL_MEMORY_HTTP_PREFIX = "/api/personal-memory";
|
|
12
|
+
export type PersonalMemoryState = 'active' | 'superseded' | 'retracted' | 'unrecorded';
|
|
13
|
+
export interface PersonalMemoryItem {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly kind: string | null;
|
|
16
|
+
readonly text: string;
|
|
17
|
+
readonly tags: readonly string[];
|
|
18
|
+
readonly state: PersonalMemoryState;
|
|
19
|
+
readonly supersededBy: string | null;
|
|
20
|
+
readonly origin: {
|
|
21
|
+
readonly organizationLabel: string | null;
|
|
22
|
+
readonly projectLabel: string | null;
|
|
23
|
+
readonly workplace: string | null;
|
|
24
|
+
readonly inferred: boolean;
|
|
25
|
+
};
|
|
26
|
+
readonly occurredAt: string | null;
|
|
27
|
+
readonly updatedAt: string | null;
|
|
28
|
+
readonly recallScope: 'organization' | 'general' | 'unknown';
|
|
29
|
+
readonly history: readonly {
|
|
30
|
+
readonly state: PersonalMemoryState;
|
|
31
|
+
readonly at: string;
|
|
32
|
+
readonly reason: string | null;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
export type PersonalMemoryList = {
|
|
36
|
+
readonly status: 'ok';
|
|
37
|
+
readonly items: readonly PersonalMemoryItem[];
|
|
38
|
+
} | {
|
|
39
|
+
readonly status: 'unavailable';
|
|
40
|
+
readonly reason: string;
|
|
41
|
+
};
|
|
42
|
+
export interface PersonalMemoryJudgment {
|
|
43
|
+
readonly decisionId: string;
|
|
44
|
+
readonly title: string | null;
|
|
45
|
+
readonly at: string;
|
|
46
|
+
readonly kind: string | null;
|
|
47
|
+
}
|
|
48
|
+
export type PersonalMemoryJudgments = {
|
|
49
|
+
readonly status: 'ok';
|
|
50
|
+
readonly items: readonly PersonalMemoryJudgment[];
|
|
51
|
+
readonly total: number;
|
|
52
|
+
} | {
|
|
53
|
+
readonly status: 'unavailable';
|
|
54
|
+
readonly reason: string;
|
|
55
|
+
};
|
|
56
|
+
export type PersonalMemoryJudgmentCounts = {
|
|
57
|
+
readonly status: 'ok';
|
|
58
|
+
readonly counts: Readonly<Record<string, number>>;
|
|
59
|
+
} | {
|
|
60
|
+
readonly status: 'unavailable';
|
|
61
|
+
readonly reason: string;
|
|
62
|
+
};
|
|
63
|
+
/** The local writer keeps no state, origin organization or recall scope: the spec's fixed values. */
|
|
64
|
+
export declare function personalMemoryFromEntry(entry: PersonalKgEntry): PersonalMemoryItem;
|
|
65
|
+
/** An empty file is zero memories; a missing or malformed one is unavailable, never zero. */
|
|
66
|
+
export declare function readLocalPersonalMemories(dataDir: string): Promise<PersonalMemoryList>;
|
|
67
|
+
export interface PersonalMemoryJudgmentIndex {
|
|
68
|
+
judgmentsUsing(id: string, limit: number): Promise<PersonalMemoryJudgments>;
|
|
69
|
+
counts(): Promise<PersonalMemoryJudgmentCounts>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Finds judgments back from their `personal_knowledge` references. A
|
|
73
|
+
* reference names a memory by its id, so the journal is bound to these
|
|
74
|
+
* memories only when at least one recorded reference is one of their ids.
|
|
75
|
+
* When every reference names a memory kept elsewhere (for example the
|
|
76
|
+
* organization's personal KG), "not used" cannot be told from "used under
|
|
77
|
+
* another id", and the answer is unavailable instead of zero.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createPersonalMemoryJudgmentIndex(options: {
|
|
80
|
+
readonly source: JudgmentHistorySource;
|
|
81
|
+
/** The memory ids, or null when the memories cannot be read. */
|
|
82
|
+
readonly memoryIds: () => Promise<readonly string[] | null>;
|
|
83
|
+
/**
|
|
84
|
+
* How long one journal read answers later requests. A large journal takes
|
|
85
|
+
* tens of seconds to scan, and one opened memory asks for its judgments and
|
|
86
|
+
* the list's counts at the same time.
|
|
87
|
+
*/
|
|
88
|
+
readonly reuseMs?: number;
|
|
89
|
+
readonly now?: () => number;
|
|
90
|
+
}): PersonalMemoryJudgmentIndex;
|
|
91
|
+
/**
|
|
92
|
+
* GET routes under `/api/personal-memory`. `authorize` runs before any read
|
|
93
|
+
* and throws to refuse the request (the local host requires its launch token
|
|
94
|
+
* on these reads, not only on writes).
|
|
95
|
+
*/
|
|
96
|
+
export declare function createPersonalMemoryHttpHandler(options: {
|
|
97
|
+
readonly readMemories: () => Promise<PersonalMemoryList>;
|
|
98
|
+
readonly index: PersonalMemoryJudgmentIndex;
|
|
99
|
+
readonly authorize: (request: IncomingMessage) => void;
|
|
100
|
+
readonly basePath?: string;
|
|
101
|
+
}): (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { requestUrl, writeJson } from './local-web-security.js';
|
|
4
|
+
import { readPersonalKg } from './ssot.js';
|
|
5
|
+
/**
|
|
6
|
+
* 「あなたの記憶」 (docs/specs/personal-memory-viewer.md): the owner's own
|
|
7
|
+
* memories read from the local `personal-kg.jsonl`, and the judgments that
|
|
8
|
+
* used each one, found back from the judgment journal's `personal_knowledge`
|
|
9
|
+
* references. Read only. A memory body travels in the response body only,
|
|
10
|
+
* never in a URL or a log line.
|
|
11
|
+
*/
|
|
12
|
+
export const PERSONAL_MEMORY_HTTP_PREFIX = '/api/personal-memory';
|
|
13
|
+
/** The local writer keeps no state, origin organization or recall scope: the spec's fixed values. */
|
|
14
|
+
export function personalMemoryFromEntry(entry) {
|
|
15
|
+
return {
|
|
16
|
+
id: entry.id,
|
|
17
|
+
kind: entry.type,
|
|
18
|
+
text: entry.text,
|
|
19
|
+
tags: [...entry.tags ?? []],
|
|
20
|
+
state: 'unrecorded',
|
|
21
|
+
supersededBy: null,
|
|
22
|
+
origin: { organizationLabel: '手元', projectLabel: null, workplace: null, inferred: false },
|
|
23
|
+
occurredAt: null,
|
|
24
|
+
updatedAt: entry.updatedAt ?? null,
|
|
25
|
+
recallScope: 'general',
|
|
26
|
+
history: []
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function newestFirst(left, right) {
|
|
30
|
+
if (left === right)
|
|
31
|
+
return 0;
|
|
32
|
+
if (left === null)
|
|
33
|
+
return 1;
|
|
34
|
+
if (right === null)
|
|
35
|
+
return -1;
|
|
36
|
+
return left < right ? 1 : -1;
|
|
37
|
+
}
|
|
38
|
+
/** An empty file is zero memories; a missing or malformed one is unavailable, never zero. */
|
|
39
|
+
export async function readLocalPersonalMemories(dataDir) {
|
|
40
|
+
try {
|
|
41
|
+
await stat(join(dataDir, 'personal-kg.jsonl'));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { status: 'unavailable', reason: 'personal_kg_missing' };
|
|
45
|
+
}
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = await readPersonalKg(dataDir);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { status: 'unavailable', reason: 'personal_kg_invalid' };
|
|
52
|
+
}
|
|
53
|
+
const items = entries.map(personalMemoryFromEntry);
|
|
54
|
+
items.sort((left, right) => newestFirst(left.updatedAt, right.updatedAt));
|
|
55
|
+
return { status: 'ok', items };
|
|
56
|
+
}
|
|
57
|
+
function personalReferences(record) {
|
|
58
|
+
const refs = new Set();
|
|
59
|
+
for (const reference of record.judgment.selected_references ?? []) {
|
|
60
|
+
if (reference.kind === 'personal_knowledge' && reference.ref)
|
|
61
|
+
refs.add(reference.ref);
|
|
62
|
+
}
|
|
63
|
+
return refs;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Finds judgments back from their `personal_knowledge` references. A
|
|
67
|
+
* reference names a memory by its id, so the journal is bound to these
|
|
68
|
+
* memories only when at least one recorded reference is one of their ids.
|
|
69
|
+
* When every reference names a memory kept elsewhere (for example the
|
|
70
|
+
* organization's personal KG), "not used" cannot be told from "used under
|
|
71
|
+
* another id", and the answer is unavailable instead of zero.
|
|
72
|
+
*/
|
|
73
|
+
export function createPersonalMemoryJudgmentIndex(options) {
|
|
74
|
+
async function build() {
|
|
75
|
+
const [snapshot, ids] = await Promise.all([options.source.read(), options.memoryIds()]);
|
|
76
|
+
if (ids === null)
|
|
77
|
+
return { status: 'unavailable', reason: 'personal_kg_unavailable' };
|
|
78
|
+
if (snapshot.status !== 'available' || !snapshot.records) {
|
|
79
|
+
return { status: 'unavailable', reason: `journal_${snapshot.status === 'partial' ? 'partial' : 'unavailable'}` };
|
|
80
|
+
}
|
|
81
|
+
const known = new Set(ids);
|
|
82
|
+
const byMemory = new Map(ids.map((id) => [id, []]));
|
|
83
|
+
let referenced = 0;
|
|
84
|
+
let bound = 0;
|
|
85
|
+
for (const record of snapshot.records) {
|
|
86
|
+
const refs = personalReferences(record);
|
|
87
|
+
for (const ref of refs) {
|
|
88
|
+
referenced += 1;
|
|
89
|
+
if (!known.has(ref))
|
|
90
|
+
continue;
|
|
91
|
+
bound += 1;
|
|
92
|
+
byMemory.get(ref)?.push({ decisionId: record.record_id, title: record.judgment.summary, at: record.recorded_at, kind: null });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (referenced > 0 && bound === 0)
|
|
96
|
+
return { status: 'unavailable', reason: 'reference_source_mismatch' };
|
|
97
|
+
for (const judgments of byMemory.values())
|
|
98
|
+
judgments.sort((left, right) => newestFirst(left.at, right.at));
|
|
99
|
+
return { status: 'ok', byMemory };
|
|
100
|
+
}
|
|
101
|
+
const reuseMs = options.reuseMs ?? 30_000;
|
|
102
|
+
const now = options.now ?? (() => Date.now());
|
|
103
|
+
let current = null;
|
|
104
|
+
function shared() {
|
|
105
|
+
if (current && now() - current.startedAt < reuseMs)
|
|
106
|
+
return current.promise;
|
|
107
|
+
const entry = { promise: build(), startedAt: now() };
|
|
108
|
+
current = entry;
|
|
109
|
+
// A failed read, a missing journal and unreadable memories are not reused: the owner may fix them now.
|
|
110
|
+
const forget = () => { if (current === entry)
|
|
111
|
+
current = null; };
|
|
112
|
+
entry.promise.then((index) => {
|
|
113
|
+
if (index.status === 'unavailable' && (index.reason === 'journal_unavailable' || index.reason === 'personal_kg_unavailable'))
|
|
114
|
+
forget();
|
|
115
|
+
}, forget);
|
|
116
|
+
return entry.promise;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
async judgmentsUsing(id, limit) {
|
|
120
|
+
const index = await shared();
|
|
121
|
+
if (index.status !== 'ok')
|
|
122
|
+
return index;
|
|
123
|
+
const judgments = index.byMemory.get(id) ?? [];
|
|
124
|
+
return { status: 'ok', items: judgments.slice(0, Math.max(0, limit)), total: judgments.length };
|
|
125
|
+
},
|
|
126
|
+
async counts() {
|
|
127
|
+
const index = await shared();
|
|
128
|
+
if (index.status !== 'ok')
|
|
129
|
+
return index;
|
|
130
|
+
return { status: 'ok', counts: Object.fromEntries([...index.byMemory].map(([id, judgments]) => [id, judgments.length])) };
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const DEFAULT_JUDGMENT_LIMIT = 5;
|
|
135
|
+
const MAX_JUDGMENT_LIMIT = 500;
|
|
136
|
+
/**
|
|
137
|
+
* GET routes under `/api/personal-memory`. `authorize` runs before any read
|
|
138
|
+
* and throws to refuse the request (the local host requires its launch token
|
|
139
|
+
* on these reads, not only on writes).
|
|
140
|
+
*/
|
|
141
|
+
export function createPersonalMemoryHttpHandler(options) {
|
|
142
|
+
const basePath = (options.basePath ?? PERSONAL_MEMORY_HTTP_PREFIX).replace(/\/+$/u, '');
|
|
143
|
+
return async (request, response) => {
|
|
144
|
+
const url = requestUrl(request);
|
|
145
|
+
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`))
|
|
146
|
+
return false;
|
|
147
|
+
if (request.method !== 'GET') {
|
|
148
|
+
writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'Use GET' } });
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
options.authorize(request);
|
|
152
|
+
const unavailable = (result) => writeJson(response, 503, { status: 'unavailable', reason: result.reason });
|
|
153
|
+
if (url.pathname === basePath) {
|
|
154
|
+
if (url.searchParams.get('view') === 'judgment-counts') {
|
|
155
|
+
const counts = await options.index.counts();
|
|
156
|
+
if (counts.status === 'ok')
|
|
157
|
+
writeJson(response, 200, counts);
|
|
158
|
+
else
|
|
159
|
+
unavailable(counts);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
const list = await options.readMemories();
|
|
163
|
+
if (list.status === 'ok')
|
|
164
|
+
writeJson(response, 200, { status: 'ok', items: list.items, next_cursor: null });
|
|
165
|
+
else
|
|
166
|
+
unavailable(list);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
const rest = url.pathname.slice(basePath.length + 1).split('/');
|
|
170
|
+
let id;
|
|
171
|
+
try {
|
|
172
|
+
id = decodeURIComponent(rest[0] ?? '');
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
writeJson(response, 400, { error: { code: 'invalid_id', message: 'The memory id is not valid' } });
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (!id || rest.length > 2 || (rest.length === 2 && rest[1] !== 'judgments')) {
|
|
179
|
+
writeJson(response, 404, { error: { code: 'not_found', message: 'Not found' } });
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
if (rest.length === 2) {
|
|
183
|
+
const requested = Number.parseInt(url.searchParams.get('limit') ?? '', 10);
|
|
184
|
+
const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, MAX_JUDGMENT_LIMIT) : DEFAULT_JUDGMENT_LIMIT;
|
|
185
|
+
const judgments = await options.index.judgmentsUsing(id, limit);
|
|
186
|
+
if (judgments.status === 'ok')
|
|
187
|
+
writeJson(response, 200, judgments);
|
|
188
|
+
else
|
|
189
|
+
unavailable(judgments);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
const list = await options.readMemories();
|
|
193
|
+
if (list.status !== 'ok') {
|
|
194
|
+
unavailable(list);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
const item = list.items.find((candidate) => candidate.id === id);
|
|
198
|
+
if (item)
|
|
199
|
+
writeJson(response, 200, { status: 'ok', item });
|
|
200
|
+
else
|
|
201
|
+
writeJson(response, 404, { status: 'not_found' });
|
|
202
|
+
return true;
|
|
203
|
+
};
|
|
204
|
+
}
|
package/dist/ssot.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type CanonicalGraphMigrationPlan } from './ontology-migration.js';
|
|
2
|
-
import type { PersonalOs } from './types.js';
|
|
2
|
+
import type { PersonalKgEntry, PersonalOs } from './types.js';
|
|
3
3
|
export declare function initializePersonalOs(dataDir: string): Promise<void>;
|
|
4
4
|
export declare function loadPersonalOs(dataDir: string): Promise<PersonalOs>;
|
|
5
|
+
/** Only `personal-kg.jsonl`, validated line by line as `loadPersonalOs` does. */
|
|
6
|
+
export declare function readPersonalKg(dataDir: string): Promise<PersonalKgEntry[]>;
|
|
5
7
|
export declare function mutatePersonalOs(dataDir: string, mutator: (current: PersonalOs) => PersonalOs | Promise<PersonalOs>): Promise<PersonalOs>;
|
|
6
8
|
export declare function mutatePersonalOsWithSidecar<T>(dataDir: string, sidecarPath: string, mutator: (current: PersonalOs, sidecarContent?: string) => {
|
|
7
9
|
next: PersonalOs;
|
package/dist/ssot.js
CHANGED
|
@@ -74,6 +74,10 @@ export async function loadPersonalOs(dataDir) {
|
|
|
74
74
|
return loadPersonalOsUnlocked(dataDir);
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
+
/** Only `personal-kg.jsonl`, validated line by line as `loadPersonalOs` does. */
|
|
78
|
+
export async function readPersonalKg(dataDir) {
|
|
79
|
+
return readJsonl(join(dataDir, 'personal-kg.jsonl'), personalKgSchema, 'personal-kg.jsonl');
|
|
80
|
+
}
|
|
77
81
|
export async function mutatePersonalOs(dataDir, mutator) {
|
|
78
82
|
return withSsotLock(dataDir, async () => {
|
|
79
83
|
await recoverTransactions(dataDir);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
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",
|
|
@@ -295,6 +295,10 @@
|
|
|
295
295
|
"types": "./dist/streamable-http.d.ts",
|
|
296
296
|
"import": "./dist/streamable-http.js"
|
|
297
297
|
},
|
|
298
|
+
"./personal-memory": {
|
|
299
|
+
"types": "./dist/personal-memory.d.ts",
|
|
300
|
+
"import": "./dist/personal-memory.js"
|
|
301
|
+
},
|
|
298
302
|
"./meeting-minutes": {
|
|
299
303
|
"types": "./dist/meeting-minutes.d.ts",
|
|
300
304
|
"import": "./dist/meeting-minutes.js"
|
|
@@ -328,6 +332,11 @@
|
|
|
328
332
|
"./ui/world-model-view.css": "./ui/world-model-view.css",
|
|
329
333
|
"./ui/people-and-companies-view": "./ui/people-and-companies-view.js",
|
|
330
334
|
"./ui/people-and-companies-view.css": "./ui/people-and-companies-view.css",
|
|
335
|
+
"./ui/personal-memory": "./ui/personal-memory.js",
|
|
336
|
+
"./ui/personal-memory.css": "./ui/personal-memory.css",
|
|
337
|
+
"./ui/personal-memory-http-port": "./ui/personal-memory-http-port.js",
|
|
338
|
+
"./ui/area-nav": "./ui/area-nav.js",
|
|
339
|
+
"./ui/area-nav.css": "./ui/area-nav.css",
|
|
331
340
|
"./ui/local-web-shell": "./ui/local-web-shell.js",
|
|
332
341
|
"./ui/local-web-shell.css": "./ui/local-web-shell.css",
|
|
333
342
|
"./ui/workspace-kit": "./ui/workspace-kit.js",
|
|
@@ -342,6 +351,8 @@
|
|
|
342
351
|
"./ui/graph-projects-view.css": "./ui/graph-projects-view.css",
|
|
343
352
|
"./ui/graph-registry-view": "./ui/graph-registry-view.js",
|
|
344
353
|
"./ui/graph-registry-view.css": "./ui/graph-registry-view.css",
|
|
354
|
+
"./ui/graph-ontology-view": "./ui/graph-ontology-view.js",
|
|
355
|
+
"./ui/graph-ontology-view.css": "./ui/graph-ontology-view.css",
|
|
345
356
|
"./ui/meeting-minutes-storage": "./ui/meeting-minutes-storage.js",
|
|
346
357
|
"./ui/meeting-minutes-storage.css": "./ui/meeting-minutes-storage.css",
|
|
347
358
|
"./ui/brainbase-tokens.css": "./ui/brainbase-tokens.css",
|
|
@@ -483,5 +494,5 @@
|
|
|
483
494
|
"engines": {
|
|
484
495
|
"node": ">=20"
|
|
485
496
|
},
|
|
486
|
-
"gitHead": "
|
|
497
|
+
"gitHead": "35fc142733aac0f385f2d21e6d0651e1a9d8d7db"
|
|
487
498
|
}
|
package/ui/area-nav.css
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Area navigation (ledger D10, stage 2): the area links on the left and the
|
|
3
|
+
* tabs of the current area above the page. Uses only the shared tokens in
|
|
4
|
+
* brainbase-tokens.css; a host gives the area links its own look through
|
|
5
|
+
* `linkClassName`.
|
|
6
|
+
*/
|
|
7
|
+
.bb-area-nav {
|
|
8
|
+
display: grid;
|
|
9
|
+
gap: 2px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.bb-area-link[hidden],
|
|
13
|
+
.bb-area-tabs[hidden] {
|
|
14
|
+
display: none;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.bb-area-link-label {
|
|
18
|
+
min-width: 0;
|
|
19
|
+
flex: 1 1 auto;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.bb-area-count {
|
|
23
|
+
min-width: 20px;
|
|
24
|
+
flex: 0 0 auto;
|
|
25
|
+
border-radius: 999px;
|
|
26
|
+
padding: 0 6px;
|
|
27
|
+
background: var(--bb-color-accent-soft);
|
|
28
|
+
color: var(--bb-color-accent);
|
|
29
|
+
font-size: 11px;
|
|
30
|
+
font-weight: 700;
|
|
31
|
+
line-height: 18px;
|
|
32
|
+
text-align: center;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.bb-area-tabs {
|
|
36
|
+
display: flex;
|
|
37
|
+
gap: var(--bb-space-1);
|
|
38
|
+
overflow-x: auto;
|
|
39
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
40
|
+
padding: 0 var(--bb-space-5);
|
|
41
|
+
background: var(--bb-color-paper);
|
|
42
|
+
scrollbar-width: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.bb-area-tab {
|
|
46
|
+
min-height: 44px;
|
|
47
|
+
display: inline-flex;
|
|
48
|
+
flex: 0 0 auto;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: var(--bb-space-2);
|
|
51
|
+
border-bottom: 2px solid transparent;
|
|
52
|
+
padding: 0 var(--bb-space-3);
|
|
53
|
+
color: var(--bb-color-muted);
|
|
54
|
+
font-size: 13px;
|
|
55
|
+
font-weight: 620;
|
|
56
|
+
text-decoration: none;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.bb-area-tab:hover {
|
|
61
|
+
color: var(--bb-color-ink);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.bb-area-tab[aria-current="page"] {
|
|
65
|
+
border-bottom-color: var(--bb-color-accent);
|
|
66
|
+
color: var(--bb-color-accent);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.bb-area-tab:focus-visible,
|
|
70
|
+
.bb-area-link:focus-visible {
|
|
71
|
+
outline: 2px solid var(--bb-color-accent);
|
|
72
|
+
outline-offset: -2px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@media (max-width: 760px) {
|
|
76
|
+
.bb-area-tabs {
|
|
77
|
+
padding: 0 var(--bb-space-4);
|
|
78
|
+
}
|
|
79
|
+
}
|