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