@unson/brainbase-mcp 0.6.0 → 0.7.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.
Files changed (80) hide show
  1. package/dist/canonical-task-contract.d.ts +9 -0
  2. package/dist/canonical-task-contract.js +39 -0
  3. package/dist/canonical-task-principal.d.ts +16 -0
  4. package/dist/canonical-task-principal.js +36 -0
  5. package/dist/canonical-task-service.d.ts +251 -0
  6. package/dist/canonical-task-service.js +809 -0
  7. package/dist/company-os-evaluation.d.ts +193 -0
  8. package/dist/company-os-evaluation.js +1150 -0
  9. package/dist/company-os-impact-review.d.ts +248 -0
  10. package/dist/company-os-impact-review.js +663 -0
  11. package/dist/company-os-learning-adoption.d.ts +288 -0
  12. package/dist/company-os-learning-adoption.js +944 -0
  13. package/dist/company-os-objectives.d.ts +52 -0
  14. package/dist/company-os-objectives.js +197 -0
  15. package/dist/company-os-receipt-adapter.d.ts +109 -0
  16. package/dist/company-os-receipt-adapter.js +636 -0
  17. package/dist/constraint-exception-store.d.ts +23 -0
  18. package/dist/constraint-exception-store.js +108 -0
  19. package/dist/constraint-resolution.d.ts +175 -0
  20. package/dist/constraint-resolution.js +922 -0
  21. package/dist/decision-adapter.d.ts +145 -0
  22. package/dist/decision-adapter.js +586 -0
  23. package/dist/durable-wait-http.d.ts +51 -0
  24. package/dist/durable-wait-http.js +497 -0
  25. package/dist/durable-waits.d.ts +234 -0
  26. package/dist/durable-waits.js +944 -0
  27. package/dist/execution-authority-http.d.ts +28 -0
  28. package/dist/execution-authority-http.js +359 -0
  29. package/dist/execution-authority.d.ts +201 -0
  30. package/dist/execution-authority.js +634 -0
  31. package/dist/foundation-catalog.d.ts +26 -0
  32. package/dist/foundation-catalog.js +206 -0
  33. package/dist/foundation-constraint-store.d.ts +50 -0
  34. package/dist/foundation-constraint-store.js +215 -0
  35. package/dist/foundation-http.d.ts +90 -0
  36. package/dist/foundation-http.js +439 -0
  37. package/dist/foundation-store.d.ts +132 -0
  38. package/dist/foundation-store.js +654 -0
  39. package/dist/judgment-dag-composition.d.ts +224 -0
  40. package/dist/judgment-dag-composition.js +731 -0
  41. package/dist/judgment-dag.d.ts +2 -0
  42. package/dist/judgment-dag.js +1 -0
  43. package/dist/judgment-problem-snapshot.d.ts +185 -0
  44. package/dist/judgment-problem-snapshot.js +777 -0
  45. package/dist/judgment-view-http.d.ts +22 -0
  46. package/dist/judgment-view-http.js +128 -0
  47. package/dist/judgment-view.d.ts +222 -0
  48. package/dist/judgment-view.js +627 -0
  49. package/dist/knowledge-adapter.d.ts +112 -0
  50. package/dist/knowledge-adapter.js +691 -0
  51. package/dist/ontology-foundation.d.ts +209 -0
  52. package/dist/ontology-foundation.js +1000 -0
  53. package/dist/ontology.d.ts +13 -0
  54. package/dist/ontology.js +14 -0
  55. package/dist/organization-connection.d.ts +164 -0
  56. package/dist/organization-connection.js +428 -0
  57. package/dist/personal-knowledge.d.ts +120 -0
  58. package/dist/personal-knowledge.js +750 -0
  59. package/dist/problem-candidates.d.ts +149 -0
  60. package/dist/problem-candidates.js +898 -0
  61. package/dist/problem-selection.d.ts +238 -0
  62. package/dist/problem-selection.js +995 -0
  63. package/dist/resource-reservation-http.d.ts +31 -0
  64. package/dist/resource-reservation-http.js +371 -0
  65. package/dist/resource-reservations.d.ts +221 -0
  66. package/dist/resource-reservations.js +577 -0
  67. package/dist/server.d.ts +97 -0
  68. package/dist/server.js +180 -1
  69. package/dist/ssot.d.ts +6 -1
  70. package/dist/ssot.js +63 -5
  71. package/dist/streamable-http.d.ts +42 -0
  72. package/dist/streamable-http.js +288 -0
  73. package/dist/types.d.ts +20 -0
  74. package/dist/world-model.d.ts +181 -0
  75. package/dist/world-model.js +873 -0
  76. package/package.json +139 -3
  77. package/ui/judgment-view.css +238 -0
  78. package/ui/judgment-view.js +508 -0
  79. package/ui/objective-editor.css +180 -0
  80. package/ui/objective-editor.js +1139 -0
@@ -0,0 +1,750 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { isIP } from 'node:net';
3
+ import { hostname, userInfo } from 'node:os';
4
+ import { chmod, open, mkdir, readFile, rename, rm, stat } from 'node:fs/promises';
5
+ import { dirname, join } from 'node:path';
6
+ import { resolveDataDir } from './paths.js';
7
+ /** The context header used by the v1 register and search contracts. */
8
+ export const PERSONAL_KNOWLEDGE_CONTEXT_HEADER = 'X-Brainbase-Personal-Context';
9
+ export const PERSONAL_KNOWLEDGE_CONTEXT_VERSION_HEADER = 'X-Brainbase-Personal-Context-Version';
10
+ /** Short alias retained for consumers that call this a personal context header. */
11
+ export const PERSONAL_CONTEXT_HEADER = PERSONAL_KNOWLEDGE_CONTEXT_HEADER;
12
+ export const PERSONAL_KNOWLEDGE_CONTRACT_VERSION = 1;
13
+ export const PERSONAL_KNOWLEDGE_LOCAL_DIRECTORY = 'personal-knowledge-v1';
14
+ export class PersonalKnowledgeContextError extends Error {
15
+ code;
16
+ field;
17
+ constructor(message, code = 'invalid_personal_knowledge_context', field) {
18
+ super(message);
19
+ this.name = 'PersonalKnowledgeContextError';
20
+ this.code = code;
21
+ this.field = field;
22
+ }
23
+ }
24
+ const CONTEXT_FIELDS = [
25
+ 'contract_version',
26
+ 'scope',
27
+ 'owner_person_id',
28
+ 'organization_id',
29
+ 'source_id'
30
+ ];
31
+ function isRecord(value) {
32
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
33
+ }
34
+ function nonEmptyString(value, field) {
35
+ if (typeof value !== 'string' || value.trim().length === 0) {
36
+ throw new PersonalKnowledgeContextError(`${field} must be a non-empty string.`, 'invalid_context_field', field);
37
+ }
38
+ return value;
39
+ }
40
+ /** Validate and return a copy of a strict v1 context object. */
41
+ export function validatePersonalKnowledgeContext(value) {
42
+ if (!isRecord(value)) {
43
+ throw new PersonalKnowledgeContextError('Personal knowledge context must be an object.');
44
+ }
45
+ const unknown = Object.keys(value).filter((key) => !CONTEXT_FIELDS.includes(key));
46
+ if (unknown.length > 0) {
47
+ throw new PersonalKnowledgeContextError(`Personal knowledge context contains unsupported fields: ${unknown.join(', ')}.`, 'unsupported_context_fields', unknown[0]);
48
+ }
49
+ for (const field of CONTEXT_FIELDS) {
50
+ if (!Object.prototype.hasOwnProperty.call(value, field)) {
51
+ throw new PersonalKnowledgeContextError(`Personal knowledge context is missing ${field}.`, 'missing_context_field', field);
52
+ }
53
+ }
54
+ if (value.contract_version !== PERSONAL_KNOWLEDGE_CONTRACT_VERSION) {
55
+ throw new PersonalKnowledgeContextError('Personal knowledge context contract_version must be 1.', 'invalid_context_field', 'contract_version');
56
+ }
57
+ if (value.scope !== 'personal_owned' && value.scope !== 'organization_private') {
58
+ throw new PersonalKnowledgeContextError('Personal knowledge context scope is invalid.', 'invalid_context_field', 'scope');
59
+ }
60
+ const owner = nonEmptyString(value.owner_person_id, 'owner_person_id');
61
+ const source = nonEmptyString(value.source_id, 'source_id');
62
+ if (value.scope === 'personal_owned') {
63
+ if (value.organization_id !== null) {
64
+ throw new PersonalKnowledgeContextError('organization_id must be null for personal_owned context.', 'invalid_context_scope', 'organization_id');
65
+ }
66
+ }
67
+ else {
68
+ nonEmptyString(value.organization_id, 'organization_id');
69
+ }
70
+ return {
71
+ contract_version: PERSONAL_KNOWLEDGE_CONTRACT_VERSION,
72
+ scope: value.scope,
73
+ owner_person_id: owner,
74
+ organization_id: value.organization_id,
75
+ source_id: source
76
+ };
77
+ }
78
+ export const assertPersonalKnowledgeContext = validatePersonalKnowledgeContext;
79
+ function canonicalContextJson(context) {
80
+ const validated = validatePersonalKnowledgeContext(context);
81
+ return JSON.stringify({
82
+ contract_version: validated.contract_version,
83
+ scope: validated.scope,
84
+ owner_person_id: validated.owner_person_id,
85
+ organization_id: validated.organization_id,
86
+ source_id: validated.source_id
87
+ });
88
+ }
89
+ /** Encode a trusted expected context for `X-Brainbase-Personal-Context`. */
90
+ export function encodePersonalKnowledgeContextHeader(value) {
91
+ const context = validatePersonalKnowledgeContext(value);
92
+ return Buffer.from(canonicalContextJson(context), 'utf8').toString('base64url');
93
+ }
94
+ export const encodeExpectedPersonalKnowledgeContextHeader = encodePersonalKnowledgeContextHeader;
95
+ /** Decode and strictly validate a v1 context header. */
96
+ export function decodePersonalKnowledgeContextHeader(value) {
97
+ if (typeof value !== 'string' || value.length === 0 || !/^[A-Za-z0-9_-]+$/.test(value)) {
98
+ throw new PersonalKnowledgeContextError('Personal knowledge context header must be unpadded base64url.', 'invalid_context_header');
99
+ }
100
+ let decoded;
101
+ try {
102
+ decoded = Buffer.from(value, 'base64url').toString('utf8');
103
+ if (!decoded || Buffer.from(value, 'base64url').toString('base64url') !== value) {
104
+ throw new Error('invalid base64url');
105
+ }
106
+ }
107
+ catch {
108
+ throw new PersonalKnowledgeContextError('Personal knowledge context header is not valid base64url.', 'invalid_context_header');
109
+ }
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse(decoded);
113
+ }
114
+ catch {
115
+ throw new PersonalKnowledgeContextError('Personal knowledge context header does not contain JSON.', 'invalid_context_header');
116
+ }
117
+ return validatePersonalKnowledgeContext(parsed);
118
+ }
119
+ export const decodeExpectedPersonalKnowledgeContextHeader = decodePersonalKnowledgeContextHeader;
120
+ /** Assert that two contexts identify exactly the same source of truth. */
121
+ export function assertPersonalKnowledgeContextMatch(expected, actual) {
122
+ const expectedContext = validatePersonalKnowledgeContext(expected);
123
+ const actualContext = validatePersonalKnowledgeContext(actual);
124
+ for (const field of CONTEXT_FIELDS) {
125
+ if (expectedContext[field] !== actualContext[field]) {
126
+ throw new PersonalKnowledgeContextError(`Personal knowledge context mismatch for ${field}.`, 'personal_knowledge_context_mismatch', field);
127
+ }
128
+ }
129
+ return actualContext;
130
+ }
131
+ const EVENT_FIELDS = new Set([
132
+ 'event_id',
133
+ 'occurred_at',
134
+ 'captured_at',
135
+ 'source',
136
+ 'source_pointer',
137
+ 'body_hash',
138
+ 'body',
139
+ 'content',
140
+ 'parent_episode_id',
141
+ 'permission_snapshot',
142
+ 'sensitivity',
143
+ 'kind',
144
+ 'type',
145
+ 'tags',
146
+ 'metadata'
147
+ ]);
148
+ const FORBIDDEN_EVENT_IDENTITY_FIELDS = new Set([
149
+ 'owner_person_id',
150
+ 'ownerPersonId',
151
+ 'organization_id',
152
+ 'organizationId',
153
+ 'person_entity_id',
154
+ 'company_authority_response',
155
+ 'destination',
156
+ 'context',
157
+ 'source_id',
158
+ 'sourceId',
159
+ 'dataDir',
160
+ 'data_dir',
161
+ 'path',
162
+ 'storage_path'
163
+ ]);
164
+ const ISO8601_TIMESTAMP_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(Z|[+-](\d{2}):(\d{2}))$/;
165
+ function eventError(message) {
166
+ return new Error(`Personal knowledge event ${message}`);
167
+ }
168
+ function validateOptionalString(value, field) {
169
+ if (typeof value !== 'string' || !value.trim())
170
+ throw eventError(`${field} must be a non-empty string.`);
171
+ return value;
172
+ }
173
+ function validateTimestamp(value, field) {
174
+ const timestamp = validateOptionalString(value, field);
175
+ const match = ISO8601_TIMESTAMP_PATTERN.exec(timestamp);
176
+ if (!match)
177
+ throw eventError(`${field} must be a valid ISO 8601 timestamp.`);
178
+ const [, yearText, monthText, dayText, hourText, minuteText, secondText, , zone, offsetHourText, offsetMinuteText] = match;
179
+ const year = Number(yearText);
180
+ const month = Number(monthText);
181
+ const day = Number(dayText);
182
+ const hour = Number(hourText);
183
+ const minute = Number(minuteText);
184
+ const second = Number(secondText);
185
+ const offsetHour = offsetHourText === undefined ? 0 : Number(offsetHourText);
186
+ const offsetMinute = offsetMinuteText === undefined ? 0 : Number(offsetMinuteText);
187
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
188
+ const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
189
+ if (month < 1 || month > 12 || day < 1 || day > (daysInMonth ?? 0)
190
+ || hour > 23 || minute > 59 || second > 59
191
+ || (zone !== 'Z' && (offsetHour > 23 || offsetMinute > 59))) {
192
+ throw eventError(`${field} must be a valid ISO 8601 timestamp.`);
193
+ }
194
+ return timestamp;
195
+ }
196
+ function bodyDigest(body) {
197
+ return `sha256:${createHash('sha256').update(body, 'utf8').digest('hex')}`;
198
+ }
199
+ /** Normalize a caller event without allowing caller-controlled identity fields. */
200
+ export function normalizePersonalKnowledgeEvent(value) {
201
+ if (!isRecord(value))
202
+ throw eventError('must be an object.');
203
+ const forbidden = Object.keys(value).filter((field) => FORBIDDEN_EVENT_IDENTITY_FIELDS.has(field));
204
+ if (forbidden.length > 0)
205
+ throw eventError(`owner, organization, context, path, and destination fields are authentication-derived: ${forbidden.join(', ')}.`);
206
+ const unknown = Object.keys(value).filter((field) => !EVENT_FIELDS.has(field));
207
+ if (unknown.length > 0)
208
+ throw eventError(`contains unsupported fields: ${unknown.join(', ')}.`);
209
+ const eventId = validateOptionalString(value.event_id, 'event_id');
210
+ const bodyValue = value.body;
211
+ const contentValue = value.content;
212
+ if (bodyValue !== undefined && typeof bodyValue !== 'string')
213
+ throw eventError('body must be a string.');
214
+ if (contentValue !== undefined && typeof contentValue !== 'string')
215
+ throw eventError('content must be a string.');
216
+ if (bodyValue !== undefined && contentValue !== undefined && bodyValue !== contentValue) {
217
+ throw eventError('body and content must match when both are supplied.');
218
+ }
219
+ const body = (bodyValue ?? contentValue);
220
+ if (typeof body !== 'string' || !body.trim())
221
+ throw eventError('body must be a non-empty string.');
222
+ const providedHash = value.body_hash;
223
+ if (providedHash !== undefined && (typeof providedHash !== 'string' || !providedHash.trim())) {
224
+ throw eventError('body_hash must be a non-empty string.');
225
+ }
226
+ const calculatedHash = bodyDigest(body);
227
+ if (providedHash !== undefined && providedHash !== calculatedHash) {
228
+ throw eventError('body_hash does not match the SHA-256 digest of body.');
229
+ }
230
+ if (value.parent_episode_id !== undefined && value.parent_episode_id !== null) {
231
+ validateOptionalString(value.parent_episode_id, 'parent_episode_id');
232
+ }
233
+ for (const field of ['occurred_at', 'captured_at']) {
234
+ if (value[field] !== undefined)
235
+ validateTimestamp(value[field], field);
236
+ }
237
+ for (const field of ['sensitivity', 'kind', 'type']) {
238
+ if (value[field] !== undefined)
239
+ validateOptionalString(value[field], field);
240
+ }
241
+ if (value.tags !== undefined && (!Array.isArray(value.tags) || value.tags.some((tag) => typeof tag !== 'string' || !tag.trim()))) {
242
+ throw eventError('tags must be an array of non-empty strings.');
243
+ }
244
+ if (value.permission_snapshot !== undefined && !isRecord(value.permission_snapshot)) {
245
+ throw eventError('permission_snapshot must be an object.');
246
+ }
247
+ if (value.metadata !== undefined && !isRecord(value.metadata)) {
248
+ throw eventError('metadata must be an object.');
249
+ }
250
+ const normalized = {
251
+ event_id: eventId,
252
+ body,
253
+ body_hash: calculatedHash
254
+ };
255
+ for (const field of [
256
+ 'occurred_at',
257
+ 'captured_at',
258
+ 'source',
259
+ 'source_pointer',
260
+ 'parent_episode_id',
261
+ 'permission_snapshot',
262
+ 'sensitivity',
263
+ 'kind',
264
+ 'type',
265
+ 'tags',
266
+ 'metadata'
267
+ ]) {
268
+ if (value[field] !== undefined)
269
+ normalized[field] = value[field];
270
+ }
271
+ return normalized;
272
+ }
273
+ export function normalizePersonalKnowledgeSearchInput(query, limit) {
274
+ if (typeof query !== 'string' || !query.trim())
275
+ throw new Error('Personal knowledge search query must be a non-empty string.');
276
+ if (query.length > 4000)
277
+ throw new Error('Personal knowledge search query must be at most 4000 characters.');
278
+ const resolvedLimit = limit === undefined ? 10 : limit;
279
+ if (!Number.isInteger(resolvedLimit) || resolvedLimit < 1 || resolvedLimit > 50) {
280
+ throw new Error('Personal knowledge search limit must be an integer between 1 and 50.');
281
+ }
282
+ return { query: query.trim(), limit: resolvedLimit };
283
+ }
284
+ const LOCAL_LOCK_FILE = '.personal-knowledge.lock';
285
+ const LOCAL_CONTEXT_FILE = 'context.json';
286
+ const LOCAL_EVENTS_FILE = 'events.jsonl';
287
+ const LOCK_TIMEOUT_MS = 30_000;
288
+ function ownerFromEnvironment() {
289
+ const configured = process.env.BRAINBASE_PERSONAL_KNOWLEDGE_OWNER_PERSON_ID;
290
+ if (configured?.trim())
291
+ return configured;
292
+ try {
293
+ return userInfo().username;
294
+ }
295
+ catch {
296
+ return process.env.USER?.trim() || 'local-owner';
297
+ }
298
+ }
299
+ function sourceFromEnvironment() {
300
+ const configured = process.env.BRAINBASE_PERSONAL_KNOWLEDGE_SOURCE_ID;
301
+ return configured?.trim() || undefined;
302
+ }
303
+ function contextForLocalOptions(options) {
304
+ if (options.context !== undefined) {
305
+ const context = validatePersonalKnowledgeContext(options.context);
306
+ if (context.scope !== 'personal_owned' || context.organization_id !== null) {
307
+ throw new Error('OSS local Personal Knowledge storage requires a personal_owned context.');
308
+ }
309
+ return { context };
310
+ }
311
+ const ownerPersonId = options.ownerPersonId?.trim() || ownerFromEnvironment();
312
+ if (!ownerPersonId)
313
+ throw new Error('OSS local Personal Knowledge storage requires an OS owner identity.');
314
+ return { ownerPersonId, sourceId: options.sourceId?.trim() || sourceFromEnvironment() };
315
+ }
316
+ class LocalPersonalKnowledgeStore {
317
+ mode = 'local';
318
+ dataDir;
319
+ requestedContext;
320
+ requestedOwner;
321
+ requestedSource;
322
+ constructor(options = {}) {
323
+ const resolved = contextForLocalOptions(options);
324
+ this.dataDir = join(resolveDataDir(options.dataDir), PERSONAL_KNOWLEDGE_LOCAL_DIRECTORY);
325
+ this.requestedContext = resolved.context;
326
+ this.requestedOwner = resolved.ownerPersonId;
327
+ this.requestedSource = resolved.sourceId;
328
+ }
329
+ async getContext() {
330
+ return withFileLock(this.dataDir, async () => {
331
+ const state = await this.ensureState();
332
+ return { context: state.context };
333
+ });
334
+ }
335
+ async register(input) {
336
+ await this.getContext();
337
+ const event = normalizePersonalKnowledgeEvent(input);
338
+ return withFileLock(this.dataDir, async () => {
339
+ const state = await this.ensureState();
340
+ const existing = state.events.find((candidate) => candidate.event_id === event.event_id);
341
+ if (existing) {
342
+ if (canonicalJson(existing) !== canonicalJson(event)) {
343
+ throw new Error(`personal_knowledge_event_identity_conflict: event_id ${event.event_id} already has different content.`);
344
+ }
345
+ return { context: state.context, event: existing };
346
+ }
347
+ const next = [...state.events, event];
348
+ await writeEventsFile(this.dataDir, next);
349
+ return { context: state.context, event };
350
+ });
351
+ }
352
+ async search(query, limit) {
353
+ await this.getContext();
354
+ const input = normalizePersonalKnowledgeSearchInput(query, limit);
355
+ return withFileLock(this.dataDir, async () => {
356
+ const state = await this.ensureState();
357
+ const needle = input.query.toLocaleLowerCase();
358
+ const results = state.events.filter((event) => canonicalJson(event).toLocaleLowerCase().includes(needle)).slice(0, input.limit);
359
+ return { context: state.context, results };
360
+ });
361
+ }
362
+ async ensureState() {
363
+ await mkdir(this.dataDir, { recursive: true, mode: 0o700 });
364
+ await enforcePrivateDirectory(this.dataDir);
365
+ const contextPath = join(this.dataDir, LOCAL_CONTEXT_FILE);
366
+ const eventsPath = join(this.dataDir, LOCAL_EVENTS_FILE);
367
+ const contextExists = await exists(contextPath);
368
+ const eventsExists = await exists(eventsPath);
369
+ if (contextExists !== eventsExists) {
370
+ throw new Error(`Partial Personal Knowledge v1 store in ${this.dataDir}.`);
371
+ }
372
+ let context;
373
+ let events;
374
+ if (!contextExists) {
375
+ context = this.initialContext();
376
+ await writeFileAtomic(contextPath, `${canonicalContextJson(context)}\n`);
377
+ await writeFileAtomic(eventsPath, '');
378
+ return { context, events: [] };
379
+ }
380
+ context = validatePersonalKnowledgeContext(JSON.parse(await readFile(contextPath, 'utf8')));
381
+ this.assertRequestedContext(context);
382
+ await enforcePrivateFile(contextPath);
383
+ await enforcePrivateFile(eventsPath);
384
+ events = await readEventsFile(eventsPath);
385
+ return { context, events };
386
+ }
387
+ initialContext() {
388
+ if (this.requestedContext)
389
+ return this.requestedContext;
390
+ return validatePersonalKnowledgeContext({
391
+ contract_version: PERSONAL_KNOWLEDGE_CONTRACT_VERSION,
392
+ scope: 'personal_owned',
393
+ owner_person_id: this.requestedOwner,
394
+ organization_id: null,
395
+ source_id: this.requestedSource || `local-${randomUUID()}`
396
+ });
397
+ }
398
+ assertRequestedContext(actual) {
399
+ if (this.requestedContext) {
400
+ assertPersonalKnowledgeContextMatch(this.requestedContext, actual);
401
+ return;
402
+ }
403
+ if (this.requestedOwner && actual.owner_person_id !== this.requestedOwner) {
404
+ throw new PersonalKnowledgeContextError('Persisted local owner does not match the trusted OS owner.', 'personal_knowledge_context_mismatch', 'owner_person_id');
405
+ }
406
+ if (this.requestedSource && actual.source_id !== this.requestedSource) {
407
+ throw new PersonalKnowledgeContextError('Persisted local source does not match the trusted configured source.', 'personal_knowledge_context_mismatch', 'source_id');
408
+ }
409
+ }
410
+ }
411
+ export function createLocalPersonalKnowledgeStore(options = {}) {
412
+ return new LocalPersonalKnowledgeStore(options);
413
+ }
414
+ export const createPersonalKnowledgeStore = createLocalPersonalKnowledgeStore;
415
+ async function readEventsFile(path) {
416
+ const serialized = await readFile(path, 'utf8');
417
+ const events = [];
418
+ const ids = new Set();
419
+ for (const [index, line] of serialized.split('\n').entries()) {
420
+ if (!line.trim())
421
+ continue;
422
+ let parsed;
423
+ try {
424
+ parsed = JSON.parse(line);
425
+ }
426
+ catch {
427
+ throw new Error(`Invalid Personal Knowledge event at line ${index + 1}.`);
428
+ }
429
+ const event = normalizePersonalKnowledgeEvent(parsed);
430
+ if (ids.has(event.event_id))
431
+ throw new Error(`Duplicate Personal Knowledge event_id ${event.event_id}.`);
432
+ ids.add(event.event_id);
433
+ events.push(event);
434
+ }
435
+ return events;
436
+ }
437
+ async function writeEventsFile(dataDir, events) {
438
+ const content = events.map((event) => `${canonicalJson(event)}\n`).join('');
439
+ await writeFileAtomic(join(dataDir, LOCAL_EVENTS_FILE), content);
440
+ }
441
+ function canonicalJson(value) {
442
+ if (Array.isArray(value))
443
+ return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
444
+ if (isRecord(value)) {
445
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
446
+ }
447
+ return JSON.stringify(value) ?? 'null';
448
+ }
449
+ async function exists(path) {
450
+ try {
451
+ await stat(path);
452
+ return true;
453
+ }
454
+ catch (error) {
455
+ if (error.code === 'ENOENT')
456
+ return false;
457
+ throw error;
458
+ }
459
+ }
460
+ async function enforcePrivateDirectory(path) {
461
+ await chmod(path, 0o700);
462
+ }
463
+ async function enforcePrivateFile(path) {
464
+ await chmod(path, 0o600);
465
+ }
466
+ async function writeFileAtomic(path, content) {
467
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
468
+ const temporary = `${path}.${randomUUID()}.tmp`;
469
+ let handle;
470
+ try {
471
+ handle = await open(temporary, 'wx', 0o600);
472
+ await handle.writeFile(content, 'utf8');
473
+ await handle.sync();
474
+ await handle.close();
475
+ handle = undefined;
476
+ await rename(temporary, path);
477
+ }
478
+ finally {
479
+ await handle?.close().catch(() => undefined);
480
+ await rm(temporary, { force: true }).catch(() => undefined);
481
+ }
482
+ }
483
+ async function withFileLock(dataDir, operation) {
484
+ await mkdir(dataDir, { recursive: true, mode: 0o700 });
485
+ const lockPath = join(dataDir, LOCAL_LOCK_FILE);
486
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
487
+ let lock;
488
+ const token = randomUUID();
489
+ while (!lock) {
490
+ let lockCreated = false;
491
+ try {
492
+ lock = await open(lockPath, 'wx', 0o600);
493
+ lockCreated = true;
494
+ await lock.writeFile(JSON.stringify({ token, pid: process.pid, hostname: hostname(), created_at: new Date().toISOString() }), 'utf8');
495
+ await lock.sync();
496
+ }
497
+ catch (error) {
498
+ await lock?.close().catch(() => undefined);
499
+ lock = undefined;
500
+ if (error.code !== 'EEXIST') {
501
+ if (lockCreated)
502
+ await rm(lockPath, { force: true }).catch(() => undefined);
503
+ throw error;
504
+ }
505
+ if (Date.now() >= deadline)
506
+ throw new Error(`Timed out acquiring Personal Knowledge lock ${lockPath}.`);
507
+ await delay(10 + Math.floor(Math.random() * 25));
508
+ }
509
+ }
510
+ try {
511
+ return await operation();
512
+ }
513
+ finally {
514
+ await lock.close().catch(() => undefined);
515
+ await removeOwnedLock(lockPath, token);
516
+ }
517
+ }
518
+ /** Remove a lock only when its ownership token still belongs to this holder. */
519
+ async function removeOwnedLock(lockPath, token) {
520
+ try {
521
+ const current = JSON.parse(await readFile(lockPath, 'utf8'));
522
+ if (current.token !== token)
523
+ return;
524
+ }
525
+ catch (error) {
526
+ if (error.code === 'ENOENT')
527
+ return;
528
+ return;
529
+ }
530
+ await rm(lockPath, { force: true }).catch(() => undefined);
531
+ }
532
+ function delay(milliseconds) {
533
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
534
+ }
535
+ export class PersonalKnowledgeClientError extends Error {
536
+ status;
537
+ responseBody;
538
+ constructor(message, options = {}) {
539
+ super(message);
540
+ this.name = 'PersonalKnowledgeClientError';
541
+ this.status = options.status ?? null;
542
+ this.responseBody = options.responseBody ?? null;
543
+ }
544
+ }
545
+ function parseIPv6Words(host) {
546
+ if (isIP(host) !== 6)
547
+ return null;
548
+ let normalized = host.toLowerCase();
549
+ if (normalized.includes('.')) {
550
+ const separator = normalized.lastIndexOf(':');
551
+ const ipv4 = normalized.slice(separator + 1);
552
+ if (isIP(ipv4) !== 4)
553
+ return null;
554
+ const octets = ipv4.split('.').map(Number);
555
+ normalized = `${normalized.slice(0, separator + 1)}${((octets[0] << 8) | octets[1]).toString(16)}:${((octets[2] << 8) | octets[3]).toString(16)}`;
556
+ }
557
+ const halves = normalized.split('::');
558
+ if (halves.length > 2)
559
+ return null;
560
+ const left = halves[0] ? halves[0].split(':') : [];
561
+ const right = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
562
+ const missing = 8 - left.length - right.length;
563
+ if ((halves.length === 1 && missing !== 0) || (halves.length === 2 && missing < 1))
564
+ return null;
565
+ const groups = [...left, ...Array.from({ length: missing }, () => '0'), ...right];
566
+ if (groups.length !== 8 || groups.some((group) => !/^[0-9a-f]{1,4}$/u.test(group)))
567
+ return null;
568
+ return groups.map((group) => Number.parseInt(group, 16));
569
+ }
570
+ function isLoopback(host) {
571
+ let normalized = host.toLowerCase().replace(/^\[|\]$/g, '');
572
+ if (normalized.endsWith('.'))
573
+ normalized = normalized.slice(0, -1);
574
+ if (normalized === 'localhost')
575
+ return true;
576
+ if (isIP(normalized) === 4)
577
+ return normalized.split('.')[0] === '127';
578
+ const words = parseIPv6Words(normalized);
579
+ if (!words)
580
+ return false;
581
+ const isIPv6Loopback = words.slice(0, 7).every((word) => word === 0) && words[7] === 1;
582
+ if (isIPv6Loopback)
583
+ return true;
584
+ const isIPv4Mapped = words.slice(0, 5).every((word) => word === 0) && words[5] === 0xffff;
585
+ return isIPv4Mapped && (words[6] >> 8) === 0x7f;
586
+ }
587
+ export function normalizePersonalKnowledgeApiUrl(apiUrl, mode) {
588
+ if (typeof apiUrl !== 'string' || !apiUrl.trim())
589
+ throw new Error('Personal Knowledge API URL is required.');
590
+ let url;
591
+ try {
592
+ url = new URL(apiUrl);
593
+ }
594
+ catch {
595
+ throw new Error('Personal Knowledge API URL must be absolute HTTP(S).');
596
+ }
597
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
598
+ throw new Error('Personal Knowledge API URL must use HTTP(S).');
599
+ if (url.username || url.password || url.search || url.hash)
600
+ throw new Error('Personal Knowledge API URL cannot contain credentials, query, or hash.');
601
+ if (mode === 'managed_cloud') {
602
+ if (url.protocol !== 'https:')
603
+ throw new Error('managed_cloud Personal Knowledge API requires HTTPS.');
604
+ if (isLoopback(url.hostname))
605
+ throw new Error('managed_cloud Personal Knowledge API must not use a loopback host.');
606
+ }
607
+ else if (!isLoopback(url.hostname)) {
608
+ throw new Error('local Personal Knowledge API test client requires a loopback host.');
609
+ }
610
+ url.pathname = url.pathname.replace(/\/+$/u, '');
611
+ return url.toString().replace(/\/$/u, '');
612
+ }
613
+ export class PersonalKnowledgeClient {
614
+ mode;
615
+ apiUrl;
616
+ expectedContext;
617
+ getToken;
618
+ fetchImpl;
619
+ constructor(options) {
620
+ if (options.mode !== 'local' && options.mode !== 'managed_cloud')
621
+ throw new Error('Personal Knowledge storage mode must be local or managed_cloud.');
622
+ this.mode = options.mode;
623
+ this.apiUrl = normalizePersonalKnowledgeApiUrl(options.apiUrl, options.mode);
624
+ this.expectedContext = validatePersonalKnowledgeContext(options.expectedContext);
625
+ if (options.getToken)
626
+ this.getToken = options.getToken;
627
+ else if (options.tokenManager?.getToken)
628
+ this.getToken = () => options.tokenManager.getToken();
629
+ else if (options.token !== undefined)
630
+ this.getToken = async () => options.token;
631
+ else
632
+ throw new Error('Personal Knowledge client requires an authenticated token.');
633
+ this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
634
+ }
635
+ async getContext() {
636
+ const payload = await this.request('/api/personal-knowledge/context', { method: 'GET' });
637
+ return assertContextEnvelope(payload, this.expectedContext);
638
+ }
639
+ async register(input) {
640
+ await this.getContext();
641
+ const event = normalizePersonalKnowledgeEvent(input);
642
+ const payload = await this.request('/api/personal-knowledge/events', {
643
+ method: 'POST',
644
+ body: event,
645
+ expectedHeader: true
646
+ });
647
+ return assertRegisterEnvelope(payload, this.expectedContext);
648
+ }
649
+ async search(query, limit) {
650
+ await this.getContext();
651
+ const input = normalizePersonalKnowledgeSearchInput(query, limit);
652
+ const payload = await this.request('/api/personal-knowledge/search', {
653
+ method: 'POST',
654
+ body: input,
655
+ expectedHeader: true
656
+ });
657
+ return assertSearchEnvelope(payload, this.expectedContext);
658
+ }
659
+ async request(path, options) {
660
+ let token;
661
+ try {
662
+ token = await this.getToken();
663
+ }
664
+ catch (error) {
665
+ throw new PersonalKnowledgeClientError(`Personal Knowledge authentication token retrieval failed: ${error instanceof Error ? error.message : String(error)}`);
666
+ }
667
+ if (typeof token !== 'string' || !token.trim())
668
+ throw new PersonalKnowledgeClientError('Personal Knowledge authentication token is unavailable.');
669
+ const headers = new Headers({
670
+ Authorization: `Bearer ${token}`,
671
+ [PERSONAL_KNOWLEDGE_CONTEXT_VERSION_HEADER]: String(PERSONAL_KNOWLEDGE_CONTRACT_VERSION)
672
+ });
673
+ if (options.body !== undefined)
674
+ headers.set('Content-Type', 'application/json');
675
+ if (options.expectedHeader)
676
+ headers.set(PERSONAL_KNOWLEDGE_CONTEXT_HEADER, encodePersonalKnowledgeContextHeader(this.expectedContext));
677
+ let response;
678
+ try {
679
+ response = await this.fetchImpl(`${this.apiUrl}${path}`, {
680
+ method: options.method,
681
+ redirect: 'error',
682
+ headers,
683
+ ...(options.body === undefined ? {} : { body: JSON.stringify(options.body) })
684
+ });
685
+ }
686
+ catch (error) {
687
+ throw new PersonalKnowledgeClientError(`Personal Knowledge API request failed: ${error instanceof Error ? error.message : String(error)}`);
688
+ }
689
+ let raw;
690
+ try {
691
+ raw = await response.text();
692
+ }
693
+ catch (error) {
694
+ throw new PersonalKnowledgeClientError(`Personal Knowledge API response could not be read: ${error instanceof Error ? error.message : String(error)}`);
695
+ }
696
+ const responseBody = raw.trim() ? parseJson(raw) : null;
697
+ if (!response.ok) {
698
+ throw new PersonalKnowledgeClientError(`Personal Knowledge API error: ${response.status} ${response.statusText || 'Request failed'}.`, {
699
+ status: response.status,
700
+ responseBody
701
+ });
702
+ }
703
+ return responseBody;
704
+ }
705
+ }
706
+ function parseJson(raw) {
707
+ try {
708
+ return JSON.parse(raw);
709
+ }
710
+ catch {
711
+ throw new PersonalKnowledgeClientError('Personal Knowledge API returned invalid JSON.', { responseBody: raw });
712
+ }
713
+ }
714
+ function assertContextEnvelope(value, expected) {
715
+ if (!isRecord(value) || !Object.prototype.hasOwnProperty.call(value, 'context'))
716
+ throw new PersonalKnowledgeClientError('Personal Knowledge context response schema is invalid.');
717
+ return { context: assertPersonalKnowledgeContextMatch(expected, value.context) };
718
+ }
719
+ function assertRegisterEnvelope(value, expected) {
720
+ if (!isRecord(value) || !Object.prototype.hasOwnProperty.call(value, 'context') || !Object.prototype.hasOwnProperty.call(value, 'event')) {
721
+ throw new PersonalKnowledgeClientError('Personal Knowledge register response schema is invalid.');
722
+ }
723
+ const context = assertPersonalKnowledgeContextMatch(expected, value.context);
724
+ let event;
725
+ try {
726
+ event = normalizePersonalKnowledgeEvent(value.event);
727
+ }
728
+ catch {
729
+ throw new PersonalKnowledgeClientError('Personal Knowledge register response event is invalid.');
730
+ }
731
+ return { context, event };
732
+ }
733
+ function assertSearchEnvelope(value, expected) {
734
+ if (!isRecord(value) || !Object.prototype.hasOwnProperty.call(value, 'context') || !Object.prototype.hasOwnProperty.call(value, 'results') || !Array.isArray(value.results)) {
735
+ throw new PersonalKnowledgeClientError('Personal Knowledge search response schema is invalid.');
736
+ }
737
+ const context = assertPersonalKnowledgeContextMatch(expected, value.context);
738
+ const results = value.results.map((event, index) => {
739
+ try {
740
+ return normalizePersonalKnowledgeEvent(event);
741
+ }
742
+ catch {
743
+ throw new PersonalKnowledgeClientError(`Personal Knowledge search result ${index} is invalid.`);
744
+ }
745
+ });
746
+ return { context, results };
747
+ }
748
+ export function createPersonalKnowledgeClient(options) {
749
+ return new PersonalKnowledgeClient(options);
750
+ }