triflux 8.12.2 → 9.0.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +64 -0
- package/hub/team/backend.mjs +2 -1
- package/hub/team/cli/commands/start/index.mjs +2 -2
- package/hub/team/cli/commands/start/parse-args.mjs +10 -0
- package/hub/workers/delegator-mcp.mjs +2 -5
- package/package.json +1 -1
- package/scripts/cache-buildup.mjs +24 -395
- package/scripts/cache-doctor.mjs +149 -0
- package/scripts/cache-warmup.mjs +514 -0
- package/scripts/cross-review-gate.mjs +180 -0
- package/scripts/cross-review-tracker.mjs +279 -0
- package/scripts/headless-guard.mjs +38 -0
- package/scripts/lib/env-probe.mjs +130 -0
- package/scripts/lib/mcp-filter.mjs +730 -720
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/mcp-gateway-config.mjs +104 -7
- package/scripts/mcp-gateway-start.mjs +7 -0
- package/scripts/mcp-gateway-verify.mjs +15 -1
- package/scripts/preflight-cache.mjs +68 -137
- package/scripts/session-spawn-helper.mjs +184 -0
- package/scripts/setup.mjs +7 -8
- package/scripts/tfx-route-worker.mjs +59 -1
- package/skills/merge-worktree/SKILL.md +144 -0
- package/skills/tfx-analysis/SKILL.md +1 -0
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md +1 -0
- package/skills/tfx-autopilot/SKILL.md +1 -2
- package/skills/tfx-codex/SKILL.md +2 -0
- package/skills/tfx-codex-swarm/SKILL.md +62 -18
- package/skills/tfx-codex-swarm/mcp-daemon/start-daemons.ps1 +54 -0
- package/skills/tfx-codex-swarm/mcp-daemon/stop-daemons.ps1 +15 -0
- package/skills/tfx-consensus/SKILL.md +1 -0
- package/skills/tfx-deep-analysis/SKILL.md +1 -0
- package/skills/tfx-deep-plan/SKILL.md +1 -0
- package/skills/tfx-deep-qa/SKILL.md +1 -0
- package/skills/tfx-deep-research/SKILL.md +1 -0
- package/skills/tfx-deep-review/SKILL.md +1 -0
- package/skills/tfx-doctor/SKILL.md +5 -0
- package/skills/tfx-gemini/SKILL.md +1 -0
- package/skills/tfx-hub/SKILL.md +1 -0
- package/skills/tfx-multi/SKILL.md +1 -0
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-ralph/SKILL.md +2 -5
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +2 -0
- package/skills/tfx-setup/SKILL.md +182 -7
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CACHE_TARGETS,
|
|
8
|
+
checkSearchEngines,
|
|
9
|
+
extractProjectMeta,
|
|
10
|
+
probeTierEnvironment,
|
|
11
|
+
resolveTargetPath,
|
|
12
|
+
scanCodexSkills,
|
|
13
|
+
} from "./cache-warmup.mjs";
|
|
14
|
+
|
|
15
|
+
const TARGET_PAYLOAD_BUILDERS = Object.freeze({
|
|
16
|
+
codexSkills: scanCodexSkills,
|
|
17
|
+
tierEnvironment: probeTierEnvironment,
|
|
18
|
+
projectMeta: extractProjectMeta,
|
|
19
|
+
searchEngines: checkSearchEngines,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const VOLATILE_KEYS = new Set([
|
|
23
|
+
"timestamp",
|
|
24
|
+
"scanned_at",
|
|
25
|
+
"probed_at",
|
|
26
|
+
"extracted_at",
|
|
27
|
+
"checked_at",
|
|
28
|
+
"generated_at",
|
|
29
|
+
"built_at",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function normalizeValue(value) {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value.map((item) => normalizeValue(item));
|
|
35
|
+
}
|
|
36
|
+
if (value && typeof value === "object") {
|
|
37
|
+
const normalized = {};
|
|
38
|
+
for (const key of Object.keys(value).sort()) {
|
|
39
|
+
if (VOLATILE_KEYS.has(key)) continue;
|
|
40
|
+
normalized[key] = normalizeValue(value[key]);
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function inspectTarget(target, options = {}) {
|
|
48
|
+
const filePath = resolveTargetPath(target, options);
|
|
49
|
+
if (!existsSync(filePath)) {
|
|
50
|
+
return { target, status: "missing", file: filePath };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
target,
|
|
59
|
+
status: "invalid",
|
|
60
|
+
file: filePath,
|
|
61
|
+
error: error.message,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const expected = TARGET_PAYLOAD_BUILDERS[target](options);
|
|
66
|
+
const currentComparable = JSON.stringify(normalizeValue(parsed));
|
|
67
|
+
const expectedComparable = JSON.stringify(normalizeValue(expected));
|
|
68
|
+
|
|
69
|
+
if (currentComparable !== expectedComparable) {
|
|
70
|
+
return {
|
|
71
|
+
target,
|
|
72
|
+
status: "mismatch",
|
|
73
|
+
file: filePath,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
target,
|
|
79
|
+
status: "ok",
|
|
80
|
+
file: filePath,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function verifyCaches(options = {}) {
|
|
85
|
+
const targets = options.targets?.length ? options.targets : Object.keys(CACHE_TARGETS);
|
|
86
|
+
const results = targets.map((target) => inspectTarget(target, options));
|
|
87
|
+
const issueCount = results.filter((result) => result.status !== "ok").length;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
ok: issueCount === 0,
|
|
91
|
+
issue_count: issueCount,
|
|
92
|
+
results,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function fixCaches(options = {}) {
|
|
97
|
+
const verification = options.verification || verifyCaches(options);
|
|
98
|
+
const brokenTargets = verification.results
|
|
99
|
+
.filter((result) => result.status !== "ok")
|
|
100
|
+
.map((result) => result.target);
|
|
101
|
+
|
|
102
|
+
if (brokenTargets.length === 0) {
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
fixed: [],
|
|
106
|
+
summary: { ok: true, built: 0, skipped: 0, failed: 0, results: [] },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const warmup = await import("./cache-warmup.mjs");
|
|
111
|
+
const summary = warmup.buildAll({
|
|
112
|
+
...options,
|
|
113
|
+
force: true,
|
|
114
|
+
targets: brokenTargets,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
ok: summary.ok,
|
|
119
|
+
fixed: brokenTargets,
|
|
120
|
+
summary,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatVerificationSummary(verification) {
|
|
125
|
+
const label = verification.ok ? "cache-doctor: ok" : "cache-doctor: issues";
|
|
126
|
+
const details = verification.results.map((result) => `${result.target}:${result.status}`);
|
|
127
|
+
return `${label} (${details.join(", ")})`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function main() {
|
|
131
|
+
const shouldFix = process.argv.includes("--fix");
|
|
132
|
+
const verification = verifyCaches();
|
|
133
|
+
|
|
134
|
+
if (!shouldFix) {
|
|
135
|
+
console.log(formatVerificationSummary(verification));
|
|
136
|
+
if (!verification.ok) process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const repair = await fixCaches({ verification });
|
|
141
|
+
const repaired = repair.fixed.length > 0 ? `fixed:${repair.fixed.join(",")}` : "fixed:none";
|
|
142
|
+
const suffix = repair.summary.results.map((result) => `${result.target}:${result.status}`).join(", ");
|
|
143
|
+
console.log(`cache-doctor: fix (${repaired}${suffix ? `, ${suffix}` : ""})`);
|
|
144
|
+
if (!repair.ok) process.exitCode = 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
148
|
+
await main();
|
|
149
|
+
}
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import { join, basename, dirname } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
import { readPreflightCache } from "./preflight-cache.mjs";
|
|
17
|
+
import { checkCli, checkHub, detectCodexPlan } from "./lib/env-probe.mjs";
|
|
18
|
+
import { SEARCH_SERVER_ORDER, MCP_SERVER_DOMAIN_TAGS } from "./lib/mcp-server-catalog.mjs";
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
export const CACHE_TARGETS = Object.freeze({
|
|
23
|
+
codexSkills: Object.freeze({
|
|
24
|
+
key: "codexSkills",
|
|
25
|
+
file: ["cache", "codex-skills.json"],
|
|
26
|
+
}),
|
|
27
|
+
tierEnvironment: Object.freeze({
|
|
28
|
+
key: "tierEnvironment",
|
|
29
|
+
file: ["state", "tier-environment.json"],
|
|
30
|
+
}),
|
|
31
|
+
projectMeta: Object.freeze({
|
|
32
|
+
key: "projectMeta",
|
|
33
|
+
file: ["cache", "project-meta.json"],
|
|
34
|
+
}),
|
|
35
|
+
searchEngines: Object.freeze({
|
|
36
|
+
key: "searchEngines",
|
|
37
|
+
file: ["state", "search-engines.json"],
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const ROLE_KEYWORDS = {
|
|
42
|
+
plan: ["plan", "계획", "decompos", "strategy", "설계"],
|
|
43
|
+
auto: ["autonomous", "자율", "auto-execute", "autopilot", "full auto"],
|
|
44
|
+
persist: ["loop", "반복", "completion", "persist", "until", "끝까지"],
|
|
45
|
+
investigate: ["investigate", "research", "조사", "분석", "analysis"],
|
|
46
|
+
review: ["review", "리뷰", "inspect", "검수"],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function resolveHomeDir(homeDir = homedir()) {
|
|
50
|
+
return homeDir;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveRootDirs(cwd = process.cwd()) {
|
|
54
|
+
const omcDir = join(cwd, ".omc");
|
|
55
|
+
return {
|
|
56
|
+
cwd,
|
|
57
|
+
omcDir,
|
|
58
|
+
cacheDir: join(omcDir, "cache"),
|
|
59
|
+
stateDir: join(omcDir, "state"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ensureDir(dirPath) {
|
|
64
|
+
mkdirSync(dirPath, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeJSON(filePath, payload) {
|
|
68
|
+
ensureDir(dirname(filePath));
|
|
69
|
+
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeTargets(targets) {
|
|
73
|
+
if (!targets?.length) return Object.keys(CACHE_TARGETS);
|
|
74
|
+
const allowed = new Set(Object.keys(CACHE_TARGETS));
|
|
75
|
+
return [...new Set(targets)].filter((target) => allowed.has(target));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveTargetPath(target, { cwd = process.cwd() } = {}) {
|
|
79
|
+
const spec = CACHE_TARGETS[target];
|
|
80
|
+
if (!spec) throw new Error(`unknown cache target: ${target}`);
|
|
81
|
+
|
|
82
|
+
const { omcDir } = resolveRootDirs(cwd);
|
|
83
|
+
return join(omcDir, ...spec.file);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveTtlMs(target, options = {}) {
|
|
87
|
+
if (Number.isFinite(options.ttlByTarget?.[target])) {
|
|
88
|
+
return Math.max(0, Math.trunc(options.ttlByTarget[target]));
|
|
89
|
+
}
|
|
90
|
+
if (Number.isFinite(options.ttlMs)) {
|
|
91
|
+
return Math.max(0, Math.trunc(options.ttlMs));
|
|
92
|
+
}
|
|
93
|
+
return DEFAULT_CACHE_TTL_MS;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isFresh(target, options = {}) {
|
|
97
|
+
const filePath = resolveTargetPath(target, options);
|
|
98
|
+
if (!existsSync(filePath)) return false;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const ttlMs = resolveTtlMs(target, options);
|
|
102
|
+
const now = options.now ?? Date.now();
|
|
103
|
+
const stat = statSync(filePath);
|
|
104
|
+
return ttlMs > 0 && (now - stat.mtimeMs) < ttlMs;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function classifyRole(description) {
|
|
111
|
+
const lower = (description || "").toLowerCase();
|
|
112
|
+
for (const [role, keywords] of Object.entries(ROLE_KEYWORDS)) {
|
|
113
|
+
if (keywords.some((keyword) => lower.includes(keyword))) return role;
|
|
114
|
+
}
|
|
115
|
+
return "general";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseSkillFrontmatter(content) {
|
|
119
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
120
|
+
if (!match) return null;
|
|
121
|
+
|
|
122
|
+
const frontmatter = match[1];
|
|
123
|
+
const name = frontmatter.match(/^name:\s*(.+)$/m)?.[1]?.trim() || null;
|
|
124
|
+
const description = frontmatter.match(/^description:\s*(.+)$/m)?.[1]?.trim() || null;
|
|
125
|
+
return name ? { name, description } : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function scanCodexSkills(options = {}) {
|
|
129
|
+
const homeDir = resolveHomeDir(options.homeDir);
|
|
130
|
+
const codexSkillsDir = join(homeDir, ".codex", "skills");
|
|
131
|
+
const skills = [];
|
|
132
|
+
|
|
133
|
+
if (existsSync(codexSkillsDir)) {
|
|
134
|
+
for (const entry of readdirSync(codexSkillsDir, { withFileTypes: true })) {
|
|
135
|
+
if (!entry.isDirectory()) continue;
|
|
136
|
+
|
|
137
|
+
const skillMd = join(codexSkillsDir, entry.name, "SKILL.md");
|
|
138
|
+
if (!existsSync(skillMd)) continue;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const content = readFileSync(skillMd, "utf8");
|
|
142
|
+
const frontmatter = parseSkillFrontmatter(content);
|
|
143
|
+
const name = frontmatter?.name || entry.name;
|
|
144
|
+
const description = frontmatter?.description || "";
|
|
145
|
+
skills.push({
|
|
146
|
+
name,
|
|
147
|
+
role: classifyRole(description),
|
|
148
|
+
description: description.slice(0, 200),
|
|
149
|
+
source: "custom",
|
|
150
|
+
path: skillMd,
|
|
151
|
+
});
|
|
152
|
+
} catch {
|
|
153
|
+
skills.push({
|
|
154
|
+
name: entry.name,
|
|
155
|
+
role: "general",
|
|
156
|
+
description: "",
|
|
157
|
+
source: "custom",
|
|
158
|
+
path: skillMd,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const builtinSkills = [
|
|
165
|
+
{ name: "web-clone", role: "general", description: "Clone and analyze web pages" },
|
|
166
|
+
{ name: "help", role: "general", description: "Show available commands" },
|
|
167
|
+
{ name: "note", role: "general", description: "Save notes during session" },
|
|
168
|
+
{ name: "worker", role: "auto", description: "Spawn background worker for tasks" },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
for (const builtin of builtinSkills) {
|
|
172
|
+
if (skills.some((skill) => skill.name === builtin.name)) continue;
|
|
173
|
+
if (existsSync(join(codexSkillsDir, builtin.name))) continue;
|
|
174
|
+
skills.push({ ...builtin, source: "builtin" });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
skills.sort((left, right) => left.name.localeCompare(right.name));
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
scanned_at: new Date(options.now ?? Date.now()).toISOString(),
|
|
181
|
+
codex_skills_dir: codexSkillsDir,
|
|
182
|
+
total: skills.length,
|
|
183
|
+
skills,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function probeTierEnvironment(options = {}) {
|
|
188
|
+
const homeDir = resolveHomeDir(options.homeDir);
|
|
189
|
+
const preflight = options.preflight ?? readPreflightCache();
|
|
190
|
+
const execSyncFn = options.execSyncFn || execSync;
|
|
191
|
+
|
|
192
|
+
const codexCheck = preflight?.codex || checkCli("codex", { execSyncFn });
|
|
193
|
+
const geminiCheck = preflight?.gemini || checkCli("gemini", { execSyncFn });
|
|
194
|
+
const hubCheck = preflight?.hub || checkHub({
|
|
195
|
+
pkgRoot: options.pkgRoot,
|
|
196
|
+
restart: options.hubRestart === true,
|
|
197
|
+
requestTimeoutMs: options.hubTimeoutMs ?? 1000,
|
|
198
|
+
pollAttempts: options.hubRestart === true ? 8 : 0,
|
|
199
|
+
execSyncFn,
|
|
200
|
+
});
|
|
201
|
+
const codexPlan = preflight?.codex_plan || detectCodexPlan({ homeDir });
|
|
202
|
+
|
|
203
|
+
const checks = {
|
|
204
|
+
psmux: false,
|
|
205
|
+
hub: !!hubCheck?.ok,
|
|
206
|
+
codex: !!codexCheck?.ok,
|
|
207
|
+
gemini: !!geminiCheck?.ok,
|
|
208
|
+
wt: false,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
execSyncFn("psmux --version", {
|
|
213
|
+
stdio: "ignore",
|
|
214
|
+
timeout: 2000,
|
|
215
|
+
windowsHide: true,
|
|
216
|
+
});
|
|
217
|
+
checks.psmux = true;
|
|
218
|
+
} catch {}
|
|
219
|
+
|
|
220
|
+
if (process.platform === "win32") {
|
|
221
|
+
try {
|
|
222
|
+
execSyncFn("where wt.exe", {
|
|
223
|
+
stdio: "ignore",
|
|
224
|
+
timeout: 2000,
|
|
225
|
+
windowsHide: true,
|
|
226
|
+
});
|
|
227
|
+
checks.wt = true;
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let tier = "minimal";
|
|
232
|
+
if (checks.codex || checks.gemini) tier = "standard";
|
|
233
|
+
if (checks.psmux && checks.hub && (checks.codex || checks.gemini)) tier = "full";
|
|
234
|
+
|
|
235
|
+
const agents = ["claude"];
|
|
236
|
+
if (checks.codex) agents.push("codex");
|
|
237
|
+
if (checks.gemini) agents.push("gemini");
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
probed_at: new Date(options.now ?? Date.now()).toISOString(),
|
|
241
|
+
tier,
|
|
242
|
+
checks,
|
|
243
|
+
available_agents: agents,
|
|
244
|
+
codex_plan: codexPlan,
|
|
245
|
+
source: {
|
|
246
|
+
preflight: !!preflight,
|
|
247
|
+
home_dir: homeDir,
|
|
248
|
+
hub_state: hubCheck?.state || "unknown",
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function extractProjectMeta(options = {}) {
|
|
254
|
+
const cwd = options.cwd || process.cwd();
|
|
255
|
+
const execSyncFn = options.execSyncFn || execSync;
|
|
256
|
+
|
|
257
|
+
let name = basename(cwd);
|
|
258
|
+
let description = "";
|
|
259
|
+
let lang = "unknown";
|
|
260
|
+
let testCmd = null;
|
|
261
|
+
let isGit = false;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const toplevel = execSyncFn("git rev-parse --show-toplevel", {
|
|
265
|
+
encoding: "utf8",
|
|
266
|
+
timeout: 3000,
|
|
267
|
+
windowsHide: true,
|
|
268
|
+
cwd,
|
|
269
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
270
|
+
}).trim();
|
|
271
|
+
name = basename(toplevel);
|
|
272
|
+
isGit = true;
|
|
273
|
+
} catch {}
|
|
274
|
+
|
|
275
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
276
|
+
if (existsSync(packageJsonPath)) {
|
|
277
|
+
try {
|
|
278
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
279
|
+
description = pkg.description || "";
|
|
280
|
+
testCmd = pkg.scripts?.test || null;
|
|
281
|
+
lang = "JavaScript/ESM (Node.js)";
|
|
282
|
+
} catch {}
|
|
283
|
+
} else if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py"))) {
|
|
284
|
+
lang = "Python";
|
|
285
|
+
} else if (existsSync(join(cwd, "Cargo.toml"))) {
|
|
286
|
+
lang = "Rust";
|
|
287
|
+
} else if (existsSync(join(cwd, "go.mod"))) {
|
|
288
|
+
lang = "Go";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
extracted_at: new Date(options.now ?? Date.now()).toISOString(),
|
|
293
|
+
name,
|
|
294
|
+
description: description.slice(0, 300),
|
|
295
|
+
lang,
|
|
296
|
+
test_cmd: testCmd,
|
|
297
|
+
is_git: isGit,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function loadMcpInventory(options = {}) {
|
|
302
|
+
const inventoryPath = join(resolveHomeDir(options.homeDir), ".claude", "cache", "mcp-inventory.json");
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(readFileSync(inventoryPath, "utf8"));
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function loadMcpConfigs(options = {}) {
|
|
311
|
+
const cwd = options.cwd || process.cwd();
|
|
312
|
+
const homeDir = resolveHomeDir(options.homeDir);
|
|
313
|
+
const configPaths = [
|
|
314
|
+
join(homeDir, ".claude", "settings.json"),
|
|
315
|
+
join(homeDir, ".claude", "settings.local.json"),
|
|
316
|
+
join(cwd, ".claude", "mcp.json"),
|
|
317
|
+
join(cwd, ".mcp.json"),
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
const servers = {};
|
|
321
|
+
|
|
322
|
+
for (const configPath of configPaths) {
|
|
323
|
+
try {
|
|
324
|
+
const data = JSON.parse(readFileSync(configPath, "utf8"));
|
|
325
|
+
const mcpServers = data.mcpServers || {};
|
|
326
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
327
|
+
servers[name] = {
|
|
328
|
+
configured: true,
|
|
329
|
+
source: basename(configPath),
|
|
330
|
+
...config,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return servers;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function checkSearchEngines(options = {}) {
|
|
340
|
+
const inventory = options.inventory ?? loadMcpInventory(options);
|
|
341
|
+
const configuredServers = options.configuredServers ?? loadMcpConfigs(options);
|
|
342
|
+
const engines = [];
|
|
343
|
+
|
|
344
|
+
const knownSearchServers = [...SEARCH_SERVER_ORDER, "context7"];
|
|
345
|
+
const allServerNames = new Set(knownSearchServers);
|
|
346
|
+
|
|
347
|
+
if (inventory) {
|
|
348
|
+
for (const scope of ["codex", "gemini", "claude"]) {
|
|
349
|
+
const scopeData = inventory[scope];
|
|
350
|
+
if (!scopeData?.servers) continue;
|
|
351
|
+
for (const server of scopeData.servers) {
|
|
352
|
+
const tags = server.domain_tags || MCP_SERVER_DOMAIN_TAGS[server.name] || [];
|
|
353
|
+
if (tags.includes("search") || tags.includes("web") || tags.includes("research")) {
|
|
354
|
+
allServerNames.add(server.name);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const name of Object.keys(configuredServers)) {
|
|
361
|
+
const tags = MCP_SERVER_DOMAIN_TAGS[name] || [];
|
|
362
|
+
if (tags.includes("search") || tags.includes("web") || knownSearchServers.includes(name)) {
|
|
363
|
+
allServerNames.add(name);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const name of allServerNames) {
|
|
368
|
+
const configured = !!configuredServers[name];
|
|
369
|
+
const tags = MCP_SERVER_DOMAIN_TAGS[name] || [];
|
|
370
|
+
|
|
371
|
+
let inventoryStatus = null;
|
|
372
|
+
if (inventory) {
|
|
373
|
+
for (const scope of ["codex", "gemini", "claude"]) {
|
|
374
|
+
const server = inventory[scope]?.servers?.find((item) => item.name === name);
|
|
375
|
+
if (server) {
|
|
376
|
+
inventoryStatus = {
|
|
377
|
+
scope,
|
|
378
|
+
status: server.status,
|
|
379
|
+
tool_count: server.tool_count,
|
|
380
|
+
};
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let status = "unavailable";
|
|
387
|
+
if (inventoryStatus?.status === "enabled" || inventoryStatus?.status === "configured") {
|
|
388
|
+
status = "available";
|
|
389
|
+
} else if (configured) {
|
|
390
|
+
status = "configured";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
engines.push({
|
|
394
|
+
name,
|
|
395
|
+
status,
|
|
396
|
+
domain_tags: tags,
|
|
397
|
+
configured,
|
|
398
|
+
inventory: inventoryStatus,
|
|
399
|
+
source: configuredServers[name]?.source || null,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
engines.sort((left, right) => {
|
|
404
|
+
const leftIndex = SEARCH_SERVER_ORDER.indexOf(left.name);
|
|
405
|
+
const rightIndex = SEARCH_SERVER_ORDER.indexOf(right.name);
|
|
406
|
+
if (leftIndex !== -1 && rightIndex !== -1) return leftIndex - rightIndex;
|
|
407
|
+
if (leftIndex !== -1) return -1;
|
|
408
|
+
if (rightIndex !== -1) return 1;
|
|
409
|
+
return left.name.localeCompare(right.name);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const available = engines.filter((engine) => engine.status === "available");
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
checked_at: new Date(options.now ?? Date.now()).toISOString(),
|
|
416
|
+
primary_engine: available[0]?.name || null,
|
|
417
|
+
available_count: available.length,
|
|
418
|
+
total_count: engines.length,
|
|
419
|
+
engines,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function buildTarget(target, options = {}) {
|
|
424
|
+
const filePath = resolveTargetPath(target, options);
|
|
425
|
+
if (!options.force && isFresh(target, options)) {
|
|
426
|
+
return { target, status: "skipped", file: filePath, reason: "fresh" };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let payload;
|
|
430
|
+
if (target === "codexSkills") payload = scanCodexSkills(options);
|
|
431
|
+
else if (target === "tierEnvironment") payload = probeTierEnvironment(options);
|
|
432
|
+
else if (target === "projectMeta") payload = extractProjectMeta(options);
|
|
433
|
+
else if (target === "searchEngines") payload = checkSearchEngines(options);
|
|
434
|
+
else throw new Error(`unknown cache target: ${target}`);
|
|
435
|
+
|
|
436
|
+
writeJSON(filePath, payload);
|
|
437
|
+
return { target, status: "built", file: filePath, payload };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function buildCodexSkills(options = {}) {
|
|
441
|
+
return buildTarget("codexSkills", options);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function buildTierEnvironment(options = {}) {
|
|
445
|
+
return buildTarget("tierEnvironment", options);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function buildProjectMeta(options = {}) {
|
|
449
|
+
return buildTarget("projectMeta", options);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function buildSearchEngines(options = {}) {
|
|
453
|
+
return buildTarget("searchEngines", options);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function buildAll(options = {}) {
|
|
457
|
+
const targets = normalizeTargets(options.targets);
|
|
458
|
+
const dirs = resolveRootDirs(options.cwd);
|
|
459
|
+
ensureDir(dirs.cacheDir);
|
|
460
|
+
ensureDir(dirs.stateDir);
|
|
461
|
+
|
|
462
|
+
const results = [];
|
|
463
|
+
for (const target of targets) {
|
|
464
|
+
try {
|
|
465
|
+
results.push(buildTarget(target, options));
|
|
466
|
+
} catch (error) {
|
|
467
|
+
results.push({
|
|
468
|
+
target,
|
|
469
|
+
status: "failed",
|
|
470
|
+
file: resolveTargetPath(target, options),
|
|
471
|
+
error: error.message,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const built = results.filter((result) => result.status === "built").length;
|
|
477
|
+
const skipped = results.filter((result) => result.status === "skipped").length;
|
|
478
|
+
const failed = results.filter((result) => result.status === "failed").length;
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
ok: failed === 0,
|
|
482
|
+
built,
|
|
483
|
+
skipped,
|
|
484
|
+
failed,
|
|
485
|
+
results,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function formatBuildSummary(summary, { label = "cache-warmup" } = {}) {
|
|
490
|
+
const details = summary.results
|
|
491
|
+
.filter((result) => result.status !== "failed")
|
|
492
|
+
.map((result) => `${result.target}:${result.status}`);
|
|
493
|
+
|
|
494
|
+
if (summary.failed > 0) {
|
|
495
|
+
const errors = summary.results
|
|
496
|
+
.filter((result) => result.status === "failed")
|
|
497
|
+
.map((result) => `${result.target}:failed`);
|
|
498
|
+
return `${label}: partial (${[...details, ...errors].join(", ")})`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return `${label}: ok (${details.join(", ")})`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export async function runCli(options = {}) {
|
|
505
|
+
const force = options.force ?? process.argv.includes("--force");
|
|
506
|
+
const summary = buildAll({ ...options, force });
|
|
507
|
+
console.log(formatBuildSummary(summary));
|
|
508
|
+
if (!summary.ok) process.exitCode = 1;
|
|
509
|
+
return summary;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
513
|
+
await runCli();
|
|
514
|
+
}
|