synomem 0.1.0 → 0.2.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 -1
- package/README.md +28 -15
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +311 -70
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +96 -17
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +333 -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 +207 -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 +156 -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 +18 -1
- package/dist/ports/repository.d.ts.map +1 -1
- package/dist/projections.d.ts +18 -1
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +137 -44
- package/dist/projections.js.map +1 -1
- package/dist/remote.d.ts +57 -9
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +43 -2
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +302 -21
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +142 -26
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +57 -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 +41 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +254 -36
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +213 -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 +572 -173
- package/src/client.ts +391 -79
- package/src/config.ts +8 -8
- package/src/index.ts +11 -1
- package/src/mcp/index.ts +189 -30
- package/src/mcp-server.ts +58 -8
- package/src/ports/repository.ts +16 -0
- package/src/projections.ts +139 -44
- package/src/remote.ts +118 -8
- package/src/schemas.ts +147 -26
- package/src/service.ts +59 -7
- package/src/skill-install.ts +14 -17
- package/src/storage.ts +326 -34
- package/src/types.ts +228 -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,53 @@ export async function createSynomemMcpServer(
|
|
|
324
326
|
},
|
|
325
327
|
);
|
|
326
328
|
|
|
329
|
+
server.registerTool(
|
|
330
|
+
'synomem_agent_resolve',
|
|
331
|
+
{
|
|
332
|
+
title: 'Resolve an agent name',
|
|
333
|
+
description:
|
|
334
|
+
'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.',
|
|
335
|
+
inputSchema: z.object({
|
|
336
|
+
query: z.string().min(1).describe('An agent ID or alias, in any casing.'),
|
|
337
|
+
}),
|
|
338
|
+
outputSchema,
|
|
339
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
340
|
+
},
|
|
341
|
+
async ({ query }) => {
|
|
342
|
+
try {
|
|
343
|
+
const resolution = await client.agents.resolve(query);
|
|
344
|
+
const message = resolution.match
|
|
345
|
+
? `"${resolution.query}" resolves to ${resolution.match.id}.`
|
|
346
|
+
: resolution.candidates.length
|
|
347
|
+
? `"${resolution.query}" is ambiguous across ${resolution.candidates.length} agents. Ask which one is meant.`
|
|
348
|
+
: `No agent answers to "${resolution.query}".`;
|
|
349
|
+
return success(actor, message, resolution);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
return failure(actor, error);
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
server.registerTool(
|
|
357
|
+
'synomem_agent_directory',
|
|
358
|
+
{
|
|
359
|
+
title: 'Browse the agent directory',
|
|
360
|
+
description:
|
|
361
|
+
'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.',
|
|
362
|
+
inputSchema: z.object({}),
|
|
363
|
+
outputSchema,
|
|
364
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
365
|
+
},
|
|
366
|
+
async () => {
|
|
367
|
+
try {
|
|
368
|
+
const entries = await client.agents.directory();
|
|
369
|
+
return success(actor, `Found ${entries.length} agent identity or identities.`, { entries });
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return failure(actor, error);
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
|
|
327
376
|
server.registerTool(
|
|
328
377
|
'synomem_rebuild',
|
|
329
378
|
{
|
|
@@ -377,7 +426,7 @@ export async function createSynomemMcpServer(
|
|
|
377
426
|
{
|
|
378
427
|
title: 'List Synomem items',
|
|
379
428
|
description:
|
|
380
|
-
'Discover a bounded page of compact kudos, memo, note, and
|
|
429
|
+
'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
430
|
inputSchema: itemListInputSchema,
|
|
382
431
|
outputSchema,
|
|
383
432
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
@@ -401,7 +450,7 @@ export async function createSynomemMcpServer(
|
|
|
401
450
|
{
|
|
402
451
|
title: 'Get one Synomem item',
|
|
403
452
|
description:
|
|
404
|
-
'Read the full authorized record for one explicitly selected kudos, memo, note, or
|
|
453
|
+
'Read the full authorized record for one explicitly selected kudos, memo, note, or task ID.',
|
|
405
454
|
inputSchema: z.object({ itemId: z.string().length(26) }),
|
|
406
455
|
outputSchema,
|
|
407
456
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
@@ -445,7 +494,7 @@ export async function createSynomemMcpServer(
|
|
|
445
494
|
{
|
|
446
495
|
title: 'Review an agent inbox',
|
|
447
496
|
description:
|
|
448
|
-
'Return compact pending kudos, unread memos, and open
|
|
497
|
+
'Return compact pending kudos, unread memos, and open tasks for the configured agent. An agent may inspect only its own private items.',
|
|
449
498
|
inputSchema: z.object({
|
|
450
499
|
limit: z.number().int().min(1).max(50).default(10),
|
|
451
500
|
cursor: z.string().max(500).optional(),
|
|
@@ -585,12 +634,58 @@ export async function createSynomemMcpServer(
|
|
|
585
634
|
},
|
|
586
635
|
);
|
|
587
636
|
|
|
637
|
+
server.registerTool(
|
|
638
|
+
'synomem_task_create',
|
|
639
|
+
{
|
|
640
|
+
title: 'Create a task',
|
|
641
|
+
description:
|
|
642
|
+
'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.',
|
|
643
|
+
inputSchema: createTaskSchema,
|
|
644
|
+
outputSchema,
|
|
645
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
646
|
+
},
|
|
647
|
+
async (input) => {
|
|
648
|
+
try {
|
|
649
|
+
const result = await client.tasks.create(input);
|
|
650
|
+
return success(
|
|
651
|
+
actor,
|
|
652
|
+
`${result.deduplicated ? 'Returned existing' : 'Created'} task “${result.record.current.title}” for ${result.record.event.assigneeDisplayName} (ID ${result.record.event.id}).`,
|
|
653
|
+
result,
|
|
654
|
+
);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
return failure(actor, error);
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
);
|
|
660
|
+
server.registerTool(
|
|
661
|
+
'synomem_task_update',
|
|
662
|
+
{
|
|
663
|
+
title: 'Update a task',
|
|
664
|
+
description:
|
|
665
|
+
'Append an update to an open task using the version last read. Stale versions fail rather than overwriting concurrent work.',
|
|
666
|
+
inputSchema: updateTaskSchema,
|
|
667
|
+
outputSchema,
|
|
668
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
669
|
+
},
|
|
670
|
+
async (input) => {
|
|
671
|
+
try {
|
|
672
|
+
const record = await client.tasks.update(input);
|
|
673
|
+
return success(
|
|
674
|
+
actor,
|
|
675
|
+
`Updated task ${record.event.id} to version ${record.current.version}.`,
|
|
676
|
+
{ record },
|
|
677
|
+
);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
return failure(actor, error);
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
);
|
|
588
683
|
server.registerTool(
|
|
589
684
|
'synomem_todo_create',
|
|
590
685
|
{
|
|
591
|
-
title: 'Create a todo',
|
|
686
|
+
title: 'Create a private todo',
|
|
592
687
|
description:
|
|
593
|
-
'Create a
|
|
688
|
+
'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
689
|
inputSchema: createTodoSchema,
|
|
595
690
|
outputSchema,
|
|
596
691
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
@@ -600,7 +695,7 @@ export async function createSynomemMcpServer(
|
|
|
600
695
|
const result = await client.todos.create(input);
|
|
601
696
|
return success(
|
|
602
697
|
actor,
|
|
603
|
-
`${result.deduplicated ? 'Returned existing' : 'Created'} todo “${result.record.current.title}”
|
|
698
|
+
`${result.deduplicated ? 'Returned existing' : 'Created'} private todo “${result.record.current.title}” (ID ${result.record.event.id}).`,
|
|
604
699
|
result,
|
|
605
700
|
);
|
|
606
701
|
} catch (error) {
|
|
@@ -611,9 +706,9 @@ export async function createSynomemMcpServer(
|
|
|
611
706
|
server.registerTool(
|
|
612
707
|
'synomem_todo_update',
|
|
613
708
|
{
|
|
614
|
-
title: 'Update a todo',
|
|
709
|
+
title: 'Update a private todo',
|
|
615
710
|
description:
|
|
616
|
-
'Append an update to
|
|
711
|
+
'Append an update to one of your own todos using the version last read. Stale versions fail rather than overwriting concurrent work.',
|
|
617
712
|
inputSchema: updateTodoSchema,
|
|
618
713
|
outputSchema,
|
|
619
714
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
@@ -624,15 +719,17 @@ export async function createSynomemMcpServer(
|
|
|
624
719
|
return success(
|
|
625
720
|
actor,
|
|
626
721
|
`Updated todo ${record.event.id} to version ${record.current.version}.`,
|
|
627
|
-
{
|
|
722
|
+
{
|
|
723
|
+
record,
|
|
724
|
+
},
|
|
628
725
|
);
|
|
629
726
|
} catch (error) {
|
|
630
727
|
return failure(actor, error);
|
|
631
728
|
}
|
|
632
729
|
},
|
|
633
730
|
);
|
|
634
|
-
for (const operation of ['
|
|
635
|
-
const
|
|
731
|
+
for (const operation of ['complete', 'reopen', 'cancel', 'archive'] as const) {
|
|
732
|
+
const todoInputSchema = z.object({
|
|
636
733
|
todoId: z.string().length(26),
|
|
637
734
|
note: z.string().trim().min(1).max(2000).optional(),
|
|
638
735
|
reason: z.string().trim().min(1).max(2000).optional(),
|
|
@@ -641,9 +738,9 @@ export async function createSynomemMcpServer(
|
|
|
641
738
|
server.registerTool(
|
|
642
739
|
`synomem_todo_${operation}`,
|
|
643
740
|
{
|
|
644
|
-
title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a todo`,
|
|
645
|
-
description: `${operation[0]!.toUpperCase()}${operation.slice(1)}
|
|
646
|
-
inputSchema,
|
|
741
|
+
title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a private todo`,
|
|
742
|
+
description: `${operation[0]!.toUpperCase()}${operation.slice(1)} one of your own todos by appending a lifecycle event; history is never deleted.`,
|
|
743
|
+
inputSchema: todoInputSchema,
|
|
647
744
|
outputSchema,
|
|
648
745
|
annotations: {
|
|
649
746
|
readOnlyHint: false,
|
|
@@ -654,34 +751,96 @@ export async function createSynomemMcpServer(
|
|
|
654
751
|
async (input) => {
|
|
655
752
|
try {
|
|
656
753
|
const record =
|
|
657
|
-
operation === '
|
|
658
|
-
? await client.todos.
|
|
754
|
+
operation === 'complete'
|
|
755
|
+
? await client.todos.complete({
|
|
659
756
|
todoId: input.todoId,
|
|
757
|
+
...(input.note ? { note: input.note } : {}),
|
|
660
758
|
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
661
759
|
})
|
|
662
|
-
: operation === '
|
|
663
|
-
? await client.todos.
|
|
760
|
+
: operation === 'cancel'
|
|
761
|
+
? await client.todos.cancel({
|
|
664
762
|
todoId: input.todoId,
|
|
665
763
|
...(input.reason ? { reason: input.reason } : {}),
|
|
666
764
|
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
667
765
|
})
|
|
668
|
-
: operation === '
|
|
669
|
-
? await client.todos.
|
|
766
|
+
: operation === 'archive'
|
|
767
|
+
? await client.todos.archive({
|
|
768
|
+
todoId: input.todoId,
|
|
769
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
770
|
+
})
|
|
771
|
+
: await client.todos.reopen({
|
|
670
772
|
todoId: input.todoId,
|
|
773
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
774
|
+
});
|
|
775
|
+
return success(actor, `Todo ${record.event.id} is now ${record.status}.`, { record });
|
|
776
|
+
} catch (error) {
|
|
777
|
+
return failure(actor, error);
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
|
|
784
|
+
const inputSchema = z.object({
|
|
785
|
+
taskId: z.string().length(26),
|
|
786
|
+
note: z.string().trim().min(1).max(2000).optional(),
|
|
787
|
+
reason: z.string().trim().min(1).max(2000).optional(),
|
|
788
|
+
// Required on reject, optional on accept. Declaring it required in the
|
|
789
|
+
// tool schema for reject means a model is told before it calls, rather
|
|
790
|
+
// than discovering it from an error.
|
|
791
|
+
...(operation === 'reject'
|
|
792
|
+
? { response: z.string().trim().min(1).max(2000) }
|
|
793
|
+
: operation === 'accept'
|
|
794
|
+
? { response: z.string().trim().min(1).max(2000).optional() }
|
|
795
|
+
: {}),
|
|
796
|
+
idempotencyKey: z.string().max(200).optional(),
|
|
797
|
+
});
|
|
798
|
+
server.registerTool(
|
|
799
|
+
`synomem_task_${operation}`,
|
|
800
|
+
{
|
|
801
|
+
title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a task`,
|
|
802
|
+
description: `${operation[0]!.toUpperCase()}${operation.slice(1)} an authorized task by appending a lifecycle event; history is never deleted.`,
|
|
803
|
+
inputSchema,
|
|
804
|
+
outputSchema,
|
|
805
|
+
annotations: {
|
|
806
|
+
readOnlyHint: false,
|
|
807
|
+
destructiveHint: operation === 'cancel',
|
|
808
|
+
idempotentHint: true,
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
async (input) => {
|
|
812
|
+
try {
|
|
813
|
+
const record =
|
|
814
|
+
operation === 'accept'
|
|
815
|
+
? await client.tasks.accept({
|
|
816
|
+
taskId: input.taskId,
|
|
817
|
+
...('response' in input && input.response ? { response: input.response } : {}),
|
|
818
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
819
|
+
})
|
|
820
|
+
: operation === 'reject'
|
|
821
|
+
? await client.tasks.reject({
|
|
822
|
+
taskId: input.taskId,
|
|
823
|
+
response:
|
|
824
|
+
('response' in input ? input.response : undefined) ?? input.reason ?? '',
|
|
825
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
826
|
+
})
|
|
827
|
+
: operation === 'complete'
|
|
828
|
+
? await client.tasks.complete({
|
|
829
|
+
taskId: input.taskId,
|
|
671
830
|
...(input.note ? { note: input.note } : {}),
|
|
672
831
|
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
673
832
|
})
|
|
674
833
|
: operation === 'cancel'
|
|
675
|
-
? await client.
|
|
676
|
-
|
|
834
|
+
? await client.tasks.cancel({
|
|
835
|
+
taskId: input.taskId,
|
|
677
836
|
...(input.reason ? { reason: input.reason } : {}),
|
|
678
837
|
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
679
838
|
})
|
|
680
|
-
: await client.
|
|
681
|
-
|
|
839
|
+
: await client.tasks.reopen({
|
|
840
|
+
taskId: input.taskId,
|
|
682
841
|
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
683
842
|
});
|
|
684
|
-
return success(actor, `
|
|
843
|
+
return success(actor, `Task ${record.event.id} is ${record.status}.`, { record });
|
|
685
844
|
} catch (error) {
|
|
686
845
|
return failure(actor, error);
|
|
687
846
|
}
|
|
@@ -758,7 +917,7 @@ export async function createSynomemMcpServer(
|
|
|
758
917
|
new ResourceTemplate('synomem://agents/{agentId}/inbox', { list: undefined }),
|
|
759
918
|
{
|
|
760
919
|
title: 'Agent inbox',
|
|
761
|
-
description: 'Ten recent visible pending kudos, memos, and
|
|
920
|
+
description: 'Ten recent visible pending kudos, memos, and tasks for one agent',
|
|
762
921
|
mimeType: 'application/json',
|
|
763
922
|
},
|
|
764
923
|
async (uri, { agentId }) => {
|
|
@@ -932,9 +1091,9 @@ export async function createSynomemMcpServer(
|
|
|
932
1091
|
);
|
|
933
1092
|
|
|
934
1093
|
server.registerPrompt(
|
|
935
|
-
'
|
|
1094
|
+
'synomem_create_actionable_task',
|
|
936
1095
|
{
|
|
937
|
-
title: 'Create an actionable
|
|
1096
|
+
title: 'Create an actionable task',
|
|
938
1097
|
description:
|
|
939
1098
|
'Turn requested work into a concrete assigned action without inventing deadlines.',
|
|
940
1099
|
argsSchema: { assignee: agentIdSchema, action: z.string() },
|
|
@@ -945,7 +1104,7 @@ export async function createSynomemMcpServer(
|
|
|
945
1104
|
role: 'user',
|
|
946
1105
|
content: {
|
|
947
1106
|
type: 'text',
|
|
948
|
-
text: `Prepare a concrete
|
|
1107
|
+
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
1108
|
},
|
|
950
1109
|
},
|
|
951
1110
|
],
|
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,10 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ActorIdentity,
|
|
3
3
|
AgentProfile,
|
|
4
|
+
AgentRuntimeBinding,
|
|
4
5
|
ChangePage,
|
|
5
6
|
ItemListInput,
|
|
7
|
+
JsonValue,
|
|
6
8
|
ItemSummary,
|
|
7
9
|
KudosListInput,
|
|
8
10
|
KudosSummary,
|
|
@@ -41,6 +43,20 @@ export interface SynomemRepository {
|
|
|
41
43
|
updateAgent(profile: AgentProfile, updatedAt: string): Awaitable<void>;
|
|
42
44
|
getAgent(idOrAlias: string): Awaitable<AgentProfile | undefined>;
|
|
43
45
|
listAgents(): Awaitable<AgentProfile[]>;
|
|
46
|
+
/** Resolves a name case-insensitively, reporting ambiguity instead of guessing. */
|
|
47
|
+
resolveAgent(query: string): Awaitable<{ match?: AgentProfile; candidates: AgentProfile[] }>;
|
|
48
|
+
listRuntimeBindings(agentId: string): Awaitable<AgentRuntimeBinding[]>;
|
|
49
|
+
bindRuntime(binding: {
|
|
50
|
+
id: string;
|
|
51
|
+
agentId: string;
|
|
52
|
+
installationId?: string;
|
|
53
|
+
runtime: string;
|
|
54
|
+
profile?: string;
|
|
55
|
+
capabilities?: Record<string, JsonValue>;
|
|
56
|
+
boundAt: string;
|
|
57
|
+
}): Awaitable<void>;
|
|
58
|
+
unbindRuntime(bindingId: string): Awaitable<boolean>;
|
|
59
|
+
touchRuntimeBinding(agentId: string, runtime: string, at: string): Awaitable<void>;
|
|
44
60
|
|
|
45
61
|
listKudosSummaries(
|
|
46
62
|
input: Required<Pick<KudosListInput, 'limit' | 'offset'>> & KudosListInput,
|