@bybrawe/opencode-loop 0.4.4 → 0.5.1
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 +171 -78
- 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
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
**Claude Code style auto-continue for OpenCode.**
|
|
4
4
|
|
|
5
|
-
OpenCode Loop adds a practical `/loop` command to OpenCode so an agent can keep working after each idle turn instead of waiting for you to type “continue” again.
|
|
5
|
+
OpenCode Loop adds a practical `/loop` command and `opencode-loopd` daemon to OpenCode so an agent can keep working after each idle turn instead of waiting for you to type “continue” again.
|
|
6
|
+
|
|
7
|
+
It is useful for long coding sessions, `progress.md` workflows, TODO automation, test-fix loops, periodic `/compact`, checkpoints, safe autonomous development, and background OpenCode continuation jobs.
|
|
6
8
|
|
|
7
9
|
Repository: **ByBrawe/opencode-loop**
|
|
8
10
|
NPM package name: **@bybrawe/opencode-loop**
|
|
9
11
|
|
|
10
12
|
## Why this exists
|
|
11
13
|
|
|
12
|
-
Claude Code users often rely on a loop-like workflow where the agent finishes one step, then immediately continues with the next step.
|
|
14
|
+
Claude Code users often rely on a loop-like workflow where the agent finishes one step, then immediately continues with the next step.
|
|
15
|
+
|
|
16
|
+
OpenCode is powerful, but long-running autonomous workflows usually need extra control around:
|
|
13
17
|
|
|
14
18
|
- auto-continue after idle
|
|
15
19
|
- compact / summarize scheduling
|
|
@@ -18,6 +22,8 @@ Claude Code users often rely on a loop-like workflow where the agent finishes on
|
|
|
18
22
|
- patch checkpoints
|
|
19
23
|
- maximum runtime limits
|
|
20
24
|
- failure limits
|
|
25
|
+
- background daemon execution
|
|
26
|
+
- Windows Task Scheduler support
|
|
21
27
|
- safety prompts and destructive command guards
|
|
22
28
|
|
|
23
29
|
OpenCode Loop is designed for developers searching for:
|
|
@@ -32,15 +38,22 @@ OpenCode Loop is designed for developers searching for:
|
|
|
32
38
|
- Claude Code style loop for OpenCode
|
|
33
39
|
- autonomous coding loop for OpenCode
|
|
34
40
|
- progress.md TODO automation for OpenCode
|
|
41
|
+
- OpenCode background daemon
|
|
42
|
+
- OpenCode loop daemon
|
|
43
|
+
- OpenCode Windows Task Scheduler loop
|
|
35
44
|
|
|
36
45
|
## Features
|
|
37
46
|
|
|
38
|
-
>
|
|
39
|
-
|
|
47
|
+
> The TUI `/loop` command is session-bound. It runs while OpenCode is open and the session receives idle/status 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.
|
|
40
48
|
|
|
41
49
|
- **Claude Code style auto-continue** with `/loop 0s ...`.
|
|
50
|
+
- **TUI loop mode** with `/loop`, active while OpenCode is open.
|
|
51
|
+
- **Background daemon mode** with `opencode-loopd` for long-running loops outside the OpenCode TUI.
|
|
52
|
+
- **Windows Task Scheduler helper** with `opencode-loopd install-task`.
|
|
53
|
+
- **One-shot scheduled runs** for periodic continuation jobs.
|
|
42
54
|
- **Interval loops** for prompts, slash commands, and shell commands.
|
|
43
55
|
- **Prompt-file support** with `--prompt-file loop-prompt.md` for long reusable instructions.
|
|
56
|
+
- **Prompt-file daemon support** with `opencode-loopd --prompt-file loop-prompt.md`.
|
|
44
57
|
- **Include extra context files** with `--include-file`.
|
|
45
58
|
- **progress.md workflow** with `--progress-file progress.md`.
|
|
46
59
|
- **Large TODO support** with `--batch`.
|
|
@@ -50,6 +63,8 @@ OpenCode Loop is designed for developers searching for:
|
|
|
50
63
|
- **Post-run commands** with `--postrun`.
|
|
51
64
|
- **Failure control** with `--max-failures` and `--pause-on-verify-fail`.
|
|
52
65
|
- **Runtime control** with `--max-runtime 6h`.
|
|
66
|
+
- **Run limit support** with `--max-runs`.
|
|
67
|
+
- **Sleep-before-first-run support** with `--sleep-first`.
|
|
53
68
|
- **Checkpoints** with `--checkpoint-only` or `--git-checkpoint`.
|
|
54
69
|
- **Safe mode** with `--safe` and prompt-level destructive command warnings.
|
|
55
70
|
- **Branch setup** with `--branch ai-loop`.
|
|
@@ -59,38 +74,6 @@ OpenCode Loop is designed for developers searching for:
|
|
|
59
74
|
- **Starter progress file** with `/loop-init`.
|
|
60
75
|
- **State export** with `/loop-export`.
|
|
61
76
|
|
|
62
|
-
## Quick start
|
|
63
|
-
|
|
64
|
-
```text
|
|
65
|
-
/loop 0s continue from progress.md and implement the next unfinished TODO
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
This is the closest behavior to a Claude Code CLI style loop: every time OpenCode becomes idle, the loop can send the next continuation prompt.
|
|
69
|
-
|
|
70
|
-
A safer development loop:
|
|
71
|
-
|
|
72
|
-
```text
|
|
73
|
-
/loop 0s --name dev --ask-never --safe --no-overlap --batch 5 --compact-every 200m --checkpoint-only --progress-file progress.md Treat progress.md as the main project state file. Continue with the next unfinished TODO, implement it, mark completed items with [x], add useful follow-up TODOs when you discover them, run tests/lint/build when available, and keep going while work remains.
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Periodic compact:
|
|
77
|
-
|
|
78
|
-
```text
|
|
79
|
-
/loop 200m --name compact --no-now /compact
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Test-fix loop:
|
|
83
|
-
|
|
84
|
-
```text
|
|
85
|
-
/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
|
-
|
|
88
|
-
Shell command loop:
|
|
89
|
-
|
|
90
|
-
```text
|
|
91
|
-
/loop 10m --name tests --safe !npm test
|
|
92
|
-
```
|
|
93
|
-
|
|
94
77
|
## Installation
|
|
95
78
|
|
|
96
79
|
### Recommended: install from npm
|
|
@@ -136,9 +119,20 @@ Then fully restart OpenCode and run:
|
|
|
136
119
|
/loop-doctor
|
|
137
120
|
```
|
|
138
121
|
|
|
122
|
+
The npm package also installs the `opencode-loopd` CLI for background loops:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
opencode-loopd --help
|
|
126
|
+
```
|
|
127
|
+
|
|
139
128
|
### Why `npx` is the recommended npm install
|
|
140
129
|
|
|
141
|
-
OpenCode can load npm plugins from the `plugin` array in `opencode.json`, but OpenCode slash commands are discovered from command definitions such as markdown files in a `commands/` directory or command entries in config.
|
|
130
|
+
OpenCode can load npm plugins from the `plugin` array in `opencode.json`, but OpenCode slash commands are discovered from command definitions such as markdown files in a `commands/` directory or command entries in config.
|
|
131
|
+
|
|
132
|
+
The `npx` installer installs both parts:
|
|
133
|
+
|
|
134
|
+
- the OpenCode plugin file
|
|
135
|
+
- the `/loop-*` command markdown files
|
|
142
136
|
|
|
143
137
|
Use the OpenCode config-only method only if you already installed the command files separately or you are only testing plugin loading.
|
|
144
138
|
|
|
@@ -153,7 +147,9 @@ If you want OpenCode to load the npm plugin package directly, add the scoped pac
|
|
|
153
147
|
}
|
|
154
148
|
```
|
|
155
149
|
|
|
156
|
-
Use the scoped package name exactly as shown.
|
|
150
|
+
Use the scoped package name exactly as shown.
|
|
151
|
+
|
|
152
|
+
`opencode-loop` without `@bybrawe/` is a different npm package name.
|
|
157
153
|
|
|
158
154
|
If `/loop` does not appear after using only the config method, run the installer once:
|
|
159
155
|
|
|
@@ -227,52 +223,132 @@ copy .\src\index.js .opencode\plugins\opencode-loop.js
|
|
|
227
223
|
copy .\commands\*.md .opencode\commands\
|
|
228
224
|
```
|
|
229
225
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
### Verify installation
|
|
227
|
+
|
|
228
|
+
After restarting OpenCode, run:
|
|
229
|
+
|
|
230
|
+
```text
|
|
231
|
+
/loop-help
|
|
232
|
+
/loop-doctor
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
If the commands do not appear:
|
|
236
|
+
|
|
237
|
+
1. Make sure OpenCode was fully restarted.
|
|
238
|
+
2. Check that `opencode-loop.js` exists in the OpenCode plugin directory.
|
|
239
|
+
3. Check that `loop.md`, `loop-help.md`, and the other command files exist in the OpenCode commands directory.
|
|
240
|
+
4. Run `npx -y @bybrawe/opencode-loop` again to reinstall the command files.
|
|
241
|
+
|
|
242
|
+
## Quick start
|
|
243
|
+
|
|
244
|
+
```text
|
|
245
|
+
/loop 0s continue from progress.md and implement the next unfinished TODO
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This is the closest behavior to a Claude Code CLI style loop: every time OpenCode becomes idle, the loop can send the next continuation prompt.
|
|
249
|
+
|
|
250
|
+
A safer development loop:
|
|
251
|
+
|
|
252
|
+
```text
|
|
253
|
+
/loop 0s --name dev --ask-never --safe --no-overlap --batch 5 --compact-every 200m --checkpoint-only --progress-file progress.md Treat progress.md as the main project state file. Continue with the next unfinished TODO, implement it, mark completed items with [x], add useful follow-up TODOs when you discover them, run tests/lint/build when available, and keep going while work remains.
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Periodic compact:
|
|
233
257
|
|
|
234
|
-
|
|
258
|
+
```text
|
|
259
|
+
/loop 200m --name compact --no-now /compact
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Test-fix loop:
|
|
263
|
+
|
|
264
|
+
```text
|
|
265
|
+
/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.
|
|
266
|
+
```
|
|
235
267
|
|
|
236
|
-
|
|
268
|
+
Shell command loop:
|
|
237
269
|
|
|
238
270
|
```text
|
|
239
|
-
|
|
271
|
+
/loop 10m --name tests --safe !npm test
|
|
240
272
|
```
|
|
241
273
|
|
|
242
|
-
|
|
274
|
+
## Background daemon
|
|
275
|
+
|
|
276
|
+
The `/loop` command is session-bound. It works while OpenCode is open and the current session emits idle/status events.
|
|
277
|
+
|
|
278
|
+
If you close OpenCode, restart the terminal, lose connection, or your PC sleeps, the TUI loop will not keep running.
|
|
279
|
+
|
|
280
|
+
For long-running loops, use the daemon:
|
|
243
281
|
|
|
244
282
|
```bash
|
|
245
|
-
|
|
283
|
+
opencode-loopd --project . --every 5m --prompt-file loop-prompt.md
|
|
246
284
|
```
|
|
247
285
|
|
|
248
|
-
|
|
286
|
+
Run immediately after each OpenCode turn:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
opencode-loopd --project . --every 0s --prompt "continue from progress.md and implement the next unfinished TODO"
|
|
290
|
+
```
|
|
249
291
|
|
|
250
|
-
|
|
292
|
+
Limit runs:
|
|
251
293
|
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
"$schema": "https://opencode.ai/config.json",
|
|
255
|
-
"plugin": ["@bybrawe/opencode-loop"]
|
|
256
|
-
}
|
|
294
|
+
```bash
|
|
295
|
+
opencode-loopd --project . --every 5m --max-runs 20 --prompt-file loop-prompt.md
|
|
257
296
|
```
|
|
258
297
|
|
|
298
|
+
Wait before the first run:
|
|
259
299
|
|
|
260
|
-
|
|
261
|
-
|
|
300
|
+
```bash
|
|
301
|
+
opencode-loopd --project . --every 10m --sleep-first --prompt-file loop-prompt.md
|
|
302
|
+
```
|
|
262
303
|
|
|
263
|
-
|
|
304
|
+
Use an inline prompt:
|
|
264
305
|
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
/loop-doctor
|
|
306
|
+
```bash
|
|
307
|
+
opencode-loopd --project . --every 0s --prompt "continue from progress.md and implement the next unfinished TODO"
|
|
268
308
|
```
|
|
269
309
|
|
|
270
|
-
|
|
310
|
+
Example `loop-prompt.md`:
|
|
271
311
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
312
|
+
```md
|
|
313
|
+
Continue from progress.md and implement the next unfinished TODO.
|
|
314
|
+
|
|
315
|
+
Rules:
|
|
316
|
+
- Do not ask questions.
|
|
317
|
+
- Make reasonable assumptions.
|
|
318
|
+
- Mark completed TODO items with [x].
|
|
319
|
+
- Add useful follow-up TODOs when needed.
|
|
320
|
+
- Run tests/lint/build when available.
|
|
321
|
+
- Do not run destructive commands such as git reset, git clean, rm -rf, force push, deploy, or production migrations.
|
|
322
|
+
- Keep going while work remains.
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Windows Task Scheduler
|
|
326
|
+
|
|
327
|
+
You can create a Windows scheduled task that runs a one-shot daemon job every N minutes.
|
|
328
|
+
|
|
329
|
+
Install a scheduled task:
|
|
330
|
+
|
|
331
|
+
```powershell
|
|
332
|
+
opencode-loopd install-task --project "C:\path\to\project" --every 10m --prompt-file loop-prompt.md --name OpenCodeLoop
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Remove it:
|
|
336
|
+
|
|
337
|
+
```powershell
|
|
338
|
+
opencode-loopd uninstall-task --name OpenCodeLoop
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Check existing tasks:
|
|
342
|
+
|
|
343
|
+
```powershell
|
|
344
|
+
Get-ScheduledTask | Where-Object { $_.TaskName -like "*OpenCode*" }
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
For active development, a visible terminal running daemon mode is usually easier to monitor:
|
|
348
|
+
|
|
349
|
+
```powershell
|
|
350
|
+
opencode-loopd --project "C:\path\to\project" --every 0s --prompt-file loop-prompt.md
|
|
351
|
+
```
|
|
276
352
|
|
|
277
353
|
## Multiple loops and duplicate protection
|
|
278
354
|
|
|
@@ -614,6 +690,12 @@ By default a new loop is due immediately. Use `--no-now` to wait for the first i
|
|
|
614
690
|
/loop 0s --name dev --ask-never --safe --no-overlap --compact-every 20 --timeout 45m --max-runtime 6h --max-failures 3 --stop-file STOP_LOOP --checkpoint-only Continue from progress.md. Do not ask questions. Make reasonable assumptions. Complete TODOs in order, mark finished items with [x], add useful new ideas to progress.md, and keep going while work remains.
|
|
615
691
|
```
|
|
616
692
|
|
|
693
|
+
### Daemon-based long-running development loop
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
opencode-loopd --project . --every 5m --prompt-file loop-prompt.md
|
|
697
|
+
```
|
|
698
|
+
|
|
617
699
|
### Test-fix loop
|
|
618
700
|
|
|
619
701
|
```text
|
|
@@ -727,23 +809,34 @@ Improve the application in small safe steps.
|
|
|
727
809
|
- None.
|
|
728
810
|
```
|
|
729
811
|
|
|
730
|
-
##
|
|
812
|
+
## Changelog highlights
|
|
813
|
+
|
|
814
|
+
### v0.5.1
|
|
815
|
+
|
|
816
|
+
- Cleaned up README installation docs.
|
|
817
|
+
- Removed duplicate npm install section.
|
|
818
|
+
- Removed unresolved merge conflict markers.
|
|
819
|
+
- Added clearer daemon mode documentation.
|
|
820
|
+
- Added Windows Task Scheduler examples.
|
|
821
|
+
- Added daemon usage examples for `--every`, `--max-runs`, `--sleep-first`, and `--prompt-file`.
|
|
822
|
+
|
|
823
|
+
### v0.5.0
|
|
824
|
+
|
|
825
|
+
- Added `opencode-loopd` background daemon.
|
|
826
|
+
- Added Windows Task Scheduler helper.
|
|
827
|
+
- Documented TUI loop vs daemon loop.
|
|
828
|
+
- Added background long-running loop examples.
|
|
829
|
+
|
|
830
|
+
### v0.4.3
|
|
731
831
|
|
|
732
|
-
-
|
|
733
|
-
-
|
|
734
|
-
- Added `--prompt-file` for long prompts instead of huge command lines.
|
|
735
|
-
- Added `--include-file` for extra context files.
|
|
736
|
-
- Added `--max-runtime` so loops can stop after a total runtime.
|
|
737
|
-
- Added `--max-failures` and `--pause-on-verify-fail` for failing verification loops.
|
|
738
|
-
- Added `--postrun` and `--notify` hooks.
|
|
739
|
-
- Added `/loop-doctor`, `/loop-init`, and `/loop-export`.
|
|
740
|
-
- Fixed the max-runs/checkpoint ordering issue so the last run can still finalize and checkpoint.
|
|
741
|
-
- Improved no-overlap behavior and state cleanup for non-assistant actions such as `/compact`.
|
|
742
|
-
- Kept all README examples in English for public GitHub discovery.
|
|
832
|
+
- Added duplicate loop protection.
|
|
833
|
+
- Quieted command markdown files.
|
|
743
834
|
|
|
744
835
|
## Notes and limits
|
|
745
836
|
|
|
746
|
-
- The plugin is idle-driven. It does not run a background daemon while OpenCode is busy.
|
|
837
|
+
- The TUI plugin is idle-driven. It does not run a background daemon while OpenCode is busy.
|
|
838
|
+
- The TUI `/loop` command stops when OpenCode closes, the terminal closes, the machine sleeps, or the session stops emitting idle events.
|
|
839
|
+
- For long-running work outside the TUI, use `opencode-loopd`.
|
|
747
840
|
- `--timeout` is best-effort and relies on OpenCode's abort API.
|
|
748
841
|
- `--verify`, `--preflight`, `--postrun`, and `--notify` run shell commands, so configure OpenCode permissions carefully.
|
|
749
842
|
- `--until` scans common state files and a limited number of markdown/text/json/yaml files to avoid walking huge projects.
|
|
@@ -752,4 +845,4 @@ Improve the application in small safe steps.
|
|
|
752
845
|
|
|
753
846
|
## License
|
|
754
847
|
|
|
755
|
-
MIT
|
|
848
|
+
MIT
|
|
@@ -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.1",
|
|
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
|
+
}
|