@plaited/acp-harness 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaited/acp-harness",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "CLI tool for capturing agent trajectories from ACP-compatible agents",
5
5
  "license": "ISC",
6
6
  "engines": {
@@ -235,9 +235,10 @@ const mapToACPUpdate = (update: { type: string; content?: string; title?: string
235
235
  * Runs the headless adapter main loop.
236
236
  *
237
237
  * @param schema - Headless adapter configuration
238
+ * @param verbose - Whether to show debug output
238
239
  */
239
- const runAdapter = async (schema: HeadlessAdapterConfig): Promise<void> => {
240
- const sessions = createSessionManager({ schema })
240
+ const runAdapter = async (schema: HeadlessAdapterConfig, verbose = false): Promise<void> => {
241
+ const sessions = createSessionManager({ schema, verbose })
241
242
  const handlers = createHandlers(schema, sessions)
242
243
 
243
244
  // Method handlers (requests expect responses)
@@ -349,6 +350,7 @@ export const headless = async (args: string[]): Promise<void> => {
349
350
  args,
350
351
  options: {
351
352
  schema: { type: 'string', short: 's' },
353
+ verbose: { type: 'boolean', short: 'v' },
352
354
  help: { type: 'boolean', short: 'h' },
353
355
  },
354
356
  allowPositionals: false,
@@ -357,10 +359,11 @@ export const headless = async (args: string[]): Promise<void> => {
357
359
  if (values.help) {
358
360
  // biome-ignore lint/suspicious/noConsole: CLI help output
359
361
  console.log(`
360
- Usage: acp-harness headless --schema <path>
362
+ Usage: acp-harness headless --schema <path> [--verbose]
361
363
 
362
364
  Arguments:
363
365
  -s, --schema Path to headless adapter schema (JSON)
366
+ -v, --verbose Show constructed commands (for debugging)
364
367
  -h, --help Show this help message
365
368
 
366
369
  Description:
@@ -421,7 +424,7 @@ Examples:
421
424
  }
422
425
 
423
426
  // Run the adapter
424
- await runAdapter(schema)
427
+ await runAdapter(schema, values.verbose ?? false)
425
428
  }
426
429
 
427
430
  // Allow direct execution
@@ -57,6 +57,8 @@ export type SessionManagerConfig = {
57
57
  schema: HeadlessAdapterConfig
58
58
  /** Default timeout for operations in ms */
59
59
  timeout?: number
60
+ /** Whether to show debug output (constructed commands) */
61
+ verbose?: boolean
60
62
  }
61
63
 
62
64
  // ============================================================================
@@ -84,7 +86,7 @@ export type SessionManagerConfig = {
84
86
  * @returns Session manager with create, prompt, and cancel methods
85
87
  */
86
88
  export const createSessionManager = (config: SessionManagerConfig) => {
87
- const { schema, timeout = 60000 } = config
89
+ const { schema, timeout = 60000, verbose = false } = config
88
90
  const sessions = new Map<string, Session>()
89
91
  const outputParser = createOutputParser(schema)
90
92
 
@@ -164,9 +166,10 @@ export const createSessionManager = (config: SessionManagerConfig) => {
164
166
  stderr: 'inherit',
165
167
  })
166
168
 
167
- // If using stdin, write the prompt
169
+ // If using stdin, write the prompt and close stdin
170
+ // (stream mode spawns new process per turn, so stdin should close after writing)
168
171
  if (schema.prompt.stdin && session.process) {
169
- writePromptToStdin(session.process, promptText)
172
+ writePromptToStdin(session.process, promptText, true)
170
173
  }
171
174
  } else {
172
175
  // Subsequent turns: spawn new process with resume flag
@@ -180,9 +183,10 @@ export const createSessionManager = (config: SessionManagerConfig) => {
180
183
  stderr: 'inherit',
181
184
  })
182
185
 
183
- // If using stdin, write the prompt
186
+ // If using stdin, write the prompt and close stdin
187
+ // (stream mode spawns new process per turn, so stdin should close after writing)
184
188
  if (schema.prompt.stdin && session.process) {
185
- writePromptToStdin(session.process, promptText)
189
+ writePromptToStdin(session.process, promptText, true)
186
190
  }
187
191
  }
188
192
 
@@ -211,9 +215,10 @@ export const createSessionManager = (config: SessionManagerConfig) => {
211
215
  stderr: 'inherit',
212
216
  })
213
217
 
214
- // If using stdin, write the prompt
218
+ // If using stdin, write the prompt and close stdin
219
+ // (iterative mode spawns new process per turn, so stdin should close after writing)
215
220
  if (schema.prompt.stdin && session.process) {
216
- writePromptToStdin(session.process, fullPrompt)
221
+ writePromptToStdin(session.process, fullPrompt, true)
217
222
  }
218
223
 
219
224
  const result = await collectOutput(session, outputParser, onUpdate, timeout)
@@ -233,8 +238,10 @@ export const createSessionManager = (config: SessionManagerConfig) => {
233
238
  const buildCommand = (session: Session, promptText: string): string[] => {
234
239
  const args = [...schema.command]
235
240
 
236
- // Add output format flags
237
- args.push(schema.output.flag, schema.output.value)
241
+ // Add output format flags (only if non-empty)
242
+ if (schema.output.flag) {
243
+ args.push(schema.output.flag, schema.output.value)
244
+ }
238
245
 
239
246
  // Add auto-approve flags
240
247
  if (schema.autoApprove) {
@@ -261,6 +268,12 @@ export const createSessionManager = (config: SessionManagerConfig) => {
261
268
  }
262
269
  }
263
270
 
271
+ // Debug output: show constructed command
272
+ if (verbose) {
273
+ const stdinNote = schema.prompt.stdin ? ' (+ stdin)' : ''
274
+ console.error(`[headless] Command: ${args.join(' ')}${stdinNote}`)
275
+ }
276
+
264
277
  return args
265
278
  }
266
279
 
@@ -334,15 +347,24 @@ const generateSessionId = (): string => {
334
347
  * - `'ignore'` → null (not writable)
335
348
  * - number → file descriptor (not a FileSink)
336
349
  *
350
+ * **Closing stdin:** When `closeAfterWrite` is true, the stdin stream is
351
+ * closed after writing. This is required for CLIs that read from stdin
352
+ * with `-` and wait for EOF before processing (e.g., Codex). For stream
353
+ * mode sessions where stdin stays open for subsequent prompts, pass false.
354
+ *
337
355
  * @param process - Subprocess with stdin stream
338
356
  * @param prompt - Prompt text to write
357
+ * @param closeAfterWrite - Whether to close stdin after writing (default: false)
339
358
  *
340
359
  * @internal
341
360
  */
342
- const writePromptToStdin = (process: Subprocess, prompt: string): void => {
361
+ const writePromptToStdin = (process: Subprocess, prompt: string, closeAfterWrite = false): void => {
343
362
  if (process.stdin && typeof process.stdin !== 'number') {
344
363
  process.stdin.write(`${prompt}\n`)
345
364
  process.stdin.flush()
365
+ if (closeAfterWrite) {
366
+ process.stdin.end()
367
+ }
346
368
  }
347
369
  }
348
370