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/projections.ts
CHANGED
|
@@ -16,9 +16,25 @@ import type {
|
|
|
16
16
|
KudosRecord,
|
|
17
17
|
MemoRecord,
|
|
18
18
|
NoteRecord,
|
|
19
|
+
PostRecord,
|
|
20
|
+
TaskDue,
|
|
21
|
+
TaskRecord,
|
|
19
22
|
TodoRecord,
|
|
20
23
|
} from './types.js';
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The instant a deadline has passed.
|
|
27
|
+
*
|
|
28
|
+
* A date-only deadline does not invent a time of day; it lapses at the end of
|
|
29
|
+
* that UTC day, which is the latest reading that cannot call work late while
|
|
30
|
+
* the stated day is still running. Every projection must use this one rule, or
|
|
31
|
+
* two stores would disagree about what is overdue.
|
|
32
|
+
*/
|
|
33
|
+
export function dueInstant(due: TaskDue | undefined): string | undefined {
|
|
34
|
+
if (!due) return undefined;
|
|
35
|
+
return due.kind === 'date' ? `${due.date}T23:59:59.999Z` : due.datetime;
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
const generatedMarker = '<!-- Generated by Synomem. Rebuild with `synomem rebuild`. -->';
|
|
23
39
|
|
|
24
40
|
function manifestPath(home: string, path: string): string {
|
|
@@ -111,7 +127,7 @@ function memoInboxMarkdown(record: MemoRecord): string {
|
|
|
111
127
|
].join('\n')}\n`;
|
|
112
128
|
}
|
|
113
129
|
|
|
114
|
-
function
|
|
130
|
+
function taskInboxMarkdown(record: TaskRecord): string {
|
|
115
131
|
const due =
|
|
116
132
|
record.current.due?.kind === 'date'
|
|
117
133
|
? record.current.due.date
|
|
@@ -126,7 +142,7 @@ function todoInboxMarkdown(record: TodoRecord): string {
|
|
|
126
142
|
`**Status:** ${record.status} `,
|
|
127
143
|
`**Priority:** ${record.current.priority} `,
|
|
128
144
|
`**Due:** ${escapeMarkdown(due)} `,
|
|
129
|
-
`**
|
|
145
|
+
`**Task ID:** \`${record.event.id}\``,
|
|
130
146
|
'',
|
|
131
147
|
...(record.current.description ? [escapeMarkdown(record.current.description), ''] : []),
|
|
132
148
|
].join('\n')}\n`;
|
|
@@ -157,16 +173,16 @@ function memoryMarkdown(profile: AgentProfile, records: NoteRecord[], rebuiltAt:
|
|
|
157
173
|
return `${lines.join('\n')}\n`;
|
|
158
174
|
}
|
|
159
175
|
|
|
160
|
-
function
|
|
176
|
+
function tasksMarkdown(profile: AgentProfile, records: TaskRecord[], rebuiltAt: string): string {
|
|
161
177
|
const lines = [
|
|
162
178
|
generatedMarker,
|
|
163
179
|
'',
|
|
164
|
-
`#
|
|
180
|
+
`# Tasks — ${escapeMarkdown(profile.displayName)}`,
|
|
165
181
|
'',
|
|
166
182
|
`_Last rebuilt: ${rebuiltAt}_`,
|
|
167
183
|
'',
|
|
168
184
|
];
|
|
169
|
-
if (!records.length) lines.push('_No
|
|
185
|
+
if (!records.length) lines.push('_No tasks yet._', '');
|
|
170
186
|
for (const record of records) {
|
|
171
187
|
const due =
|
|
172
188
|
record.current.due?.kind === 'date'
|
|
@@ -180,7 +196,7 @@ function todosMarkdown(profile: AgentProfile, records: TodoRecord[], rebuiltAt:
|
|
|
180
196
|
`**Status:** ${record.status} `,
|
|
181
197
|
`**Priority:** ${record.current.priority} `,
|
|
182
198
|
`**Due:** ${escapeMarkdown(due)} `,
|
|
183
|
-
`**
|
|
199
|
+
`**Task ID:** \`${record.event.id}\``,
|
|
184
200
|
'',
|
|
185
201
|
);
|
|
186
202
|
}
|
|
@@ -239,6 +255,75 @@ export function memoRecordsFromEvents(events: SynomemEvent[]): MemoRecord[] {
|
|
|
239
255
|
return [...records.values()].sort((a, b) => b.event.createdAt.localeCompare(a.event.createdAt));
|
|
240
256
|
}
|
|
241
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Rebuilds posts from their events, acknowledgements included.
|
|
260
|
+
*
|
|
261
|
+
* Edits are kept as a list rather than collapsed into the current text. A reader
|
|
262
|
+
* has to be able to see that a post changed after somebody acknowledged it —
|
|
263
|
+
* silently rewriting what was acknowledged is how a record becomes a lie.
|
|
264
|
+
*/
|
|
265
|
+
export function postRecordsFromEvents(events: SynomemEvent[]): PostRecord[] {
|
|
266
|
+
const records = new Map<string, PostRecord>();
|
|
267
|
+
for (const event of events) {
|
|
268
|
+
if (event.type === 'post.created') {
|
|
269
|
+
records.set(event.id, {
|
|
270
|
+
event,
|
|
271
|
+
edits: [],
|
|
272
|
+
acknowledgments: [],
|
|
273
|
+
status: 'active',
|
|
274
|
+
title: event.title,
|
|
275
|
+
body: event.body,
|
|
276
|
+
...(event.tags ? { tags: event.tags } : {}),
|
|
277
|
+
// The text version, counting only changes to the text.
|
|
278
|
+
//
|
|
279
|
+
// Deliberately not the aggregate version, which also counts every
|
|
280
|
+
// acknowledgement. If they were the same number, somebody
|
|
281
|
+
// acknowledging a post would invalidate an edit the author was in the
|
|
282
|
+
// middle of making — a conflict with nothing to reconcile.
|
|
283
|
+
version: 1,
|
|
284
|
+
});
|
|
285
|
+
} else if (event.type === 'post.edited') {
|
|
286
|
+
const record = records.get(event.postId);
|
|
287
|
+
if (record) {
|
|
288
|
+
record.edits.push(event);
|
|
289
|
+
record.title = event.title;
|
|
290
|
+
record.body = event.body;
|
|
291
|
+
if (event.tags) record.tags = event.tags;
|
|
292
|
+
record.version += 1;
|
|
293
|
+
}
|
|
294
|
+
} else if (event.type === 'post.archived') {
|
|
295
|
+
const record = records.get(event.postId);
|
|
296
|
+
if (record) {
|
|
297
|
+
record.archived = event;
|
|
298
|
+
record.status = 'archived';
|
|
299
|
+
}
|
|
300
|
+
} else if (event.type === 'post.acknowledged') {
|
|
301
|
+
const record = records.get(event.postId);
|
|
302
|
+
if (record) {
|
|
303
|
+
// One statement per actor: a repeat replaces rather than accumulates.
|
|
304
|
+
const existing = record.acknowledgments.findIndex(
|
|
305
|
+
(entry) => entry.actor.id === event.actor.id && entry.actor.kind === event.actor.kind,
|
|
306
|
+
);
|
|
307
|
+
const entry = {
|
|
308
|
+
actor: event.actor,
|
|
309
|
+
acknowledgedAt: event.createdAt,
|
|
310
|
+
...(event.note ? { note: event.note } : {}),
|
|
311
|
+
};
|
|
312
|
+
if (existing >= 0) record.acknowledgments[existing] = entry;
|
|
313
|
+
else record.acknowledgments.push(entry);
|
|
314
|
+
}
|
|
315
|
+
} else if (event.type === 'post.acknowledgment.withdrawn') {
|
|
316
|
+
const record = records.get(event.postId);
|
|
317
|
+
if (record) {
|
|
318
|
+
record.acknowledgments = record.acknowledgments.filter(
|
|
319
|
+
(entry) => !(entry.actor.id === event.actor.id && entry.actor.kind === event.actor.kind),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return [...records.values()];
|
|
325
|
+
}
|
|
326
|
+
|
|
242
327
|
export function noteRecordsFromEvents(events: SynomemEvent[]): NoteRecord[] {
|
|
243
328
|
const records = new Map<string, NoteRecord>();
|
|
244
329
|
for (const event of events) {
|
|
@@ -278,6 +363,13 @@ export function noteRecordsFromEvents(events: SynomemEvent[]): NoteRecord[] {
|
|
|
278
363
|
return [...records.values()].sort((a, b) => b.event.createdAt.localeCompare(a.event.createdAt));
|
|
279
364
|
}
|
|
280
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Builds private Todo records.
|
|
368
|
+
*
|
|
369
|
+
* Deliberately simpler than the Task reducer: there is no acceptance state and
|
|
370
|
+
* no response history, because there is nobody to negotiate with. A Todo goes
|
|
371
|
+
* open → completed / canceled / archived, and back to open on reopen.
|
|
372
|
+
*/
|
|
281
373
|
export function todoRecordsFromEvents(events: SynomemEvent[]): TodoRecord[] {
|
|
282
374
|
const records = new Map<string, TodoRecord>();
|
|
283
375
|
for (const event of events) {
|
|
@@ -286,14 +378,13 @@ export function todoRecordsFromEvents(events: SynomemEvent[]): TodoRecord[] {
|
|
|
286
378
|
event,
|
|
287
379
|
current: {
|
|
288
380
|
title: event.title,
|
|
289
|
-
...(event.
|
|
381
|
+
...(event.details !== undefined ? { details: event.details } : {}),
|
|
290
382
|
priority: event.priority,
|
|
291
383
|
...(event.due ? { due: event.due } : {}),
|
|
292
384
|
tags: event.tags ?? [],
|
|
293
|
-
visibility: event.visibility,
|
|
294
385
|
version: event.aggregateVersion,
|
|
295
386
|
},
|
|
296
|
-
status:
|
|
387
|
+
status: 'open',
|
|
297
388
|
});
|
|
298
389
|
} else if (event.type === 'todo.updated') {
|
|
299
390
|
const record = records.get(event.todoId);
|
|
@@ -301,24 +392,25 @@ export function todoRecordsFromEvents(events: SynomemEvent[]): TodoRecord[] {
|
|
|
301
392
|
record.update = event;
|
|
302
393
|
record.current = {
|
|
303
394
|
title: event.title,
|
|
304
|
-
...(event.
|
|
395
|
+
...(event.details !== undefined ? { details: event.details } : {}),
|
|
305
396
|
priority: event.priority,
|
|
306
397
|
...(event.due ? { due: event.due } : {}),
|
|
307
398
|
tags: event.tags ?? [],
|
|
308
|
-
visibility: event.visibility,
|
|
309
399
|
version: event.aggregateVersion,
|
|
310
400
|
};
|
|
311
401
|
}
|
|
312
|
-
} else if (event.type === 'todo.
|
|
402
|
+
} else if (event.type === 'todo.reopened') {
|
|
313
403
|
const record = records.get(event.todoId);
|
|
314
404
|
if (record) {
|
|
405
|
+
record.reopened = event;
|
|
315
406
|
record.current.version = event.aggregateVersion;
|
|
316
407
|
record.status = 'open';
|
|
408
|
+
delete record.terminal;
|
|
317
409
|
}
|
|
318
410
|
} else if (
|
|
319
411
|
event.type === 'todo.completed' ||
|
|
320
|
-
event.type === 'todo.
|
|
321
|
-
event.type === 'todo.
|
|
412
|
+
event.type === 'todo.canceled' ||
|
|
413
|
+
event.type === 'todo.archived'
|
|
322
414
|
) {
|
|
323
415
|
const record = records.get(event.todoId);
|
|
324
416
|
if (record) {
|
|
@@ -327,12 +419,85 @@ export function todoRecordsFromEvents(events: SynomemEvent[]): TodoRecord[] {
|
|
|
327
419
|
record.status =
|
|
328
420
|
event.type === 'todo.completed'
|
|
329
421
|
? 'completed'
|
|
330
|
-
: event.type === 'todo.
|
|
422
|
+
: event.type === 'todo.canceled'
|
|
423
|
+
? 'canceled'
|
|
424
|
+
: 'archived';
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return [...records.values()];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function taskRecordsFromEvents(events: SynomemEvent[]): TaskRecord[] {
|
|
432
|
+
const records = new Map<string, TaskRecord>();
|
|
433
|
+
for (const event of events) {
|
|
434
|
+
if (event.type === 'task.created') {
|
|
435
|
+
records.set(event.id, {
|
|
436
|
+
event,
|
|
437
|
+
current: {
|
|
438
|
+
title: event.title,
|
|
439
|
+
...(event.description !== undefined ? { description: event.description } : {}),
|
|
440
|
+
priority: event.priority,
|
|
441
|
+
...(event.due ? { due: event.due } : {}),
|
|
442
|
+
tags: event.tags ?? [],
|
|
443
|
+
visibility: event.visibility,
|
|
444
|
+
version: event.aggregateVersion,
|
|
445
|
+
},
|
|
446
|
+
status: event.requiresAcceptance ? 'assigned' : 'open',
|
|
447
|
+
responses: [],
|
|
448
|
+
});
|
|
449
|
+
} else if (event.type === 'task.updated') {
|
|
450
|
+
const record = records.get(event.taskId);
|
|
451
|
+
if (record) {
|
|
452
|
+
record.update = event;
|
|
453
|
+
record.current = {
|
|
454
|
+
title: event.title,
|
|
455
|
+
...(event.description !== undefined ? { description: event.description } : {}),
|
|
456
|
+
priority: event.priority,
|
|
457
|
+
...(event.due ? { due: event.due } : {}),
|
|
458
|
+
tags: event.tags ?? [],
|
|
459
|
+
visibility: event.visibility,
|
|
460
|
+
version: event.aggregateVersion,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
} else if (event.type === 'task.accepted') {
|
|
464
|
+
const record = records.get(event.taskId);
|
|
465
|
+
if (record) {
|
|
466
|
+
record.current.version = event.aggregateVersion;
|
|
467
|
+
record.status = 'open';
|
|
468
|
+
record.responses.push({
|
|
469
|
+
kind: 'accepted',
|
|
470
|
+
...(event.response !== undefined ? { response: event.response } : {}),
|
|
471
|
+
actor: event.actor,
|
|
472
|
+
at: event.createdAt,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
} else if (
|
|
476
|
+
event.type === 'task.completed' ||
|
|
477
|
+
event.type === 'task.rejected' ||
|
|
478
|
+
event.type === 'task.canceled'
|
|
479
|
+
) {
|
|
480
|
+
const record = records.get(event.taskId);
|
|
481
|
+
if (record) {
|
|
482
|
+
record.terminal = event;
|
|
483
|
+
record.current.version = event.aggregateVersion;
|
|
484
|
+
if (event.type === 'task.rejected') {
|
|
485
|
+
record.responses.push({
|
|
486
|
+
kind: 'rejected',
|
|
487
|
+
response: event.response,
|
|
488
|
+
actor: event.actor,
|
|
489
|
+
at: event.createdAt,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
record.status =
|
|
493
|
+
event.type === 'task.completed'
|
|
494
|
+
? 'completed'
|
|
495
|
+
: event.type === 'task.rejected'
|
|
331
496
|
? 'rejected'
|
|
332
497
|
: 'canceled';
|
|
333
498
|
}
|
|
334
|
-
} else if (event.type === '
|
|
335
|
-
const record = records.get(event.
|
|
499
|
+
} else if (event.type === 'task.reopened') {
|
|
500
|
+
const record = records.get(event.taskId);
|
|
336
501
|
if (record) {
|
|
337
502
|
record.reopened = event;
|
|
338
503
|
record.current.version = event.aggregateVersion;
|
|
@@ -358,9 +523,9 @@ export function renderMarkdownExport(events: SynomemEvent[]): string {
|
|
|
358
523
|
(record) =>
|
|
359
524
|
`## Note: ${escapeMarkdown(record.current.title)}\n\n${escapeMarkdown(record.current.body)}\n\nStatus: ${record.status}; version ${record.current.version}\n\nID: \`${record.event.id}\``,
|
|
360
525
|
),
|
|
361
|
-
...
|
|
526
|
+
...taskRecordsFromEvents(events).map(
|
|
362
527
|
(record) =>
|
|
363
|
-
`##
|
|
528
|
+
`## Task: ${escapeMarkdown(record.current.title)}\n\n${record.current.description ? `${escapeMarkdown(record.current.description)}\n\n` : ''}Status: ${record.status}; priority ${record.current.priority}\n\nID: \`${record.event.id}\``,
|
|
364
529
|
),
|
|
365
530
|
];
|
|
366
531
|
return `${sections.join('\n\n')}\n`;
|
|
@@ -380,7 +545,7 @@ export class ProjectionManager implements ProjectionWriter {
|
|
|
380
545
|
const records = recordsFromEvents(events);
|
|
381
546
|
const memos = memoRecordsFromEvents(events);
|
|
382
547
|
const notes = noteRecordsFromEvents(events);
|
|
383
|
-
const
|
|
548
|
+
const tasks = taskRecordsFromEvents(events);
|
|
384
549
|
const rebuiltAt = events.at(-1)?.createdAt ?? new Date(0).toISOString();
|
|
385
550
|
const generated: string[] = [];
|
|
386
551
|
|
|
@@ -394,12 +559,12 @@ export class ProjectionManager implements ProjectionWriter {
|
|
|
394
559
|
const inboxDirectory = join(agentDirectory, 'inbox');
|
|
395
560
|
const kudosInboxDirectory = join(inboxDirectory, 'kudos');
|
|
396
561
|
const memoInboxDirectory = join(inboxDirectory, 'memos');
|
|
397
|
-
const
|
|
562
|
+
const taskInboxDirectory = join(inboxDirectory, 'tasks');
|
|
398
563
|
assertNoSymlinkEscape(this.storage.home, inboxDirectory);
|
|
399
564
|
ensureDirectory(inboxDirectory);
|
|
400
565
|
ensureDirectory(kudosInboxDirectory);
|
|
401
566
|
ensureDirectory(memoInboxDirectory);
|
|
402
|
-
ensureDirectory(
|
|
567
|
+
ensureDirectory(taskInboxDirectory);
|
|
403
568
|
|
|
404
569
|
atomicWriteDerivedFile(
|
|
405
570
|
profilePath,
|
|
@@ -424,11 +589,11 @@ export class ProjectionManager implements ProjectionWriter {
|
|
|
424
589
|
atomicWriteDerivedFile(memoryPath, memoryMarkdown(profile, agentNotes, rebuiltAt));
|
|
425
590
|
generated.push(manifestPath(this.storage.home, memoryPath));
|
|
426
591
|
}
|
|
427
|
-
const
|
|
428
|
-
if (this.storage.config.projection.
|
|
429
|
-
const
|
|
430
|
-
atomicWriteDerivedFile(
|
|
431
|
-
generated.push(manifestPath(this.storage.home,
|
|
592
|
+
const agentTasks = tasks.filter((record) => record.event.assigneeAgentId === profile.id);
|
|
593
|
+
if (this.storage.config.projection.writeTasksMarkdown) {
|
|
594
|
+
const tasksPath = join(agentDirectory, 'TASKS.md');
|
|
595
|
+
atomicWriteDerivedFile(tasksPath, tasksMarkdown(profile, agentTasks, rebuiltAt));
|
|
596
|
+
generated.push(manifestPath(this.storage.home, tasksPath));
|
|
432
597
|
}
|
|
433
598
|
if (this.storage.config.projection.writeInboxEntries) {
|
|
434
599
|
for (const record of agentRecords.filter(
|
|
@@ -445,11 +610,11 @@ export class ProjectionManager implements ProjectionWriter {
|
|
|
445
610
|
atomicWriteDerivedFile(inboxPath, memoInboxMarkdown(record));
|
|
446
611
|
generated.push(manifestPath(this.storage.home, inboxPath));
|
|
447
612
|
}
|
|
448
|
-
for (const record of
|
|
613
|
+
for (const record of agentTasks.filter(
|
|
449
614
|
(item) => item.status === 'open' || item.status === 'assigned',
|
|
450
615
|
)) {
|
|
451
|
-
const inboxPath = join(
|
|
452
|
-
atomicWriteDerivedFile(inboxPath,
|
|
616
|
+
const inboxPath = join(taskInboxDirectory, `${record.event.id}.md`);
|
|
617
|
+
atomicWriteDerivedFile(inboxPath, taskInboxMarkdown(record));
|
|
453
618
|
generated.push(manifestPath(this.storage.home, inboxPath));
|
|
454
619
|
}
|
|
455
620
|
}
|
|
@@ -487,7 +652,7 @@ export class ProjectionManager implements ProjectionWriter {
|
|
|
487
652
|
const notes = noteRecordsFromEvents(events).filter(
|
|
488
653
|
(record) => record.event.ownerAgentId === profile.id,
|
|
489
654
|
);
|
|
490
|
-
const
|
|
655
|
+
const tasks = taskRecordsFromEvents(events).filter(
|
|
491
656
|
(record) => record.event.assigneeAgentId === profile.id,
|
|
492
657
|
);
|
|
493
658
|
const rebuiltAt = events.at(-1)?.createdAt ?? new Date(0).toISOString();
|
|
@@ -495,14 +660,14 @@ export class ProjectionManager implements ProjectionWriter {
|
|
|
495
660
|
const inboxDirectory = join(agentDirectory, 'inbox');
|
|
496
661
|
const kudosInboxDirectory = join(inboxDirectory, 'kudos');
|
|
497
662
|
const memoInboxDirectory = join(inboxDirectory, 'memos');
|
|
498
|
-
const
|
|
663
|
+
const taskInboxDirectory = join(inboxDirectory, 'tasks');
|
|
499
664
|
assertNoSymlinkEscape(this.storage.home, agentDirectory);
|
|
500
665
|
assertNoSymlinkEscape(this.storage.home, inboxDirectory);
|
|
501
666
|
ensureDirectory(agentDirectory);
|
|
502
667
|
ensureDirectory(inboxDirectory);
|
|
503
668
|
ensureDirectory(kudosInboxDirectory);
|
|
504
669
|
ensureDirectory(memoInboxDirectory);
|
|
505
|
-
ensureDirectory(
|
|
670
|
+
ensureDirectory(taskInboxDirectory);
|
|
506
671
|
|
|
507
672
|
const generated: string[] = [];
|
|
508
673
|
const profilePath = join(agentDirectory, 'profile.json');
|
|
@@ -532,10 +697,10 @@ export class ProjectionManager implements ProjectionWriter {
|
|
|
532
697
|
generated.push(manifestPath(this.storage.home, memoryPath));
|
|
533
698
|
}
|
|
534
699
|
|
|
535
|
-
if (this.storage.config.projection.
|
|
536
|
-
const
|
|
537
|
-
atomicWriteDerivedFile(
|
|
538
|
-
generated.push(manifestPath(this.storage.home,
|
|
700
|
+
if (this.storage.config.projection.writeTasksMarkdown) {
|
|
701
|
+
const tasksPath = join(agentDirectory, 'TASKS.md');
|
|
702
|
+
atomicWriteDerivedFile(tasksPath, tasksMarkdown(profile, tasks, rebuiltAt));
|
|
703
|
+
generated.push(manifestPath(this.storage.home, tasksPath));
|
|
539
704
|
}
|
|
540
705
|
|
|
541
706
|
if (this.storage.config.projection.writeInboxEntries) {
|
|
@@ -553,12 +718,12 @@ export class ProjectionManager implements ProjectionWriter {
|
|
|
553
718
|
if (!existsSync(inboxPath)) atomicWriteDerivedFile(inboxPath, memoInboxMarkdown(record));
|
|
554
719
|
generated.push(relativePath);
|
|
555
720
|
}
|
|
556
|
-
for (const record of
|
|
721
|
+
for (const record of tasks.filter(
|
|
557
722
|
(item) => item.status === 'open' || item.status === 'assigned',
|
|
558
723
|
)) {
|
|
559
|
-
const inboxPath = join(
|
|
724
|
+
const inboxPath = join(taskInboxDirectory, `${record.event.id}.md`);
|
|
560
725
|
const relativePath = manifestPath(this.storage.home, inboxPath);
|
|
561
|
-
if (!existsSync(inboxPath)) atomicWriteDerivedFile(inboxPath,
|
|
726
|
+
if (!existsSync(inboxPath)) atomicWriteDerivedFile(inboxPath, taskInboxMarkdown(record));
|
|
562
727
|
generated.push(relativePath);
|
|
563
728
|
}
|
|
564
729
|
}
|
|
@@ -589,13 +754,13 @@ export class ProjectionManager implements ProjectionWriter {
|
|
|
589
754
|
const events = this.storage.getReadableEvents();
|
|
590
755
|
const records = recordsFromEvents(events);
|
|
591
756
|
const memos = memoRecordsFromEvents(events);
|
|
592
|
-
const
|
|
757
|
+
const tasks = taskRecordsFromEvents(events);
|
|
593
758
|
const paths: string[] = [];
|
|
594
759
|
for (const profile of profiles) {
|
|
595
760
|
paths.push(`${profile.id}/profile.json`);
|
|
596
761
|
if (this.storage.config.projection.writeWinsMarkdown) paths.push(`${profile.id}/WINS.md`);
|
|
597
762
|
if (this.storage.config.projection.writeMemoryMarkdown) paths.push(`${profile.id}/MEMORY.md`);
|
|
598
|
-
if (this.storage.config.projection.
|
|
763
|
+
if (this.storage.config.projection.writeTasksMarkdown) paths.push(`${profile.id}/TASKS.md`);
|
|
599
764
|
if (this.storage.config.projection.writeInboxEntries) {
|
|
600
765
|
for (const record of records.filter(
|
|
601
766
|
(item) =>
|
|
@@ -610,12 +775,12 @@ export class ProjectionManager implements ProjectionWriter {
|
|
|
610
775
|
)) {
|
|
611
776
|
paths.push(`${profile.id}/inbox/memos/${record.event.id}.md`);
|
|
612
777
|
}
|
|
613
|
-
for (const record of
|
|
778
|
+
for (const record of tasks.filter(
|
|
614
779
|
(item) =>
|
|
615
780
|
item.event.assigneeAgentId === profile.id &&
|
|
616
781
|
(item.status === 'open' || item.status === 'assigned'),
|
|
617
782
|
)) {
|
|
618
|
-
paths.push(`${profile.id}/inbox/
|
|
783
|
+
paths.push(`${profile.id}/inbox/tasks/${record.event.id}.md`);
|
|
619
784
|
}
|
|
620
785
|
}
|
|
621
786
|
}
|
package/src/remote.ts
CHANGED
|
@@ -4,16 +4,21 @@ import type { SynomemService, SynomemServiceCapabilities, SynomemServiceInfo } f
|
|
|
4
4
|
import type {
|
|
5
5
|
ActorIdentity,
|
|
6
6
|
ChangesInput,
|
|
7
|
+
BindRuntimeInput,
|
|
7
8
|
CreateAgentInput,
|
|
9
|
+
CreatePostInput,
|
|
10
|
+
UpdatePostInput,
|
|
8
11
|
CreateNoteInput,
|
|
9
|
-
|
|
12
|
+
CreateTaskInput,
|
|
10
13
|
GiveKudosInput,
|
|
11
14
|
ItemListInput,
|
|
12
15
|
KudosListInput,
|
|
13
16
|
ReviseNoteInput,
|
|
14
17
|
SendMemoInput,
|
|
15
18
|
UpdateAgentInput,
|
|
19
|
+
UpdateTaskInput,
|
|
16
20
|
UpdateTodoInput,
|
|
21
|
+
CreateTodoInput,
|
|
17
22
|
} from './types.js';
|
|
18
23
|
|
|
19
24
|
const defaultMaximumResponseBytes = 1024 * 1024;
|
|
@@ -169,6 +174,78 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
169
174
|
),
|
|
170
175
|
list: () =>
|
|
171
176
|
this.request<Awaited<ReturnType<SynomemService['agents']['list']>>>('GET', 'agents'),
|
|
177
|
+
resolve: (query: string) =>
|
|
178
|
+
this.request<Awaited<ReturnType<SynomemService['agents']['resolve']>>>(
|
|
179
|
+
'GET',
|
|
180
|
+
`agents/resolve?query=${encodeURIComponent(query)}`,
|
|
181
|
+
),
|
|
182
|
+
directory: () =>
|
|
183
|
+
this.request<Awaited<ReturnType<SynomemService['agents']['directory']>>>(
|
|
184
|
+
'GET',
|
|
185
|
+
'agents/directory',
|
|
186
|
+
),
|
|
187
|
+
bindings: (idOrAlias: string) =>
|
|
188
|
+
this.request<Awaited<ReturnType<SynomemService['agents']['bindings']>>>(
|
|
189
|
+
'GET',
|
|
190
|
+
`agents/${encodeURIComponent(idOrAlias)}/runtimes`,
|
|
191
|
+
),
|
|
192
|
+
bindRuntime: (input: BindRuntimeInput) =>
|
|
193
|
+
this.mutation<Awaited<ReturnType<SynomemService['agents']['bindRuntime']>>>(
|
|
194
|
+
'POST',
|
|
195
|
+
`agents/${encodeURIComponent(input.agentId)}/runtimes`,
|
|
196
|
+
input,
|
|
197
|
+
),
|
|
198
|
+
unbindRuntime: (bindingId: string) =>
|
|
199
|
+
this.request<Awaited<ReturnType<SynomemService['agents']['unbindRuntime']>>>(
|
|
200
|
+
'DELETE',
|
|
201
|
+
`agents/runtimes/${encodeURIComponent(bindingId)}`,
|
|
202
|
+
),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
readonly posts = {
|
|
206
|
+
create: (input: CreatePostInput) =>
|
|
207
|
+
this.mutation<Awaited<ReturnType<SynomemService['posts']['create']>>>('POST', 'posts', input),
|
|
208
|
+
list: (input: Omit<ItemListInput, 'kinds'> = {}) =>
|
|
209
|
+
this.request<Awaited<ReturnType<SynomemService['posts']['list']>>>(
|
|
210
|
+
'GET',
|
|
211
|
+
`posts${queryString(input)}`,
|
|
212
|
+
),
|
|
213
|
+
get: (id: string) =>
|
|
214
|
+
this.request<Awaited<ReturnType<SynomemService['posts']['get']>>>(
|
|
215
|
+
'GET',
|
|
216
|
+
`posts/${encodeURIComponent(id)}`,
|
|
217
|
+
),
|
|
218
|
+
update: (input: UpdatePostInput) =>
|
|
219
|
+
this.mutation<Awaited<ReturnType<SynomemService['posts']['update']>>>(
|
|
220
|
+
'POST',
|
|
221
|
+
`posts/${encodeURIComponent(input.postId)}/revisions`,
|
|
222
|
+
input,
|
|
223
|
+
['postId'],
|
|
224
|
+
),
|
|
225
|
+
archive: (input: { postId: string; reason?: string; idempotencyKey?: string }) =>
|
|
226
|
+
this.mutation<Awaited<ReturnType<SynomemService['posts']['archive']>>>(
|
|
227
|
+
'POST',
|
|
228
|
+
`posts/${encodeURIComponent(input.postId)}/archive`,
|
|
229
|
+
input,
|
|
230
|
+
['postId'],
|
|
231
|
+
),
|
|
232
|
+
acknowledge: (input: { postId: string; note?: string; idempotencyKey?: string }) =>
|
|
233
|
+
this.mutation<Awaited<ReturnType<SynomemService['posts']['acknowledge']>>>(
|
|
234
|
+
'POST',
|
|
235
|
+
`posts/${encodeURIComponent(input.postId)}/acknowledgment`,
|
|
236
|
+
input,
|
|
237
|
+
['postId'],
|
|
238
|
+
),
|
|
239
|
+
withdrawAcknowledgment: (input: { postId: string; reason?: string }) =>
|
|
240
|
+
this.request<Awaited<ReturnType<SynomemService['posts']['withdrawAcknowledgment']>>>(
|
|
241
|
+
'DELETE',
|
|
242
|
+
`posts/${encodeURIComponent(input.postId)}/acknowledgment`,
|
|
243
|
+
),
|
|
244
|
+
roster: (postId: string) =>
|
|
245
|
+
this.request<Awaited<ReturnType<SynomemService['posts']['roster']>>>(
|
|
246
|
+
'GET',
|
|
247
|
+
`posts/${encodeURIComponent(postId)}/roster`,
|
|
248
|
+
),
|
|
172
249
|
};
|
|
173
250
|
|
|
174
251
|
readonly kudos = {
|
|
@@ -261,6 +338,42 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
261
338
|
),
|
|
262
339
|
};
|
|
263
340
|
|
|
341
|
+
readonly tasks = {
|
|
342
|
+
create: (input: CreateTaskInput) =>
|
|
343
|
+
this.mutation<Awaited<ReturnType<SynomemService['tasks']['create']>>>('POST', 'tasks', input),
|
|
344
|
+
list: (input: Omit<ItemListInput, 'kinds'> = {}) =>
|
|
345
|
+
this.request<Awaited<ReturnType<SynomemService['tasks']['list']>>>(
|
|
346
|
+
'GET',
|
|
347
|
+
`tasks${queryString(input)}`,
|
|
348
|
+
),
|
|
349
|
+
get: (id: string) =>
|
|
350
|
+
this.request<Awaited<ReturnType<SynomemService['tasks']['get']>>>(
|
|
351
|
+
'GET',
|
|
352
|
+
`tasks/${encodeURIComponent(id)}`,
|
|
353
|
+
),
|
|
354
|
+
update: (input: UpdateTaskInput) =>
|
|
355
|
+
this.mutation<Awaited<ReturnType<SynomemService['tasks']['update']>>>(
|
|
356
|
+
'POST',
|
|
357
|
+
`tasks/${encodeURIComponent(input.taskId)}/revisions`,
|
|
358
|
+
input,
|
|
359
|
+
['taskId'],
|
|
360
|
+
),
|
|
361
|
+
accept: (input: { taskId: string; response?: string; idempotencyKey?: string }) =>
|
|
362
|
+
this.taskTransition('accept', input),
|
|
363
|
+
reject: (input: { taskId: string; response: string; idempotencyKey?: string }) =>
|
|
364
|
+
this.taskTransition('reject', input),
|
|
365
|
+
complete: (input: { taskId: string; note?: string; idempotencyKey?: string }) =>
|
|
366
|
+
this.taskTransition('complete', input),
|
|
367
|
+
reopen: (input: { taskId: string; idempotencyKey?: string }) =>
|
|
368
|
+
this.taskTransition('reopen', input),
|
|
369
|
+
cancel: (input: { taskId: string; reason?: string; idempotencyKey?: string }) =>
|
|
370
|
+
this.taskTransition('cancel', input),
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Private self-reminders. The remote surface mirrors the local one exactly so
|
|
375
|
+
* a caller does not have to know which backend it is talking to.
|
|
376
|
+
*/
|
|
264
377
|
readonly todos = {
|
|
265
378
|
create: (input: CreateTodoInput) =>
|
|
266
379
|
this.mutation<Awaited<ReturnType<SynomemService['todos']['create']>>>('POST', 'todos', input),
|
|
@@ -281,16 +394,43 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
281
394
|
input,
|
|
282
395
|
['todoId'],
|
|
283
396
|
),
|
|
284
|
-
accept: (input: { todoId: string; idempotencyKey?: string }) =>
|
|
285
|
-
this.todoTransition('accept', input),
|
|
286
|
-
reject: (input: { todoId: string; reason?: string; idempotencyKey?: string }) =>
|
|
287
|
-
this.todoTransition('reject', input),
|
|
288
397
|
complete: (input: { todoId: string; note?: string; idempotencyKey?: string }) =>
|
|
289
398
|
this.todoTransition('complete', input),
|
|
290
399
|
reopen: (input: { todoId: string; idempotencyKey?: string }) =>
|
|
291
400
|
this.todoTransition('reopen', input),
|
|
292
401
|
cancel: (input: { todoId: string; reason?: string; idempotencyKey?: string }) =>
|
|
293
402
|
this.todoTransition('cancel', input),
|
|
403
|
+
archive: (input: { todoId: string; idempotencyKey?: string }) =>
|
|
404
|
+
this.todoTransition('archive', input),
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Discovery mirrors the local semantics: the age/deadline is resolved here so
|
|
409
|
+
* both backends answer the same question, rather than each server deciding
|
|
410
|
+
* what "older than 24 hours" means.
|
|
411
|
+
*/
|
|
412
|
+
readonly discovery = {
|
|
413
|
+
unanswered: (
|
|
414
|
+
input: Omit<ItemListInput, 'awaitingResponse' | 'pending'> & { olderThanHours?: number } = {},
|
|
415
|
+
) => {
|
|
416
|
+
const { olderThanHours, awaitingSince, ...rest } = input;
|
|
417
|
+
const since =
|
|
418
|
+
awaitingSince ??
|
|
419
|
+
(olderThanHours !== undefined
|
|
420
|
+
? new Date(Date.now() - olderThanHours * 3_600_000).toISOString()
|
|
421
|
+
: undefined);
|
|
422
|
+
return this.request<Awaited<ReturnType<SynomemService['items']['list']>>>(
|
|
423
|
+
'GET',
|
|
424
|
+
`items${queryString({ ...rest, awaitingResponse: true, ...(since ? { awaitingSince: since } : {}) })}`,
|
|
425
|
+
);
|
|
426
|
+
},
|
|
427
|
+
overdue: (input: Omit<ItemListInput, 'overdueAsOf'> & { asOf?: string } = {}) => {
|
|
428
|
+
const { asOf, ...rest } = input;
|
|
429
|
+
return this.request<Awaited<ReturnType<SynomemService['items']['list']>>>(
|
|
430
|
+
'GET',
|
|
431
|
+
`items${queryString({ ...rest, overdueAsOf: asOf ?? new Date().toISOString() })}`,
|
|
432
|
+
);
|
|
433
|
+
},
|
|
294
434
|
};
|
|
295
435
|
|
|
296
436
|
readonly items = {
|
|
@@ -410,11 +550,29 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
410
550
|
);
|
|
411
551
|
}
|
|
412
552
|
|
|
413
|
-
private
|
|
553
|
+
private taskTransition(
|
|
414
554
|
transition: 'accept' | 'reject' | 'complete' | 'reopen' | 'cancel',
|
|
555
|
+
input: {
|
|
556
|
+
taskId: string;
|
|
557
|
+
idempotencyKey?: string;
|
|
558
|
+
reason?: string;
|
|
559
|
+
note?: string;
|
|
560
|
+
response?: string;
|
|
561
|
+
},
|
|
562
|
+
) {
|
|
563
|
+
return this.mutation<Awaited<ReturnType<SynomemService['tasks']['accept']>>>(
|
|
564
|
+
'POST',
|
|
565
|
+
`tasks/${encodeURIComponent(input.taskId)}/${transition}`,
|
|
566
|
+
input,
|
|
567
|
+
['taskId'],
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private todoTransition(
|
|
572
|
+
transition: 'complete' | 'reopen' | 'cancel' | 'archive',
|
|
415
573
|
input: { todoId: string; idempotencyKey?: string; reason?: string; note?: string },
|
|
416
574
|
) {
|
|
417
|
-
return this.mutation<Awaited<ReturnType<SynomemService['todos']['
|
|
575
|
+
return this.mutation<Awaited<ReturnType<SynomemService['todos']['complete']>>>(
|
|
418
576
|
'POST',
|
|
419
577
|
`todos/${encodeURIComponent(input.todoId)}/${transition}`,
|
|
420
578
|
input,
|
|
@@ -437,7 +595,7 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
437
595
|
}
|
|
438
596
|
|
|
439
597
|
private async request<T>(
|
|
440
|
-
method: 'GET' | 'POST' | 'PATCH',
|
|
598
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
|
|
441
599
|
path: string,
|
|
442
600
|
body?: object,
|
|
443
601
|
idempotencyKey?: string,
|