@plainconceptsplatform/loop-task 2.6.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.
Files changed (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +534 -0
  3. package/dist/app/App.js +132 -0
  4. package/dist/app/BoardLayout.js +5 -0
  5. package/dist/app/index.js +48 -0
  6. package/dist/app/providers/index.js +1 -0
  7. package/dist/app/router/index.js +16 -0
  8. package/dist/app/types.js +1 -0
  9. package/dist/cli/import-validator.js +152 -0
  10. package/dist/cli/import-writer.js +81 -0
  11. package/dist/cli.js +573 -0
  12. package/dist/client/cli-format.js +7 -0
  13. package/dist/client/commands.js +162 -0
  14. package/dist/client/ipc.js +96 -0
  15. package/dist/client/project-commands.js +164 -0
  16. package/dist/core/command/command-runner.js +300 -0
  17. package/dist/core/command/process-tree.js +73 -0
  18. package/dist/core/command/resolve-cwd.js +8 -0
  19. package/dist/core/command/stdout-capture-transform.js +73 -0
  20. package/dist/core/context/context-parser.js +65 -0
  21. package/dist/core/context/log-context.js +3 -0
  22. package/dist/core/context/template.js +20 -0
  23. package/dist/core/context/validate-context.js +19 -0
  24. package/dist/core/foreground/index.js +81 -0
  25. package/dist/core/logging/bounded-log-reader.js +368 -0
  26. package/dist/core/logging/log-follower.js +26 -0
  27. package/dist/core/logging/log-parser.js +29 -0
  28. package/dist/core/logging/log-rotator.js +34 -0
  29. package/dist/core/logging/rotating-log-stream.js +137 -0
  30. package/dist/core/loop/chain-executor.js +171 -0
  31. package/dist/core/loop/delay-utils.js +65 -0
  32. package/dist/core/loop/loop-controller.js +240 -0
  33. package/dist/core/loop/loop-runner.js +194 -0
  34. package/dist/core/loop/run-executor.js +144 -0
  35. package/dist/core/loop/types.js +1 -0
  36. package/dist/core/scheduling/index.js +16 -0
  37. package/dist/daemon/daemon-log.js +11 -0
  38. package/dist/daemon/diagnostics.js +85 -0
  39. package/dist/daemon/http/helpers.js +81 -0
  40. package/dist/daemon/http/openapi.js +119 -0
  41. package/dist/daemon/http/route-loops.js +311 -0
  42. package/dist/daemon/http/route-misc.js +60 -0
  43. package/dist/daemon/http/route-projects.js +55 -0
  44. package/dist/daemon/http/route-tasks.js +141 -0
  45. package/dist/daemon/http/routes.js +19 -0
  46. package/dist/daemon/http/server.js +82 -0
  47. package/dist/daemon/http/sse.js +36 -0
  48. package/dist/daemon/index.js +189 -0
  49. package/dist/daemon/ipc/send.js +5 -0
  50. package/dist/daemon/managers/loop-entry.js +27 -0
  51. package/dist/daemon/managers/loop-manager.js +346 -0
  52. package/dist/daemon/managers/loop-options.js +17 -0
  53. package/dist/daemon/managers/loop-serialization.js +53 -0
  54. package/dist/daemon/managers/project-manager.js +170 -0
  55. package/dist/daemon/managers/task-manager.js +94 -0
  56. package/dist/daemon/mcp/index.js +2 -0
  57. package/dist/daemon/mcp/openapi-sync.js +50 -0
  58. package/dist/daemon/mcp/server.js +167 -0
  59. package/dist/daemon/mcp/tools.js +382 -0
  60. package/dist/daemon/recipe/deferred-reload.js +32 -0
  61. package/dist/daemon/recipe/diagram-reader.js +26 -0
  62. package/dist/daemon/recipe/file-writer.js +52 -0
  63. package/dist/daemon/recipe/id-remapper.js +46 -0
  64. package/dist/daemon/recipe/runtime-state.js +23 -0
  65. package/dist/daemon/recipe/scanner.js +165 -0
  66. package/dist/daemon/recipe/task-store.js +33 -0
  67. package/dist/daemon/recipe/validator.js +35 -0
  68. package/dist/daemon/server/handlers/diagnostics-handlers.js +7 -0
  69. package/dist/daemon/server/handlers/index.js +101 -0
  70. package/dist/daemon/server/handlers/log-handlers.js +129 -0
  71. package/dist/daemon/server/handlers/loop-handlers.js +64 -0
  72. package/dist/daemon/server/handlers/project-handlers.js +42 -0
  73. package/dist/daemon/server/handlers/settings-handlers.js +8 -0
  74. package/dist/daemon/server/handlers/task-handlers.js +20 -0
  75. package/dist/daemon/server/handlers/telemetry-handlers.js +5 -0
  76. package/dist/daemon/server/index.js +117 -0
  77. package/dist/daemon/settings-manager.js +73 -0
  78. package/dist/daemon/spawner/index.js +204 -0
  79. package/dist/daemon/state/index.js +209 -0
  80. package/dist/daemon/telemetry/agent-integrations/agent-integration.js +1 -0
  81. package/dist/daemon/telemetry/agent-integrations/claude-code-integration.js +55 -0
  82. package/dist/daemon/telemetry/agent-integrations/detect-agent-integration.js +20 -0
  83. package/dist/daemon/telemetry/agent-integrations/index.js +3 -0
  84. package/dist/daemon/telemetry/agent-integrations/opencode-integration.js +72 -0
  85. package/dist/daemon/telemetry/index.js +6 -0
  86. package/dist/daemon/telemetry/noop-telemetry-adapter.js +53 -0
  87. package/dist/daemon/telemetry/open-telemetry-adapter.js +464 -0
  88. package/dist/daemon/telemetry/telemetry-manager.js +147 -0
  89. package/dist/daemon/telemetry/telemetry-redaction.js +67 -0
  90. package/dist/daemon/telemetry/telemetry-types.js +45 -0
  91. package/dist/daemon/telemetry/telemetry.js +1 -0
  92. package/dist/daemon/watcher/index.js +250 -0
  93. package/dist/duration.js +24 -0
  94. package/dist/entities/loops/filters.js +109 -0
  95. package/dist/entities/loops/index.js +1 -0
  96. package/dist/entities/projects/filters.js +69 -0
  97. package/dist/entities/projects/index.js +1 -0
  98. package/dist/entities/tasks/filters.js +41 -0
  99. package/dist/entities/tasks/index.js +1 -0
  100. package/dist/entry.js +16 -0
  101. package/dist/esm-loader.js +39 -0
  102. package/dist/features/chain-editor/ChainEditor.js +116 -0
  103. package/dist/features/chain-editor/mermaidToAscii.js +375 -0
  104. package/dist/features/chain-editor/renderChainDiagram.js +106 -0
  105. package/dist/features/code-editor/CodeEditorModal.js +84 -0
  106. package/dist/features/code-editor/CodeEditorPreview.js +14 -0
  107. package/dist/features/code-editor/useEditorKeyboard.js +150 -0
  108. package/dist/features/code-editor/useModalDimensions.js +9 -0
  109. package/dist/features/commands/commands.js +132 -0
  110. package/dist/features/commands/useCommandHandlers.js +280 -0
  111. package/dist/features/commands/useContextualActions.js +78 -0
  112. package/dist/features/commands/useGlobalShortcuts.js +154 -0
  113. package/dist/features/forms/FormRouter.js +46 -0
  114. package/dist/features/overlays/ContextHelpModal.js +8 -0
  115. package/dist/features/overlays/DiagramModal.js +45 -0
  116. package/dist/features/overlays/ExportModal.js +51 -0
  117. package/dist/features/overlays/HelpGuideModal.js +23 -0
  118. package/dist/features/overlays/HelpModal.js +59 -0
  119. package/dist/features/overlays/OverlayStack.js +10 -0
  120. package/dist/features/overlays/ProjectsModal.js +42 -0
  121. package/dist/features/overlays/TaskPickerModal.js +62 -0
  122. package/dist/features/overlays/WelcomeScreen.js +74 -0
  123. package/dist/features/overlays/useOverlayStack.js +55 -0
  124. package/dist/features/state/useAppState.js +151 -0
  125. package/dist/logger.js +25 -0
  126. package/dist/loop-config.js +152 -0
  127. package/dist/shared/clipboard.js +111 -0
  128. package/dist/shared/config/constants.js +104 -0
  129. package/dist/shared/config/paths.js +67 -0
  130. package/dist/shared/container/index.js +24 -0
  131. package/dist/shared/fs-utils.js +44 -0
  132. package/dist/shared/hooks/useBreakpoint.js +11 -0
  133. package/dist/shared/hooks/useDaemonSettings.js +41 -0
  134. package/dist/shared/hooks/useInject.js +6 -0
  135. package/dist/shared/hooks/useLogStream.js +37 -0
  136. package/dist/shared/hooks/useLoopFormValidation.js +131 -0
  137. package/dist/shared/hooks/useLoopPolling.js +30 -0
  138. package/dist/shared/hooks/useMouseScroll.js +56 -0
  139. package/dist/shared/hooks/useUndoRedo.js +77 -0
  140. package/dist/shared/i18n/en.json +709 -0
  141. package/dist/shared/i18n/index.js +11 -0
  142. package/dist/shared/providers/InversifyProvider.js +6 -0
  143. package/dist/shared/services/export-service.js +43 -0
  144. package/dist/shared/services/log-service.js +28 -0
  145. package/dist/shared/services/loop-service.js +78 -0
  146. package/dist/shared/services/project-service.js +39 -0
  147. package/dist/shared/services/settings-service.js +102 -0
  148. package/dist/shared/services/task-service.js +41 -0
  149. package/dist/shared/services/types.js +8 -0
  150. package/dist/shared/sleep.js +17 -0
  151. package/dist/shared/tail.js +4 -0
  152. package/dist/shared/ui/Button.js +18 -0
  153. package/dist/shared/ui/DebugPanel.js +9 -0
  154. package/dist/shared/ui/FocusableButton.js +18 -0
  155. package/dist/shared/ui/FocusableInput.js +72 -0
  156. package/dist/shared/ui/FocusableList.js +48 -0
  157. package/dist/shared/ui/Modal.js +13 -0
  158. package/dist/shared/ui/SelectModal.js +78 -0
  159. package/dist/shared/ui/Toast.js +37 -0
  160. package/dist/shared/ui/format.js +117 -0
  161. package/dist/shared/ui/hooks/useHoverState.js +11 -0
  162. package/dist/shared/ui/state.js +9 -0
  163. package/dist/shared/ui/theme.js +96 -0
  164. package/dist/shared/utils/log-lines.js +13 -0
  165. package/dist/shared/utils/paste.js +37 -0
  166. package/dist/shared/utils/syntax.js +128 -0
  167. package/dist/shared/utils/validation.js +56 -0
  168. package/dist/types.js +1 -0
  169. package/dist/visual-evidence/capture.js +51 -0
  170. package/dist/visual-evidence/cli.js +85 -0
  171. package/dist/visual-evidence/evidence-required.js +71 -0
  172. package/dist/visual-evidence/index.js +10 -0
  173. package/dist/visual-evidence/launch.js +61 -0
  174. package/dist/visual-evidence/manifest.js +32 -0
  175. package/dist/visual-evidence/openspec-resolver.js +45 -0
  176. package/dist/visual-evidence/publish.js +126 -0
  177. package/dist/visual-evidence/run.js +121 -0
  178. package/dist/visual-evidence/scenario-registry.js +132 -0
  179. package/dist/widgets/command-input/CommandDropdown.js +69 -0
  180. package/dist/widgets/command-input/CommandInput.js +133 -0
  181. package/dist/widgets/command-input/HintBar.js +9 -0
  182. package/dist/widgets/command-input/InputModes.js +72 -0
  183. package/dist/widgets/commands-browser/CommandsBrowserModal.js +85 -0
  184. package/dist/widgets/header/Header.js +54 -0
  185. package/dist/widgets/header/TabBar.js +20 -0
  186. package/dist/widgets/left-panel/LeftPanel.js +34 -0
  187. package/dist/widgets/left-panel/Navigator.js +107 -0
  188. package/dist/widgets/left-panel/ProjectsPage.js +76 -0
  189. package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
  190. package/dist/widgets/left-panel/TaskBrowser.js +132 -0
  191. package/dist/widgets/log-modal/LogModal.js +211 -0
  192. package/dist/widgets/loop-form/CreateForm.js +82 -0
  193. package/dist/widgets/loop-form/TextField.js +9 -0
  194. package/dist/widgets/loop-form/WizardForm.js +199 -0
  195. package/dist/widgets/loop-form/useCreateSteps.js +135 -0
  196. package/dist/widgets/loop-form/useHandleComplete.js +94 -0
  197. package/dist/widgets/project-form/ProjectForm.js +83 -0
  198. package/dist/widgets/right-panel/Inspector.js +41 -0
  199. package/dist/widgets/right-panel/RightPanel.js +38 -0
  200. package/dist/widgets/right-panel/RunHistory.js +187 -0
  201. package/dist/widgets/task-form/TaskForm.js +272 -0
  202. package/package.json +119 -0
package/dist/cli.js ADDED
@@ -0,0 +1,573 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { createRequire } from "node:module";
4
+ import { Logger } from "./logger.js";
5
+ import { runLoop } from "./core/foreground/index.js";
6
+ import { buildLoopOptions } from "./loop-config.js";
7
+ import { parseDuration } from "./duration.js";
8
+ import { startLoop } from "./client/commands.js";
9
+ import { listProjectsCli, createProjectCli, renameProjectCli, setProjectColorCli, deleteProjectCli, resolveProjectId, } from "./client/project-commands.js";
10
+ import { t } from "./shared/i18n/index.js";
11
+ import { HTTP_API_PORT, HTTP_API_HOST } from "./shared/config/constants.js";
12
+ const require = createRequire(import.meta.url);
13
+ const packageJson = require("../package.json");
14
+ const program = new Command();
15
+ program
16
+ .name("loop-task")
17
+ .description(t("cli.programDescription"))
18
+ .version(packageJson.version, "-V, --version");
19
+ program
20
+ .command("start")
21
+ .description(t("cli.startDescription"))
22
+ .action(async () => {
23
+ const { ensureDaemon } = await import("./daemon/spawner/index.js");
24
+ ensureDaemon();
25
+ console.log(t("cli.daemonStarted"));
26
+ });
27
+ program
28
+ .command("stop")
29
+ .description("Stop a running loop and interrupt its current execution")
30
+ .argument("<id>", "Loop ID")
31
+ .action(async (id) => {
32
+ try {
33
+ const { sendRequest } = await import("./client/ipc.js");
34
+ const res = await sendRequest({ type: "stop-loop", payload: { id } });
35
+ if (res.type === "ok" && res.data) {
36
+ console.log(`Stopped loop ${id}`);
37
+ }
38
+ else if (res.type === "error") {
39
+ console.error(res.message);
40
+ process.exit(1);
41
+ }
42
+ else {
43
+ console.error(`Loop ${id} not found`);
44
+ process.exit(1);
45
+ }
46
+ }
47
+ catch (error) {
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ console.error(t("cli.error", { message }));
50
+ process.exit(1);
51
+ }
52
+ });
53
+ program
54
+ .command("restart")
55
+ .description("Kill the daemon and all running loops, then restart fresh")
56
+ .action(async () => {
57
+ const { stopDaemon, ensureDaemon } = await import("./daemon/spawner/index.js");
58
+ const { readDaemonPid, removeDaemonPid, removeDaemonSignature } = await import("./daemon/state/index.js");
59
+ const pid = readDaemonPid();
60
+ if (pid !== null) {
61
+ console.log("Stopping daemon...");
62
+ stopDaemon(pid);
63
+ removeDaemonPid();
64
+ removeDaemonSignature();
65
+ }
66
+ else {
67
+ stopDaemon(0);
68
+ }
69
+ console.log("Starting fresh daemon...");
70
+ ensureDaemon();
71
+ console.log("Daemon restarted.");
72
+ });
73
+ program
74
+ .command("new")
75
+ .description(t("cli.newDescription"))
76
+ .argument("<interval>", t("cli.argInterval"))
77
+ .argument("<command...>", t("cli.argCommand"))
78
+ .option("--now", t("cli.optNow"), false)
79
+ .option("--max-runs <n>", t("cli.optMaxRuns"), parseInt)
80
+ .option("--verbose", t("cli.optVerbose"), false)
81
+ .option("--cwd <dir>", t("cli.optCwd"))
82
+ .option("--project <name>", t("cli.optProject"))
83
+ .option("--offset <duration>", "Phase offset (e.g. 5m, 15m)")
84
+ .option("--description <desc>", "Description of the loop")
85
+ .option("-C, --context <json>", "Initial context JSON (e.g. {\"env\":\"staging\"})")
86
+ .action(async (intervalStr, cmdArgs, opts) => {
87
+ try {
88
+ const projectId = opts.project
89
+ ? await resolveProjectId(opts.project)
90
+ : undefined;
91
+ const offsetMs = opts.offset ? parseDuration(opts.offset) : null;
92
+ let context;
93
+ if (opts.context) {
94
+ const { validateContext } = await import("./core/context/validate-context.js");
95
+ let parsed;
96
+ try {
97
+ parsed = JSON.parse(opts.context);
98
+ }
99
+ catch {
100
+ console.error("Invalid JSON in --context flag");
101
+ process.exit(1);
102
+ }
103
+ const result = validateContext(parsed);
104
+ if (!result.valid) {
105
+ console.error(`Invalid context: ${result.error}`);
106
+ process.exit(1);
107
+ }
108
+ context = result.context;
109
+ }
110
+ const built = buildLoopOptions(intervalStr, {
111
+ ...opts,
112
+ command: cmdArgs[0],
113
+ commandArgs: cmdArgs.slice(1),
114
+ cwd: opts.cwd ?? process.cwd(),
115
+ projectId,
116
+ offset: offsetMs,
117
+ description: opts.description ?? cmdArgs.join(" "),
118
+ context,
119
+ });
120
+ await startLoop(built.options, built.intervalHuman);
121
+ }
122
+ catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ console.error(t("cli.error", { message }));
125
+ process.exit(1);
126
+ }
127
+ });
128
+ program
129
+ .command("run")
130
+ .description(t("cli.runDescription"))
131
+ .argument("<interval>", t("cli.argInterval"))
132
+ .argument("<command...>", t("cli.argCommand"))
133
+ .option("--now", t("cli.optNow"))
134
+ .option("--max-runs <n>", t("cli.optMaxRuns"))
135
+ .option("--verbose", t("cli.optVerbose"))
136
+ .option("--cwd <dir>", t("cli.optCwd"))
137
+ .option("--description <desc>", "Description of the loop")
138
+ .option("-C, --context <json>", "Initial context JSON (e.g. {\"env\":\"staging\"})")
139
+ .action(async (intervalStr, cmdArgs, opts) => {
140
+ if (!intervalStr || !cmdArgs || cmdArgs.length === 0) {
141
+ program.help();
142
+ return;
143
+ }
144
+ const logger = new Logger(opts.verbose ?? false);
145
+ let context;
146
+ if (opts.context) {
147
+ const { validateContext } = await import("./core/context/validate-context.js");
148
+ let parsed;
149
+ try {
150
+ parsed = JSON.parse(opts.context);
151
+ }
152
+ catch {
153
+ console.error("Invalid JSON in --context flag");
154
+ process.exit(1);
155
+ }
156
+ const result = validateContext(parsed);
157
+ if (!result.valid) {
158
+ console.error(`Invalid context: ${result.error}`);
159
+ process.exit(1);
160
+ }
161
+ context = result.context;
162
+ }
163
+ const built = buildLoopOptions(intervalStr, {
164
+ ...opts,
165
+ command: cmdArgs[0],
166
+ commandArgs: cmdArgs.slice(1),
167
+ cwd: opts.cwd ?? process.cwd(),
168
+ description: opts.description ?? cmdArgs.join(" "),
169
+ context,
170
+ });
171
+ if (built.options.interval === 0) {
172
+ console.error("Manual loops are not supported in foreground mode. Use 'loop-task new manual -- <command>' instead.");
173
+ process.exit(1);
174
+ }
175
+ const controller = new AbortController();
176
+ process.on("SIGINT", () => controller.abort());
177
+ process.on("SIGTERM", () => controller.abort());
178
+ await runLoop(built.options, logger, controller.signal);
179
+ process.exit(0);
180
+ });
181
+ const projectCmd = program.command("project").description(t("cli.projectDescription"));
182
+ projectCmd
183
+ .command("list")
184
+ .description(t("cli.projectListDescription"))
185
+ .action(async () => {
186
+ await listProjectsCli();
187
+ });
188
+ projectCmd
189
+ .command("new")
190
+ .description(t("cli.projectNewDescription"))
191
+ .argument("<name>", t("cli.projectArgName"))
192
+ .option("--color <color>", t("cli.optProjectColor"))
193
+ .option("--directory <path>", t("cli.optProjectDirectory"))
194
+ .option("--github-source <owner/repo>", t("cli.optProjectGithubSource"))
195
+ .action(async (name, opts) => {
196
+ await createProjectCli(name, opts.color, opts.directory, opts.githubSource);
197
+ });
198
+ projectCmd
199
+ .command("rename")
200
+ .description(t("cli.projectRenameDescription"))
201
+ .argument("<id-or-name>", t("cli.projectArgIdOrName"))
202
+ .argument("<new-name>", t("cli.projectArgNewName"))
203
+ .action(async (idOrName, newName) => {
204
+ await renameProjectCli(idOrName, newName);
205
+ });
206
+ projectCmd
207
+ .command("color")
208
+ .description(t("cli.projectColorDescription"))
209
+ .argument("<id-or-name>", t("cli.projectArgIdOrName"))
210
+ .argument("<color>", t("cli.projectArgColor"))
211
+ .action(async (idOrName, color) => {
212
+ await setProjectColorCli(idOrName, color);
213
+ });
214
+ projectCmd
215
+ .command("delete")
216
+ .description(t("cli.projectDeleteDescription"))
217
+ .argument("<id-or-name>", t("cli.projectArgIdOrName"))
218
+ .action(async (idOrName) => {
219
+ await deleteProjectCli(idOrName);
220
+ });
221
+ program.action(async () => {
222
+ const { launchBoard } = await import("./app/index.js");
223
+ const port = process.env.LOOP_CLI_HTTP_PORT ?? String(HTTP_API_PORT);
224
+ const baseUrl = `http://${HTTP_API_HOST}:${port}`;
225
+ console.log(`HTTP API Server`);
226
+ console.log(` Base URL: ${baseUrl}`);
227
+ console.log(` Swagger UI: ${baseUrl}/api/docs`);
228
+ console.log(` OpenAPI: ${baseUrl}/api/openapi.json`);
229
+ console.log(` Events: ${baseUrl}/api/events (SSE)`);
230
+ await launchBoard();
231
+ });
232
+ program
233
+ .command("status")
234
+ .description("Show status of all loops")
235
+ .option("--json", "Output as JSON")
236
+ .option("--verbose", "Show extra details (skipped, silent-chain counts)")
237
+ .action(async (opts) => {
238
+ const { sendRequest } = await import("./client/ipc.js");
239
+ const response = await sendRequest({ type: "list" });
240
+ if (response.type !== "ok") {
241
+ console.error("Failed to get status");
242
+ process.exit(1);
243
+ }
244
+ const loops = response.data;
245
+ if (opts.json) {
246
+ console.log(JSON.stringify(loops, null, 2));
247
+ }
248
+ else {
249
+ for (const loop of loops) {
250
+ const { describeLoop, statusLabel } = await import("./shared/ui/format.js");
251
+ let line = `${loop.id} ${statusLabel(loop.status)} ${describeLoop(loop)}`;
252
+ if (opts.verbose) {
253
+ const extra = [];
254
+ if (loop.skippedCount > 0)
255
+ extra.push(`skipped=${loop.skippedCount}`);
256
+ if ((loop.silentChainCount ?? 0) > 0)
257
+ extra.push(`silent=${loop.silentChainCount}`);
258
+ if (extra.length)
259
+ line += ` [${extra.join(", ")}]`;
260
+ }
261
+ console.log(line);
262
+ }
263
+ }
264
+ });
265
+ program
266
+ .command("export")
267
+ .description("Export all configs to a JSON file (or stdout)")
268
+ .argument("[file]", "Output file (defaults to stdout)")
269
+ .action(async (file) => {
270
+ const { sendRequest } = await import("./client/ipc.js");
271
+ const [loopsRes, tasksRes, projectsRes] = await Promise.all([
272
+ sendRequest({ type: "list" }),
273
+ sendRequest({ type: "task-list" }),
274
+ sendRequest({ type: "project-list" }),
275
+ ]);
276
+ const loops = loopsRes.type === "ok" ? loopsRes.data : [];
277
+ const exportData = {
278
+ version: 2,
279
+ exportedAt: new Date().toISOString(),
280
+ loops: loops.map(({ runHistory: _ignored, ...rest }) => rest),
281
+ tasks: tasksRes.type === "ok" ? tasksRes.data : [],
282
+ projects: projectsRes.type === "ok" ? projectsRes.data : [],
283
+ };
284
+ const json = JSON.stringify(exportData, null, 2);
285
+ if (file) {
286
+ const { writeFile } = await import("node:fs/promises");
287
+ await writeFile(file, json, "utf-8");
288
+ console.log(`Exported to ${file}`);
289
+ }
290
+ else {
291
+ console.log(json);
292
+ }
293
+ });
294
+ program
295
+ .command("import")
296
+ .description("Restore a previously exported state file (inverse of loop-task export)")
297
+ .argument("<file>", "Input file")
298
+ .action(async (file) => {
299
+ const { readFile } = await import("node:fs/promises");
300
+ let content;
301
+ try {
302
+ content = await readFile(file, "utf-8");
303
+ }
304
+ catch (err) {
305
+ const e = err;
306
+ console.error(`Failed to read file: ${e.message}`);
307
+ process.exit(1);
308
+ }
309
+ let parsed;
310
+ try {
311
+ parsed = JSON.parse(content);
312
+ }
313
+ catch {
314
+ console.error("Invalid JSON in import file");
315
+ process.exit(1);
316
+ }
317
+ const { validateExportFile } = await import("./cli/import-validator.js");
318
+ const result = validateExportFile(parsed);
319
+ if (!result.valid) {
320
+ for (const error of result.errors) {
321
+ console.error(error.message);
322
+ }
323
+ process.exit(1);
324
+ }
325
+ const { atomicImportWrite } = await import("./cli/import-writer.js");
326
+ const writeResult = atomicImportWrite(result.data.loops, result.data.tasks, result.data.projects);
327
+ if (!writeResult.success) {
328
+ console.error(`Failed to write store: ${writeResult.error}`);
329
+ if (writeResult.writtenStores.length > 0) {
330
+ console.error("Rolled back previously written stores.");
331
+ }
332
+ process.exit(1);
333
+ }
334
+ console.log(`Imported ${result.data.loops.length} loops, ${result.data.tasks.length} tasks, ${result.data.projects.length} projects`);
335
+ console.log("Daemon will hot-reload automatically.");
336
+ });
337
+ program
338
+ .command("diagram")
339
+ .description("View the loop diagram from the recipe YAML file")
340
+ .argument("<id>", "Loop ID")
341
+ .action(async (id) => {
342
+ try {
343
+ const { sendRequest } = await import("./client/ipc.js");
344
+ const loopRes = await sendRequest({ type: "status", payload: { id } });
345
+ if (loopRes.type === "error") {
346
+ console.error(loopRes.message);
347
+ process.exit(1);
348
+ }
349
+ if (loopRes.type !== "ok" || !loopRes.data) {
350
+ console.error(`Loop ${id} not found`);
351
+ process.exit(1);
352
+ }
353
+ const loop = loopRes.data;
354
+ if (!loop.taskId) {
355
+ console.log(`Loop ${id} has no task chain`);
356
+ process.exit(0);
357
+ }
358
+ // Recipe loops: read diagram from YAML
359
+ if (loop.isRecipe && loop.recipeFilePath) {
360
+ const { readRecipeDiagram } = await import("./daemon/recipe/diagram-reader.js");
361
+ const { renderMermaidAsAscii } = await import("./features/chain-editor/mermaidToAscii.js");
362
+ const diagram = readRecipeDiagram(loop.recipeFilePath);
363
+ if (diagram) {
364
+ console.log(renderMermaidAsAscii(diagram));
365
+ }
366
+ else {
367
+ console.error(t("diagram.noDiagramInRecipe"));
368
+ process.exit(1);
369
+ }
370
+ }
371
+ else {
372
+ console.error(t("diagram.notARecipe"));
373
+ process.exit(1);
374
+ }
375
+ }
376
+ catch (error) {
377
+ const message = error instanceof Error ? error.message : String(error);
378
+ console.error(t("cli.error", { message }));
379
+ process.exit(1);
380
+ }
381
+ });
382
+ program
383
+ .command("http-host")
384
+ .description("Show or set the network interface the HTTP API + MCP server bind to (default 0.0.0.0)")
385
+ .argument("[address]", "IP to bind — or 'local' (127.0.0.1) / 'all' (0.0.0.0)")
386
+ .action(async (address) => {
387
+ const { ensureDaemon } = await import("./daemon/spawner/index.js");
388
+ const { sendRequest } = await import("./client/ipc.js");
389
+ const port = process.env.LOOP_CLI_HTTP_PORT ?? String(HTTP_API_PORT);
390
+ try {
391
+ ensureDaemon();
392
+ if (!address) {
393
+ const res = await sendRequest({ type: "settings-get" });
394
+ const host = res.type === "ok" ? res.data.httpApiHost : HTTP_API_HOST;
395
+ const shown = host === "0.0.0.0" ? "<this-machine-ip>" : host;
396
+ const mcpPort = process.env.LOOP_CLI_MCP_PORT ?? "8846";
397
+ console.log(`API bind host: ${host}`);
398
+ console.log(` HTTP API: http://${shown}:${port}`);
399
+ console.log(` MCP SSE: http://${shown}:${mcpPort}/sse`);
400
+ return;
401
+ }
402
+ const normalized = address === "local" || address === "localhost"
403
+ ? "127.0.0.1"
404
+ : address === "all" || address === "any"
405
+ ? "0.0.0.0"
406
+ : address;
407
+ const res = await sendRequest({ type: "settings-set", settings: { httpApiHost: normalized } });
408
+ if (res.type !== "ok") {
409
+ console.error(res.type === "error" ? res.message : "Failed to update setting");
410
+ process.exit(1);
411
+ }
412
+ console.log(`HTTP API + MCP server now binding to ${normalized}`);
413
+ if (normalized !== "127.0.0.1") {
414
+ console.log("");
415
+ console.log("Note: the API is unauthenticated — anything that can reach this");
416
+ console.log("interface can create and trigger loops. Secure access at the network");
417
+ console.log("layer (SSH/Tailscale/firewall). Use `loop-task http-host local` for");
418
+ console.log("loopback-only.");
419
+ }
420
+ }
421
+ catch (error) {
422
+ const message = error instanceof Error ? error.message : String(error);
423
+ console.error(t("cli.error", { message }));
424
+ process.exit(1);
425
+ }
426
+ });
427
+ program
428
+ .command("mcp")
429
+ .description("Show MCP server info and how to connect an MCP client")
430
+ .action(() => {
431
+ const transport = process.env.LOOP_CLI_MCP_TRANSPORT === "stdio" ? "stdio" : "sse";
432
+ const port = process.env.LOOP_CLI_MCP_PORT ?? "8846";
433
+ const sseUrl = `http://${HTTP_API_HOST}:${port}/sse`;
434
+ console.log(`MCP Server`);
435
+ console.log(` Transport: ${transport}`);
436
+ console.log(` SSE URL: ${sseUrl}`);
437
+ console.log("");
438
+ console.log(` Connect an MCP client to the SSE URL above.`);
439
+ console.log("");
440
+ if (transport === "stdio") {
441
+ console.log(` Currently using stdio transport. To switch to SSE (default),`);
442
+ console.log(` restart without LOOP_CLI_MCP_TRANSPORT=stdio.`);
443
+ console.log("");
444
+ }
445
+ console.log(` Example MCP client configs:`);
446
+ console.log("");
447
+ console.log(` OpenCode (opencode.json):`);
448
+ console.log(` { "mcp": { "loop-task": { "type": "remote", "url": "${sseUrl}" } } }`);
449
+ console.log("");
450
+ console.log(` Claude Code (.claude/mcp.json):`);
451
+ console.log(` { "mcpServers": { "loop-task": { "type": "sse", "url": "${sseUrl}" } } }`);
452
+ console.log("");
453
+ console.log(` Cursor (.cursor/mcp.json):`);
454
+ console.log(` { "mcpServers": { "loop-task": { "type": "sse", "url": "${sseUrl}" } } }`);
455
+ });
456
+ const telemetryCmd = program.command("telemetry").description("Manage OpenTelemetry configuration");
457
+ telemetryCmd
458
+ .command("status")
459
+ .description("Show OpenTelemetry status and configuration")
460
+ .action(async () => {
461
+ const { sendRequest } = await import("./client/ipc.js");
462
+ const res = await sendRequest({ type: "settings-get" });
463
+ if (res.type !== "ok") {
464
+ console.error("Failed to get settings");
465
+ process.exit(1);
466
+ }
467
+ const s = res.data;
468
+ const endpoint = s.telemetryEndpoint ?? "not configured";
469
+ const hasHeaders = !!(process.env.OTEL_EXPORTER_OTLP_HEADERS || process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS);
470
+ console.log("OpenTelemetry");
471
+ console.log(` Enabled: ${s.telemetryEnabled ? "yes" : "no"}`);
472
+ console.log(` Endpoint: ${endpoint}`);
473
+ console.log(` Protocol: ${s.telemetryProtocol}`);
474
+ console.log(` Service: ${s.telemetryServiceName}`);
475
+ console.log(` Auto-instrument agents: ${s.telemetryAutoInstrumentAgents ? "yes" : "no"}`);
476
+ console.log(` Capture content: ${s.telemetryCaptureContent ? "yes" : "no"}`);
477
+ console.log(` Headers: ${hasHeaders ? "configured" : "not configured"}`);
478
+ console.log(` Configuration source: daemon settings`);
479
+ });
480
+ telemetryCmd
481
+ .command("on")
482
+ .description("Enable OpenTelemetry")
483
+ .action(async () => {
484
+ const { sendRequest } = await import("./client/ipc.js");
485
+ const res = await sendRequest({ type: "settings-set", settings: { telemetryEnabled: true } });
486
+ if (res.type !== "ok") {
487
+ console.error("Failed to enable telemetry");
488
+ process.exit(1);
489
+ }
490
+ console.log("OpenTelemetry enabled");
491
+ });
492
+ telemetryCmd
493
+ .command("off")
494
+ .description("Disable OpenTelemetry")
495
+ .action(async () => {
496
+ const { sendRequest } = await import("./client/ipc.js");
497
+ const res = await sendRequest({ type: "settings-set", settings: { telemetryEnabled: false } });
498
+ if (res.type !== "ok") {
499
+ console.error("Failed to disable telemetry");
500
+ process.exit(1);
501
+ }
502
+ console.log("OpenTelemetry disabled");
503
+ });
504
+ telemetryCmd
505
+ .command("endpoint")
506
+ .description("Set the OTLP export endpoint")
507
+ .argument("<url>", "OTLP endpoint URL (e.g. http://127.0.0.1:4318)")
508
+ .action(async (url) => {
509
+ const { sendRequest } = await import("./client/ipc.js");
510
+ const res = await sendRequest({ type: "settings-set", settings: { telemetryEndpoint: url } });
511
+ if (res.type !== "ok") {
512
+ console.error("Failed to set telemetry endpoint");
513
+ process.exit(1);
514
+ }
515
+ console.log(`Telemetry endpoint set to ${url}`);
516
+ });
517
+ telemetryCmd
518
+ .command("protocol")
519
+ .description("Set the OTLP export protocol")
520
+ .argument("<protocol>", "Protocol: grpc or http/protobuf")
521
+ .action(async (protocol) => {
522
+ if (protocol !== "grpc" && protocol !== "http/protobuf") {
523
+ console.error("Protocol must be 'grpc' or 'http/protobuf'");
524
+ process.exit(1);
525
+ }
526
+ const { sendRequest } = await import("./client/ipc.js");
527
+ const res = await sendRequest({ type: "settings-set", settings: { telemetryProtocol: protocol } });
528
+ if (res.type !== "ok") {
529
+ console.error("Failed to set telemetry protocol");
530
+ process.exit(1);
531
+ }
532
+ console.log(`Telemetry protocol set to ${protocol}`);
533
+ });
534
+ telemetryCmd
535
+ .command("test")
536
+ .description("Test the OpenTelemetry connection")
537
+ .action(async () => {
538
+ const { sendRequest } = await import("./client/ipc.js");
539
+ const res = await sendRequest({ type: "telemetry-test" });
540
+ if (res.type !== "ok") {
541
+ console.error("Telemetry test failed");
542
+ process.exit(1);
543
+ }
544
+ const result = res.data;
545
+ if (result.success) {
546
+ console.log(`OpenTelemetry test succeeded: ${result.message}`);
547
+ }
548
+ else {
549
+ console.error(`OpenTelemetry test failed: ${result.message}`);
550
+ process.exit(1);
551
+ }
552
+ });
553
+ // Default: show status
554
+ telemetryCmd.action(async () => {
555
+ const { sendRequest } = await import("./client/ipc.js");
556
+ const res = await sendRequest({ type: "settings-get" });
557
+ if (res.type !== "ok") {
558
+ console.error("Failed to get settings");
559
+ process.exit(1);
560
+ }
561
+ const s = res.data;
562
+ const endpoint = s.telemetryEndpoint ?? "not configured";
563
+ const hasHeaders = !!(process.env.OTEL_EXPORTER_OTLP_HEADERS || process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS);
564
+ console.log("OpenTelemetry");
565
+ console.log(` Enabled: ${s.telemetryEnabled ? "yes" : "no"}`);
566
+ console.log(` Endpoint: ${endpoint}`);
567
+ console.log(` Protocol: ${s.telemetryProtocol}`);
568
+ console.log(` Service: ${s.telemetryServiceName}`);
569
+ console.log(` Auto-instrument agents: ${s.telemetryAutoInstrumentAgents ? "yes" : "no"}`);
570
+ console.log(` Capture content: ${s.telemetryCaptureContent ? "yes" : "no"}`);
571
+ console.log(` Headers: ${hasHeaders ? "configured" : "not configured"}`);
572
+ });
573
+ await program.parseAsync(process.argv);
@@ -0,0 +1,7 @@
1
+ export function formatCmd(command, args) {
2
+ const full = `${command} ${args.join(" ")}`.trim();
3
+ return full.length > 30 ? full.slice(0, 27) + "..." : full;
4
+ }
5
+ export function pad(str, width) {
6
+ return str.length >= width ? str + " " : str + " ".repeat(width - str.length);
7
+ }