Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 130 additions & 23 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,27 @@ async function run(argv) {
if (cmd === "init") {
const { init } = await import("./init.js");
const noSettings = argv.includes("--no-settings");
const { report, bytes, settings } = init({ targetRoot: process.cwd(), noSettings });
const { report, bytes, settings, detected } = init({ targetRoot: process.cwd(), noSettings });
const wrote = report.filter((r) => r.action === "written").map((r) => r.target);
console.log(`${BRAND.brand} init — this repo now speaks every AI tool from one source.\n`);
console.log(` emitted: ${wrote.length ? wrote.join(", ") : "(all up to date)"}`);
console.log(
` source: AGENTS.md (${bytes} B) — edit rules in source/, re-run \`${BRAND.cli} sync\``,
);
if (settings?.action === "merged") {
console.log(
` settings: merged ${settings.added.join(", ")} into ${settings.path}`,
);
} else if (settings?.action === "unchanged") {
if (settings?.action === "merged" && "added" in settings) {
console.log(` settings: merged ${settings.added.join(", ")} into ${settings.path}`);
} else if (settings?.action === "unchanged" && "path" in settings) {
console.log(` settings: already up to date (${settings.path})`);
} else if (settings?.action === "skipped") {
console.log(" settings: skipped (--no-settings)");
}
if (detected) {
console.log(` provider: auto-detected ${detected.name} from ${detected.source}`);
} else {
console.log(
` provider: none detected — set ANTHROPIC_API_KEY, OPENROUTER_API_KEY, or LITELLM_BASE_URL`,
);
}
console.log(` active: tools · crew · guards → \`${BRAND.cli} catalog\``);
console.log(` verify: \`${BRAND.cli} doctor\``);
return;
Expand Down Expand Up @@ -588,7 +593,7 @@ async function run(argv) {
}
const r = scan(target);
console.log(`${BRAND.brand} scan — skill-gate (${r.scanner})\n`);
if (r.findings && r.findings.length) {
if (r.findings?.length) {
for (const f of r.findings) console.log(` [${f.sev}] ${f.msg}`);
} else if (r.raw) {
console.log(" " + r.raw.trim().split("\n").slice(-6).join("\n "));
Expand Down Expand Up @@ -692,9 +697,14 @@ async function run(argv) {
const { estimateSpendFromLogs } = await import("./cost_report.js");
const est = estimateSpendFromLogs();
if (est && est.totalCost > 0) {
console.log(` $${est.totalCost.toFixed(2)} estimated from Claude session logs (${est.sessions} session(s))`);
console.log(
` $${est.totalCost.toFixed(2)} estimated from Claude session logs (${est.sessions} session(s))`,
);
if (est.byModel.length) {
for (const m of est.byModel) console.log(` ${m.model.padEnd(30)} $${m.cost.toFixed(4)} (${m.inTokens} in / ${m.outTokens} out)`);
for (const m of est.byModel)
console.log(
` ${m.model.padEnd(30)} $${m.cost.toFixed(4)} (${m.inTokens} in / ${m.outTokens} out)`,
);
}
console.log("\n install ccusage for precise tracking: npm i -g ccusage");
} else {
Expand Down Expand Up @@ -861,11 +871,15 @@ async function run(argv) {
if (sub === "show") {
const prov = activeProvider(process.cwd());
const config = loadProviders(process.cwd());
if (json) return console.log(JSON.stringify({ active: config.active, provider: prov }, null, 2));
if (json)
return console.log(JSON.stringify({ active: config.active, provider: prov }, null, 2));
console.log(`${BRAND.brand} config\n`);
console.log(` provider: ${prov.name} (${prov.label || prov.name})`);
if (prov._autoDetected) console.log(` detected: auto (from ${prov._source})`);
console.log(` base URL: ${prov.baseUrl}`);
console.log(` env key: ${prov.envKey || "(none)"}${prov.envKey ? (process.env[prov.envKey] ? " ✓ set" : " ✗ not set") : ""}`);
console.log(
` env key: ${prov.envKey || "(none)"}${prov.envKey ? (process.env[prov.envKey] ? " ✓ set" : " ✗ not set") : ""}`,
);
console.log(` models:`);
for (const [tier, id] of Object.entries(prov.models || {}))
console.log(` ${tier.padEnd(8)} ${id}`);
Expand All @@ -885,23 +899,36 @@ async function run(argv) {
if (sub === "provider") {
const name = argv[2];
if (!name) {
console.error(`usage: ${BRAND.cli} config provider <name> | ${BRAND.cli} config provider add <name> --base-url <url> [--key-env <VAR>]`);
console.error(
`usage: ${BRAND.cli} config provider <name> | ${BRAND.cli} config provider add <name> --base-url <url> [--key-env <VAR>]`,
);
process.exitCode = 1;
return;
}
if (name === "add") {
const addName = argv[3];
const flagVal = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
const flagVal = (f) => {
const i = argv.indexOf(f);
return i >= 0 ? argv[i + 1] : undefined;
};
const baseUrl = flagVal("--base-url");
const envKey = flagVal("--key-env");
const label = flagVal("--label");
const r = addProvider(process.cwd(), addName, { baseUrl, envKey, label });
if (!r.ok) { console.error(` ${r.reason}`); process.exitCode = 1; return; }
if (!r.ok) {
console.error(` ${r.reason}`);
process.exitCode = 1;
return;
}
console.log(` added provider "${addName}" → ${r.provider.baseUrl}`);
return;
}
const r = setProvider(process.cwd(), name);
if (!r.ok) { console.error(` ${r.reason}`); process.exitCode = 1; return; }
if (!r.ok) {
console.error(` ${r.reason}`);
process.exitCode = 1;
return;
}
console.log(` switched to provider "${name}" (${r.provider.label || name})`);
return;
}
Expand All @@ -913,20 +940,91 @@ async function run(argv) {
return;
}
const r = applyRoute(tier);
if (!r.ok) { console.error(` ${r.reason}`); process.exitCode = 1; return; }
if (!r.ok) {
console.error(` ${r.reason}`);
process.exitCode = 1;
return;
}
console.log(` model set to ${r.model} (${r.modelId})${r.prev ? ` — was: ${r.prev}` : ""}`);
console.log(` written to ${r.path}`);
return;
}
console.error(`config: unknown subcommand "${sub}" (show | providers | provider <name> | model <tier>)`);
if (sub === "gateway") {
const { emitGatewayConfig } = await import("./route.js");
const result = emitGatewayConfig(process.cwd());
if (typeof result === "object" && !result.ok) {
console.log(` ${result.reason}`);
return;
}
console.log(` wrote ${result}`);
console.log(`\n setup LiteLLM gateway:`);
console.log(` 1. pip install "litellm[proxy]" # pin an exact verified version`);
console.log(` 2. litellm --config litellm.config.yaml`);
console.log(` 3. export ANTHROPIC_BASE_URL=http://localhost:4000`);
console.log(`\n then switch to the gateway provider:`);
console.log(` ${BRAND.cli} config provider litellm`);
console.log(`\n routing flows through: forge route → tier alias → LiteLLM → model`);
return;
}
if (sub === "setup") {
const { providerStatus, listDetectedProviders } = await import("./providers.js");
const prov = activeProvider(process.cwd());
const status = providerStatus(process.cwd());
const detected = listDetectedProviders();
console.log(`${BRAND.brand} config setup\n`);
console.log(` active provider: ${prov.name} (${prov.label || prov.name})`);
if (prov._autoDetected) console.log(` source: auto-detected from ${prov._source}`);
console.log();
for (const c of status.checks) {
console.log(` ${c.ok ? "✓" : "✗"} ${c.detail}`);
}
console.log(`\n environment:`);
for (const e of status.envScan) {
console.log(` ${e.set ? "✓" : "·"} ${e.key}${e.set ? " (set)" : ""}`);
}
if (detected.length) {
console.log(`\n available providers (auto-detected):`);
for (const d of detected) {
console.log(` ${d.name.padEnd(18)} ${d.label.padEnd(24)} via ${d.source}`);
}
}
console.log(`\n Anthropic Console API key:`);
console.log(` 1. Go to console.anthropic.com/settings/keys`);
console.log(` 2. Create a key, then: export ANTHROPIC_API_KEY=sk-ant-...`);
console.log(`\n OpenRouter API key:`);
console.log(` 1. Go to openrouter.ai/keys`);
console.log(` 2. Create a key, then: export OPENROUTER_API_KEY=sk-or-...`);
console.log(`\n LiteLLM hosted gateway (no admin access needed):`);
console.log(` export LITELLM_BASE_URL=https://your-gateway.example.com`);
console.log(` export LITELLM_API_KEY=sk-... # or uses ANTHROPIC_API_KEY`);
console.log(`\n LiteLLM self-hosted gateway:`);
console.log(` ${BRAND.cli} config gateway # emit litellm.config.yaml`);
console.log(` ${BRAND.cli} config provider litellm`);
console.log(`\n quick start:`);
console.log(` ${BRAND.cli} config provider anthropic # direct Anthropic API`);
console.log(` ${BRAND.cli} config provider openrouter # OpenRouter multi-model`);
console.log(` ${BRAND.cli} config provider litellm # LiteLLM self-hosted`);
console.log(` ${BRAND.cli} config model sonnet # set default model tier`);
console.log(` ${BRAND.cli} route "<task>" --apply # route + apply model`);
return;
}
console.error(
`config: unknown subcommand "${sub}" (show | providers | provider <name> | model <tier> | gateway | setup)`,
);
process.exitCode = 1;
return;
}
if (cmd === "route") {
const r = await import("./route.js");
if (argv[1] === "gateway") {
const path = r.emitGatewayConfig(process.cwd());
console.log(` wrote ${path} — LiteLLM tiers: forge-simple / forge-medium / forge-complex.`);
const result = r.emitGatewayConfig(process.cwd());
if (typeof result === "object" && !result.ok) {
console.log(` ${result.reason}`);
return;
}
console.log(
` wrote ${result} — LiteLLM tiers: forge-simple / forge-medium / forge-complex.`,
);
console.log(" next: pin+install litellm, run it, point ANTHROPIC_BASE_URL at it, then");
console.log(
" REQUEST the tier `forge route` recommends (a plain claude-* request passes through).",
Expand All @@ -943,14 +1041,20 @@ async function run(argv) {
.filter((a, i) => !FLAGS.has(a) && a !== "--provider" && argv[i] !== "--provider")
.join(" ");
if (!task) {
console.error('usage: forge route "<task>" [--apply] [--provider <name>] [--json] | forge route gateway');
console.error(
'usage: forge route "<task>" [--apply] [--provider <name>] [--json] | forge route gateway',
);
process.exitCode = 1;
return;
}
if (providerName) {
const { setProvider } = await import("./providers.js");
const sr = setProvider(process.cwd(), providerName);
if (!sr.ok) { console.error(` ${sr.reason}`); process.exitCode = 1; return; }
if (!sr.ok) {
console.error(` ${sr.reason}`);
process.exitCode = 1;
return;
}
}
const rec = r.routeTask(process.cwd(), task);
r.meterRoute(process.cwd(), task, rec);
Expand All @@ -973,13 +1077,16 @@ async function run(argv) {
const { applyRoute } = await import("./providers.js");
const ar = applyRoute(rec.key);
if (ar.ok) {
if (!json) console.log(`\n applied: model set to ${ar.model} (${ar.modelId}) in ${ar.path}`);
if (!json)
console.log(`\n applied: model set to ${ar.model} (${ar.modelId}) in ${ar.path}`);
} else {
if (!json) console.error(`\n apply failed: ${ar.reason}`);
process.exitCode = 1;
}
} else if (!json) {
console.log(`\n advisory · apply: \`${BRAND.cli} route "<task>" --apply\` · gateway: \`${BRAND.cli} route gateway\``);
console.log(
`\n advisory · apply: \`${BRAND.cli} route "<task>" --apply\` · gateway: \`${BRAND.cli} route gateway\``,
);
}
return;
}
Expand Down
79 changes: 63 additions & 16 deletions src/cortex_mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import { argv } from "node:process";
import { createInterface } from "node:readline";
import { fileURLToPath } from "node:url";
import { lessonsForContext, summary } from "./cortex.js";
import { report as costReport } from "./cost_report.js";
import { dashData } from "./dash.js";
import { diagnose } from "./diagnose.js";
import { doctor } from "./doctor.js";
import { assessTask, clarifyBlock, preflightRepo } from "./preflight.js";
import { routeTask } from "./route.js";
import { decompose } from "./scope.js";
import { predictImpact, substrateCheck } from "./substrate.js";
import { epochDay } from "./util.js";
import { report as costReport } from "./cost_report.js";
import { dashData } from "./dash.js";
import { diagnose } from "./diagnose.js";
import { doctor } from "./doctor.js";

const PKG_VERSION = (() => {
try {
Expand Down Expand Up @@ -132,8 +132,7 @@ const TOOLS = [
},
{
name: "forge_brain",
description:
"Project memory index — list all remembered facts stored in .forge/brain/.",
description: "Project memory index — list all remembered facts stored in .forge/brain/.",
inputSchema: { type: "object", properties: {} },
},
{
Expand All @@ -142,7 +141,9 @@ const TOOLS = [
"Query the proof-carrying memory ledger with a natural language query. Returns ranked matching claims.",
inputSchema: {
type: "object",
properties: { query: { type: "string", description: "what you are about to do or looking for" } },
properties: {
query: { type: "string", description: "what you are about to do or looking for" },
},
required: ["query"],
},
},
Expand All @@ -166,6 +167,12 @@ const TOOLS = [
"Health check — verify installed tools, guards, MCP auth, config drift, and system state.",
inputSchema: { type: "object", properties: {} },
},
{
name: "forge_provider_status",
description:
"Provider detection — which API provider is active (auto-detected or configured), env vars set, and health checks.",
inputSchema: { type: "object", properties: {} },
},
];

async function callTool(name, args = {}) {
Expand Down Expand Up @@ -212,7 +219,9 @@ async function callTool(name, args = {}) {
const idx = buildIndex(store);
const items = list(store);
return JSON.stringify({ items, indexed: idx.indexed, overflow: idx.overflow }, null, 2);
} catch { return "No brain store found — run `forge remember` to start."; }
} catch {
return "No brain store found — run `forge remember` to start.";
}
}
if (name === "forge_ledger_query") {
try {
Expand All @@ -224,11 +233,22 @@ async function callTool(name, args = {}) {
const claims = loadClaims(dir);
const sim = claimSim(root, q, claims, claimText);
const ranked = retrieve(q, claims, { nowDay: today(), budget: 8, sim });
return JSON.stringify({
sim: simLabel(sim),
results: ranked.map((r) => ({ id: r.claim.id, kind: r.claim.kind, score: r.score, text: claimText(r.claim).slice(0, 200) })),
}, null, 2);
} catch { return "No ledger claims found."; }
return JSON.stringify(
{
sim: simLabel(sim),
results: ranked.map((r) => ({
id: r.claim.id,
kind: r.claim.kind,
score: r.score,
text: claimText(r.claim).slice(0, 200),
})),
},
null,
2,
);
} catch {
return "No ledger claims found.";
}
}
if (name === "forge_diagnose") {
const r = diagnose(root, {
Expand All @@ -242,6 +262,31 @@ async function callTool(name, args = {}) {
const { results, failed } = doctor({ targetRoot: root });
return JSON.stringify({ results, failed }, null, 2);
}
if (name === "forge_provider_status") {
const { activeProvider, providerStatus, listDetectedProviders } = await import(
"./providers.js"
);
const prov = activeProvider(root);
const status = providerStatus(root);
const detected = listDetectedProviders();
return JSON.stringify(
{
active: {
name: prov.name,
type: prov.type,
label: prov.label || prov.name,
baseUrl: prov.baseUrl,
autoDetected: Boolean(prov._autoDetected),
source: prov._source || null,
},
checks: status.checks,
availableProviders: detected,
envScan: status.envScan,
},
null,
2,
);
}
return null;
}

Expand Down Expand Up @@ -294,9 +339,11 @@ export function serve(input = process.stdin, output = process.stdout) {
} catch {
return; // ignore malformed frames
}
handle(msg).then((res) => {
if (res) output.write(`${JSON.stringify(res)}\n`);
}).catch(() => {});
handle(msg)
.then((res) => {
if (res) output.write(`${JSON.stringify(res)}\n`);
})
.catch(() => {});
});
}

Expand Down
Loading
Loading