frinus-mcp 3.3.0 → 3.4.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.
@@ -55,6 +55,21 @@ export declare class MemoryClient implements MemoryClientInterface {
55
55
  task_description: string;
56
56
  max_tokens?: number;
57
57
  }): Promise<any>;
58
+ /** List memories with a classification suggestion awaiting approval. */
59
+ getPendingClassifications(limit?: number): Promise<any>;
60
+ /**
61
+ * Approve a proposed classification. Without overrides, the backend applies
62
+ * its own suggestion. Passing universe_id and/or context_id overrides the
63
+ * suggested target (also used to CORRECT a wrong suggestion).
64
+ */
65
+ approveClassification(memoryId: string, data?: {
66
+ universe_id?: string;
67
+ context_id?: string;
68
+ }): Promise<any>;
69
+ /** Reject a proposed classification (memory stays where it is). */
70
+ rejectClassification(memoryId: string): Promise<any>;
71
+ /** Approve multiple proposed classifications in one call. */
72
+ approveClassificationBatch(ids: string[]): Promise<any>;
58
73
  reinforceMemory(memoryId: string, boost?: number): Promise<any>;
59
74
  weakenMemory(memoryId: string, penalty?: number): Promise<any>;
60
75
  getWorkingMemory(contextId: string): Promise<any>;
@@ -142,6 +142,46 @@ export class MemoryClient {
142
142
  return response.data;
143
143
  }
144
144
  // =========================================================================
145
+ // Auto-classification (human-in-the-loop)
146
+ //
147
+ // The backend classifies a memory by content at write time and returns a
148
+ // SUGGESTION with classification_status="proposed". The user reviews the
149
+ // pending queue and explicitly approves (optionally overriding the target)
150
+ // or rejects. Nothing is moved automatically.
151
+ // =========================================================================
152
+ /** List memories with a classification suggestion awaiting approval. */
153
+ async getPendingClassifications(limit) {
154
+ const params = limit ? `?limit=${limit}` : "";
155
+ const response = await this.client.get(`/memories/classification/pending${params}`);
156
+ return response.data;
157
+ }
158
+ /**
159
+ * Approve a proposed classification. Without overrides, the backend applies
160
+ * its own suggestion. Passing universe_id and/or context_id overrides the
161
+ * suggested target (also used to CORRECT a wrong suggestion).
162
+ */
163
+ async approveClassification(memoryId, data) {
164
+ const payload = {};
165
+ if (data?.universe_id)
166
+ payload.universe_id = data.universe_id;
167
+ if (data?.context_id)
168
+ payload.context_id = data.context_id;
169
+ const response = await this.client.post(`/memories/${memoryId}/classification/approve`, payload);
170
+ return response.data;
171
+ }
172
+ /** Reject a proposed classification (memory stays where it is). */
173
+ async rejectClassification(memoryId) {
174
+ const response = await this.client.post(`/memories/${memoryId}/classification/reject`);
175
+ return response.data;
176
+ }
177
+ /** Approve multiple proposed classifications in one call. */
178
+ async approveClassificationBatch(ids) {
179
+ const response = await this.client.post("/memories/classification/approve-batch", {
180
+ ids,
181
+ });
182
+ return response.data;
183
+ }
184
+ // =========================================================================
145
185
  // Dynamic Relevance
146
186
  // =========================================================================
147
187
  async reinforceMemory(memoryId, boost) {
@@ -9,6 +9,13 @@ export const TOOLS = [
9
9
  Use this to record important information, learnings, decisions, or procedures
10
10
  that should be remembered for future reference.
11
11
 
12
+ The backend classifies the memory by content at write time and returns a
13
+ SUGGESTION (status "proposed") of which universe/context node it belongs to —
14
+ nothing is moved automatically. Review pending suggestions with
15
+ memory_classification_pending and apply them with memory_classification_approve.
16
+ Passing scope/universe_id/context_id explicitly sets the destination directly
17
+ (no suggestion needed).
18
+
12
19
  Memory types:
13
20
  - episodic: Specific experiences and events (what happened)
14
21
  - semantic: General knowledge and facts (what I know)
@@ -38,7 +45,7 @@ Scopes:
38
45
  scope: {
39
46
  type: "string",
40
47
  enum: ["user", "agent", "universe", "organization"],
41
- description: "Visibility scope. Padrão: a memória é associada ao universo do agente (contexto). 'organization' (org-wide) e 'user' (private) só são usados quando explicitamente definidos; 'agent' (agent-only) é o fallback automático quando não há universo identificável.",
48
+ description: "Optional explicit visibility scope. Omit to let the backend auto-classify and return a suggestion (status 'proposed'). When set, it overrides classification (status 'manual'): 'organization' (org-wide), 'user' (private), 'universe' (department-shared), 'agent' (agent-only).",
42
49
  },
43
50
  importance: {
44
51
  type: "number",
@@ -58,7 +65,7 @@ Scopes:
58
65
  },
59
66
  universe_id: {
60
67
  type: "string",
61
- description: "UUID do universo/contexto ao qual associar a memória. Se omitido, a memória é associada automaticamente ao universo do agente.",
68
+ description: "Optional UUID of a universe to associate the memory with explicitly (overrides auto-classification → status 'manual'). Omit to let the backend suggest one.",
62
69
  },
63
70
  context_id: {
64
71
  type: "string",
@@ -156,6 +163,64 @@ Use with caution - this permanently removes the memory.`,
156
163
  },
157
164
  },
158
165
  // ==========================================================================
166
+ // Auto-classification — human-in-the-loop (3)
167
+ // ==========================================================================
168
+ {
169
+ name: "memory_classification_pending",
170
+ description: `List memories whose auto-classification is awaiting your approval.
171
+
172
+ The backend classifies each new memory by content and proposes a target
173
+ universe/context node (status "proposed"). This returns that review queue with,
174
+ per item: the memory id, the suggested destination breadcrumb, and the
175
+ confidence. Approve with memory_classification_approve, or correct the target by
176
+ passing universe_id/context_id to it.`,
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: {
180
+ limit: { type: "integer", description: "Maximum suggestions to return (default: backend default)" },
181
+ },
182
+ required: [],
183
+ },
184
+ },
185
+ {
186
+ name: "memory_classification_approve",
187
+ description: `Approve a proposed classification, moving the memory to its suggested
188
+ universe/context node.
189
+
190
+ Without overrides, the backend applies its own suggestion. Pass universe_id
191
+ and/or context_id to override the suggested target (also the way to CORRECT a
192
+ wrong suggestion).`,
193
+ inputSchema: {
194
+ type: "object",
195
+ properties: {
196
+ memory_id: { type: "string", description: "UUID of the memory whose classification is being approved" },
197
+ universe_id: { type: "string", description: "Optional UUID of a universe to override the suggested destination" },
198
+ context_id: { type: "string", description: "Optional UUID of a hierarchy node (concept/theme/topic/point) to override the suggested destination" },
199
+ },
200
+ required: ["memory_id"],
201
+ },
202
+ },
203
+ {
204
+ name: "memory_reclassify",
205
+ description: `Re-classify a memory. Either pin it to an explicit destination, or ask the
206
+ backend to re-run its automatic classifier.
207
+
208
+ - Pass universe_id and/or context_id to set the destination directly (correction).
209
+ - Pass rerun_auto=true (and no explicit target) to discard the current
210
+ suggestion and let the backend propose a new one (review via
211
+ memory_classification_pending).`,
212
+ inputSchema: {
213
+ type: "object",
214
+ properties: {
215
+ memory_id: { type: "string", description: "UUID of the memory to reclassify" },
216
+ universe_id: { type: "string", description: "Optional UUID of a universe to set as the explicit destination" },
217
+ context_id: { type: "string", description: "Optional UUID of a hierarchy node to set as the explicit destination" },
218
+ rerun_auto: { type: "boolean", description: "When true (and no explicit target), re-run the backend's automatic classification" },
219
+ },
220
+ required: ["memory_id"],
221
+ },
222
+ },
223
+ // ==========================================================================
159
224
  // Dynamic Relevance (2)
160
225
  // ==========================================================================
161
226
  {
@@ -196,84 +196,255 @@ export function handleApiError(error) {
196
196
  }
197
197
  return null; // Not a known error, let it propagate
198
198
  }
199
+ // ---------------------------------------------------------------------------
200
+ // Auto-classification rendering helpers (human-in-the-loop)
201
+ // ---------------------------------------------------------------------------
202
+ // The Memory Engine now classifies a memory by content at write time and
203
+ // returns a SUGGESTION with classification_status="proposed". These helpers
204
+ // render that suggestion enxuto, and degrade gracefully to the minimal
205
+ // ID/Type/Scope format when an OLDER backend omits the classification fields.
206
+ /** Human-readable confidence bucket from a 0..1 score. >=0.75 alta, >=0.45 média. */
207
+ function confidenceLabel(confidence) {
208
+ if (typeof confidence !== "number" || Number.isNaN(confidence))
209
+ return null;
210
+ if (confidence >= 0.75)
211
+ return "alta";
212
+ if (confidence >= 0.45)
213
+ return "média";
214
+ return "baixa";
215
+ }
216
+ /** Name of a classified universe, tolerating string | {name} shapes. */
217
+ function universeName(u) {
218
+ if (!u)
219
+ return null;
220
+ if (typeof u === "string")
221
+ return u;
222
+ if (typeof u === "object") {
223
+ const name = u.name;
224
+ if (typeof name === "string" && name)
225
+ return name;
226
+ }
227
+ return null;
228
+ }
229
+ /** Breadcrumb path of a classified context node ("Checkout › Antifraude"). */
230
+ function contextName(c) {
231
+ if (!c)
232
+ return null;
233
+ if (typeof c === "string")
234
+ return c;
235
+ if (typeof c === "object") {
236
+ const obj = c;
237
+ const path = obj.path || obj.context_path || obj.name;
238
+ if (typeof path === "string" && path)
239
+ return path;
240
+ }
241
+ return null;
242
+ }
243
+ /**
244
+ * Build the suggestion breadcrumb from a result that carries
245
+ * classified_universe / classified_context, e.g.
246
+ * "Monetizze › Checkout › Antifraude" or just "Monetizze".
247
+ */
248
+ function suggestionLabel(result) {
249
+ const uni = universeName(result?.classified_universe);
250
+ const ctx = contextName(result?.classified_context);
251
+ if (uni && ctx)
252
+ return `${uni} › ${ctx}`;
253
+ if (uni)
254
+ return uni;
255
+ if (ctx)
256
+ return ctx;
257
+ return "";
258
+ }
259
+ /**
260
+ * Build the FINAL destination breadcrumb for an approved/manual memory. Falls
261
+ * back to the classified_* suggestion shape, then to a bare scope.
262
+ */
263
+ function destinationLabel(result) {
264
+ const fromSuggestion = suggestionLabel(result);
265
+ if (fromSuggestion)
266
+ return fromSuggestion;
267
+ if (typeof result?.scope === "string" && result.scope)
268
+ return `scope ${result.scope}`;
269
+ return "";
270
+ }
271
+ /**
272
+ * Render the memory_store response. New backend: shows the auto-classification
273
+ * suggestion (proposed) or final destination (manual/approved) enxuto, no
274
+ * intrusive "Dica/Universos disponíveis" block. Old backend (no classification
275
+ * fields): minimal ID/Type/Scope, also WITHOUT any hint.
276
+ */
277
+ function renderStoreResult(result) {
278
+ const id = result?.id;
279
+ const status = result?.classification_status;
280
+ const head = `✓ Salva`;
281
+ // Defensive: older backend without classification fields.
282
+ if (!status) {
283
+ return `Memory stored successfully.\nID: ${id}\nType: ${result?.memory_type}\nScope: ${result?.scope}`;
284
+ }
285
+ if (status === "proposed") {
286
+ const uni = universeName(result?.classified_universe);
287
+ const ctx = contextName(result?.classified_context);
288
+ const conf = confidenceLabel(result?.confidence);
289
+ // High/medium-confidence node identified.
290
+ if (uni && ctx) {
291
+ const confTxt = conf ? `, confiança ${conf}` : "";
292
+ return `${head} · sugestão: ${suggestionLabel(result)} (auto${confTxt}) — aprove com memory_classification_approve`;
293
+ }
294
+ // Universe-only suggestion (no specific node).
295
+ if (uni) {
296
+ return `${head} · sugestão de universo: ${uni} (nó específico não identificado) — aprove com memory_classification_approve`;
297
+ }
298
+ // Low / no confidence: no reliable suggestion.
299
+ return `${head} · sem sugestão confiável — use memory_classification_pending para revisar`;
300
+ }
301
+ // manual / approved: show final destination without "sugestão".
302
+ const dest = destinationLabel(result);
303
+ if (dest)
304
+ return `${head} · classificada em: ${dest}`;
305
+ return `${head}\nID: ${id}\nType: ${result?.memory_type}\nScope: ${result?.scope}`;
306
+ }
199
307
  const handlers = {
200
308
  // ==========================================================================
201
309
  // Core Memory
202
310
  // ==========================================================================
203
- async memory_store(args, { memoryClient, cpClient, resolvedUserId }) {
311
+ async memory_store(args, { memoryClient, resolvedUserId }) {
204
312
  const createdByUser = args.created_by_user_id || resolvedUserId || undefined;
205
313
  const userId = args.user_id || resolvedUserId || undefined;
206
314
  const meta = args.metadata || {};
207
315
  const universeId = args.universe_id || meta.universe_id || undefined;
208
- // Context-first default: when the caller does NOT pass an explicit `scope`,
209
- // memories are associated with a CONTEXT (universe). The backend resolves
210
- // the universe_id from the agent when none is supplied. `organization` and
211
- // `user` are only used when explicitly requested by the caller. When the
212
- // agent has no resolvable universe, we fall back to `agent` scope and help
213
- // future categorization by returning the list of available universes.
214
- const explicitScope = args.scope;
215
- const resolvedScope = explicitScope || "universe";
216
- const baseInput = {
217
- agent_id: args.agent_id,
218
- content: args.content,
219
- memory_type: args.memory_type,
220
- importance: args.importance,
221
- user_id: userId,
222
- created_by_user_id: createdByUser,
223
- metadata: args.metadata,
224
- context_id: args.context_id,
225
- credential_ref: args.credential_ref,
226
- universe_id: universeId,
227
- };
316
+ // Human-in-the-loop classification: the backend classifies the memory by
317
+ // content at write time and returns a SUGGESTION (classification_status
318
+ // "proposed"). Nothing is moved automatically — the user reviews via
319
+ // memory_classification_pending and approves with
320
+ // memory_classification_approve. The MCP layer no longer injects an
321
+ // implicit "universe" scope nor surfaces "available universes"; scope,
322
+ // universe_id and context_id are forwarded ONLY when the caller sets them
323
+ // (explicit overrides → classification_status "manual").
228
324
  try {
229
- const result = await memoryClient.storeMemory({ ...baseInput, scope: resolvedScope });
325
+ const result = await memoryClient.storeMemory({
326
+ agent_id: args.agent_id,
327
+ content: args.content,
328
+ memory_type: args.memory_type,
329
+ importance: args.importance,
330
+ user_id: userId,
331
+ created_by_user_id: createdByUser,
332
+ metadata: args.metadata,
333
+ context_id: args.context_id,
334
+ credential_ref: args.credential_ref,
335
+ universe_id: universeId,
336
+ scope: args.scope,
337
+ });
338
+ return { content: [{ type: "text", text: renderStoreResult(result) }] };
339
+ }
340
+ catch (error) {
341
+ const apiErr = handleApiError(error);
342
+ if (apiErr)
343
+ return apiErr;
344
+ throw error;
345
+ }
346
+ },
347
+ // ==========================================================================
348
+ // Auto-classification (human-in-the-loop)
349
+ // ==========================================================================
350
+ async memory_classification_pending(args, { memoryClient }) {
351
+ let result;
352
+ try {
353
+ result = await memoryClient.getPendingClassifications(args.limit);
354
+ }
355
+ catch (error) {
356
+ const apiErr = handleApiError(error);
357
+ if (apiErr)
358
+ return apiErr;
359
+ throw error;
360
+ }
361
+ const items = Array.isArray(result) ? result : (result?.items || result?.pending || []);
362
+ if (!items || items.length === 0) {
363
+ return { content: [{ type: "text", text: "Nenhuma sugestão de classificação aguardando aprovação." }] };
364
+ }
365
+ let formatted = `Sugestões aguardando aprovação (${items.length}):\n\n`;
366
+ items.forEach((m, i) => {
367
+ const label = suggestionLabel(m);
368
+ const conf = confidenceLabel(m.confidence);
369
+ const preview = typeof m.content === "string"
370
+ ? (m.content.length > 120 ? m.content.slice(0, 120) + "…" : m.content)
371
+ : "";
372
+ formatted += `${i + 1}. (id: ${m.id})`;
373
+ if (conf)
374
+ formatted += ` · confiança ${conf}`;
375
+ formatted += `\n sugestão: ${label}\n`;
376
+ if (preview)
377
+ formatted += ` ${preview}\n`;
378
+ formatted += `\n`;
379
+ });
380
+ formatted += `Aprove com memory_classification_approve(memory_id) — passe universe_id/context_id para corrigir o destino.`;
381
+ return { content: [{ type: "text", text: formatted }] };
382
+ },
383
+ async memory_classification_approve(args, { memoryClient }) {
384
+ const memoryId = args.memory_id;
385
+ const override = {};
386
+ if (args.universe_id)
387
+ override.universe_id = args.universe_id;
388
+ if (args.context_id)
389
+ override.context_id = args.context_id;
390
+ let result;
391
+ try {
392
+ result = await memoryClient.approveClassification(memoryId, override);
393
+ }
394
+ catch (error) {
395
+ const apiErr = handleApiError(error);
396
+ if (apiErr)
397
+ return apiErr;
398
+ throw error;
399
+ }
400
+ const dest = destinationLabel(result) || suggestionLabel(result);
401
+ const overridden = Boolean(override.universe_id || override.context_id);
402
+ let text = `✓ Classificação aprovada`;
403
+ if (dest)
404
+ text += ` · ${dest}`;
405
+ if (overridden)
406
+ text += ` (destino corrigido pelo usuário)`;
407
+ text += `\nID: ${memoryId}`;
408
+ return { content: [{ type: "text", text }] };
409
+ },
410
+ async memory_reclassify(args, { memoryClient }) {
411
+ const memoryId = args.memory_id;
412
+ const rerunAuto = args.rerun_auto;
413
+ const universeId = args.universe_id;
414
+ const contextId = args.context_id;
415
+ try {
416
+ // Correction = approve with an override (or reject to re-run the
417
+ // backend's auto-classifier when no explicit target is given).
418
+ if (universeId || contextId) {
419
+ const result = await memoryClient.approveClassification(memoryId, {
420
+ universe_id: universeId,
421
+ context_id: contextId,
422
+ });
423
+ const dest = destinationLabel(result) || suggestionLabel(result);
424
+ let text = `✓ Reclassificada`;
425
+ if (dest)
426
+ text += ` · ${dest}`;
427
+ text += ` (destino definido pelo usuário)\nID: ${memoryId}`;
428
+ return { content: [{ type: "text", text }] };
429
+ }
430
+ if (rerunAuto) {
431
+ const result = await memoryClient.rejectClassification(memoryId);
432
+ const dest = suggestionLabel(result);
433
+ let text = `✓ Sugestão anterior descartada; reclassificação automática solicitada`;
434
+ if (dest)
435
+ text += ` · nova sugestão: ${dest}`;
436
+ text += `\nID: ${memoryId}\nRevise em memory_classification_pending.`;
437
+ return { content: [{ type: "text", text }] };
438
+ }
230
439
  return {
231
440
  content: [{
232
441
  type: "text",
233
- text: `Memory stored successfully.\nID: ${result.id}\nType: ${result.memory_type}\nScope: ${result.scope}`,
442
+ text: "memory_reclassify requer universe_id/context_id (destino corrigido) ou rerun_auto=true (reexecutar classificação automática).",
234
443
  }],
444
+ isError: true,
235
445
  };
236
446
  }
237
447
  catch (error) {
238
- // Fallback: scope was implicitly "universe" but the agent has no
239
- // resolvable universe (backend returns 400 "must be linked to a
240
- // universe"). Retry as `agent` scope and surface available universes.
241
- const status = error?.response?.status;
242
- const detail = String(error?.response?.data?.detail ?? "");
243
- const isMissingUniverse = status === 400 &&
244
- !explicitScope &&
245
- !universeId &&
246
- detail.includes("must be linked to a universe");
247
- if (isMissingUniverse) {
248
- try {
249
- const result = await memoryClient.storeMemory({ ...baseInput, scope: "agent" });
250
- let text = `Memory stored successfully.\nID: ${result.id}\nType: ${result.memory_type}\nScope: ${result.scope}`;
251
- // Enrich with available universes to help future categorization.
252
- try {
253
- const orgId = getResolvedTenantOrgId();
254
- if (orgId) {
255
- const list = await cpClient.listUniverses(orgId);
256
- const universes = Array.isArray(list) ? list : (list.universes || list.items || []);
257
- if (universes && universes.length > 0) {
258
- text += `\n\nDica: esta memória não foi associada a um universo (salva como memória do agente). Para categorizá-la, informe universe_id na próxima vez. Universos disponíveis:`;
259
- universes.forEach((u) => {
260
- text += `\n- ${u.name} (id: ${u.id})`;
261
- });
262
- }
263
- }
264
- }
265
- catch {
266
- // Listing universes is best-effort; never break a successful store.
267
- }
268
- return { content: [{ type: "text", text }] };
269
- }
270
- catch (retryError) {
271
- const retryApiErr = handleApiError(retryError);
272
- if (retryApiErr)
273
- return retryApiErr;
274
- throw retryError;
275
- }
276
- }
277
448
  const apiErr = handleApiError(error);
278
449
  if (apiErr)
279
450
  return apiErr;
@@ -106,6 +106,7 @@ export interface MemoryClientInterface {
106
106
  metadata?: Record<string, unknown>;
107
107
  context_id?: string;
108
108
  credential_ref?: string;
109
+ universe_id?: string;
109
110
  }): Promise<any>;
110
111
  searchMemories(data: {
111
112
  query_text: string;
@@ -121,6 +122,13 @@ export interface MemoryClientInterface {
121
122
  task_description: string;
122
123
  max_tokens?: number;
123
124
  }): Promise<any>;
125
+ getPendingClassifications(limit?: number): Promise<any>;
126
+ approveClassification(memoryId: string, data?: {
127
+ universe_id?: string;
128
+ context_id?: string;
129
+ }): Promise<any>;
130
+ rejectClassification(memoryId: string): Promise<any>;
131
+ approveClassificationBatch(ids: string[]): Promise<any>;
124
132
  reinforceMemory(memoryId: string, boost?: number): Promise<any>;
125
133
  weakenMemory(memoryId: string, penalty?: number): Promise<any>;
126
134
  getWorkingMemory(contextId: string): Promise<any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frinus-mcp",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Frinus - MCP Server for Cognitive Memory (φρήν + νοῦς). Long-term memory, sessions, agents and knowledge for Claude and any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",