Skip to content

Commit e7f7db3

Browse files
that-github-userunknownclaude
authored
Fix -n flag ignored by adding --file prompt input and stdin support (#90)
The -n flag was consumed by shell expansion when prompts contained special characters. Fix: make prompt optional, add --file flag to read from file, and support stdin piping. resolvePrompt() utility handles all three input methods with clear error messages. Generated by thinktank Opus (5 agents, all pass, Agent #1 recommended). Closes #87 Co-authored-by: unknown <that-github-user@github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8d45ad3 commit e7f7db3

File tree

3 files changed

+111
-2
lines changed

3 files changed

+111
-2
lines changed

src/cli.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { compare } from "./commands/compare.js";
66
import { list } from "./commands/list.js";
77
import { run } from "./commands/run.js";
88
import { stats } from "./commands/stats.js";
9+
import { resolvePrompt } from "./utils/prompt.js";
910

1011
const program = new Command();
1112

@@ -19,14 +20,17 @@ program
1920
program
2021
.command("run")
2122
.description("Run a task with N parallel AI coding agents")
22-
.argument("<prompt>", "The coding task to perform")
23+
.argument("[prompt]", "The coding task to perform")
2324
.option("-n, --attempts <number>", "Number of parallel attempts", "3")
25+
.option("-f, --file <path>", "Read prompt from a file (avoids shell expansion issues)")
2426
.option("-t, --test-cmd <command>", "Test command to verify results (e.g., 'npm test')")
2527
.option("--timeout <seconds>", "Timeout per agent in seconds", "300")
2628
.option("--model <model>", "Claude model to use", "sonnet")
2729
.option("-r, --runner <name>", "AI coding tool to use (default: claude-code)")
2830
.option("--verbose", "Show detailed output from each agent")
29-
.action(async (prompt: string, opts) => {
31+
.action(async (promptArg: string | undefined, opts) => {
32+
const prompt = resolvePrompt(promptArg, opts.file);
33+
3034
const attempts = parseInt(opts.attempts, 10);
3135
if (Number.isNaN(attempts) || attempts < 1 || attempts > 20) {
3236
console.error("Error: --attempts must be a number between 1 and 20");

src/utils/prompt.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import assert from "node:assert/strict";
2+
import { unlinkSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { describe, it } from "node:test";
6+
import { resolvePrompt } from "./prompt.js";
7+
8+
describe("resolvePrompt", () => {
9+
it("returns positional argument when provided", () => {
10+
const result = resolvePrompt("fix the bug", undefined);
11+
assert.equal(result, "fix the bug");
12+
});
13+
14+
it("reads prompt from file via --file", () => {
15+
const tmpFile = join(tmpdir(), `thinktank-test-prompt-${Date.now()}.txt`);
16+
writeFileSync(tmpFile, "prompt from file\nwith newlines\n");
17+
try {
18+
const result = resolvePrompt(undefined, tmpFile);
19+
assert.equal(result, "prompt from file\nwith newlines");
20+
} finally {
21+
unlinkSync(tmpFile);
22+
}
23+
});
24+
25+
it("--file takes priority over positional argument", () => {
26+
const tmpFile = join(tmpdir(), `thinktank-test-prompt-${Date.now()}.txt`);
27+
writeFileSync(tmpFile, "file wins\n");
28+
try {
29+
const result = resolvePrompt("positional", tmpFile);
30+
assert.equal(result, "file wins");
31+
} finally {
32+
unlinkSync(tmpFile);
33+
}
34+
});
35+
36+
it("handles prompt containing flag-like strings via --file", () => {
37+
const tmpFile = join(tmpdir(), `thinktank-test-prompt-${Date.now()}.txt`);
38+
writeFileSync(tmpFile, "Fix the -n parsing bug and --timeout handling\n");
39+
try {
40+
const result = resolvePrompt(undefined, tmpFile);
41+
assert.equal(result, "Fix the -n parsing bug and --timeout handling");
42+
} finally {
43+
unlinkSync(tmpFile);
44+
}
45+
});
46+
47+
it("handles prompt with special characters via --file", () => {
48+
const tmpFile = join(tmpdir(), `thinktank-test-prompt-${Date.now()}.txt`);
49+
const specialPrompt = "Line 1\nLine 2\n---\n- bullet with -n 5\n$VAR `backticks`";
50+
writeFileSync(tmpFile, specialPrompt);
51+
try {
52+
const result = resolvePrompt(undefined, tmpFile);
53+
assert.equal(result, specialPrompt);
54+
} finally {
55+
unlinkSync(tmpFile);
56+
}
57+
});
58+
});

src/utils/prompt.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { readFileSync } from "node:fs";
2+
3+
/**
4+
* Resolve the prompt from positional arg, --file, or stdin (piped).
5+
* Exits with a helpful error when no prompt source is found.
6+
*/
7+
export function resolvePrompt(
8+
positionalArg: string | undefined,
9+
filePath: string | undefined,
10+
): string {
11+
if (filePath) {
12+
try {
13+
const content = readFileSync(filePath, "utf-8").trim();
14+
if (!content) {
15+
console.error(`Error: prompt file is empty: ${filePath}`);
16+
process.exit(1);
17+
}
18+
return content;
19+
} catch (err) {
20+
console.error(`Error: could not read prompt file: ${filePath}`);
21+
console.error((err as Error).message);
22+
process.exit(1);
23+
}
24+
}
25+
26+
if (positionalArg) {
27+
return positionalArg;
28+
}
29+
30+
// Try reading from stdin if it's piped (not a TTY)
31+
if (!process.stdin.isTTY) {
32+
try {
33+
const stdinContent = readFileSync(0, "utf-8").trim();
34+
if (stdinContent) {
35+
return stdinContent;
36+
}
37+
} catch {
38+
// stdin not readable, fall through to error
39+
}
40+
}
41+
42+
console.error("Error: no prompt provided.");
43+
console.error("Usage: thinktank run <prompt>");
44+
console.error(" thinktank run -f <file>");
45+
console.error(" echo 'prompt' | thinktank run");
46+
process.exit(1);
47+
}

0 commit comments

Comments
 (0)