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 { 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 { 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; }