UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

171 lines 6.7 kB
// CLI HitlDeliveryAdapter — terminal-based operator I/O for hitl-prompt/v1. // // Consumes the `HitlPromptEnvelope` extracted by `extractHitlEnvelope`, // renders it to stdout, prompts the operator on stdin, and returns a // JsonValue ready to be wrapped by `buildHitlResponseMessage` and POSTed // via `A2AClient.sendMessage`. // // Behaviors: // - Renders the prompt verbatim (markdown allowed; we don't try to // re-render — terminal users get the raw markdown which is readable). // - Surfaces `response_schema` properties so the operator knows what // shape to type. // - Reads a single JSON line from stdin (multi-line input via repeated // reads until a balanced JSON value is parsed). // - Honors `signal` for cancellation (deadline expiry, task cancel). // // Not in scope for this adapter: // - Validation against `response_schema` — the driver does that with // Ajv (see `validateResponseWithAjv` in this module) so the adapter // can stay framework-free and unit-testable with mock streams. // // @issue #1255 import * as readline from 'node:readline'; export class CliHitlDeliveryAdapter { name = 'cli'; operatorId; input; output; constructor(opts = {}) { this.input = opts.input ?? process.stdin; this.output = opts.output ?? process.stdout; // errorOutput is reserved for future use (currently unused — kept on // the options surface so callers can plug it in once we add stderr // diagnostics from the adapter itself rather than the driver). void opts.errorOutput; this.operatorId = opts.operatorId ?? process.env['USER'] ?? process.env['USERNAME'] ?? 'unknown-operator'; } async collect(envelope, ctx) { this.render(envelope, ctx); if (ctx.signal?.aborted) { throw new HitlAdapterAborted('signal aborted before reading response'); } const raw = await this.readJsonFromStdin(ctx.signal); if (raw === null) { throw new HitlAdapterAborted('stdin closed without response'); } return raw; } render(envelope, ctx) { const lines = []; lines.push(''); lines.push('━'.repeat(72)); lines.push('HITL prompt — operator response required'); lines.push('━'.repeat(72)); lines.push(`prompt_id: ${envelope.prompt_id}`); if (ctx.taskId) lines.push(`task_id: ${ctx.taskId}`); if (ctx.contextId) lines.push(`context: ${ctx.contextId}`); if (envelope.deadline) lines.push(`deadline: ${envelope.deadline}`); if (envelope.allowed_responders && envelope.allowed_responders.length > 0) { lines.push(`responders allowed: ${envelope.allowed_responders.join(', ')}`); } lines.push(''); lines.push('Prompt:'); lines.push('-'.repeat(72)); lines.push(envelope.prompt); lines.push('-'.repeat(72)); lines.push('Expected response shape (response_schema):'); lines.push(JSON.stringify(envelope.response_schema, null, 2)); lines.push(''); lines.push('Enter response as a single JSON value (object, array, string, number, boolean, or null).'); lines.push('Press Ctrl-D after the closing bracket/brace if your JSON spans multiple lines.'); lines.push(''); this.output.write(lines.join('\n') + '\n'); } /** * Read JSON from `this.input` until either: * - a balanced JSON value parses cleanly (success), or * - the stream ends (returns null), or * - the abort signal fires (throws HitlAdapterAborted). * * Allows the operator to type a multi-line JSON object — we accumulate * lines and try parsing at each newline boundary. */ readJsonFromStdin(signal) { return new Promise((resolve, reject) => { let aborted = false; const rl = readline.createInterface({ input: this.input, crlfDelay: Infinity, terminal: false, }); const abortListener = () => { aborted = true; rl.close(); reject(new HitlAdapterAborted('signal aborted while reading stdin')); }; if (signal) { if (signal.aborted) { rl.close(); reject(new HitlAdapterAborted('signal aborted before reading')); return; } signal.addEventListener('abort', abortListener, { once: true }); } let buffer = ''; rl.on('line', (line) => { buffer += (buffer ? '\n' : '') + line; // Try parsing each time the operator hits enter — supports both // one-line responses and multi-line pretty-printed JSON. const parsed = tryParseJson(buffer); if (parsed.ok) { rl.close(); if (signal) signal.removeEventListener('abort', abortListener); resolve(parsed.value); } }); rl.on('close', () => { if (aborted) return; if (signal) signal.removeEventListener('abort', abortListener); if (buffer.length === 0) { resolve(null); return; } const finalParse = tryParseJson(buffer); if (finalParse.ok) resolve(finalParse.value); else reject(new HitlAdapterParseError(`could not parse stdin as JSON: ${finalParse.error}`)); }); rl.on('error', (err) => { if (signal) signal.removeEventListener('abort', abortListener); reject(err); }); }); } } export class HitlAdapterAborted extends Error { constructor(message) { super(message); this.name = 'HitlAdapterAborted'; } } export class HitlAdapterParseError extends Error { constructor(message) { super(message); this.name = 'HitlAdapterParseError'; } } function tryParseJson(s) { const trimmed = s.trim(); if (trimmed.length === 0) return { ok: false, error: 'empty input' }; try { return { ok: true, value: JSON.parse(trimmed) }; } catch (e) { return { ok: false, error: e.message }; } } //# sourceMappingURL=hitl-cli.js.map