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
JavaScript
// 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