@unson/brainbase-mcp 0.28.0 → 0.29.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.
@@ -0,0 +1,140 @@
1
+ declare const SOURCE_CLASS = "owning_repo";
2
+ declare const CONTENT_TYPE = "team_document";
3
+ type Revision = string | number;
4
+ export interface CanonicalDocumentResolution {
5
+ status?: unknown;
6
+ source_class?: unknown;
7
+ content_type?: unknown;
8
+ project_code?: unknown;
9
+ canonical_location?: {
10
+ repository?: string;
11
+ [key: string]: unknown;
12
+ } | null;
13
+ [key: string]: unknown;
14
+ }
15
+ export interface CanonicalDocumentSaveInput {
16
+ project_code?: unknown;
17
+ resolution?: CanonicalDocumentResolution | null;
18
+ content?: unknown;
19
+ path?: unknown;
20
+ repository_path?: unknown;
21
+ base_hash?: unknown;
22
+ expected_hash?: unknown;
23
+ base_revision?: unknown;
24
+ expected_revision?: unknown;
25
+ idempotency_key?: unknown;
26
+ access?: unknown;
27
+ [key: string]: unknown;
28
+ }
29
+ export interface CanonicalDocumentWriteRequest {
30
+ project_code: string;
31
+ path: string;
32
+ content: string;
33
+ content_hash: string;
34
+ base_hash: string | null;
35
+ base_revision: string;
36
+ idempotency_key: string;
37
+ request_fingerprint: string;
38
+ resolution: CanonicalDocumentResolution;
39
+ access: unknown;
40
+ }
41
+ export interface CanonicalDocumentWriteResult {
42
+ path?: string;
43
+ canonical_url?: string | null;
44
+ revision?: Revision;
45
+ idempotency_replayed?: boolean;
46
+ [key: string]: unknown;
47
+ }
48
+ export interface CanonicalDocumentReadResult {
49
+ path?: string;
50
+ content?: unknown;
51
+ content_hash?: unknown;
52
+ revision?: Revision;
53
+ canonical_url?: string | null;
54
+ [key: string]: unknown;
55
+ }
56
+ export interface CanonicalDocumentProvider {
57
+ write(request: CanonicalDocumentWriteRequest): Promise<CanonicalDocumentWriteResult> | CanonicalDocumentWriteResult;
58
+ read(request: {
59
+ project_code: string;
60
+ path: string;
61
+ canonical_url: string | null;
62
+ resolution: CanonicalDocumentResolution;
63
+ access: unknown;
64
+ }): Promise<CanonicalDocumentReadResult> | CanonicalDocumentReadResult;
65
+ }
66
+ export interface CanonicalDocumentSaveResult {
67
+ status: 'saved';
68
+ project_code: string;
69
+ source_class: typeof SOURCE_CLASS;
70
+ content_type: typeof CONTENT_TYPE;
71
+ path: string;
72
+ canonical_url: string | null;
73
+ content_hash: string;
74
+ revision: string;
75
+ readback: {
76
+ path: string;
77
+ content_hash: string;
78
+ revision: string;
79
+ canonical_url: string | null;
80
+ };
81
+ search_indexed: 'unknown';
82
+ idempotency_key: string;
83
+ idempotency_replayed: boolean;
84
+ request_fingerprint: string;
85
+ }
86
+ export interface CanonicalDocumentReceiptStore {
87
+ find?(request: CanonicalDocumentWriteRequest): Promise<{
88
+ request_fingerprint: string;
89
+ result: CanonicalDocumentSaveResult;
90
+ } | null | undefined>;
91
+ put?(record: CanonicalDocumentWriteRequest & {
92
+ result: CanonicalDocumentSaveResult;
93
+ }): Promise<unknown>;
94
+ }
95
+ declare function stableValue(value: unknown): unknown;
96
+ declare function digest(value: string): string;
97
+ export declare class CanonicalDocumentWriterError extends Error {
98
+ code: string;
99
+ status: number;
100
+ details: Record<string, unknown>;
101
+ constructor(code: string, message: string, status?: number, details?: Record<string, unknown>);
102
+ }
103
+ /**
104
+ * Canonical document provider contract.
105
+ *
106
+ * A provider is intentionally supplied by the caller. It must implement:
107
+ * write({ project_code, path, content, base_hash, base_revision,
108
+ * idempotency_key, request_fingerprint, resolution, access })
109
+ * -> { path?, canonical_url?, revision? }
110
+ * read({ project_code, path, canonical_url?, resolution, access })
111
+ * -> { path?, content, content_hash?, revision, canonical_url? }
112
+ *
113
+ * The provider owns the atomic CAS and durable idempotency boundary. This
114
+ * adapter validates the request, forwards both CAS values, and verifies the
115
+ * provider's post-write readback before reporting success.
116
+ */
117
+ export declare class CanonicalDocumentWriterAdapter {
118
+ provider: CanonicalDocumentProvider | null;
119
+ receiptStore: CanonicalDocumentReceiptStore | null;
120
+ completed: Map<string, {
121
+ request_fingerprint: string;
122
+ result: CanonicalDocumentSaveResult;
123
+ }>;
124
+ constructor({ provider, receiptStore }?: {
125
+ provider?: CanonicalDocumentProvider | null;
126
+ receiptStore?: CanonicalDocumentReceiptStore | null;
127
+ });
128
+ save(input?: CanonicalDocumentSaveInput): Promise<CanonicalDocumentSaveResult>;
129
+ normalizeRequest(input: CanonicalDocumentSaveInput): CanonicalDocumentWriteRequest;
130
+ assertProvider(): void;
131
+ assertOwningRepositoryResolution(resolution: CanonicalDocumentResolution | null | undefined, projectCode: string): asserts resolution is CanonicalDocumentResolution;
132
+ readback(request: CanonicalDocumentWriteRequest, writeResult?: CanonicalDocumentWriteResult): Promise<Omit<CanonicalDocumentSaveResult, 'idempotency_key' | 'idempotency_replayed' | 'request_fingerprint'>>;
133
+ normalizeProviderError(error: unknown): CanonicalDocumentWriterError;
134
+ }
135
+ export declare function normalizeRepositoryRelativePath(value: unknown): string;
136
+ export declare const canonicalDocumentWriterInternals: {
137
+ digest: typeof digest;
138
+ stableValue: typeof stableValue;
139
+ };
140
+ export {};
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Saves a team document into its owning repository through a caller-supplied
3
+ * provider. The adapter validates the request, forwards both CAS values and the
4
+ * idempotency key, and reports success only after the provider's readback
5
+ * matches the requested content and write revision.
6
+ *
7
+ * Moved unchanged from brainbase-unson server/services/canonical-document-writer-adapter.js
8
+ * (story-document-and-meeting-source-exits-v1).
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+ import path from 'node:path';
12
+ const SOURCE_CLASS = 'owning_repo';
13
+ const CONTENT_TYPE = 'team_document';
14
+ function stableValue(value) {
15
+ if (Array.isArray(value))
16
+ return value.map(stableValue);
17
+ if (value && typeof value === 'object') {
18
+ const record = value;
19
+ return Object.fromEntries(Object.keys(record).sort().map((key) => [key, stableValue(record[key])]));
20
+ }
21
+ return value;
22
+ }
23
+ function digest(value) {
24
+ return createHash('sha256').update(value, 'utf8').digest('hex');
25
+ }
26
+ function requiredString(value, name) {
27
+ if (typeof value !== 'string' || !value.trim()) {
28
+ throw new CanonicalDocumentWriterError('canonical_document_input_invalid', `${name} is required`, 400, { field: name });
29
+ }
30
+ return value.trim();
31
+ }
32
+ function normalizeRevision(value, name) {
33
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0)
34
+ return String(value);
35
+ return requiredString(value, name);
36
+ }
37
+ function errorField(error, field) {
38
+ return error && typeof error === 'object' ? error[field] : undefined;
39
+ }
40
+ export class CanonicalDocumentWriterError extends Error {
41
+ code;
42
+ status;
43
+ details;
44
+ constructor(code, message, status = 400, details = {}) {
45
+ super(message);
46
+ this.name = 'CanonicalDocumentWriterError';
47
+ this.code = code;
48
+ this.status = status;
49
+ this.details = details;
50
+ }
51
+ }
52
+ /**
53
+ * Canonical document provider contract.
54
+ *
55
+ * A provider is intentionally supplied by the caller. It must implement:
56
+ * write({ project_code, path, content, base_hash, base_revision,
57
+ * idempotency_key, request_fingerprint, resolution, access })
58
+ * -> { path?, canonical_url?, revision? }
59
+ * read({ project_code, path, canonical_url?, resolution, access })
60
+ * -> { path?, content, content_hash?, revision, canonical_url? }
61
+ *
62
+ * The provider owns the atomic CAS and durable idempotency boundary. This
63
+ * adapter validates the request, forwards both CAS values, and verifies the
64
+ * provider's post-write readback before reporting success.
65
+ */
66
+ export class CanonicalDocumentWriterAdapter {
67
+ provider;
68
+ receiptStore;
69
+ completed;
70
+ constructor({ provider = null, receiptStore = null } = {}) {
71
+ this.provider = provider;
72
+ this.receiptStore = receiptStore;
73
+ this.completed = new Map();
74
+ }
75
+ async save(input = {}) {
76
+ const request = this.normalizeRequest(input);
77
+ this.assertProvider();
78
+ let durable = null;
79
+ if (typeof this.receiptStore?.find === 'function') {
80
+ try {
81
+ durable = await this.receiptStore.find(request);
82
+ }
83
+ catch (error) {
84
+ if (error?.name === 'CanonicalDocumentWriterError')
85
+ throw error;
86
+ throw new CanonicalDocumentWriterError('canonical_document_receipt_unavailable', 'canonical document write receipt could not be read', 503, { cause: errorField(error, 'code') || errorField(error, 'message') || 'unknown' });
87
+ }
88
+ }
89
+ if (durable) {
90
+ if (durable.request_fingerprint !== request.request_fingerprint) {
91
+ throw new CanonicalDocumentWriterError('canonical_document_idempotency_conflict', 'idempotency_key was already used with different document content', 409, { idempotency_key: request.idempotency_key });
92
+ }
93
+ return { ...durable.result, idempotency_replayed: true };
94
+ }
95
+ const previous = this.completed.get(request.idempotency_key);
96
+ if (previous) {
97
+ if (previous.request_fingerprint !== request.request_fingerprint) {
98
+ throw new CanonicalDocumentWriterError('canonical_document_idempotency_conflict', 'idempotency_key was already used with different document content', 409, { idempotency_key: request.idempotency_key });
99
+ }
100
+ return { ...previous.result, idempotency_replayed: true };
101
+ }
102
+ const provider = this.provider;
103
+ let result;
104
+ try {
105
+ const writeResult = await provider.write(request);
106
+ if (!writeResult || typeof writeResult !== 'object' || !writeResult.revision) {
107
+ throw new CanonicalDocumentWriterError('canonical_document_write_invalid', 'canonical document provider did not return the write revision', 503);
108
+ }
109
+ const readback = await this.readback(request, writeResult);
110
+ result = {
111
+ ...readback,
112
+ idempotency_key: request.idempotency_key,
113
+ idempotency_replayed: Boolean(writeResult.idempotency_replayed),
114
+ request_fingerprint: request.request_fingerprint,
115
+ };
116
+ }
117
+ catch (error) {
118
+ throw this.normalizeProviderError(error);
119
+ }
120
+ if (typeof this.receiptStore?.put === 'function') {
121
+ try {
122
+ await this.receiptStore.put({
123
+ ...request,
124
+ result,
125
+ });
126
+ }
127
+ catch (error) {
128
+ throw this.normalizeProviderError(new CanonicalDocumentWriterError('canonical_document_receipt_unavailable', 'canonical document write receipt could not be stored', 503, { cause: errorField(error, 'code') || errorField(error, 'message') || 'unknown' }));
129
+ }
130
+ }
131
+ this.completed.set(request.idempotency_key, {
132
+ request_fingerprint: request.request_fingerprint,
133
+ result,
134
+ });
135
+ return result;
136
+ }
137
+ normalizeRequest(input) {
138
+ const projectCode = requiredString(input.project_code, 'project_code');
139
+ const resolution = input.resolution;
140
+ this.assertOwningRepositoryResolution(resolution, projectCode);
141
+ const content = input.content;
142
+ if (typeof content !== 'string' || !content.trim()) {
143
+ throw new CanonicalDocumentWriterError('canonical_document_input_invalid', 'content is required', 400, { field: 'content' });
144
+ }
145
+ const repositoryPath = normalizeRepositoryRelativePath(input.path ?? input.repository_path);
146
+ const rawBaseHash = input.base_hash ?? input.expected_hash;
147
+ const baseHash = rawBaseHash === undefined || rawBaseHash === null || rawBaseHash === ''
148
+ ? null
149
+ : requiredString(rawBaseHash, 'base_hash');
150
+ const baseRevision = normalizeRevision(input.base_revision ?? input.expected_revision, 'base_revision');
151
+ const idempotencyKey = requiredString(input.idempotency_key, 'idempotency_key');
152
+ const contentHash = digest(content);
153
+ const fingerprint = digest(JSON.stringify(stableValue({
154
+ project_code: projectCode,
155
+ path: repositoryPath,
156
+ content_hash: contentHash,
157
+ base_hash: baseHash,
158
+ base_revision: baseRevision,
159
+ idempotency_key: idempotencyKey,
160
+ source_class: SOURCE_CLASS,
161
+ content_type: CONTENT_TYPE,
162
+ canonical_location: resolution.canonical_location || null,
163
+ })));
164
+ return {
165
+ project_code: projectCode,
166
+ path: repositoryPath,
167
+ content,
168
+ content_hash: contentHash,
169
+ base_hash: baseHash,
170
+ base_revision: baseRevision,
171
+ idempotency_key: idempotencyKey,
172
+ request_fingerprint: fingerprint,
173
+ resolution,
174
+ access: input.access || null,
175
+ };
176
+ }
177
+ assertProvider() {
178
+ if (!this.provider || typeof this.provider.write !== 'function' || typeof this.provider.read !== 'function') {
179
+ throw new CanonicalDocumentWriterError('canonical_document_writer_not_configured', 'canonical document writer is not configured', 503);
180
+ }
181
+ }
182
+ assertOwningRepositoryResolution(resolution, projectCode) {
183
+ if (!resolution
184
+ || resolution.status !== 'resolved'
185
+ || resolution.source_class !== SOURCE_CLASS
186
+ || resolution.content_type !== CONTENT_TYPE
187
+ || resolution.project_code !== projectCode) {
188
+ throw new CanonicalDocumentWriterError('canonical_document_route_invalid', 'team_document must resolve to the owning repository before it can be saved', 422, { required_source_class: SOURCE_CLASS, required_content_type: CONTENT_TYPE });
189
+ }
190
+ const expectedRepository = `project:${projectCode}`;
191
+ const repository = resolution.canonical_location?.repository;
192
+ if (repository && repository !== expectedRepository) {
193
+ throw new CanonicalDocumentWriterError('canonical_document_route_invalid', 'resolution repository does not match project_code', 422, { expected_repository: expectedRepository, repository });
194
+ }
195
+ }
196
+ async readback(request, writeResult = {}) {
197
+ if (!writeResult || typeof writeResult !== 'object') {
198
+ throw new CanonicalDocumentWriterError('canonical_document_write_invalid', 'canonical document provider returned an invalid write result', 503);
199
+ }
200
+ const pathFromWrite = normalizeRepositoryRelativePath(writeResult.path ?? request.path);
201
+ const readback = await this.provider.read({
202
+ project_code: request.project_code,
203
+ path: pathFromWrite,
204
+ canonical_url: writeResult.canonical_url || null,
205
+ resolution: request.resolution,
206
+ access: request.access,
207
+ });
208
+ if (!readback || typeof readback !== 'object' || typeof readback.content !== 'string') {
209
+ throw new CanonicalDocumentWriterError('canonical_document_readback_unavailable', 'canonical document could not be read back after save', 503);
210
+ }
211
+ const readbackPath = normalizeRepositoryRelativePath(readback.path ?? pathFromWrite);
212
+ const readbackHash = digest(readback.content);
213
+ const declaredReadbackHash = typeof readback.content_hash === 'string' ? readback.content_hash.trim() : null;
214
+ if (readbackPath !== request.path
215
+ || (declaredReadbackHash && declaredReadbackHash !== readbackHash)
216
+ || readbackHash !== request.content_hash
217
+ || readback.content !== request.content) {
218
+ throw new CanonicalDocumentWriterError('canonical_document_readback_mismatch', 'canonical document readback does not match the requested content', 502, {
219
+ path: request.path,
220
+ expected_content_hash: request.content_hash,
221
+ actual_content_hash: readbackHash,
222
+ ...(declaredReadbackHash ? { declared_content_hash: declaredReadbackHash } : {}),
223
+ actual_path: readbackPath,
224
+ });
225
+ }
226
+ const writeRevision = normalizeRevision(writeResult.revision, 'writeResult.revision');
227
+ const revision = normalizeRevision(readback.revision, 'readback.revision');
228
+ if (revision !== writeRevision) {
229
+ throw new CanonicalDocumentWriterError('canonical_document_readback_revision_mismatch', 'canonical document readback revision does not match the write commit', 502, { expected_revision: writeRevision, actual_revision: revision });
230
+ }
231
+ return {
232
+ status: 'saved',
233
+ project_code: request.project_code,
234
+ source_class: SOURCE_CLASS,
235
+ content_type: CONTENT_TYPE,
236
+ path: request.path,
237
+ canonical_url: readback.canonical_url || writeResult.canonical_url || null,
238
+ content_hash: request.content_hash,
239
+ revision,
240
+ readback: {
241
+ path: readbackPath,
242
+ content_hash: readbackHash,
243
+ revision,
244
+ canonical_url: readback.canonical_url || writeResult.canonical_url || null,
245
+ },
246
+ search_indexed: 'unknown',
247
+ };
248
+ }
249
+ normalizeProviderError(error) {
250
+ if (error instanceof CanonicalDocumentWriterError)
251
+ return error;
252
+ const rawStatus = errorField(error, 'status');
253
+ const status = Number.isInteger(rawStatus) ? rawStatus : 503;
254
+ const rawCode = errorField(error, 'code');
255
+ const code = typeof rawCode === 'string' && rawCode
256
+ ? rawCode
257
+ : 'canonical_document_writer_failed';
258
+ return new CanonicalDocumentWriterError(code, status === 409 ? 'canonical document write conflicts with the current version' : 'canonical document writer is unavailable', status, status === 409 ? { provider_code: code } : {});
259
+ }
260
+ }
261
+ export function normalizeRepositoryRelativePath(value) {
262
+ const raw = requiredString(value, 'path');
263
+ if (raw.includes('\0') || raw.includes('\\') || raw.startsWith('/') || raw.startsWith('~') || /^[A-Za-z]:[\\/]/.test(raw)) {
264
+ throw new CanonicalDocumentWriterError('canonical_document_path_invalid', 'path must be a repository-relative POSIX path', 400, { field: 'path' });
265
+ }
266
+ const segments = raw.split('/');
267
+ if (segments.some((segment) => !segment || segment === '.' || segment === '..')) {
268
+ throw new CanonicalDocumentWriterError('canonical_document_path_invalid', 'path must not contain empty, dot, or parent segments', 400, { field: 'path' });
269
+ }
270
+ const normalized = path.posix.normalize(raw);
271
+ if (normalized !== raw || normalized === '.' || normalized === '..' || normalized.startsWith('../') || path.posix.isAbsolute(normalized)) {
272
+ throw new CanonicalDocumentWriterError('canonical_document_path_invalid', 'path must be a normalized repository-relative path', 400, { field: 'path' });
273
+ }
274
+ return normalized;
275
+ }
276
+ export const canonicalDocumentWriterInternals = { digest, stableValue };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Contracts shared by the local knowledge capture and preview adapters.
3
+ *
4
+ * The adapter is deliberately provider-agnostic. Production bootstrap does
5
+ * not create one; callers must inject an explicit implementation. This file
6
+ * only normalizes and validates the boundary so an adapter cannot turn
7
+ * missing evidence or an unverified version into a successful result.
8
+ *
9
+ * Moved unchanged from brainbase-unson server/services/knowledge-capture-preview-adapter.js
10
+ * (story-document-and-meeting-source-exits-v1).
11
+ */
12
+ export type KnowledgeVersion = string | {
13
+ state: string;
14
+ value: string | null;
15
+ };
16
+ type Details = Record<string, unknown>;
17
+ type Entry = Record<string, unknown>;
18
+ export declare class KnowledgeAIAdapterContractError extends Error {
19
+ code: string;
20
+ status: number;
21
+ details: Details;
22
+ constructor(message: string, details?: Details);
23
+ }
24
+ export declare function normalizeVersion(value: unknown, field?: string): KnowledgeVersion;
25
+ export declare function versionValue(value: KnowledgeVersion): string | null;
26
+ export declare function normalizeUnknown(value: unknown, field?: string): Array<string | Entry>;
27
+ export declare function normalizeReadback(value: unknown, field?: string): Entry & {
28
+ state: string;
29
+ verified: boolean;
30
+ };
31
+ /**
32
+ * Resolve only explicitly injected adapter methods. There is intentionally
33
+ * no environment or default-provider lookup here.
34
+ */
35
+ export declare function resolveKnowledgeAdapter(adapter: unknown, method: string): ((...args: any[]) => any) | null;
36
+ export declare function normalizeEvidence(value: unknown, { field, allowedReferences, requireExactReference, }?: {
37
+ field?: string;
38
+ allowedReferences?: Set<string> | null;
39
+ requireExactReference?: boolean;
40
+ }): Array<Entry & {
41
+ id: string | null;
42
+ version: KnowledgeVersion;
43
+ source_ref: string | null;
44
+ }>;
45
+ export declare function referenceKey(id: unknown, version: KnowledgeVersion): string;
46
+ export declare function normalizeCitations(value: unknown, { field, allowedReferences, }?: {
47
+ field?: string;
48
+ allowedReferences?: Set<string> | null;
49
+ }): Array<Entry & {
50
+ id: string;
51
+ version: string;
52
+ }>;
53
+ export declare function validateCaptureProposal(value: unknown, { allowedReferences }?: {
54
+ allowedReferences?: Set<string> | null;
55
+ }): {
56
+ proposal: {
57
+ kind: unknown;
58
+ summary: unknown;
59
+ scope: unknown;
60
+ owner_candidate: unknown;
61
+ relations: unknown;
62
+ };
63
+ evidence: (Entry & {
64
+ id: string | null;
65
+ version: KnowledgeVersion;
66
+ source_ref: string | null;
67
+ })[];
68
+ unknown: (string | Entry)[];
69
+ version: KnowledgeVersion;
70
+ readback: Entry & {
71
+ state: string;
72
+ verified: boolean;
73
+ };
74
+ };
75
+ export declare function validatePreviewAnswer(value: unknown, { allowedReferences }: {
76
+ allowedReferences?: Set<string> | null;
77
+ }): {
78
+ answer: string;
79
+ citations: (Entry & {
80
+ id: string;
81
+ version: string;
82
+ })[];
83
+ evidence: (Entry & {
84
+ id: string | null;
85
+ version: KnowledgeVersion;
86
+ source_ref: string | null;
87
+ })[];
88
+ exclusions: {
89
+ [x: string]: unknown;
90
+ }[];
91
+ unknown: (string | Entry)[];
92
+ version: KnowledgeVersion;
93
+ readback: Entry & {
94
+ state: string;
95
+ verified: boolean;
96
+ };
97
+ };
98
+ export {};
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Contracts shared by the local knowledge capture and preview adapters.
3
+ *
4
+ * The adapter is deliberately provider-agnostic. Production bootstrap does
5
+ * not create one; callers must inject an explicit implementation. This file
6
+ * only normalizes and validates the boundary so an adapter cannot turn
7
+ * missing evidence or an unverified version into a successful result.
8
+ *
9
+ * Moved unchanged from brainbase-unson server/services/knowledge-capture-preview-adapter.js
10
+ * (story-document-and-meeting-source-exits-v1).
11
+ */
12
+ export class KnowledgeAIAdapterContractError extends Error {
13
+ code;
14
+ status;
15
+ details;
16
+ constructor(message, details = {}) {
17
+ super(message);
18
+ this.name = 'KnowledgeAIAdapterContractError';
19
+ this.code = 'knowledge_ai_adapter_contract_invalid';
20
+ this.status = 502;
21
+ this.details = details;
22
+ }
23
+ }
24
+ function text(value) {
25
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
26
+ }
27
+ function hasOwn(value, key) {
28
+ return Boolean(value && Object.prototype.hasOwnProperty.call(value, key));
29
+ }
30
+ function fail(message, details = {}) {
31
+ throw new KnowledgeAIAdapterContractError(message, details);
32
+ }
33
+ function isRecord(value) {
34
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
35
+ }
36
+ export function normalizeVersion(value, field = 'version') {
37
+ if (typeof value === 'string' && value.trim())
38
+ return value.trim();
39
+ if (typeof value === 'number' && Number.isFinite(value))
40
+ return String(value);
41
+ if (isRecord(value)) {
42
+ const state = text(value.state) || text(value.status);
43
+ if (!state)
44
+ fail(`${field} must include an explicit state`, { field });
45
+ return {
46
+ state,
47
+ value: typeof value.value === 'string' && value.value.trim()
48
+ ? value.value.trim()
49
+ : typeof value.value === 'number' && Number.isFinite(value.value)
50
+ ? String(value.value)
51
+ : null,
52
+ };
53
+ }
54
+ fail(`${field} is required`, { field });
55
+ }
56
+ export function versionValue(value) {
57
+ return typeof value === 'string' ? value : null;
58
+ }
59
+ function versionKey(value) {
60
+ return typeof value === 'string' ? `known:${value}` : `unknown:${value.state}:${value.value || ''}`;
61
+ }
62
+ export function normalizeUnknown(value, field = 'unknown') {
63
+ if (!Array.isArray(value))
64
+ fail(`${field} must be an array`, { field });
65
+ return value.map((entry, index) => {
66
+ if (typeof entry === 'string' && entry.trim())
67
+ return entry.trim();
68
+ if (isRecord(entry))
69
+ return { ...entry };
70
+ fail(`${field}[${index}] must be text or an object`, { field, index });
71
+ });
72
+ }
73
+ export function normalizeReadback(value, field = 'readback') {
74
+ if (!isRecord(value)) {
75
+ fail(`${field} is required`, { field });
76
+ }
77
+ const state = text(value.state) || text(value.status);
78
+ if (!state)
79
+ fail(`${field}.state is required`, { field });
80
+ if (typeof value.verified !== 'boolean') {
81
+ fail(`${field}.verified must be explicit`, { field });
82
+ }
83
+ return { ...value, state, verified: value.verified };
84
+ }
85
+ /**
86
+ * Resolve only explicitly injected adapter methods. There is intentionally
87
+ * no environment or default-provider lookup here.
88
+ */
89
+ export function resolveKnowledgeAdapter(adapter, method) {
90
+ if (typeof adapter === 'function')
91
+ return adapter;
92
+ if (!adapter || typeof adapter !== 'object')
93
+ return null;
94
+ const candidate = adapter[method];
95
+ if (typeof candidate === 'function')
96
+ return candidate.bind(adapter);
97
+ return null;
98
+ }
99
+ export function normalizeEvidence(value, { field = 'evidence', allowedReferences = null, requireExactReference = false, } = {}) {
100
+ if (!Array.isArray(value))
101
+ fail(`${field} must be an array`, { field });
102
+ return value.map((entry, index) => {
103
+ if (!isRecord(entry)) {
104
+ fail(`${field}[${index}] must be an object`, { field, index });
105
+ }
106
+ const id = text(entry.id) || text(entry.candidate_id) || null;
107
+ const sourceRef = text(entry.source_ref) || text(entry.pointer) || text(entry.source) || null;
108
+ if (requireExactReference && !id) {
109
+ fail(`${field}[${index}].id is required`, { field, index });
110
+ }
111
+ if (!id && !sourceRef) {
112
+ fail(`${field}[${index}] needs an id or source_ref`, { field, index });
113
+ }
114
+ if (!hasOwn(entry, 'version')) {
115
+ fail(`${field}[${index}].version is required`, { field, index });
116
+ }
117
+ const version = normalizeVersion(entry.version, `${field}[${index}].version`);
118
+ if (allowedReferences && id) {
119
+ const key = `${id}\u0000${versionKey(version)}`;
120
+ if (!allowedReferences.has(key)) {
121
+ fail(`${field}[${index}] references an unavailable candidate`, {
122
+ field,
123
+ index,
124
+ id,
125
+ version,
126
+ });
127
+ }
128
+ }
129
+ return {
130
+ ...entry,
131
+ id,
132
+ version,
133
+ source_ref: sourceRef,
134
+ };
135
+ });
136
+ }
137
+ export function referenceKey(id, version) {
138
+ return `${text(id) || ''}\u0000${versionKey(version)}`;
139
+ }
140
+ export function normalizeCitations(value, { field = 'citations', allowedReferences, } = {}) {
141
+ if (!Array.isArray(value))
142
+ fail(`${field} must be an array`, { field });
143
+ return value.map((entry, index) => {
144
+ if (!isRecord(entry)) {
145
+ fail(`${field}[${index}] must be an object`, { field, index });
146
+ }
147
+ const id = text(entry.id) || text(entry.candidate_id);
148
+ if (!id)
149
+ fail(`${field}[${index}].id is required`, { field, index });
150
+ if (!hasOwn(entry, 'version'))
151
+ fail(`${field}[${index}].version is required`, { field, index });
152
+ const version = normalizeVersion(entry.version, `${field}[${index}].version`);
153
+ if (typeof version !== 'string') {
154
+ fail(`${field}[${index}].version must be an exact value`, { field, index, id });
155
+ }
156
+ const key = referenceKey(id, version);
157
+ if (allowedReferences && !allowedReferences.has(key)) {
158
+ fail(`${field}[${index}] references a candidate outside the isolated set`, {
159
+ field,
160
+ index,
161
+ id,
162
+ version,
163
+ });
164
+ }
165
+ return {
166
+ ...entry,
167
+ id,
168
+ version,
169
+ };
170
+ });
171
+ }
172
+ export function validateCaptureProposal(value, { allowedReferences = null } = {}) {
173
+ if (!isRecord(value)) {
174
+ fail('capture adapter must return an object');
175
+ }
176
+ const proposal = isRecord(value.proposal)
177
+ ? value.proposal
178
+ : value;
179
+ for (const field of ['kind', 'summary', 'scope', 'owner_candidate', 'relations']) {
180
+ if (!hasOwn(proposal, field))
181
+ fail(`capture proposal.${field} is required`, { field });
182
+ }
183
+ const version = normalizeVersion(value.version, 'capture proposal.version');
184
+ const unknown = normalizeUnknown(value.unknown, 'capture proposal.unknown');
185
+ const evidence = normalizeEvidence(value.evidence, {
186
+ field: 'capture proposal.evidence',
187
+ allowedReferences,
188
+ });
189
+ const readback = normalizeReadback(value.readback, 'capture proposal.readback');
190
+ return {
191
+ proposal: {
192
+ kind: proposal.kind,
193
+ summary: proposal.summary,
194
+ scope: proposal.scope,
195
+ owner_candidate: proposal.owner_candidate,
196
+ relations: proposal.relations,
197
+ },
198
+ evidence,
199
+ unknown,
200
+ version,
201
+ readback,
202
+ };
203
+ }
204
+ export function validatePreviewAnswer(value, { allowedReferences }) {
205
+ if (!isRecord(value)) {
206
+ fail('preview adapter must return an object');
207
+ }
208
+ const answer = text(value.answer);
209
+ if (!answer)
210
+ fail('preview answer is required', { field: 'answer' });
211
+ const rawCitations = value.citations ?? value.cited_refs ?? value.selected_refs;
212
+ const citations = normalizeCitations(rawCitations, {
213
+ field: 'preview citations',
214
+ allowedReferences,
215
+ });
216
+ const unknown = normalizeUnknown(value.unknown, 'preview unknown');
217
+ const evidence = normalizeEvidence(value.evidence, {
218
+ field: 'preview evidence',
219
+ allowedReferences,
220
+ requireExactReference: true,
221
+ });
222
+ const readback = normalizeReadback(value.readback, 'preview readback');
223
+ const version = normalizeVersion(value.version, 'preview version');
224
+ const exclusions = Array.isArray(value.exclusions)
225
+ ? value.exclusions.map((entry) => ({ ...entry }))
226
+ : [];
227
+ return {
228
+ answer,
229
+ citations,
230
+ evidence,
231
+ exclusions,
232
+ unknown,
233
+ version,
234
+ readback,
235
+ };
236
+ }
@@ -293,7 +293,16 @@ export class NativeMcpRuntime {
293
293
  await this.options.credentials.remove(connection.connectionId);
294
294
  }
295
295
  }
296
- /** The SDK's OAuth provider for one connection. It never starts an interactive authorization. */
296
+ /**
297
+ * Marks this provider as an authorization-code client without a reachable redirect. The SDK
298
+ * treats a provider with no redirect URL as a non-interactive (client-credentials) client and
299
+ * then never uses the refresh token, so an expired access token could not be renewed.
300
+ */
301
+ const NO_INTERACTIVE_REDIRECT = 'urn:brainbase:meeting-source:no-interactive-redirect';
302
+ /**
303
+ * The SDK's OAuth provider for one connection. It renews tokens with the stored refresh token
304
+ * and never starts an interactive authorization: that path ends in `reauth_required`.
305
+ */
297
306
  class ConnectionOAuthProvider {
298
307
  store;
299
308
  connection;
@@ -302,7 +311,7 @@ class ConnectionOAuthProvider {
302
311
  this.connection = connection;
303
312
  }
304
313
  get redirectUrl() {
305
- return undefined;
314
+ return NO_INTERACTIVE_REDIRECT;
306
315
  }
307
316
  get clientMetadata() {
308
317
  return { redirect_uris: [], client_name: 'Brainbase Meeting Source' };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The meeting source providers Brainbase treats as effective (Tactiq for
3
+ * online meetings, Plaud.ai for offline recordings and calls), with an
4
+ * optional live check of their public integration surfaces on integrations.sh.
5
+ *
6
+ * Moved unchanged from brainbase-unson server/services/meeting-source/meeting-source-integration-catalog.js
7
+ * (story-document-and-meeting-source-exits-v1).
8
+ */
9
+ export interface MeetingSourceCatalogSurface {
10
+ type: string;
11
+ transport: string;
12
+ configured_by: string;
13
+ confidence: string;
14
+ purpose: string;
15
+ }
16
+ export interface MeetingSourceCatalogProvider {
17
+ provider: string;
18
+ domain: string;
19
+ label: string;
20
+ role: string;
21
+ source_role: string;
22
+ catalog_source: string;
23
+ catalog_status: string;
24
+ surfaces: readonly MeetingSourceCatalogSurface[];
25
+ auth: {
26
+ managed_by: string;
27
+ credential_ref_required: boolean;
28
+ local_secret_storage: boolean;
29
+ user_action: string;
30
+ };
31
+ notes: readonly string[];
32
+ }
33
+ export interface MeetingSourceUpstreamDetect {
34
+ ok: boolean;
35
+ status?: number;
36
+ found?: unknown[];
37
+ mcp?: unknown[];
38
+ integrations_json?: unknown;
39
+ auth?: unknown[];
40
+ error?: string;
41
+ }
42
+ export interface MeetingSourceCatalogEntry extends MeetingSourceCatalogProvider {
43
+ effective: true;
44
+ upstream: {
45
+ name: 'integrations.sh';
46
+ domain: string;
47
+ detect_url: string;
48
+ detect_status: 'pending' | 'not_queried';
49
+ };
50
+ upstream_detect: MeetingSourceUpstreamDetect | null;
51
+ }
52
+ export interface MeetingSourceCatalog {
53
+ version: 1;
54
+ generated_at: string;
55
+ upstream: {
56
+ name: 'integrations.sh';
57
+ base_url: string;
58
+ purpose: string;
59
+ };
60
+ providers: MeetingSourceCatalogEntry[];
61
+ }
62
+ type FetchLike = (url: string, init: {
63
+ headers: Record<string, string>;
64
+ }) => Promise<{
65
+ ok: boolean;
66
+ status: number;
67
+ json(): Promise<unknown>;
68
+ }>;
69
+ export interface MeetingSourceIntegrationCatalogOptions {
70
+ baseURL?: string;
71
+ fetchImpl?: FetchLike | null;
72
+ clock?: () => string;
73
+ }
74
+ export declare class MeetingSourceIntegrationCatalogService {
75
+ baseURL: string;
76
+ fetchImpl: FetchLike | null | undefined;
77
+ clock: () => string;
78
+ constructor({ baseURL, fetchImpl, clock, }?: MeetingSourceIntegrationCatalogOptions);
79
+ listCatalog({ includeLiveDetect }?: {
80
+ includeLiveDetect?: boolean;
81
+ }): Promise<MeetingSourceCatalog>;
82
+ getCatalogEntry(provider: unknown, { includeLiveDetect }?: {
83
+ includeLiveDetect?: boolean;
84
+ }): Promise<MeetingSourceCatalogEntry>;
85
+ refreshProvider(provider: unknown): Promise<MeetingSourceCatalogEntry>;
86
+ detectUpstream(domain: string): Promise<MeetingSourceUpstreamDetect>;
87
+ }
88
+ export declare function createMeetingSourceIntegrationCatalogService(options?: MeetingSourceIntegrationCatalogOptions): MeetingSourceIntegrationCatalogService;
89
+ export {};
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The meeting source providers Brainbase treats as effective (Tactiq for
3
+ * online meetings, Plaud.ai for offline recordings and calls), with an
4
+ * optional live check of their public integration surfaces on integrations.sh.
5
+ *
6
+ * Moved unchanged from brainbase-unson server/services/meeting-source/meeting-source-integration-catalog.js
7
+ * (story-document-and-meeting-source-exits-v1).
8
+ */
9
+ const INTEGRATIONS_SH_BASE_URL = 'https://integrations.sh';
10
+ const MEETING_SOURCE_PROVIDER_CATALOG = Object.freeze({
11
+ tactiq: Object.freeze({
12
+ provider: 'tactiq',
13
+ domain: 'tactiq.io',
14
+ label: 'Tactiq',
15
+ role: 'online_primary',
16
+ source_role: 'online meetings',
17
+ catalog_source: 'brainbase_override',
18
+ catalog_status: 'override_required',
19
+ surfaces: Object.freeze([
20
+ Object.freeze({
21
+ type: 'mcp',
22
+ transport: 'brainbase_runtime',
23
+ configured_by: 'brainbase_mcp_runtime',
24
+ confidence: 'manual_override',
25
+ purpose: 'online meeting transcripts and speaker timeline',
26
+ }),
27
+ ]),
28
+ auth: Object.freeze({
29
+ managed_by: 'brainbase_runtime',
30
+ credential_ref_required: true,
31
+ local_secret_storage: false,
32
+ user_action: 'provider login is completed outside Mac Companion and stored by Brainbase runtime',
33
+ }),
34
+ notes: Object.freeze([
35
+ 'integrations.sh may not detect a published MCP surface yet; Brainbase keeps Tactiq as an effective provider override.',
36
+ 'Calendar data is context only. Tactiq is the primary source for online meetings.',
37
+ ]),
38
+ }),
39
+ plaud: Object.freeze({
40
+ provider: 'plaud',
41
+ domain: 'plaud.ai',
42
+ label: 'Plaud.ai',
43
+ role: 'offline_or_call_primary',
44
+ source_role: 'offline recordings, calls, and online meetings without Tactiq',
45
+ catalog_source: 'brainbase_override',
46
+ catalog_status: 'override_required',
47
+ surfaces: Object.freeze([
48
+ Object.freeze({
49
+ type: 'mcp',
50
+ transport: 'brainbase_runtime',
51
+ configured_by: 'brainbase_mcp_runtime',
52
+ confidence: 'manual_override',
53
+ purpose: 'offline recordings, calls, and fallback meeting transcripts',
54
+ }),
55
+ ]),
56
+ auth: Object.freeze({
57
+ managed_by: 'brainbase_runtime',
58
+ credential_ref_required: true,
59
+ local_secret_storage: false,
60
+ user_action: 'provider login is completed outside Mac Companion and stored by Brainbase runtime',
61
+ }),
62
+ notes: Object.freeze([
63
+ 'integrations.sh may not detect a published MCP surface yet; Brainbase keeps Plaud.ai as an effective provider override.',
64
+ 'Calendar data is context only. Plaud.ai is the primary source for offline meetings and calls.',
65
+ ]),
66
+ }),
67
+ });
68
+ function nowIso() {
69
+ return new Date().toISOString();
70
+ }
71
+ function normalizeProvider(provider) {
72
+ return String(provider || '').trim().toLowerCase();
73
+ }
74
+ function providerEntry(provider) {
75
+ const key = normalizeProvider(provider);
76
+ const entry = MEETING_SOURCE_PROVIDER_CATALOG[key];
77
+ if (!entry) {
78
+ const error = new Error(`unsupported meeting source provider: ${provider}`);
79
+ error.statusCode = 404;
80
+ throw error;
81
+ }
82
+ return structuredClone(entry);
83
+ }
84
+ function integrationDetectURL(baseURL, domain) {
85
+ return `${String(baseURL).replace(/\/$/, '')}/api/${encodeURIComponent(domain)}/detect`;
86
+ }
87
+ export class MeetingSourceIntegrationCatalogService {
88
+ baseURL;
89
+ fetchImpl;
90
+ clock;
91
+ constructor({ baseURL = INTEGRATIONS_SH_BASE_URL, fetchImpl = globalThis.fetch, clock = nowIso, } = {}) {
92
+ this.baseURL = baseURL;
93
+ this.fetchImpl = fetchImpl;
94
+ this.clock = clock;
95
+ }
96
+ async listCatalog({ includeLiveDetect = false } = {}) {
97
+ const providers = Object.keys(MEETING_SOURCE_PROVIDER_CATALOG);
98
+ const entries = [];
99
+ for (const provider of providers) {
100
+ entries.push(await this.getCatalogEntry(provider, { includeLiveDetect }));
101
+ }
102
+ return {
103
+ version: 1,
104
+ generated_at: this.clock(),
105
+ upstream: {
106
+ name: 'integrations.sh',
107
+ base_url: this.baseURL,
108
+ purpose: 'detect public integration surfaces; Brainbase override decides effective meeting-source providers',
109
+ },
110
+ providers: entries,
111
+ };
112
+ }
113
+ async getCatalogEntry(provider, { includeLiveDetect = false } = {}) {
114
+ const entry = providerEntry(provider);
115
+ return {
116
+ ...entry,
117
+ effective: true,
118
+ upstream: {
119
+ name: 'integrations.sh',
120
+ domain: entry.domain,
121
+ detect_url: integrationDetectURL(this.baseURL, entry.domain),
122
+ detect_status: includeLiveDetect ? 'pending' : 'not_queried',
123
+ },
124
+ upstream_detect: includeLiveDetect ? await this.detectUpstream(entry.domain) : null,
125
+ };
126
+ }
127
+ async refreshProvider(provider) {
128
+ return this.getCatalogEntry(provider, { includeLiveDetect: true });
129
+ }
130
+ async detectUpstream(domain) {
131
+ if (typeof this.fetchImpl !== 'function') {
132
+ return {
133
+ ok: false,
134
+ error: 'fetch_not_available',
135
+ };
136
+ }
137
+ try {
138
+ const response = await this.fetchImpl(integrationDetectURL(this.baseURL, domain), {
139
+ headers: { accept: 'application/json' },
140
+ });
141
+ const body = await response.json();
142
+ return {
143
+ ok: response.ok,
144
+ status: response.status,
145
+ found: body?.found || [],
146
+ mcp: body?.mcp || [],
147
+ integrations_json: body?.integrationsJson || null,
148
+ auth: body?.auth || [],
149
+ };
150
+ }
151
+ catch (error) {
152
+ return {
153
+ ok: false,
154
+ error: error?.message || 'upstream_detect_failed',
155
+ };
156
+ }
157
+ }
158
+ }
159
+ export function createMeetingSourceIntegrationCatalogService(options = {}) {
160
+ return new MeetingSourceIntegrationCatalogService(options);
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.28.0",
3
+ "version": "0.29.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",
@@ -183,6 +183,18 @@
183
183
  "types": "./dist/knowledge-resolution-service.d.ts",
184
184
  "import": "./dist/knowledge-resolution-service.js"
185
185
  },
186
+ "./canonical-document-writer": {
187
+ "types": "./dist/canonical-document-writer.d.ts",
188
+ "import": "./dist/canonical-document-writer.js"
189
+ },
190
+ "./knowledge-capture-preview": {
191
+ "types": "./dist/knowledge-capture-preview.d.ts",
192
+ "import": "./dist/knowledge-capture-preview.js"
193
+ },
194
+ "./meeting-source-integration-catalog": {
195
+ "types": "./dist/meeting-source-integration-catalog.d.ts",
196
+ "import": "./dist/meeting-source-integration-catalog.js"
197
+ },
186
198
  "./knowledge-adapter": {
187
199
  "types": "./dist/knowledge-adapter.d.ts",
188
200
  "import": "./dist/knowledge-adapter.js"
@@ -439,5 +451,5 @@
439
451
  "engines": {
440
452
  "node": ">=20"
441
453
  },
442
- "gitHead": "b8d86d3e394b94cd77bcfe71e8cb6c8703abce4b"
454
+ "gitHead": "6a77b6883ff11a89306f168ae2c79a6bb0606b70"
443
455
  }