@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 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: OpenCode Loop 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 loop cannot continue in the background. Use an external process manager or scheduled `opencode run` script for true daemon behavior.
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.4",
4
- "description": "Claude Code style auto-continue /loop command for OpenCode: autonomous loops, compact scheduling, progress.md TODO automation, verification, checkpoints, and safe long-running coding sessions.",
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
+ }