engineering-memory 1.11.7 → 1.11.9

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.
@@ -399,9 +399,15 @@ export class BridgeService {
399
399
  (repository.projectId && repository.projectId !== projectId)) {
400
400
  throw refuse('The live task belongs to a different project than the one this repository is bound to.', 'project.resolve');
401
401
  }
402
- if (pointer?.worktreeGeneration && this.dependencies.worktreePool) {
402
+ const adoptable = pointer &&
403
+ !pointer.worktreeGeneration &&
404
+ normalizeTaskMode(pointer.mode) !== 'read_only' &&
405
+ this.dependencies.worktreePool
406
+ ? await this.dependencies.worktreePool.find(projectId, taskSlug, repository.repoRoot)
407
+ : null;
408
+ if (pointer && this.dependencies.worktreePool && (pointer.worktreeGeneration || adoptable)) {
403
409
  const pool = this.dependencies.worktreePool;
404
- const previous = await pool.find(projectId, taskSlug, repository.repoRoot);
410
+ const previous = adoptable ?? (await pool.find(projectId, taskSlug, repository.repoRoot));
405
411
  if (previous && previous.repoRoot !== repository.repoRoot)
406
412
  return asJsonValue({
407
413
  worktreeResumeRequired: true,
@@ -411,8 +417,11 @@ export class BridgeService {
411
417
  nextAction: 'Call session.resume from this returned repoRoot and use it for every subsequent command.',
412
418
  });
413
419
  const sourceCommit = previous?.baseCommit ?? pointer.source?.sourceCommit;
420
+ const branch = pointer.worktreeGeneration
421
+ ? pointer.branch
422
+ : (previous?.branch ?? pointer.branch);
414
423
  if ((!sourceCommit && !(previous && previous.baseCommit === null && !previous.managed)) ||
415
- pointer.branch === undefined)
424
+ branch === undefined)
416
425
  throw refuse('The task branch and base must be restored before resuming its worktree.', 'worktree.reconcile');
417
426
  const allocation = await pool.allocate({
418
427
  projectId,
@@ -420,7 +429,7 @@ export class BridgeService {
420
429
  repoFingerprint: repository.repoFingerprint,
421
430
  repoRoot: repository.repoRoot,
422
431
  externalTaskId: taskSlug,
423
- branch: pointer.branch,
432
+ branch,
424
433
  baseCommit: sourceCommit ?? null,
425
434
  keepCurrent: previous ? !previous.managed : false,
426
435
  resume: true,
@@ -433,6 +442,7 @@ export class BridgeService {
433
442
  ...pointer,
434
443
  worktreeId: sha256(allocation.repoRoot),
435
444
  worktreeGeneration: allocation.generation,
445
+ ...(branch !== undefined ? { branch } : {}),
436
446
  };
437
447
  await this.dependencies.activeContexts.save(pointer);
438
448
  if (allocation.repoRoot !== repository.repoRoot)
@@ -449,7 +459,7 @@ export class BridgeService {
449
459
  const recoveredClose = await this.retryCloseIntent(repository, pointer);
450
460
  return asJsonValue({
451
461
  backend: { close: recoveredClose },
452
- worktree: this.poolAllocations.get(repository.repoRoot) ?? null,
462
+ worktree: (await this.ownedAllocation(projectId, repository.repoRoot)) ?? null,
453
463
  pendingQuestionnaires,
454
464
  repository: publicRepository(repository),
455
465
  localJournal: await this.dependencies.journal.load(projectId, taskSlug),
@@ -632,7 +642,7 @@ export class BridgeService {
632
642
  return asJsonValue({
633
643
  backend,
634
644
  repository: publicRepository(repository),
635
- worktree: this.poolAllocations.get(repository.repoRoot) ?? null,
645
+ worktree: (await this.ownedAllocation(projectId, repository.repoRoot)) ?? null,
636
646
  localJournal,
637
647
  pendingQuestionnaires,
638
648
  synchronization: {
@@ -731,7 +741,7 @@ export class BridgeService {
731
741
  transitionTaskVersion = refreshed.taskVersion;
732
742
  }
733
743
  if (decision && this.dependencies.worktreePool) {
734
- const allocation = this.poolAllocations.get(repository.repoRoot);
744
+ const allocation = await this.ownedAllocation(pointer.projectId, repository.repoRoot);
735
745
  if (!allocation || allocation.externalTaskId !== pointer.taskSlug)
736
746
  throw refuse('Choose this task branch and use its allocated repoRoot before transitioning to write.', 'task.branch');
737
747
  await this.dependencies.worktreePool.assertOwnership(pointer.projectId, repository.repoRoot, allocation.generation);
@@ -792,16 +802,14 @@ export class BridgeService {
792
802
  await this.dependencies.repositories.git
793
803
  .branchStore(repository.repoRoot)
794
804
  .bind(preparedPointer.projectId, preparedPointer.taskSlug, preparedPointer.taskId);
805
+ const allocation = await this.ownedAllocation(preparedPointer.projectId, repository.repoRoot);
795
806
  await this.dependencies.activeContexts.save({
796
807
  ...preparedPointer,
797
808
  worktreeId: sha256(repository.repoRoot),
798
809
  branch: typeof preparedTask?.branch === 'string' ? preparedTask.branch : decision.branch,
799
810
  mode: 'write',
800
- ...(this.poolAllocations.get(repository.repoRoot)
801
- ? { worktreeGeneration: this.poolAllocations.get(repository.repoRoot).generation }
802
- : {}),
811
+ ...(allocation ? { worktreeGeneration: allocation.generation } : {}),
803
812
  });
804
- const allocation = this.poolAllocations.get(repository.repoRoot);
805
813
  if (allocation && this.dependencies.worktreePool) {
806
814
  await this.dependencies.worktreePool.bind(preparedPointer.projectId, repository.repoRoot, allocation.generation, preparedPointer.taskId);
807
815
  allocation.taskId = preparedPointer.taskId;
@@ -1884,11 +1892,26 @@ export class BridgeService {
1884
1892
  const pool = this.dependencies.worktreePool;
1885
1893
  if (!projectId || !pool)
1886
1894
  throw refuse('Resolve the project with a pool-capable client.', 'project.resolve');
1887
- if (operation === 'list' || operation === 'reconcile')
1895
+ if (operation === 'list' || operation === 'reconcile') {
1896
+ const managedRoot = await pool.managedRoot();
1888
1897
  return asJsonValue({
1889
1898
  items: await pool.reconcile(projectId, repository.repoRoot, await this.poolPolicy()),
1890
1899
  selected: await pool.forPath(projectId, repository.repoRoot),
1900
+ managedRoot,
1901
+ ...(managedRoot.problem || managedRoot.synced
1902
+ ? {
1903
+ notice: [
1904
+ ...(managedRoot.problem ? [managedRoot.problem] : []),
1905
+ ...(managedRoot.synced
1906
+ ? [
1907
+ `Managed worktrees are created under ${managedRoot.root}, which a cloud sync client watches, so every Git operation there is re-scanned and uploaded. To create new worktrees elsewhere, write {"root": "<absolute directory>"} to ${managedRoot.override}; it takes effect for the next allocation, and existing worktrees keep working where they are.`,
1908
+ ]
1909
+ : []),
1910
+ ].join(' '),
1911
+ }
1912
+ : {}),
1891
1913
  });
1914
+ }
1892
1915
  if (operation === 'release_unowned') {
1893
1916
  const entry = await pool.forPath(projectId, repository.repoRoot);
1894
1917
  if (!entry || !input.sourceCommit || !input.generation || !input.decisionId)
@@ -1974,12 +1997,46 @@ export class BridgeService {
1974
1997
  resume: Boolean(existing),
1975
1998
  }, await this.poolPolicy(true));
1976
1999
  this.poolAllocations.set(allocation.repoRoot, allocation);
2000
+ if (existing && allocation.repoRoot === repository.repoRoot) {
2001
+ const current = await this.dependencies.activeContexts.loadForSlug(repository.repoFingerprint, input.externalTaskId);
2002
+ if (current &&
2003
+ current.taskSlug === input.externalTaskId &&
2004
+ !current.worktreeGeneration &&
2005
+ normalizeTaskMode(current.mode) !== 'read_only')
2006
+ await this.dependencies.activeContexts.save({
2007
+ ...current,
2008
+ worktreeId: sha256(allocation.repoRoot),
2009
+ worktreeGeneration: allocation.generation,
2010
+ ...(current.branch === undefined && allocation.branch !== null
2011
+ ? { branch: allocation.branch }
2012
+ : {}),
2013
+ });
2014
+ }
1977
2015
  return asJsonValue({
1978
2016
  ...allocation,
1979
2017
  kept: !allocation.managed,
1980
2018
  nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot. Use task.heartbeat during long work. Pause before handing the task to another client.',
1981
2019
  });
1982
2020
  }
2021
+ adoptOwned(repoRoot, entry) {
2022
+ const cached = this.poolAllocations.get(repoRoot);
2023
+ if (cached)
2024
+ return cached;
2025
+ const pool = this.dependencies.worktreePool;
2026
+ if (!entry || !pool || !pool.owns(entry))
2027
+ return undefined;
2028
+ this.poolAllocations.set(repoRoot, entry);
2029
+ return entry;
2030
+ }
2031
+ async ownedAllocation(projectId, repoRoot) {
2032
+ const cached = this.poolAllocations.get(repoRoot);
2033
+ if (cached)
2034
+ return cached;
2035
+ const pool = this.dependencies.worktreePool;
2036
+ if (!pool)
2037
+ return undefined;
2038
+ return this.adoptOwned(repoRoot, await pool.forPath(projectId, repoRoot));
2039
+ }
1983
2040
  async assertPoolPointer(repository, pointer) {
1984
2041
  const pool = this.dependencies.worktreePool;
1985
2042
  if (!pool)
@@ -1987,11 +2044,10 @@ export class BridgeService {
1987
2044
  const entry = await pool.forPath(pointer.projectId, repository.repoRoot);
1988
2045
  if (!entry && !pointer.worktreeGeneration)
1989
2046
  return; // existing unmanaged tasks retain their legacy reservation
1990
- const owned = this.poolAllocations.get(repository.repoRoot);
1991
- if (!entry ||
1992
- !owned ||
1993
- owned.generation !== pointer.worktreeGeneration ||
1994
- entry.externalTaskId !== pointer.taskSlug)
2047
+ if (entry && entry.externalTaskId !== pointer.taskSlug)
2048
+ throw refuse(`This directory is allocated to task ${entry.externalTaskId}. Resume this task from its own worktree, or reconcile the pool to release the directory.`, 'worktree.reconcile');
2049
+ const owned = this.adoptOwned(repository.repoRoot, entry);
2050
+ if (!entry || !owned || owned.generation !== pointer.worktreeGeneration)
1995
2051
  throw refuse('Resume this task to acquire its worktree before writing or delivering.', 'session.resume');
1996
2052
  await pool.assertOwnership(pointer.projectId, repository.repoRoot, owned.generation);
1997
2053
  }
@@ -2117,7 +2173,9 @@ export class BridgeService {
2117
2173
  if (!abandoned || abandoned.abandoned !== true) {
2118
2174
  throw refuse('The backend did not record the abandonment, so the task is still open.', 'session.resume');
2119
2175
  }
2120
- const allocation = this.poolAllocations.get(repository.repoRoot);
2176
+ const allocation = repository.projectId
2177
+ ? await this.ownedAllocation(repository.projectId, repository.repoRoot)
2178
+ : this.poolAllocations.get(repository.repoRoot);
2121
2179
  if (allocation && this.dependencies.worktreePool) {
2122
2180
  await this.dependencies.worktreePool.pause(allocation.projectId, allocation.repoRoot, allocation.generation);
2123
2181
  this.poolAllocations.delete(repository.repoRoot);
@@ -3385,7 +3443,9 @@ export class BridgeService {
3385
3443
  taskChanges,
3386
3444
  });
3387
3445
  await this.dependencies.activeContexts.clearCloseIntent(repository.repoFingerprint, String(body.taskId));
3388
- const allocation = this.poolAllocations.get(repository.repoRoot);
3446
+ const allocation = repository.projectId
3447
+ ? await this.ownedAllocation(repository.projectId, repository.repoRoot)
3448
+ : this.poolAllocations.get(repository.repoRoot);
3389
3449
  if (allocation && this.dependencies.worktreePool)
3390
3450
  await this.dependencies.worktreePool.delivery(allocation.projectId, repository.repoRoot, allocation.generation);
3391
3451
  else
@@ -3471,7 +3531,7 @@ export class BridgeService {
3471
3531
  try {
3472
3532
  const allocation = await this.dependencies.worktreePool?.forTask(taskId);
3473
3533
  if (allocation) {
3474
- const owned = this.poolAllocations.get(allocation.repoRoot);
3534
+ const owned = this.adoptOwned(allocation.repoRoot, allocation);
3475
3535
  if (!owned || owned.generation !== allocation.generation)
3476
3536
  throw refuse('Resume the task in its owned worktree before this operation.', 'session.resume');
3477
3537
  await this.dependencies.worktreePool.assertOwnership(allocation.projectId, allocation.repoRoot, owned.generation);
@@ -0,0 +1,82 @@
1
+ import { WorktreePool } from './worktree-pool.js';
2
+ import { WorktreePolicyCache } from './worktree-policy.js';
3
+ import { BrowserAuthCoordinator } from '../auth/browser-auth.js';
4
+ import { createCredentialStore } from '../auth/credential-store.js';
5
+ import { EtagCache } from '../cache/etag-cache.js';
6
+ import { apiNamespaceKey, apiStateRoot, endpoints, loadBridgeConfig } from '../config.js';
7
+ import { GitInspector } from '../git/git-inspector.js';
8
+ import { VerificationGate } from '../git/verification-gate.js';
9
+ import { JournalStore } from '../journal/journal-store.js';
10
+ import { RepositoryResolver } from '../project/repository.js';
11
+ import { ApiClient } from './api-client.js';
12
+ import { BridgeService } from './bridge-service.js';
13
+ import { OfflineOutbox } from './offline-outbox.js';
14
+ import { ActiveContextStore } from './active-context-store.js';
15
+ import { RepositoryDecisionStore } from './repository-decision-store.js';
16
+ import { ShadowNoticeStore } from './shadow-notice-store.js';
17
+ import { UpdateChoiceStore } from './update-choice-store.js';
18
+ import { PrincipalStateGuard } from './principal-state.js';
19
+ import { OnboardingStore } from './onboarding-store.js';
20
+ import { QuestionnaireStore } from './questionnaire-store.js';
21
+ export function createBridgeService(options = {}) {
22
+ const config = loadBridgeConfig();
23
+ const apiNamespace = apiNamespaceKey(config.apiBaseUrl);
24
+ const stateRoot = apiStateRoot(config);
25
+ const credentials = createCredentialStore({
26
+ service: config.credentialService,
27
+ account: `${config.credentialAccount}:${apiNamespace}`,
28
+ stateRoot,
29
+ });
30
+ const cache = new EtagCache({
31
+ stateRoot,
32
+ maxEntries: config.cacheMaxEntries,
33
+ maxBytes: config.cacheMaxBytes,
34
+ maxAgeMs: config.cacheMaxAgeMs,
35
+ });
36
+ const client = new ApiClient({
37
+ baseUrl: config.apiBaseUrl,
38
+ timeoutMs: config.requestTimeoutMs,
39
+ credentials,
40
+ cache,
41
+ refreshPath: endpoints.authRefresh,
42
+ });
43
+ const git = new GitInspector();
44
+ const outbox = new OfflineOutbox(stateRoot);
45
+ const activeContexts = new ActiveContextStore(stateRoot);
46
+ const repositoryDecisions = new RepositoryDecisionStore(stateRoot);
47
+ const updateChoices = new UpdateChoiceStore(stateRoot);
48
+ const shadowNotices = new ShadowNoticeStore(stateRoot);
49
+ const worktreePool = new WorktreePool(stateRoot, {
50
+ ...(options.ownerId ? { ownerId: options.ownerId } : {}),
51
+ pendingWork: async (entry) => {
52
+ if (!entry.taskId)
53
+ return false;
54
+ const pointer = await activeContexts.loadForTask(entry.repoFingerprint, entry.taskId);
55
+ return Boolean(pointer?.verificationIntent ||
56
+ pointer?.closeIntent ||
57
+ (await outbox.listForTask(entry.taskId)).length);
58
+ },
59
+ });
60
+ const gate = new VerificationGate(stateRoot, git, outbox, undefined, worktreePool);
61
+ const principalState = new PrincipalStateGuard(stateRoot, credentials, cache, outbox, activeContexts, gate);
62
+ return new BridgeService({
63
+ worktreePool,
64
+ worktreePolicy: new WorktreePolicyCache(stateRoot),
65
+ client,
66
+ credentials,
67
+ browserAuth: new BrowserAuthCoordinator(client, credentials),
68
+ repositories: new RepositoryResolver(git, config.markerSchemaVersion, stateRoot),
69
+ journal: new JournalStore(stateRoot),
70
+ outbox,
71
+ gate,
72
+ activeContexts,
73
+ repositoryDecisions,
74
+ updateChoices,
75
+ shadowNotices,
76
+ questionnaires: new QuestionnaireStore(stateRoot),
77
+ onboarding: new OnboardingStore(stateRoot),
78
+ clientVersion: options.clientVersion === undefined ? config.clientVersion : options.clientVersion,
79
+ principalState,
80
+ });
81
+ }
82
+ //# sourceMappingURL=create-bridge-service.js.map
@@ -0,0 +1,16 @@
1
+ import { registerAllTools } from '../mcp/server.js';
2
+ import { recordingHost } from '../mcp/tool-host.js';
3
+ import { createBridgeService } from './create-bridge-service.js';
4
+ export function collectTools(service, server) {
5
+ const recording = recordingHost(server);
6
+ registerAllTools(recording.host, service);
7
+ return recording.tools;
8
+ }
9
+ export function createRuntime(options) {
10
+ const service = createBridgeService({
11
+ ownerId: options.ownerId,
12
+ clientVersion: options.clientVersion,
13
+ });
14
+ return { tools: collectTools(service, options.server) };
15
+ }
16
+ //# sourceMappingURL=runtime-entry.js.map
@@ -0,0 +1,265 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { z } from 'zod';
6
+ export const currentReleaseSchema = z.object({
7
+ product: z.literal('engineering-memory'),
8
+ schemaVersion: z.literal(1),
9
+ path: z.string().min(1),
10
+ version: z.string().min(1).nullable().optional(),
11
+ sourceHash: z.string().optional(),
12
+ bridgeEntry: z.string().optional(),
13
+ gateEntry: z.string().optional(),
14
+ });
15
+ export const runtimeEntryRelativePath = ['dist', 'src', 'runtime', 'runtime-entry.js'];
16
+ const emptyObjectSchema = { type: 'object', properties: {} };
17
+ export async function readCurrentRelease(currentPath) {
18
+ let raw;
19
+ try {
20
+ raw = await readFile(currentPath, 'utf8');
21
+ }
22
+ catch (error) {
23
+ return { problem: `${currentPath} cannot be read: ${describe(error)}` };
24
+ }
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(raw);
28
+ }
29
+ catch {
30
+ return { problem: `${currentPath} is not valid JSON` };
31
+ }
32
+ const release = currentReleaseSchema.safeParse(parsed);
33
+ if (!release.success)
34
+ return { problem: `${currentPath} does not describe a release` };
35
+ const path = isAbsolute(release.data.path)
36
+ ? resolve(release.data.path)
37
+ : resolve(currentPath, '..', release.data.path);
38
+ return { release: release.data, path };
39
+ }
40
+ export async function importRelease(releasePath) {
41
+ const entry = join(releasePath, ...runtimeEntryRelativePath);
42
+ const loaded = (await import(pathToFileURL(entry).href));
43
+ if (typeof loaded.createRuntime !== 'function')
44
+ throw new Error(`${entry} does not export createRuntime`);
45
+ return { createRuntime: loaded.createRuntime };
46
+ }
47
+ export class RuntimeHost {
48
+ options;
49
+ ownerId;
50
+ registered = new Map();
51
+ specs = new Map();
52
+ loadedPath;
53
+ loadedVersion;
54
+ refreshing;
55
+ failedPath;
56
+ seen;
57
+ timer;
58
+ started = false;
59
+ constructor(options) {
60
+ this.options = options;
61
+ this.ownerId = options.ownerId ?? randomUUID();
62
+ this.loadedPath = resolve(options.release.path);
63
+ this.loadedVersion = options.release.version;
64
+ }
65
+ get version() {
66
+ return this.loadedVersion;
67
+ }
68
+ get releasePath() {
69
+ return this.loadedPath;
70
+ }
71
+ start() {
72
+ if (this.started)
73
+ return;
74
+ this.started = true;
75
+ this.apply(this.instantiate(this.options.release.module, this.options.release.version));
76
+ }
77
+ watch(intervalMs) {
78
+ if (this.timer || !this.options.currentPath)
79
+ return;
80
+ this.timer = setInterval(() => {
81
+ void this.refresh();
82
+ }, intervalMs);
83
+ this.timer.unref();
84
+ }
85
+ stop() {
86
+ if (this.timer)
87
+ clearInterval(this.timer);
88
+ this.timer = undefined;
89
+ }
90
+ async refresh() {
91
+ if (!this.options.currentPath || !this.started)
92
+ return false;
93
+ this.refreshing ??= this.reload().finally(() => {
94
+ this.refreshing = undefined;
95
+ });
96
+ return this.refreshing;
97
+ }
98
+ async reload() {
99
+ const currentPath = this.options.currentPath;
100
+ let marker;
101
+ try {
102
+ marker = await stat(currentPath);
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ if (this.seen && this.seen.mtimeMs === marker.mtimeMs && this.seen.size === marker.size)
108
+ return false;
109
+ this.seen = { mtimeMs: marker.mtimeMs, size: marker.size };
110
+ const current = await readCurrentRelease(currentPath);
111
+ if ('problem' in current) {
112
+ this.log(`Engineering Memory bridge keeps release ${this.describeLoaded()}: ${current.problem}`);
113
+ return false;
114
+ }
115
+ if (current.path === this.loadedPath || current.path === this.failedPath)
116
+ return false;
117
+ const version = current.release.version ?? null;
118
+ try {
119
+ const module = await (this.options.load ?? importRelease)(current.path);
120
+ this.apply(this.instantiate(module, version));
121
+ }
122
+ catch (error) {
123
+ this.failedPath = current.path;
124
+ this.log(`Engineering Memory bridge keeps release ${this.describeLoaded()} because ${current.path} could not be loaded: ${describe(error)}`);
125
+ return false;
126
+ }
127
+ this.loadedPath = current.path;
128
+ this.loadedVersion = version;
129
+ this.failedPath = undefined;
130
+ this.log(`Engineering Memory bridge switched to release ${this.describeLoaded()}`);
131
+ return true;
132
+ }
133
+ instantiate(module, version) {
134
+ const runtime = module.createRuntime({
135
+ ownerId: this.ownerId,
136
+ clientVersion: version,
137
+ server: this.options.server.server,
138
+ });
139
+ if (!runtime || !Array.isArray(runtime.tools))
140
+ throw new Error('createRuntime did not return a tool list');
141
+ const names = new Set();
142
+ for (const spec of runtime.tools) {
143
+ if (typeof spec.name !== 'string' || !spec.name)
144
+ throw new Error('createRuntime returned a tool without a name');
145
+ if (names.has(spec.name))
146
+ throw new Error(`Tool ${spec.name} is defined twice`);
147
+ if (typeof spec.callback !== 'function')
148
+ throw new Error(`Tool ${spec.name} has no callback`);
149
+ names.add(spec.name);
150
+ }
151
+ return runtime;
152
+ }
153
+ apply(runtime) {
154
+ const names = new Set();
155
+ const register = this.options.server.registerTool.bind(this.options.server);
156
+ for (const spec of runtime.tools) {
157
+ names.add(spec.name);
158
+ this.specs.set(spec.name, spec);
159
+ const signature = toolSignature(spec.config);
160
+ const existing = this.registered.get(spec.name);
161
+ if (!existing) {
162
+ const tool = register(spec.name, { ...spec.config, inputSchema: this.inputSchemaOf(spec.name) }, this.dispatcher(spec.name));
163
+ this.registered.set(spec.name, { tool, signature });
164
+ }
165
+ else if (existing.signature !== signature) {
166
+ existing.tool.update({
167
+ ...(spec.config.title !== undefined ? { title: spec.config.title } : {}),
168
+ ...(spec.config.description !== undefined
169
+ ? { description: spec.config.description }
170
+ : {}),
171
+ ...(spec.config.outputSchema !== undefined
172
+ ? { outputSchema: spec.config.outputSchema }
173
+ : {}),
174
+ ...(spec.config.annotations !== undefined
175
+ ? { annotations: spec.config.annotations }
176
+ : {}),
177
+ });
178
+ existing.signature = signature;
179
+ }
180
+ }
181
+ for (const [name, entry] of [...this.registered]) {
182
+ if (names.has(name))
183
+ continue;
184
+ entry.tool.remove();
185
+ this.registered.delete(name);
186
+ this.specs.delete(name);
187
+ }
188
+ }
189
+ inputSchemaOf(name) {
190
+ const jsonSchema = (io) => (options) => {
191
+ const schema = this.specs.get(name)?.config.inputSchema;
192
+ if (!schema)
193
+ return emptyObjectSchema;
194
+ return schema['~standard'].jsonSchema[io](options);
195
+ };
196
+ const props = {
197
+ version: 1,
198
+ vendor: 'engineering-memory',
199
+ validate: (value) => ({ value }),
200
+ jsonSchema: { input: jsonSchema('input'), output: jsonSchema('output') },
201
+ };
202
+ return { '~standard': props };
203
+ }
204
+ dispatcher(name) {
205
+ return async (args, context) => {
206
+ await this.refresh();
207
+ const spec = this.specs.get(name);
208
+ if (!spec)
209
+ throw new Error(`Tool ${name} is no longer provided by this bridge release; list the tools again and use the current lifecycle.`);
210
+ const schema = spec.config.inputSchema;
211
+ if (!schema)
212
+ return spec.callback(context);
213
+ const outcome = await schema['~standard'].validate(args ?? {});
214
+ if (outcome.issues && outcome.issues.length > 0)
215
+ return {
216
+ content: [
217
+ {
218
+ type: 'text',
219
+ text: `Input validation error: Invalid arguments for tool ${name}: ${outcome.issues
220
+ .map((issue) => formatIssue(issue))
221
+ .join(', ')}`,
222
+ },
223
+ ],
224
+ isError: true,
225
+ };
226
+ return spec.callback(outcome.value, context);
227
+ };
228
+ }
229
+ describeLoaded() {
230
+ return this.loadedVersion ?? this.loadedPath;
231
+ }
232
+ log(message) {
233
+ this.options.log?.(message);
234
+ }
235
+ }
236
+ export function toolSignature(config) {
237
+ return JSON.stringify({
238
+ title: config.title ?? null,
239
+ description: config.description ?? null,
240
+ annotations: config.annotations ?? null,
241
+ input: jsonSchemaOf(config.inputSchema),
242
+ output: jsonSchemaOf(config.outputSchema),
243
+ });
244
+ }
245
+ function jsonSchemaOf(schema) {
246
+ if (!schema)
247
+ return null;
248
+ try {
249
+ return schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' });
250
+ }
251
+ catch {
252
+ return randomUUID();
253
+ }
254
+ }
255
+ function formatIssue(issue) {
256
+ if (!issue.path?.length)
257
+ return issue.message;
258
+ return `${issue.path
259
+ .map((segment) => String(typeof segment === 'object' && segment !== null ? segment.key : segment))
260
+ .join('.')}: ${issue.message}`;
261
+ }
262
+ function describe(error) {
263
+ return error instanceof Error ? error.message : String(error);
264
+ }
265
+ //# sourceMappingURL=runtime-host.js.map