loop-task 1.3.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.
Files changed (43) hide show
  1. package/README.md +279 -195
  2. package/dist/board/App.js +225 -87
  3. package/dist/board/components/ActionButtons.js +28 -8
  4. package/dist/board/components/CreateForm.js +230 -70
  5. package/dist/board/components/CreateProjectModal.js +104 -0
  6. package/dist/board/components/DeleteProjectConfirm.js +53 -0
  7. package/dist/board/components/EditProjectModal.js +106 -0
  8. package/dist/board/components/FilterBar.js +5 -4
  9. package/dist/board/components/Footer.js +19 -6
  10. package/dist/board/components/Header.js +33 -3
  11. package/dist/board/components/HelpModal.js +11 -10
  12. package/dist/board/components/Inspector.js +1 -1
  13. package/dist/board/components/LogModal.js +39 -8
  14. package/dist/board/components/Navigator.js +30 -14
  15. package/dist/board/components/ProjectsModal.js +70 -0
  16. package/dist/board/components/ProjectsPage.js +105 -0
  17. package/dist/board/components/RunHistory.js +48 -9
  18. package/dist/board/components/TaskBrowser.js +96 -0
  19. package/dist/board/components/TaskFilterBar.js +6 -0
  20. package/dist/board/components/TaskForm.js +175 -0
  21. package/dist/board/daemon.js +56 -0
  22. package/dist/board/format.js +19 -12
  23. package/dist/board/hooks/useBoardKeybindings.js +264 -156
  24. package/dist/board/hooks/useTaskKeybindings.js +130 -0
  25. package/dist/board/router.js +16 -0
  26. package/dist/board/state.js +18 -14
  27. package/dist/cli.js +61 -6
  28. package/dist/client/commands.js +157 -1
  29. package/dist/config/constants.js +15 -0
  30. package/dist/config/paths.js +6 -0
  31. package/dist/core/command-runner.js +2 -0
  32. package/dist/core/foreground-loop.js +4 -1
  33. package/dist/core/loop-controller.js +210 -32
  34. package/dist/daemon/index.js +5 -2
  35. package/dist/daemon/manager.js +113 -34
  36. package/dist/daemon/projects.js +104 -0
  37. package/dist/daemon/server.js +177 -6
  38. package/dist/daemon/state.js +32 -1
  39. package/dist/daemon/task-manager.js +58 -0
  40. package/dist/i18n/en.json +140 -21
  41. package/dist/loop-config.js +13 -4
  42. package/dist/shared/clipboard.js +19 -0
  43. package/package.json +13 -3
package/dist/i18n/en.json CHANGED
@@ -39,7 +39,32 @@
39
39
  "cli.deleted": "Loop {id} deleted.",
40
40
  "cli.attaching": "Attaching to loop {id}... (Ctrl+C to detach)\n",
41
41
  "cli.detached": "\nDetached.",
42
- "loop.runHeader": "\n[Run #{runNumber} {timestamp}]\n",
42
+ "cli.optProject": "Assign the loop to a project (name or id)",
43
+ "cli.projectDescription": "Manage projects",
44
+ "cli.projectListDescription": "List all projects",
45
+ "cli.projectNewDescription": "Create a new project",
46
+ "cli.projectRenameDescription": "Rename a project",
47
+ "cli.projectColorDescription": "Change a project's color",
48
+ "cli.projectDeleteDescription": "Delete a project (loops move to Default)",
49
+ "cli.optProjectColor": "Project color (name or #hex)",
50
+ "cli.projectArgName": "Project name",
51
+ "cli.projectArgNewName": "New project name",
52
+ "cli.projectArgIdOrName": "Project id or name",
53
+ "cli.projectArgColor": "Color name or #hex",
54
+ "cli.noProjects": "No projects.",
55
+ "cli.projectHeaderName": "NAME",
56
+ "cli.projectHeaderId": "ID",
57
+ "cli.projectHeaderColor": "COLOR",
58
+ "cli.projectHeaderLoops": "LOOPS",
59
+ "cli.projectHeaderSystem": "SYSTEM",
60
+ "cli.projectCreated": "Project \"{name}\" created (id: {id})",
61
+ "cli.projectRenamed": "Project renamed to \"{name}\"",
62
+ "cli.projectColorSet": "Project \"{name}\" color set to {color}",
63
+ "cli.projectDeleted": "Project \"{name}\" deleted (loops moved to Default)",
64
+ "cli.projectNotFound": "Project not found: {name}",
65
+ "cli.projectAmbiguous": "Multiple projects named \"{name}\" - use the id instead",
66
+ "cli.projectInvalidColor": "Invalid color \"{color}\". Valid names: {valid}, or a #rrggbb hex",
67
+ "loop.runHeader": "\n[Run #{runNumber} - {timestamp}]\n",
43
68
  "loop.exitMarker": "[exit {code} · {duration}]\n",
44
69
  "loop.cwdMissingLog": "[error] working directory does not exist: {cwd}\n",
45
70
  "loop.cwdMissing": "Working directory does not exist: {cwd}",
@@ -60,6 +85,7 @@
60
85
  "errors.maxRunsInvalid": "--max-runs must be a positive integer",
61
86
  "errors.unbalancedQuote": "Unbalanced quote in command",
62
87
  "errors.commandEmpty": "Command cannot be empty",
88
+ "errors.descriptionEmpty": "Description cannot be empty",
63
89
  "errors.loopNotFound": "Loop {id} not found",
64
90
  "errors.invalidJson": "Invalid JSON",
65
91
  "errors.noLogsYet": "No logs yet",
@@ -74,6 +100,8 @@
74
100
  "errors.resumeFailed": "resume failed",
75
101
  "errors.forceRunFailed": "force run failed",
76
102
  "errors.deleteFailed": "delete failed",
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",
77
105
  "format.dash": "-",
78
106
  "format.justNow": "just now",
79
107
  "format.secsAgo": "{secs}s ago",
@@ -85,6 +113,7 @@
85
113
  "format.hrsAhead": "{hrs}h",
86
114
  "format.daysAhead": "{days}d",
87
115
  "format.timingPaused": "paused",
116
+ "format.timingIdle": "stopped",
88
117
  "format.timingNext": "in {timeAgo}",
89
118
  "format.timingLast": "last {timeAgo}",
90
119
  "format.timingNew": "new",
@@ -95,13 +124,21 @@
95
124
  "board.runningLabel": " Running: ",
96
125
  "board.waitingLabel": " Waiting: ",
97
126
  "board.pausedLabel": " Paused: ",
127
+ "board.idleLabel": " Stopped: ",
98
128
  "board.searchTitle": " Search / ",
99
129
  "board.searchPlaceholder": "type to filter, enter to apply",
100
130
  "board.searchEmpty": "press / to search",
101
- "board.statusFilterTitle": " Status f ",
102
- "board.sortTitle": " Sort s ",
103
- "board.newLoopTitle": " New ",
104
- "board.newLoopLabel": "+ New Loop",
131
+ "board.statusFilterTitle": " Status x ",
132
+ "board.sortTitle": " Order o ",
133
+ "board.newLoopTitle": "",
134
+ "board.newLoopLabel": "Add Loop",
135
+ "board.viewTasksTitle": "",
136
+ "board.viewTasksLabel": "View Tasks",
137
+ "board.viewLoopsTitle": " Loops ",
138
+ "board.viewLoopsLabel": "View Loops",
139
+ "board.headerSince": "SINCE",
140
+ "board.headerRuns": "RUNS",
141
+ "board.headerSkipped": "SKP",
105
142
  "board.headerStatus": "STATUS",
106
143
  "board.headerDescription": "DESCRIPTION",
107
144
  "board.headerTiming": "TIMING",
@@ -113,6 +150,7 @@
113
150
  "board.fieldId": "ID: ",
114
151
  "board.fieldDesc": "Desc: ",
115
152
  "board.fieldCommand": "Command: ",
153
+ "board.fieldTask": "Task: ",
116
154
  "board.fieldDir": "Dir: ",
117
155
  "board.fieldInterval": "Interval: ",
118
156
  "board.fieldStatus": "Status: ",
@@ -126,6 +164,7 @@
126
164
  "board.timelineTitle": " Timeline ",
127
165
  "board.timelineEmpty": " No output yet.",
128
166
  "board.runHistoryTitle": " Run History ",
167
+ "board.runHistoryTitleHint": " Run History (Press Enter to view log) ",
129
168
  "board.runHistoryEmpty": " No runs yet.",
130
169
  "board.runHistoryTime": "TIME",
131
170
  "board.runHistoryDesc": "DESCRIPTION",
@@ -135,6 +174,7 @@
135
174
  "board.logModalLoading": " Loading...",
136
175
  "board.logModalEmpty": " No output captured.",
137
176
  "board.logModalEscClose": "esc to close",
177
+ "board.logModalRunning": "running…",
138
178
  "board.detailTitle": " Loop Detail ",
139
179
  "board.detailFieldId": "ID: ",
140
180
  "board.detailFieldDesc": "Desc: ",
@@ -158,8 +198,10 @@
158
198
  "board.helpMoveSelection": "move selection",
159
199
  "board.helpToggleDetail": "toggle loop detail",
160
200
  "board.helpCreate": "create loop",
201
+ "board.helpCreateTask": "create task",
161
202
  "board.helpEdit": "edit loop (pauses it)",
162
203
  "board.helpPauseResume": "pause/resume selected loop",
204
+ "board.helpStopPlay": "stop/play selected loop (resets schedule)",
163
205
  "board.helpForceRun": "force run selected loop now",
164
206
  "board.helpDelete": "delete selected loop",
165
207
  "board.helpSearch": "search loops",
@@ -178,12 +220,29 @@
178
220
  "board.labelCwd": "Working dir",
179
221
  "board.labelRunNow": "Run immediately?",
180
222
  "board.labelMaxRuns": "Max runs",
223
+ "board.labelTaskMode": "Task",
224
+ "board.labelTask": "Select task",
225
+ "board.taskModeInline": "Inline command",
226
+ "board.taskModeExisting": "Existing task",
227
+ "board.selectTask": "(select a task)",
228
+ "board.taskLabelName": "Name",
229
+ "board.taskLabelCommand": "Command",
230
+ "board.taskLabelOnSuccess": "On success",
231
+ "board.taskLabelOnFailure": "On failure",
232
+ "board.taskHintName": "A short name for this task",
233
+ "board.taskHintOnSuccess": "Task to run when this one succeeds (optional)",
234
+ "board.taskHintOnFailure": "Task to run when this one fails (optional)",
235
+ "board.taskNone": "(none)",
236
+ "board.taskCreateTitle": " New Task ",
237
+ "board.taskEditTitle": " Edit Task ",
238
+ "board.hintTaskMode": "type a command inline, or pick an existing task",
181
239
  "board.hintInterval": "How often to run, e.g. 30s, 5m, 30m, 1h, 1d",
182
240
  "board.hintCommand": "Full command line. Quote args with spaces",
183
241
  "board.hintDescription": "Short label shown in the list. Blank uses the command",
184
242
  "board.hintCwd": "Directory the command runs in. Must exist at run time",
185
243
  "board.hintRunNow": "run once now then every interval, or wait the first interval",
186
244
  "board.hintMaxRuns": "Stop after N runs. Leave blank to run forever",
245
+ "board.hintTask": "Pick a previously defined task",
187
246
  "board.exampleInterval": "30m",
188
247
  "board.exampleCommand": "opencode run \"search missing translations and translate them, 3 maximum\" --model \"opencode/big-pickle\"",
189
248
  "board.exampleDescription": "translate missing strings",
@@ -201,36 +260,45 @@
201
260
  "board.create": "Create",
202
261
  "board.cancel": "Cancel",
203
262
  "board.formNav": "Tab/Shift+Tab to move | Enter advances or activates | Esc cancels",
204
- "board.badgeNormal": "DASHBOARD",
263
+ "board.badgeNormal": "Loops",
205
264
  "board.badgeSearch": "SEARCH",
206
265
  "board.badgeCreate": "CREATE",
266
+ "board.badgeTask": "TASK",
207
267
  "board.badgeHelp": "HELP",
208
268
  "board.badgeDetail": "DETAIL",
209
269
  "board.badgeConfirm": "CONFIRM",
210
270
  "board.confirmTitle": " Confirm ",
211
271
  "board.yes": "Yes",
212
272
  "board.no": "No",
213
- "board.actionResume": "Resume",
214
- "board.actionPause": "Pause",
215
- "board.actionResumed": "Resumed",
216
- "board.actionPaused": "Paused",
217
- "board.actionRun": "Run",
218
273
  "board.actionEdit": "Edit",
219
274
  "board.actionDelete": "Delete",
275
+ "board.actionPause": "Pause",
276
+ "board.actionStop": "Stop",
277
+ "board.actionPlay": "Play",
278
+ "board.actionTrigger": "Force Run",
220
279
  "board.noActions": "Select a loop to see actions",
221
- "board.confirmPauseResume": "{action} loop {id}?",
222
- "board.confirmDelete": "Delete loop {id}?",
223
- "board.confirmForceRun": "Force run loop {id} now?",
224
- "board.toastActionId": "{verb} {id}",
225
- "board.toastDeleted": "Deleted {id}",
226
- "board.toastTriggered": "Triggered {id}",
227
- "board.toastUpdated": "Updated {id} (paused)",
228
- "board.toastStarted": "Started {id}",
280
+ "board.confirmPause": "Pause loop \"{desc}\"?",
281
+ "board.confirmStop": "Stop loop \"{desc}\"? (schedule will reset on Play)",
282
+ "board.confirmPlay": "Play loop \"{desc}\"? (starts fresh interval from now)",
283
+ "board.confirmResume": "Resume loop \"{desc}\"?",
284
+ "board.confirmTrigger": "Force run loop \"{desc}\"?",
285
+ "board.confirmDelete": "Delete loop \"{desc}\"?",
286
+ "board.toastPaused": "Paused \"{desc}\"",
287
+ "board.toastStopped": "Stopped \"{desc}\"",
288
+ "board.toastPlayed": "Playing \"{desc}\"",
289
+ "board.toastResumed": "Resumed \"{desc}\"",
290
+ "board.toastDeleted": "Deleted \"{desc}\"",
291
+ "board.toastUpdated": "Updated \"{desc}\" (paused)",
292
+ "board.toastTriggered": "Triggered \"{desc}\"",
293
+ "board.toastStarted": "Started \"{desc}\"",
294
+ "board.toastTaskCreated": "Task created {id}",
295
+ "board.toastTaskUpdated": "Task updated {id}",
296
+ "board.toastTaskSelected": "Selected task \"{desc}\"",
229
297
  "board.logWaiting": " Waiting for live output...",
230
298
  "board.logStreamError": "Log stream error: {message}",
231
299
  "board.hintSearch": "search",
232
300
  "board.hintRunHistory": "runs",
233
- "board.hintOpenRun": "open log",
301
+ "board.hintOpenRun": "select loop / open log",
234
302
  "board.hintSwitchPanel": "panel",
235
303
  "board.hintHelp": "help",
236
304
  "board.hintQuit": "quit",
@@ -263,5 +331,56 @@
263
331
  "board.hintKeyTab": "tab",
264
332
  "board.hintKeyHelpEsc": "h/esc",
265
333
  "board.hintKeyArrows": "←/→",
266
- "board.hintKeyYN": "y/n"
334
+ "board.hintKeyYN": "y/n",
335
+ "board.taskBrowserTitle": " Tasks {visible}/{total} ",
336
+ "board.taskBrowserEmpty": " No tasks defined yet. Press n to create one.",
337
+ "board.taskHeaderName": "NAME",
338
+ "board.taskHeaderCommand": "COMMAND",
339
+ "board.taskHeaderChains": "CHAINS",
340
+ "board.taskActionSelect": "Select",
341
+ "board.taskActionNew": "+ New Task",
342
+ "board.newTaskTitle": " New ",
343
+ "board.taskActionEdit": "Edit",
344
+ "board.taskActionDelete": "Delete",
345
+ "board.confirmDeleteTask": "Delete task \"{desc}\"?",
346
+ "board.toastTaskDeleted": "Deleted task \"{desc}\"",
347
+ "board.chooseTask": "Choose Task",
348
+ "board.selectedTask": "Task: {name}",
349
+ "board.taskChainsNone": "-",
350
+ "board.taskChainsFormat": "✓:{success} ✗:{failure}",
351
+ "board.logModalCopied": "Log copied to clipboard",
352
+ "project.selectProject": " Select Project ",
353
+ "project.projectsLabel": "Projects",
354
+ "project.projectsTitle": " Projects ",
355
+ "project.filterTitle": " Project r ",
356
+ "project.newProjectLabel": "+ New Project",
357
+ "project.manageLabel": "View Projects",
358
+ "project.showAll": "Show All",
359
+ "project.showAllLoops": "All loops",
360
+ "project.manageTitle": "",
361
+ "project.createTitle": " New Project ",
362
+ "project.editTitle": " Edit Project ",
363
+ "project.deleteTitle": " Delete Project ",
364
+ "project.labelName": "Name",
365
+ "project.labelColor": "Color",
366
+ "project.hintName": "A short name for this project",
367
+ "project.hintColor": "Pick a color for this project",
368
+ "project.systemLabel": "System project",
369
+ "project.editProjectLabel": "Edit",
370
+ "project.deleteProjectLabel": "Delete",
371
+ "project.loopCount": "{count} loop(s)",
372
+ "project.noLoops": "No loops",
373
+ "project.deleteConfirm": "Delete project \"{name}\"?",
374
+ "project.deleteWarning": "This project has {count} loop(s). Loops will be moved to Default.",
375
+ "project.moveToDefault": "Yes, move to Default",
376
+ "project.keyNewHint": "n: new",
377
+ "project.keyEditHint": "e: edit",
378
+ "project.keyDeleteHint": "d: delete",
379
+ "project.error.nameRequired": "Project name is required",
380
+ "project.error.nameEmpty": "Project name cannot be empty",
381
+ "project.error.updateFailed": "Failed to update project",
382
+ "project.error.deleteFailed": "Failed to delete project",
383
+ "project.toastCreated": "Project \"{name}\" created",
384
+ "project.toastUpdated": "Project \"{name}\" updated",
385
+ "project.toastDeleted": "Project \"{name}\" deleted"
267
386
  }
@@ -54,21 +54,30 @@ export function parseCommandLine(input) {
54
54
  }
55
55
  return tokens;
56
56
  }
57
- export function buildLoopOptions(intervalHuman, command, commandArgs, input = {}) {
58
- if (!command.trim()) {
57
+ export function buildLoopOptions(intervalHuman, input = {}) {
58
+ const command = input.command ?? "";
59
+ const commandArgs = input.commandArgs ?? [];
60
+ const taskId = input.taskId ?? null;
61
+ if (!taskId && !command.trim()) {
59
62
  throw new Error(t("errors.commandEmpty"));
60
63
  }
64
+ const description = input.description?.trim() ?? "";
65
+ if (!description) {
66
+ throw new Error(t("errors.descriptionEmpty"));
67
+ }
61
68
  return {
62
69
  intervalHuman,
63
70
  options: {
64
71
  interval: parseDuration(intervalHuman),
72
+ taskId,
65
73
  command,
66
74
  commandArgs,
75
+ cwd: input.cwd ?? "",
67
76
  immediate: input.now ?? false,
68
77
  maxRuns: parseMaxRuns(input.maxRuns),
69
78
  verbose: input.verbose ?? false,
70
- cwd: input.cwd?.trim() ?? "",
71
- description: input.description?.trim() ?? "",
79
+ description,
80
+ projectId: input.projectId ?? "default",
72
81
  },
73
82
  };
74
83
  }
@@ -0,0 +1,19 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { platform } from "node:os";
3
+ export function copyToClipboard(text) {
4
+ const os = platform();
5
+ if (os === "win32") {
6
+ execFileSync("clip", { input: text });
7
+ }
8
+ else if (os === "darwin") {
9
+ execFileSync("pbcopy", { input: text });
10
+ }
11
+ else {
12
+ try {
13
+ execFileSync("xclip", ["-selection", "clipboard"], { input: text });
14
+ }
15
+ catch {
16
+ execFileSync("xsel", ["--clipboard", "--input"], { input: text });
17
+ }
18
+ }
19
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loop-task",
3
- "version": "1.3.0",
4
- "description": "Run commands on repeat. Manage them from a terminal board.",
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",