@matatbread/matbot-core 0.3.6 → 0.3.8

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
@@ -4,7 +4,7 @@
4
4
  "node",
5
5
  "browser"
6
6
  ],
7
- "version": "0.3.6",
7
+ "version": "0.3.8",
8
8
  "engines": {
9
9
  "node": ">=24.0.0"
10
10
  },
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@matatbread/matbot-plugin-api": "^0.3.6"
38
+ "@matatbread/matbot-plugin-api": "^0.3.8"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "^6.0.3"
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export { unifyServices } from '@matatbread/matbot-plugin-api';
19
19
  export { forwardingProxy, makeSwappable, singleTurnRequest, createMountTable } from '@matatbread/matbot-plugin-api';
20
20
  // Observation-stream primitives (broadcaster + the bare-subscribe wrapper); the host wires `mounted`.
21
21
  export { createBroadcaster, subscribable } from '@matatbread/matbot-plugin-api';
22
+ export { createNotifier, scopedNotifier, notifyingStore, ItemChangeKind, RegistryChangeKind } from '@matatbread/matbot-plugin-api';
22
23
  export type { Subscribable, Broadcaster } from '@matatbread/matbot-plugin-api';
23
24
  // Ambient security principal: interface is a type (carried by `export type *`); these are the values.
24
25
  export {
package/src/loader.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { MatbotPlugin, MatbotPluginSpec, MatbotMachine, PluginSource, Runtime } from './plugin.js';
2
2
  import type { PromptFn } from './types.js';
3
3
  import { incompatibleRuntimeError, notAPluginError } from '@matatbread/matbot-plugin-api';
4
- import { registerPlugin, setupPlugin, unloadPlugin, recordFailedPlugin, clearFailedPlugin } from './registry.js';
4
+ import { registerPlugin, setupPlugin, unloadPlugin, recordFailedPlugin, clearFailedPlugin, announcePluginLoaded } from './registry.js';
5
5
 
6
6
  /**
7
7
  * The runtime this process is executing in. Detected the same way the rejected-import branch below
@@ -235,6 +235,9 @@ export async function loadPlugins(
235
235
  try {
236
236
  registerPlugin(plugin);
237
237
  registered = true;
238
+ // Announced at registration, not after setup: a setup throw rolls back through unloadPlugin, which
239
+ // announces the removal — so an observer sees a matched add/remove pair rather than a bare removal.
240
+ announcePluginLoaded(services, plugin.name);
238
241
  await setupPlugin(plugin, services, prompt);
239
242
  loaded.push(plugin);
240
243
  // Successful (re)load: a fixed plugin drops off the failed list (reload runs this same path).
package/src/registry.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Tool, ToolRegistry, Hook, PromptFn, FormField, FrontendInfo, PluginRegistryEvent, ProviderAdapter, ProviderConfig } from './types.js';
2
- import { createBroadcaster } from '@matatbread/matbot-plugin-api';
1
+ import type { Tool, ToolRegistry, Hook, PromptFn, FormField, FrontendInfo, ProviderAdapter, ProviderConfig } from './types.js';
2
+ import { scopedNotifier, RegistryChangeKind } from '@matatbread/matbot-plugin-api';
3
3
  import type {
4
4
  MatbotPlugin, MatbotMachine, MatbotRuntime, Mounted,
5
5
  ProviderAdapterFactory, StoreFactory,
@@ -34,14 +34,13 @@ const state = {
34
34
  overwriteAllTools: undefined as boolean | undefined, // persisted "overwrite on collision, this install" choice, loaded lazily
35
35
  };
36
36
 
37
- // Read-only observation of plugin load/unload, for consumers that key off plugin presence (e.g. the
38
- // web plugins panel refreshing live when a backend restores a plugin set out of band). Module-level,
39
- // not in `state` — it owns subscribers across the registry's lifetime, not per-plugin data.
40
- const pluginEvents = createBroadcaster<PluginRegistryEvent>();
41
-
42
- export async function* watchPlugins(signal?: AbortSignal): AsyncIterable<PluginRegistryEvent> {
43
- // Plugin CRUD is global — no origin — so unwrap the envelope to the bare event the panel expects.
44
- for await (const { value } of pluginEvents.subscribe(signal)) yield value;
37
+ // Plugin load/unload is announced on the Notifier as a `RegistryChange` with `registry: 'plugins'`, for
38
+ // consumers that key off plugin presence (e.g. the web plugins panel refreshing live when a backend
39
+ // restores a plugin set out of band). It was a module-level broadcaster of its own, which is the same
40
+ // primitive the bus already is. `loaded` is announced by the loader — the only caller of
41
+ // registerPlugin, and the one place holding the machine this registry deliberately doesn't.
42
+ export function announcePluginLoaded(services: MatbotMachine, name: string): void {
43
+ services.Notifier.notify({ kind: RegistryChangeKind, source: 'plugins', registry: 'plugins', name, operation: 'added' });
45
44
  }
46
45
 
47
46
  // Settings namespace + key under which the user's "overwrite all colliding tools" choice
@@ -150,8 +149,6 @@ export function registerPlugin(plugin: MatbotPlugin): void {
150
149
  for (const [type, factory] of Object.entries(plugin.storage ?? {})) {
151
150
  state.storage.set(type, factory);
152
151
  }
153
-
154
- pluginEvents.emit({ type: 'loaded', name: plugin.name });
155
152
  }
156
153
 
157
154
  // ── Resolution ────────────────────────────────────────────────────────────────
@@ -331,13 +328,16 @@ export async function setupPlugin(plugin: MatbotPlugin, services: MatbotMachine,
331
328
  specifier: plugin.specifier,
332
329
  ...(plugin.source !== undefined ? { source: plugin.source } : {}),
333
330
  },
331
+ // Everything this plugin publishes is attributed to it by default — the notification analogue of
332
+ // stamping `pluginName` on its tools. Reads through the host's swap proxy, so a registered
333
+ // distributed Notifier takes effect for a plugin that captured this in setup().
334
+ get Notifier() { return scopedNotifier(services.Notifier, plugin.name); },
334
335
  tools: {
335
336
  register: registerTool,
336
337
  remove: (name: string) => services.tools.remove(name),
337
338
  resolve: (name: string) => services.tools.resolve(name),
338
339
  list: () => services.tools.list(),
339
340
  removeByPlugin:(name: string) => services.tools.removeByPlugin(name),
340
- watch: (signal?: AbortSignal) => services.tools.watch(signal),
341
341
  },
342
342
  hooks: {
343
343
  register(hook: Hook) {
@@ -396,7 +396,7 @@ export async function unloadPlugin(pluginName: string, services: MatbotMachine):
396
396
  state.frontendPlugins.delete(pluginName);
397
397
 
398
398
  state.plugins.splice(idx, 1);
399
- pluginEvents.emit({ type: 'unloaded', name: pluginName });
399
+ services.Notifier.notify({ kind: RegistryChangeKind, source: 'plugins', registry: 'plugins', name: pluginName, operation: 'removed' });
400
400
  await Promise.race([
401
401
  plugin.teardown?.(),
402
402
  new Promise<void>((_, reject) => setTimeout(() => reject(new Error(`Teardown timeout for plugin ${pluginName}`)), 10000))
@@ -93,11 +93,11 @@ function validateFilter(f: Filter, ptr: string): void {
93
93
  }
94
94
  }
95
95
 
96
- const QUERY_KEYS = new Set(['where', 'sort', 'limit', 'cursor']);
96
+ const QUERY_KEYS = new Set(['where', 'sort', 'limit', 'cursor', 'immutable']);
97
97
 
98
98
  export function validateQuery(q: StoreQuery): void {
99
99
  if (q === null || typeof q !== 'object' || Array.isArray(q))
100
- throw new StoreQueryError('query must be an object with optional keys: where, sort, limit, cursor', '/', 'MALFORMED');
100
+ throw new StoreQueryError('query must be an object with optional keys: where, sort, limit, cursor, immutable', '/', 'MALFORMED');
101
101
 
102
102
  // Reject unknown top-level keys. Without this the envelope is silently permissive: a clause placed
103
103
  // under a wrong key (e.g. `filter` instead of `where`, an SQL-ism LLMs reach for) is dropped, the
@@ -1,26 +1,37 @@
1
- import type { Tool, ToolRegistry, ToolRegistryEvent } from './types.js';
2
- import { createBroadcaster } from '@matatbread/matbot-plugin-api';
1
+ import { RegistryChangeKind } from '@matatbread/matbot-plugin-api';
2
+ import type { Tool, ToolRegistry, Notifier } from './types.js';
3
3
 
4
4
  export class ToolRegistryImpl implements ToolRegistry {
5
5
  private readonly tools = new Map<string, Tool>();
6
- private readonly events = createBroadcaster<ToolRegistryEvent>();
6
+ // The host's notifier proxy, injected at boot (both hosts build it before this registry). Held as the
7
+ // proxy, not a resolved impl, so a later register('Notifier', …) takes effect here too. Optional: a
8
+ // registry built without one (tests) simply announces nothing.
9
+ private readonly notifier: Notifier | undefined;
7
10
 
8
- constructor(initial?: Iterable<Tool>) {
11
+ constructor(initial?: Iterable<Tool>, notifier?: Notifier) {
9
12
  if (initial !== undefined) for (const tool of initial) this.tools.set(tool.name, tool);
13
+ this.notifier = notifier;
14
+ }
15
+
16
+ private announce(name: string, operation: 'added' | 'removed', pluginName?: string): void {
17
+ this.notifier?.notify({
18
+ kind: RegistryChangeKind, source: 'tools', registry: 'tools', name, operation,
19
+ ...(pluginName !== undefined ? { detail: { pluginName } } : {}),
20
+ });
10
21
  }
11
22
 
12
23
  register(tool: Tool): void {
13
24
  this.tools.set(tool.name, tool);
14
- this.events.emit({ type: 'registered', name: tool.name, ...(tool.pluginName !== undefined ? { pluginName: tool.pluginName } : {}) });
25
+ this.announce(tool.name, 'added', tool.pluginName);
15
26
  }
16
27
 
17
28
  remove(name: string): void {
18
- if (this.tools.delete(name)) this.events.emit({ type: 'removed', name });
29
+ if (this.tools.delete(name)) this.announce(name, 'removed');
19
30
  }
20
31
 
21
32
  removeByPlugin(pluginName: string): void {
22
33
  for (const [name, tool] of this.tools) {
23
- if (tool.pluginName === pluginName && this.tools.delete(name)) this.events.emit({ type: 'removed', name });
34
+ if (tool.pluginName === pluginName && this.tools.delete(name)) this.announce(name, 'removed');
24
35
  }
25
36
  }
26
37
 
@@ -31,9 +42,4 @@ export class ToolRegistryImpl implements ToolRegistry {
31
42
  list(): Tool[] {
32
43
  return [...this.tools.values()];
33
44
  }
34
-
35
- async *watch(signal?: AbortSignal): AsyncIterable<ToolRegistryEvent> {
36
- // Tool CRUD is global — no origin — so unwrap the envelope to the bare event.
37
- for await (const { value } of this.events.subscribe(signal)) yield value;
38
- }
39
45
  }