@unson/brainbase-mcp 0.10.10 → 0.10.12

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.
@@ -5,9 +5,9 @@ import { type ObjectiveDefinitionBuilder } from './foundation-http.js';
5
5
  *
6
6
  * One loopback server carries every local screen. The host owns the
7
7
  * protections, the trusted principal and the static allowlist; each screen's
8
- * server side is a `LocalWebModule`. To add a screen, add its module to
9
- * `defaultLocalWebModules()` and its nav entry to `LOCAL_WEB_SCREENS` in
10
- * `ui/local-web-shell.js`.
8
+ * server side is a `LocalWebModule`. Public screens live in
9
+ * `defaultLocalWebModules()` and `LOCAL_WEB_SCREENS`; private products register
10
+ * a `LocalWebExtension` without copying either one.
11
11
  */
12
12
  export declare const LOCAL_WEB_HOST_VERSION: "local-web-host.v1";
13
13
  /** One per-launch token guards every write on this host, including value-proof feedback. */
@@ -50,6 +50,17 @@ export interface LocalWebModule {
50
50
  readonly uiFiles: readonly string[];
51
51
  handle(request: IncomingMessage, response: ServerResponse): Promise<boolean>;
52
52
  }
53
+ /** Private products add one screen and one namespaced API without changing OSS routes. */
54
+ export interface LocalWebExtension {
55
+ /** Lowercase URL segment, unique among extensions. */
56
+ readonly id: string;
57
+ /** Packaged directory containing only the files listed in uiFiles. */
58
+ readonly uiDir: string;
59
+ readonly uiFiles: readonly string[];
60
+ /** ES module exporting a `screen` compatible with createLocalWebShell. */
61
+ readonly screenEntry: string;
62
+ createModule(context: LocalWebModuleContext): LocalWebModule;
63
+ }
53
64
  export interface LocalWebHostOptions {
54
65
  /** Personal OS data directory. Defaults to `BRAINBASE_PERSONAL_OS_DIR` or `~/.brainbase/personal-os`. */
55
66
  readonly dataDir?: string;
@@ -59,6 +70,7 @@ export interface LocalWebHostOptions {
59
70
  /** Directory containing the packaged `ui/` files. */
60
71
  readonly uiDir?: string;
61
72
  readonly now?: () => Date;
73
+ readonly extensions?: readonly LocalWebExtension[];
62
74
  }
63
75
  export interface LocalWebHost {
64
76
  readonly server: Server;
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { readFile, stat } from 'node:fs/promises';
2
+ import { lstat, readFile, stat } from 'node:fs/promises';
3
3
  import { createServer } from 'node:http';
4
4
  import { join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
@@ -20,9 +20,9 @@ import { createWorldModelStore, WORLD_MODEL_EVIDENCE_SIDECAR, WorldModelStoreErr
20
20
  *
21
21
  * One loopback server carries every local screen. The host owns the
22
22
  * protections, the trusted principal and the static allowlist; each screen's
23
- * server side is a `LocalWebModule`. To add a screen, add its module to
24
- * `defaultLocalWebModules()` and its nav entry to `LOCAL_WEB_SCREENS` in
25
- * `ui/local-web-shell.js`.
23
+ * server side is a `LocalWebModule`. Public screens live in
24
+ * `defaultLocalWebModules()` and `LOCAL_WEB_SCREENS`; private products register
25
+ * a `LocalWebExtension` without copying either one.
26
26
  */
27
27
  export const LOCAL_WEB_HOST_VERSION = 'local-web-host.v1';
28
28
  /** One per-launch token guards every write on this host, including value-proof feedback. */
@@ -598,7 +598,7 @@ function shellHtml(token, stylesheets) {
598
598
  <meta name="viewport" content="width=device-width, initial-scale=1">
599
599
  <meta name="brainbase-web-token" content="${escapeAttribute(token)}">
600
600
  <title>Brainbase</title>
601
- ${stylesheets.map((file) => `<link rel="stylesheet" href="/ui/${escapeAttribute(file)}">`).join('\n')}
601
+ ${stylesheets.map((path) => `<link rel="stylesheet" href="${escapeAttribute(path)}">`).join('\n')}
602
602
  </head>
603
603
  <body>
604
604
  <div id="brainbase-local-web"></div>
@@ -607,16 +607,31 @@ ${stylesheets.map((file) => `<link rel="stylesheet" href="/ui/${escapeAttribute(
607
607
  </html>
608
608
  `;
609
609
  }
610
- const BOOTSTRAP_JS = `import { createLocalWebShell } from '/ui/local-web-shell.js';
610
+ function bootstrapJs(extensions) {
611
+ const imports = extensions.map((extension, index) => `import { screen as extensionScreen${index} } from '/ui/extensions/${extension.id}/${extension.screenEntry}';`).join('\n');
612
+ const screens = extensions.map((_, index) => `extensionScreen${index}`).join(', ');
613
+ const ids = JSON.stringify(extensions.map((extension) => extension.id));
614
+ return `import { createLocalWebShell, LOCAL_WEB_SCREENS } from '/ui/local-web-shell.js';
615
+ ${imports}
616
+ const extensionScreens = [${screens}];
617
+ const extensionIds = ${ids};
618
+ if (extensionScreens.some((screen, index) => screen?.id !== extensionIds[index] || LOCAL_WEB_SCREENS.some((base) => base.id === screen.id))) {
619
+ throw new Error('Invalid extension screen id');
620
+ }
611
621
  const meta = (name) => document.querySelector(\`meta[name="\${name}"]\`)?.getAttribute('content') ?? '';
612
622
  const screenFromHash = () => decodeURIComponent(location.hash.replace(/^#/, '')) || undefined;
613
623
  const shell = createLocalWebShell({
614
624
  root: document.getElementById('brainbase-local-web'),
615
625
  token: meta('brainbase-web-token'),
616
- initialScreen: screenFromHash()
626
+ initialScreen: screenFromHash(),
627
+ screens: [...LOCAL_WEB_SCREENS, ...extensionScreens]
617
628
  });
618
629
  addEventListener('hashchange', () => shell.show(screenFromHash()));
619
630
  `;
631
+ }
632
+ function validExtensionFile(file) {
633
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:js|css)$/u.test(file) && !file.includes('..');
634
+ }
620
635
  /** Local single-owner host. The caller owns `listen()` and must bind to a loopback address. */
621
636
  export function createLocalWebHost(options = {}) {
622
637
  const token = options.token ?? randomBytes(24).toString('base64url');
@@ -627,15 +642,40 @@ export function createLocalWebHost(options = {}) {
627
642
  const uiDir = options.uiDir ?? fileURLToPath(new URL('../ui/', import.meta.url));
628
643
  const context = { dataDir, journalRoot, token, now: options.now ?? (() => new Date()) };
629
644
  const modules = defaultLocalWebModules(context);
645
+ const extensions = [...options.extensions ?? []];
646
+ const extensionModules = [];
647
+ const ids = new Set();
630
648
  const uiFiles = new Map();
631
649
  for (const file of [...BASE_UI_FILES, ...modules.flatMap((module) => module.uiFiles)]) {
632
650
  const extension = file.slice(file.lastIndexOf('.'));
633
651
  const type = CONTENT_TYPES[extension];
634
- if (!type || file.includes('/'))
652
+ if (!type || !validExtensionFile(file))
635
653
  throw new TypeError(`Unsupported UI file ${file}`);
636
- uiFiles.set(`/ui/${file}`, type);
654
+ uiFiles.set(`/ui/${file}`, { type, root: uiDir, file });
655
+ }
656
+ for (const extension of extensions) {
657
+ if (!/^[a-z][a-z0-9-]*$/u.test(extension.id) || ids.has(extension.id)) {
658
+ throw new TypeError(`Invalid or duplicate extension id ${extension.id}`);
659
+ }
660
+ ids.add(extension.id);
661
+ if (!extension.uiFiles.includes(extension.screenEntry) || !extension.screenEntry.endsWith('.js')) {
662
+ throw new TypeError(`Extension screen entry must be an allowlisted JS file: ${extension.id}`);
663
+ }
664
+ const module = extension.createModule(context);
665
+ if (module.id !== extension.id || module.uiFiles.length !== 0) {
666
+ throw new TypeError(`Extension module must use its id and declare UI files on the extension: ${extension.id}`);
667
+ }
668
+ extensionModules.push({ prefix: `/api/extensions/${extension.id}`, module });
669
+ for (const file of extension.uiFiles) {
670
+ if (!validExtensionFile(file))
671
+ throw new TypeError(`Unsupported extension UI file ${file}`);
672
+ const path = `/ui/extensions/${extension.id}/${file}`;
673
+ if (uiFiles.has(path))
674
+ throw new TypeError(`Duplicate extension UI file ${path}`);
675
+ uiFiles.set(path, { type: CONTENT_TYPES[file.slice(file.lastIndexOf('.'))], root: extension.uiDir, file });
676
+ }
637
677
  }
638
- const stylesheets = [...uiFiles.keys()].filter((path) => path.endsWith('.css')).map((path) => path.slice('/ui/'.length));
678
+ const stylesheets = [...uiFiles.keys()].filter((path) => path.endsWith('.css'));
639
679
  async function handle(request, response) {
640
680
  if (!isLoopbackHost(request)) {
641
681
  writeJson(response, 403, { error: { code: 'host_rejected', message: 'Host must be a loopback address for this server' } });
@@ -653,6 +693,14 @@ export function createLocalWebHost(options = {}) {
653
693
  return;
654
694
  }
655
695
  const path = requestUrl(request).pathname;
696
+ for (const extension of extensionModules) {
697
+ if (path === extension.prefix || path.startsWith(`${extension.prefix}/`)) {
698
+ if (await extension.module.handle(request, response))
699
+ return;
700
+ writeJson(response, 404, { error: { code: 'not_found', message: 'Not found' } });
701
+ return;
702
+ }
703
+ }
656
704
  if (request.method !== 'GET') {
657
705
  writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'Use GET' } });
658
706
  return;
@@ -668,16 +716,21 @@ export function createLocalWebHost(options = {}) {
668
716
  }
669
717
  if (path === '/app.js') {
670
718
  response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
671
- response.end(BOOTSTRAP_JS);
719
+ response.end(bootstrapJs(extensions));
720
+ return;
721
+ }
722
+ const asset = uiFiles.get(path);
723
+ if (!asset) {
724
+ writeJson(response, 404, { error: { code: 'not_found', message: 'Not found' } });
672
725
  return;
673
726
  }
674
- const type = uiFiles.get(path);
675
- if (!type) {
727
+ const target = join(asset.root, asset.file);
728
+ if (!(await lstat(target)).isFile()) {
676
729
  writeJson(response, 404, { error: { code: 'not_found', message: 'Not found' } });
677
730
  return;
678
731
  }
679
- response.setHeader('Content-Type', type);
680
- response.end(await readFile(join(uiDir, path.slice('/ui/'.length))));
732
+ response.setHeader('Content-Type', asset.type);
733
+ response.end(await readFile(target));
681
734
  }
682
735
  const server = createServer((request, response) => {
683
736
  void handle(request, response).catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.10.10",
3
+ "version": "0.10.12",
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",
@@ -155,6 +155,10 @@
155
155
  "types": "./dist/personal-knowledge.d.ts",
156
156
  "import": "./dist/personal-knowledge.js"
157
157
  },
158
+ "./knowledge-promotion": {
159
+ "types": "./dist/knowledge-promotion.d.ts",
160
+ "import": "./dist/knowledge-promotion.js"
161
+ },
158
162
  "./organization-connection": {
159
163
  "types": "./dist/organization-connection.d.ts",
160
164
  "import": "./dist/organization-connection.js"
@@ -336,5 +340,5 @@
336
340
  "engines": {
337
341
  "node": ">=20"
338
342
  },
339
- "gitHead": "12af97cb3f02f9964c7bb7ca8ba7a5debdf29e11"
343
+ "gitHead": "10493d41000c832bf85c33fc30e920a4c194fe61"
340
344
  }
@@ -11,9 +11,8 @@
11
11
  * mounted until the host reports Graph v2; a v1 Graph shows the migration
12
12
  * commands instead of zero items, and the host never migrates by itself.
13
13
  *
14
- * To add a screen: append `{ id, label, usesGraph, mount(container, context) }`
15
- * to LOCAL_WEB_SCREENS (or pass `screens`) and add its server module to
16
- * `defaultLocalWebModules()` in `src/local-web-host.ts`.
14
+ * Public screens are listed in LOCAL_WEB_SCREENS. Private products pass an
15
+ * extended `screens` list through LocalWebHostOptions.extensions.
17
16
  */
18
17
 
19
18
  import { createGraphProjectsView } from './graph-projects-view.js';