import type { Command } from "commander"; import type { EmitContext } from "../commander.ts"; import type { HarneryConversationCatalog } from "../core/conversations/catalog.ts"; import { buildConversationContextPack } from "../core/conversations/context-pack.ts"; import type { HarneryConversationQueryRequest, HarneryConversationRecordV1, HarneryConversationRole, } from "../core/conversations/contract.ts"; import { type HarneryConversationPurgePlan, planConversationPurge, validatePurgeExecutionRequest, } from "../core/conversations/lifecycle.ts"; import { queryConversationCatalog } from "../core/conversations/query.ts"; export interface RegisterConversationsOptions { project_scope_id: string; records?: () => readonly HarneryConversationRecordV1[]; } export function registerConversationsCommand( program: Command, emit: EmitContext, catalog?: HarneryConversationCatalog, dependencies?: RegisterConversationsOptions, ): void { const pendingPlans = new Map(); const conversations = program.command("conversations").description("Query conversation evidence"); conversations .command("list") .option("--provider ") .option("--project ") .option("--json") .action(async (options: { provider?: string; project?: string; json?: boolean }) => { await run(emit, async () => { const configured = requireConversations(catalog, dependencies); const project = exactProject(configured.dependencies.project_scope_id, options.project); const providers = options.provider ? [configured.catalog.require(options.provider)] : configured.catalog.list(); const rows = ( await Promise.all( providers.map(async (provider) => provider.capabilities.can_list ? provider.list(project) : [], ), ) ).flat(); if (options.json) { emit.config({ format: "json" }); emit.data({ schema: "harnery.conversation-list/v1", project_scope_id: project, rows }); } else emit.rows(rows as unknown as Record[]); }); }); addQueryOptions(conversations.command("query")) .option("--json") .action(async (options: QueryOptions) => { await run(emit, async () => { const configured = requireConversations(catalog, dependencies); const result = await queryConversationCatalog( configured.catalog, request(configured.dependencies, options), ); if (options.json) { emit.config({ format: "json" }); emit.data(result); } else { emit.rows( result.hits.map(({ record, citation }) => ({ ...citation, content: record.content, })), ); } }); }); addQueryOptions(conversations.command("context")) .option("--max-tokens ", "Token ceiling", parsePositive, 2_000) .option("--max-bytes ", "Byte ceiling", parsePositive, 16_000) .option("--json") .action( async (options: QueryOptions & { maxTokens: number; maxBytes: number; json?: boolean }) => { await run(emit, async () => { const configured = requireConversations(catalog, dependencies); const result = await queryConversationCatalog( configured.catalog, request(configured.dependencies, options), ); const pack = buildConversationContextPack(result, { max_tokens: options.maxTokens, max_bytes: options.maxBytes, }); if (options.json) emit.config({ format: "json" }); emit.data(pack); }); }, ); conversations .command("archive") .requiredOption("--mode ", "off | source | archive") .option("--authority ", "shadow | cutover | rollback", "shadow") .option("--yes", "Reserved for a future checked capture transaction") .action((options: { mode: string; authority: string; yes?: boolean }) => { const validMode = ["off", "source", "archive"].includes(options.mode); const validAuthority = ["shadow", "cutover", "rollback"].includes(options.authority); if (!validMode || !validAuthority) { emit.error({ code: "conversation_archive_mode_invalid", message: "invalid archive mode" }); emit.setExitCode(1); return; } emit.data({ schema: "harnery.conversation-archive-plan/v1", access_mode: options.mode, authority_mode: options.authority, dry_run: true, writes: false, execution_enabled: false, }); }); conversations .command("purge") .option("--conversation ") .option("--before