@bybrawe/opencode-loop 0.4.4 → 0.5.0
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/CHANGELOG.md +8 -0
- package/README.md +69 -6
- package/examples/loop-prompt.md +10 -0
- package/package.json +9 -5
- package/scripts/loopd.mjs +205 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
- Added `opencode-loopd` background daemon for long-running loops outside the OpenCode TUI.
|
|
6
|
+
- Added `opencode-loopd install-task` and `opencode-loopd uninstall-task` helpers for Windows Task Scheduler.
|
|
7
|
+
- Updated README to clearly explain the difference between session-bound TUI loops and background daemon loops.
|
|
8
|
+
- Added npm bin entry for `opencode-loopd`.
|
|
9
|
+
- Updated package check script to validate the daemon script.
|
|
10
|
+
|
|
3
11
|
## 0.4.4
|
|
4
12
|
|
|
5
13
|
- Made the npm install path the primary README installation method now that the package is published as `@bybrawe/opencode-loop`.
|
package/README.md
CHANGED
|
@@ -35,10 +35,11 @@ OpenCode Loop is designed for developers searching for:
|
|
|
35
35
|
|
|
36
36
|
## Features
|
|
37
37
|
|
|
38
|
-
> Note:
|
|
38
|
+
> Note: The TUI `/loop` command is session-bound. It runs while OpenCode is open and the session receives idle events. If the terminal closes, the machine sleeps, the process is killed, or the provider connection is lost for a long time, the TUI loop cannot continue in the background. For long-running loops, use `opencode-loopd` daemon mode.
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
- **Claude Code style auto-continue** with `/loop 0s ...`.
|
|
41
|
+
- **Claude Code style auto-continue** with `/loop 0s ...`.
|
|
42
|
+
- **Background daemon mode** with `opencode-loopd` for long-running loops outside the OpenCode TUI.
|
|
42
43
|
- **Interval loops** for prompts, slash commands, and shell commands.
|
|
43
44
|
- **Prompt-file support** with `--prompt-file loop-prompt.md` for long reusable instructions.
|
|
44
45
|
- **Include extra context files** with `--include-file`.
|
|
@@ -85,11 +86,67 @@ Test-fix loop:
|
|
|
85
86
|
/loop 0s --name testfix --ask-never --safe --verify "npm test" Continue from progress.md. If tests fail, analyze the failure, fix it, and run the tests again.
|
|
86
87
|
```
|
|
87
88
|
|
|
88
|
-
Shell command loop:
|
|
89
|
+
Shell command loop:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
/loop 10m --name tests --safe !npm test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Background daemon
|
|
96
|
+
|
|
97
|
+
The `/loop` command is session-bound. It works while OpenCode is open and the current session emits idle/status events.
|
|
98
|
+
|
|
99
|
+
If you close OpenCode, restart the terminal, lose connection, or your PC sleeps, the TUI loop will not keep running.
|
|
100
|
+
|
|
101
|
+
For long-running loops, use the daemon:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
opencode-loopd --project . --every 5m --prompt-file loop-prompt.md
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Run immediately after each OpenCode turn:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
opencode-loopd --project . --every 0s --prompt "continue from progress.md and implement the next unfinished TODO"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Limit runs:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
opencode-loopd --project . --every 5m --max-runs 20 --prompt-file loop-prompt.md
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Example `loop-prompt.md`:
|
|
120
|
+
|
|
121
|
+
```md
|
|
122
|
+
Continue from progress.md and implement the next unfinished TODO.
|
|
123
|
+
|
|
124
|
+
Rules:
|
|
125
|
+
- Do not ask questions.
|
|
126
|
+
- Make reasonable assumptions.
|
|
127
|
+
- Mark completed TODO items with [x].
|
|
128
|
+
- Add useful follow-up TODOs when needed.
|
|
129
|
+
- Run tests/lint/build when available.
|
|
130
|
+
- Do not run destructive commands such as git reset, git clean, rm -rf, force push, deploy, or production migrations.
|
|
131
|
+
- Keep going while work remains.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Windows Task Scheduler
|
|
135
|
+
|
|
136
|
+
You can create a Windows scheduled task that runs a one-shot daemon job every N minutes:
|
|
137
|
+
|
|
138
|
+
```powershell
|
|
139
|
+
opencode-loopd install-task --project "C:\path\to\project" --every 10m --prompt-file loop-prompt.md --name OpenCodeLoop
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Remove it:
|
|
143
|
+
|
|
144
|
+
```powershell
|
|
145
|
+
opencode-loopd uninstall-task --name OpenCodeLoop
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
For active development, a visible terminal running `opencode-loopd --project . --every 0s ...` is easier to monitor.
|
|
89
149
|
|
|
90
|
-
```text
|
|
91
|
-
/loop 10m --name tests --safe !npm test
|
|
92
|
-
```
|
|
93
150
|
|
|
94
151
|
## Installation
|
|
95
152
|
|
|
@@ -134,6 +191,12 @@ Then fully restart OpenCode and run:
|
|
|
134
191
|
```text
|
|
135
192
|
/loop-help
|
|
136
193
|
/loop-doctor
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The npm package also installs the `opencode-loopd` CLI for background loops:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
opencode-loopd --help
|
|
137
200
|
```
|
|
138
201
|
|
|
139
202
|
### Why `npx` is the recommended npm install
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Continue from progress.md and implement the next unfinished TODO.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- Do not ask questions.
|
|
5
|
+
- Make reasonable assumptions.
|
|
6
|
+
- Mark completed TODO items with [x].
|
|
7
|
+
- Add useful follow-up TODOs when needed.
|
|
8
|
+
- Run tests/lint/build when available.
|
|
9
|
+
- Do not run destructive commands such as git reset, git clean, rm -rf, force push, deploy, or production migrations.
|
|
10
|
+
- Keep going while work remains.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bybrawe/opencode-loop",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Claude Code style
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Claude Code style /loop command and background daemon for OpenCode: auto-continue, progress.md TODO automation, compact scheduling, verification, checkpoints, and safe autonomous coding loops.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"CHANGELOG.md"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"check": "node --check src/index.js",
|
|
20
|
+
"check": "node --check src/index.js && node --check scripts/install-node.mjs && node --check scripts/loopd.mjs",
|
|
21
21
|
"install:global": "node scripts/install-node.mjs",
|
|
22
22
|
"pack:zip": "node scripts/make-zip.mjs"
|
|
23
23
|
},
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
"scheduler",
|
|
37
37
|
"progress-md",
|
|
38
38
|
"todo-automation",
|
|
39
|
-
"ralph-loop"
|
|
39
|
+
"ralph-loop",
|
|
40
|
+
"daemon",
|
|
41
|
+
"background-runner",
|
|
42
|
+
"windows-task-scheduler"
|
|
40
43
|
],
|
|
41
44
|
"author": "ByBrawe",
|
|
42
45
|
"license": "MIT",
|
|
@@ -52,6 +55,7 @@
|
|
|
52
55
|
"@opencode-ai/plugin": ">=1.4.0"
|
|
53
56
|
},
|
|
54
57
|
"bin": {
|
|
55
|
-
"opencode-loop": "./scripts/install-node.mjs"
|
|
58
|
+
"opencode-loop": "./scripts/install-node.mjs",
|
|
59
|
+
"opencode-loopd": "./scripts/loopd.mjs"
|
|
56
60
|
}
|
|
57
61
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process"
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2)
|
|
7
|
+
|
|
8
|
+
function arg(name, fallback = null) {
|
|
9
|
+
const i = args.indexOf(name)
|
|
10
|
+
return i >= 0 ? args[i + 1] ?? fallback : fallback
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function has(name) {
|
|
14
|
+
return args.includes(name)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseMs(value) {
|
|
18
|
+
const v = String(value || "0s").trim().toLowerCase()
|
|
19
|
+
if (v === "0" || v === "0s" || v === "now") return 0
|
|
20
|
+
|
|
21
|
+
const m = v.match(/^(\d+(?:\.\d+)?)\s*(ms|s|sec|secs|m|min|mins|h|hr|hrs|d|day|days)$/)
|
|
22
|
+
if (!m) throw new Error(`Invalid duration: ${value}`)
|
|
23
|
+
|
|
24
|
+
const n = Number(m[1])
|
|
25
|
+
const unit = m[2]
|
|
26
|
+
|
|
27
|
+
if (unit === "ms") return n
|
|
28
|
+
if (unit.startsWith("s")) return n * 1000
|
|
29
|
+
if (unit.startsWith("m")) return n * 60_000
|
|
30
|
+
if (unit.startsWith("h")) return n * 3_600_000
|
|
31
|
+
if (unit.startsWith("d")) return n * 86_400_000
|
|
32
|
+
|
|
33
|
+
return n
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sleep(ms) {
|
|
37
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run(command, cwd) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const child = spawn(command, {
|
|
43
|
+
cwd,
|
|
44
|
+
shell: true,
|
|
45
|
+
stdio: "inherit",
|
|
46
|
+
env: process.env,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
child.on("exit", (code) => resolve(code ?? 0))
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readPrompt(project) {
|
|
54
|
+
const promptFile = arg("--prompt-file")
|
|
55
|
+
const promptArg = arg("--prompt")
|
|
56
|
+
|
|
57
|
+
if (promptFile) {
|
|
58
|
+
return fs.readFileSync(path.resolve(project, promptFile), "utf8")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (promptArg) return promptArg
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
"Continue from progress.md and implement the next unfinished TODO.",
|
|
65
|
+
"Do not ask questions.",
|
|
66
|
+
"Make reasonable assumptions.",
|
|
67
|
+
"Mark completed TODO items with [x].",
|
|
68
|
+
"Add useful follow-up TODOs when needed.",
|
|
69
|
+
"Run tests/lint/build when available.",
|
|
70
|
+
"Do not run destructive commands such as git reset, git clean, rm -rf, force push, deploy, or production migrations.",
|
|
71
|
+
"Keep going while work remains.",
|
|
72
|
+
].join(" ")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function daemon() {
|
|
76
|
+
const project = path.resolve(arg("--project", process.cwd()))
|
|
77
|
+
const every = arg("--every", "0s")
|
|
78
|
+
const delay = parseMs(every)
|
|
79
|
+
const maxRuns = Number(arg("--max-runs", "0")) || 0
|
|
80
|
+
const sleepFirst = has("--sleep-first")
|
|
81
|
+
const prompt = readPrompt(project)
|
|
82
|
+
|
|
83
|
+
console.log("OpenCode Loop daemon")
|
|
84
|
+
console.log(`project: ${project}`)
|
|
85
|
+
console.log(`every: ${every}`)
|
|
86
|
+
console.log(`maxRuns: ${maxRuns || "unlimited"}`)
|
|
87
|
+
|
|
88
|
+
let count = 0
|
|
89
|
+
|
|
90
|
+
if (sleepFirst && delay > 0) {
|
|
91
|
+
await sleep(delay)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
while (true) {
|
|
95
|
+
count += 1
|
|
96
|
+
|
|
97
|
+
console.log("")
|
|
98
|
+
console.log(`[opencode-loopd] run #${count} ${new Date().toISOString()}`)
|
|
99
|
+
|
|
100
|
+
const command = `opencode run --continue --prompt ${JSON.stringify(prompt)}`
|
|
101
|
+
const code = await run(command, project)
|
|
102
|
+
|
|
103
|
+
if (code !== 0) {
|
|
104
|
+
console.log(`[opencode-loopd] opencode exited with code ${code}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (maxRuns > 0 && count >= maxRuns) {
|
|
108
|
+
console.log("[opencode-loopd] max runs reached")
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (delay > 0) {
|
|
113
|
+
await sleep(delay)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function installTask() {
|
|
119
|
+
if (process.platform !== "win32") {
|
|
120
|
+
throw new Error("install-task is currently implemented for Windows Task Scheduler only. Use daemon mode on macOS/Linux.")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const project = path.resolve(arg("--project", process.cwd()))
|
|
124
|
+
const every = arg("--every", "10m")
|
|
125
|
+
const minutes = Math.max(1, Math.round(parseMs(every) / 60_000))
|
|
126
|
+
const name = arg("--name", "OpenCodeLoop")
|
|
127
|
+
const promptFile = arg("--prompt-file")
|
|
128
|
+
const promptArg = arg("--prompt")
|
|
129
|
+
const node = process.execPath
|
|
130
|
+
const script = path.resolve(new URL(import.meta.url).pathname)
|
|
131
|
+
|
|
132
|
+
const commandParts = [
|
|
133
|
+
`"${node}"`,
|
|
134
|
+
`"${script}"`,
|
|
135
|
+
"daemon",
|
|
136
|
+
"--project",
|
|
137
|
+
`"${project}"`,
|
|
138
|
+
"--every",
|
|
139
|
+
"0s",
|
|
140
|
+
"--max-runs",
|
|
141
|
+
"1",
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
if (promptFile) commandParts.push("--prompt-file", `"${promptFile}"`)
|
|
145
|
+
if (promptArg) commandParts.push("--prompt", `"${promptArg.replaceAll('"', '\\"')}"`)
|
|
146
|
+
|
|
147
|
+
const taskCommand = commandParts.join(" ")
|
|
148
|
+
const schtasks = `schtasks /Create /F /SC MINUTE /MO ${minutes} /TN "${name}" /TR ${JSON.stringify(taskCommand)}`
|
|
149
|
+
|
|
150
|
+
console.log(schtasks)
|
|
151
|
+
const result = spawnSync(schtasks, { shell: true, stdio: "inherit" })
|
|
152
|
+
process.exit(result.status ?? 0)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function uninstallTask() {
|
|
156
|
+
if (process.platform !== "win32") {
|
|
157
|
+
throw new Error("uninstall-task is currently implemented for Windows Task Scheduler only.")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const name = arg("--name", "OpenCodeLoop")
|
|
161
|
+
const result = spawnSync(`schtasks /Delete /F /TN "${name}"`, { shell: true, stdio: "inherit" })
|
|
162
|
+
process.exit(result.status ?? 0)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function help() {
|
|
166
|
+
console.log(`
|
|
167
|
+
OpenCode Loop daemon
|
|
168
|
+
|
|
169
|
+
Usage:
|
|
170
|
+
opencode-loopd --project . --every 5m --prompt-file loop-prompt.md
|
|
171
|
+
opencode-loopd --project . --every 0s --prompt "continue from progress.md"
|
|
172
|
+
opencode-loopd install-task --project . --every 10m --prompt-file loop-prompt.md --name OpenCodeLoop
|
|
173
|
+
opencode-loopd uninstall-task --name OpenCodeLoop
|
|
174
|
+
|
|
175
|
+
Options:
|
|
176
|
+
--project <path> Project directory
|
|
177
|
+
--every <duration> 0s, 5m, 1h, etc.
|
|
178
|
+
--prompt <text> Prompt text
|
|
179
|
+
--prompt-file <file> Read prompt from file relative to the project
|
|
180
|
+
--max-runs <n> Stop after n runs
|
|
181
|
+
--sleep-first Wait before first run
|
|
182
|
+
`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const command = args[0]
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
if (command === "daemon" || command === "loopd") {
|
|
189
|
+
args.shift()
|
|
190
|
+
await daemon()
|
|
191
|
+
} else if (command === "install-task") {
|
|
192
|
+
args.shift()
|
|
193
|
+
installTask()
|
|
194
|
+
} else if (command === "uninstall-task") {
|
|
195
|
+
args.shift()
|
|
196
|
+
uninstallTask()
|
|
197
|
+
} else if (has("--help") || has("-h") || command === "help") {
|
|
198
|
+
help()
|
|
199
|
+
} else {
|
|
200
|
+
await daemon()
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error(error?.message || error)
|
|
204
|
+
process.exit(1)
|
|
205
|
+
}
|