dual-brain 0.2.30 → 0.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 (309) 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/task-classifier.mjs +328 -0
  240. package/hooks/vibe-router.mjs +387 -0
  241. package/package.json +29 -153
  242. package/src/agents/registry.mjs +0 -405
  243. package/src/awareness.mjs +0 -425
  244. package/src/brief.mjs +0 -266
  245. package/src/calibration.mjs +0 -148
  246. package/src/checkpoint.mjs +0 -109
  247. package/src/ci-triage.mjs +0 -191
  248. package/src/cognitive-loop.mjs +0 -562
  249. package/src/collaboration.mjs +0 -545
  250. package/src/context-intel.mjs +0 -158
  251. package/src/context.mjs +0 -389
  252. package/src/continuity.mjs +0 -298
  253. package/src/cost-tracker.mjs +0 -184
  254. package/src/debrief.mjs +0 -228
  255. package/src/decide.mjs +0 -1099
  256. package/src/decompose.mjs +0 -331
  257. package/src/detect.mjs +0 -702
  258. package/src/dispatch.mjs +0 -1447
  259. package/src/doctor.mjs +0 -1607
  260. package/src/envelope.mjs +0 -139
  261. package/src/failure-memory.mjs +0 -178
  262. package/src/fx.mjs +0 -276
  263. package/src/governance.mjs +0 -279
  264. package/src/handoff.mjs +0 -87
  265. package/src/head-protocol.mjs +0 -128
  266. package/src/head.mjs +0 -952
  267. package/src/health.mjs +0 -528
  268. package/src/inbox.mjs +0 -195
  269. package/src/index.mjs +0 -44
  270. package/src/install-hooks.mjs +0 -100
  271. package/src/integrity.mjs +0 -245
  272. package/src/intelligence.mjs +0 -447
  273. package/src/ledger.mjs +0 -196
  274. package/src/living-docs.mjs +0 -210
  275. package/src/memory-tiers.mjs +0 -193
  276. package/src/models.mjs +0 -363
  277. package/src/narrative.mjs +0 -169
  278. package/src/nextstep.mjs +0 -100
  279. package/src/observer.mjs +0 -241
  280. package/src/outcome.mjs +0 -400
  281. package/src/pipeline.mjs +0 -1711
  282. package/src/playbook.mjs +0 -257
  283. package/src/pr-agent.mjs +0 -214
  284. package/src/predictive.mjs +0 -250
  285. package/src/profile.mjs +0 -1411
  286. package/src/prompt-audit.mjs +0 -231
  287. package/src/prompt-intel.mjs +0 -325
  288. package/src/provider-context.mjs +0 -257
  289. package/src/receipt.mjs +0 -344
  290. package/src/recommendations.mjs +0 -296
  291. package/src/redact.mjs +0 -192
  292. package/src/replit.mjs +0 -1210
  293. package/src/repo.mjs +0 -445
  294. package/src/revert.mjs +0 -149
  295. package/src/routing-advisor.mjs +0 -204
  296. package/src/self-correct.mjs +0 -147
  297. package/src/session-lock.mjs +0 -160
  298. package/src/session.mjs +0 -1655
  299. package/src/settings-tui.mjs +0 -373
  300. package/src/setup-flow.mjs +0 -223
  301. package/src/signal.mjs +0 -115
  302. package/src/simmer.mjs +0 -241
  303. package/src/strategy.mjs +0 -235
  304. package/src/subscription.mjs +0 -212
  305. package/src/templates.mjs +0 -260
  306. package/src/think-engine.mjs +0 -428
  307. package/src/tui.mjs +0 -276
  308. package/src/update-check.mjs +0 -35
  309. 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