@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
@@ -0,0 +1,117 @@
1
+ import net from "node:net";
2
+ import { getSocketPath, removeSocketFile } from "../state/index.js";
3
+ import { t } from "../../shared/i18n/index.js";
4
+ import { send } from "../ipc/send.js";
5
+ import { dispatch } from "./handlers/index.js";
6
+ export class IpcServer {
7
+ constructor(manager, taskManager, settingsManager, telemetryManager) {
8
+ this.clients = new Set();
9
+ this.subscribers = new Set();
10
+ this.manager = manager;
11
+ this.taskManager = taskManager;
12
+ this.settingsManager = settingsManager;
13
+ this.telemetryManager = telemetryManager;
14
+ this.socketPath = getSocketPath();
15
+ this.server = net.createServer((socket) => this.handleConnection(socket));
16
+ }
17
+ async listen() {
18
+ removeSocketFile();
19
+ return new Promise((resolve, reject) => {
20
+ this.server.on("error", reject);
21
+ this.server.listen(this.socketPath, () => {
22
+ this.server.removeListener("error", reject);
23
+ resolve();
24
+ });
25
+ });
26
+ }
27
+ async close() {
28
+ for (const client of this.clients) {
29
+ client.destroy();
30
+ }
31
+ this.clients.clear();
32
+ this.subscribers.clear();
33
+ return new Promise((resolve) => {
34
+ this.server.close(() => {
35
+ removeSocketFile();
36
+ resolve();
37
+ });
38
+ });
39
+ }
40
+ pushEvent(event, data) {
41
+ for (const socket of this.subscribers) {
42
+ try {
43
+ send(socket, { type: "event", event, data });
44
+ }
45
+ catch {
46
+ this.subscribers.delete(socket);
47
+ }
48
+ }
49
+ }
50
+ handleConnection(socket) {
51
+ this.clients.add(socket);
52
+ socket.on("close", () => {
53
+ this.clients.delete(socket);
54
+ this.subscribers.delete(socket);
55
+ });
56
+ let buffer = "";
57
+ socket.on("data", (chunk) => {
58
+ buffer += chunk.toString();
59
+ const lines = buffer.split("\n");
60
+ buffer = lines.pop() ?? "";
61
+ for (const line of lines) {
62
+ if (line.trim()) {
63
+ try {
64
+ const request = JSON.parse(line);
65
+ this.handleRequest(request, socket);
66
+ }
67
+ catch {
68
+ send(socket, { type: "error", message: t("errors.invalidJson") });
69
+ }
70
+ }
71
+ }
72
+ });
73
+ socket.on("close", () => {
74
+ this.clients.delete(socket);
75
+ });
76
+ socket.on("error", () => {
77
+ this.clients.delete(socket);
78
+ });
79
+ }
80
+ respondOk(socket, ok, id, data) {
81
+ if (!ok) {
82
+ send(socket, { type: "error", message: t("errors.loopNotFound", { id }) });
83
+ }
84
+ else if (data !== undefined) {
85
+ send(socket, { type: "ok", data });
86
+ }
87
+ else {
88
+ send(socket, { type: "ok" });
89
+ }
90
+ }
91
+ async handleRequest(request, socket) {
92
+ // Dispatch to extracted handlers
93
+ const handled = await dispatch(request, socket, {
94
+ manager: this.manager,
95
+ taskManager: this.taskManager,
96
+ settingsManager: this.settingsManager,
97
+ telemetryManager: this.telemetryManager,
98
+ respondOk: (s, ok, id, data) => this.respondOk(s, ok, id, data),
99
+ });
100
+ if (!handled) {
101
+ // Handle non-dispatchable cases inline
102
+ switch (request.type) {
103
+ case "subscribe": {
104
+ this.subscribers.add(socket);
105
+ send(socket, { type: "ok" });
106
+ break;
107
+ }
108
+ case "shutdown": {
109
+ send(socket, { type: "ok" });
110
+ await this.manager.shutdown();
111
+ await this.close();
112
+ process.exit(0);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,73 @@
1
+ import fs from "node:fs";
2
+ import { settingsJson } from "../shared/config/paths.js";
3
+ import { daemonLog } from "./daemon-log.js";
4
+ const DEFAULTS = {
5
+ httpApiEnabled: true,
6
+ mcpApiEnabled: true,
7
+ // Binds to all interfaces by default: the daemon is meant to be reachable
8
+ // (e.g. over SSH/Tailscale/LAN). The API is unauthenticated, so securing
9
+ // access is the operator's job at the network layer; restrict the bind with
10
+ // `loop-task http-host <ip|local>` when you want loopback-only.
11
+ httpApiHost: "0.0.0.0",
12
+ telemetryEnabled: true,
13
+ telemetryEndpoint: "http://127.0.0.1:4318",
14
+ telemetryProtocol: "http/protobuf",
15
+ telemetryAutoInstrumentAgents: true,
16
+ telemetryCaptureContent: false,
17
+ telemetryCaptureCommandOutput: false,
18
+ telemetryServiceName: "loop-task",
19
+ };
20
+ export class SettingsManager {
21
+ constructor() {
22
+ this.settings = { ...DEFAULTS };
23
+ this.listeners = [];
24
+ }
25
+ load() {
26
+ try {
27
+ const raw = fs.readFileSync(settingsJson(), "utf-8");
28
+ const parsed = JSON.parse(raw);
29
+ if (typeof parsed === "object" && parsed !== null) {
30
+ this.settings = { ...DEFAULTS, ...parsed };
31
+ }
32
+ }
33
+ catch {
34
+ this.settings = { ...DEFAULTS };
35
+ }
36
+ }
37
+ save() {
38
+ const tmp = settingsJson() + ".tmp";
39
+ fs.writeFileSync(tmp, JSON.stringify(this.settings, null, 2));
40
+ fs.renameSync(tmp, settingsJson());
41
+ }
42
+ get() {
43
+ return { ...this.settings };
44
+ }
45
+ set(partial) {
46
+ this.settings = { ...this.settings, ...partial };
47
+ this.save();
48
+ for (const cb of this.listeners) {
49
+ try {
50
+ cb(this.get());
51
+ }
52
+ catch (err) {
53
+ daemonLog(`settings onChange error: ${String(err)}`);
54
+ }
55
+ }
56
+ return this.get();
57
+ }
58
+ getTelemetrySettings() {
59
+ const s = this.settings;
60
+ return {
61
+ enabled: s.telemetryEnabled,
62
+ endpoint: s.telemetryEndpoint,
63
+ protocol: s.telemetryProtocol,
64
+ autoInstrumentAgents: s.telemetryAutoInstrumentAgents,
65
+ captureContent: s.telemetryCaptureContent,
66
+ captureCommandOutput: s.telemetryCaptureCommandOutput,
67
+ serviceName: s.telemetryServiceName,
68
+ };
69
+ }
70
+ onChange(callback) {
71
+ this.listeners.push(callback);
72
+ }
73
+ }
@@ -0,0 +1,204 @@
1
+ import { spawn, execSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { readDaemonPid, isDaemonAlive, removeDaemonPid, removeDaemonSignature, readDaemonSignature, computeCodeSignature, getSocketPath, getDaemonStartLockFile, } from "../state/index.js";
6
+ import { t } from "../../shared/i18n/index.js";
7
+ import { DAEMON_POLL_MS, DAEMON_SPAWN_TIMEOUT_MS } from "../../shared/config/constants.js";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ let cachedSignature = null;
11
+ const START_LOCK_TIMEOUT_MS = 5000;
12
+ function currentCodeSignature() {
13
+ if (cachedSignature === null) {
14
+ cachedSignature = computeCodeSignature();
15
+ }
16
+ return cachedSignature;
17
+ }
18
+ function blockingWait(ms) {
19
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
20
+ }
21
+ function acquireStartLock() {
22
+ const lockPath = getDaemonStartLockFile();
23
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
24
+ const deadline = Date.now() + START_LOCK_TIMEOUT_MS;
25
+ while (Date.now() < deadline) {
26
+ try {
27
+ const fd = fs.openSync(lockPath, "wx");
28
+ fs.writeFileSync(fd, String(process.pid));
29
+ return fd;
30
+ }
31
+ catch (error) {
32
+ if (error.code !== "EEXIST") {
33
+ throw error;
34
+ }
35
+ let ownerPid = null;
36
+ try {
37
+ const raw = fs.readFileSync(lockPath, "utf-8").trim();
38
+ const parsed = Number.parseInt(raw, 10);
39
+ ownerPid = Number.isNaN(parsed) ? null : parsed;
40
+ }
41
+ catch {
42
+ // The lock may be between creation and its owner PID write.
43
+ }
44
+ if (ownerPid !== null && !isDaemonAlive(ownerPid)) {
45
+ try {
46
+ fs.unlinkSync(lockPath);
47
+ }
48
+ catch {
49
+ // Another waiter may have removed or replaced the stale lock.
50
+ }
51
+ }
52
+ else {
53
+ blockingWait(DAEMON_POLL_MS);
54
+ }
55
+ }
56
+ }
57
+ throw new Error("Timed out waiting for another process to start the daemon");
58
+ }
59
+ function releaseStartLock(fd) {
60
+ fs.closeSync(fd);
61
+ try {
62
+ const lockPath = getDaemonStartLockFile();
63
+ if (fs.readFileSync(lockPath, "utf-8").trim() === String(process.pid)) {
64
+ fs.unlinkSync(lockPath);
65
+ }
66
+ }
67
+ catch {
68
+ // The lock may already have been cleaned up after an interrupted start.
69
+ }
70
+ }
71
+ /**
72
+ * Find PIDs of all running loop-task daemon processes, excluding the current
73
+ * process and optionally a tracked PID.
74
+ */
75
+ function findOrphanedDaemonPids(excludePid) {
76
+ const pids = [];
77
+ try {
78
+ const platform = process.platform;
79
+ let output;
80
+ if (platform === "win32") {
81
+ // Use Get-CimInstance instead of wmic (removed in Windows 11 24H2).
82
+ // Output format: one line per process: "CommandLine <tab> ProcessId"
83
+ output = execSync(`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name='node.exe'\\" | Select-Object CommandLine,ProcessId | Format-Table -HideTableHeaders"`, { encoding: "utf-8", timeout: 5000 });
84
+ for (const line of output.split("\n")) {
85
+ if (line.includes("daemon/index.js") || line.includes("daemon/index.ts")) {
86
+ const pid = parseInt(line.trim().split(/\s+/).pop() ?? "", 10);
87
+ if (!Number.isNaN(pid) && pid !== process.pid && pid !== excludePid) {
88
+ pids.push(pid);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ else {
94
+ output = execSync("pgrep -f 'daemon/index'", {
95
+ encoding: "utf-8",
96
+ timeout: 3000,
97
+ });
98
+ for (const line of output.split("\n")) {
99
+ const pid = parseInt(line.trim(), 10);
100
+ if (!Number.isNaN(pid) && pid !== process.pid && pid !== excludePid) {
101
+ pids.push(pid);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ catch {
107
+ // pgrep not available or no matches
108
+ }
109
+ return pids;
110
+ }
111
+ export function stopDaemon(pid) {
112
+ killDaemonPid(pid);
113
+ killOrphanedDaemons(pid);
114
+ removeDaemonPid();
115
+ removeDaemonSignature();
116
+ }
117
+ function killDaemonPid(pid) {
118
+ try {
119
+ process.kill(pid, "SIGTERM");
120
+ }
121
+ catch {
122
+ // already gone
123
+ }
124
+ const deadline = Date.now() + DAEMON_SPAWN_TIMEOUT_MS;
125
+ while (Date.now() < deadline) {
126
+ if (!isDaemonAlive(pid)) {
127
+ break;
128
+ }
129
+ blockingWait(DAEMON_POLL_MS);
130
+ }
131
+ if (isDaemonAlive(pid)) {
132
+ try {
133
+ process.kill(pid, "SIGKILL");
134
+ }
135
+ catch {
136
+ // best effort
137
+ }
138
+ }
139
+ }
140
+ function killOrphanedDaemons(excludePid) {
141
+ const orphans = findOrphanedDaemonPids(excludePid);
142
+ for (const orphanPid of orphans) {
143
+ try {
144
+ process.kill(orphanPid, "SIGTERM");
145
+ }
146
+ catch {
147
+ // already gone
148
+ }
149
+ }
150
+ blockingWait(1000);
151
+ for (const pid of findOrphanedDaemonPids(excludePid)) {
152
+ try {
153
+ process.kill(pid, "SIGKILL");
154
+ }
155
+ catch {
156
+ // best effort
157
+ }
158
+ }
159
+ }
160
+ export function ensureDaemon() {
161
+ const lockFd = acquireStartLock();
162
+ try {
163
+ const currentSignature = currentCodeSignature();
164
+ const pid = readDaemonPid();
165
+ if (pid !== null && isDaemonAlive(pid)) {
166
+ if (readDaemonSignature() === currentSignature) {
167
+ return;
168
+ }
169
+ stopDaemon(pid);
170
+ }
171
+ else if (pid !== null) {
172
+ removeDaemonPid();
173
+ removeDaemonSignature();
174
+ }
175
+ const daemonScript = path.resolve(__dirname, __filename.endsWith(".ts") ? "../index.ts" : "../index.js");
176
+ const args = __filename.endsWith(".ts")
177
+ ? process.execPath.includes("bun")
178
+ ? [daemonScript]
179
+ : ["--import", "tsx", daemonScript]
180
+ : [daemonScript];
181
+ const child = spawn(process.execPath, args, {
182
+ detached: true,
183
+ stdio: "ignore",
184
+ });
185
+ child.unref();
186
+ const deadline = Date.now() + DAEMON_SPAWN_TIMEOUT_MS;
187
+ while (Date.now() < deadline) {
188
+ const newPid = readDaemonPid();
189
+ if (newPid !== null &&
190
+ isDaemonAlive(newPid) &&
191
+ readDaemonSignature() === currentSignature) {
192
+ return;
193
+ }
194
+ blockingWait(DAEMON_POLL_MS);
195
+ }
196
+ throw new Error(t("errors.daemonFailedToStart"));
197
+ }
198
+ finally {
199
+ releaseStartLock(lockFd);
200
+ }
201
+ }
202
+ export function getSocket() {
203
+ return getSocketPath();
204
+ }
@@ -0,0 +1,209 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { fileURLToPath } from "node:url";
5
+ import { removeIfExists, writeFileAtomic } from "../../shared/fs-utils.js";
6
+ import { getLoopsDir, getTasksDir, getLogsDir, logFile, getPidFile, getSignatureFile, getSocketPath, loopsJson, tasksJson, getDataDir, } from "../../shared/config/paths.js";
7
+ export { getDataDir, getPidFile, getSocketPath, getDaemonStartLockFile } from "../../shared/config/paths.js";
8
+ let selfWriteNotifier = null;
9
+ export function setSelfWriteNotifier(notifier) {
10
+ selfWriteNotifier = notifier;
11
+ }
12
+ function ensureDirs() {
13
+ fs.mkdirSync(getDataDir(), { recursive: true });
14
+ fs.mkdirSync(getLogsDir(), { recursive: true });
15
+ }
16
+ function readJsonArray(filePath) {
17
+ if (!fs.existsSync(filePath))
18
+ return [];
19
+ try {
20
+ const raw = fs.readFileSync(filePath, "utf-8");
21
+ const parsed = JSON.parse(raw);
22
+ return Array.isArray(parsed) ? parsed : [];
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ }
28
+ function writeJsonArray(filePath, items) {
29
+ ensureDirs();
30
+ const content = JSON.stringify(items, null, 2);
31
+ writeFileAtomic(filePath, content);
32
+ if (selfWriteNotifier)
33
+ selfWriteNotifier(filePath, content);
34
+ }
35
+ export function migrateLoopsToJson() {
36
+ ensureDirs();
37
+ const jsonFile = loopsJson();
38
+ if (fs.existsSync(jsonFile))
39
+ return;
40
+ const dir = getLoopsDir();
41
+ if (!fs.existsSync(dir))
42
+ return;
43
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
44
+ if (files.length === 0)
45
+ return;
46
+ const loops = [];
47
+ for (const file of files) {
48
+ try {
49
+ const raw = fs.readFileSync(path.join(dir, file), "utf-8");
50
+ loops.push(JSON.parse(raw));
51
+ }
52
+ catch {
53
+ // skip corrupted files
54
+ }
55
+ }
56
+ loops.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
57
+ writeJsonArray(jsonFile, loops);
58
+ }
59
+ export function migrateTasksToJson() {
60
+ ensureDirs();
61
+ const jsonFile = tasksJson();
62
+ if (fs.existsSync(jsonFile))
63
+ return;
64
+ const dir = getTasksDir();
65
+ if (!fs.existsSync(dir))
66
+ return;
67
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
68
+ if (files.length === 0)
69
+ return;
70
+ const tasks = [];
71
+ for (const file of files) {
72
+ try {
73
+ const raw = fs.readFileSync(path.join(dir, file), "utf-8");
74
+ tasks.push(JSON.parse(raw));
75
+ }
76
+ catch {
77
+ // skip corrupted files
78
+ }
79
+ }
80
+ tasks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
81
+ writeJsonArray(jsonFile, tasks);
82
+ }
83
+ export function saveLoop(meta) {
84
+ const loops = readJsonArray(loopsJson());
85
+ const idx = loops.findIndex((l) => l.id === meta.id);
86
+ if (idx >= 0)
87
+ loops[idx] = meta;
88
+ else
89
+ loops.push(meta);
90
+ writeJsonArray(loopsJson(), loops);
91
+ }
92
+ export function loadLoop(id) {
93
+ const loops = readJsonArray(loopsJson());
94
+ return loops.find((l) => l.id === id) ?? null;
95
+ }
96
+ export function loadAllLoops() {
97
+ const loops = readJsonArray(loopsJson());
98
+ return loops.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
99
+ }
100
+ export function deleteLoop(id) {
101
+ const loops = readJsonArray(loopsJson());
102
+ const filtered = loops.filter((l) => l.id !== id);
103
+ if (filtered.length !== loops.length) {
104
+ writeJsonArray(loopsJson(), filtered);
105
+ }
106
+ removeIfExists(logFile(id));
107
+ }
108
+ export function saveTask(task) {
109
+ const tasks = readJsonArray(tasksJson());
110
+ const idx = tasks.findIndex((t) => t.id === task.id);
111
+ if (idx >= 0)
112
+ tasks[idx] = task;
113
+ else
114
+ tasks.push(task);
115
+ writeJsonArray(tasksJson(), tasks);
116
+ }
117
+ export function loadTask(id) {
118
+ const tasks = readJsonArray(tasksJson());
119
+ return tasks.find((t) => t.id === id) ?? null;
120
+ }
121
+ export function loadAllTasks() {
122
+ const tasks = readJsonArray(tasksJson());
123
+ return tasks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
124
+ }
125
+ export function deleteTask(id) {
126
+ const tasks = readJsonArray(tasksJson());
127
+ const filtered = tasks.filter((t) => t.id !== id);
128
+ if (filtered.length !== tasks.length) {
129
+ writeJsonArray(tasksJson(), filtered);
130
+ }
131
+ }
132
+ export function getLogPath(id) {
133
+ ensureDirs();
134
+ return logFile(id);
135
+ }
136
+ export function computeCodeSignature() {
137
+ const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
138
+ let maxMtimeMs = 0;
139
+ let totalSize = 0;
140
+ let count = 0;
141
+ const walk = (dir) => {
142
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
143
+ const full = path.join(dir, entry.name);
144
+ if (entry.isDirectory()) {
145
+ walk(full);
146
+ }
147
+ else if (/\.(ts|tsx|js|jsx|json)$/.test(entry.name)) {
148
+ const stat = fs.statSync(full);
149
+ maxMtimeMs = Math.max(maxMtimeMs, stat.mtimeMs);
150
+ totalSize += stat.size;
151
+ count += 1;
152
+ }
153
+ }
154
+ };
155
+ try {
156
+ walk(srcDir);
157
+ }
158
+ catch {
159
+ return "unknown";
160
+ }
161
+ return crypto
162
+ .createHash("sha1")
163
+ .update(`${Math.round(maxMtimeMs)}:${totalSize}:${count}`)
164
+ .digest("hex")
165
+ .slice(0, 16);
166
+ }
167
+ export function readDaemonSignature() {
168
+ const file = getSignatureFile();
169
+ if (!fs.existsSync(file))
170
+ return null;
171
+ return fs.readFileSync(file, "utf-8").trim() || null;
172
+ }
173
+ export function writeDaemonSignature(signature) {
174
+ ensureDirs();
175
+ fs.writeFileSync(getSignatureFile(), signature);
176
+ }
177
+ export function removeDaemonSignature() {
178
+ removeIfExists(getSignatureFile());
179
+ }
180
+ export function removeSocketFile() {
181
+ if (process.platform === "win32") {
182
+ return;
183
+ }
184
+ removeIfExists(getSocketPath());
185
+ }
186
+ export function readDaemonPid() {
187
+ const file = getPidFile();
188
+ if (!fs.existsSync(file))
189
+ return null;
190
+ const raw = fs.readFileSync(file, "utf-8").trim();
191
+ const pid = parseInt(raw, 10);
192
+ return isNaN(pid) ? null : pid;
193
+ }
194
+ export function writeDaemonPid(pid) {
195
+ ensureDirs();
196
+ fs.writeFileSync(getPidFile(), String(pid));
197
+ }
198
+ export function removeDaemonPid() {
199
+ removeIfExists(getPidFile());
200
+ }
201
+ export function isDaemonAlive(pid) {
202
+ try {
203
+ process.kill(pid, 0);
204
+ return true;
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ }
@@ -0,0 +1,55 @@
1
+ const CLAUDE_BINARIES = ["claude"];
2
+ /**
3
+ * Detects `claude -p ...` or `claude --print ...` invocations and
4
+ * enables Claude Code's native telemetry, routing to loop-task's endpoint.
5
+ */
6
+ export class ClaudeCodeTelemetryIntegration {
7
+ constructor() {
8
+ this.id = "claude-code";
9
+ }
10
+ matches(command, args) {
11
+ const basename = command.split("/").pop()?.replace(/\.exe$/, "").toLowerCase() ?? "";
12
+ if (!CLAUDE_BINARIES.includes(basename))
13
+ return false;
14
+ // Must have -p or --print flag
15
+ return args.includes("-p") || args.includes("--print");
16
+ }
17
+ prepare(invocation, _context) {
18
+ const env = { ...invocation.env };
19
+ // Enable Claude Code's telemetry
20
+ env.CLAUDE_CODE_ENABLE_TELEMETRY = "1";
21
+ // The standard OTEL_* env vars are already set by the adapter's
22
+ // prepareChildProcess. Claude Code reads these natively.
23
+ return { command: invocation.command, args: [...invocation.args], env };
24
+ }
25
+ parseUsage(result) {
26
+ if (!result.stdout)
27
+ return undefined;
28
+ try {
29
+ // Claude Code may output JSON with usage info
30
+ // Try to find JSON in the output
31
+ const jsonMatch = result.stdout.match(/\{[\s\S]*"usage"[\s\S]*\}/);
32
+ if (!jsonMatch)
33
+ return undefined;
34
+ const parsed = JSON.parse(jsonMatch[0]);
35
+ if (typeof parsed !== "object" || parsed === null)
36
+ return undefined;
37
+ const usageData = parsed.usage ?? parsed;
38
+ const usage = {};
39
+ if (typeof usageData.input_tokens === "number")
40
+ usage.inputTokens = usageData.input_tokens;
41
+ if (typeof usageData.output_tokens === "number")
42
+ usage.outputTokens = usageData.output_tokens;
43
+ if (typeof usageData.cache_read_input_tokens === "number")
44
+ usage.cacheReadTokens = usageData.cache_read_input_tokens;
45
+ if (typeof usageData.cache_creation_input_tokens === "number")
46
+ usage.cacheWriteTokens = usageData.cache_creation_input_tokens;
47
+ if (Object.keys(usage).length === 0)
48
+ return undefined;
49
+ return usage;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,20 @@
1
+ import { OpenCodeTelemetryIntegration } from "./opencode-integration.js";
2
+ import { ClaudeCodeTelemetryIntegration } from "./claude-code-integration.js";
3
+ /** Built-in agent integrations in priority order */
4
+ const INTEGRATIONS = [
5
+ new OpenCodeTelemetryIntegration(),
6
+ new ClaudeCodeTelemetryIntegration(),
7
+ ];
8
+ /**
9
+ * Detect which agent integration matches the given command.
10
+ * Returns the first matching integration, or undefined if none match.
11
+ */
12
+ export function detectAgentIntegration(command, args) {
13
+ return INTEGRATIONS.find((integration) => integration.matches(command, args));
14
+ }
15
+ /**
16
+ * Get all registered integrations.
17
+ */
18
+ export function getAgentIntegrations() {
19
+ return INTEGRATIONS;
20
+ }
@@ -0,0 +1,3 @@
1
+ export { OpenCodeTelemetryIntegration } from "./opencode-integration.js";
2
+ export { ClaudeCodeTelemetryIntegration } from "./claude-code-integration.js";
3
+ export { detectAgentIntegration, getAgentIntegrations } from "./detect-agent-integration.js";