{"version":3,"sources":["../src/index.ts","../src/template.ts"],"sourcesContent":["import type {\n  CliEvent,\n  CliName,\n  CliProcess,\n  SpawnOptions,\n} from \"@0xtiby/spawner\";\nimport { spawn as spawnCli } from \"@0xtiby/spawner\";\nimport { substitute } from \"./template.js\";\n\ntype TranscriptChunk =\n  | { type: \"raw\"; text: string }\n  | { type: \"line\"; text: string };\n\nfunction transcriptChunkForEvent(event: CliEvent): TranscriptChunk | null {\n  if (event.type === \"text\" && typeof event.content === \"string\") {\n    return { type: \"raw\", text: event.content };\n  }\n  if (event.type === \"error\" && typeof event.content === \"string\") {\n    return { type: \"line\", text: `[error] ${event.content}` };\n  }\n  if (event.type === \"tool_result\" && event.toolResult?.error) {\n    return {\n      type: \"line\",\n      text: `[tool ${event.toolResult.name} error] ${event.toolResult.error}`,\n    };\n  }\n  return null;\n}\n\nfunction appendTranscriptChunk(stdout: string, chunk: TranscriptChunk): string {\n  if (chunk.type === \"raw\") return chunk.text;\n  return lineChunk(stdout, chunk.text);\n}\n\nfunction lineChunk(stdout: string, text: string): string {\n  const prefix = stdout.length > 0 && !stdout.endsWith(\"\\n\") ? \"\\n\" : \"\";\n  const suffix = text.endsWith(\"\\n\") ? \"\" : \"\\n\";\n  return `${prefix}${text}${suffix}`;\n}\n\nexport type StopReason = \"sentinel\" | \"max_iterations\" | \"error\" | \"aborted\";\n\nexport interface IterationError {\n  code: string;\n  message: string;\n  raw: string;\n}\n\nexport interface IterationResult {\n  number: number;\n  exitCode: number;\n  sentinelDetected: boolean;\n  stdout: string;\n  startedAt: string;\n  durationMs: number;\n  tokensIn: number | null;\n  tokensOut: number | null;\n  error: IterationError | null;\n}\n\nexport interface LoopResult {\n  iterations: IterationResult[];\n  stopReason: StopReason;\n}\n\nexport interface LoopOptions {\n  cli: CliName;\n  prompt: string;\n  cwd: string;\n  model?: string;\n  maxIterations?: number;\n  sentinel?: string;\n  vars?: Record<string, string>;\n  sessionId?: string;\n  signal?: AbortSignal;\n  startIteration?: number;\n  autoApprove?: boolean;\n  onOutput?: (chunk: string) => void;\n}\n\nexport type Spawner = (options: SpawnOptions) => CliProcess;\n\nexport interface LoopDeps {\n  spawn?: Spawner;\n}\n\nconst DEFAULT_MAX_ITERATIONS = 10;\nconst DEFAULT_SENTINEL = \":::LOOPER_DONE:::\";\n\nexport async function loop(\n  options: LoopOptions,\n  deps: LoopDeps = {},\n): Promise<LoopResult> {\n  const spawnFn = deps.spawn ?? spawnCli;\n  const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;\n  const sentinel = options.sentinel ?? DEFAULT_SENTINEL;\n  const iterations: IterationResult[] = [];\n\n  const startIteration = options.startIteration ?? 1;\n  for (let number = startIteration; number <= maxIterations; number++) {\n    if (options.signal?.aborted) {\n      return { iterations, stopReason: \"aborted\" };\n    }\n    const prompt = substitute(\n      options.prompt,\n      buildVars(number, maxIterations, options.sessionId, options.vars),\n    );\n    const iteration = await runIteration(\n      spawnFn,\n      {\n        cli: options.cli,\n        prompt,\n        cwd: options.cwd,\n        model: options.model,\n        autoApprove: options.autoApprove ?? true,\n      },\n      {\n        sentinel,\n        number,\n        onOutput: options.onOutput,\n        signal: options.signal,\n      },\n    );\n    iterations.push(iteration);\n    if (options.signal?.aborted) {\n      return { iterations, stopReason: \"aborted\" };\n    }\n    if (iteration.sentinelDetected) {\n      return { iterations, stopReason: \"sentinel\" };\n    }\n    if (iteration.exitCode !== 0) {\n      return { iterations, stopReason: \"error\" };\n    }\n  }\n  return { iterations, stopReason: \"max_iterations\" };\n}\n\ninterface IterationContext {\n  sentinel: string;\n  number: number;\n  onOutput?: (chunk: string) => void;\n  signal?: AbortSignal;\n}\n\nfunction buildVars(\n  iteration: number,\n  maxIterations: number,\n  sessionId: string | undefined,\n  userVars: Record<string, string> | undefined,\n): Record<string, string> {\n  const builtIns: Record<string, string> = {\n    ITERATION: String(iteration),\n    MAX_ITERATIONS: String(maxIterations),\n  };\n  if (sessionId !== undefined) builtIns.SESSION_ID = sessionId;\n  return { ...builtIns, ...(userVars ?? {}) };\n}\n\nasync function runIteration(\n  spawnFn: Spawner,\n  spawnOptions: SpawnOptions,\n  ctx: IterationContext,\n): Promise<IterationResult> {\n  const startedAt = new Date().toISOString();\n  const proc = spawnFn(spawnOptions);\n  const onAbort = () => {\n    proc.interrupt().catch(() => {});\n  };\n  if (ctx.signal) {\n    if (ctx.signal.aborted) onAbort();\n    else ctx.signal.addEventListener(\"abort\", onAbort, { once: true });\n  }\n  try {\n    let stdout = \"\";\n    let sentinelDetected = false;\n    for await (const event of proc.events) {\n      const transcriptChunk = transcriptChunkForEvent(event);\n      if (transcriptChunk === null) continue;\n      const chunk = appendTranscriptChunk(stdout, transcriptChunk);\n      stdout += chunk;\n      ctx.onOutput?.(chunk);\n      if (!sentinelDetected && stdout.includes(ctx.sentinel)) {\n        sentinelDetected = true;\n      }\n    }\n    const result = await proc.done;\n    const error: IterationError | null = result.error\n      ? {\n          code: result.error.code,\n          message: result.error.message,\n          raw: result.error.raw,\n        }\n      : null;\n    if (error && !stdout.includes(error.message)) {\n      const line = lineChunk(stdout, `[${error.code}] ${error.message}`);\n      stdout += line;\n      ctx.onOutput?.(line);\n    }\n    return {\n      number: ctx.number,\n      exitCode: result.exitCode,\n      sentinelDetected,\n      stdout,\n      startedAt,\n      durationMs: result.durationMs,\n      tokensIn: result.usage?.inputTokens ?? null,\n      tokensOut: result.usage?.outputTokens ?? null,\n      error,\n    };\n  } finally {\n    ctx.signal?.removeEventListener(\"abort\", onAbort);\n  }\n}\n","import { readFile, stat } from \"node:fs/promises\";\nimport type { Readable } from \"node:stream\";\n\nconst PLACEHOLDER = /\\{\\{([A-Za-z0-9_]+)\\}\\}/g;\n\nexport function substitute(\n  template: string,\n  vars: Record<string, string>,\n): string {\n  return template.replace(PLACEHOLDER, (match, key: string) => {\n    if (Object.hasOwn(vars, key)) return vars[key] ?? \"\";\n    const upper = key.toUpperCase();\n    if (upper !== key && Object.hasOwn(vars, upper)) return vars[upper] ?? \"\";\n    return match;\n  });\n}\n\nexport interface LoadPromptOptions {\n  value?: string;\n  fromStdin?: boolean;\n  stdin?: Readable;\n}\n\nexport async function loadPrompt(options: LoadPromptOptions): Promise<string> {\n  if (options.fromStdin) {\n    if (!options.stdin)\n      throw new Error(\"stdin stream required when fromStdin is set\");\n    return readStream(options.stdin);\n  }\n  if (typeof options.value !== \"string\") {\n    throw new Error(\"prompt value required when not reading from stdin\");\n  }\n  if (await fileExists(options.value)) {\n    return readFile(options.value, \"utf8\");\n  }\n  return options.value;\n}\n\nexport async function readStream(stream: Readable): Promise<string> {\n  const chunks: string[] = [];\n  stream.setEncoding(\"utf8\");\n  for await (const chunk of stream) {\n    chunks.push(typeof chunk === \"string\" ? chunk : String(chunk));\n  }\n  return chunks.join(\"\");\n}\n\nasync function fileExists(candidate: string): Promise<boolean> {\n  try {\n    const info = await stat(candidate);\n    return info.isFile();\n  } catch {\n    return false;\n  }\n}\n"],"mappings":"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","names":["spawnCli"]}