@iwo-szapar/data-mcp 0.4.0 → 0.6.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 (36) hide show
  1. package/dist/adapter/factory.d.ts +2 -2
  2. package/dist/adapter/factory.js +7 -3
  3. package/dist/adapter/owner-scope.d.ts +31 -0
  4. package/dist/adapter/owner-scope.js +155 -0
  5. package/dist/adapter/types.d.ts +2 -1
  6. package/dist/config.d.ts +20 -4
  7. package/dist/config.js +14 -1
  8. package/dist/server.js +2 -2
  9. package/dist/tools/memory/contact-create.js +3 -1
  10. package/dist/tools/memory/goal-create.js +3 -1
  11. package/dist/tools/memory/knowledge-decide.js +3 -1
  12. package/dist/tools/memory/knowledge-learn.js +3 -1
  13. package/dist/tools/memory/knowledge-list.js +5 -1
  14. package/dist/tools/memory/knowledge-recall.js +22 -7
  15. package/dist/tools/memory/knowledge-store.js +11 -5
  16. package/dist/tools/memory/link-create.js +15 -9
  17. package/dist/tools/memory/link-delete.js +1 -1
  18. package/dist/tools/memory/link-related.js +1 -1
  19. package/dist/tools/memory/link-suggest.js +1 -1
  20. package/dist/tools/memory/session-log.js +3 -1
  21. package/dist/tools/memory/task-create.js +3 -1
  22. package/dist/tools/register.js +3 -1
  23. package/dist/tools/setup/setup-bootstrap.js +71 -0
  24. package/dist/tools/setup/setup-migrate.js +20 -16
  25. package/dist/tools/setup/setup-status.js +11 -6
  26. package/migrations/pocketbase/001_core_schema.js +24 -28
  27. package/migrations/pocketbase/002_goals_tasks.js +15 -18
  28. package/migrations/pocketbase/003_contacts.js +9 -12
  29. package/migrations/pocketbase/004_entity_aliases.js +9 -12
  30. package/migrations/pocketbase/005_prospects.js +11 -14
  31. package/migrations/pocketbase/006_business.js +29 -33
  32. package/migrations/pocketbase/007_newsletter_affiliates.js +16 -19
  33. package/migrations/pocketbase/008_knowledge_links.js +34 -37
  34. package/migrations/supabase/009_align_to_production.sql +14 -11
  35. package/package.json +1 -1
  36. package/README.md +0 -286
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Adapter factory — creates the correct DataAdapter based on configuration.
3
3
  *
4
- * Always wraps the adapter in SchemaMapProxy (transparent if no mappings).
4
+ * Wraps adapters in SchemaMapProxy and optional OwnerScopeProxy.
5
5
  */
6
6
  import type { Config } from '../config.js';
7
7
  import type { DataAdapter } from './types.js';
8
8
  export declare function createAdapter(config: Config): DataAdapter;
9
- //# sourceMappingURL=factory.d.ts.map
9
+ //# sourceMappingURL=factory.d.ts.map
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Adapter factory — creates the correct DataAdapter based on configuration.
3
3
  *
4
- * Always wraps the adapter in SchemaMapProxy (transparent if no mappings).
4
+ * Wraps adapters in SchemaMapProxy and optional OwnerScopeProxy.
5
5
  */
6
6
  import { PocketBaseAdapter } from './pocketbase.js';
7
7
  import { SupabaseAdapter } from './supabase.js';
8
8
  import { MarkdownAdapter } from './markdown.js';
9
9
  import { SchemaMap, SchemaMapProxy } from './schema-map.js';
10
+ import { OwnerScopeProxy } from './owner-scope.js';
10
11
  export function createAdapter(config) {
11
12
  let adapter;
12
13
  if (config.backend === 'pocketbase') {
@@ -20,8 +21,11 @@ export function createAdapter(config) {
20
21
  }
21
22
  const schemaMap = new SchemaMap(config.schemaMap);
22
23
  if (!schemaMap.isEmpty) {
23
- return new SchemaMapProxy(adapter, schemaMap);
24
+ adapter = new SchemaMapProxy(adapter, schemaMap);
25
+ }
26
+ if (config.backend !== 'pocketbase' && config.ownerRouting) {
27
+ adapter = new OwnerScopeProxy(adapter, config.ownerRouting);
24
28
  }
25
29
  return adapter;
26
30
  }
27
- //# sourceMappingURL=factory.js.map
31
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1,31 @@
1
+ import type { DataAdapter, Filter, ListResult, PageOptions, SortClause } from './types.js';
2
+ export interface OwnerRoutingConfig {
3
+ ownerId: string;
4
+ sharedOwnerId: string;
5
+ }
6
+ export declare class OwnerScopeProxy implements DataAdapter {
7
+ private inner;
8
+ private ownerId;
9
+ private sharedOwnerId;
10
+ readonly ownerScopeEnabled = true;
11
+ constructor(inner: DataAdapter, config: OwnerRoutingConfig);
12
+ get backend(): 'pocketbase' | 'supabase' | 'markdown';
13
+ create<T extends Record<string, unknown>>(collection: string, data: Record<string, unknown>): Promise<T>;
14
+ getOne<T extends Record<string, unknown>>(collection: string, id: string): Promise<T>;
15
+ list<T extends Record<string, unknown>>(collection: string, options?: {
16
+ filter?: Filter;
17
+ sort?: SortClause[];
18
+ page?: PageOptions;
19
+ }): Promise<ListResult<T>>;
20
+ textSearch<T extends Record<string, unknown>>(collection: string, query: string, options?: {
21
+ fields?: string[];
22
+ filter?: Filter;
23
+ limit?: number;
24
+ }): Promise<T[]>;
25
+ update<T extends Record<string, unknown>>(collection: string, id: string, data: Record<string, unknown>): Promise<T>;
26
+ delete(collection: string, id: string): Promise<void>;
27
+ upsert<T extends Record<string, unknown>>(collection: string, data: Record<string, unknown>, uniqueFields: string[]): Promise<T>;
28
+ count(collection: string, filter?: Filter): Promise<number>;
29
+ collectionExists(collection: string): Promise<boolean>;
30
+ listCollections(): Promise<string[]>;
31
+ }
@@ -0,0 +1,155 @@
1
+ import { AdapterError } from '../errors/adapter-error.js';
2
+
3
+ const DEFAULT_SCOPED_COLLECTIONS = new Set([
4
+ 'knowledge',
5
+ 'decisions',
6
+ 'sessions',
7
+ 'goals',
8
+ 'tasks',
9
+ 'contacts',
10
+ 'knowledge_links',
11
+ ]);
12
+
13
+ export class OwnerScopeProxy {
14
+ inner;
15
+ ownerId;
16
+ sharedOwnerId;
17
+ ownerScopeEnabled = true;
18
+ constructor(inner, config) {
19
+ this.inner = inner;
20
+ this.ownerId = config.ownerId;
21
+ this.sharedOwnerId = config.sharedOwnerId;
22
+ }
23
+ get backend() {
24
+ return this.inner.backend;
25
+ }
26
+ async create(collection, data) {
27
+ return this.inner.create(collection, this.withWriteOwner(collection, data));
28
+ }
29
+ async getOne(collection, id) {
30
+ const record = await this.inner.getOne(collection, id);
31
+ this.assertReadable(collection, record);
32
+ return record;
33
+ }
34
+ async list(collection, options) {
35
+ return this.inner.list(collection, this.withReadFilter(collection, options));
36
+ }
37
+ async textSearch(collection, query, options) {
38
+ return this.inner.textSearch(collection, query, this.withReadFilter(collection, options));
39
+ }
40
+ async update(collection, id, data) {
41
+ await this.getOne(collection, id);
42
+ return this.inner.update(collection, id, this.withUpdateOwner(collection, data));
43
+ }
44
+ async delete(collection, id) {
45
+ await this.getOne(collection, id);
46
+ return this.inner.delete(collection, id);
47
+ }
48
+ async upsert(collection, data, uniqueFields) {
49
+ const scopedData = this.withWriteOwner(collection, data);
50
+ const scopedUniqueFields = this.isScoped(collection) && !uniqueFields.includes('owner_id')
51
+ ? [...uniqueFields, 'owner_id']
52
+ : uniqueFields;
53
+ return this.inner.upsert(collection, scopedData, scopedUniqueFields);
54
+ }
55
+ async count(collection, filter) {
56
+ return this.inner.count(collection, this.ownerFilter(collection, filter));
57
+ }
58
+ async collectionExists(collection) {
59
+ return this.inner.collectionExists(collection);
60
+ }
61
+ async listCollections() {
62
+ return this.inner.listCollections();
63
+ }
64
+ isScoped(collection) {
65
+ return DEFAULT_SCOPED_COLLECTIONS.has(collection);
66
+ }
67
+ visibleOwners() {
68
+ return this.ownerId === this.sharedOwnerId
69
+ ? [this.sharedOwnerId]
70
+ : [this.ownerId, this.sharedOwnerId];
71
+ }
72
+ ownerFilter(collection, filter) {
73
+ if (!this.isScoped(collection))
74
+ return filter;
75
+ const groups = filter && filter.length > 0 ? filter : [[]];
76
+ return groups.map((andGroup) => {
77
+ let requestedOwners;
78
+ const cleanGroup = [];
79
+ for (const clause of andGroup) {
80
+ if (clause.field === 'owner_scope') {
81
+ requestedOwners = this.resolveReadOwners(clause.value);
82
+ continue;
83
+ }
84
+ cleanGroup.push(clause);
85
+ }
86
+ const owners = requestedOwners ?? this.visibleOwners();
87
+ const ownerClause = owners.length === 1
88
+ ? { field: 'owner_id', op: 'eq', value: owners[0] }
89
+ : { field: 'owner_id', op: 'in', value: owners };
90
+ return [...cleanGroup, ownerClause];
91
+ });
92
+ }
93
+ withReadFilter(collection, options) {
94
+ if (!this.isScoped(collection))
95
+ return options;
96
+ return { ...options, filter: this.ownerFilter(collection, options?.filter) };
97
+ }
98
+ withWriteOwner(collection, data) {
99
+ if (!this.isScoped(collection))
100
+ return stripOwnerScope(data);
101
+ const ownerId = this.resolveWriteOwner(data);
102
+ return { ...stripOwnerScope(data), owner_id: ownerId };
103
+ }
104
+ withUpdateOwner(collection, data) {
105
+ if (!this.isScoped(collection))
106
+ return stripOwnerScope(data);
107
+ if (!hasOwnerIntent(data))
108
+ return stripOwnerScope(data);
109
+ const ownerId = this.resolveWriteOwner(data);
110
+ return { ...stripOwnerScope(data), owner_id: ownerId };
111
+ }
112
+ resolveWriteOwner(data) {
113
+ const ownerScope = typeof data.owner_scope === 'string' ? data.owner_scope : undefined;
114
+ if (ownerScope === 'shared')
115
+ return this.sharedOwnerId;
116
+ if (ownerScope === 'private')
117
+ return this.ownerId;
118
+ const requestedOwner = typeof data.owner_id === 'string' ? data.owner_id.trim() : '';
119
+ if (!requestedOwner)
120
+ return this.ownerId;
121
+ if (requestedOwner === this.ownerId || requestedOwner === this.sharedOwnerId)
122
+ return requestedOwner;
123
+ throw new AdapterError('VALIDATION_ERROR', 'owner_id must be the current owner or shared owner');
124
+ }
125
+ resolveReadOwners(value) {
126
+ const scopes = Array.isArray(value) ? value : [value];
127
+ const owners = [];
128
+ for (const scope of scopes) {
129
+ if (scope === 'private')
130
+ owners.push(this.ownerId);
131
+ else if (scope === 'shared')
132
+ owners.push(this.sharedOwnerId);
133
+ else
134
+ throw new AdapterError('VALIDATION_ERROR', 'owner_scope must be private or shared');
135
+ }
136
+ return [...new Set(owners)];
137
+ }
138
+ assertReadable(collection, record) {
139
+ if (!this.isScoped(collection))
140
+ return;
141
+ if (this.visibleOwners().includes(record.owner_id))
142
+ return;
143
+ throw new AdapterError('RECORD_NOT_FOUND', `Record not found in '${collection}'`);
144
+ }
145
+ }
146
+
147
+ function stripOwnerScope(data) {
148
+ const { owner_scope: _ownerScope, ...clean } = data;
149
+ return clean;
150
+ }
151
+
152
+ function hasOwnerIntent(data) {
153
+ return Object.prototype.hasOwnProperty.call(data, 'owner_scope')
154
+ || Object.prototype.hasOwnProperty.call(data, 'owner_id');
155
+ }
@@ -63,5 +63,6 @@ export interface DataAdapter {
63
63
  collectionExists(collection: string): Promise<boolean>;
64
64
  /** List all collection names */
65
65
  listCollections(): Promise<string[]>;
66
+ readonly ownerScopeEnabled?: boolean;
66
67
  }
67
- //# sourceMappingURL=types.d.ts.map
68
+ //# sourceMappingURL=types.d.ts.map
package/dist/config.d.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  /**
2
2
  * Configuration parsing from environment variables.
3
3
  *
4
- * SB_BACKEND: 'pocketbase' | 'supabase' (required)
4
+ * SB_BACKEND: 'pocketbase' | 'supabase' | 'markdown' (required)
5
5
  * SB_POCKETBASE_URL: PocketBase server URL (required if backend=pocketbase)
6
6
  * SB_POCKETBASE_ADMIN_EMAIL: PocketBase admin email (required if backend=pocketbase)
7
7
  * SB_POCKETBASE_ADMIN_PASSWORD: PocketBase admin password (required if backend=pocketbase)
8
8
  * SB_SUPABASE_URL: Supabase project URL (required if backend=supabase)
9
9
  * SB_SUPABASE_KEY: Supabase service role key (required if backend=supabase)
10
+ * SB_MARKDOWN_ROOT: filesystem path to the memory/ folder (required if backend=markdown)
10
11
  * SB_SCHEMA_MAP: JSON string mapping logical names to actual table names (optional)
11
12
  * SB_RESEND_API_KEY: Resend API key for email sending (optional)
13
+ * MEMORYOS_OWNER_ID: current memory owner id, enables owner routing when set (optional)
14
+ * MEMORYOS_SHARED_OWNER_ID: shared/team memory owner id (optional, default: firma)
12
15
  */
13
- export type Backend = 'pocketbase' | 'supabase';
16
+ export type Backend = 'pocketbase' | 'supabase' | 'markdown';
14
17
  export interface PocketBaseConfig {
15
18
  backend: 'pocketbase';
16
19
  pocketbaseUrl: string;
@@ -18,6 +21,7 @@ export interface PocketBaseConfig {
18
21
  pocketbaseAdminPassword: string;
19
22
  schemaMap: Record<string, string>;
20
23
  resendApiKey?: string;
24
+ ownerRouting?: OwnerRoutingConfig;
21
25
  }
22
26
  export interface SupabaseConfig {
23
27
  backend: 'supabase';
@@ -25,7 +29,19 @@ export interface SupabaseConfig {
25
29
  supabaseKey: string;
26
30
  schemaMap: Record<string, string>;
27
31
  resendApiKey?: string;
32
+ ownerRouting?: OwnerRoutingConfig;
28
33
  }
29
- export type Config = PocketBaseConfig | SupabaseConfig;
34
+ export interface MarkdownConfig {
35
+ backend: 'markdown';
36
+ markdownRoot: string;
37
+ schemaMap: Record<string, string>;
38
+ resendApiKey?: string;
39
+ ownerRouting?: OwnerRoutingConfig;
40
+ }
41
+ export interface OwnerRoutingConfig {
42
+ ownerId: string;
43
+ sharedOwnerId: string;
44
+ }
45
+ export type Config = PocketBaseConfig | SupabaseConfig | MarkdownConfig;
30
46
  export declare function parseConfig(): Config;
31
- //# sourceMappingURL=config.d.ts.map
47
+ //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -10,6 +10,8 @@
10
10
  * SB_MARKDOWN_ROOT: filesystem path to the memory/ folder (required if backend=markdown)
11
11
  * SB_SCHEMA_MAP: JSON string mapping logical names to actual table names (optional)
12
12
  * SB_RESEND_API_KEY: Resend API key for email sending (optional)
13
+ * MEMORYOS_OWNER_ID: current memory owner id, enables owner routing when set (optional)
14
+ * MEMORYOS_SHARED_OWNER_ID: shared/team memory owner id (optional, default: firma)
13
15
  */
14
16
  const ALLOWED_BACKENDS = ['pocketbase', 'supabase', 'markdown'];
15
17
  export function parseConfig() {
@@ -19,6 +21,7 @@ export function parseConfig() {
19
21
  }
20
22
  const schemaMap = parseSchemaMap(process.env.SB_SCHEMA_MAP);
21
23
  const resendApiKey = process.env.SB_RESEND_API_KEY || undefined;
24
+ const ownerRouting = parseOwnerRouting();
22
25
  if (backend === 'pocketbase') {
23
26
  return {
24
27
  backend,
@@ -27,6 +30,7 @@ export function parseConfig() {
27
30
  pocketbaseAdminPassword: requireEnv('SB_POCKETBASE_ADMIN_PASSWORD'),
28
31
  schemaMap,
29
32
  resendApiKey,
33
+ ownerRouting,
30
34
  };
31
35
  }
32
36
  if (backend === 'markdown') {
@@ -35,6 +39,7 @@ export function parseConfig() {
35
39
  markdownRoot: requireEnv('SB_MARKDOWN_ROOT'),
36
40
  schemaMap,
37
41
  resendApiKey,
42
+ ownerRouting,
38
43
  };
39
44
  }
40
45
  return {
@@ -43,6 +48,7 @@ export function parseConfig() {
43
48
  supabaseKey: requireEnv('SB_SUPABASE_KEY'),
44
49
  schemaMap,
45
50
  resendApiKey,
51
+ ownerRouting,
46
52
  };
47
53
  }
48
54
  function requireEnv(name) {
@@ -69,4 +75,11 @@ function parseSchemaMap(raw) {
69
75
  throw err;
70
76
  }
71
77
  }
72
- //# sourceMappingURL=config.js.map
78
+ function parseOwnerRouting() {
79
+ const ownerId = process.env.MEMORYOS_OWNER_ID?.trim();
80
+ if (!ownerId)
81
+ return undefined;
82
+ const sharedOwnerId = process.env.MEMORYOS_SHARED_OWNER_ID?.trim() || 'firma';
83
+ return { ownerId, sharedOwnerId };
84
+ }
85
+ //# sourceMappingURL=config.js.map
package/dist/server.js CHANGED
@@ -29,8 +29,8 @@ const SERVER_INSTRUCTIONS = `You are the user's AI Second Brain data layer. This
29
29
  - Use setup_status to check database readiness`;
30
30
  export function createServer(adapter) {
31
31
  const server = new McpServer({
32
- name: '@iwo-szapar/data-mcp',
33
- version: '0.3.3',
32
+ name: '@second-brain/data-mcp',
33
+ version: '0.1.0',
34
34
  }, {
35
35
  instructions: SERVER_INSTRUCTIONS,
36
36
  });
@@ -15,6 +15,7 @@ export function registerContactCreate(server, adapter) {
15
15
  relationship: z.enum(['colleague', 'client', 'prospect', 'partner', 'other']).optional().describe('Relationship type'),
16
16
  notes: z.string().max(5000).optional().describe('Notes about the contact'),
17
17
  tags: z.array(z.string().max(100)).max(20).optional().describe('Tags for categorization'),
18
+ owner_scope: z.enum(['private', 'shared']).optional().describe('Store privately for this user or in shared team memory'),
18
19
  }, withGracefulDegradation('contacts', adapter, async (params) => {
19
20
  try {
20
21
  const record = await adapter.create('contacts', {
@@ -26,6 +27,7 @@ export function registerContactCreate(server, adapter) {
26
27
  relationship: params.relationship ?? null,
27
28
  notes: params.notes ?? null,
28
29
  tags: params.tags ?? [],
30
+ ...(adapter.ownerScopeEnabled ? { owner_scope: params.owner_scope } : {}),
29
31
  last_contact_date: null,
30
32
  });
31
33
  return makeToolResponse({
@@ -39,4 +41,4 @@ export function registerContactCreate(server, adapter) {
39
41
  }
40
42
  }));
41
43
  }
42
- //# sourceMappingURL=contact-create.js.map
44
+ //# sourceMappingURL=contact-create.js.map
@@ -16,6 +16,7 @@ export function registerGoalCreate(server, adapter) {
16
16
  current: z.union([z.number(), z.string()]).optional(),
17
17
  })).optional().describe('Key results to track progress'),
18
18
  tags: z.array(z.string().max(100)).max(20).optional().describe('Tags for categorization'),
19
+ owner_scope: z.enum(['private', 'shared']).optional().describe('Store privately for this user or in shared team memory'),
19
20
  }, withGracefulDegradation('goals', adapter, async (params) => {
20
21
  try {
21
22
  const record = await adapter.create('goals', {
@@ -25,6 +26,7 @@ export function registerGoalCreate(server, adapter) {
25
26
  status: 'active',
26
27
  key_results: params.key_results ?? [],
27
28
  tags: params.tags ?? [],
29
+ ...(adapter.ownerScopeEnabled ? { owner_scope: params.owner_scope } : {}),
28
30
  });
29
31
  return makeToolResponse({
30
32
  created: true,
@@ -37,4 +39,4 @@ export function registerGoalCreate(server, adapter) {
37
39
  }
38
40
  }));
39
41
  }
40
- //# sourceMappingURL=goal-create.js.map
42
+ //# sourceMappingURL=goal-create.js.map
@@ -14,6 +14,7 @@ export function registerKnowledgeDecide(server, adapter) {
14
14
  chosen_option: z.string().min(1).max(500).describe('The option that was chosen'),
15
15
  rationale: z.string().max(5000).optional().describe('Why this option was chosen'),
16
16
  tags: z.array(z.string().max(100)).max(20).optional().describe('Tags for categorization'),
17
+ owner_scope: z.enum(['private', 'shared']).optional().describe('Store privately for this user or in shared team memory'),
17
18
  }, withGracefulDegradation('decisions', adapter, async (params) => {
18
19
  try {
19
20
  const record = await adapter.create('decisions', {
@@ -24,6 +25,7 @@ export function registerKnowledgeDecide(server, adapter) {
24
25
  rationale: params.rationale ?? null,
25
26
  outcome: null,
26
27
  tags: params.tags ?? [],
28
+ ...(adapter.ownerScopeEnabled ? { owner_scope: params.owner_scope } : {}),
27
29
  });
28
30
  return makeToolResponse({
29
31
  stored: true,
@@ -36,4 +38,4 @@ export function registerKnowledgeDecide(server, adapter) {
36
38
  }
37
39
  }));
38
40
  }
39
- //# sourceMappingURL=knowledge-decide.js.map
41
+ //# sourceMappingURL=knowledge-decide.js.map
@@ -13,6 +13,7 @@ export function registerKnowledgeLearn(server, adapter) {
13
13
  content: z.string().min(1).max(10000).describe('Detailed description of the learning'),
14
14
  tags: z.array(z.string().max(100)).max(20).optional().describe('Tags for categorization'),
15
15
  source: z.string().max(500).optional().describe('Context where this was learned'),
16
+ owner_scope: z.enum(['private', 'shared']).optional().describe('Store privately for this user or in shared team memory'),
16
17
  }, withGracefulDegradation('knowledge', adapter, async (params) => {
17
18
  try {
18
19
  const record = await adapter.create('knowledge', {
@@ -22,6 +23,7 @@ export function registerKnowledgeLearn(server, adapter) {
22
23
  summary: generateSummary(params.content),
23
24
  tags: params.tags ?? [],
24
25
  source: params.source ?? null,
26
+ ...(adapter.ownerScopeEnabled ? { owner_scope: params.owner_scope } : {}),
25
27
  confidence: 0.8,
26
28
  last_validated_at: new Date().toISOString(),
27
29
  });
@@ -36,4 +38,4 @@ export function registerKnowledgeLearn(server, adapter) {
36
38
  }
37
39
  }));
38
40
  }
39
- //# sourceMappingURL=knowledge-learn.js.map
41
+ //# sourceMappingURL=knowledge-learn.js.map
@@ -9,6 +9,7 @@ export function registerKnowledgeList(server, adapter) {
9
9
  server.tool('knowledge_list', 'List knowledge items with optional type and tag filters. Supports pagination.', {
10
10
  type: z.enum(['fact', 'pattern', 'insight', 'lesson', 'reference']).optional().describe('Filter by knowledge type'),
11
11
  tags: z.array(z.string()).optional().describe('Filter by tags (items must contain ALL specified tags)'),
12
+ owner_scope: z.enum(['private', 'shared']).optional().describe('Filter to private or shared team memory'),
12
13
  limit: z.number().int().min(1).max(100).optional().describe('Max results (default 20)'),
13
14
  offset: z.number().int().min(0).optional().describe('Offset for pagination (default 0)'),
14
15
  }, { readOnlyHint: true }, withGracefulDegradation('knowledge', adapter, async (params) => {
@@ -22,6 +23,9 @@ export function registerKnowledgeList(server, adapter) {
22
23
  clauses.push({ field: 'tags', op: 'contains', value: tag });
23
24
  }
24
25
  }
26
+ if (params.owner_scope && adapter.ownerScopeEnabled) {
27
+ clauses.push({ field: 'owner_scope', op: 'eq', value: params.owner_scope });
28
+ }
25
29
  const filter = clauses.length > 0 ? [clauses] : undefined;
26
30
  const result = await adapter.list('knowledge', {
27
31
  filter,
@@ -40,4 +44,4 @@ export function registerKnowledgeList(server, adapter) {
40
44
  }
41
45
  }));
42
46
  }
43
- //# sourceMappingURL=knowledge-list.js.map
47
+ //# sourceMappingURL=knowledge-list.js.map
@@ -12,15 +12,21 @@ export function registerKnowledgeRecall(server, adapter) {
12
12
  server.tool('knowledge_recall', 'Search your persistent memory. Full-text search across knowledge items and decisions. Empty query returns most recent items. Supports alias expansion (e.g., "payment" also finds "stripe").', {
13
13
  query: z.string().max(500).optional().describe('Search query (full-text search). Empty returns recent items.'),
14
14
  type: z.enum(['fact', 'pattern', 'insight', 'lesson', 'reference']).optional().describe('Filter by knowledge type'),
15
+ owner_scope: z.enum(['private', 'shared']).optional().describe('Filter to private or shared team memory'),
15
16
  limit: z.number().int().min(1).max(50).optional().describe('Max results (default 10)'),
16
17
  }, { readOnlyHint: true }, withGracefulDegradation('knowledge', adapter, async (params) => {
17
18
  try {
18
19
  const resultLimit = params.limit ?? 10;
19
20
  // Empty query: return most recent items
20
21
  if (!params.query || params.query.trim() === '') {
21
- const filter = params.type
22
- ? [[{ field: 'type', op: 'eq', value: params.type }]]
23
- : undefined;
22
+ const clauses = [];
23
+ if (params.type) {
24
+ clauses.push({ field: 'type', op: 'eq', value: params.type });
25
+ }
26
+ if (params.owner_scope && adapter.ownerScopeEnabled) {
27
+ clauses.push({ field: 'owner_scope', op: 'eq', value: params.owner_scope });
28
+ }
29
+ const filter = clauses.length > 0 ? [clauses] : undefined;
24
30
  const result = await adapter.list('knowledge', {
25
31
  filter,
26
32
  sort: [{ field: 'created_at', direction: 'desc' }],
@@ -38,9 +44,14 @@ export function registerKnowledgeRecall(server, adapter) {
38
44
  const expandedTerms = await expandQueryWithAliases(adapter, searchQuery);
39
45
  const expandedQuery = expandedTerms.join(' ');
40
46
  // Search knowledge
41
- const typeFilter = params.type
42
- ? [[{ field: 'type', op: 'eq', value: params.type }]]
43
- : undefined;
47
+ const clauses = [];
48
+ if (params.type) {
49
+ clauses.push({ field: 'type', op: 'eq', value: params.type });
50
+ }
51
+ if (params.owner_scope && adapter.ownerScopeEnabled) {
52
+ clauses.push({ field: 'owner_scope', op: 'eq', value: params.owner_scope });
53
+ }
54
+ const typeFilter = clauses.length > 0 ? [clauses] : undefined;
44
55
  const knowledgeResults = await adapter.textSearch('knowledge', expandedQuery, {
45
56
  fields: ['title', 'content', 'summary'],
46
57
  filter: typeFilter,
@@ -51,8 +62,12 @@ export function registerKnowledgeRecall(server, adapter) {
51
62
  try {
52
63
  const decisionsExist = await adapter.collectionExists('decisions');
53
64
  if (decisionsExist) {
65
+ const decisionFilter = params.owner_scope && adapter.ownerScopeEnabled
66
+ ? [[{ field: 'owner_scope', op: 'eq', value: params.owner_scope }]]
67
+ : undefined;
54
68
  decisionResults = await adapter.textSearch('decisions', expandedQuery, {
55
69
  fields: ['title', 'context', 'chosen_option'],
70
+ filter: decisionFilter,
56
71
  limit: 5,
57
72
  });
58
73
  }
@@ -76,4 +91,4 @@ export function registerKnowledgeRecall(server, adapter) {
76
91
  }
77
92
  }));
78
93
  }
79
- //# sourceMappingURL=knowledge-recall.js.map
94
+ //# sourceMappingURL=knowledge-recall.js.map
@@ -13,14 +13,19 @@ export function registerKnowledgeStore(server, adapter) {
13
13
  content: z.string().min(1).max(10000).describe('Content of the knowledge item'),
14
14
  tags: z.array(z.string().max(100)).max(20).optional().describe('Tags for categorization'),
15
15
  source: z.string().max(500).optional().describe('Source of the knowledge (URL, book, conversation, etc.)'),
16
+ owner_scope: z.enum(['private', 'shared']).optional().describe('Store privately for this user or in shared team memory'),
16
17
  }, withGracefulDegradation('knowledge', adapter, async (params) => {
17
18
  try {
18
19
  // Dedup: check for existing item with same type + title
20
+ const dedupFilter = [
21
+ { field: 'type', op: 'eq', value: params.type },
22
+ { field: 'title', op: 'eq', value: params.title.trim() },
23
+ ];
24
+ if (adapter.ownerScopeEnabled) {
25
+ dedupFilter.push({ field: 'owner_scope', op: 'eq', value: params.owner_scope ?? 'private' });
26
+ }
19
27
  const existing = await adapter.list('knowledge', {
20
- filter: [[
21
- { field: 'type', op: 'eq', value: params.type },
22
- { field: 'title', op: 'eq', value: params.title.trim() },
23
- ]],
28
+ filter: [dedupFilter],
24
29
  page: { limit: 1, offset: 0 },
25
30
  });
26
31
  if (existing.items.length > 0) {
@@ -39,6 +44,7 @@ export function registerKnowledgeStore(server, adapter) {
39
44
  summary: generateSummary(params.content),
40
45
  tags: params.tags ?? [],
41
46
  source: params.source ?? null,
47
+ ...(adapter.ownerScopeEnabled ? { owner_scope: params.owner_scope } : {}),
42
48
  confidence: 0.8,
43
49
  last_validated_at: new Date().toISOString(),
44
50
  });
@@ -53,4 +59,4 @@ export function registerKnowledgeStore(server, adapter) {
53
59
  }
54
60
  }));
55
61
  }
56
- //# sourceMappingURL=knowledge-store.js.map
62
+ //# sourceMappingURL=knowledge-store.js.map
@@ -12,12 +12,13 @@ export function registerLinkCreate(server, adapter) {
12
12
  'Links express how knowledge items relate: supports, contradicts, derived_from, etc. ' +
13
13
  'Deduplicates by (source, target, relation_type).', {
14
14
  source_type: z.enum(ENTITY_TYPES).describe('Type of the source entity'),
15
- source_id: z.string().min(1).max(50).describe('ID of the source entity (UUID on Supabase, 15-char native ID on PocketBase)'),
15
+ source_id: z.string().uuid().describe('UUID of the source entity'),
16
16
  target_type: z.enum(ENTITY_TYPES).describe('Type of the target entity'),
17
- target_id: z.string().min(1).max(50).describe('ID of the target entity (UUID on Supabase, 15-char native ID on PocketBase)'),
17
+ target_id: z.string().uuid().describe('UUID of the target entity'),
18
18
  relation_type: z.enum(RELATION_TYPES).describe('Type of relationship'),
19
19
  confidence: z.number().min(0).max(1).optional().default(0.8).describe('Confidence in this relationship (0-1)'),
20
20
  notes: z.string().max(500).optional().describe('Optional context for why this link exists'),
21
+ owner_scope: z.enum(['private', 'shared']).optional().describe('Store privately for this user or in shared team memory'),
21
22
  }, withGracefulDegradation('knowledge_links', adapter, async (params) => {
22
23
  try {
23
24
  if (params.source_type === params.target_type && params.source_id === params.target_id) {
@@ -26,14 +27,18 @@ export function registerLinkCreate(server, adapter) {
26
27
  message: 'Cannot create a self-link.',
27
28
  });
28
29
  }
30
+ const dedupFilter = [
31
+ { field: 'source_type', op: 'eq', value: params.source_type },
32
+ { field: 'source_id', op: 'eq', value: params.source_id },
33
+ { field: 'target_type', op: 'eq', value: params.target_type },
34
+ { field: 'target_id', op: 'eq', value: params.target_id },
35
+ { field: 'relation_type', op: 'eq', value: params.relation_type },
36
+ ];
37
+ if (adapter.ownerScopeEnabled) {
38
+ dedupFilter.push({ field: 'owner_scope', op: 'eq', value: params.owner_scope ?? 'private' });
39
+ }
29
40
  const existing = await adapter.list('knowledge_links', {
30
- filter: [[
31
- { field: 'source_type', op: 'eq', value: params.source_type },
32
- { field: 'source_id', op: 'eq', value: params.source_id },
33
- { field: 'target_type', op: 'eq', value: params.target_type },
34
- { field: 'target_id', op: 'eq', value: params.target_id },
35
- { field: 'relation_type', op: 'eq', value: params.relation_type },
36
- ]],
41
+ filter: [dedupFilter],
37
42
  page: { limit: 1, offset: 0 },
38
43
  });
39
44
  if (existing.items.length > 0) {
@@ -51,6 +56,7 @@ export function registerLinkCreate(server, adapter) {
51
56
  relation_type: params.relation_type,
52
57
  confidence: params.confidence ?? 0.8,
53
58
  notes: params.notes ?? null,
59
+ ...(adapter.ownerScopeEnabled ? { owner_scope: params.owner_scope } : {}),
54
60
  auto_suggested: false,
55
61
  });
56
62
  return makeToolResponse({
@@ -7,7 +7,7 @@ import { z } from 'zod';
7
7
  import { makeToolResponse, handleAdapterError, withGracefulDegradation } from '../shared.js';
8
8
  export function registerLinkDelete(server, adapter) {
9
9
  server.tool('link_delete', 'Delete a knowledge link by its ID.', {
10
- link_id: z.string().min(1).max(50).describe('ID of the link to delete'),
10
+ link_id: z.string().uuid().describe('UUID of the link to delete'),
11
11
  }, withGracefulDegradation('knowledge_links', adapter, async (params) => {
12
12
  try {
13
13
  try {
@@ -9,7 +9,7 @@ const ENTITY_TYPES = ['knowledge', 'decision', 'session', 'blog_post', 'prospect
9
9
  export function registerLinkRelated(server, adapter) {
10
10
  server.tool('link_related', 'Get all links for an entity. Shows outgoing and incoming relationships with resolved titles.', {
11
11
  entity_type: z.enum(ENTITY_TYPES).describe('Type of the entity'),
12
- entity_id: z.string().min(1).max(50).describe('ID of the entity (UUID on Supabase, 15-char native ID on PocketBase)'),
12
+ entity_id: z.string().uuid().describe('UUID of the entity'),
13
13
  direction: z.enum(['both', 'outgoing', 'incoming']).optional().default('both').describe('Filter direction'),
14
14
  relation_type: z.enum(['supports', 'contradicts', 'derived_from', 'example_of', 'supersedes', 'part_of', 'prerequisite'])
15
15
  .optional().describe('Filter by relation type'),
@@ -8,7 +8,7 @@ import { makeToolResponse, handleAdapterError, withGracefulDegradation } from '.
8
8
  export function registerLinkSuggest(server, adapter) {
9
9
  server.tool('link_suggest', 'Find knowledge items similar to a given item and suggest links. ' +
10
10
  'Uses text search to find related items. Returns matches with suggested relation types.', {
11
- item_id: z.string().min(1).max(50).describe('ID of the knowledge item to find suggestions for'),
11
+ item_id: z.string().uuid().describe('UUID of the knowledge item to find suggestions for'),
12
12
  limit: z.number().min(1).max(20).optional().default(5).describe('Max suggestions'),
13
13
  }, withGracefulDegradation('knowledge', adapter, async (params) => {
14
14
  try {