Skip to content

Commit 3f5392f

Browse files
committed
feat(cli): add idea command and ideas-list option for queue management
Add two new CLI features for managing the ideas queue: 1. **opencoder idea <description>** command: - Creates a new .md file in .opencode/opencoder/ideas/ - Generates timestamped filename (YYYYMMDD_HHMMSS_slug.md) - Pre-fills with markdown template for easy editing - Supports --project option for multi-project use 2. **--ideas-list** option: - Lists all ideas currently in the queue - Shows filename and summary (first line) - Displays queue location when empty - Helps users track pending work These features make it easy to queue up tasks for opencoder to work on autonomously without manually creating files. Examples: opencoder idea "Fix login bug" opencoder idea "Add dark mode" -p ./myproject opencoder --ideas-list Signed-off-by: leocavalcante <leo@cavalcante.dev>
1 parent 73777df commit 3f5392f

File tree

3 files changed

+141
-2
lines changed

3 files changed

+141
-2
lines changed

src/cli.ts

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
* CLI argument parsing and program setup
33
*/
44

5-
import { resolve } from "node:path"
5+
import { existsSync, mkdirSync } from "node:fs"
6+
import { join, resolve } from "node:path"
67
import { createInterface } from "node:readline"
78
import { Command } from "commander"
89
import { loadConfig } from "./config.ts"
9-
import { initializePaths } from "./fs.ts"
10+
import { getTimestampForFilename, initializePaths } from "./fs.ts"
11+
import { countIdeas, getIdeaSummary, loadAllIdeas } from "./ideas.ts"
1012
import { runLoop } from "./loop.ts"
1113
import { formatMetricsSummary, loadMetrics, resetMetrics, saveMetrics } from "./metrics.ts"
1214
import type { CliOptions } from "./types.ts"
@@ -23,6 +25,51 @@ export interface ParsedCli {
2325
hint?: string
2426
}
2527

28+
/**
29+
* Handle the 'idea' subcommand: save an idea to the queue
30+
*/
31+
async function handleIdeaCommand(
32+
description: string,
33+
opts: Record<string, unknown>,
34+
): Promise<void> {
35+
try {
36+
const projectDir = opts.project ? resolve(opts.project as string) : process.cwd()
37+
const paths = initializePaths(projectDir)
38+
39+
// Ensure ideas directory exists
40+
if (!existsSync(paths.ideasDir)) {
41+
mkdirSync(paths.ideasDir, { recursive: true })
42+
}
43+
44+
// Generate filename: YYYYMMDD_HHMMSS_slugified-description.md
45+
const timestamp = getTimestampForFilename()
46+
const slug = description
47+
.toLowerCase()
48+
.replace(/[^a-z0-9]+/g, "-")
49+
.replace(/^-+|-+$/g, "")
50+
.slice(0, 50) // Limit slug length
51+
52+
const filename = `${timestamp}_${slug}.md`
53+
const filepath = join(paths.ideasDir, filename)
54+
55+
// Create markdown content
56+
const content = `# ${description}
57+
58+
<!-- Add details, steps, or context here -->
59+
`
60+
61+
// Write the file
62+
await Bun.write(filepath, content)
63+
64+
console.log(`\n✓ Idea added to queue: ${filename}`)
65+
console.log(` Location: ${filepath}`)
66+
console.log(`\nYou can edit this file to add more details before opencoder processes it.\n`)
67+
} catch (err) {
68+
console.error(`Error: Failed to add idea: ${err instanceof Error ? err.message : String(err)}`)
69+
process.exit(1)
70+
}
71+
}
72+
2673
/**
2774
* Create and configure the CLI program
2875
*/
@@ -44,6 +91,28 @@ function createProgram(): Command {
4491
.option("-s, --signoff", "Add Signed-off-by line to commits")
4592
.option("--status", "Display metrics summary and exit")
4693
.option("--metrics-reset", "Reset metrics to default values (requires confirmation)")
94+
.option("--ideas-list", "List all ideas in the queue and exit")
95+
96+
// Add 'idea' subcommand
97+
const ideaCommand = program
98+
.command("idea")
99+
.description("Add a new idea to the queue")
100+
.argument("<description>", "Description of the idea")
101+
.option("-p, --project <dir>", "Project directory (default: current directory)")
102+
.action(async (description: string, opts: Record<string, unknown>) => {
103+
await handleIdeaCommand(description, opts)
104+
})
105+
106+
// Add help text for idea command
107+
ideaCommand.addHelpText(
108+
"after",
109+
`
110+
Examples:
111+
$ opencoder idea "Fix login bug"
112+
$ opencoder idea "Add dark mode support" -p ./myproject
113+
$ opencoder idea "Implement user authentication with JWT tokens"
114+
`,
115+
)
47116

48117
// Add examples to help
49118
program.addHelpText(
@@ -80,6 +149,21 @@ Examples:
80149
$ opencoder --metrics-reset
81150
Reset metrics to default values (with confirmation)
82151
152+
$ opencoder --ideas-list
153+
List all ideas in the queue without starting the loop
154+
155+
$ opencoder --ideas-list -p ./myproject
156+
List ideas for a specific project
157+
158+
$ opencoder idea "Fix login bug"
159+
Add a new idea to the queue
160+
161+
$ opencoder idea "Add dark mode support" -p ./myproject
162+
Add idea to a specific project
163+
164+
Commands:
165+
idea <description> Add a new idea to the queue
166+
83167
Options:
84168
-p, --project <dir> Project directory (default: current directory)
85169
-m, --model <model> Model for both plan and build
@@ -92,6 +176,7 @@ Options:
92176
-s, --signoff Add Signed-off-by line to commits
93177
--status Display metrics summary and exit
94178
--metrics-reset Reset metrics to default values (requires confirmation)
179+
--ideas-list List all ideas in the queue and exit
95180
96181
Environment variables:
97182
OPENCODER_PLAN_MODEL Default plan model
@@ -175,6 +260,7 @@ export function parseCli(argv: string[] = process.argv): ParsedCli {
175260
commitSignoff: opts.signoff as boolean | undefined,
176261
status: opts.status as boolean | undefined,
177262
metricsReset: opts.metricsReset as boolean | undefined,
263+
ideasList: opts.ideasList as boolean | undefined,
178264
},
179265
hint: args[0],
180266
}
@@ -213,6 +299,7 @@ export async function run(): Promise<void> {
213299
commitSignoff: opts.signoff as boolean | undefined,
214300
status: opts.status as boolean | undefined,
215301
metricsReset: opts.metricsReset as boolean | undefined,
302+
ideasList: opts.ideasList as boolean | undefined,
216303
}
217304

218305
// Handle --version flag: display version info and exit
@@ -266,6 +353,36 @@ export async function run(): Promise<void> {
266353
return
267354
}
268355

356+
// Handle --ideas-list flag: display ideas queue and exit
357+
if (cliOptions.ideasList) {
358+
const projectDir = cliOptions.project ? resolve(cliOptions.project) : process.cwd()
359+
const paths = initializePaths(projectDir)
360+
361+
const ideas = await loadAllIdeas(paths.ideasDir)
362+
const count = await countIdeas(paths.ideasDir)
363+
364+
console.log("\nIdeas Queue")
365+
console.log("===========\n")
366+
367+
if (count === 0) {
368+
console.log("No ideas in queue.")
369+
console.log(`\nTo add ideas, place .md files in: ${paths.ideasDir}`)
370+
} else {
371+
console.log(`Found ${count} idea(s):\n`)
372+
for (let i = 0; i < ideas.length; i++) {
373+
const idea = ideas[i]
374+
if (!idea) continue
375+
const summary = getIdeaSummary(idea.content)
376+
console.log(` ${i + 1}. ${idea.filename}`)
377+
console.log(` ${summary}`)
378+
console.log("")
379+
}
380+
}
381+
382+
console.log("")
383+
return
384+
}
385+
269386
const config = await loadConfig(cliOptions, hint)
270387
await runLoop(config)
271388
} catch (err) {

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,5 @@ export interface CliOptions {
204204
commitSignoff?: boolean
205205
status?: boolean
206206
metricsReset?: boolean
207+
ideasList?: boolean
207208
}

tests/cli.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,27 @@ describe("parseCli", () => {
227227
})
228228
})
229229

230+
describe("ideas-list option", () => {
231+
test("parses --ideas-list flag", () => {
232+
const result = parseCli(argv("--ideas-list"))
233+
234+
expect(result.options.ideasList).toBe(true)
235+
})
236+
237+
test("parses --ideas-list with project option", () => {
238+
const result = parseCli(argv("--ideas-list", "-p", "./myproject"))
239+
240+
expect(result.options.ideasList).toBe(true)
241+
expect(result.options.project).toBe("./myproject")
242+
})
243+
244+
test("ideasList is undefined when not provided", () => {
245+
const result = parseCli(argv("-m", "anthropic/claude-sonnet-4"))
246+
247+
expect(result.options.ideasList).toBeUndefined()
248+
})
249+
})
250+
230251
describe("git options", () => {
231252
test("parses --no-auto-commit flag", () => {
232253
const result = parseCli(argv("--no-auto-commit"))

0 commit comments

Comments
 (0)