loop-task 1.4.0 → 1.4.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/README.md +54 -25
- package/dist/board/components/CreateForm.js +7 -1
- package/dist/board/components/TaskBrowser.js +7 -6
- package/dist/board/components/TaskForm.js +0 -2
- package/dist/board/hooks/useTaskKeybindings.js +16 -0
- package/dist/config/constants.js +1 -1
- package/dist/core/loop-controller.js +2 -2
- package/dist/daemon/manager.js +2 -2
- package/dist/daemon/task-manager.js +1 -2
- package/dist/i18n/en.json +4 -4
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
# loop-task
|
|
4
5
|
|
|
5
|
-
**
|
|
6
|
+
**Loop engineering for your terminal. Run any command on a cadence.**
|
|
6
7
|
|
|
7
|
-
`loop-task` is a cross-platform CLI that runs shell commands at human-readable intervals. Create loops in the background, manage them from an interactive TUI board, or run them in the foreground.
|
|
8
|
+
`loop-task` is a cross-platform CLI that runs shell commands at human-readable intervals. Create loops in the background, manage them from an interactive TUI board, or run them in the foreground. It is the **heartbeat** primitive for [loop engineering](#loop-engineering): instead of running a task by hand every time, you schedule it once and let it run.
|
|
8
9
|
|
|
9
10
|
[](https://www.npmjs.com/package/loop-task)
|
|
10
11
|
[](https://www.npmjs.com/package/loop-task)
|
|
@@ -13,6 +14,34 @@
|
|
|
13
14
|
|
|
14
15
|
</div>
|
|
15
16
|
|
|
17
|
+
## Loop engineering
|
|
18
|
+
|
|
19
|
+
**Loop engineering** is designing systems that run work on a cadence instead of triggering each run yourself. A *loop* is a recurring goal: you define a purpose, give it an interval, and let it iterate. It applies to ordinary engineering work just as much as to AI agents: health checks, sync jobs, test watches, data pulls, deploy polls, and report generation are all loops.
|
|
20
|
+
|
|
21
|
+
`loop-task` is that heartbeat as a tiny local primitive. Some examples:
|
|
22
|
+
|
|
23
|
+
<div align="center">
|
|
24
|
+
<img src="https://raw.githubusercontent.com/CKGrafico/opencode-task/refs/heads/main/demo.gif" alt="opencode-task demo" width="700" />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Run the test suite every 30 minutes
|
|
29
|
+
loop-task new 30m -- npm test
|
|
30
|
+
|
|
31
|
+
# Poll a deploy every 10 seconds until you stop it
|
|
32
|
+
loop-task new 10s -- curl -sf https://example.com/health
|
|
33
|
+
|
|
34
|
+
# Re-sync a data export once an hour, scoped to a project
|
|
35
|
+
loop-task new 1h --project etl -- ./scripts/sync.sh
|
|
36
|
+
|
|
37
|
+
# Have a coding agent chip away at a backlog every 30 minutes
|
|
38
|
+
loop-task new 30m -- opencode run "find missing translations and translate them, 3 max"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
No cron files to maintain and no daemon to babysit: loops persist across reboots, run in the background, and you watch them from a terminal board. The idea is described well in Addy Osmani's [Loop Engineering](https://addyosmani.com/blog/loop-engineering/), where scheduled automations are the first of the five pieces of a working loop.
|
|
42
|
+
|
|
43
|
+
> **Stay in control.** A loop running unattended is also a loop failing unattended. Use `--max-runs`, watch the run history on the board, and review what each loop produces. The leverage moves to the loop; the responsibility stays with you.
|
|
44
|
+
|
|
16
45
|
## Quick start
|
|
17
46
|
|
|
18
47
|
```bash
|
|
@@ -32,8 +61,8 @@ npx loop-task new 30m -- npm test
|
|
|
32
61
|
|
|
33
62
|
## Requirements
|
|
34
63
|
|
|
35
|
-
- **Node.js >= 20**
|
|
36
|
-
- **Bun >= 1.2**
|
|
64
|
+
- **Node.js >= 20** - required for all commands
|
|
65
|
+
- **Bun >= 1.2** - required for the interactive board only
|
|
37
66
|
|
|
38
67
|
Install Bun:
|
|
39
68
|
|
|
@@ -47,7 +76,7 @@ npm install -g bun
|
|
|
47
76
|
|
|
48
77
|
### Loops
|
|
49
78
|
|
|
50
|
-
A **loop** is a schedule
|
|
79
|
+
A **loop** is a schedule - it defines *when* something runs. Loops trigger **tasks**.
|
|
51
80
|
|
|
52
81
|
| Field | Description |
|
|
53
82
|
| ----- | ----------- |
|
|
@@ -59,7 +88,7 @@ A **loop** is a schedule — it defines *when* something runs. Loops trigger **t
|
|
|
59
88
|
|
|
60
89
|
### Tasks
|
|
61
90
|
|
|
62
|
-
A **task** is an executable unit
|
|
91
|
+
A **task** is an executable unit - it defines *what* runs. Tasks can chain to other tasks on success or failure.
|
|
63
92
|
|
|
64
93
|
| Field | Description |
|
|
65
94
|
| ----- | ----------- |
|
|
@@ -68,7 +97,7 @@ A **task** is an executable unit — it defines *what* runs. Tasks can chain to
|
|
|
68
97
|
| **On success** | Optional task to run when this one exits with code 0 |
|
|
69
98
|
| **On failure** | Optional task to run when this one exits with a non-zero code |
|
|
70
99
|
|
|
71
|
-
Tasks are reusable
|
|
100
|
+
Tasks are reusable - the same task can be referenced by multiple loops or by other tasks' success/failure chains.
|
|
72
101
|
|
|
73
102
|
### Projects
|
|
74
103
|
|
|
@@ -80,9 +109,9 @@ A **project** is an organizational scope for loops. Every loop belongs to exactl
|
|
|
80
109
|
| **Color** | One of six colors: white, cyan, orange, green, red, yellow |
|
|
81
110
|
|
|
82
111
|
Key behaviors:
|
|
83
|
-
- **Default project**
|
|
84
|
-
- **Color bullets**
|
|
85
|
-
- **Project filter**
|
|
112
|
+
- **Default project** - always present, cannot be renamed or deleted. New loops are assigned here when no other project is selected.
|
|
113
|
+
- **Color bullets** - each loop in the navigator displays a colored bullet (●) matching its project color.
|
|
114
|
+
- **Project filter** - the board shows only loops belonging to the currently active project. The selection persists across sessions via `localStorage`.
|
|
86
115
|
|
|
87
116
|
To use projects from the board:
|
|
88
117
|
- Press `c` to open the **Project selector** (switch between projects)
|
|
@@ -90,12 +119,12 @@ To use projects from the board:
|
|
|
90
119
|
- From the Manage Projects page: `n` creates a new project, `e` renames the selected project, `d` deletes it, `Esc` returns to the board
|
|
91
120
|
|
|
92
121
|
From the CLI:
|
|
93
|
-
- `loop-task project list`
|
|
94
|
-
- `loop-task project new <name> [--color <color>]`
|
|
95
|
-
- `loop-task project rename <id|name> <new-name>`
|
|
96
|
-
- `loop-task project color <id|name> <color>`
|
|
97
|
-
- `loop-task project delete <id|name>`
|
|
98
|
-
- `loop-task new <interval> --project <name> -- <command>`
|
|
122
|
+
- `loop-task project list` - list all projects
|
|
123
|
+
- `loop-task project new <name> [--color <color>]` - create a project
|
|
124
|
+
- `loop-task project rename <id|name> <new-name>` - rename a project
|
|
125
|
+
- `loop-task project color <id|name> <color>` - change a project's color
|
|
126
|
+
- `loop-task project delete <id|name>` - delete a project (loops move to Default)
|
|
127
|
+
- `loop-task new <interval> --project <name> -- <command>` - create a loop assigned to a project
|
|
99
128
|
|
|
100
129
|
Colors can be a name (`white`, `cyan`, `green`, `yellow`, `orange`, `pink`) or a `#rrggbb` hex value.
|
|
101
130
|
|
|
@@ -137,7 +166,7 @@ loop-task new --now 1h -- npm test
|
|
|
137
166
|
# Run up to 5 times, then stop
|
|
138
167
|
loop-task run --max-runs 5 5m -- npm test
|
|
139
168
|
|
|
140
|
-
# Agent workflow
|
|
169
|
+
# Agent workflow - schedule an AI task every 30 minutes
|
|
141
170
|
loop-task new 30m --now -- opencode run "search missing translations and translate them, 3 maximum" --model "opencode/big-pickle"
|
|
142
171
|
# Run in a specific directory
|
|
143
172
|
loop-task new 30m --cwd ./packages/api -- npm test
|
|
@@ -178,8 +207,8 @@ Destructive actions (pause, force run, delete) prompt a confirmation before exec
|
|
|
178
207
|
|
|
179
208
|
### Pause vs Stop
|
|
180
209
|
|
|
181
|
-
- **Pause** (`p`)
|
|
182
|
-
- **Stop** (`s`)
|
|
210
|
+
- **Pause** (`p`) - temporarily halts the loop. Resuming continues the original schedule (e.g., a loop that runs every 6h at :00 paused at 12:00 and resumed at 14:00 will still fire at 16:00).
|
|
211
|
+
- **Stop** (`s`) - halts the loop and clears the schedule. Playing starts a fresh interval from now (e.g., the same loop stopped at 12:00 and played at 14:00 will fire at 20:00).
|
|
183
212
|
|
|
184
213
|
## How it works
|
|
185
214
|
|
|
@@ -192,14 +221,14 @@ loop-task (board) ──IPC──► daemon ──► loop 1 ──► task (com
|
|
|
192
221
|
- The **daemon** is a background process that manages all loops and tasks. It starts automatically when you run `loop-task start` or any command that needs it.
|
|
193
222
|
- The **board** is a terminal UI that connects to the daemon via IPC.
|
|
194
223
|
- **Loops** define schedules and reference tasks. **Tasks** define commands and optional success/failure chains.
|
|
195
|
-
- Loops and tasks **persist to disk**
|
|
224
|
+
- Loops and tasks **persist to disk** - they survive daemon restarts and system reboots. When the daemon starts, it restores all loops and accounts for elapsed time.
|
|
196
225
|
|
|
197
226
|
### Lifecycle
|
|
198
227
|
|
|
199
228
|
1. `loop-task start` or `loop-task new ...` spawns the daemon if not running
|
|
200
229
|
2. The daemon creates a loop and a task, and persists their state to disk
|
|
201
230
|
3. `loop-task` opens the board for interactive management
|
|
202
|
-
4. Closing the board or terminal does **not** stop loops
|
|
231
|
+
4. Closing the board or terminal does **not** stop loops - the daemon keeps running
|
|
203
232
|
5. After a reboot, `loop-task start` restores all persisted loops with correct timing
|
|
204
233
|
|
|
205
234
|
## Supported intervals
|
|
@@ -214,10 +243,10 @@ loop-task (board) ──IPC──► daemon ──► loop 1 ──► task (com
|
|
|
214
243
|
|
|
215
244
|
## Behavior
|
|
216
245
|
|
|
217
|
-
- **No overlapping**
|
|
218
|
-
- **Resilient**
|
|
219
|
-
- **Persistent**
|
|
220
|
-
- **Graceful shutdown**
|
|
246
|
+
- **No overlapping** - waits for the command to finish before starting the next interval
|
|
247
|
+
- **Resilient** - continues looping even if a command exits with a non-zero code
|
|
248
|
+
- **Persistent** - loop and task state is saved after every run; survives restarts
|
|
249
|
+
- **Graceful shutdown** - background loops are daemon-managed; foreground loops finish the current execution on Ctrl+C
|
|
221
250
|
|
|
222
251
|
## Development
|
|
223
252
|
|
|
@@ -63,7 +63,7 @@ export function CreateView(props) {
|
|
|
63
63
|
? createFields.filter((f) => f !== "runNow")
|
|
64
64
|
: [...createFields];
|
|
65
65
|
const filteredFields = visibleFields.filter((f) => {
|
|
66
|
-
if (f === "command"
|
|
66
|
+
if (f === "command")
|
|
67
67
|
return isInline;
|
|
68
68
|
if (f === "taskId")
|
|
69
69
|
return !isInline;
|
|
@@ -202,12 +202,18 @@ export function CreateView(props) {
|
|
|
202
202
|
setError(t("errors.descriptionEmpty"));
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
|
+
const cwd = current.cwd.trim();
|
|
206
|
+
if (cwd && !fs.existsSync(cwd)) {
|
|
207
|
+
setError(t("board.cwdMissing", { cwd }));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
205
210
|
const built = buildLoopOptions(current.interval.trim(), {
|
|
206
211
|
now: props.mode === "create" && current.runNow === "y",
|
|
207
212
|
maxRuns: current.maxRuns.trim() || null,
|
|
208
213
|
verbose: false,
|
|
209
214
|
description: current.description.trim(),
|
|
210
215
|
taskId: current.taskId,
|
|
216
|
+
cwd,
|
|
211
217
|
});
|
|
212
218
|
built.options.projectId = current.project || "default";
|
|
213
219
|
const request = props.mode === "edit" && props.editId
|
|
@@ -72,7 +72,7 @@ export function TaskInspector(props) {
|
|
|
72
72
|
return (_jsx("box", { title: t("board.inspectorTitle"), border: true, style: { backgroundColor: "#0b0b0b" }, children: _jsx("text", { fg: "#9ca3af", children: t("board.inspectorEmpty") }) }));
|
|
73
73
|
}
|
|
74
74
|
const cmd = commandLine(task.command, task.commandArgs);
|
|
75
|
-
return (_jsxs("box", { title: t("board.inspectorTitle"), border: true, style: { flexDirection: "column", backgroundColor: "#0b0b0b" }, children: [_jsxs("text", { children: [_jsx("strong", { children: t("board.fieldId") }), " ", task.id] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.taskLabelName") }), " ", task.name] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldCommand") }), " ", cmd] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.
|
|
75
|
+
return (_jsxs("box", { title: t("board.inspectorTitle"), border: true, style: { flexDirection: "column", backgroundColor: "#0b0b0b" }, children: [_jsxs("text", { children: [_jsx("strong", { children: t("board.fieldId") }), " ", task.id] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.taskLabelName") }), " ", task.name] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldCommand") }), " ", cmd] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.taskLabelOnSuccess") }), " ", task.onSuccessTaskId ?? t("board.taskNone")] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.taskLabelOnFailure") }), " ", task.onFailureTaskId ?? t("board.taskNone")] })] }));
|
|
76
76
|
}
|
|
77
77
|
export function TaskActionButtons(props) {
|
|
78
78
|
const { task, focused, selectedAction, selectable = true, onAction } = props;
|
|
@@ -80,16 +80,17 @@ export function TaskActionButtons(props) {
|
|
|
80
80
|
return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, backgroundColor: "#0b0b0b", justifyContent: "center", alignItems: "center" }, children: _jsx("text", { fg: "#9ca3af", children: t("board.noActions") }) }));
|
|
81
81
|
}
|
|
82
82
|
const allActions = [
|
|
83
|
-
{ key: "select", label: t("board.taskActionSelect") },
|
|
84
|
-
{ key: "edit", label: t("board.taskActionEdit") },
|
|
85
|
-
{ key: "delete", label: t("board.taskActionDelete") },
|
|
83
|
+
{ key: "select", label: t("board.taskActionSelect"), hotkey: "s" },
|
|
84
|
+
{ key: "edit", label: t("board.taskActionEdit"), hotkey: "e" },
|
|
85
|
+
{ key: "delete", label: t("board.taskActionDelete"), hotkey: "d" },
|
|
86
86
|
];
|
|
87
87
|
const actions = selectable ? allActions : allActions.filter((a) => a.key !== "select");
|
|
88
|
-
return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, paddingLeft: 1, backgroundColor: "#0b0b0b", alignItems: "center" }, children: actions.map((action, i) => (_jsx(TaskActionButton, { label: action.label, selected: focused && selectedAction === i, onMouseDown: () => onAction(action.key) }, action.key))) }));
|
|
88
|
+
return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, paddingLeft: 1, backgroundColor: "#0b0b0b", alignItems: "center" }, children: actions.map((action, i) => (_jsx(TaskActionButton, { label: action.label, hotkey: action.hotkey, selected: focused && selectedAction === i, onMouseDown: () => onAction(action.key) }, action.key))) }));
|
|
89
89
|
}
|
|
90
90
|
function TaskActionButton(props) {
|
|
91
91
|
const { isHovered, hoverProps } = useHoverState();
|
|
92
92
|
const bg = props.selected ? "#1e3a8a" : isHovered ? HOVER_BG : undefined;
|
|
93
93
|
const fg = props.selected ? "#ffffff" : isHovered ? "#e5e7eb" : "#9ca3af";
|
|
94
|
-
|
|
94
|
+
const keycapFg = props.selected ? "#93c5fd" : "#6b7280";
|
|
95
|
+
return (_jsxs("box", { onMouseDown: props.onMouseDown, style: { backgroundColor: bg, paddingLeft: 1, paddingRight: 1, marginRight: 1, flexDirection: "row", alignItems: "center" }, ...hoverProps, children: [_jsxs("text", { fg: keycapFg, children: ["[", props.hotkey, "] "] }), _jsx("text", { fg: fg, children: _jsx("strong", { children: props.label }) })] }));
|
|
95
96
|
}
|
|
@@ -102,7 +102,6 @@ export function TaskForm(props) {
|
|
|
102
102
|
name: current.name.trim() || command,
|
|
103
103
|
command,
|
|
104
104
|
commandArgs,
|
|
105
|
-
cwd: props.editTask.cwd,
|
|
106
105
|
onSuccessTaskId,
|
|
107
106
|
onFailureTaskId,
|
|
108
107
|
});
|
|
@@ -115,7 +114,6 @@ export function TaskForm(props) {
|
|
|
115
114
|
name: current.name.trim() || command,
|
|
116
115
|
command,
|
|
117
116
|
commandArgs,
|
|
118
|
-
cwd: "",
|
|
119
117
|
onSuccessTaskId,
|
|
120
118
|
onFailureTaskId,
|
|
121
119
|
});
|
|
@@ -91,10 +91,18 @@ export function useTaskKeybindings(params) {
|
|
|
91
91
|
onTaskAction("edit");
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
|
+
if (name === "d" && tasks[taskSelectedIndex]) {
|
|
95
|
+
onTaskAction("delete");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
94
98
|
if (name === "delete" && tasks[taskSelectedIndex]) {
|
|
95
99
|
onTaskAction("delete");
|
|
96
100
|
return;
|
|
97
101
|
}
|
|
102
|
+
if (selectable && name === "s" && tasks[taskSelectedIndex]) {
|
|
103
|
+
onTaskAction("select");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
98
106
|
}
|
|
99
107
|
if (taskFocusedPanel === "actions") {
|
|
100
108
|
if (name === "up" || name === "k") {
|
|
@@ -109,6 +117,14 @@ export function useTaskKeybindings(params) {
|
|
|
109
117
|
onTaskAction(taskActions[taskSelectedAction] ?? "select");
|
|
110
118
|
return;
|
|
111
119
|
}
|
|
120
|
+
if (name === "d") {
|
|
121
|
+
onTaskAction("delete");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (selectable && name === "s") {
|
|
125
|
+
onTaskAction("select");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
112
128
|
}
|
|
113
129
|
});
|
|
114
130
|
}
|
package/dist/config/constants.js
CHANGED
|
@@ -21,7 +21,7 @@ export const PROJECT_COLORS = {
|
|
|
21
21
|
pink: "#f472b6",
|
|
22
22
|
};
|
|
23
23
|
export const PROJECT_COLOR_KEYS = Object.keys(PROJECT_COLORS);
|
|
24
|
-
// Entity theme colors
|
|
24
|
+
// Entity theme colors - used for header action buttons and view accents
|
|
25
25
|
export const ENTITY_COLORS = {
|
|
26
26
|
loop: "#38bdf8", // cyan/blue
|
|
27
27
|
task: "#a78bfa", // purple
|
|
@@ -309,7 +309,7 @@ export class LoopController extends EventEmitter {
|
|
|
309
309
|
const task = this.options.taskId ? this.taskResolver(this.options.taskId) : null;
|
|
310
310
|
const command = task?.command ?? this.options.command;
|
|
311
311
|
const commandArgs = task?.commandArgs ?? this.options.commandArgs;
|
|
312
|
-
const cwd =
|
|
312
|
+
const cwd = this.options.cwd;
|
|
313
313
|
const result = await executeCommand(command, commandArgs, cwd, this.logStream, AbortSignal.any([signal, this.runAbortController.signal]), this.runCount);
|
|
314
314
|
this.runAbortController = null;
|
|
315
315
|
this.lastExitCode = result.exitCode;
|
|
@@ -354,7 +354,7 @@ export class LoopController extends EventEmitter {
|
|
|
354
354
|
chainGroupId,
|
|
355
355
|
chainName: chainTask.name,
|
|
356
356
|
});
|
|
357
|
-
const chainResult = await executeCommand(chainTask.command, chainTask.commandArgs,
|
|
357
|
+
const chainResult = await executeCommand(chainTask.command, chainTask.commandArgs, this.options.cwd, this.logStream, signal, this.runCount);
|
|
358
358
|
const chainLogSize = fs.existsSync(this.logPath) ? fs.statSync(this.logPath).size - chainOffset : 0;
|
|
359
359
|
const chainRecord = this.runHistory.find((r) => r.chainGroupId === chainGroupId && r.status === "running");
|
|
360
360
|
if (chainRecord) {
|
package/dist/daemon/manager.js
CHANGED
|
@@ -27,7 +27,7 @@ export class LoopManager {
|
|
|
27
27
|
}
|
|
28
28
|
let taskId = meta.taskId;
|
|
29
29
|
if (!taskId && meta.command) {
|
|
30
|
-
const task = this.taskManager.createInline(meta.command, meta.commandArgs
|
|
30
|
+
const task = this.taskManager.createInline(meta.command, meta.commandArgs);
|
|
31
31
|
taskId = task.id;
|
|
32
32
|
meta.taskId = taskId;
|
|
33
33
|
saveLoop(meta);
|
|
@@ -245,7 +245,7 @@ export class LoopManager {
|
|
|
245
245
|
...runtime,
|
|
246
246
|
command: task?.command ?? options.command,
|
|
247
247
|
commandArgs: task?.commandArgs ?? options.commandArgs,
|
|
248
|
-
cwd:
|
|
248
|
+
cwd: options.cwd || "",
|
|
249
249
|
interval: options.interval,
|
|
250
250
|
intervalHuman,
|
|
251
251
|
immediate: options.immediate,
|
|
@@ -43,7 +43,7 @@ export class TaskManager {
|
|
|
43
43
|
deleteTaskState(id);
|
|
44
44
|
return true;
|
|
45
45
|
}
|
|
46
|
-
createInline(command, commandArgs
|
|
46
|
+
createInline(command, commandArgs) {
|
|
47
47
|
const id = crypto.randomUUID().slice(0, 8);
|
|
48
48
|
const name = [command, ...commandArgs].join(" ").slice(0, 40);
|
|
49
49
|
return this.create({
|
|
@@ -51,7 +51,6 @@ export class TaskManager {
|
|
|
51
51
|
name,
|
|
52
52
|
command,
|
|
53
53
|
commandArgs,
|
|
54
|
-
cwd,
|
|
55
54
|
onSuccessTaskId: null,
|
|
56
55
|
onFailureTaskId: null,
|
|
57
56
|
});
|
package/dist/i18n/en.json
CHANGED
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"cli.projectColorSet": "Project \"{name}\" color set to {color}",
|
|
63
63
|
"cli.projectDeleted": "Project \"{name}\" deleted (loops moved to Default)",
|
|
64
64
|
"cli.projectNotFound": "Project not found: {name}",
|
|
65
|
-
"cli.projectAmbiguous": "Multiple projects named \"{name}\"
|
|
65
|
+
"cli.projectAmbiguous": "Multiple projects named \"{name}\" - use the id instead",
|
|
66
66
|
"cli.projectInvalidColor": "Invalid color \"{color}\". Valid names: {valid}, or a #rrggbb hex",
|
|
67
|
-
"loop.runHeader": "\n[Run #{runNumber}
|
|
67
|
+
"loop.runHeader": "\n[Run #{runNumber} - {timestamp}]\n",
|
|
68
68
|
"loop.exitMarker": "[exit {code} · {duration}]\n",
|
|
69
69
|
"loop.cwdMissingLog": "[error] working directory does not exist: {cwd}\n",
|
|
70
70
|
"loop.cwdMissing": "Working directory does not exist: {cwd}",
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
"errors.resumeFailed": "resume failed",
|
|
101
101
|
"errors.forceRunFailed": "force run failed",
|
|
102
102
|
"errors.deleteFailed": "delete failed",
|
|
103
|
-
"errors.maxRunsReached": "Max runs reached
|
|
104
|
-
"errors.triggerWhileRunning": "Loop is already running
|
|
103
|
+
"errors.maxRunsReached": "Max runs reached - edit the loop and increase 'Max runs' to continue",
|
|
104
|
+
"errors.triggerWhileRunning": "Loop is already running - force run skipped",
|
|
105
105
|
"format.dash": "-",
|
|
106
106
|
"format.justNow": "just now",
|
|
107
107
|
"format.secsAgo": "{secs}s ago",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "1.4.
|
|
4
|
-
"description": "Run
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"loop-task": "dist/entry.js"
|
|
@@ -29,14 +29,24 @@
|
|
|
29
29
|
"keywords": [
|
|
30
30
|
"cli",
|
|
31
31
|
"loop",
|
|
32
|
+
"loop-engineering",
|
|
32
33
|
"repeat",
|
|
33
34
|
"interval",
|
|
34
35
|
"schedule",
|
|
35
36
|
"timer",
|
|
36
37
|
"cron",
|
|
37
38
|
"automation",
|
|
39
|
+
"automations",
|
|
38
40
|
"devops",
|
|
39
|
-
"agent"
|
|
41
|
+
"agent",
|
|
42
|
+
"ai-agent",
|
|
43
|
+
"coding-agent",
|
|
44
|
+
"agent-automation",
|
|
45
|
+
"claude-code",
|
|
46
|
+
"codex",
|
|
47
|
+
"opencode",
|
|
48
|
+
"background-tasks",
|
|
49
|
+
"scheduler"
|
|
40
50
|
],
|
|
41
51
|
"author": "Quique Fdez Guerra",
|
|
42
52
|
"license": "MIT",
|