130 lines
4.0 KiB
TypeScript
130 lines
4.0 KiB
TypeScript
import { join } from 'path';
|
|
import { getEntityDir } from '../paths.js';
|
|
import { getNextId } from '../counter.js';
|
|
import { readEntity, writeEntity } from '../files.js';
|
|
import { MessageSchema, type Message } from '../schema.js';
|
|
import { Index } from '../index.js';
|
|
|
|
function indexEntryFor(record: Message, filePath: string) {
|
|
return {
|
|
id: record.id,
|
|
type: 'message',
|
|
title: `${record.from} → ${record.to}`,
|
|
content: record.text,
|
|
filePath,
|
|
createdAt: record.createdAt,
|
|
updatedAt: record.updatedAt,
|
|
status: record.status,
|
|
fromAgent: record.from,
|
|
toAgent: record.to,
|
|
taskId: record.taskId,
|
|
};
|
|
}
|
|
|
|
/** Send a direct message from one agent to another. */
|
|
export function createMessage(cwd: string, options: Partial<Message> = {}): Message {
|
|
if (!options.from) throw new Error('Message requires a "from" agent');
|
|
if (!options.to) throw new Error('Message requires a "to" agent');
|
|
if (!options.text) throw new Error('Message requires text');
|
|
|
|
const now = new Date().toISOString();
|
|
const record: Message = MessageSchema.parse({
|
|
id: getNextId(cwd, 'message'),
|
|
from: options.from,
|
|
to: options.to,
|
|
text: options.text,
|
|
taskId: options.taskId,
|
|
status: 'unread',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
|
|
const filePath = join(getEntityDir(cwd, 'messages'), `${record.id}.md`);
|
|
writeEntity(filePath, record, `# ${record.from} → ${record.to}\n\n${record.text}`);
|
|
|
|
const index = new Index(cwd);
|
|
index.upsert(indexEntryFor(record, filePath));
|
|
index.close();
|
|
|
|
return record;
|
|
}
|
|
|
|
/** Agent aliases that should see the same inbox. Keep deliberately small. */
|
|
export function messageRecipientAliases(agent: string): Set<string> {
|
|
const key = agent.toLowerCase();
|
|
const aliases = new Set([agent, key]);
|
|
if (key === 'architect' || key === 'claude') {
|
|
aliases.add('architect');
|
|
aliases.add('claude');
|
|
}
|
|
return aliases;
|
|
}
|
|
|
|
export interface InboxMessage {
|
|
id: string;
|
|
from: string;
|
|
to: string;
|
|
text: string;
|
|
taskId?: string;
|
|
status: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
/** Messages addressed to `agent`, newest first. */
|
|
export function listInbox(cwd: string, agent: string, opts: { unreadOnly?: boolean } = {}): InboxMessage[] {
|
|
const index = new Index(cwd);
|
|
const all = index.list('message');
|
|
index.close();
|
|
const recipients = messageRecipientAliases(agent);
|
|
return all
|
|
.filter((m) => m.toAgent && recipients.has(String(m.toAgent).toLowerCase()))
|
|
.filter((m) => !opts.unreadOnly || m.status === 'unread')
|
|
.map((m) => ({
|
|
id: m.id,
|
|
from: m.fromAgent ?? '',
|
|
to: m.toAgent ?? '',
|
|
text: m.content,
|
|
taskId: m.taskId,
|
|
status: m.status ?? 'unread',
|
|
createdAt: m.createdAt,
|
|
}));
|
|
}
|
|
|
|
/** All messages (architect-visible view), newest first. */
|
|
export function listMessages(cwd: string): InboxMessage[] {
|
|
const index = new Index(cwd);
|
|
const all = index.list('message');
|
|
index.close();
|
|
return all.map((m) => ({
|
|
id: m.id,
|
|
from: m.fromAgent ?? '',
|
|
to: m.toAgent ?? '',
|
|
text: m.content,
|
|
taskId: m.taskId,
|
|
status: m.status ?? 'unread',
|
|
createdAt: m.createdAt,
|
|
}));
|
|
}
|
|
|
|
export function getMessage(cwd: string, id: string): { message: Message; body: string; filePath: string } {
|
|
if (!id) throw new Error('Message ID is required');
|
|
const filePath = join(getEntityDir(cwd, 'messages'), `${id}.md`);
|
|
const { frontmatter, body } = readEntity(filePath);
|
|
return { message: MessageSchema.parse(frontmatter), body, filePath };
|
|
}
|
|
|
|
/** Mark a message as read. */
|
|
export function markMessageRead(cwd: string, id: string): Message {
|
|
const filePath = join(getEntityDir(cwd, 'messages'), `${id}.md`);
|
|
const { frontmatter, body } = readEntity(filePath);
|
|
const message = MessageSchema.parse(frontmatter);
|
|
const updated: Message = { ...message, status: 'read', updatedAt: new Date().toISOString() };
|
|
writeEntity(filePath, updated, body);
|
|
|
|
const index = new Index(cwd);
|
|
index.upsert(indexEntryFor(updated, filePath));
|
|
index.close();
|
|
|
|
return updated;
|
|
}
|