@intutic/cli 1.3.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 (243) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/cli.d.ts +14 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +348 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/benchmark.d.ts +31 -0
  8. package/dist/commands/benchmark.d.ts.map +1 -0
  9. package/dist/commands/benchmark.js +211 -0
  10. package/dist/commands/benchmark.js.map +1 -0
  11. package/dist/commands/budget.d.ts +7 -0
  12. package/dist/commands/budget.d.ts.map +1 -0
  13. package/dist/commands/budget.js +68 -0
  14. package/dist/commands/budget.js.map +1 -0
  15. package/dist/commands/budgetCompatibility.test.d.ts +2 -0
  16. package/dist/commands/budgetCompatibility.test.d.ts.map +1 -0
  17. package/dist/commands/budgetCompatibility.test.js +19 -0
  18. package/dist/commands/budgetCompatibility.test.js.map +1 -0
  19. package/dist/commands/connect.d.ts +20 -0
  20. package/dist/commands/connect.d.ts.map +1 -0
  21. package/dist/commands/connect.js +1010 -0
  22. package/dist/commands/connect.js.map +1 -0
  23. package/dist/commands/doctor.d.ts +23 -0
  24. package/dist/commands/doctor.d.ts.map +1 -0
  25. package/dist/commands/doctor.js +429 -0
  26. package/dist/commands/doctor.js.map +1 -0
  27. package/dist/commands/enterprise-install.d.ts +36 -0
  28. package/dist/commands/enterprise-install.d.ts.map +1 -0
  29. package/dist/commands/enterprise-install.js +289 -0
  30. package/dist/commands/enterprise-install.js.map +1 -0
  31. package/dist/commands/eval.d.ts +13 -0
  32. package/dist/commands/eval.d.ts.map +1 -0
  33. package/dist/commands/eval.js +67 -0
  34. package/dist/commands/eval.js.map +1 -0
  35. package/dist/commands/exec.d.ts +34 -0
  36. package/dist/commands/exec.d.ts.map +1 -0
  37. package/dist/commands/exec.js +114 -0
  38. package/dist/commands/exec.js.map +1 -0
  39. package/dist/commands/exec.test.d.ts +2 -0
  40. package/dist/commands/exec.test.d.ts.map +1 -0
  41. package/dist/commands/exec.test.js +29 -0
  42. package/dist/commands/exec.test.js.map +1 -0
  43. package/dist/commands/export.d.ts +21 -0
  44. package/dist/commands/export.d.ts.map +1 -0
  45. package/dist/commands/export.js +116 -0
  46. package/dist/commands/export.js.map +1 -0
  47. package/dist/commands/init.d.ts +13 -0
  48. package/dist/commands/init.d.ts.map +1 -0
  49. package/dist/commands/init.js +110 -0
  50. package/dist/commands/init.js.map +1 -0
  51. package/dist/commands/install-daemon.d.ts +61 -0
  52. package/dist/commands/install-daemon.d.ts.map +1 -0
  53. package/dist/commands/install-daemon.js +696 -0
  54. package/dist/commands/install-daemon.js.map +1 -0
  55. package/dist/commands/install-daemon.test.d.ts +2 -0
  56. package/dist/commands/install-daemon.test.d.ts.map +1 -0
  57. package/dist/commands/install-daemon.test.js +93 -0
  58. package/dist/commands/install-daemon.test.js.map +1 -0
  59. package/dist/commands/login.d.ts +14 -0
  60. package/dist/commands/login.d.ts.map +1 -0
  61. package/dist/commands/login.js +121 -0
  62. package/dist/commands/login.js.map +1 -0
  63. package/dist/commands/logout.d.ts +8 -0
  64. package/dist/commands/logout.d.ts.map +1 -0
  65. package/dist/commands/logout.js +13 -0
  66. package/dist/commands/logout.js.map +1 -0
  67. package/dist/commands/mdm.d.ts +23 -0
  68. package/dist/commands/mdm.d.ts.map +1 -0
  69. package/dist/commands/mdm.js +134 -0
  70. package/dist/commands/mdm.js.map +1 -0
  71. package/dist/commands/policy.d.ts +19 -0
  72. package/dist/commands/policy.d.ts.map +1 -0
  73. package/dist/commands/policy.js +232 -0
  74. package/dist/commands/policy.js.map +1 -0
  75. package/dist/commands/prompt.d.ts +11 -0
  76. package/dist/commands/prompt.d.ts.map +1 -0
  77. package/dist/commands/prompt.js +71 -0
  78. package/dist/commands/prompt.js.map +1 -0
  79. package/dist/commands/skill.d.ts +31 -0
  80. package/dist/commands/skill.d.ts.map +1 -0
  81. package/dist/commands/skill.js +349 -0
  82. package/dist/commands/skill.js.map +1 -0
  83. package/dist/commands/sop-audit.d.ts +17 -0
  84. package/dist/commands/sop-audit.d.ts.map +1 -0
  85. package/dist/commands/sop-audit.js +136 -0
  86. package/dist/commands/sop-audit.js.map +1 -0
  87. package/dist/commands/sops.d.ts +4 -0
  88. package/dist/commands/sops.d.ts.map +1 -0
  89. package/dist/commands/sops.js +88 -0
  90. package/dist/commands/sops.js.map +1 -0
  91. package/dist/commands/status.d.ts +11 -0
  92. package/dist/commands/status.d.ts.map +1 -0
  93. package/dist/commands/status.js +107 -0
  94. package/dist/commands/status.js.map +1 -0
  95. package/dist/commands/syncContext.d.ts +18 -0
  96. package/dist/commands/syncContext.d.ts.map +1 -0
  97. package/dist/commands/syncContext.js +37 -0
  98. package/dist/commands/syncContext.js.map +1 -0
  99. package/dist/commands/traces.d.ts +32 -0
  100. package/dist/commands/traces.d.ts.map +1 -0
  101. package/dist/commands/traces.js +207 -0
  102. package/dist/commands/traces.js.map +1 -0
  103. package/dist/commands/whoami.d.ts +12 -0
  104. package/dist/commands/whoami.d.ts.map +1 -0
  105. package/dist/commands/whoami.js +34 -0
  106. package/dist/commands/whoami.js.map +1 -0
  107. package/dist/config/keychain.d.ts +28 -0
  108. package/dist/config/keychain.d.ts.map +1 -0
  109. package/dist/config/keychain.js +98 -0
  110. package/dist/config/keychain.js.map +1 -0
  111. package/dist/config/paths.d.ts +30 -0
  112. package/dist/config/paths.d.ts.map +1 -0
  113. package/dist/config/paths.js +53 -0
  114. package/dist/config/paths.js.map +1 -0
  115. package/dist/config/store.d.ts +25 -0
  116. package/dist/config/store.d.ts.map +1 -0
  117. package/dist/config/store.js +93 -0
  118. package/dist/config/store.js.map +1 -0
  119. package/dist/harness/aider.d.ts +13 -0
  120. package/dist/harness/aider.d.ts.map +1 -0
  121. package/dist/harness/aider.js +48 -0
  122. package/dist/harness/aider.js.map +1 -0
  123. package/dist/harness/antigravity.d.ts +12 -0
  124. package/dist/harness/antigravity.d.ts.map +1 -0
  125. package/dist/harness/antigravity.js +69 -0
  126. package/dist/harness/antigravity.js.map +1 -0
  127. package/dist/harness/base.d.ts +20 -0
  128. package/dist/harness/base.d.ts.map +1 -0
  129. package/dist/harness/base.js +73 -0
  130. package/dist/harness/base.js.map +1 -0
  131. package/dist/harness/claudeCode.d.ts +2 -0
  132. package/dist/harness/claudeCode.d.ts.map +1 -0
  133. package/dist/harness/claudeCode.js +9 -0
  134. package/dist/harness/claudeCode.js.map +1 -0
  135. package/dist/harness/claudeDesktop.d.ts +18 -0
  136. package/dist/harness/claudeDesktop.d.ts.map +1 -0
  137. package/dist/harness/claudeDesktop.js +69 -0
  138. package/dist/harness/claudeDesktop.js.map +1 -0
  139. package/dist/harness/cline.d.ts +13 -0
  140. package/dist/harness/cline.d.ts.map +1 -0
  141. package/dist/harness/cline.js +75 -0
  142. package/dist/harness/cline.js.map +1 -0
  143. package/dist/harness/codex.d.ts +13 -0
  144. package/dist/harness/codex.d.ts.map +1 -0
  145. package/dist/harness/codex.js +86 -0
  146. package/dist/harness/codex.js.map +1 -0
  147. package/dist/harness/continue.d.ts +14 -0
  148. package/dist/harness/continue.d.ts.map +1 -0
  149. package/dist/harness/continue.js +97 -0
  150. package/dist/harness/continue.js.map +1 -0
  151. package/dist/harness/cursor.d.ts +14 -0
  152. package/dist/harness/cursor.d.ts.map +1 -0
  153. package/dist/harness/cursor.js +65 -0
  154. package/dist/harness/cursor.js.map +1 -0
  155. package/dist/harness/detector.d.ts +29 -0
  156. package/dist/harness/detector.d.ts.map +1 -0
  157. package/dist/harness/detector.js +81 -0
  158. package/dist/harness/detector.js.map +1 -0
  159. package/dist/harness/githubCopilot.d.ts +12 -0
  160. package/dist/harness/githubCopilot.d.ts.map +1 -0
  161. package/dist/harness/githubCopilot.js +54 -0
  162. package/dist/harness/githubCopilot.js.map +1 -0
  163. package/dist/harness/goose.d.ts +13 -0
  164. package/dist/harness/goose.d.ts.map +1 -0
  165. package/dist/harness/goose.js +45 -0
  166. package/dist/harness/goose.js.map +1 -0
  167. package/dist/harness/hermes.d.ts +12 -0
  168. package/dist/harness/hermes.d.ts.map +1 -0
  169. package/dist/harness/hermes.js +51 -0
  170. package/dist/harness/hermes.js.map +1 -0
  171. package/dist/harness/n8n.d.ts +17 -0
  172. package/dist/harness/n8n.d.ts.map +1 -0
  173. package/dist/harness/n8n.js +125 -0
  174. package/dist/harness/n8n.js.map +1 -0
  175. package/dist/harness/openWebUI.d.ts +13 -0
  176. package/dist/harness/openWebUI.d.ts.map +1 -0
  177. package/dist/harness/openWebUI.js +25 -0
  178. package/dist/harness/openWebUI.js.map +1 -0
  179. package/dist/harness/openclaw.d.ts +12 -0
  180. package/dist/harness/openclaw.d.ts.map +1 -0
  181. package/dist/harness/openclaw.js +51 -0
  182. package/dist/harness/openclaw.js.map +1 -0
  183. package/dist/harness/openhands.d.ts +12 -0
  184. package/dist/harness/openhands.d.ts.map +1 -0
  185. package/dist/harness/openhands.js +67 -0
  186. package/dist/harness/openhands.js.map +1 -0
  187. package/dist/harness/pi.d.ts +12 -0
  188. package/dist/harness/pi.d.ts.map +1 -0
  189. package/dist/harness/pi.js +51 -0
  190. package/dist/harness/pi.js.map +1 -0
  191. package/dist/harness/rooCode.d.ts +14 -0
  192. package/dist/harness/rooCode.d.ts.map +1 -0
  193. package/dist/harness/rooCode.js +77 -0
  194. package/dist/harness/rooCode.js.map +1 -0
  195. package/dist/harness/types.d.ts +30 -0
  196. package/dist/harness/types.d.ts.map +1 -0
  197. package/dist/harness/types.js +32 -0
  198. package/dist/harness/types.js.map +1 -0
  199. package/dist/harness/vscodeSettingsWriter.d.ts +40 -0
  200. package/dist/harness/vscodeSettingsWriter.d.ts.map +1 -0
  201. package/dist/harness/vscodeSettingsWriter.js +116 -0
  202. package/dist/harness/vscodeSettingsWriter.js.map +1 -0
  203. package/dist/harness/windsurf.d.ts +14 -0
  204. package/dist/harness/windsurf.d.ts.map +1 -0
  205. package/dist/harness/windsurf.js +64 -0
  206. package/dist/harness/windsurf.js.map +1 -0
  207. package/dist/lib/api.d.ts +48 -0
  208. package/dist/lib/api.d.ts.map +1 -0
  209. package/dist/lib/api.js +128 -0
  210. package/dist/lib/api.js.map +1 -0
  211. package/dist/lib/envInjector.d.ts +24 -0
  212. package/dist/lib/envInjector.d.ts.map +1 -0
  213. package/dist/lib/envInjector.js +149 -0
  214. package/dist/lib/envInjector.js.map +1 -0
  215. package/dist/lib/envInjector.test.d.ts +2 -0
  216. package/dist/lib/envInjector.test.d.ts.map +1 -0
  217. package/dist/lib/envInjector.test.js +32 -0
  218. package/dist/lib/envInjector.test.js.map +1 -0
  219. package/dist/lib/gitHooks.d.ts +19 -0
  220. package/dist/lib/gitHooks.d.ts.map +1 -0
  221. package/dist/lib/gitHooks.js +58 -0
  222. package/dist/lib/gitHooks.js.map +1 -0
  223. package/dist/lib/hash.d.ts +11 -0
  224. package/dist/lib/hash.d.ts.map +1 -0
  225. package/dist/lib/hash.js +18 -0
  226. package/dist/lib/hash.js.map +1 -0
  227. package/dist/lib/logger.d.ts +18 -0
  228. package/dist/lib/logger.d.ts.map +1 -0
  229. package/dist/lib/logger.js +33 -0
  230. package/dist/lib/logger.js.map +1 -0
  231. package/dist/lib/onboarding.d.ts +10 -0
  232. package/dist/lib/onboarding.d.ts.map +1 -0
  233. package/dist/lib/onboarding.js +178 -0
  234. package/dist/lib/onboarding.js.map +1 -0
  235. package/dist/lib/process.d.ts +53 -0
  236. package/dist/lib/process.d.ts.map +1 -0
  237. package/dist/lib/process.js +181 -0
  238. package/dist/lib/process.js.map +1 -0
  239. package/dist/lib/process.test.d.ts +2 -0
  240. package/dist/lib/process.test.d.ts.map +1 -0
  241. package/dist/lib/process.test.js +44 -0
  242. package/dist/lib/process.test.js.map +1 -0
  243. package/package.json +55 -0
@@ -0,0 +1,1010 @@
1
+ /**
2
+ * `intutic connect` — Start the sync daemon.
3
+ *
4
+ * Runs a persistent WebSocket client for real-time config updates,
5
+ * a real-time filesystem watcher for configuration drift detection,
6
+ * and a 30-second HTTP polling loop as a secondary fallback.
7
+ *
8
+ * LLD #14 — connect.ts
9
+ * HLD §3.14 — Real-Time State Mirroring
10
+ *
11
+ * @module
12
+ */
13
+ import * as node_path from 'node:path';
14
+ import * as node_fs from 'node:fs/promises';
15
+ import { log } from '../lib/logger.js';
16
+ import { loadCredentials, loadConfig, saveConfig, loadIntegrity, saveIntegrity, } from '../config/store.js';
17
+ import { resolveControlPlaneUrl, getIntuticDir } from '../config/paths.js';
18
+ import { createApiClient } from '../lib/api.js';
19
+ import { getActiveAgentProcesses } from '../lib/process.js';
20
+ import { getAdapter } from '../harness/detector.js';
21
+ import { printOnboardingGuide } from '../lib/onboarding.js';
22
+ import { newIso } from '@intutic/id';
23
+ import pc from 'picocolors';
24
+ import { SyncWsClient, startWatcher, updatePreToolUseHooks, injectMcpServer, guardSettingsFile, writeRuntimeEnv, runComplianceProbes, drainHookEvents, syncOfflineTraces, TrajectoryMonitor, } from '@intutic/sync-daemon';
25
+ import { watch } from 'chokidar';
26
+ import Redis from 'ioredis';
27
+ import * as net from 'node:net';
28
+ import { spawn, execSync } from 'node:child_process';
29
+ import { createWriteStream } from 'node:fs';
30
+ const DEFAULT_POLL_INTERVAL = 30_000;
31
+ function isPortInUse(port) {
32
+ return new Promise((resolve) => {
33
+ const server = net.createServer();
34
+ server.once('error', (err) => {
35
+ if (err.code === 'EADDRINUSE') {
36
+ resolve(true);
37
+ }
38
+ else {
39
+ resolve(false);
40
+ }
41
+ });
42
+ server.once('listening', () => {
43
+ server.close();
44
+ resolve(false);
45
+ });
46
+ server.listen(port, '127.0.0.1');
47
+ });
48
+ }
49
+ function isValkeyRunning(port = 6379) {
50
+ return new Promise((resolve) => {
51
+ const socket = new net.Socket();
52
+ socket.setTimeout(1000);
53
+ socket.once('connect', () => {
54
+ socket.destroy();
55
+ resolve(true);
56
+ });
57
+ socket.once('error', () => {
58
+ resolve(false);
59
+ });
60
+ socket.once('timeout', () => {
61
+ socket.destroy();
62
+ resolve(false);
63
+ });
64
+ socket.connect(port, '127.0.0.1');
65
+ });
66
+ }
67
+ async function downloadValkeyBinary(destPath) {
68
+ const platform = process.platform;
69
+ const arch = process.arch;
70
+ let assetName = '';
71
+ if (platform === 'darwin') {
72
+ if (arch === 'arm64')
73
+ assetName = 'valkey-server-darwin-arm64';
74
+ else if (arch === 'x64')
75
+ assetName = 'valkey-server-darwin-x64';
76
+ }
77
+ else if (platform === 'linux') {
78
+ if (arch === 'x64')
79
+ assetName = 'valkey-server-linux-x64';
80
+ else if (arch === 'arm64')
81
+ assetName = 'valkey-server-linux-arm64';
82
+ }
83
+ else if (platform === 'win32') {
84
+ if (arch === 'x64')
85
+ assetName = 'valkey-server-win32-x64.exe';
86
+ }
87
+ if (!assetName) {
88
+ throw new Error(`Unsupported platform/architecture for Valkey: ${platform}-${arch}`);
89
+ }
90
+ const valkeyVersion = '1.0.0';
91
+ const url = `https://intutic.ai/valkey/v${valkeyVersion}/${assetName}`;
92
+ log.info(`Downloading precompiled Valkey server from ${url}...`);
93
+ const response = await fetch(url);
94
+ if (!response.ok) {
95
+ throw new Error(`Failed to download Valkey binary: HTTP ${response.status} ${response.statusText}`);
96
+ }
97
+ const arrayBuffer = await response.arrayBuffer();
98
+ const buffer = Buffer.from(arrayBuffer);
99
+ const destDir = node_path.dirname(destPath);
100
+ await node_fs.mkdir(destDir, { recursive: true });
101
+ await node_fs.writeFile(destPath, buffer);
102
+ if (platform !== 'win32') {
103
+ await node_fs.chmod(destPath, 0o755);
104
+ }
105
+ log.success(`Successfully downloaded and installed Valkey binary to ${destPath}`);
106
+ return destPath;
107
+ }
108
+ async function downloadProxyBinary(destPath) {
109
+ const platform = process.platform;
110
+ const arch = process.arch;
111
+ let assetName = '';
112
+ if (platform === 'darwin') {
113
+ if (arch === 'arm64')
114
+ assetName = 'intutic-proxy-darwin-arm64';
115
+ else if (arch === 'x64')
116
+ assetName = 'intutic-proxy-darwin-x64';
117
+ }
118
+ else if (platform === 'linux') {
119
+ if (arch === 'x64')
120
+ assetName = 'intutic-proxy-linux-x64';
121
+ else if (arch === 'arm64')
122
+ assetName = 'intutic-proxy-linux-arm64';
123
+ }
124
+ else if (platform === 'win32') {
125
+ if (arch === 'x64')
126
+ assetName = 'intutic-proxy-win32-x64.exe';
127
+ }
128
+ if (!assetName) {
129
+ throw new Error(`Unsupported platform/architecture: ${platform}-${arch}`);
130
+ }
131
+ const cliVersion = '1.0.5';
132
+ const url = `https://intutic.ai/proxy/v${cliVersion}/${assetName}`;
133
+ log.info(`Downloading precompiled Intutic proxy from ${url}...`);
134
+ const response = await fetch(url);
135
+ if (!response.ok) {
136
+ throw new Error(`Failed to download binary from release server: HTTP ${response.status} ${response.statusText}`);
137
+ }
138
+ const arrayBuffer = await response.arrayBuffer();
139
+ const buffer = Buffer.from(arrayBuffer);
140
+ const destDir = node_path.dirname(destPath);
141
+ await node_fs.mkdir(destDir, { recursive: true });
142
+ await node_fs.writeFile(destPath, buffer);
143
+ if (platform !== 'win32') {
144
+ await node_fs.chmod(destPath, 0o755);
145
+ }
146
+ log.success(`Successfully downloaded and installed proxy binary to ${destPath}`);
147
+ return destPath;
148
+ }
149
+ export async function runConnect(opts) {
150
+ // 1. Load credentials + config
151
+ let creds = await loadCredentials();
152
+ if (opts.workspaceId && opts.apiKey) {
153
+ creds = {
154
+ workspaceId: opts.workspaceId,
155
+ apiKey: opts.apiKey,
156
+ email: 'daemon@intutic.ai',
157
+ controlPlaneUrl: opts.controlPlaneUrl ?? 'https://api.intutic.ai',
158
+ storedAt: newIso(),
159
+ };
160
+ }
161
+ if (!creds) {
162
+ log.error('Not authenticated. Run `intutic login` first.');
163
+ process.exit(1);
164
+ }
165
+ let config = loadConfig();
166
+ if (!config && opts.workspaceId && opts.apiKey) {
167
+ config = {
168
+ workspaceId: opts.workspaceId,
169
+ harnesses: [],
170
+ configVersion: 0,
171
+ devMode: opts.dev || false,
172
+ };
173
+ }
174
+ if (!config) {
175
+ log.error('Workspace not initialized. Run `intutic init` first.');
176
+ process.exit(1);
177
+ }
178
+ const safeCreds = creds;
179
+ const safeConfig = config;
180
+ const devMode = opts.dev || process.env.INTUTIC_DEV === '1' || safeConfig.devMode;
181
+ const controlPlaneUrl = opts.controlPlaneUrl || resolveControlPlaneUrl(devMode);
182
+ const pollInterval = opts.interval ? parseInt(opts.interval, 10) : DEFAULT_POLL_INTERVAL;
183
+ const connectedSince = newIso();
184
+ const client = createApiClient(controlPlaneUrl, safeCreds.apiKey);
185
+ log.header('Intutic — Sync Daemon');
186
+ log.field('Workspace', safeCreds.workspaceId);
187
+ log.field('Control Plane', controlPlaneUrl);
188
+ log.field('Poll Interval', `${pollInterval / 1000}s`);
189
+ log.field('Harnesses', safeConfig.harnesses.join(', ') || '(none)');
190
+ // Print onboarding setup instructions for active harnesses
191
+ printOnboardingGuide(safeConfig.harnesses, safeCreds.apiKey, devMode);
192
+ log.info('Starting sync daemon... (Ctrl+C to stop)');
193
+ console.log('');
194
+ // Start the Trajectory Monitor & Valkey Subscriber
195
+ let trajectoryMonitor = null;
196
+ let trajectorySubscriber = null;
197
+ const valkeyUrl = process.env.VALKEY_URL ?? 'redis://127.0.0.1:6379';
198
+ trajectoryMonitor = new TrajectoryMonitor({
199
+ valkeyUrl,
200
+ controlPlaneUrl,
201
+ apiKey: safeCreds.apiKey,
202
+ windowMs: 300_000,
203
+ submitIntervalMs: 60_000,
204
+ });
205
+ try {
206
+ await trajectoryMonitor.start();
207
+ trajectorySubscriber = new Redis(valkeyUrl);
208
+ await trajectorySubscriber.psubscribe('trace:live:*');
209
+ trajectorySubscriber.on('pmessage', (pattern, channel, message) => {
210
+ try {
211
+ const event = JSON.parse(message);
212
+ trajectoryMonitor?.handleTraceEvent(event);
213
+ }
214
+ catch (err) {
215
+ log.warn(`[sync-daemon] Failed to parse trajectory trace event: ${err instanceof Error ? err.message : String(err)}`);
216
+ }
217
+ });
218
+ log.info('[sync-daemon] Trajectory monitor & subscriber started successfully');
219
+ }
220
+ catch (err) {
221
+ log.warn(`[sync-daemon] Could not start trajectory monitor: ${err instanceof Error ? err.message : String(err)}`);
222
+ }
223
+ // 2. AbortController for clean shutdown
224
+ let proxyProc = null;
225
+ const ac = new AbortController();
226
+ const shutdown = () => {
227
+ log.info('Shutting down sync daemon...');
228
+ if (proxyProc) {
229
+ log.info('Stopping managed proxy gateway...');
230
+ proxyProc.kill('SIGTERM');
231
+ }
232
+ trajectoryMonitor?.stop();
233
+ trajectorySubscriber?.disconnect();
234
+ ac.abort();
235
+ };
236
+ process.on('SIGINT', shutdown);
237
+ process.on('SIGTERM', shutdown);
238
+ // 2.4. Pre-flight Valkey Validation
239
+ const valkeyPort = 6379;
240
+ let valkeyActive = await isValkeyRunning(valkeyPort);
241
+ if (!valkeyActive) {
242
+ log.info('Valkey cache not detected on port 6379. Initiating auto-provisioning...');
243
+ // Check if Docker is running
244
+ let dockerActive = false;
245
+ try {
246
+ execSync('docker info', { stdio: 'ignore' });
247
+ dockerActive = true;
248
+ }
249
+ catch { }
250
+ if (dockerActive) {
251
+ log.info('Docker detected. Spawning background Valkey container (intutic-valkey)...');
252
+ try {
253
+ const existing = execSync('docker ps -a --filter name=intutic-valkey --format "{{.Names}}"', { encoding: 'utf8' }).trim();
254
+ if (existing === 'intutic-valkey') {
255
+ execSync('docker start intutic-valkey', { stdio: 'ignore' });
256
+ }
257
+ else {
258
+ execSync('docker run -d --name intutic-valkey -p 6379:6379 valkey/valkey', { stdio: 'ignore' });
259
+ }
260
+ // Wait for Valkey to be ready
261
+ for (let i = 0; i < 10; i++) {
262
+ if (await isValkeyRunning(valkeyPort)) {
263
+ valkeyActive = true;
264
+ log.success('Successfully started background Valkey container (intutic-valkey).');
265
+ break;
266
+ }
267
+ await new Promise(r => setTimeout(r, 500));
268
+ }
269
+ }
270
+ catch (err) {
271
+ log.warn(`Docker auto-spawn failed: ${err.message}`);
272
+ }
273
+ }
274
+ if (!valkeyActive) {
275
+ // Try native binary in PATH
276
+ let hasNativeBinary = false;
277
+ let nativeCmd = 'valkey-server';
278
+ try {
279
+ execSync('which valkey-server', { stdio: 'ignore' });
280
+ hasNativeBinary = true;
281
+ }
282
+ catch {
283
+ try {
284
+ execSync('which redis-server', { stdio: 'ignore' });
285
+ nativeCmd = 'redis-server';
286
+ hasNativeBinary = true;
287
+ }
288
+ catch { }
289
+ }
290
+ if (hasNativeBinary) {
291
+ log.info(`Found native ${nativeCmd} in PATH. Spawning background daemon...`);
292
+ try {
293
+ const proc = spawn(nativeCmd, ['--port', '6379', '--daemonize', 'yes'], { stdio: 'ignore', detached: true });
294
+ proc.unref();
295
+ for (let i = 0; i < 10; i++) {
296
+ if (await isValkeyRunning(valkeyPort)) {
297
+ valkeyActive = true;
298
+ log.success(`Successfully spawned native ${nativeCmd} in background.`);
299
+ break;
300
+ }
301
+ await new Promise(r => setTimeout(r, 500));
302
+ }
303
+ }
304
+ catch (err) {
305
+ log.warn(`Native spawn failed: ${err.message}`);
306
+ }
307
+ }
308
+ }
309
+ if (!valkeyActive) {
310
+ // Dynamic Static Download
311
+ log.info('No native database binary on path. Downloading precompiled static Valkey binary...');
312
+ const globalValkeyBinPath = node_path.join(getIntuticDir(), 'bin', process.platform === 'win32' ? 'valkey-server.exe' : 'valkey-server');
313
+ try {
314
+ await downloadValkeyBinary(globalValkeyBinPath);
315
+ log.info('Spawning downloaded Valkey server in background...');
316
+ const args = process.platform === 'win32' ? ['--port', '6379'] : ['--port', '6379', '--daemonize', 'yes'];
317
+ const proc = spawn(globalValkeyBinPath, args, { stdio: 'ignore', detached: true });
318
+ proc.unref();
319
+ for (let i = 0; i < 10; i++) {
320
+ if (await isValkeyRunning(valkeyPort)) {
321
+ valkeyActive = true;
322
+ log.success('Successfully spawned downloaded Valkey server in background.');
323
+ break;
324
+ }
325
+ await new Promise(r => setTimeout(r, 500));
326
+ }
327
+ }
328
+ catch (err) {
329
+ log.error(`Valkey server auto-download/setup failed: ${err.message}`);
330
+ log.warn('Running in degraded offline mode (caching disabled; local JSONL trace logging active).');
331
+ }
332
+ }
333
+ }
334
+ // 2.5. Manage LiteLLM-Rust Proxy Gateway Process
335
+ const proxyPort = parseInt(process.env.PORT || '4000', 10);
336
+ let exeCmd = 'cargo';
337
+ let exeArgs = ['run', '--manifest-path', node_path.join(safeConfig.workspaceRoot, 'packages', 'proxy', 'Cargo.toml')];
338
+ let proxyEnv = {};
339
+ try {
340
+ const inUse = await isPortInUse(proxyPort);
341
+ if (inUse) {
342
+ log.info(`Proxy already running on port ${proxyPort} (assuming external instance).`);
343
+ }
344
+ else {
345
+ log.info(`Port ${proxyPort} is free. Spawning managed proxy gateway...`);
346
+ const logDir = node_path.join(safeConfig.workspaceRoot, '.intutic', 'logs');
347
+ await node_fs.mkdir(logDir, { recursive: true });
348
+ const logStream = createWriteStream(node_path.join(logDir, 'proxy-gateway.log'), { flags: 'a' });
349
+ // Determine proxy binary or build command
350
+ exeCmd = 'cargo';
351
+ exeArgs = ['run', '--manifest-path', node_path.join(safeConfig.workspaceRoot, 'packages', 'proxy', 'Cargo.toml')];
352
+ if (!devMode) {
353
+ // In production, try to resolve precompiled binary path
354
+ const releasePath = node_path.join(safeConfig.workspaceRoot, 'packages', 'proxy', 'target', 'release', 'intutic-proxy');
355
+ const debugPath = node_path.join(safeConfig.workspaceRoot, 'packages', 'proxy', 'target', 'debug', 'intutic-proxy');
356
+ try {
357
+ await node_fs.access(releasePath);
358
+ exeCmd = releasePath;
359
+ exeArgs = [];
360
+ }
361
+ catch {
362
+ try {
363
+ await node_fs.access(debugPath);
364
+ exeCmd = debugPath;
365
+ exeArgs = [];
366
+ }
367
+ catch {
368
+ // Fallback to globally cached binary in ~/.intutic/bin/
369
+ const globalBinPath = node_path.join(getIntuticDir(), 'bin', process.platform === 'win32' ? 'intutic-proxy.exe' : 'intutic-proxy');
370
+ try {
371
+ await node_fs.access(globalBinPath);
372
+ exeCmd = globalBinPath;
373
+ exeArgs = [];
374
+ }
375
+ catch {
376
+ log.info('Precompiled proxy binary not found in workspace or cache.');
377
+ try {
378
+ const downloadedPath = await downloadProxyBinary(globalBinPath);
379
+ exeCmd = downloadedPath;
380
+ exeArgs = [];
381
+ }
382
+ catch (downloadErr) {
383
+ log.warn(`Auto-download failed: ${downloadErr.message}`);
384
+ log.dim('Falling back to cargo run...');
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+ proxyEnv = {
391
+ ...process.env,
392
+ VALKEY_URL: process.env.VALKEY_URL || 'redis://127.0.0.1:6380',
393
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || '',
394
+ INTUTIC_CONTROL_PLANE_URL: controlPlaneUrl,
395
+ CONTROL_PLANE_URL: controlPlaneUrl,
396
+ INTUTIC_WORKSPACE_ID: safeCreds.workspaceId,
397
+ INTUTIC_API_KEY: safeCreds.apiKey,
398
+ CONFIG_PATH: node_path.join(safeConfig.workspaceRoot, 'config.yaml'),
399
+ };
400
+ proxyProc = spawn(exeCmd, exeArgs, {
401
+ cwd: safeConfig.workspaceRoot,
402
+ env: proxyEnv,
403
+ stdio: ['ignore', 'pipe', 'pipe'],
404
+ });
405
+ proxyProc.stdout?.pipe(logStream);
406
+ proxyProc.stderr?.pipe(logStream);
407
+ proxyProc.on('error', (err) => {
408
+ log.error(`Managed proxy process failed to start: ${err.message}`);
409
+ });
410
+ proxyProc.on('exit', (code, signal) => {
411
+ log.info(`Managed proxy process exited: code=${code}, signal=${signal}`);
412
+ });
413
+ log.success('Managed proxy gateway process spawned.');
414
+ // Wait for ca.crt to be written by the proxy if it doesn't exist
415
+ const caCertPath = node_path.join(getIntuticDir(), 'ca.crt');
416
+ let certExists = false;
417
+ for (let i = 0; i < 20; i++) {
418
+ try {
419
+ await node_fs.access(caCertPath);
420
+ certExists = true;
421
+ break;
422
+ }
423
+ catch {
424
+ await new Promise(r => setTimeout(r, 100));
425
+ }
426
+ }
427
+ if (certExists) {
428
+ // Run trust check and add to keychain
429
+ try {
430
+ if (process.platform === 'darwin') {
431
+ try {
432
+ execSync(`security verify-cert -c "${caCertPath}" 2>/dev/null`, { timeout: 3000 });
433
+ }
434
+ catch {
435
+ log.info('Auto-trusting Intutic SSL CA certificate in macOS Login Keychain...');
436
+ execSync(`security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db "${caCertPath}"`, { stdio: 'ignore' });
437
+ log.success('Successfully trusted SSL CA certificate.');
438
+ }
439
+ }
440
+ else if (process.platform === 'win32') {
441
+ try {
442
+ execSync(`certutil -addstore Root "${caCertPath}"`, { stdio: 'ignore' });
443
+ log.success('Successfully trusted SSL CA certificate.');
444
+ }
445
+ catch (err) {
446
+ log.warn(`Failed to auto-trust SSL certificate on Windows: ${err.message}`);
447
+ }
448
+ }
449
+ }
450
+ catch (err) {
451
+ log.warn(`Could not verify or auto-trust CA certificate: ${err.message}`);
452
+ }
453
+ }
454
+ }
455
+ }
456
+ catch (err) {
457
+ log.warn(`Failed to set up managed proxy gateway: ${err instanceof Error ? err.message : String(err)}`);
458
+ }
459
+ let localConfigVersion = safeConfig.configVersion;
460
+ let lastCachedConfig = null;
461
+ // 3. Define configuration applier function
462
+ async function applySyncConfig(syncConfig, force = false) {
463
+ let sopsWritten = 0;
464
+ lastCachedConfig = syncConfig;
465
+ if (syncConfig.configVersion > localConfigVersion || force) {
466
+ log.info(`Applying configuration v${syncConfig.configVersion}...`);
467
+ // Load and compile local SOP entries
468
+ const localSopEntries = [];
469
+ try {
470
+ const sessionContextPath = node_path.join(safeConfig.workspaceRoot, '.intutic', 'session-context.json');
471
+ let activeLocalSops;
472
+ try {
473
+ const raw = await node_fs.readFile(sessionContextPath, 'utf-8');
474
+ const parsed = JSON.parse(raw);
475
+ activeLocalSops = parsed.activeLocalSops;
476
+ }
477
+ catch {
478
+ // not configured yet
479
+ }
480
+ const sopsDir = node_path.join(safeConfig.workspaceRoot, '.intutic', 'sops');
481
+ const entries = await node_fs.readdir(sopsDir, { withFileTypes: true });
482
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
483
+ const activeDirs = activeLocalSops !== undefined
484
+ ? dirs.filter((d) => activeLocalSops.includes(d))
485
+ : dirs;
486
+ for (const dirName of activeDirs) {
487
+ const dirPath = node_path.join(sopsDir, dirName);
488
+ const files = await node_fs.readdir(dirPath);
489
+ const mdFiles = files.filter((f) => f.endsWith('.md'));
490
+ for (const file of mdFiles) {
491
+ const filePath = node_path.join(dirPath, file);
492
+ const content = await node_fs.readFile(filePath, 'utf-8');
493
+ localSopEntries.push({
494
+ sopId: `local:${dirName}:${file}`,
495
+ title: `Local SOP: ${dirName}/${file}`,
496
+ content,
497
+ contentHash: '',
498
+ harnessTargets: safeConfig.harnesses,
499
+ });
500
+ }
501
+ }
502
+ }
503
+ catch (err) {
504
+ // ignore directory read errors
505
+ }
506
+ const combinedSops = [...syncConfig.sops, ...localSopEntries];
507
+ // a. Write configs for all active harnesses
508
+ for (const harnessType of safeConfig.harnesses) {
509
+ const adapter = getAdapter(harnessType);
510
+ if (!adapter)
511
+ continue;
512
+ const targetSops = combinedSops.filter((sop) => sop.harnessTargets.includes(harnessType));
513
+ if (targetSops.length === 0 && !force)
514
+ continue;
515
+ const written = await adapter.writeConfig(safeConfig.workspaceRoot, targetSops, syncConfig.proxyUrl);
516
+ if (written) {
517
+ sopsWritten += targetSops.length;
518
+ }
519
+ }
520
+ // b. Invalidate/update Claude Code hooks and settings
521
+ if (safeConfig.harnesses.includes('claude-code')) {
522
+ try {
523
+ await updatePreToolUseHooks(safeConfig.workspaceRoot, syncConfig.sops, syncConfig.settings);
524
+ }
525
+ catch (err) {
526
+ log.warn(`Failed to update Claude Code hooks: ${err instanceof Error ? err.message : String(err)}`);
527
+ }
528
+ }
529
+ // c. Inject + proxy-wrap MCP servers across all supported harnesses
530
+ try {
531
+ await injectMcpServer(safeConfig.workspaceRoot, safeCreds.workspaceId);
532
+ }
533
+ catch (err) {
534
+ log.warn(`Failed to inject MCP server configs: ${err instanceof Error ? err.message : String(err)}`);
535
+ }
536
+ localConfigVersion = syncConfig.configVersion;
537
+ saveConfig({ ...safeConfig, configVersion: localConfigVersion });
538
+ }
539
+ // c. Compute file hashes + update integrity store
540
+ const fileHashes = [];
541
+ const canonicalHashes = {};
542
+ // Load current integrity file list
543
+ const integrity = loadIntegrity(safeConfig.workspaceRoot);
544
+ if (integrity) {
545
+ Object.assign(canonicalHashes, integrity.files);
546
+ }
547
+ for (const harnessType of safeConfig.harnesses) {
548
+ const adapter = getAdapter(harnessType);
549
+ if (!adapter || !adapter.configFileName)
550
+ continue;
551
+ const currentHash = await adapter.readCurrentHash(safeConfig.workspaceRoot);
552
+ if (!currentHash)
553
+ continue;
554
+ const canonical = canonicalHashes[adapter.configFileName] ?? currentHash;
555
+ fileHashes.push({
556
+ filePath: adapter.configFileName,
557
+ localHash: currentHash,
558
+ canonicalHash: canonical,
559
+ drifted: currentHash !== canonical,
560
+ });
561
+ // Update canonical hash to current
562
+ canonicalHashes[adapter.configFileName] = currentHash;
563
+ }
564
+ // Save integrity store
565
+ saveIntegrity(safeConfig.workspaceRoot, {
566
+ lastSyncAt: newIso(),
567
+ configVersion: localConfigVersion,
568
+ files: canonicalHashes,
569
+ });
570
+ // d. Report hashes to control plane
571
+ let driftCount = 0;
572
+ if (fileHashes.length > 0) {
573
+ try {
574
+ const hashReport = await client.reportHashes({
575
+ workspaceId: safeCreds.workspaceId,
576
+ harnessType: safeConfig.harnesses[0],
577
+ files: fileHashes,
578
+ reportedAt: newIso(),
579
+ });
580
+ driftCount = hashReport.driftCount;
581
+ }
582
+ catch (err) {
583
+ log.warn(`Failed to report integrity hashes: ${err instanceof Error ? err.message : String(err)}`);
584
+ }
585
+ }
586
+ // e. Health Check & Disaster Recovery (DR)
587
+ let valkeyStatus = 'healthy';
588
+ let proxyStatus = 'healthy';
589
+ let sslTrustStatus = 'trusted';
590
+ // Check & Healing: Valkey
591
+ try {
592
+ const valkeyActive = await isValkeyRunning(6379);
593
+ if (!valkeyActive) {
594
+ valkeyStatus = 'unhealthy';
595
+ log.warn('[DR] Valkey database is offline. Attempting auto-healing restart...');
596
+ // Check if Docker is running
597
+ let dockerActive = false;
598
+ try {
599
+ execSync('docker info', { stdio: 'ignore' });
600
+ dockerActive = true;
601
+ }
602
+ catch { }
603
+ if (dockerActive) {
604
+ try {
605
+ execSync('docker start intutic-valkey', { stdio: 'ignore' });
606
+ log.info('[DR] Successfully sent container start command to intutic-valkey.');
607
+ }
608
+ catch (err) {
609
+ log.warn(`[DR] Docker container start failed: ${err.message}`);
610
+ }
611
+ }
612
+ else {
613
+ let hasNativeBinary = false;
614
+ let nativeCmd = 'valkey-server';
615
+ try {
616
+ execSync('which valkey-server', { stdio: 'ignore' });
617
+ hasNativeBinary = true;
618
+ }
619
+ catch {
620
+ try {
621
+ execSync('which redis-server', { stdio: 'ignore' });
622
+ nativeCmd = 'redis-server';
623
+ hasNativeBinary = true;
624
+ }
625
+ catch { }
626
+ }
627
+ if (hasNativeBinary) {
628
+ try {
629
+ const proc = spawn(nativeCmd, ['--port', '6379', '--daemonize', 'yes'], { stdio: 'ignore', detached: true });
630
+ proc.unref();
631
+ log.info(`[DR] Successfully spawned native ${nativeCmd} in background.`);
632
+ }
633
+ catch (err) {
634
+ log.warn(`[DR] Native daemon spawn failed: ${err.message}`);
635
+ }
636
+ }
637
+ else {
638
+ // Downloaded static
639
+ try {
640
+ const globalValkeyBinPath = node_path.join(getIntuticDir(), 'bin', process.platform === 'win32' ? 'valkey-server.exe' : 'valkey-server');
641
+ await node_fs.access(globalValkeyBinPath);
642
+ const proc = spawn(globalValkeyBinPath, ['--port', '6379', '--daemonize', 'yes'], { stdio: 'ignore', detached: true });
643
+ proc.unref();
644
+ log.info('[DR] Successfully spawned downloaded static Valkey server in background.');
645
+ }
646
+ catch (err) {
647
+ log.warn(`[DR] Static binary spawn failed: ${err.message}`);
648
+ }
649
+ }
650
+ }
651
+ }
652
+ }
653
+ catch {
654
+ valkeyStatus = 'unhealthy';
655
+ }
656
+ // Check & Healing: Proxy
657
+ try {
658
+ const proxyActive = await isPortInUse(proxyPort);
659
+ if (!proxyActive) {
660
+ proxyStatus = 'unhealthy';
661
+ if (proxyProc) {
662
+ log.warn('[DR] Managed proxy gateway process has terminated. Auto-healing re-spawn...');
663
+ const logDir = node_path.join(safeConfig.workspaceRoot, '.intutic', 'logs');
664
+ const logStream = createWriteStream(node_path.join(logDir, 'proxy-gateway.log'), { flags: 'a' });
665
+ proxyProc = spawn(exeCmd, exeArgs, {
666
+ cwd: safeConfig.workspaceRoot,
667
+ env: proxyEnv,
668
+ stdio: ['ignore', 'pipe', 'pipe'],
669
+ });
670
+ proxyProc.stdout?.pipe(logStream);
671
+ proxyProc.stderr?.pipe(logStream);
672
+ proxyProc.on('exit', (code, signal) => {
673
+ log.info(`Managed proxy process exited: code=${code}, signal=${signal}`);
674
+ });
675
+ log.success('[DR] Successfully re-spawned proxy gateway process.');
676
+ }
677
+ else {
678
+ proxyStatus = 'stopped';
679
+ }
680
+ }
681
+ }
682
+ catch {
683
+ proxyStatus = 'unhealthy';
684
+ }
685
+ // Check: CA SSL trust store
686
+ try {
687
+ const caCertPath = node_path.join(getIntuticDir(), 'ca.crt');
688
+ await node_fs.access(caCertPath);
689
+ if (process.platform === 'darwin') {
690
+ execSync(`security verify-cert -c "${caCertPath}" 2>/dev/null`, { timeout: 3000 });
691
+ }
692
+ else {
693
+ // Windows/Linux simple checks fallback
694
+ sslTrustStatus = 'trusted';
695
+ }
696
+ }
697
+ catch {
698
+ sslTrustStatus = 'untrusted';
699
+ }
700
+ // f. Report status heartbeat
701
+ try {
702
+ const activeProcs = getActiveAgentProcesses().map((p) => p.name);
703
+ await client.reportStatus({
704
+ workspaceId: safeCreds.workspaceId,
705
+ configVersion: localConfigVersion,
706
+ connectedSince,
707
+ lastSyncAt: newIso(),
708
+ harnesses: safeConfig.harnesses.map((h) => ({
709
+ type: h,
710
+ configPath: getAdapter(h)?.configFileName ?? '',
711
+ detected: true,
712
+ lastWriteAt: sopsWritten > 0 ? newIso() : null,
713
+ })),
714
+ activeProcesses: activeProcs,
715
+ components: {
716
+ proxy: proxyStatus,
717
+ valkey: valkeyStatus,
718
+ sslTrust: sslTrustStatus,
719
+ },
720
+ });
721
+ }
722
+ catch (err) {
723
+ log.warn(`Failed to send daemon heartbeat: ${err instanceof Error ? err.message : String(err)}`);
724
+ }
725
+ const driftLabel = driftCount > 0 ? pc.yellow(` — ${driftCount} drift(s) detected`) : '';
726
+ log.dim(`[sync] Config v${localConfigVersion} — ${sopsWritten} SOPs synced${driftLabel}`);
727
+ // Refresh runtime env with resolved settings
728
+ try {
729
+ await writeRuntimeEnv({
730
+ controlPlaneUrl,
731
+ apiKey: safeCreds.apiKey,
732
+ workspaceId: safeCreds.workspaceId,
733
+ mcpProxyFailBehavior: syncConfig.settings?.mcpProxyFailBehavior,
734
+ mcpProxyMode: syncConfig.settings?.mcpProxyMode,
735
+ bypassEnforcementTier: syncConfig.settings?.bypassEnforcementTier,
736
+ });
737
+ }
738
+ catch (err) {
739
+ log.warn(`Could not write runtime env file (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
740
+ }
741
+ return sopsWritten;
742
+ }
743
+ // Helper to run compliance probes (Phase 6)
744
+ const runProbes = async () => {
745
+ try {
746
+ const hookEventsLog = node_path.join(safeConfig.workspaceRoot, '.intutic', 'events', 'hook-events.jsonl');
747
+ await node_fs.mkdir(node_path.dirname(hookEventsLog), { recursive: true });
748
+ const probeResults = await runComplianceProbes(safeCreds.workspaceId);
749
+ let hasBypass = false;
750
+ for (const res of probeResults) {
751
+ if (!res.contained && res.incident) {
752
+ const entry = JSON.stringify(res.incident) + '\n';
753
+ await node_fs.appendFile(hookEventsLog, entry, 'utf-8');
754
+ hasBypass = true;
755
+ }
756
+ }
757
+ if (hasBypass) {
758
+ log.warn('[Security] Network containment bypass detected! Incident recorded.');
759
+ }
760
+ }
761
+ catch (err) {
762
+ log.warn(`Compliance probes failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
763
+ }
764
+ };
765
+ // Helper to drain hook events (WS-A)
766
+ const runDrain = async () => {
767
+ try {
768
+ const drained = await drainHookEvents(safeConfig.workspaceRoot, controlPlaneUrl, safeCreds.apiKey);
769
+ if (drained > 0) {
770
+ log.info(`[sync-daemon] Drained ${drained} hook governance events to control plane`);
771
+ }
772
+ }
773
+ catch (err) {
774
+ log.warn(`[sync-daemon] Hook event drain error (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
775
+ }
776
+ };
777
+ // Step 0: Write runtime env file (hook scripts source this for credentials)
778
+ try {
779
+ await writeRuntimeEnv({
780
+ controlPlaneUrl,
781
+ apiKey: safeCreds.apiKey,
782
+ workspaceId: safeCreds.workspaceId,
783
+ });
784
+ }
785
+ catch (err) {
786
+ log.warn(`Could not write runtime env file (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
787
+ }
788
+ // Sync offline traces back to PostgreSQL on startup
789
+ try {
790
+ await syncOfflineTraces(controlPlaneUrl, safeCreds.apiKey);
791
+ }
792
+ catch (err) {
793
+ log.warn(`Could not sync offline traces (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
794
+ }
795
+ // 4. Start WebSocket client
796
+ const wsClient = new SyncWsClient({
797
+ controlPlaneUrl,
798
+ apiKey: safeCreds.apiKey,
799
+ workspaceId: safeCreds.workspaceId,
800
+ onConfigUpdate: async (syncConfig) => {
801
+ try {
802
+ await applySyncConfig(syncConfig);
803
+ }
804
+ catch (err) {
805
+ log.error(`Failed to apply push configuration: ${err instanceof Error ? err.message : String(err)}`);
806
+ }
807
+ },
808
+ onActiveLocalSopsUpdate: async (activeLocalSops) => {
809
+ try {
810
+ const sessionContextPath = node_path.join(safeConfig.workspaceRoot, '.intutic', 'session-context.json');
811
+ await node_fs.writeFile(sessionContextPath, JSON.stringify({ activeLocalSops }, null, 2) + '\n', 'utf-8');
812
+ log.info(`Active local SOPs configuration updated: ${activeLocalSops.join(', ') || 'all'}`);
813
+ if (lastCachedConfig) {
814
+ await applySyncConfig(lastCachedConfig, true);
815
+ }
816
+ }
817
+ catch (err) {
818
+ log.error(`Failed to update active local SOPs: ${err instanceof Error ? err.message : String(err)}`);
819
+ }
820
+ },
821
+ signal: ac.signal,
822
+ });
823
+ wsClient.connect();
824
+ // Send initial context report on startup
825
+ try {
826
+ const gitContextPath = node_path.join(safeConfig.workspaceRoot, '.intutic', 'git-context.json');
827
+ let gitData = {};
828
+ try {
829
+ const raw = await node_fs.readFile(gitContextPath, 'utf-8');
830
+ const parsed = JSON.parse(raw);
831
+ gitData = parsed.git || {};
832
+ }
833
+ catch {
834
+ // ignore
835
+ }
836
+ scanLocalSops(safeConfig.workspaceRoot).then((localSops) => {
837
+ setTimeout(() => {
838
+ wsClient.send({
839
+ type: 'context_report',
840
+ git: gitData,
841
+ localSops,
842
+ });
843
+ log.info(`Initial context and ${localSops.length} local SOPs reported to control plane`);
844
+ }, 1000);
845
+ }).catch(() => { });
846
+ }
847
+ catch (err) {
848
+ // ignore
849
+ }
850
+ // FSEvents-driven hook event drain
851
+ const hookEventsLog = node_path.join(safeConfig.workspaceRoot, '.intutic', 'events', 'hook-events.jsonl');
852
+ let fsWatcher = null;
853
+ let drainSafetyTimer = null;
854
+ try {
855
+ await node_fs.mkdir(node_path.dirname(hookEventsLog), { recursive: true });
856
+ fsWatcher = watch(hookEventsLog, { ignoreInitial: true, persistent: false });
857
+ fsWatcher.on('change', runDrain);
858
+ fsWatcher.on('add', runDrain);
859
+ }
860
+ catch (err) {
861
+ // chokidar unavailable - rely on fallback
862
+ }
863
+ // 60-second safety-net drain poll
864
+ drainSafetyTimer = setInterval(runDrain, 60_000);
865
+ // Run initial compliance check on startup
866
+ await runProbes();
867
+ // 5. Start Filesystem Watcher
868
+ const watcher = startWatcher(safeConfig.workspaceRoot, safeConfig.harnesses, async (changedPath) => {
869
+ const filename = node_path.basename(changedPath);
870
+ // A. Handle git-context.json and local sops changes
871
+ const relativePath = node_path.relative(safeConfig.workspaceRoot, changedPath);
872
+ const isSopsDirChange = relativePath.split(node_path.sep).includes('sops');
873
+ if (filename === 'git-context.json' || isSopsDirChange) {
874
+ try {
875
+ let gitData = {};
876
+ const gitContextPath = node_path.join(safeConfig.workspaceRoot, '.intutic', 'git-context.json');
877
+ try {
878
+ const raw = await node_fs.readFile(gitContextPath, 'utf-8');
879
+ const data = JSON.parse(raw);
880
+ gitData = data.git || {};
881
+ }
882
+ catch {
883
+ // ignore if context file doesn't exist yet
884
+ }
885
+ const localSops = await scanLocalSops(safeConfig.workspaceRoot);
886
+ wsClient.send({
887
+ type: 'context_report',
888
+ git: gitData,
889
+ localSops,
890
+ });
891
+ log.info(`Git context and ${localSops.length} local SOPs reported to control plane`);
892
+ }
893
+ catch (err) {
894
+ log.warn(`Failed to sync Git context metadata: ${err instanceof Error ? err.message : String(err)}`);
895
+ }
896
+ return;
897
+ }
898
+ // B. Handle Claude Code settings.json tamper detection (privilege escalation guard)
899
+ if (filename === 'settings.json' || filename === 'settings.local.json') {
900
+ try {
901
+ const sops = lastCachedConfig?.sops ?? [];
902
+ const tampered = await guardSettingsFile(changedPath, safeConfig.workspaceRoot, sops);
903
+ if (tampered) {
904
+ log.warn(`[Security] Governance settings tamper detected and restored: ${changedPath}`);
905
+ wsClient.send({
906
+ type: 'drift_report',
907
+ harnessType: 'claude-code',
908
+ filePath: changedPath,
909
+ localHash: '',
910
+ canonicalHash: '',
911
+ });
912
+ }
913
+ }
914
+ catch (err) {
915
+ log.error(`Settings guard error: ${err instanceof Error ? err.message : String(err)}`);
916
+ }
917
+ return;
918
+ }
919
+ // B. Handle governed harness file drift detection
920
+ const matchingHarness = safeConfig.harnesses.find((h) => getAdapter(h)?.configFileName === filename);
921
+ if (!matchingHarness)
922
+ return;
923
+ const adapter = getAdapter(matchingHarness);
924
+ if (!adapter)
925
+ return;
926
+ const currentHash = await adapter.readCurrentHash(safeConfig.workspaceRoot);
927
+ const integrity = loadIntegrity(safeConfig.workspaceRoot);
928
+ if (!integrity)
929
+ return;
930
+ const canonical = integrity.files[adapter.configFileName] ?? '';
931
+ if (currentHash !== canonical) {
932
+ log.warn(`Governed config file "${filename}" modification detected! Reverting to approved baseline...`);
933
+ // Backup drifted version first
934
+ try {
935
+ const content = await node_fs.readFile(changedPath, 'utf-8');
936
+ const backupPath = changedPath + '.drift-backup';
937
+ await node_fs.writeFile(backupPath, content, 'utf-8');
938
+ log.dim(`Drifted file backed up to: ${backupPath}`);
939
+ }
940
+ catch {
941
+ // Ignore backup failure
942
+ }
943
+ // Revert from cached config or fetch fresh config
944
+ try {
945
+ let syncConfig = lastCachedConfig;
946
+ if (!syncConfig) {
947
+ syncConfig = await client.fetchConfig(safeCreds.workspaceId);
948
+ }
949
+ await applySyncConfig(syncConfig, true);
950
+ // Report incident via WebSocket/HTTP
951
+ wsClient.send({
952
+ type: 'drift_report',
953
+ harnessType: adapter.type,
954
+ filePath: adapter.configFileName,
955
+ localHash: currentHash || '',
956
+ canonicalHash: canonical,
957
+ });
958
+ }
959
+ catch (err) {
960
+ log.error(`Failed to automatically revert file drift: ${err instanceof Error ? err.message : String(err)}`);
961
+ }
962
+ }
963
+ });
964
+ // 6. Secondary fallback HTTP poll loop
965
+ while (!ac.signal.aborted) {
966
+ try {
967
+ const syncConfig = await client.fetchConfig(safeCreds.workspaceId);
968
+ await applySyncConfig(syncConfig);
969
+ // Sync offline traces back on every iteration
970
+ try {
971
+ await syncOfflineTraces(controlPlaneUrl, safeCreds.apiKey);
972
+ }
973
+ catch { }
974
+ // Run compliance probes on each iteration
975
+ await runProbes();
976
+ }
977
+ catch (err) {
978
+ log.error(`Sync iteration failed: ${err instanceof Error ? err.message : String(err)}`);
979
+ log.dim(`Retrying in ${pollInterval / 1000}s...`);
980
+ }
981
+ // Sleep until next interval (AbortSignal-aware)
982
+ await new Promise((resolve) => {
983
+ const timer = setTimeout(resolve, pollInterval);
984
+ ac.signal.addEventListener('abort', () => {
985
+ clearTimeout(timer);
986
+ resolve();
987
+ }, { once: true });
988
+ });
989
+ }
990
+ // Cleanup watcher, intervals, and WS connection on exit
991
+ watcher.stop();
992
+ fsWatcher?.close();
993
+ if (drainSafetyTimer)
994
+ clearInterval(drainSafetyTimer);
995
+ wsClient.close();
996
+ log.success('Sync daemon stopped.');
997
+ }
998
+ async function scanLocalSops(workspaceRoot) {
999
+ const sopsDir = node_path.join(workspaceRoot, '.intutic', 'sops');
1000
+ try {
1001
+ const entries = await node_fs.readdir(sopsDir, { withFileTypes: true });
1002
+ return entries
1003
+ .filter((e) => e.isDirectory())
1004
+ .map((e) => e.name);
1005
+ }
1006
+ catch {
1007
+ return [];
1008
+ }
1009
+ }
1010
+ //# sourceMappingURL=connect.js.map