synomem 0.1.1 → 0.3.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/ARCHITECTURE.md +2 -2
- package/CHANGELOG.md +37 -0
- package/README.md +26 -13
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +379 -51
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +131 -17
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +489 -76
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +8 -8
- package/dist/import.d.ts +356 -13
- package/dist/import.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +213 -31
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp-server.js +54 -8
- package/dist/mcp-server.js.map +1 -1
- package/dist/ports/repository.d.ts +21 -1
- package/dist/ports/repository.d.ts.map +1 -1
- package/dist/projections.d.ts +26 -1
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +208 -44
- package/dist/projections.js.map +1 -1
- package/dist/remote.d.ts +82 -9
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +53 -2
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +476 -21
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +204 -26
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +82 -10
- package/dist/service.d.ts.map +1 -1
- package/dist/skill-install.d.ts +8 -2
- package/dist/skill-install.d.ts.map +1 -1
- package/dist/skill-install.js +6 -11
- package/dist/skill-install.js.map +1 -1
- package/dist/storage.d.ts +51 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +366 -36
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +310 -38
- package/dist/types.d.ts.map +1 -1
- package/docs/cli.md +53 -20
- package/docs/examples.md +4 -4
- package/docs/mcp.md +12 -4
- package/docs/skill.md +10 -7
- package/docs/storage-format.md +4 -4
- package/openapi/synomem-v1.yaml +24 -24
- package/package.json +1 -1
- package/skills/synomem/SKILL.md +24 -8
- package/skills/synomem/agents/openai.yaml +1 -1
- package/skills/synomem/references/examples.md +1 -1
- package/src/cli.ts +610 -95
- package/src/client.ts +587 -79
- package/src/config.ts +8 -8
- package/src/index.ts +11 -1
- package/src/mcp/index.ts +265 -30
- package/src/mcp-server.ts +58 -8
- package/src/ports/repository.ts +21 -0
- package/src/projections.ts +209 -44
- package/src/remote.ts +166 -8
- package/src/schemas.ts +211 -26
- package/src/service.ts +85 -7
- package/src/skill-install.ts +14 -17
- package/src/storage.ts +472 -34
- package/src/types.ts +332 -39
package/src/config.ts
CHANGED
|
@@ -10,14 +10,14 @@ export const defaultConfig: SynomemConfig = {
|
|
|
10
10
|
workspaceId: 'workspace',
|
|
11
11
|
defaultVisibility: 'workspace',
|
|
12
12
|
allowSelfAwards: false,
|
|
13
|
-
|
|
13
|
+
allowCrossAgentTasks: true,
|
|
14
14
|
allowAgentCreationViaMcp: false,
|
|
15
15
|
allowRebuildViaMcp: false,
|
|
16
16
|
includePrivateInStats: false,
|
|
17
17
|
projection: {
|
|
18
18
|
writeWinsMarkdown: true,
|
|
19
19
|
writeMemoryMarkdown: true,
|
|
20
|
-
|
|
20
|
+
writeTasksMarkdown: true,
|
|
21
21
|
writeInboxEntries: true,
|
|
22
22
|
},
|
|
23
23
|
};
|
|
@@ -53,14 +53,14 @@ const policySchema = z.object({
|
|
|
53
53
|
workspaceId: z.string().trim().min(1).max(100),
|
|
54
54
|
defaultVisibility: z.enum(['private', 'workspace', 'public']),
|
|
55
55
|
allowSelfAwards: z.boolean(),
|
|
56
|
-
|
|
56
|
+
allowCrossAgentTasks: z.boolean(),
|
|
57
57
|
allowAgentCreationViaMcp: z.boolean(),
|
|
58
58
|
allowRebuildViaMcp: z.boolean(),
|
|
59
59
|
includePrivateInStats: z.boolean(),
|
|
60
60
|
projection: z.object({
|
|
61
61
|
writeWinsMarkdown: z.boolean(),
|
|
62
62
|
writeMemoryMarkdown: z.boolean(),
|
|
63
|
-
|
|
63
|
+
writeTasksMarkdown: z.boolean(),
|
|
64
64
|
writeInboxEntries: z.boolean(),
|
|
65
65
|
}),
|
|
66
66
|
});
|
|
@@ -144,24 +144,24 @@ function environmentConfig(env: NodeJS.ProcessEnv): SynomemConfigOverrides {
|
|
|
144
144
|
|
|
145
145
|
const writeWinsMarkdown = optionalBoolean(env, 'SYNOMEM_WRITE_WINS_MARKDOWN');
|
|
146
146
|
const writeMemoryMarkdown = optionalBoolean(env, 'SYNOMEM_WRITE_MEMORY_MARKDOWN');
|
|
147
|
-
const
|
|
147
|
+
const writeTasksMarkdown = optionalBoolean(env, 'SYNOMEM_WRITE_TASKS_MARKDOWN');
|
|
148
148
|
const writeInboxEntries = optionalBoolean(env, 'SYNOMEM_WRITE_INBOX_ENTRIES');
|
|
149
149
|
const allowSelfAwards = optionalBoolean(env, 'SYNOMEM_ALLOW_SELF_AWARDS');
|
|
150
|
-
const
|
|
150
|
+
const allowCrossAgentTasks = optionalBoolean(env, 'SYNOMEM_ALLOW_CROSS_AGENT_TODOS');
|
|
151
151
|
const allowAgentCreationViaMcp = optionalBoolean(env, 'SYNOMEM_ALLOW_AGENT_CREATION_VIA_MCP');
|
|
152
152
|
const allowRebuildViaMcp = optionalBoolean(env, 'SYNOMEM_ALLOW_REBUILD_VIA_MCP');
|
|
153
153
|
const includePrivateInStats = optionalBoolean(env, 'SYNOMEM_INCLUDE_PRIVATE_IN_STATS');
|
|
154
154
|
const projection = {
|
|
155
155
|
...(writeWinsMarkdown !== undefined ? { writeWinsMarkdown } : {}),
|
|
156
156
|
...(writeMemoryMarkdown !== undefined ? { writeMemoryMarkdown } : {}),
|
|
157
|
-
...(
|
|
157
|
+
...(writeTasksMarkdown !== undefined ? { writeTasksMarkdown } : {}),
|
|
158
158
|
...(writeInboxEntries !== undefined ? { writeInboxEntries } : {}),
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
return {
|
|
162
162
|
...(visibility ? { defaultVisibility: visibility as SynomemConfig['defaultVisibility'] } : {}),
|
|
163
163
|
...(allowSelfAwards !== undefined ? { allowSelfAwards } : {}),
|
|
164
|
-
...(
|
|
164
|
+
...(allowCrossAgentTasks !== undefined ? { allowCrossAgentTasks } : {}),
|
|
165
165
|
...(allowAgentCreationViaMcp !== undefined ? { allowAgentCreationViaMcp } : {}),
|
|
166
166
|
...(allowRebuildViaMcp !== undefined ? { allowRebuildViaMcp } : {}),
|
|
167
167
|
...(includePrivateInStats !== undefined ? { includePrivateInStats } : {}),
|
package/src/index.ts
CHANGED
|
@@ -36,13 +36,22 @@ export type {
|
|
|
36
36
|
} from './service.js';
|
|
37
37
|
export { defaultConfig, resolveHome } from './config.js';
|
|
38
38
|
export { asSynomemError, errorCodes, SynomemError } from './errors.js';
|
|
39
|
-
export {
|
|
39
|
+
export {
|
|
40
|
+
dueInstant,
|
|
41
|
+
escapeMarkdown,
|
|
42
|
+
recordsFromEvents,
|
|
43
|
+
renderMarkdownExport,
|
|
44
|
+
} from './projections.js';
|
|
40
45
|
export {
|
|
41
46
|
actorSchema,
|
|
47
|
+
agentAliasSchema,
|
|
42
48
|
agentIdSchema,
|
|
49
|
+
agentLookupSchema,
|
|
50
|
+
bindRuntimeSchema,
|
|
43
51
|
changesInputSchema,
|
|
44
52
|
createAgentSchema,
|
|
45
53
|
createNoteSchema,
|
|
54
|
+
createTaskSchema,
|
|
46
55
|
createTodoSchema,
|
|
47
56
|
eventSchema,
|
|
48
57
|
evidenceSchema,
|
|
@@ -55,6 +64,7 @@ export {
|
|
|
55
64
|
profileSchema,
|
|
56
65
|
reviseNoteSchema,
|
|
57
66
|
sendMemoSchema,
|
|
67
|
+
updateTaskSchema,
|
|
58
68
|
updateTodoSchema,
|
|
59
69
|
updateAgentSchema,
|
|
60
70
|
} from './schemas.js';
|
package/src/mcp/index.ts
CHANGED
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
agentIdSchema,
|
|
10
10
|
changesInputSchema,
|
|
11
11
|
createNoteSchema,
|
|
12
|
+
createTaskSchema,
|
|
12
13
|
createTodoSchema,
|
|
13
14
|
giveKudosMcpSchema,
|
|
14
15
|
itemListInputSchema,
|
|
15
16
|
listInputSchema,
|
|
16
17
|
reviseNoteSchema,
|
|
17
18
|
sendMemoSchema,
|
|
19
|
+
updateTaskSchema,
|
|
18
20
|
updateTodoSchema,
|
|
19
21
|
} from '../schemas.js';
|
|
20
22
|
import { packageVersion } from '../version.js';
|
|
@@ -93,7 +95,7 @@ export async function createSynomemMcpServer(
|
|
|
93
95
|
{ name: 'synomem', version: packageVersion() },
|
|
94
96
|
{
|
|
95
97
|
instructions:
|
|
96
|
-
'Use Synomem for durable kudos, memos, notes, and
|
|
98
|
+
'Use Synomem for durable kudos, memos, notes, and tasks. Store only necessary, factual content; never secrets or raw sensitive tool output. The server binds every write to its configured actor.',
|
|
97
99
|
},
|
|
98
100
|
);
|
|
99
101
|
|
|
@@ -324,6 +326,129 @@ export async function createSynomemMcpServer(
|
|
|
324
326
|
},
|
|
325
327
|
);
|
|
326
328
|
|
|
329
|
+
server.registerTool(
|
|
330
|
+
'synomem_post_create',
|
|
331
|
+
{
|
|
332
|
+
title: 'Publish a post',
|
|
333
|
+
description:
|
|
334
|
+
'Publish something the whole workspace can read. Use for an announcement, a decision, or context several agents need. A post has no recipient — if one named actor must act, send a memo or assign a task instead.',
|
|
335
|
+
inputSchema: z.object({
|
|
336
|
+
title: z.string().trim().min(1).max(200),
|
|
337
|
+
body: z.string().trim().min(1).max(32_000),
|
|
338
|
+
tags: z.array(z.string()).max(20).optional(),
|
|
339
|
+
replyTo: z.string().length(26).optional(),
|
|
340
|
+
idempotencyKey: z.string().max(200).optional(),
|
|
341
|
+
}),
|
|
342
|
+
outputSchema,
|
|
343
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
344
|
+
},
|
|
345
|
+
async (input) => {
|
|
346
|
+
try {
|
|
347
|
+
const result = await client.posts.create(input);
|
|
348
|
+
return success(actor, `Published post ${result.record.event.id}.`, {
|
|
349
|
+
post: result.record,
|
|
350
|
+
});
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return failure(actor, error);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
server.registerTool(
|
|
358
|
+
'synomem_post_acknowledge',
|
|
359
|
+
{
|
|
360
|
+
title: 'Acknowledge a post',
|
|
361
|
+
description:
|
|
362
|
+
'Record that YOU have seen a post. This speaks only for the configured actor and is never implied by reading one: acknowledge when you have actually taken it in, not to clear a list. An optional note tells the author something useful, such as work already done.',
|
|
363
|
+
inputSchema: z.object({
|
|
364
|
+
postId: z.string().length(26),
|
|
365
|
+
note: z.string().trim().min(1).max(2000).optional(),
|
|
366
|
+
idempotencyKey: z.string().max(200).optional(),
|
|
367
|
+
}),
|
|
368
|
+
outputSchema,
|
|
369
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
370
|
+
},
|
|
371
|
+
async (input) => {
|
|
372
|
+
try {
|
|
373
|
+
const record = await client.posts.acknowledge(input);
|
|
374
|
+
return success(actor, `Acknowledged post ${input.postId}.`, { post: record });
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return failure(actor, error);
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
server.registerTool(
|
|
382
|
+
'synomem_post_roster',
|
|
383
|
+
{
|
|
384
|
+
title: 'See who has acknowledged a post',
|
|
385
|
+
description:
|
|
386
|
+
'Who has acknowledged a post and who has not. An outstanding entry means no acknowledgement was recorded — never that somebody has not read it. Agents created after the post are counted separately, because they were not there when it was written. This is read-only and does not acknowledge anything.',
|
|
387
|
+
inputSchema: z.object({ postId: z.string().length(26) }),
|
|
388
|
+
outputSchema,
|
|
389
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
390
|
+
},
|
|
391
|
+
async ({ postId }) => {
|
|
392
|
+
try {
|
|
393
|
+
const roster = await client.posts.roster(postId);
|
|
394
|
+
return success(
|
|
395
|
+
actor,
|
|
396
|
+
`${roster.acknowledged.length} acknowledged, ${roster.outstanding.length} with no acknowledgement recorded.`,
|
|
397
|
+
roster,
|
|
398
|
+
);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
return failure(actor, error);
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
server.registerTool(
|
|
406
|
+
'synomem_agent_resolve',
|
|
407
|
+
{
|
|
408
|
+
title: 'Resolve an agent name',
|
|
409
|
+
description:
|
|
410
|
+
'Resolve a name or alias to exactly one agent. Matching ignores case. When several agents answer to the name, no match is returned and the candidates are listed instead — ask which one is meant rather than choosing.',
|
|
411
|
+
inputSchema: z.object({
|
|
412
|
+
query: z.string().min(1).describe('An agent ID or alias, in any casing.'),
|
|
413
|
+
}),
|
|
414
|
+
outputSchema,
|
|
415
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
416
|
+
},
|
|
417
|
+
async ({ query }) => {
|
|
418
|
+
try {
|
|
419
|
+
const resolution = await client.agents.resolve(query);
|
|
420
|
+
const message = resolution.match
|
|
421
|
+
? `"${resolution.query}" resolves to ${resolution.match.id}.`
|
|
422
|
+
: resolution.candidates.length
|
|
423
|
+
? `"${resolution.query}" is ambiguous across ${resolution.candidates.length} agents. Ask which one is meant.`
|
|
424
|
+
: `No agent answers to "${resolution.query}".`;
|
|
425
|
+
return success(actor, message, resolution);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return failure(actor, error);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
server.registerTool(
|
|
433
|
+
'synomem_agent_directory',
|
|
434
|
+
{
|
|
435
|
+
title: 'Browse the agent directory',
|
|
436
|
+
description:
|
|
437
|
+
'List known agents with their aliases and runtime bindings. Runtime bindings describe where an agent was registered to run and when Synomem last observed it act; they never mean the agent is reachable now. This is read-only.',
|
|
438
|
+
inputSchema: z.object({}),
|
|
439
|
+
outputSchema,
|
|
440
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
441
|
+
},
|
|
442
|
+
async () => {
|
|
443
|
+
try {
|
|
444
|
+
const entries = await client.agents.directory();
|
|
445
|
+
return success(actor, `Found ${entries.length} agent identity or identities.`, { entries });
|
|
446
|
+
} catch (error) {
|
|
447
|
+
return failure(actor, error);
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
);
|
|
451
|
+
|
|
327
452
|
server.registerTool(
|
|
328
453
|
'synomem_rebuild',
|
|
329
454
|
{
|
|
@@ -377,7 +502,7 @@ export async function createSynomemMcpServer(
|
|
|
377
502
|
{
|
|
378
503
|
title: 'List Synomem items',
|
|
379
504
|
description:
|
|
380
|
-
'Discover a bounded page of compact kudos, memo, note, and
|
|
505
|
+
'Discover a bounded page of compact kudos, memo, note, and task summaries. Full bodies, reasons, evidence, descriptions, source, and metadata are omitted; use synomem_get for one selected item.',
|
|
381
506
|
inputSchema: itemListInputSchema,
|
|
382
507
|
outputSchema,
|
|
383
508
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
@@ -401,7 +526,7 @@ export async function createSynomemMcpServer(
|
|
|
401
526
|
{
|
|
402
527
|
title: 'Get one Synomem item',
|
|
403
528
|
description:
|
|
404
|
-
'Read the full authorized record for one explicitly selected kudos, memo, note, or
|
|
529
|
+
'Read the full authorized record for one explicitly selected kudos, memo, note, or task ID.',
|
|
405
530
|
inputSchema: z.object({ itemId: z.string().length(26) }),
|
|
406
531
|
outputSchema,
|
|
407
532
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
@@ -445,7 +570,7 @@ export async function createSynomemMcpServer(
|
|
|
445
570
|
{
|
|
446
571
|
title: 'Review an agent inbox',
|
|
447
572
|
description:
|
|
448
|
-
'Return compact pending kudos, unread memos, and open
|
|
573
|
+
'Return compact pending kudos, unread memos, and open tasks for the configured agent. An agent may inspect only its own private items.',
|
|
449
574
|
inputSchema: z.object({
|
|
450
575
|
limit: z.number().int().min(1).max(50).default(10),
|
|
451
576
|
cursor: z.string().max(500).optional(),
|
|
@@ -585,12 +710,58 @@ export async function createSynomemMcpServer(
|
|
|
585
710
|
},
|
|
586
711
|
);
|
|
587
712
|
|
|
713
|
+
server.registerTool(
|
|
714
|
+
'synomem_task_create',
|
|
715
|
+
{
|
|
716
|
+
title: 'Create a task',
|
|
717
|
+
description:
|
|
718
|
+
'Create a concrete actionable task assigned to an agent, optionally with a date-only or timezone-aware deadline. Do not use as a substitute for a memo when no action is required.',
|
|
719
|
+
inputSchema: createTaskSchema,
|
|
720
|
+
outputSchema,
|
|
721
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
722
|
+
},
|
|
723
|
+
async (input) => {
|
|
724
|
+
try {
|
|
725
|
+
const result = await client.tasks.create(input);
|
|
726
|
+
return success(
|
|
727
|
+
actor,
|
|
728
|
+
`${result.deduplicated ? 'Returned existing' : 'Created'} task “${result.record.current.title}” for ${result.record.event.assigneeDisplayName} (ID ${result.record.event.id}).`,
|
|
729
|
+
result,
|
|
730
|
+
);
|
|
731
|
+
} catch (error) {
|
|
732
|
+
return failure(actor, error);
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
);
|
|
736
|
+
server.registerTool(
|
|
737
|
+
'synomem_task_update',
|
|
738
|
+
{
|
|
739
|
+
title: 'Update a task',
|
|
740
|
+
description:
|
|
741
|
+
'Append an update to an open task using the version last read. Stale versions fail rather than overwriting concurrent work.',
|
|
742
|
+
inputSchema: updateTaskSchema,
|
|
743
|
+
outputSchema,
|
|
744
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
745
|
+
},
|
|
746
|
+
async (input) => {
|
|
747
|
+
try {
|
|
748
|
+
const record = await client.tasks.update(input);
|
|
749
|
+
return success(
|
|
750
|
+
actor,
|
|
751
|
+
`Updated task ${record.event.id} to version ${record.current.version}.`,
|
|
752
|
+
{ record },
|
|
753
|
+
);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
return failure(actor, error);
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
);
|
|
588
759
|
server.registerTool(
|
|
589
760
|
'synomem_todo_create',
|
|
590
761
|
{
|
|
591
|
-
title: 'Create a todo',
|
|
762
|
+
title: 'Create a private todo',
|
|
592
763
|
description:
|
|
593
|
-
'Create a
|
|
764
|
+
'Create a private reminder for yourself. A todo has no assignee and nobody else can read it — use synomem_task_create when the work belongs to another agent.',
|
|
594
765
|
inputSchema: createTodoSchema,
|
|
595
766
|
outputSchema,
|
|
596
767
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
@@ -600,7 +771,7 @@ export async function createSynomemMcpServer(
|
|
|
600
771
|
const result = await client.todos.create(input);
|
|
601
772
|
return success(
|
|
602
773
|
actor,
|
|
603
|
-
`${result.deduplicated ? 'Returned existing' : 'Created'} todo “${result.record.current.title}”
|
|
774
|
+
`${result.deduplicated ? 'Returned existing' : 'Created'} private todo “${result.record.current.title}” (ID ${result.record.event.id}).`,
|
|
604
775
|
result,
|
|
605
776
|
);
|
|
606
777
|
} catch (error) {
|
|
@@ -611,9 +782,9 @@ export async function createSynomemMcpServer(
|
|
|
611
782
|
server.registerTool(
|
|
612
783
|
'synomem_todo_update',
|
|
613
784
|
{
|
|
614
|
-
title: 'Update a todo',
|
|
785
|
+
title: 'Update a private todo',
|
|
615
786
|
description:
|
|
616
|
-
'Append an update to
|
|
787
|
+
'Append an update to one of your own todos using the version last read. Stale versions fail rather than overwriting concurrent work.',
|
|
617
788
|
inputSchema: updateTodoSchema,
|
|
618
789
|
outputSchema,
|
|
619
790
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
@@ -624,15 +795,17 @@ export async function createSynomemMcpServer(
|
|
|
624
795
|
return success(
|
|
625
796
|
actor,
|
|
626
797
|
`Updated todo ${record.event.id} to version ${record.current.version}.`,
|
|
627
|
-
{
|
|
798
|
+
{
|
|
799
|
+
record,
|
|
800
|
+
},
|
|
628
801
|
);
|
|
629
802
|
} catch (error) {
|
|
630
803
|
return failure(actor, error);
|
|
631
804
|
}
|
|
632
805
|
},
|
|
633
806
|
);
|
|
634
|
-
for (const operation of ['
|
|
635
|
-
const
|
|
807
|
+
for (const operation of ['complete', 'reopen', 'cancel', 'archive'] as const) {
|
|
808
|
+
const todoInputSchema = z.object({
|
|
636
809
|
todoId: z.string().length(26),
|
|
637
810
|
note: z.string().trim().min(1).max(2000).optional(),
|
|
638
811
|
reason: z.string().trim().min(1).max(2000).optional(),
|
|
@@ -641,9 +814,9 @@ export async function createSynomemMcpServer(
|
|
|
641
814
|
server.registerTool(
|
|
642
815
|
`synomem_todo_${operation}`,
|
|
643
816
|
{
|
|
644
|
-
title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a todo`,
|
|
645
|
-
description: `${operation[0]!.toUpperCase()}${operation.slice(1)}
|
|
646
|
-
inputSchema,
|
|
817
|
+
title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a private todo`,
|
|
818
|
+
description: `${operation[0]!.toUpperCase()}${operation.slice(1)} one of your own todos by appending a lifecycle event; history is never deleted.`,
|
|
819
|
+
inputSchema: todoInputSchema,
|
|
647
820
|
outputSchema,
|
|
648
821
|
annotations: {
|
|
649
822
|
readOnlyHint: false,
|
|
@@ -654,34 +827,96 @@ export async function createSynomemMcpServer(
|
|
|
654
827
|
async (input) => {
|
|
655
828
|
try {
|
|
656
829
|
const record =
|
|
657
|
-
operation === '
|
|
658
|
-
? await client.todos.
|
|
830
|
+
operation === 'complete'
|
|
831
|
+
? await client.todos.complete({
|
|
659
832
|
todoId: input.todoId,
|
|
833
|
+
...(input.note ? { note: input.note } : {}),
|
|
660
834
|
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
661
835
|
})
|
|
662
|
-
: operation === '
|
|
663
|
-
? await client.todos.
|
|
836
|
+
: operation === 'cancel'
|
|
837
|
+
? await client.todos.cancel({
|
|
664
838
|
todoId: input.todoId,
|
|
665
839
|
...(input.reason ? { reason: input.reason } : {}),
|
|
666
840
|
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
667
841
|
})
|
|
668
|
-
: operation === '
|
|
669
|
-
? await client.todos.
|
|
842
|
+
: operation === 'archive'
|
|
843
|
+
? await client.todos.archive({
|
|
844
|
+
todoId: input.todoId,
|
|
845
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
846
|
+
})
|
|
847
|
+
: await client.todos.reopen({
|
|
670
848
|
todoId: input.todoId,
|
|
849
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
850
|
+
});
|
|
851
|
+
return success(actor, `Todo ${record.event.id} is now ${record.status}.`, { record });
|
|
852
|
+
} catch (error) {
|
|
853
|
+
return failure(actor, error);
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
|
|
860
|
+
const inputSchema = z.object({
|
|
861
|
+
taskId: z.string().length(26),
|
|
862
|
+
note: z.string().trim().min(1).max(2000).optional(),
|
|
863
|
+
reason: z.string().trim().min(1).max(2000).optional(),
|
|
864
|
+
// Required on reject, optional on accept. Declaring it required in the
|
|
865
|
+
// tool schema for reject means a model is told before it calls, rather
|
|
866
|
+
// than discovering it from an error.
|
|
867
|
+
...(operation === 'reject'
|
|
868
|
+
? { response: z.string().trim().min(1).max(2000) }
|
|
869
|
+
: operation === 'accept'
|
|
870
|
+
? { response: z.string().trim().min(1).max(2000).optional() }
|
|
871
|
+
: {}),
|
|
872
|
+
idempotencyKey: z.string().max(200).optional(),
|
|
873
|
+
});
|
|
874
|
+
server.registerTool(
|
|
875
|
+
`synomem_task_${operation}`,
|
|
876
|
+
{
|
|
877
|
+
title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a task`,
|
|
878
|
+
description: `${operation[0]!.toUpperCase()}${operation.slice(1)} an authorized task by appending a lifecycle event; history is never deleted.`,
|
|
879
|
+
inputSchema,
|
|
880
|
+
outputSchema,
|
|
881
|
+
annotations: {
|
|
882
|
+
readOnlyHint: false,
|
|
883
|
+
destructiveHint: operation === 'cancel',
|
|
884
|
+
idempotentHint: true,
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
async (input) => {
|
|
888
|
+
try {
|
|
889
|
+
const record =
|
|
890
|
+
operation === 'accept'
|
|
891
|
+
? await client.tasks.accept({
|
|
892
|
+
taskId: input.taskId,
|
|
893
|
+
...('response' in input && input.response ? { response: input.response } : {}),
|
|
894
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
895
|
+
})
|
|
896
|
+
: operation === 'reject'
|
|
897
|
+
? await client.tasks.reject({
|
|
898
|
+
taskId: input.taskId,
|
|
899
|
+
response:
|
|
900
|
+
('response' in input ? input.response : undefined) ?? input.reason ?? '',
|
|
901
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
902
|
+
})
|
|
903
|
+
: operation === 'complete'
|
|
904
|
+
? await client.tasks.complete({
|
|
905
|
+
taskId: input.taskId,
|
|
671
906
|
...(input.note ? { note: input.note } : {}),
|
|
672
907
|
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
673
908
|
})
|
|
674
909
|
: operation === 'cancel'
|
|
675
|
-
? await client.
|
|
676
|
-
|
|
910
|
+
? await client.tasks.cancel({
|
|
911
|
+
taskId: input.taskId,
|
|
677
912
|
...(input.reason ? { reason: input.reason } : {}),
|
|
678
913
|
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
679
914
|
})
|
|
680
|
-
: await client.
|
|
681
|
-
|
|
915
|
+
: await client.tasks.reopen({
|
|
916
|
+
taskId: input.taskId,
|
|
682
917
|
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
683
918
|
});
|
|
684
|
-
return success(actor, `
|
|
919
|
+
return success(actor, `Task ${record.event.id} is ${record.status}.`, { record });
|
|
685
920
|
} catch (error) {
|
|
686
921
|
return failure(actor, error);
|
|
687
922
|
}
|
|
@@ -758,7 +993,7 @@ export async function createSynomemMcpServer(
|
|
|
758
993
|
new ResourceTemplate('synomem://agents/{agentId}/inbox', { list: undefined }),
|
|
759
994
|
{
|
|
760
995
|
title: 'Agent inbox',
|
|
761
|
-
description: 'Ten recent visible pending kudos, memos, and
|
|
996
|
+
description: 'Ten recent visible pending kudos, memos, and tasks for one agent',
|
|
762
997
|
mimeType: 'application/json',
|
|
763
998
|
},
|
|
764
999
|
async (uri, { agentId }) => {
|
|
@@ -932,9 +1167,9 @@ export async function createSynomemMcpServer(
|
|
|
932
1167
|
);
|
|
933
1168
|
|
|
934
1169
|
server.registerPrompt(
|
|
935
|
-
'
|
|
1170
|
+
'synomem_create_actionable_task',
|
|
936
1171
|
{
|
|
937
|
-
title: 'Create an actionable
|
|
1172
|
+
title: 'Create an actionable task',
|
|
938
1173
|
description:
|
|
939
1174
|
'Turn requested work into a concrete assigned action without inventing deadlines.',
|
|
940
1175
|
argsSchema: { assignee: agentIdSchema, action: z.string() },
|
|
@@ -945,7 +1180,7 @@ export async function createSynomemMcpServer(
|
|
|
945
1180
|
role: 'user',
|
|
946
1181
|
content: {
|
|
947
1182
|
type: 'text',
|
|
948
|
-
text: `Prepare a concrete
|
|
1183
|
+
text: `Prepare a concrete task assigned to ${assignee}: ${action}. Preserve a date-only deadline as a date, require timezone data for a timed deadline, and do not invent missing timing.`,
|
|
949
1184
|
},
|
|
950
1185
|
},
|
|
951
1186
|
],
|
package/src/mcp-server.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
+
import { createConfiguredService } from './backend.js';
|
|
4
|
+
import { SynomemError } from './errors.js';
|
|
3
5
|
import { actorSchema } from './schemas.js';
|
|
4
6
|
import { startMcpServer } from './mcp/index.js';
|
|
7
|
+
import type { ActorIdentity } from './types.js';
|
|
5
8
|
import { packageVersion } from './version.js';
|
|
6
9
|
|
|
7
10
|
const version = packageVersion();
|
|
@@ -9,6 +12,7 @@ const version = packageVersion();
|
|
|
9
12
|
const { values } = parseArgs({
|
|
10
13
|
options: {
|
|
11
14
|
home: { type: 'string' },
|
|
15
|
+
'agent-id': { type: 'string' },
|
|
12
16
|
'actor-id': { type: 'string' },
|
|
13
17
|
'actor-kind': { type: 'string' },
|
|
14
18
|
'actor-name': { type: 'string' },
|
|
@@ -17,6 +21,43 @@ const { values } = parseArgs({
|
|
|
17
21
|
},
|
|
18
22
|
});
|
|
19
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Loads an agent's identity from canonical state rather than from arguments.
|
|
26
|
+
*
|
|
27
|
+
* A display name passed on the command line is a claim the runtime makes about
|
|
28
|
+
* itself, and it is written into every event the session appends. Reading the
|
|
29
|
+
* profile instead means a misconfigured runtime cannot sign another agent's
|
|
30
|
+
* name to work, and renaming an agent takes effect without re-registering it
|
|
31
|
+
* with every harness.
|
|
32
|
+
*/
|
|
33
|
+
async function resolveAgentActor(agentId: string, home?: string): Promise<ActorIdentity> {
|
|
34
|
+
const lookup = createConfiguredService({
|
|
35
|
+
actor: { kind: 'system', id: 'synomem-mcp' },
|
|
36
|
+
...(home ? { home } : {}),
|
|
37
|
+
});
|
|
38
|
+
await lookup.init();
|
|
39
|
+
try {
|
|
40
|
+
const resolution = await lookup.agents.resolve(agentId);
|
|
41
|
+
if (!resolution.match) {
|
|
42
|
+
throw new SynomemError(
|
|
43
|
+
'AGENT_NOT_FOUND',
|
|
44
|
+
resolution.candidates.length
|
|
45
|
+
? `"${agentId}" matches ${resolution.candidates.length} agents: ${resolution.candidates
|
|
46
|
+
.map((candidate) => candidate.id)
|
|
47
|
+
.join(', ')}. Register the MCP server with a canonical agent ID.`
|
|
48
|
+
: `Unknown agent: ${agentId}. Create it with \`synomem agent create\` first.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
kind: 'agent',
|
|
53
|
+
id: resolution.match.id,
|
|
54
|
+
...(resolution.match.displayName ? { displayName: resolution.match.displayName } : {}),
|
|
55
|
+
};
|
|
56
|
+
} finally {
|
|
57
|
+
await lookup.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
20
61
|
if (values.help) {
|
|
21
62
|
process.stdout.write(`synomem-mcp ${version}
|
|
22
63
|
|
|
@@ -24,20 +65,29 @@ Actor-bound Synomem MCP server (stdio transport)
|
|
|
24
65
|
|
|
25
66
|
Options:
|
|
26
67
|
--home <path> Storage root
|
|
27
|
-
--
|
|
28
|
-
|
|
29
|
-
--actor-
|
|
68
|
+
--agent-id <id> Bound agent, whose identity is read from Synomem
|
|
69
|
+
(or SYNOMEM_AGENT_ID)
|
|
70
|
+
--actor-id <id> Bound non-agent actor ID (or SYNOMEM_ACTOR_ID)
|
|
71
|
+
--actor-kind <kind> human or system (or SYNOMEM_ACTOR_KIND)
|
|
72
|
+
--actor-name <name> Display name for a non-agent actor
|
|
73
|
+
(or SYNOMEM_ACTOR_NAME)
|
|
30
74
|
-h, --help Show help
|
|
31
75
|
-v, --version Show version
|
|
76
|
+
|
|
77
|
+
Prefer --agent-id for an agent runtime: the display name and kind then come
|
|
78
|
+
from the agent's profile instead of from whatever the harness was told to pass.
|
|
32
79
|
`);
|
|
33
80
|
} else if (values.version) {
|
|
34
81
|
process.stdout.write(`${version}\n`);
|
|
35
82
|
} else {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
83
|
+
const agentId = values['agent-id'] ?? process.env.SYNOMEM_AGENT_ID;
|
|
84
|
+
const actor = agentId
|
|
85
|
+
? await resolveAgentActor(agentId, values.home)
|
|
86
|
+
: actorSchema.parse({
|
|
87
|
+
id: values['actor-id'] ?? process.env.SYNOMEM_ACTOR_ID,
|
|
88
|
+
kind: values['actor-kind'] ?? process.env.SYNOMEM_ACTOR_KIND,
|
|
89
|
+
displayName: values['actor-name'] ?? process.env.SYNOMEM_ACTOR_NAME,
|
|
90
|
+
});
|
|
41
91
|
|
|
42
92
|
await startMcpServer({
|
|
43
93
|
actor,
|
package/src/ports/repository.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ActorIdentity,
|
|
3
3
|
AgentProfile,
|
|
4
|
+
AgentRuntimeBinding,
|
|
4
5
|
ChangePage,
|
|
5
6
|
ItemListInput,
|
|
7
|
+
JsonValue,
|
|
8
|
+
PostAcknowledgment,
|
|
9
|
+
PostRoster,
|
|
6
10
|
ItemSummary,
|
|
7
11
|
KudosListInput,
|
|
8
12
|
KudosSummary,
|
|
@@ -41,6 +45,23 @@ export interface SynomemRepository {
|
|
|
41
45
|
updateAgent(profile: AgentProfile, updatedAt: string): Awaitable<void>;
|
|
42
46
|
getAgent(idOrAlias: string): Awaitable<AgentProfile | undefined>;
|
|
43
47
|
listAgents(): Awaitable<AgentProfile[]>;
|
|
48
|
+
/** Resolves a name case-insensitively, reporting ambiguity instead of guessing. */
|
|
49
|
+
resolveAgent(query: string): Awaitable<{ match?: AgentProfile; candidates: AgentProfile[] }>;
|
|
50
|
+
listPostAcknowledgments(postId: string): Awaitable<PostAcknowledgment[]>;
|
|
51
|
+
/** Who has acknowledged a post and who has not; see PostRoster. */
|
|
52
|
+
postRoster(postId: string): Awaitable<PostRoster | undefined>;
|
|
53
|
+
listRuntimeBindings(agentId: string): Awaitable<AgentRuntimeBinding[]>;
|
|
54
|
+
bindRuntime(binding: {
|
|
55
|
+
id: string;
|
|
56
|
+
agentId: string;
|
|
57
|
+
installationId?: string;
|
|
58
|
+
runtime: string;
|
|
59
|
+
profile?: string;
|
|
60
|
+
capabilities?: Record<string, JsonValue>;
|
|
61
|
+
boundAt: string;
|
|
62
|
+
}): Awaitable<void>;
|
|
63
|
+
unbindRuntime(bindingId: string): Awaitable<boolean>;
|
|
64
|
+
touchRuntimeBinding(agentId: string, runtime: string, at: string): Awaitable<void>;
|
|
44
65
|
|
|
45
66
|
listKudosSummaries(
|
|
46
67
|
input: Required<Pick<KudosListInput, 'limit' | 'offset'>> & KudosListInput,
|