Skip to content

Commit cb01420

Browse files
Merge pull request #42 from CodeWithJuber/claude/forge-work-system-setup-p26ka5
feat: zero-config provider auto-detection + hosted LiteLLM gateway
2 parents 08ab613 + 1243321 commit cb01420

10 files changed

Lines changed: 685 additions & 82 deletions

File tree

‎src/cli.js‎

Lines changed: 130 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,22 +69,27 @@ async function run(argv) {
6969
if (cmd === "init") {
7070
const { init } = await import("./init.js");
7171
const noSettings = argv.includes("--no-settings");
72-
const { report, bytes, settings } = init({ targetRoot: process.cwd(), noSettings });
72+
const { report, bytes, settings, detected } = init({ targetRoot: process.cwd(), noSettings });
7373
const wrote = report.filter((r) => r.action === "written").map((r) => r.target);
7474
console.log(`${BRAND.brand} init — this repo now speaks every AI tool from one source.\n`);
7575
console.log(` emitted: ${wrote.length ? wrote.join(", ") : "(all up to date)"}`);
7676
console.log(
7777
` source: AGENTS.md (${bytes} B) — edit rules in source/, re-run \`${BRAND.cli} sync\``,
7878
);
79-
if (settings?.action === "merged") {
80-
console.log(
81-
` settings: merged ${settings.added.join(", ")} into ${settings.path}`,
82-
);
83-
} else if (settings?.action === "unchanged") {
79+
if (settings?.action === "merged" && "added" in settings) {
80+
console.log(` settings: merged ${settings.added.join(", ")} into ${settings.path}`);
81+
} else if (settings?.action === "unchanged" && "path" in settings) {
8482
console.log(` settings: already up to date (${settings.path})`);
8583
} else if (settings?.action === "skipped") {
8684
console.log(" settings: skipped (--no-settings)");
8785
}
86+
if (detected) {
87+
console.log(` provider: auto-detected ${detected.name} from ${detected.source}`);
88+
} else {
89+
console.log(
90+
` provider: none detected — set ANTHROPIC_API_KEY, OPENROUTER_API_KEY, or LITELLM_BASE_URL`,
91+
);
92+
}
8893
console.log(` active: tools · crew · guards → \`${BRAND.cli} catalog\``);
8994
console.log(` verify: \`${BRAND.cli} doctor\``);
9095
return;
@@ -588,7 +593,7 @@ async function run(argv) {
588593
}
589594
const r = scan(target);
590595
console.log(`${BRAND.brand} scan — skill-gate (${r.scanner})\n`);
591-
if (r.findings && r.findings.length) {
596+
if (r.findings?.length) {
592597
for (const f of r.findings) console.log(` [${f.sev}] ${f.msg}`);
593598
} else if (r.raw) {
594599
console.log(" " + r.raw.trim().split("\n").slice(-6).join("\n "));
@@ -692,9 +697,14 @@ async function run(argv) {
692697
const { estimateSpendFromLogs } = await import("./cost_report.js");
693698
const est = estimateSpendFromLogs();
694699
if (est && est.totalCost > 0) {
695-
console.log(` $${est.totalCost.toFixed(2)} estimated from Claude session logs (${est.sessions} session(s))`);
700+
console.log(
701+
` $${est.totalCost.toFixed(2)} estimated from Claude session logs (${est.sessions} session(s))`,
702+
);
696703
if (est.byModel.length) {
697-
for (const m of est.byModel) console.log(` ${m.model.padEnd(30)} $${m.cost.toFixed(4)} (${m.inTokens} in / ${m.outTokens} out)`);
704+
for (const m of est.byModel)
705+
console.log(
706+
` ${m.model.padEnd(30)} $${m.cost.toFixed(4)} (${m.inTokens} in / ${m.outTokens} out)`,
707+
);
698708
}
699709
console.log("\n install ccusage for precise tracking: npm i -g ccusage");
700710
} else {
@@ -861,11 +871,15 @@ async function run(argv) {
861871
if (sub === "show") {
862872
const prov = activeProvider(process.cwd());
863873
const config = loadProviders(process.cwd());
864-
if (json) return console.log(JSON.stringify({ active: config.active, provider: prov }, null, 2));
874+
if (json)
875+
return console.log(JSON.stringify({ active: config.active, provider: prov }, null, 2));
865876
console.log(`${BRAND.brand} config\n`);
866877
console.log(` provider: ${prov.name} (${prov.label || prov.name})`);
878+
if (prov._autoDetected) console.log(` detected: auto (from ${prov._source})`);
867879
console.log(` base URL: ${prov.baseUrl}`);
868-
console.log(` env key: ${prov.envKey || "(none)"}${prov.envKey ? (process.env[prov.envKey] ? " ✓ set" : " ✗ not set") : ""}`);
880+
console.log(
881+
` env key: ${prov.envKey || "(none)"}${prov.envKey ? (process.env[prov.envKey] ? " ✓ set" : " ✗ not set") : ""}`,
882+
);
869883
console.log(` models:`);
870884
for (const [tier, id] of Object.entries(prov.models || {}))
871885
console.log(` ${tier.padEnd(8)} ${id}`);
@@ -885,23 +899,36 @@ async function run(argv) {
885899
if (sub === "provider") {
886900
const name = argv[2];
887901
if (!name) {
888-
console.error(`usage: ${BRAND.cli} config provider <name> | ${BRAND.cli} config provider add <name> --base-url <url> [--key-env <VAR>]`);
902+
console.error(
903+
`usage: ${BRAND.cli} config provider <name> | ${BRAND.cli} config provider add <name> --base-url <url> [--key-env <VAR>]`,
904+
);
889905
process.exitCode = 1;
890906
return;
891907
}
892908
if (name === "add") {
893909
const addName = argv[3];
894-
const flagVal = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
910+
const flagVal = (f) => {
911+
const i = argv.indexOf(f);
912+
return i >= 0 ? argv[i + 1] : undefined;
913+
};
895914
const baseUrl = flagVal("--base-url");
896915
const envKey = flagVal("--key-env");
897916
const label = flagVal("--label");
898917
const r = addProvider(process.cwd(), addName, { baseUrl, envKey, label });
899-
if (!r.ok) { console.error(` ${r.reason}`); process.exitCode = 1; return; }
918+
if (!r.ok) {
919+
console.error(` ${r.reason}`);
920+
process.exitCode = 1;
921+
return;
922+
}
900923
console.log(` added provider "${addName}" → ${r.provider.baseUrl}`);
901924
return;
902925
}
903926
const r = setProvider(process.cwd(), name);
904-
if (!r.ok) { console.error(` ${r.reason}`); process.exitCode = 1; return; }
927+
if (!r.ok) {
928+
console.error(` ${r.reason}`);
929+
process.exitCode = 1;
930+
return;
931+
}
905932
console.log(` switched to provider "${name}" (${r.provider.label || name})`);
906933
return;
907934
}
@@ -913,20 +940,91 @@ async function run(argv) {
913940
return;
914941
}
915942
const r = applyRoute(tier);
916-
if (!r.ok) { console.error(` ${r.reason}`); process.exitCode = 1; return; }
943+
if (!r.ok) {
944+
console.error(` ${r.reason}`);
945+
process.exitCode = 1;
946+
return;
947+
}
917948
console.log(` model set to ${r.model} (${r.modelId})${r.prev ? ` — was: ${r.prev}` : ""}`);
918949
console.log(` written to ${r.path}`);
919950
return;
920951
}
921-
console.error(`config: unknown subcommand "${sub}" (show | providers | provider <name> | model <tier>)`);
952+
if (sub === "gateway") {
953+
const { emitGatewayConfig } = await import("./route.js");
954+
const result = emitGatewayConfig(process.cwd());
955+
if (typeof result === "object" && !result.ok) {
956+
console.log(` ${result.reason}`);
957+
return;
958+
}
959+
console.log(` wrote ${result}`);
960+
console.log(`\n setup LiteLLM gateway:`);
961+
console.log(` 1. pip install "litellm[proxy]" # pin an exact verified version`);
962+
console.log(` 2. litellm --config litellm.config.yaml`);
963+
console.log(` 3. export ANTHROPIC_BASE_URL=http://localhost:4000`);
964+
console.log(`\n then switch to the gateway provider:`);
965+
console.log(` ${BRAND.cli} config provider litellm`);
966+
console.log(`\n routing flows through: forge route → tier alias → LiteLLM → model`);
967+
return;
968+
}
969+
if (sub === "setup") {
970+
const { providerStatus, listDetectedProviders } = await import("./providers.js");
971+
const prov = activeProvider(process.cwd());
972+
const status = providerStatus(process.cwd());
973+
const detected = listDetectedProviders();
974+
console.log(`${BRAND.brand} config setup\n`);
975+
console.log(` active provider: ${prov.name} (${prov.label || prov.name})`);
976+
if (prov._autoDetected) console.log(` source: auto-detected from ${prov._source}`);
977+
console.log();
978+
for (const c of status.checks) {
979+
console.log(` ${c.ok ? "✓" : "✗"} ${c.detail}`);
980+
}
981+
console.log(`\n environment:`);
982+
for (const e of status.envScan) {
983+
console.log(` ${e.set ? "✓" : "·"} ${e.key}${e.set ? " (set)" : ""}`);
984+
}
985+
if (detected.length) {
986+
console.log(`\n available providers (auto-detected):`);
987+
for (const d of detected) {
988+
console.log(` ${d.name.padEnd(18)} ${d.label.padEnd(24)} via ${d.source}`);
989+
}
990+
}
991+
console.log(`\n Anthropic Console API key:`);
992+
console.log(` 1. Go to console.anthropic.com/settings/keys`);
993+
console.log(` 2. Create a key, then: export ANTHROPIC_API_KEY=sk-ant-...`);
994+
console.log(`\n OpenRouter API key:`);
995+
console.log(` 1. Go to openrouter.ai/keys`);
996+
console.log(` 2. Create a key, then: export OPENROUTER_API_KEY=sk-or-...`);
997+
console.log(`\n LiteLLM hosted gateway (no admin access needed):`);
998+
console.log(` export LITELLM_BASE_URL=https://your-gateway.example.com`);
999+
console.log(` export LITELLM_API_KEY=sk-... # or uses ANTHROPIC_API_KEY`);
1000+
console.log(`\n LiteLLM self-hosted gateway:`);
1001+
console.log(` ${BRAND.cli} config gateway # emit litellm.config.yaml`);
1002+
console.log(` ${BRAND.cli} config provider litellm`);
1003+
console.log(`\n quick start:`);
1004+
console.log(` ${BRAND.cli} config provider anthropic # direct Anthropic API`);
1005+
console.log(` ${BRAND.cli} config provider openrouter # OpenRouter multi-model`);
1006+
console.log(` ${BRAND.cli} config provider litellm # LiteLLM self-hosted`);
1007+
console.log(` ${BRAND.cli} config model sonnet # set default model tier`);
1008+
console.log(` ${BRAND.cli} route "<task>" --apply # route + apply model`);
1009+
return;
1010+
}
1011+
console.error(
1012+
`config: unknown subcommand "${sub}" (show | providers | provider <name> | model <tier> | gateway | setup)`,
1013+
);
9221014
process.exitCode = 1;
9231015
return;
9241016
}
9251017
if (cmd === "route") {
9261018
const r = await import("./route.js");
9271019
if (argv[1] === "gateway") {
928-
const path = r.emitGatewayConfig(process.cwd());
929-
console.log(` wrote ${path} — LiteLLM tiers: forge-simple / forge-medium / forge-complex.`);
1020+
const result = r.emitGatewayConfig(process.cwd());
1021+
if (typeof result === "object" && !result.ok) {
1022+
console.log(` ${result.reason}`);
1023+
return;
1024+
}
1025+
console.log(
1026+
` wrote ${result} — LiteLLM tiers: forge-simple / forge-medium / forge-complex.`,
1027+
);
9301028
console.log(" next: pin+install litellm, run it, point ANTHROPIC_BASE_URL at it, then");
9311029
console.log(
9321030
" REQUEST the tier `forge route` recommends (a plain claude-* request passes through).",
@@ -943,14 +1041,20 @@ async function run(argv) {
9431041
.filter((a, i) => !FLAGS.has(a) && a !== "--provider" && argv[i] !== "--provider")
9441042
.join(" ");
9451043
if (!task) {
946-
console.error('usage: forge route "<task>" [--apply] [--provider <name>] [--json] | forge route gateway');
1044+
console.error(
1045+
'usage: forge route "<task>" [--apply] [--provider <name>] [--json] | forge route gateway',
1046+
);
9471047
process.exitCode = 1;
9481048
return;
9491049
}
9501050
if (providerName) {
9511051
const { setProvider } = await import("./providers.js");
9521052
const sr = setProvider(process.cwd(), providerName);
953-
if (!sr.ok) { console.error(` ${sr.reason}`); process.exitCode = 1; return; }
1053+
if (!sr.ok) {
1054+
console.error(` ${sr.reason}`);
1055+
process.exitCode = 1;
1056+
return;
1057+
}
9541058
}
9551059
const rec = r.routeTask(process.cwd(), task);
9561060
r.meterRoute(process.cwd(), task, rec);
@@ -973,13 +1077,16 @@ async function run(argv) {
9731077
const { applyRoute } = await import("./providers.js");
9741078
const ar = applyRoute(rec.key);
9751079
if (ar.ok) {
976-
if (!json) console.log(`\n applied: model set to ${ar.model} (${ar.modelId}) in ${ar.path}`);
1080+
if (!json)
1081+
console.log(`\n applied: model set to ${ar.model} (${ar.modelId}) in ${ar.path}`);
9771082
} else {
9781083
if (!json) console.error(`\n apply failed: ${ar.reason}`);
9791084
process.exitCode = 1;
9801085
}
9811086
} else if (!json) {
982-
console.log(`\n advisory · apply: \`${BRAND.cli} route "<task>" --apply\` · gateway: \`${BRAND.cli} route gateway\``);
1087+
console.log(
1088+
`\n advisory · apply: \`${BRAND.cli} route "<task>" --apply\` · gateway: \`${BRAND.cli} route gateway\``,
1089+
);
9831090
}
9841091
return;
9851092
}

‎src/cortex_mcp.js‎

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import { argv } from "node:process";
77
import { createInterface } from "node:readline";
88
import { fileURLToPath } from "node:url";
99
import { lessonsForContext, summary } from "./cortex.js";
10+
import { report as costReport } from "./cost_report.js";
11+
import { dashData } from "./dash.js";
12+
import { diagnose } from "./diagnose.js";
13+
import { doctor } from "./doctor.js";
1014
import { assessTask, clarifyBlock, preflightRepo } from "./preflight.js";
1115
import { routeTask } from "./route.js";
1216
import { decompose } from "./scope.js";
1317
import { predictImpact, substrateCheck } from "./substrate.js";
1418
import { epochDay } from "./util.js";
15-
import { report as costReport } from "./cost_report.js";
16-
import { dashData } from "./dash.js";
17-
import { diagnose } from "./diagnose.js";
18-
import { doctor } from "./doctor.js";
1919

2020
const PKG_VERSION = (() => {
2121
try {
@@ -132,8 +132,7 @@ const TOOLS = [
132132
},
133133
{
134134
name: "forge_brain",
135-
description:
136-
"Project memory index — list all remembered facts stored in .forge/brain/.",
135+
description: "Project memory index — list all remembered facts stored in .forge/brain/.",
137136
inputSchema: { type: "object", properties: {} },
138137
},
139138
{
@@ -142,7 +141,9 @@ const TOOLS = [
142141
"Query the proof-carrying memory ledger with a natural language query. Returns ranked matching claims.",
143142
inputSchema: {
144143
type: "object",
145-
properties: { query: { type: "string", description: "what you are about to do or looking for" } },
144+
properties: {
145+
query: { type: "string", description: "what you are about to do or looking for" },
146+
},
146147
required: ["query"],
147148
},
148149
},
@@ -166,6 +167,12 @@ const TOOLS = [
166167
"Health check — verify installed tools, guards, MCP auth, config drift, and system state.",
167168
inputSchema: { type: "object", properties: {} },
168169
},
170+
{
171+
name: "forge_provider_status",
172+
description:
173+
"Provider detection — which API provider is active (auto-detected or configured), env vars set, and health checks.",
174+
inputSchema: { type: "object", properties: {} },
175+
},
169176
];
170177

171178
async function callTool(name, args = {}) {
@@ -212,7 +219,9 @@ async function callTool(name, args = {}) {
212219
const idx = buildIndex(store);
213220
const items = list(store);
214221
return JSON.stringify({ items, indexed: idx.indexed, overflow: idx.overflow }, null, 2);
215-
} catch { return "No brain store found — run `forge remember` to start."; }
222+
} catch {
223+
return "No brain store found — run `forge remember` to start.";
224+
}
216225
}
217226
if (name === "forge_ledger_query") {
218227
try {
@@ -224,11 +233,22 @@ async function callTool(name, args = {}) {
224233
const claims = loadClaims(dir);
225234
const sim = claimSim(root, q, claims, claimText);
226235
const ranked = retrieve(q, claims, { nowDay: today(), budget: 8, sim });
227-
return JSON.stringify({
228-
sim: simLabel(sim),
229-
results: ranked.map((r) => ({ id: r.claim.id, kind: r.claim.kind, score: r.score, text: claimText(r.claim).slice(0, 200) })),
230-
}, null, 2);
231-
} catch { return "No ledger claims found."; }
236+
return JSON.stringify(
237+
{
238+
sim: simLabel(sim),
239+
results: ranked.map((r) => ({
240+
id: r.claim.id,
241+
kind: r.claim.kind,
242+
score: r.score,
243+
text: claimText(r.claim).slice(0, 200),
244+
})),
245+
},
246+
null,
247+
2,
248+
);
249+
} catch {
250+
return "No ledger claims found.";
251+
}
232252
}
233253
if (name === "forge_diagnose") {
234254
const r = diagnose(root, {
@@ -242,6 +262,31 @@ async function callTool(name, args = {}) {
242262
const { results, failed } = doctor({ targetRoot: root });
243263
return JSON.stringify({ results, failed }, null, 2);
244264
}
265+
if (name === "forge_provider_status") {
266+
const { activeProvider, providerStatus, listDetectedProviders } = await import(
267+
"./providers.js"
268+
);
269+
const prov = activeProvider(root);
270+
const status = providerStatus(root);
271+
const detected = listDetectedProviders();
272+
return JSON.stringify(
273+
{
274+
active: {
275+
name: prov.name,
276+
type: prov.type,
277+
label: prov.label || prov.name,
278+
baseUrl: prov.baseUrl,
279+
autoDetected: Boolean(prov._autoDetected),
280+
source: prov._source || null,
281+
},
282+
checks: status.checks,
283+
availableProviders: detected,
284+
envScan: status.envScan,
285+
},
286+
null,
287+
2,
288+
);
289+
}
245290
return null;
246291
}
247292

@@ -294,9 +339,11 @@ export function serve(input = process.stdin, output = process.stdout) {
294339
} catch {
295340
return; // ignore malformed frames
296341
}
297-
handle(msg).then((res) => {
298-
if (res) output.write(`${JSON.stringify(res)}\n`);
299-
}).catch(() => {});
342+
handle(msg)
343+
.then((res) => {
344+
if (res) output.write(`${JSON.stringify(res)}\n`);
345+
})
346+
.catch(() => {});
300347
});
301348
}
302349

0 commit comments

Comments
 (0)