dual-brain 0.2.30 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/.dual-brain/docs/claude-code-extension-points.md +32 -0
  2. package/.dual-brain/docs/data-tools-capabilities.md +181 -0
  3. package/.dual-brain/docs/ecosystem-tools.md +91 -0
  4. package/.dual-brain/docs/panel-handoff.md +124 -0
  5. package/.dual-brain/docs/ruflo-analysis.md +48 -0
  6. package/bin/dual-brain.mjs +56 -56
  7. package/dist/mcp-server/index.d.ts +27 -0
  8. package/dist/mcp-server/index.js +359 -0
  9. package/dist/mcp-server/index.js.map +1 -0
  10. package/dist/src/agent-protocol.d.ts +163 -0
  11. package/dist/src/agent-protocol.js +368 -0
  12. package/dist/src/agent-protocol.js.map +1 -0
  13. package/dist/src/agents/registry.d.ts +52 -0
  14. package/dist/src/agents/registry.js +393 -0
  15. package/dist/src/agents/registry.js.map +1 -0
  16. package/dist/src/awareness.d.ts +93 -0
  17. package/dist/src/awareness.js +406 -0
  18. package/dist/src/awareness.js.map +1 -0
  19. package/dist/src/brief.d.ts +48 -0
  20. package/dist/src/brief.js +179 -0
  21. package/dist/src/brief.js.map +1 -0
  22. package/dist/src/calibration.d.ts +32 -0
  23. package/dist/src/calibration.js +133 -0
  24. package/dist/src/calibration.js.map +1 -0
  25. package/dist/src/checkpoint.d.ts +33 -0
  26. package/dist/src/checkpoint.js +99 -0
  27. package/dist/src/checkpoint.js.map +1 -0
  28. package/dist/src/ci-triage.d.ts +33 -0
  29. package/dist/src/ci-triage.js +193 -0
  30. package/dist/src/ci-triage.js.map +1 -0
  31. package/dist/src/cognitive-loop.d.ts +56 -0
  32. package/dist/src/cognitive-loop.js +495 -0
  33. package/dist/src/cognitive-loop.js.map +1 -0
  34. package/dist/src/collaboration.d.ts +147 -0
  35. package/dist/src/collaboration.js +438 -0
  36. package/dist/src/collaboration.js.map +1 -0
  37. package/dist/src/context-intel.d.ts +47 -0
  38. package/dist/src/context-intel.js +156 -0
  39. package/dist/src/context-intel.js.map +1 -0
  40. package/dist/src/context.d.ts +53 -0
  41. package/dist/src/context.js +332 -0
  42. package/dist/src/context.js.map +1 -0
  43. package/dist/src/continuity.d.ts +89 -0
  44. package/dist/src/continuity.js +230 -0
  45. package/dist/src/continuity.js.map +1 -0
  46. package/dist/src/cost-tracker.d.ts +47 -0
  47. package/dist/src/cost-tracker.js +170 -0
  48. package/dist/src/cost-tracker.js.map +1 -0
  49. package/dist/src/debrief.d.ts +53 -0
  50. package/dist/src/debrief.js +222 -0
  51. package/dist/src/debrief.js.map +1 -0
  52. package/dist/src/decide.d.ts +96 -0
  53. package/dist/src/decide.js +744 -0
  54. package/dist/src/decide.js.map +1 -0
  55. package/dist/src/decompose.d.ts +39 -0
  56. package/dist/src/decompose.js +218 -0
  57. package/dist/src/decompose.js.map +1 -0
  58. package/dist/src/detect.d.ts +91 -0
  59. package/dist/src/detect.js +544 -0
  60. package/dist/src/detect.js.map +1 -0
  61. package/dist/src/dispatch.d.ts +154 -0
  62. package/dist/src/dispatch.js +1306 -0
  63. package/dist/src/dispatch.js.map +1 -0
  64. package/dist/src/doctor.d.ts +421 -0
  65. package/dist/src/doctor.js +1689 -0
  66. package/dist/src/doctor.js.map +1 -0
  67. package/dist/src/engine.d.ts +70 -0
  68. package/dist/src/engine.js +155 -0
  69. package/dist/src/engine.js.map +1 -0
  70. package/dist/src/envelope.d.ts +36 -0
  71. package/dist/src/envelope.js +80 -0
  72. package/dist/src/envelope.js.map +1 -0
  73. package/dist/src/failure-memory.d.ts +55 -0
  74. package/dist/src/failure-memory.js +175 -0
  75. package/dist/src/failure-memory.js.map +1 -0
  76. package/dist/src/fx.d.ts +87 -0
  77. package/dist/src/fx.js +272 -0
  78. package/dist/src/fx.js.map +1 -0
  79. package/dist/src/governance.d.ts +93 -0
  80. package/dist/src/governance.js +261 -0
  81. package/dist/src/governance.js.map +1 -0
  82. package/dist/src/handoff.d.ts +11 -0
  83. package/dist/src/handoff.js +90 -0
  84. package/dist/src/handoff.js.map +1 -0
  85. package/dist/src/head-protocol.d.ts +76 -0
  86. package/dist/src/head-protocol.js +109 -0
  87. package/dist/src/head-protocol.js.map +1 -0
  88. package/dist/src/head.d.ts +222 -0
  89. package/dist/src/head.js +765 -0
  90. package/dist/src/head.js.map +1 -0
  91. package/dist/src/health.d.ts +132 -0
  92. package/dist/src/health.js +435 -0
  93. package/dist/src/health.js.map +1 -0
  94. package/dist/src/inbox.d.ts +70 -0
  95. package/dist/src/inbox.js +218 -0
  96. package/dist/src/inbox.js.map +1 -0
  97. package/dist/src/index.d.ts +33 -0
  98. package/dist/src/index.js +38 -0
  99. package/dist/src/index.js.map +1 -0
  100. package/dist/src/install-hooks.d.ts +13 -0
  101. package/dist/src/install-hooks.js +88 -0
  102. package/dist/src/install-hooks.js.map +1 -0
  103. package/dist/src/integrity.d.ts +59 -0
  104. package/dist/src/integrity.js +206 -0
  105. package/dist/src/integrity.js.map +1 -0
  106. package/dist/src/intelligence.d.ts +104 -0
  107. package/dist/src/intelligence.js +391 -0
  108. package/dist/src/intelligence.js.map +1 -0
  109. package/dist/src/ledger.d.ts +54 -0
  110. package/dist/src/ledger.js +179 -0
  111. package/dist/src/ledger.js.map +1 -0
  112. package/dist/src/living-docs.d.ts +14 -0
  113. package/dist/src/living-docs.js +197 -0
  114. package/dist/src/living-docs.js.map +1 -0
  115. package/dist/src/memory-tiers.d.ts +37 -0
  116. package/dist/src/memory-tiers.js +160 -0
  117. package/dist/src/memory-tiers.js.map +1 -0
  118. package/dist/src/model-profiles.d.ts +65 -0
  119. package/dist/src/model-profiles.js +568 -0
  120. package/dist/src/model-profiles.js.map +1 -0
  121. package/dist/src/models.d.ts +58 -0
  122. package/dist/src/models.js +327 -0
  123. package/dist/src/models.js.map +1 -0
  124. package/dist/src/narrative.d.ts +54 -0
  125. package/dist/src/narrative.js +163 -0
  126. package/dist/src/narrative.js.map +1 -0
  127. package/dist/src/nextstep.d.ts +16 -0
  128. package/dist/src/nextstep.js +103 -0
  129. package/dist/src/nextstep.js.map +1 -0
  130. package/dist/src/observer.d.ts +18 -0
  131. package/dist/src/observer.js +251 -0
  132. package/dist/src/observer.js.map +1 -0
  133. package/dist/src/outcome.d.ts +110 -0
  134. package/dist/src/outcome.js +377 -0
  135. package/dist/src/outcome.js.map +1 -0
  136. package/dist/src/pipeline.d.ts +167 -0
  137. package/dist/src/pipeline.js +1503 -0
  138. package/dist/src/pipeline.js.map +1 -0
  139. package/dist/src/playbook.d.ts +59 -0
  140. package/dist/src/playbook.js +238 -0
  141. package/dist/src/playbook.js.map +1 -0
  142. package/dist/src/pr-agent.d.ts +97 -0
  143. package/dist/src/pr-agent.js +195 -0
  144. package/dist/src/pr-agent.js.map +1 -0
  145. package/dist/src/predictive.d.ts +57 -0
  146. package/dist/src/predictive.js +230 -0
  147. package/dist/src/predictive.js.map +1 -0
  148. package/dist/src/profile.d.ts +294 -0
  149. package/dist/src/profile.js +1347 -0
  150. package/dist/src/profile.js.map +1 -0
  151. package/dist/src/prompt-audit.d.ts +22 -0
  152. package/dist/src/prompt-audit.js +194 -0
  153. package/dist/src/prompt-audit.js.map +1 -0
  154. package/dist/src/prompt-intel.d.ts +12 -0
  155. package/dist/src/prompt-intel.js +321 -0
  156. package/dist/src/prompt-intel.js.map +1 -0
  157. package/dist/src/provider-context.d.ts +121 -0
  158. package/dist/src/provider-context.js +222 -0
  159. package/dist/src/provider-context.js.map +1 -0
  160. package/dist/src/provider-manager.d.ts +92 -0
  161. package/dist/src/provider-manager.js +428 -0
  162. package/dist/src/provider-manager.js.map +1 -0
  163. package/dist/src/receipt.d.ts +87 -0
  164. package/dist/src/receipt.js +326 -0
  165. package/dist/src/receipt.js.map +1 -0
  166. package/dist/src/recommendations.d.ts +13 -0
  167. package/dist/src/recommendations.js +291 -0
  168. package/dist/src/recommendations.js.map +1 -0
  169. package/dist/src/redact.d.ts +15 -0
  170. package/dist/src/redact.js +129 -0
  171. package/dist/src/redact.js.map +1 -0
  172. package/dist/src/replit.d.ts +397 -0
  173. package/dist/src/replit.js +1160 -0
  174. package/dist/src/replit.js.map +1 -0
  175. package/dist/src/repo.d.ts +149 -0
  176. package/dist/src/repo.js +416 -0
  177. package/dist/src/repo.js.map +1 -0
  178. package/dist/src/revert.d.ts +30 -0
  179. package/dist/src/revert.js +166 -0
  180. package/dist/src/revert.js.map +1 -0
  181. package/dist/src/room.d.ts +102 -0
  182. package/dist/src/room.js +212 -0
  183. package/dist/src/room.js.map +1 -0
  184. package/dist/src/routing-advisor.d.ts +57 -0
  185. package/dist/src/routing-advisor.js +221 -0
  186. package/dist/src/routing-advisor.js.map +1 -0
  187. package/dist/src/self-correct.d.ts +40 -0
  188. package/dist/src/self-correct.js +137 -0
  189. package/dist/src/self-correct.js.map +1 -0
  190. package/dist/src/session-lock.d.ts +35 -0
  191. package/dist/src/session-lock.js +134 -0
  192. package/dist/src/session-lock.js.map +1 -0
  193. package/dist/src/session.d.ts +267 -0
  194. package/dist/src/session.js +1660 -0
  195. package/dist/src/session.js.map +1 -0
  196. package/dist/src/settings-tui.d.ts +5 -0
  197. package/dist/src/settings-tui.js +422 -0
  198. package/dist/src/settings-tui.js.map +1 -0
  199. package/dist/src/setup-flow.d.ts +63 -0
  200. package/dist/src/setup-flow.js +233 -0
  201. package/dist/src/setup-flow.js.map +1 -0
  202. package/dist/src/signal.d.ts +19 -0
  203. package/dist/src/signal.js +122 -0
  204. package/dist/src/signal.js.map +1 -0
  205. package/dist/src/simmer.d.ts +85 -0
  206. package/dist/src/simmer.js +224 -0
  207. package/dist/src/simmer.js.map +1 -0
  208. package/dist/src/state-export.d.ts +129 -0
  209. package/dist/src/state-export.js +233 -0
  210. package/dist/src/state-export.js.map +1 -0
  211. package/dist/src/strategy.d.ts +54 -0
  212. package/dist/src/strategy.js +95 -0
  213. package/dist/src/strategy.js.map +1 -0
  214. package/dist/src/subscription.d.ts +40 -0
  215. package/dist/src/subscription.js +189 -0
  216. package/dist/src/subscription.js.map +1 -0
  217. package/dist/src/templates.d.ts +208 -0
  218. package/dist/src/templates.js +238 -0
  219. package/dist/src/templates.js.map +1 -0
  220. package/dist/src/test.d.ts +9 -0
  221. package/dist/src/test.js +1173 -0
  222. package/dist/src/test.js.map +1 -0
  223. package/dist/src/think-engine.d.ts +67 -0
  224. package/dist/src/think-engine.js +412 -0
  225. package/dist/src/think-engine.js.map +1 -0
  226. package/dist/src/tui.d.ts +71 -0
  227. package/dist/src/tui.js +242 -0
  228. package/dist/src/tui.js.map +1 -0
  229. package/dist/src/types.d.ts +177 -0
  230. package/dist/src/types.js +6 -0
  231. package/dist/src/types.js.map +1 -0
  232. package/dist/src/update-check.d.ts +7 -0
  233. package/dist/src/update-check.js +36 -0
  234. package/dist/src/update-check.js.map +1 -0
  235. package/dist/src/wave-planner.d.ts +30 -0
  236. package/dist/src/wave-planner.js +281 -0
  237. package/dist/src/wave-planner.js.map +1 -0
  238. package/hooks/head-guard.sh +41 -0
  239. package/hooks/precompact.mjs +3 -3
  240. package/hooks/session-end.mjs +3 -3
  241. package/hooks/task-classifier.mjs +328 -0
  242. package/hooks/vibe-router.mjs +387 -0
  243. package/install.mjs +2 -2
  244. package/package.json +29 -153
  245. package/src/agents/registry.mjs +0 -405
  246. package/src/awareness.mjs +0 -425
  247. package/src/brief.mjs +0 -266
  248. package/src/calibration.mjs +0 -148
  249. package/src/checkpoint.mjs +0 -109
  250. package/src/ci-triage.mjs +0 -191
  251. package/src/cognitive-loop.mjs +0 -562
  252. package/src/collaboration.mjs +0 -545
  253. package/src/context-intel.mjs +0 -158
  254. package/src/context.mjs +0 -389
  255. package/src/continuity.mjs +0 -298
  256. package/src/cost-tracker.mjs +0 -184
  257. package/src/debrief.mjs +0 -228
  258. package/src/decide.mjs +0 -1099
  259. package/src/decompose.mjs +0 -331
  260. package/src/detect.mjs +0 -702
  261. package/src/dispatch.mjs +0 -1447
  262. package/src/doctor.mjs +0 -1607
  263. package/src/envelope.mjs +0 -139
  264. package/src/failure-memory.mjs +0 -178
  265. package/src/fx.mjs +0 -276
  266. package/src/governance.mjs +0 -279
  267. package/src/handoff.mjs +0 -87
  268. package/src/head-protocol.mjs +0 -128
  269. package/src/head.mjs +0 -952
  270. package/src/health.mjs +0 -528
  271. package/src/inbox.mjs +0 -195
  272. package/src/index.mjs +0 -44
  273. package/src/install-hooks.mjs +0 -100
  274. package/src/integrity.mjs +0 -245
  275. package/src/intelligence.mjs +0 -447
  276. package/src/ledger.mjs +0 -196
  277. package/src/living-docs.mjs +0 -210
  278. package/src/memory-tiers.mjs +0 -193
  279. package/src/models.mjs +0 -363
  280. package/src/narrative.mjs +0 -169
  281. package/src/nextstep.mjs +0 -100
  282. package/src/observer.mjs +0 -241
  283. package/src/outcome.mjs +0 -400
  284. package/src/pipeline.mjs +0 -1711
  285. package/src/playbook.mjs +0 -257
  286. package/src/pr-agent.mjs +0 -214
  287. package/src/predictive.mjs +0 -250
  288. package/src/profile.mjs +0 -1411
  289. package/src/prompt-audit.mjs +0 -231
  290. package/src/prompt-intel.mjs +0 -325
  291. package/src/provider-context.mjs +0 -257
  292. package/src/receipt.mjs +0 -344
  293. package/src/recommendations.mjs +0 -296
  294. package/src/redact.mjs +0 -192
  295. package/src/replit.mjs +0 -1210
  296. package/src/repo.mjs +0 -445
  297. package/src/revert.mjs +0 -149
  298. package/src/routing-advisor.mjs +0 -204
  299. package/src/self-correct.mjs +0 -147
  300. package/src/session-lock.mjs +0 -160
  301. package/src/session.mjs +0 -1655
  302. package/src/settings-tui.mjs +0 -373
  303. package/src/setup-flow.mjs +0 -223
  304. package/src/signal.mjs +0 -115
  305. package/src/simmer.mjs +0 -241
  306. package/src/strategy.mjs +0 -235
  307. package/src/subscription.mjs +0 -212
  308. package/src/templates.mjs +0 -260
  309. package/src/think-engine.mjs +0 -428
  310. package/src/tui.mjs +0 -276
  311. package/src/update-check.mjs +0 -35
  312. package/src/wave-planner.mjs +0 -294
@@ -0,0 +1,1160 @@
1
+ /**
2
+ * replit.mjs — Replit platform integration for dual-brain.
3
+ *
4
+ * Treats replit-tools as infrastructure and adds intelligence on top.
5
+ * Uses only Node built-ins. Never reads or returns secret values.
6
+ *
7
+ * Sections:
8
+ * 1. Discovery — read-only inspection of environment and replit-tools
9
+ * 2. Planning — compute .replit config changes, no side effects
10
+ * 3. Apply — mutating; low-risk changes only in v1
11
+ * 4. Formatters — pretty-print integration reports
12
+ */
13
+ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, renameSync, createReadStream, } from 'node:fs';
14
+ import { join, resolve } from 'node:path';
15
+ import { spawnSync } from 'node:child_process';
16
+ import { createInterface } from 'node:readline';
17
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
18
+ function safeRead(filePath) {
19
+ try {
20
+ return readFileSync(filePath, 'utf8');
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ function safeJson(filePath) {
27
+ const raw = safeRead(filePath);
28
+ if (!raw)
29
+ return null;
30
+ try {
31
+ return JSON.parse(raw);
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ function safeReaddir(dirPath) {
38
+ try {
39
+ return readdirSync(dirPath);
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
45
+ function safeStat(filePath) {
46
+ try {
47
+ return statSync(filePath);
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /** Returns the replit-tools root directory for a given workspace cwd, or null. */
54
+ function findReplitToolsDir(cwd) {
55
+ const candidates = [
56
+ join(cwd, '.replit-tools'),
57
+ '/home/runner/workspace/.replit-tools',
58
+ ];
59
+ for (const c of candidates) {
60
+ if (existsSync(c))
61
+ return resolve(c);
62
+ }
63
+ return null;
64
+ }
65
+ // ─── Section 1: Discovery ─────────────────────────────────────────────────────
66
+ /**
67
+ * Detect the Replit runtime environment from env vars.
68
+ * @param {string} [cwd]
69
+ * @returns {{ isReplit, replId, replSlug, replOwner, replUrl, nixChannel, containerType, uptimeSeconds }}
70
+ */
71
+ export function detectReplitEnvironment(cwd = process.cwd()) {
72
+ const env = process.env;
73
+ const isReplit = Boolean(env.REPL_ID || env.REPL_SLUG);
74
+ let uptimeSeconds = null;
75
+ try {
76
+ const raw = readFileSync('/proc/uptime', 'utf8');
77
+ uptimeSeconds = Math.floor(parseFloat(raw.split(' ')[0]));
78
+ }
79
+ catch { /* not available */ }
80
+ // Container type from env signals
81
+ let containerType = 'local';
82
+ if (isReplit)
83
+ containerType = 'replit';
84
+ else if (env.CODESPACES)
85
+ containerType = 'codespace';
86
+ else if (env.CI || env.GITHUB_ACTIONS || env.GITLAB_CI)
87
+ containerType = 'ci';
88
+ // nixChannel from .replit file if available
89
+ let nixChannel = env.NIX_CHANNEL || null;
90
+ const replitFile = join(resolve(cwd), '.replit');
91
+ if (!nixChannel && existsSync(replitFile)) {
92
+ const content = safeRead(replitFile) || '';
93
+ const m = content.match(/channel\s*=\s*["']?([^\s"'\n]+)["']?/);
94
+ if (m)
95
+ nixChannel = m[1];
96
+ }
97
+ return {
98
+ isReplit,
99
+ replId: env.REPL_ID || null,
100
+ replSlug: env.REPL_SLUG || null,
101
+ replOwner: env.REPL_OWNER || null,
102
+ replUrl: env.REPL_URL || (env.REPL_SLUG ? `https://replit.com/@${env.REPL_OWNER || 'unknown'}/${env.REPL_SLUG}` : null),
103
+ nixChannel,
104
+ containerType,
105
+ uptimeSeconds,
106
+ };
107
+ }
108
+ /**
109
+ * Read and parse the .replit config file.
110
+ * Uses simple line-by-line parsing — no TOML dependency.
111
+ * @param {string} [cwd]
112
+ * @returns {{ raw, run, onBoot, expertMode, hidden, modules, nix, deployment, hasRun, hasOnBoot, hasExpertMode }}
113
+ */
114
+ export function inspectReplitConfig(cwd = process.cwd()) {
115
+ const replitPath = join(resolve(cwd), '.replit');
116
+ const raw = safeRead(replitPath);
117
+ if (!raw) {
118
+ return {
119
+ raw: null, run: null, onBoot: null, expertMode: null,
120
+ hidden: [], modules: [], nix: {}, deployment: {},
121
+ hasRun: false, hasOnBoot: false, hasExpertMode: false,
122
+ };
123
+ }
124
+ const lines = raw.split('\n');
125
+ let run = null;
126
+ let onBoot = null;
127
+ let expertMode = null;
128
+ const hidden = [];
129
+ const modules = [];
130
+ const nix = {};
131
+ const deployment = {};
132
+ let currentSection = null;
133
+ for (const line of lines) {
134
+ const trimmed = line.trim();
135
+ if (!trimmed || trimmed.startsWith('#'))
136
+ continue;
137
+ // Section headers: [nix], [agent], [deployment]
138
+ const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
139
+ if (sectionMatch) {
140
+ currentSection = sectionMatch[1].trim();
141
+ continue;
142
+ }
143
+ // Key = value (handle quoted and unquoted)
144
+ const kvMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/);
145
+ if (!kvMatch)
146
+ continue;
147
+ const key = kvMatch[1];
148
+ let value = kvMatch[2].trim();
149
+ // Strip surrounding quotes
150
+ if ((value.startsWith('"') && value.endsWith('"')) ||
151
+ (value.startsWith("'") && value.endsWith("'"))) {
152
+ value = value.slice(1, -1);
153
+ }
154
+ if (currentSection === 'nix') {
155
+ nix[key] = value;
156
+ }
157
+ else if (currentSection === 'deployment') {
158
+ deployment[key] = value;
159
+ }
160
+ else if (currentSection === 'agent') {
161
+ if (key === 'expertMode') {
162
+ expertMode = value === 'true' || value === '1';
163
+ }
164
+ }
165
+ else if (!currentSection) {
166
+ // Top-level keys
167
+ if (key === 'run')
168
+ run = value;
169
+ else if (key === 'onBoot')
170
+ onBoot = value;
171
+ else if (key === 'modules') {
172
+ // modules = ["nodejs-20"] style
173
+ const items = value.replace(/[\[\]"']/g, '').split(',').map(s => s.trim()).filter(Boolean);
174
+ modules.push(...items);
175
+ }
176
+ else if (key === 'hidden') {
177
+ const items = value.replace(/[\[\]"']/g, '').split(',').map(s => s.trim()).filter(Boolean);
178
+ hidden.push(...items);
179
+ }
180
+ }
181
+ }
182
+ return {
183
+ raw,
184
+ run,
185
+ onBoot,
186
+ expertMode,
187
+ hidden,
188
+ modules,
189
+ nix,
190
+ deployment,
191
+ hasRun: run !== null,
192
+ hasOnBoot: onBoot !== null,
193
+ hasExpertMode: expertMode !== null,
194
+ };
195
+ }
196
+ /**
197
+ * Inventory what replit-tools provides in the current workspace.
198
+ * @param {string} [cwd]
199
+ * @returns {object} Structured capability report
200
+ */
201
+ export function inspectReplitTools(cwd = process.cwd()) {
202
+ const toolsDir = findReplitToolsDir(resolve(cwd));
203
+ if (!toolsDir) {
204
+ return {
205
+ installed: false,
206
+ version: null,
207
+ toolsDir: null,
208
+ sessionArchive: { exists: false, sessionCount: 0, latestTimestamp: null },
209
+ persistentHomes: { claude: false, codex: false },
210
+ authRefresh: { available: false },
211
+ config: null,
212
+ codexPlugins: { count: 0 },
213
+ shellSnapshots: { available: false, count: 0 },
214
+ mcpAuthCache: { available: false, entries: 0 },
215
+ };
216
+ }
217
+ // Version
218
+ let version = null;
219
+ const versionFile = join(toolsDir, '.version');
220
+ if (existsSync(versionFile)) {
221
+ version = (safeRead(versionFile) || '').trim() || null;
222
+ }
223
+ if (!version) {
224
+ const pkg = safeJson(join(toolsDir, 'package.json'));
225
+ if (pkg?.version)
226
+ version = pkg.version;
227
+ }
228
+ // Session archive: .replit-tools/.session-archive/claude/
229
+ const archiveBase = join(toolsDir, '.session-archive', 'claude');
230
+ let sessionCount = 0;
231
+ let latestTimestamp = null;
232
+ if (existsSync(archiveBase)) {
233
+ // Recursively count all .jsonl files under the archive tree
234
+ function countJsonl(dir) {
235
+ for (const entry of safeReaddir(dir)) {
236
+ const full = join(dir, entry);
237
+ const st = safeStat(full);
238
+ if (!st)
239
+ continue;
240
+ if (st.isDirectory()) {
241
+ countJsonl(full);
242
+ }
243
+ else if (entry.endsWith('.jsonl')) {
244
+ sessionCount++;
245
+ const ts = st.mtimeMs;
246
+ if (!latestTimestamp || ts > latestTimestamp)
247
+ latestTimestamp = ts;
248
+ }
249
+ }
250
+ }
251
+ countJsonl(archiveBase);
252
+ }
253
+ // Persistent homes
254
+ const claudePersistent = join(toolsDir, '.claude-persistent');
255
+ const codexPersistent = join(toolsDir, '.codex-persistent');
256
+ // Auth refresh
257
+ const authRefreshScript = join(toolsDir, 'scripts', 'claude-auth-refresh.sh');
258
+ const authRefreshAvailable = existsSync(authRefreshScript);
259
+ // Config
260
+ const config = safeJson(join(toolsDir, 'config.json'));
261
+ // Codex plugins
262
+ const pluginsDir = join(codexPersistent, '.tmp', 'plugins', 'plugins');
263
+ const pluginCount = existsSync(pluginsDir) ? safeReaddir(pluginsDir).length : 0;
264
+ // Shell snapshots
265
+ const shellSnapshotsDir = join(claudePersistent, 'shell-snapshots');
266
+ const shellSnapshotFiles = existsSync(shellSnapshotsDir)
267
+ ? safeReaddir(shellSnapshotsDir).filter(f => f.endsWith('.sh'))
268
+ : [];
269
+ // MCP auth cache
270
+ const mcpCacheFile = join(claudePersistent, 'mcp-needs-auth-cache.json');
271
+ const mcpCache = safeJson(mcpCacheFile);
272
+ const mcpEntries = mcpCache ? Object.keys(mcpCache).length : 0;
273
+ return {
274
+ installed: true,
275
+ version,
276
+ toolsDir,
277
+ sessionArchive: {
278
+ exists: existsSync(archiveBase),
279
+ sessionCount,
280
+ latestTimestamp,
281
+ },
282
+ persistentHomes: {
283
+ claude: existsSync(claudePersistent),
284
+ codex: existsSync(codexPersistent),
285
+ },
286
+ authRefresh: {
287
+ available: authRefreshAvailable,
288
+ scriptPath: authRefreshAvailable ? authRefreshScript : null,
289
+ },
290
+ config,
291
+ codexPlugins: { count: pluginCount },
292
+ shellSnapshots: {
293
+ available: existsSync(shellSnapshotsDir),
294
+ count: shellSnapshotFiles.length,
295
+ },
296
+ mcpAuthCache: {
297
+ available: existsSync(mcpCacheFile),
298
+ entries: mcpEntries,
299
+ },
300
+ };
301
+ }
302
+ /**
303
+ * Check whether a named environment variable is set (never returns its value).
304
+ * @param {string} name
305
+ * @returns {boolean}
306
+ */
307
+ export function hasSecret(name) {
308
+ return process.env[name] !== undefined && process.env[name] !== '';
309
+ }
310
+ // System env var patterns to exclude from listSecretNames
311
+ const SYSTEM_PREFIXES = [
312
+ 'npm_', 'NODE_', 'PATH', 'HOME', 'SHELL', 'USER', 'LOGNAME', 'TERM',
313
+ 'LANG', 'LC_', 'PWD', 'OLDPWD', 'SHLVL', 'HOSTNAME', 'MAIL',
314
+ 'XDG_', 'DBUS_', 'DISPLAY', 'COLORTERM', 'LESS', 'PAGER', 'EDITOR',
315
+ 'MANPATH', 'INFOPATH', 'LS_COLORS', 'PS1', 'PS2', 'IFS', '_',
316
+ 'REPL_', 'REPLIT_', 'NIX_', 'NIXPKGS_', 'LOCALE_', 'JAVA_',
317
+ ];
318
+ const KNOWN_SECRET_NAMES = [
319
+ 'DATABASE_URL', 'REPLIT_DB_URL',
320
+ 'GITHUB_TOKEN', 'GITHUB_API_TOKEN', 'NPM_TOKEN', 'NPM_AUTH_TOKEN',
321
+ 'STRIPE_SECRET_KEY', 'STRIPE_API_KEY', 'AWS_ACCESS_KEY_ID',
322
+ 'AWS_SECRET_ACCESS_KEY', 'GOOGLE_API_KEY', 'GOOGLE_APPLICATION_CREDENTIALS',
323
+ 'FIREBASE_TOKEN', 'SUPABASE_KEY', 'SUPABASE_URL', 'POSTGRES_URL',
324
+ 'MONGODB_URI', 'REDIS_URL', 'SENDGRID_API_KEY', 'TWILIO_AUTH_TOKEN',
325
+ 'SLACK_BOT_TOKEN', 'DISCORD_TOKEN', 'VERCEL_TOKEN', 'CLOUDFLARE_API_TOKEN',
326
+ ];
327
+ function looksLikeSystemVar(name) {
328
+ for (const prefix of SYSTEM_PREFIXES) {
329
+ if (name.startsWith(prefix))
330
+ return true;
331
+ }
332
+ return false;
333
+ }
334
+ /**
335
+ * Return names of set secrets/credentials. Never returns values.
336
+ * @returns {string[]}
337
+ */
338
+ export function listSecretNames() {
339
+ const result = new Set();
340
+ // Check known secrets first
341
+ for (const name of KNOWN_SECRET_NAMES) {
342
+ if (hasSecret(name))
343
+ result.add(name);
344
+ }
345
+ // Find other non-system env vars that look like secrets
346
+ for (const name of Object.keys(process.env)) {
347
+ if (result.has(name))
348
+ continue;
349
+ if (looksLikeSystemVar(name))
350
+ continue;
351
+ // Heuristic: name contains KEY, TOKEN, SECRET, PASSWORD, PASS, URL, CREDENTIAL
352
+ if (/KEY|TOKEN|SECRET|PASS(WORD)?|CREDENTIAL|SALT|PRIVATE/i.test(name)) {
353
+ if (hasSecret(name))
354
+ result.add(name);
355
+ }
356
+ }
357
+ return [...result].sort();
358
+ }
359
+ /**
360
+ * Read the session archive from replit-tools directly.
361
+ * @param {string} [cwd]
362
+ * @returns {{ sessions: Array<{id, path, size, lastModified}>, totalSessions, latestTimestamp }}
363
+ */
364
+ export function getSessionArchive(cwd = process.cwd()) {
365
+ const toolsDir = findReplitToolsDir(resolve(cwd));
366
+ if (!toolsDir) {
367
+ return { sessions: [], totalSessions: 0, latestTimestamp: null };
368
+ }
369
+ const archiveBase = join(toolsDir, '.session-archive', 'claude');
370
+ if (!existsSync(archiveBase)) {
371
+ return { sessions: [], totalSessions: 0, latestTimestamp: null };
372
+ }
373
+ const sessions = [];
374
+ function scanDir(dir) {
375
+ for (const entry of safeReaddir(dir)) {
376
+ const full = join(dir, entry);
377
+ const st = safeStat(full);
378
+ if (!st)
379
+ continue;
380
+ if (st.isDirectory()) {
381
+ scanDir(full);
382
+ }
383
+ else if (entry.endsWith('.jsonl')) {
384
+ sessions.push({
385
+ id: entry.replace(/\.jsonl$/, ''),
386
+ path: full,
387
+ size: st.size,
388
+ lastModified: new Date(st.mtimeMs).toISOString(),
389
+ });
390
+ }
391
+ }
392
+ }
393
+ scanDir(archiveBase);
394
+ sessions.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
395
+ const latestTimestamp = sessions.length > 0 ? sessions[0].lastModified : null;
396
+ return {
397
+ sessions,
398
+ totalSessions: sessions.length,
399
+ latestTimestamp,
400
+ };
401
+ }
402
+ /**
403
+ * Get auth status from the claude-auth-refresh.sh script.
404
+ * @param {string} [cwd]
405
+ * @returns {{ available, tokenStatus, expiresAt, needsRefresh }}
406
+ */
407
+ export function getAuthStatus(cwd = process.cwd()) {
408
+ const toolsDir = findReplitToolsDir(resolve(cwd));
409
+ if (!toolsDir)
410
+ return { available: false };
411
+ const script = join(toolsDir, 'scripts', 'claude-auth-refresh.sh');
412
+ if (!existsSync(script))
413
+ return { available: false };
414
+ try {
415
+ const result = spawnSync('bash', [script, '--status'], {
416
+ encoding: 'utf8',
417
+ timeout: 10000,
418
+ stdio: ['ignore', 'pipe', 'pipe'],
419
+ });
420
+ if (result.status !== 0 && !result.stdout) {
421
+ return { available: true, tokenStatus: 'unknown', expiresAt: null, needsRefresh: false };
422
+ }
423
+ const output = (result.stdout || '') + (result.stderr || '');
424
+ // Parse common status patterns from the script output
425
+ let tokenStatus = 'unknown';
426
+ let expiresAt = null;
427
+ let needsRefresh = false;
428
+ if (/valid|ok|authenticated/i.test(output))
429
+ tokenStatus = 'valid';
430
+ else if (/expired|invalid|missing/i.test(output))
431
+ tokenStatus = 'expired';
432
+ else if (/refresh/i.test(output)) {
433
+ tokenStatus = 'expiring';
434
+ needsRefresh = true;
435
+ }
436
+ const expiresMatch = output.match(/expires[:\s]+([^\n]+)/i);
437
+ if (expiresMatch)
438
+ expiresAt = expiresMatch[1].trim();
439
+ if (/need.*refresh|should.*refresh/i.test(output))
440
+ needsRefresh = true;
441
+ return { available: true, tokenStatus, expiresAt, needsRefresh };
442
+ }
443
+ catch {
444
+ return { available: true, tokenStatus: 'unknown', expiresAt: null, needsRefresh: false };
445
+ }
446
+ }
447
+ /**
448
+ * Read replit-tools config.json settings that dual-brain should respect.
449
+ * @param {string} [cwd]
450
+ * @returns {{ recentWindowHours, persistenceDays, mirror, raw } | null}
451
+ */
452
+ export function getReplitToolsConfig(cwd = process.cwd()) {
453
+ const toolsDir = findReplitToolsDir(resolve(cwd));
454
+ if (!toolsDir)
455
+ return null;
456
+ const config = safeJson(join(toolsDir, 'config.json'));
457
+ if (!config)
458
+ return null;
459
+ return {
460
+ recentWindowHours: config.recentWindowHours ?? 48,
461
+ persistenceDays: config.persistenceDays ?? 365,
462
+ mirror: config.mirror ?? null,
463
+ raw: config,
464
+ };
465
+ }
466
+ // ─── Section 2: Planning ──────────────────────────────────────────────────────
467
+ const DUAL_BRAIN_HIDDEN = ['.dualbrain', '.replit-tools', '.dual-brain', 'node_modules'];
468
+ /**
469
+ * Plan .replit config changes needed to reach a desired state.
470
+ * No side effects — returns a plan object only.
471
+ * @param {object} [desired]
472
+ * @param {string} [cwd]
473
+ * @returns {{ changes, summary, riskLevel, preserves }}
474
+ */
475
+ export function planReplitConfig(desired = {}, cwd = process.cwd()) {
476
+ const current = inspectReplitConfig(resolve(cwd));
477
+ const changes = [];
478
+ const preserves = [];
479
+ // Track what we're keeping
480
+ if (Object.keys(current.nix).length)
481
+ preserves.push('existing nix config');
482
+ if (current.modules.length)
483
+ preserves.push(`modules: ${current.modules.join(', ')}`);
484
+ if (Object.keys(current.deployment).length)
485
+ preserves.push('existing deployment config');
486
+ // 1. Remove expertMode = true if set (suppresses random shell noise)
487
+ const wantExpertMode = desired.expertMode ?? false;
488
+ if (current.expertMode === true && !wantExpertMode) {
489
+ changes.push({
490
+ key: 'expertMode',
491
+ action: 'remove',
492
+ reason: 'prevents random shell spawning in Replit agent',
493
+ risk: 'medium',
494
+ });
495
+ }
496
+ // 2. hidden array — add dual-brain entries that are missing
497
+ const currentHidden = new Set(current.hidden);
498
+ const desiredHidden = (desired.hidden ?? DUAL_BRAIN_HIDDEN);
499
+ const missingHidden = desiredHidden.filter((h) => !currentHidden.has(h));
500
+ if (missingHidden.length) {
501
+ const merged = [...new Set([...current.hidden, ...desiredHidden])];
502
+ changes.push({
503
+ key: 'hidden',
504
+ action: 'add',
505
+ value: merged,
506
+ adds: missingHidden,
507
+ risk: 'low',
508
+ });
509
+ }
510
+ // 3. onBoot — ensure dual-brain is mentioned; if missing entirely, suggest adding
511
+ if (desired.onBoot !== undefined) {
512
+ if (current.onBoot !== desired.onBoot) {
513
+ changes.push({
514
+ key: 'onBoot',
515
+ action: 'set',
516
+ value: desired.onBoot,
517
+ previous: current.onBoot,
518
+ risk: 'low',
519
+ });
520
+ }
521
+ }
522
+ else if (!current.hasOnBoot) {
523
+ // Suggest a sensible default
524
+ const suggestedOnBoot = 'source .replit-tools/scripts/setup-claude-code.sh 2>/dev/null || true';
525
+ changes.push({
526
+ key: 'onBoot',
527
+ action: 'set',
528
+ value: suggestedOnBoot,
529
+ reason: 'ensure replit-tools auth persistence on container restart',
530
+ risk: 'low',
531
+ });
532
+ }
533
+ // 4. run — remove if it's a trivial/noop command; preserve if it looks like a dev server
534
+ if (current.hasRun && desired.removeRun !== false) {
535
+ const runVal = current.run || '';
536
+ const isTrivial = /^(echo|true|:|#|dual-brain|npx.*dual-brain)/i.test(runVal.trim());
537
+ if (isTrivial) {
538
+ changes.push({
539
+ key: 'run',
540
+ action: 'remove',
541
+ reason: 'vibe coders use `dual-brain go`, not the Run button — trivial command removed',
542
+ previous: runVal,
543
+ risk: 'medium',
544
+ });
545
+ }
546
+ else {
547
+ preserves.push(`run command: "${runVal.slice(0, 60)}${runVal.length > 60 ? '…' : ''}"`);
548
+ }
549
+ }
550
+ // Compute overall risk
551
+ const risks = changes.map(c => c.risk);
552
+ let riskLevel = 'low';
553
+ if (risks.includes('high'))
554
+ riskLevel = 'high';
555
+ else if (risks.includes('medium'))
556
+ riskLevel = 'medium';
557
+ // Summary
558
+ const actionSummary = changes.map(c => {
559
+ if (c.action === 'remove')
560
+ return `remove ${c.key}`;
561
+ if (c.action === 'add')
562
+ return `add ${c.adds?.join(', ')} to ${c.key}`;
563
+ if (c.action === 'set')
564
+ return `set ${c.key}`;
565
+ return `${c.action} ${c.key}`;
566
+ });
567
+ const summary = changes.length === 0
568
+ ? 'No changes needed — .replit is already optimal for dual-brain.'
569
+ : `${changes.length} change${changes.length > 1 ? 's' : ''}: ${actionSummary.join('; ')}.`;
570
+ return { changes, summary, riskLevel, preserves };
571
+ }
572
+ // ─── Section 3: Apply ─────────────────────────────────────────────────────────
573
+ /**
574
+ * Apply a planned change set to the .replit file.
575
+ * Only applies low-risk changes by default (skipMedium = false applies medium too).
576
+ * Preserves original file structure — patches in-place where possible.
577
+ *
578
+ * @param {Array} changes — from planReplitConfig
579
+ * @param {string} cwd
580
+ * @param {{ skipMedium?: boolean }} options
581
+ * @returns {string[]} list of applied change keys
582
+ */
583
+ function applyReplitChanges(changes, cwd, { skipMedium = false } = {}) {
584
+ if (!changes.length)
585
+ return [];
586
+ const replitPath = join(resolve(cwd), '.replit');
587
+ const raw = safeRead(replitPath) || '';
588
+ let lines = raw.split('\n');
589
+ const applied = [];
590
+ for (const change of changes) {
591
+ if (change.risk === 'high')
592
+ continue;
593
+ if (change.risk === 'medium' && skipMedium)
594
+ continue;
595
+ if (change.key === 'expertMode' && change.action === 'remove') {
596
+ // Remove the [agent] section lines containing expertMode
597
+ const newLines = [];
598
+ let inAgentSection = false;
599
+ let removedExpertMode = false;
600
+ for (const line of lines) {
601
+ const trimmed = line.trim();
602
+ if (trimmed === '[agent]') {
603
+ inAgentSection = true;
604
+ // Only include if there are other keys besides expertMode
605
+ // We'll add back if needed after scanning
606
+ newLines.push(line);
607
+ continue;
608
+ }
609
+ if (inAgentSection && trimmed.startsWith('[') && trimmed.endsWith(']')) {
610
+ inAgentSection = false;
611
+ }
612
+ if (inAgentSection && /^expertMode\s*=/.test(trimmed)) {
613
+ removedExpertMode = true;
614
+ continue; // skip this line
615
+ }
616
+ newLines.push(line);
617
+ }
618
+ if (removedExpertMode) {
619
+ // Clean up empty [agent] section
620
+ lines = cleanEmptySection(newLines, 'agent');
621
+ applied.push('expertMode');
622
+ }
623
+ }
624
+ else if (change.key === 'hidden' && change.action === 'add') {
625
+ const valueStr = formatTomlArray(change.value);
626
+ const replaced = replaceOrInsertTopLevel(lines, 'hidden', valueStr);
627
+ lines = replaced;
628
+ applied.push('hidden');
629
+ }
630
+ else if (change.key === 'onBoot' && change.action === 'set') {
631
+ const valueStr = `"${change.value}"`;
632
+ const replaced = replaceOrInsertTopLevel(lines, 'onBoot', valueStr);
633
+ lines = replaced;
634
+ applied.push('onBoot');
635
+ }
636
+ else if (change.key === 'run' && change.action === 'remove') {
637
+ lines = lines.filter(l => !/^run\s*=/.test(l.trim()));
638
+ applied.push('run');
639
+ }
640
+ }
641
+ if (applied.length) {
642
+ const newContent = lines.join('\n');
643
+ const tmp = replitPath + '.tmp.' + process.pid;
644
+ try {
645
+ writeFileSync(tmp, newContent);
646
+ renameSync(tmp, replitPath);
647
+ }
648
+ catch (err) {
649
+ try {
650
+ require('node:fs').unlinkSync(tmp);
651
+ }
652
+ catch { /* ignore */ }
653
+ throw err;
654
+ }
655
+ }
656
+ return applied;
657
+ }
658
+ function formatTomlArray(items) {
659
+ return '[' + items.map((i) => `"${i}"`).join(', ') + ']';
660
+ }
661
+ function replaceOrInsertTopLevel(lines, key, valueStr) {
662
+ const regex = new RegExp(`^${key}\\s*=`);
663
+ let found = false;
664
+ const result = lines.map((line) => {
665
+ if (regex.test(line.trim())) {
666
+ found = true;
667
+ return `${key} = ${valueStr}`;
668
+ }
669
+ return line;
670
+ });
671
+ if (!found) {
672
+ // Insert before first section header or at end
673
+ const firstSection = result.findIndex((l) => /^\s*\[/.test(l));
674
+ if (firstSection > 0) {
675
+ result.splice(firstSection, 0, `${key} = ${valueStr}`);
676
+ }
677
+ else {
678
+ result.push(`${key} = ${valueStr}`);
679
+ }
680
+ }
681
+ return result;
682
+ }
683
+ function cleanEmptySection(lines, sectionName) {
684
+ const header = `[${sectionName}]`;
685
+ const result = [];
686
+ let i = 0;
687
+ while (i < lines.length) {
688
+ const trimmed = lines[i].trim();
689
+ if (trimmed === header) {
690
+ // Look ahead: if next non-blank line is another section or EOF, skip the header
691
+ let j = i + 1;
692
+ while (j < lines.length && !lines[j].trim())
693
+ j++;
694
+ const nextIsSectionOrEnd = j >= lines.length || /^\[/.test(lines[j].trim());
695
+ if (nextIsSectionOrEnd) {
696
+ // Remove blank lines between removed header and next section
697
+ while (result.length && !result[result.length - 1].trim())
698
+ result.pop();
699
+ i = j;
700
+ continue;
701
+ }
702
+ }
703
+ result.push(lines[i]);
704
+ i++;
705
+ }
706
+ return result;
707
+ }
708
+ /**
709
+ * Main integration function: detect, inspect, plan, optionally apply.
710
+ * @param {{ dryRun?: boolean, cwd?: string, skipMedium?: boolean }} options
711
+ * @returns {{ environment, replitTools, config, plan, applied, report }}
712
+ */
713
+ export function initReplitIntegration({ dryRun = false, cwd = process.cwd() } = {}) {
714
+ const resolvedCwd = resolve(cwd);
715
+ const environment = detectReplitEnvironment(resolvedCwd);
716
+ const config = inspectReplitConfig(resolvedCwd);
717
+ const replitTools = inspectReplitTools(resolvedCwd);
718
+ const toolsConfig = getReplitToolsConfig(resolvedCwd);
719
+ // Plan optimal config
720
+ const plan = planReplitConfig({}, resolvedCwd);
721
+ let applied = [];
722
+ if (!dryRun && plan.changes.length) {
723
+ try {
724
+ applied = applyReplitChanges(plan.changes, resolvedCwd);
725
+ }
726
+ catch (err) {
727
+ applied = [];
728
+ }
729
+ }
730
+ const report = {
731
+ environment,
732
+ replitTools: {
733
+ ...replitTools,
734
+ toolsConfig,
735
+ },
736
+ config,
737
+ plan,
738
+ applied,
739
+ dryRun,
740
+ };
741
+ return report;
742
+ }
743
+ /**
744
+ * Thin escape hatch to run the replit CLI.
745
+ * @param {string[]} args
746
+ * @param {{ timeout?: number }} options
747
+ * @returns {{ ok, stdout, stderr }}
748
+ */
749
+ export function runReplitCli(args, options = {}) {
750
+ const timeout = options.timeout ?? 30000;
751
+ try {
752
+ const whichResult = spawnSync('which', ['replit'], { encoding: 'utf8' });
753
+ if (whichResult.status !== 0) {
754
+ return { ok: false, stdout: '', stderr: 'replit CLI not found in PATH' };
755
+ }
756
+ const result = spawnSync('replit', args, {
757
+ encoding: 'utf8',
758
+ timeout,
759
+ stdio: ['ignore', 'pipe', 'pipe'],
760
+ });
761
+ return {
762
+ ok: result.status === 0,
763
+ stdout: result.stdout || '',
764
+ stderr: result.stderr || '',
765
+ };
766
+ }
767
+ catch (err) {
768
+ const msg = err instanceof Error ? err.message : String(err);
769
+ return { ok: false, stdout: '', stderr: msg };
770
+ }
771
+ }
772
+ // ─── Section 4: Formatters ────────────────────────────────────────────────────
773
+ /**
774
+ * Pretty-print the integration report for TUI/dashboard display.
775
+ * @param {object} report — from initReplitIntegration
776
+ * @returns {string}
777
+ */
778
+ export function formatReplitReport(report) {
779
+ const environment = report.environment;
780
+ const replitTools = report.replitTools;
781
+ const config = report.config;
782
+ const plan = report.plan;
783
+ const applied = report.applied;
784
+ const dryRun = report.dryRun;
785
+ const lines = [];
786
+ // Environment
787
+ const envLabel = environment.isReplit ? 'Replit' : environment.containerType;
788
+ const uptimeLabel = environment.uptimeSeconds != null
789
+ ? ` (up ${Math.floor(Number(environment.uptimeSeconds) / 60)}m)`
790
+ : '';
791
+ lines.push(`Environment: ${envLabel}${uptimeLabel}`);
792
+ if (environment.nixChannel)
793
+ lines.push(` nix: ${environment.nixChannel}`);
794
+ // replit-tools
795
+ if (replitTools.installed) {
796
+ const ver = replitTools.version ? `v${replitTools.version}` : 'installed';
797
+ lines.push(`replit-tools: ${ver}`);
798
+ const sessionArchive = replitTools.sessionArchive;
799
+ const codexPlugins = replitTools.codexPlugins;
800
+ const shellSnapshots = replitTools.shellSnapshots;
801
+ const mcpAuthCache = replitTools.mcpAuthCache;
802
+ if (sessionArchive.exists) {
803
+ const tsLabel = sessionArchive.latestTimestamp
804
+ ? ` latest: ${new Date(sessionArchive.latestTimestamp).toLocaleDateString()}`
805
+ : '';
806
+ lines.push(` sessions: ${sessionArchive.sessionCount}${tsLabel}`);
807
+ }
808
+ if (Number(codexPlugins.count) > 0)
809
+ lines.push(` codex plugins: ${codexPlugins.count}`);
810
+ if (Number(shellSnapshots.count) > 0)
811
+ lines.push(` shell snapshots: ${shellSnapshots.count}`);
812
+ if (Number(mcpAuthCache.entries) > 0)
813
+ lines.push(` mcp cached: ${mcpAuthCache.entries} servers`);
814
+ if (replitTools.toolsConfig) {
815
+ lines.push(` session window: ${replitTools.toolsConfig.recentWindowHours}h`);
816
+ }
817
+ }
818
+ else {
819
+ lines.push('replit-tools: not found');
820
+ }
821
+ // Current .replit
822
+ lines.push('.replit:');
823
+ if (config.raw === null) {
824
+ lines.push(' (not found)');
825
+ }
826
+ else {
827
+ if (config.hasExpertMode)
828
+ lines.push(` expertMode: ${config.expertMode}`);
829
+ if (config.hidden?.length)
830
+ lines.push(` hidden: ${config.hidden.join(', ')}`);
831
+ if (config.hasOnBoot)
832
+ lines.push(` onBoot: ${(config.onBoot || '').slice(0, 60)}…`);
833
+ if (config.modules?.length)
834
+ lines.push(` modules: ${config.modules.join(', ')}`);
835
+ }
836
+ // Plan
837
+ if (plan.changes.length === 0) {
838
+ lines.push('Config: already optimal');
839
+ }
840
+ else {
841
+ lines.push(`Plan (${dryRun ? 'dry-run' : plan.riskLevel} risk):`);
842
+ for (const c of plan.changes) {
843
+ const prefix = ` [${c.risk}]`;
844
+ if (c.action === 'remove')
845
+ lines.push(`${prefix} remove ${c.key} — ${c.reason || ''}`);
846
+ else if (c.action === 'add')
847
+ lines.push(`${prefix} add to ${c.key}: ${c.adds?.join(', ')}`);
848
+ else if (c.action === 'set')
849
+ lines.push(`${prefix} set ${c.key}`);
850
+ }
851
+ if (plan.preserves?.length)
852
+ lines.push(` preserves: ${plan.preserves.join('; ')}`);
853
+ }
854
+ // Applied
855
+ if (!dryRun && applied?.length > 0) {
856
+ lines.push(`Applied: ${applied.join(', ')}`);
857
+ }
858
+ else if (!dryRun && plan.changes.length > 0) {
859
+ lines.push('Applied: none (errors or all changes were medium/high risk)');
860
+ }
861
+ return lines.join('\n');
862
+ }
863
+ // ─── Section 5: Plugin Inventory ──────────────────────────────────────────────
864
+ /** In-process cache for plugin inventory (plugins don't change during a session). */
865
+ let _pluginInventoryCache = null;
866
+ /**
867
+ * Parse YAML-style frontmatter from a SKILL.md string.
868
+ * Returns { name, description, metadata } — all optional.
869
+ * @param {string} content
870
+ * @returns {{ name?: string, description?: string, metadata?: object }}
871
+ */
872
+ function _parseFrontmatter(content) {
873
+ if (!content || !content.startsWith('---'))
874
+ return {};
875
+ const end = content.indexOf('\n---', 3);
876
+ if (end === -1)
877
+ return {};
878
+ const fm = content.slice(3, end).trim();
879
+ const result = {};
880
+ for (const line of fm.split('\n')) {
881
+ const colon = line.indexOf(':');
882
+ if (colon === -1)
883
+ continue;
884
+ const key = line.slice(0, colon).trim();
885
+ const val = line.slice(colon + 1).trim().replace(/^["']|["']$/g, '');
886
+ if (key === 'name')
887
+ result.name = val;
888
+ else if (key === 'description')
889
+ result.description = val;
890
+ }
891
+ return result;
892
+ }
893
+ /**
894
+ * Scan the Codex plugin directory and return a structured inventory.
895
+ * Reads each plugin's skills subdirectories for SKILL.md (name, description, capabilities).
896
+ * Result is cached after the first call.
897
+ *
898
+ * @param {string} [cwd]
899
+ * @returns {{ plugins: Array<{ id, name, description, capabilities, skillNames, path }>, count }}
900
+ */
901
+ export function getPluginInventory(cwd = process.cwd()) {
902
+ if (_pluginInventoryCache)
903
+ return _pluginInventoryCache;
904
+ const toolsDir = findReplitToolsDir(resolve(cwd));
905
+ if (!toolsDir) {
906
+ _pluginInventoryCache = { plugins: [], count: 0 };
907
+ return _pluginInventoryCache;
908
+ }
909
+ const pluginsDir = join(toolsDir, '.codex-persistent', '.tmp', 'plugins', 'plugins');
910
+ if (!existsSync(pluginsDir)) {
911
+ _pluginInventoryCache = { plugins: [], count: 0 };
912
+ return _pluginInventoryCache;
913
+ }
914
+ const plugins = [];
915
+ for (const pluginId of safeReaddir(pluginsDir)) {
916
+ const pluginPath = join(pluginsDir, pluginId);
917
+ const st = safeStat(pluginPath);
918
+ if (!st || !st.isDirectory())
919
+ continue;
920
+ const skillsDir = join(pluginPath, 'skills');
921
+ const skillDirs = existsSync(skillsDir) ? safeReaddir(skillsDir) : [];
922
+ let pluginName = pluginId;
923
+ let pluginDescription = '';
924
+ const capabilities = [];
925
+ const skillNames = [];
926
+ for (const skillDir of skillDirs) {
927
+ const skillPath = join(skillsDir, skillDir);
928
+ const skillSt = safeStat(skillPath);
929
+ if (!skillSt || !skillSt.isDirectory())
930
+ continue;
931
+ const skillMdPath = join(skillPath, 'SKILL.md');
932
+ const skillContent = safeRead(skillMdPath);
933
+ if (!skillContent)
934
+ continue;
935
+ const fm = _parseFrontmatter(skillContent);
936
+ // Use the first skill's name/description as the plugin's primary identity
937
+ if (fm.name && pluginName === pluginId)
938
+ pluginName = fm.name;
939
+ if (fm.description && !pluginDescription)
940
+ pluginDescription = fm.description;
941
+ // Collect all skill names as capabilities
942
+ if (fm.name) {
943
+ skillNames.push(fm.name);
944
+ capabilities.push(fm.name);
945
+ }
946
+ else {
947
+ skillNames.push(skillDir);
948
+ capabilities.push(skillDir);
949
+ }
950
+ // Extract additional capabilities from description keywords
951
+ if (fm.description) {
952
+ // Pull out words in "Triggers: X, Y, Z" format if present
953
+ const triggerMatch = fm.description.match(/[Tt]riggers?:\s*([^.]+)/);
954
+ if (triggerMatch) {
955
+ const triggers = triggerMatch[1].split(/[,;]+/).map((s) => s.trim()).filter((s) => s.length > 1 && s.length < 30);
956
+ capabilities.push(...triggers);
957
+ }
958
+ }
959
+ }
960
+ plugins.push({
961
+ id: pluginId,
962
+ name: pluginName,
963
+ description: pluginDescription,
964
+ capabilities: [...new Set(capabilities)],
965
+ skillNames,
966
+ path: pluginPath,
967
+ });
968
+ }
969
+ _pluginInventoryCache = { plugins, count: plugins.length };
970
+ return _pluginInventoryCache;
971
+ }
972
+ /**
973
+ * Match plugins to a task description using keyword matching.
974
+ * Returns plugins sorted by relevance score (descending).
975
+ *
976
+ * @param {string} taskDescription
977
+ * @param {Array<{ id, name, description, capabilities, skillNames, path }>} [plugins]
978
+ * @param {string} [cwd]
979
+ * @returns {Array<{ plugin: object, relevance: number, reason: string }>}
980
+ */
981
+ export function matchPluginsForTask(taskDescription, plugins, cwd = process.cwd()) {
982
+ if (!taskDescription)
983
+ return [];
984
+ const inventory = plugins ?? getPluginInventory(cwd).plugins;
985
+ if (!inventory || inventory.length === 0)
986
+ return [];
987
+ const desc = taskDescription.toLowerCase();
988
+ const results = [];
989
+ for (const plugin of inventory) {
990
+ let score = 0;
991
+ const reasons = [];
992
+ // Check plugin id (e.g. "stripe" in "check stripe webhook") — highest weight
993
+ const idLower = plugin.id.toLowerCase();
994
+ if (desc.includes(idLower)) {
995
+ score += 3;
996
+ reasons.push(`plugin id "${plugin.id}" mentioned`);
997
+ }
998
+ // Check plugin name
999
+ const nameLower = plugin.name.toLowerCase();
1000
+ if (nameLower !== idLower && desc.includes(nameLower)) {
1001
+ score += 2;
1002
+ reasons.push(`plugin name "${plugin.name}" mentioned`);
1003
+ }
1004
+ // Check description keywords (≥4 chars to avoid noise)
1005
+ if (plugin.description) {
1006
+ const descWords = plugin.description
1007
+ .toLowerCase()
1008
+ .split(/\W+/)
1009
+ .filter((w) => w.length >= 4);
1010
+ for (const word of descWords) {
1011
+ if (desc.includes(word)) {
1012
+ score += 1;
1013
+ reasons.push(`keyword "${word}"`);
1014
+ break; // one hit per description is enough
1015
+ }
1016
+ }
1017
+ }
1018
+ // Check skill names
1019
+ for (const skill of plugin.skillNames) {
1020
+ if (desc.includes(skill.toLowerCase())) {
1021
+ score += 2;
1022
+ reasons.push(`skill "${skill}" mentioned`);
1023
+ break;
1024
+ }
1025
+ }
1026
+ // Check capabilities
1027
+ for (const cap of plugin.capabilities) {
1028
+ if (cap.length >= 4 && desc.includes(cap.toLowerCase())) {
1029
+ score += 1;
1030
+ reasons.push(`capability "${cap}" matched`);
1031
+ break;
1032
+ }
1033
+ }
1034
+ if (score > 0) {
1035
+ results.push({
1036
+ plugin,
1037
+ relevance: score,
1038
+ reason: reasons.slice(0, 3).join('; '),
1039
+ });
1040
+ }
1041
+ }
1042
+ return results.sort((a, b) => b.relevance - a.relevance);
1043
+ }
1044
+ // ─── Section 6: Session Archive Search ────────────────────────────────────────
1045
+ /**
1046
+ * Search the Claude session archive for keyword matches in user messages.
1047
+ * Reads session files line by line to avoid loading full files into memory.
1048
+ * Results are recency-weighted: today ×2, this week ×1.5, older ×1.
1049
+ *
1050
+ * @param {string} query
1051
+ * @param {{ limit?: number, days?: number }} [options]
1052
+ * @param {string} [cwd]
1053
+ * @returns {Promise<Array<{ sessionId, date, matchingMessage, relevance }>>}
1054
+ */
1055
+ export async function searchSessionArchive(query, options = {}, cwd = process.cwd()) {
1056
+ const { limit = 5, days = 30 } = options;
1057
+ if (!query)
1058
+ return [];
1059
+ const toolsDir = findReplitToolsDir(resolve(cwd));
1060
+ if (!toolsDir)
1061
+ return [];
1062
+ const archiveBase = join(toolsDir, '.session-archive', 'claude');
1063
+ if (!existsSync(archiveBase))
1064
+ return [];
1065
+ const queryTerms = query.toLowerCase().split(/\s+/).filter((t) => t.length >= 2);
1066
+ if (queryTerms.length === 0)
1067
+ return [];
1068
+ const now = Date.now();
1069
+ const cutoffMs = now - days * 24 * 60 * 60 * 1000;
1070
+ const oneDayMs = 24 * 60 * 60 * 1000;
1071
+ const oneWeekMs = 7 * oneDayMs;
1072
+ // Collect JSONL session files (not history.jsonl which has different format)
1073
+ const sessionFiles = [];
1074
+ function collectJsonl(dir) {
1075
+ try {
1076
+ for (const entry of safeReaddir(dir)) {
1077
+ if (entry === 'history.jsonl')
1078
+ continue; // skip — different structure
1079
+ const full = join(dir, entry);
1080
+ const st = safeStat(full);
1081
+ if (!st)
1082
+ continue;
1083
+ if (st.isDirectory()) {
1084
+ collectJsonl(full);
1085
+ }
1086
+ else if (entry.endsWith('.jsonl')) {
1087
+ if (st.mtimeMs >= cutoffMs) {
1088
+ sessionFiles.push({ path: full, mtime: st.mtimeMs });
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+ catch { /* ignore unreadable dirs */ }
1094
+ }
1095
+ collectJsonl(archiveBase);
1096
+ if (sessionFiles.length === 0)
1097
+ return [];
1098
+ // Sort newest first so we hit the most relevant sessions early
1099
+ sessionFiles.sort((a, b) => b.mtime - a.mtime);
1100
+ const matches = [];
1101
+ for (const { path: filePath, mtime } of sessionFiles) {
1102
+ // Age-based recency weight
1103
+ const ageMs = now - mtime;
1104
+ const recency = ageMs < oneDayMs ? 2.0 : ageMs < oneWeekMs ? 1.5 : 1.0;
1105
+ // Derive sessionId from filename
1106
+ const sessionId = filePath.split('/').pop().replace(/\.jsonl$/, '');
1107
+ const date = new Date(mtime).toISOString().slice(0, 10);
1108
+ let fileMatched = false;
1109
+ await new Promise((resolveFn) => {
1110
+ try {
1111
+ const rl = createInterface({
1112
+ input: createReadStream(filePath, { encoding: 'utf8' }),
1113
+ crlfDelay: Infinity,
1114
+ });
1115
+ rl.on('line', (line) => {
1116
+ if (!line || fileMatched)
1117
+ return;
1118
+ try {
1119
+ const entry = JSON.parse(line);
1120
+ // Only look at user messages
1121
+ if (entry.type !== 'user')
1122
+ return;
1123
+ if (entry.isMeta)
1124
+ return; // skip meta/command-caveat lines
1125
+ const content = entry.message?.content;
1126
+ if (!content || typeof content !== 'string')
1127
+ return;
1128
+ if (content.length < 3)
1129
+ return;
1130
+ const contentLower = content.toLowerCase();
1131
+ let termScore = 0;
1132
+ for (const term of queryTerms) {
1133
+ if (contentLower.includes(term))
1134
+ termScore++;
1135
+ }
1136
+ if (termScore === 0)
1137
+ return;
1138
+ const relevance = Math.round(termScore * recency * 10) / 10;
1139
+ const snippet = content.length > 120 ? content.slice(0, 120) + '…' : content;
1140
+ matches.push({ sessionId, date, matchingMessage: snippet, relevance });
1141
+ fileMatched = true; // one match per session file is enough for the index
1142
+ }
1143
+ catch { /* skip malformed lines */ }
1144
+ });
1145
+ rl.on('close', resolveFn);
1146
+ rl.on('error', resolveFn);
1147
+ }
1148
+ catch {
1149
+ resolveFn();
1150
+ }
1151
+ });
1152
+ // Early exit once we have plenty of candidates
1153
+ if (matches.length >= limit * 4)
1154
+ break;
1155
+ }
1156
+ // Sort by relevance descending, return top `limit`
1157
+ matches.sort((a, b) => b.relevance - a.relevance);
1158
+ return matches.slice(0, limit);
1159
+ }
1160
+ //# sourceMappingURL=replit.js.map