@jungjaehoon/mama-server 1.11.0 β 1.12.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.
- package/package.json +1 -1
- package/src/server.js +203 -262
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -192,272 +192,194 @@ class MAMAServer {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
setupHandlers() {
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
type
|
|
222
|
-
|
|
223
|
-
type:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
items: {
|
|
255
|
-
type: 'object',
|
|
256
|
-
properties: {
|
|
257
|
-
kind: {
|
|
258
|
-
type: 'string',
|
|
259
|
-
enum: ['global', 'user', 'channel', 'project'],
|
|
260
|
-
},
|
|
261
|
-
id: { type: 'string' },
|
|
262
|
-
},
|
|
263
|
-
required: ['kind', 'id'],
|
|
195
|
+
// Tool definitions come from src/tools/ (single source of truth).
|
|
196
|
+
// Legacy unified tools (save, search, update) kept as wrappers for backward compat.
|
|
197
|
+
const legacyNotice = this.legacyHttpEmbeddingMode
|
|
198
|
+
? `${this.getLegacyMigrationNotice()}\n\n`
|
|
199
|
+
: '';
|
|
200
|
+
|
|
201
|
+
const tools = [
|
|
202
|
+
// 1. SAVE β decisions, checkpoints, conversation ingestion
|
|
203
|
+
{
|
|
204
|
+
name: 'save',
|
|
205
|
+
description: `${legacyNotice}Save to MAMA memory. Use type parameter to choose what to save.
|
|
206
|
+
|
|
207
|
+
**type='decision'** β Save architectural decisions, lessons learned, insights.
|
|
208
|
+
Required: topic, decision, reasoning. Optional: confidence, scopes, event_date.
|
|
209
|
+
Triggers: user says "κΈ°μ΅ν΄", "remember", "decided". Reuse same topic to create evolution chain.
|
|
210
|
+
|
|
211
|
+
**type='checkpoint'** β Save session state for resumption.
|
|
212
|
+
Required: summary (4-section: Goal, Evidence, Unfinished, Next Briefing).
|
|
213
|
+
Optional: next_steps, open_files. Triggers: session ending, "체ν¬ν¬μΈνΈ", "save progress".
|
|
214
|
+
|
|
215
|
+
**type='ingest'** β Import conversation messages into memory with optional extraction.
|
|
216
|
+
Required: messages (array of {role, content}). Optional: scopes, session_date, extract.
|
|
217
|
+
|
|
218
|
+
**Scopes**: Isolate memories per project/channel. Example: [{"kind":"project","id":"/my/app"}]
|
|
219
|
+
**event_date**: ISO 8601 date when event occurred (e.g. "2024-01-15"), not when saved.`,
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {
|
|
223
|
+
type: {
|
|
224
|
+
type: 'string',
|
|
225
|
+
enum: ['decision', 'checkpoint', 'ingest'],
|
|
226
|
+
description: "What to save: 'decision', 'checkpoint', or 'ingest'",
|
|
227
|
+
},
|
|
228
|
+
// Decision fields
|
|
229
|
+
topic: {
|
|
230
|
+
type: 'string',
|
|
231
|
+
description: '[Decision] Topic identifier. Reuse same topic = supersedes previous.',
|
|
232
|
+
},
|
|
233
|
+
decision: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: '[Decision] The decision made.',
|
|
236
|
+
},
|
|
237
|
+
reasoning: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
description: "[Decision] Why. End with 'builds_on: <id>' or 'debates: <id>' to link.",
|
|
240
|
+
},
|
|
241
|
+
confidence: {
|
|
242
|
+
type: 'number',
|
|
243
|
+
description: '[Decision] 0.0-1.0. Default: 0.5',
|
|
244
|
+
minimum: 0,
|
|
245
|
+
maximum: 1,
|
|
246
|
+
},
|
|
247
|
+
scopes: {
|
|
248
|
+
type: 'array',
|
|
249
|
+
items: {
|
|
250
|
+
type: 'object',
|
|
251
|
+
properties: {
|
|
252
|
+
kind: { type: 'string', enum: ['global', 'user', 'channel', 'project'] },
|
|
253
|
+
id: { type: 'string' },
|
|
264
254
|
},
|
|
265
|
-
|
|
266
|
-
'[Decision] Memory scopes for isolation. Example: [{"kind": "project", "id": "/path/to/project"}]',
|
|
267
|
-
},
|
|
268
|
-
event_date: {
|
|
269
|
-
type: 'string',
|
|
270
|
-
description:
|
|
271
|
-
'[Decision] ISO 8601 date when the event occurred (e.g., "2024-01-15"). Defaults to now.',
|
|
272
|
-
},
|
|
273
|
-
// Checkpoint fields
|
|
274
|
-
summary: {
|
|
275
|
-
type: 'string',
|
|
276
|
-
description:
|
|
277
|
-
"[Checkpoint] Session state summary. Use 4-section format: (1) π― Goal & Progress - what was the goal, where did you stop; (2) β
Evidence - mark each item as Verified/Not run/Assumed with proof; (3) β³ Unfinished & Risks - incomplete work, blockers, unknowns; (4) π¦ Next Agent Briefing - Definition of Done, quick health checks to run first. β οΈ Include 'Related decisions: decision_xxx, decision_yyy' to link context.",
|
|
278
|
-
},
|
|
279
|
-
next_steps: {
|
|
280
|
-
type: 'string',
|
|
281
|
-
description:
|
|
282
|
-
'[Checkpoint] Instructions for next session: DoD (Definition of Done), quick verification commands (npm test, curl health), constraints/cautions.',
|
|
283
|
-
},
|
|
284
|
-
open_files: {
|
|
285
|
-
type: 'array',
|
|
286
|
-
items: { type: 'string' },
|
|
287
|
-
description: '[Checkpoint] Currently relevant files.',
|
|
255
|
+
required: ['kind', 'id'],
|
|
288
256
|
},
|
|
257
|
+
description: 'Memory scopes for isolation.',
|
|
289
258
|
},
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
- What is known vs unknown (explicitly mark unknowns)
|
|
317
|
-
- What to do next (use contract fields, avoid guessing)`,
|
|
318
|
-
inputSchema: {
|
|
319
|
-
type: 'object',
|
|
320
|
-
properties: {
|
|
321
|
-
query: {
|
|
322
|
-
type: 'string',
|
|
323
|
-
description:
|
|
324
|
-
'Search query (optional). Semantic search finds related decisions even with different wording. If empty, returns recent items sorted by time.',
|
|
325
|
-
},
|
|
326
|
-
type: {
|
|
327
|
-
type: 'string',
|
|
328
|
-
enum: ['all', 'decision', 'checkpoint'],
|
|
329
|
-
description:
|
|
330
|
-
"Filter by type: 'decision' for architectural choices, 'checkpoint' for session states, 'all' for both. Default: 'all'",
|
|
331
|
-
},
|
|
332
|
-
limit: {
|
|
333
|
-
type: 'number',
|
|
334
|
-
description: 'Maximum results. Default: 10',
|
|
335
|
-
},
|
|
336
|
-
scopes: {
|
|
337
|
-
type: 'array',
|
|
338
|
-
items: {
|
|
339
|
-
type: 'object',
|
|
340
|
-
properties: {
|
|
341
|
-
kind: {
|
|
342
|
-
type: 'string',
|
|
343
|
-
enum: ['global', 'user', 'channel', 'project'],
|
|
344
|
-
},
|
|
345
|
-
id: { type: 'string' },
|
|
346
|
-
},
|
|
347
|
-
required: ['kind', 'id'],
|
|
259
|
+
event_date: {
|
|
260
|
+
type: 'string',
|
|
261
|
+
description: 'ISO 8601 date when event occurred (e.g. "2024-01-15").',
|
|
262
|
+
},
|
|
263
|
+
// Checkpoint fields
|
|
264
|
+
summary: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
description: '[Checkpoint] Session state summary.',
|
|
267
|
+
},
|
|
268
|
+
next_steps: {
|
|
269
|
+
type: 'string',
|
|
270
|
+
description: '[Checkpoint] Instructions for next session.',
|
|
271
|
+
},
|
|
272
|
+
open_files: {
|
|
273
|
+
type: 'array',
|
|
274
|
+
items: { type: 'string' },
|
|
275
|
+
description: '[Checkpoint] Currently relevant files.',
|
|
276
|
+
},
|
|
277
|
+
// Ingest fields
|
|
278
|
+
messages: {
|
|
279
|
+
type: 'array',
|
|
280
|
+
items: {
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: {
|
|
283
|
+
role: { type: 'string', enum: ['user', 'assistant', 'system'] },
|
|
284
|
+
content: { type: 'string' },
|
|
348
285
|
},
|
|
349
|
-
|
|
286
|
+
required: ['role', 'content'],
|
|
350
287
|
},
|
|
288
|
+
description: '[Ingest] Conversation messages to import.',
|
|
351
289
|
},
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
β‘ TRIGGERS - Call this when:
|
|
360
|
-
β’ Days/weeks later: issues discovered β mark 'failed' + reason
|
|
361
|
-
β’ Production success confirmed β mark 'success'
|
|
362
|
-
β’ Partial results with caveats β mark 'partial'
|
|
363
|
-
β’ User says: "μ΄κ±° μλμ΄", "this didn't work", "μ±κ³΅νμ΄"
|
|
364
|
-
|
|
365
|
-
π REASONING GRAPH IMPACT:
|
|
366
|
-
β’ 'failed' outcomes teach future LLMs what to avoid
|
|
367
|
-
β’ After failure β save NEW decision with same topic to supersede
|
|
368
|
-
|
|
369
|
-
π‘ TIP: Don't just update - if approach changed, save a NEW decision with same topic. This creates evolution history.`,
|
|
370
|
-
inputSchema: {
|
|
371
|
-
type: 'object',
|
|
372
|
-
properties: {
|
|
373
|
-
id: {
|
|
374
|
-
type: 'string',
|
|
375
|
-
description: 'Decision ID to update.',
|
|
376
|
-
},
|
|
377
|
-
outcome: {
|
|
378
|
-
type: 'string',
|
|
379
|
-
description:
|
|
380
|
-
"New outcome status (case-insensitive): 'success' or 'SUCCESS', 'failed' or 'FAILED', 'partial' or 'PARTIAL'.",
|
|
381
|
-
},
|
|
382
|
-
reason: {
|
|
383
|
-
type: 'string',
|
|
384
|
-
description:
|
|
385
|
-
'Why it succeeded/failed/was partial. Include specific evidence: error logs, metrics, user feedback, or what broke.',
|
|
386
|
-
},
|
|
290
|
+
session_date: {
|
|
291
|
+
type: 'string',
|
|
292
|
+
description: '[Ingest] ISO 8601 date when conversation occurred.',
|
|
293
|
+
},
|
|
294
|
+
extract: {
|
|
295
|
+
type: 'boolean',
|
|
296
|
+
description: '[Ingest] Extract structured memories via LLM. Default: false',
|
|
387
297
|
},
|
|
388
|
-
required: ['id', 'outcome'],
|
|
389
298
|
},
|
|
299
|
+
required: ['type'],
|
|
390
300
|
},
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
301
|
+
},
|
|
302
|
+
// 2. SEARCH β unified search across decisions, checkpoints, load latest checkpoint
|
|
303
|
+
{
|
|
304
|
+
name: 'search',
|
|
305
|
+
description: `Search MAMA memory. Returns results ranked by semantic similarity.
|
|
306
|
+
|
|
307
|
+
**With query** β Semantic search across decisions and checkpoints. Cross-lingual (Korean + English).
|
|
308
|
+
Triggers: "λμλλΌ", "what did we decide", making architectural choices, debugging.
|
|
309
|
+
|
|
310
|
+
**Without query** β List recent items sorted by time.
|
|
311
|
+
|
|
312
|
+
**Resume session**: type='checkpoint' without query β loads latest checkpoint with full context (narrative, links, next steps).
|
|
313
|
+
Triggers: "μ΄μ΄μ", "continue", "where were we", session start.
|
|
314
|
+
|
|
315
|
+
**type parameter**: 'decision' (choices/lessons only), 'checkpoint' (session states / resume), 'all' (both, default).
|
|
316
|
+
**scopes**: Filter by project/channel. Omit for global search.
|
|
317
|
+
**limit**: Max results (default: 10).
|
|
318
|
+
|
|
319
|
+
β οΈ REQUIRED: Call search BEFORE save to find related decisions and avoid orphans.`,
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
properties: {
|
|
323
|
+
query: {
|
|
324
|
+
type: 'string',
|
|
325
|
+
description: 'Search query. Omit to list recent items.',
|
|
326
|
+
},
|
|
327
|
+
type: {
|
|
328
|
+
type: 'string',
|
|
329
|
+
enum: ['all', 'decision', 'checkpoint'],
|
|
330
|
+
description: "Filter by type. Default: 'all'",
|
|
331
|
+
},
|
|
332
|
+
limit: { type: 'number', description: 'Max results. Default: 10' },
|
|
333
|
+
scopes: {
|
|
334
|
+
type: 'array',
|
|
335
|
+
items: {
|
|
336
|
+
type: 'object',
|
|
337
|
+
properties: {
|
|
338
|
+
kind: { type: 'string', enum: ['global', 'user', 'channel', 'project'] },
|
|
339
|
+
id: { type: 'string' },
|
|
340
|
+
},
|
|
341
|
+
required: ['kind', 'id'],
|
|
422
342
|
},
|
|
343
|
+
description: 'Filter by scope.',
|
|
423
344
|
},
|
|
424
345
|
},
|
|
425
346
|
},
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
347
|
+
},
|
|
348
|
+
// 3. UPDATE β decision outcome tracking
|
|
349
|
+
{
|
|
350
|
+
name: 'update',
|
|
351
|
+
description: `Update decision outcome after real-world validation.
|
|
352
|
+
|
|
353
|
+
Triggers: "μ΄κ±° μλμ΄", "this worked", days later when issues discovered.
|
|
354
|
+
outcome: 'success', 'failed', 'partial' (case-insensitive).
|
|
355
|
+
After failure β save a NEW decision with same topic to create evolution history.`,
|
|
356
|
+
inputSchema: {
|
|
357
|
+
type: 'object',
|
|
358
|
+
properties: {
|
|
359
|
+
id: { type: 'string', description: 'Decision ID to update.' },
|
|
360
|
+
outcome: {
|
|
361
|
+
type: 'string',
|
|
362
|
+
description: "'success', 'failed', or 'partial' (case-insensitive).",
|
|
363
|
+
},
|
|
364
|
+
reason: {
|
|
365
|
+
type: 'string',
|
|
366
|
+
description: 'Why it succeeded/failed/was partial. Include evidence.',
|
|
367
|
+
},
|
|
447
368
|
},
|
|
369
|
+
required: ['id', 'outcome'],
|
|
448
370
|
},
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}));
|
|
459
|
-
|
|
460
|
-
// Handle tool execution
|
|
371
|
+
},
|
|
372
|
+
// 4. SEARCH_DECISIONS_AND_CONTRACTS β PreToolUse hook RPC
|
|
373
|
+
{
|
|
374
|
+
name: 'search_decisions_and_contracts',
|
|
375
|
+
description: 'Search decisions and contracts for PreToolUse hook injection.',
|
|
376
|
+
inputSchema: memoryTools.search_decisions_and_contracts.inputSchema,
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
381
|
+
|
|
382
|
+
// Handle tool execution β legacy wrappers + v2 tools from src/tools/
|
|
461
383
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
462
384
|
const { name, arguments: args } = request.params;
|
|
463
385
|
const toolStart = Date.now();
|
|
@@ -467,6 +389,7 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
|
|
|
467
389
|
let result;
|
|
468
390
|
|
|
469
391
|
switch (name) {
|
|
392
|
+
// Legacy unified wrappers (backward compat)
|
|
470
393
|
case 'save':
|
|
471
394
|
result = await this.handleSave(args);
|
|
472
395
|
break;
|
|
@@ -476,14 +399,8 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
|
|
|
476
399
|
case 'update':
|
|
477
400
|
result = await this.handleUpdate(args);
|
|
478
401
|
break;
|
|
479
|
-
case 'search_decisions_and_contracts':
|
|
480
|
-
result = await this.handleSearchDecisionsAndContracts(args);
|
|
481
|
-
break;
|
|
482
|
-
case 'load_checkpoint':
|
|
483
|
-
result = await memoryTools.load_checkpoint.handler(args);
|
|
484
|
-
break;
|
|
485
402
|
default:
|
|
486
|
-
//
|
|
403
|
+
// All other tools β src/tools/ handlers (single source of truth)
|
|
487
404
|
if (memoryTools[name] && typeof memoryTools[name].handler === 'function') {
|
|
488
405
|
result = await memoryTools[name].handler(args);
|
|
489
406
|
} else {
|
|
@@ -575,7 +492,11 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
|
|
|
575
492
|
};
|
|
576
493
|
}
|
|
577
494
|
|
|
578
|
-
|
|
495
|
+
if (type === 'ingest') {
|
|
496
|
+
return await memoryTools.ingest_conversation.handler(args);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return { success: false, message: "β type must be 'decision', 'checkpoint', or 'ingest'" };
|
|
579
500
|
}
|
|
580
501
|
|
|
581
502
|
/**
|
|
@@ -584,6 +505,11 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
|
|
|
584
505
|
async handleSearch(args) {
|
|
585
506
|
const { query, type = 'all', limit = 10, scopes } = args;
|
|
586
507
|
|
|
508
|
+
// type='checkpoint' without query β load latest checkpoint (resume session)
|
|
509
|
+
if (type === 'checkpoint' && !query) {
|
|
510
|
+
return await memoryTools.load_checkpoint.handler(args);
|
|
511
|
+
}
|
|
512
|
+
|
|
587
513
|
const results = [];
|
|
588
514
|
|
|
589
515
|
// Search decisions
|
|
@@ -598,7 +524,6 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
|
|
|
598
524
|
} else {
|
|
599
525
|
decisions = await mama.list({ limit, ...(scopes && { scopes }) });
|
|
600
526
|
}
|
|
601
|
-
// Ensure decisions is an array
|
|
602
527
|
if (Array.isArray(decisions)) {
|
|
603
528
|
results.push(
|
|
604
529
|
...decisions.map((d) => ({
|
|
@@ -609,8 +534,24 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
|
|
|
609
534
|
}
|
|
610
535
|
}
|
|
611
536
|
|
|
612
|
-
// Search checkpoints
|
|
613
|
-
if (type === 'all' || type === 'checkpoint') {
|
|
537
|
+
// Search checkpoints (with query = search, without = handled above as load)
|
|
538
|
+
if ((type === 'all' || type === 'checkpoint') && query) {
|
|
539
|
+
const checkpoints = await mama.listCheckpoints(limit);
|
|
540
|
+
results.push(
|
|
541
|
+
...checkpoints
|
|
542
|
+
.filter((c) => c.summary && c.summary.toLowerCase().includes(query.toLowerCase()))
|
|
543
|
+
.map((c) => ({
|
|
544
|
+
id: `checkpoint_${c.id}`,
|
|
545
|
+
summary: c.summary,
|
|
546
|
+
next_steps: c.next_steps,
|
|
547
|
+
created_at: c.timestamp,
|
|
548
|
+
_type: 'checkpoint',
|
|
549
|
+
}))
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// type='all' without query β include recent checkpoints
|
|
554
|
+
if (type === 'all' && !query) {
|
|
614
555
|
const checkpoints = await mama.listCheckpoints(limit);
|
|
615
556
|
results.push(
|
|
616
557
|
...checkpoints.map((c) => ({
|