@unson/brainbase-mcp 0.45.0 → 0.46.1
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 +2 -2
- package/contracts/judgment-dag/digest.json +2 -2
- package/dist/canonical-task-postgres.d.ts +6 -0
- package/dist/canonical-task-postgres.js +44 -4
- package/dist/canonical-task-service.d.ts +16 -1
- package/dist/canonical-task-service.js +28 -9
- package/dist/meeting-source-connection.d.ts +4 -0
- package/dist/meeting-source-connection.js +16 -7
- package/dist/meeting-source-reader.js +29 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -247,8 +247,8 @@ Publication is serialized by the GitHub workflow. Direct local `release:publish`
|
|
|
247
247
|
Dispatch the reviewed ref once when automatic publication needs recovery:
|
|
248
248
|
|
|
249
249
|
```bash
|
|
250
|
-
RELEASE_REF="<reviewed-
|
|
251
|
-
gh workflow run npm-publish.yml --ref
|
|
250
|
+
RELEASE_REF="<reviewed-main-commit>"
|
|
251
|
+
gh workflow run npm-publish.yml --ref main -f release_ref="$RELEASE_REF"
|
|
252
252
|
```
|
|
253
253
|
|
|
254
254
|
After the workflow succeeds, verify npm `gitHead`, `dist.integrity`, and dist-tag, and verify that the GitHub Release targets the reviewed release commit. If npm already contains the version, treat that version as immutable; fix the source, increment the version, and run the reviewed workflow again.
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "README.md",
|
|
28
|
-
"sha256": "
|
|
28
|
+
"sha256": "38f2f0f783bba47f83950cdc286dc03a3e0ee6190b0fbb67d72002e0402c5805"
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
|
-
"digest": "
|
|
31
|
+
"digest": "fa45bdca9bc397be78314d8f24c5a383637e6c016c61d7635f85fc50e3c04800"
|
|
32
32
|
}
|
|
@@ -127,7 +127,13 @@ export declare class CanonicalTaskPostgresRepository {
|
|
|
127
127
|
limit?: number;
|
|
128
128
|
}): Promise<(JsonRecord | null)[]>;
|
|
129
129
|
findByIdempotencyKey(key: string, storageScope?: string): Promise<JsonRecord | null>;
|
|
130
|
+
private buildInsert;
|
|
130
131
|
create(input: JsonRecord, storageScope?: string): Promise<JsonRecord>;
|
|
132
|
+
/**
|
|
133
|
+
* Create the Task and run its audit writer in one transaction. A throwing
|
|
134
|
+
* writer rolls back the Task insert; the caller maps the audit error.
|
|
135
|
+
*/
|
|
136
|
+
createWithAudit(input: JsonRecord, runAudit: (created: JsonRecord, client: CanonicalTaskPgClient) => Promise<void>, storageScope?: string): Promise<JsonRecord>;
|
|
131
137
|
update(taskId: string, patch: JsonRecord, storageScope?: string): Promise<JsonRecord>;
|
|
132
138
|
listPurposeLabels({ projectCode, assigneePersonId, limit }: {
|
|
133
139
|
projectCode: string;
|
|
@@ -418,8 +418,7 @@ export class CanonicalTaskPostgresRepository {
|
|
|
418
418
|
const result = await this.query('SELECT * FROM canonical_tasks WHERE idempotency_key = $1 LIMIT 1', [key]);
|
|
419
419
|
return this.normalize(result.rows[0] || null);
|
|
420
420
|
}
|
|
421
|
-
|
|
422
|
-
this.rejectStorageScope(storageScope);
|
|
421
|
+
buildInsert(input) {
|
|
423
422
|
const columns = [
|
|
424
423
|
['id', randomUUID()],
|
|
425
424
|
['title', input.title],
|
|
@@ -447,14 +446,55 @@ export class CanonicalTaskPostgresRepository {
|
|
|
447
446
|
['last_operation_key', input.last_operation_key ?? null],
|
|
448
447
|
['last_operation_fingerprint', input.last_operation_fingerprint ?? null],
|
|
449
448
|
];
|
|
450
|
-
|
|
449
|
+
return {
|
|
450
|
+
sql: `INSERT INTO canonical_tasks (${columns.map(([column]) => column).join(', ')})
|
|
451
451
|
VALUES (${columns.map(([, , cast = ''], index) => `$${index + 1}${cast}`).join(', ')})
|
|
452
452
|
ON CONFLICT (idempotency_key) DO NOTHING
|
|
453
|
-
RETURNING *`,
|
|
453
|
+
RETURNING *`,
|
|
454
|
+
values: columns.map(([, value]) => value),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
async create(input, storageScope) {
|
|
458
|
+
this.rejectStorageScope(storageScope);
|
|
459
|
+
const { sql, values } = this.buildInsert(input);
|
|
460
|
+
const result = await this.query(sql, values);
|
|
454
461
|
if (result.rows[0])
|
|
455
462
|
return this.normalize(result.rows[0]);
|
|
456
463
|
return (await this.findByIdempotencyKey(String(input.idempotency_key)));
|
|
457
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Create the Task and run its audit writer in one transaction. A throwing
|
|
467
|
+
* writer rolls back the Task insert; the caller maps the audit error.
|
|
468
|
+
*/
|
|
469
|
+
async createWithAudit(input, runAudit, storageScope) {
|
|
470
|
+
this.rejectStorageScope(storageScope);
|
|
471
|
+
if (typeof this.pool.connect !== 'function') {
|
|
472
|
+
throw this.unavailable(new Error('PostgreSQL transaction client is unavailable'));
|
|
473
|
+
}
|
|
474
|
+
const { sql, values } = this.buildInsert(input);
|
|
475
|
+
const client = await this.pool.connect();
|
|
476
|
+
try {
|
|
477
|
+
await client.query('BEGIN');
|
|
478
|
+
const result = await client.query(sql, values);
|
|
479
|
+
const row = result.rows[0] ?? (await client.query('SELECT * FROM canonical_tasks WHERE idempotency_key = $1 LIMIT 1', [input.idempotency_key])).rows[0];
|
|
480
|
+
if (!row)
|
|
481
|
+
throw storeError('task_store_unavailable', 'PostgreSQL Task store is unavailable', 503);
|
|
482
|
+
const created = this.normalize(row);
|
|
483
|
+
await runAudit(created, client);
|
|
484
|
+
await client.query('COMMIT');
|
|
485
|
+
return created;
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
try {
|
|
489
|
+
await client.query('ROLLBACK');
|
|
490
|
+
}
|
|
491
|
+
catch { /* preserve transaction error */ }
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
client.release?.();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
458
498
|
async update(taskId, patch, storageScope) {
|
|
459
499
|
this.rejectStorageScope(storageScope);
|
|
460
500
|
// The public kernel marks an applied mutation with `_last_operation_key`
|
|
@@ -123,6 +123,11 @@ export interface CanonicalTaskAuditEntry extends JsonRecord {
|
|
|
123
123
|
changes: JsonRecord;
|
|
124
124
|
source_refs: unknown[];
|
|
125
125
|
}
|
|
126
|
+
export interface CanonicalTaskTransactionClient {
|
|
127
|
+
query(text: string, values?: unknown[]): Promise<{
|
|
128
|
+
rows: Array<Record<string, unknown>>;
|
|
129
|
+
}>;
|
|
130
|
+
}
|
|
126
131
|
export interface CanonicalTaskRepository {
|
|
127
132
|
/** Durable PostgreSQL repositories require a release readiness binding. */
|
|
128
133
|
requiresReadiness?: boolean;
|
|
@@ -131,11 +136,20 @@ export interface CanonicalTaskRepository {
|
|
|
131
136
|
get(taskId: string, storageScope?: string): Promise<CanonicalTaskRecord | null>;
|
|
132
137
|
findByIdempotencyKey(key: string, storageScope?: string): Promise<CanonicalTaskRecord | null>;
|
|
133
138
|
create(input: JsonRecord, storageScope?: string): Promise<CanonicalTaskRecord>;
|
|
139
|
+
/**
|
|
140
|
+
* Create a Task and run the audit writer inside one database transaction.
|
|
141
|
+
* A throwing writer must roll back the Task insert.
|
|
142
|
+
*/
|
|
143
|
+
createWithAudit?(input: JsonRecord, runAudit: (created: CanonicalTaskRecord, client: CanonicalTaskTransactionClient) => Promise<void>, storageScope?: string): Promise<CanonicalTaskRecord>;
|
|
134
144
|
update(taskId: string, patch: JsonRecord, storageScope?: string): Promise<CanonicalTaskRecord>;
|
|
135
145
|
delete(taskId: string, expectedVersion: number, storageScope?: string): Promise<void>;
|
|
136
146
|
}
|
|
137
147
|
export interface CanonicalTaskAuditRepository {
|
|
138
|
-
|
|
148
|
+
/** Declare support before offering the same-transaction audit contract. */
|
|
149
|
+
supportsTransactionClient?: boolean;
|
|
150
|
+
upsertAuditLog(entry: CanonicalTaskAuditEntry, options?: {
|
|
151
|
+
client?: CanonicalTaskTransactionClient;
|
|
152
|
+
}): Promise<void>;
|
|
139
153
|
}
|
|
140
154
|
export interface CanonicalTaskAuthorizationRequest {
|
|
141
155
|
action: CanonicalTaskAction;
|
|
@@ -283,6 +297,7 @@ export declare class CanonicalTaskService {
|
|
|
283
297
|
private requireExpectedVersion;
|
|
284
298
|
private assertVersion;
|
|
285
299
|
private executeOperation;
|
|
300
|
+
private buildAuditEntry;
|
|
286
301
|
private audit;
|
|
287
302
|
private read;
|
|
288
303
|
private normalizeResponse;
|
|
@@ -476,18 +476,29 @@ export class CanonicalTaskService {
|
|
|
476
476
|
await this.authorize('read', normalizedContext, existing);
|
|
477
477
|
return { recovered: true, result: this.normalizeResponse(existing) };
|
|
478
478
|
};
|
|
479
|
+
const transactionalAudit = typeof this.repository.createWithAudit === 'function'
|
|
480
|
+
&& this.auditRepository?.supportsTransactionClient === true;
|
|
481
|
+
let auditedInTransaction = false;
|
|
479
482
|
const run = async () => {
|
|
480
483
|
const recovered = await recover();
|
|
481
484
|
if (recovered.recovered && recovered.result)
|
|
482
485
|
return recovered.result;
|
|
483
486
|
await this.assertMutationReady();
|
|
484
|
-
const
|
|
487
|
+
const input = {
|
|
485
488
|
...payload,
|
|
486
489
|
status: 'pending',
|
|
487
490
|
version: 1,
|
|
488
491
|
idempotency_key: operation,
|
|
489
492
|
payload_fingerprint: inputFingerprint,
|
|
490
|
-
}
|
|
493
|
+
};
|
|
494
|
+
if (transactionalAudit) {
|
|
495
|
+
return this.read(() => this.repository.createWithAudit(input, async (created, client) => {
|
|
496
|
+
const normalizedCreated = this.normalizeResponse(created);
|
|
497
|
+
await this.audit(normalizedContext, 'canonical_task.created', normalizedCreated, { before: null, after: normalizedCreated }, operation, inputFingerprint, client);
|
|
498
|
+
auditedInTransaction = true;
|
|
499
|
+
}, ...this.scopeArgs(normalizedContext)));
|
|
500
|
+
}
|
|
501
|
+
const created = await this.read(() => this.repository.create(input, ...this.scopeArgs(normalizedContext)));
|
|
491
502
|
return this.normalizeResponse(created);
|
|
492
503
|
};
|
|
493
504
|
const result = await this.executeOperation({
|
|
@@ -501,10 +512,12 @@ export class CanonicalTaskService {
|
|
|
501
512
|
if (normalizedContext.storageScope !== undefined)
|
|
502
513
|
await this.authorize('read', normalizedContext, result);
|
|
503
514
|
const normalized = this.normalizeResponse(result);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
515
|
+
if (!auditedInTransaction) {
|
|
516
|
+
// Audit each invocation, including a replay/recovery. The deterministic
|
|
517
|
+
// entry id lets an upsert sink collapse retries without losing evidence
|
|
518
|
+
// that the audit write was attempted again.
|
|
519
|
+
await this.audit(normalizedContext, 'canonical_task.created', normalized, { before: null, after: normalized }, operation, inputFingerprint);
|
|
520
|
+
}
|
|
508
521
|
return normalized;
|
|
509
522
|
}
|
|
510
523
|
async updateTask(taskId, input, expectedVersionOrContext, maybeContext) {
|
|
@@ -1054,9 +1067,9 @@ export class CanonicalTaskService {
|
|
|
1054
1067
|
? this.operationRepository.execute(request)
|
|
1055
1068
|
: request.run();
|
|
1056
1069
|
}
|
|
1057
|
-
|
|
1070
|
+
buildAuditEntry(context, action, task, changes, operationKey, operationFingerprint) {
|
|
1058
1071
|
if (!this.auditRepository)
|
|
1059
|
-
return;
|
|
1072
|
+
return null;
|
|
1060
1073
|
const actor = context.auditPrincipal
|
|
1061
1074
|
? normalizeCanonicalTaskPrincipal(context.auditPrincipal)
|
|
1062
1075
|
: context.principal;
|
|
@@ -1078,8 +1091,14 @@ export class CanonicalTaskService {
|
|
|
1078
1091
|
changes,
|
|
1079
1092
|
source_refs: Array.isArray(task.source_refs) ? task.source_refs : [],
|
|
1080
1093
|
};
|
|
1094
|
+
return entry;
|
|
1095
|
+
}
|
|
1096
|
+
async audit(context, action, task, changes, operationKey, operationFingerprint, client) {
|
|
1097
|
+
const entry = this.buildAuditEntry(context, action, task, changes, operationKey, operationFingerprint);
|
|
1098
|
+
if (!entry)
|
|
1099
|
+
return;
|
|
1081
1100
|
try {
|
|
1082
|
-
await this.auditRepository.upsertAuditLog(entry);
|
|
1101
|
+
await this.auditRepository.upsertAuditLog(entry, client ? { client } : undefined);
|
|
1083
1102
|
}
|
|
1084
1103
|
catch {
|
|
1085
1104
|
throw new CanonicalTaskError('task_audit_unavailable', 'Task audit is unavailable', 503);
|
|
@@ -3,6 +3,8 @@ import type { AuthorizationServerMetadata, OAuthClientInformationMixed, OAuthTok
|
|
|
3
3
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
4
4
|
import { type MeetingSourceAccessDetail, type MeetingSourceCallTool, type MeetingSourceFailure, type MeetingSourceFailureReason, type MeetingSourceMeeting, type MeetingSourceProvider, type MeetingSourceSegment, type TactiqTranscriptBudget, type TactiqTranscriptRead } from './meeting-source-reader.js';
|
|
5
5
|
import type { OrganizationConnectionBinding, OrganizationConnectionProviderAdapter } from './organization-connection.js';
|
|
6
|
+
export declare const MEETING_SOURCE_LIST_REVISION = 2;
|
|
7
|
+
export declare const MEETING_SOURCE_RELIST_LOOKBACK_MS: number;
|
|
6
8
|
export type MeetingSourceCapability = 'meetings:list' | 'transcript:read';
|
|
7
9
|
export type MeetingSourceConnectionStatus = 'pending' | 'connected' | 'reauth_required' | 'disabled' | 'revoked';
|
|
8
10
|
export interface MeetingSourceConnectionRecord {
|
|
@@ -156,6 +158,8 @@ export interface MeetingSourceSyncState {
|
|
|
156
158
|
provider: MeetingSourceProvider;
|
|
157
159
|
syncEnabled: boolean;
|
|
158
160
|
listedThrough: string | null;
|
|
161
|
+
/** List-reader revision that produced listedThrough; older cursors are re-listed once. */
|
|
162
|
+
listRevision?: number;
|
|
159
163
|
pending: MeetingSourcePendingMeeting[];
|
|
160
164
|
unobtainable: Array<{
|
|
161
165
|
meetingId: string;
|
|
@@ -12,6 +12,8 @@ import { createTactiqTranscriptBudget, listMeetings, probeTranscript, readTransc
|
|
|
12
12
|
*/
|
|
13
13
|
const HOUR_MS = 60 * 60 * 1000;
|
|
14
14
|
const DAY_MS = 24 * HOUR_MS;
|
|
15
|
+
export const MEETING_SOURCE_LIST_REVISION = 2;
|
|
16
|
+
export const MEETING_SOURCE_RELIST_LOOKBACK_MS = 30 * DAY_MS;
|
|
15
17
|
const PROVIDERS = ['plaud', 'tactiq'];
|
|
16
18
|
const CAPABILITIES = ['meetings:list', 'transcript:read'];
|
|
17
19
|
const STATUSES = ['pending', 'connected', 'reauth_required', 'disabled', 'revoked'];
|
|
@@ -405,11 +407,15 @@ export async function syncMeetingSourceConnection(input) {
|
|
|
405
407
|
state.lastAttemptAt = started.toISOString();
|
|
406
408
|
const callTool = (name, args) => input.runtime.callTool(connection, name, args);
|
|
407
409
|
const until = input.window ? input.window.until : started.toISOString();
|
|
408
|
-
const
|
|
409
|
-
? input.
|
|
410
|
-
:
|
|
411
|
-
|
|
412
|
-
|
|
410
|
+
const normalSince = state.listedThrough
|
|
411
|
+
? Date.parse(state.listedThrough) - (input.overlapMs ?? DAY_MS)
|
|
412
|
+
: started.getTime() - (input.initialLookbackMs ?? 7 * DAY_MS);
|
|
413
|
+
const sinceMs = !input.window
|
|
414
|
+
&& state.listedThrough
|
|
415
|
+
&& (state.listRevision ?? 1) < MEETING_SOURCE_LIST_REVISION
|
|
416
|
+
? Math.min(normalSince, started.getTime() - MEETING_SOURCE_RELIST_LOOKBACK_MS)
|
|
417
|
+
: normalSince;
|
|
418
|
+
const since = input.window ? input.window.since : new Date(sinceMs).toISOString();
|
|
413
419
|
const finish = async (failure, delivered) => {
|
|
414
420
|
if (failure) {
|
|
415
421
|
state.lastError = { ...failure, at: now().toISOString() };
|
|
@@ -529,8 +535,11 @@ export async function syncMeetingSourceConnection(input) {
|
|
|
529
535
|
if (!listed.complete) {
|
|
530
536
|
failure ??= { reason: 'incomplete', message: 'The meeting list could not be read completely for the period' };
|
|
531
537
|
}
|
|
532
|
-
else if (!input.window
|
|
533
|
-
state.listedThrough
|
|
538
|
+
else if (!input.window) {
|
|
539
|
+
if (!state.listedThrough || Date.parse(until) > Date.parse(state.listedThrough)) {
|
|
540
|
+
state.listedThrough = until;
|
|
541
|
+
}
|
|
542
|
+
state.listRevision = MEETING_SOURCE_LIST_REVISION;
|
|
534
543
|
}
|
|
535
544
|
return finish(failure, delivered);
|
|
536
545
|
}
|
|
@@ -153,7 +153,7 @@ async function listPlaud(callTool, since, until, utcOffsetMinutes, pageSize) {
|
|
|
153
153
|
const meetings = [];
|
|
154
154
|
let complete = false;
|
|
155
155
|
for (let page = 1; page <= MAX_PAGES; page += 1) {
|
|
156
|
-
const items =
|
|
156
|
+
const items = plaudListItems(await invoke(callTool, 'list_files', { date_from: dateFrom, date_to: dateTo, page, page_size: pageSize }));
|
|
157
157
|
for (const item of items) {
|
|
158
158
|
const meeting = plaudMeeting(item, utcOffsetMinutes);
|
|
159
159
|
if (!meeting)
|
|
@@ -401,10 +401,25 @@ function payloadOf(result) {
|
|
|
401
401
|
return record.structuredContent;
|
|
402
402
|
if (Array.isArray(record.content)) {
|
|
403
403
|
const text = contentText(record);
|
|
404
|
-
|
|
404
|
+
const parsed = parseJson(text);
|
|
405
|
+
if (parsed !== null)
|
|
406
|
+
return parsed;
|
|
407
|
+
const wrapped = unwrapUntrustedUserData(text);
|
|
408
|
+
return wrapped.found ? wrapped.value : text;
|
|
405
409
|
}
|
|
406
410
|
return result;
|
|
407
411
|
}
|
|
412
|
+
function unwrapUntrustedUserData(text) {
|
|
413
|
+
const opening = /<(untrusted-user-data-[0-9a-f]+)\s+source="[^"]*">/.exec(text);
|
|
414
|
+
if (!opening)
|
|
415
|
+
return { found: false };
|
|
416
|
+
const start = opening.index + opening[0].length;
|
|
417
|
+
const end = text.lastIndexOf(`</${opening[1]}>`);
|
|
418
|
+
if (end < start)
|
|
419
|
+
return { found: false };
|
|
420
|
+
const inner = text.slice(start, end).trim();
|
|
421
|
+
return { found: true, value: parseJson(inner) ?? inner };
|
|
422
|
+
}
|
|
408
423
|
function contentText(record) {
|
|
409
424
|
if (!Array.isArray(record.content))
|
|
410
425
|
return '';
|
|
@@ -414,6 +429,18 @@ function contentText(record) {
|
|
|
414
429
|
.map((item) => item.text)
|
|
415
430
|
.join('\n');
|
|
416
431
|
}
|
|
432
|
+
function plaudListItems(payload) {
|
|
433
|
+
if (Array.isArray(payload))
|
|
434
|
+
return payload;
|
|
435
|
+
const record = asRecord(payload);
|
|
436
|
+
if (record) {
|
|
437
|
+
for (const key of ['data', 'files', 'items', 'results', 'records', 'recordings']) {
|
|
438
|
+
if (Array.isArray(record[key]))
|
|
439
|
+
return record[key];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
throw new UnexpectedShapeError('Plaud list_files returned a response in an unknown shape');
|
|
443
|
+
}
|
|
417
444
|
function extractList(payload) {
|
|
418
445
|
if (Array.isArray(payload))
|
|
419
446
|
return payload;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.1",
|
|
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",
|
|
@@ -586,7 +586,7 @@
|
|
|
586
586
|
"build:world": "node scripts/build-world-vendor.mjs"
|
|
587
587
|
},
|
|
588
588
|
"dependencies": {
|
|
589
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
589
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
590
590
|
"graphology": "0.26.0",
|
|
591
591
|
"js-yaml": "^4.1.1",
|
|
592
592
|
"sigma": "3.0.3",
|
|
@@ -620,5 +620,5 @@
|
|
|
620
620
|
"url": "https://github.com/Unson-LLC/brainbase/issues"
|
|
621
621
|
},
|
|
622
622
|
"homepage": "https://github.com/Unson-LLC/brainbase#readme",
|
|
623
|
-
"gitHead": "
|
|
623
|
+
"gitHead": "514e31c00aeb6906c01cb5df51cf9bea0f62eee2"
|
|
624
624
|
}
|