costhawk 1.5.18 → 1.5.20

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/README.md CHANGED
@@ -32,6 +32,16 @@ By default, this creates one **hybrid token** with scopes:
32
32
  - `mcp:write`
33
33
  - `otel:ingest`
34
34
 
35
+ ### Codex Desktop usage tracking (non-CLI path)
36
+
37
+ If you use Codex Desktop and do not want to set up each local CLI tool manually, use the Codex-specific setup path:
38
+
39
+ ```bash
40
+ npm exec --yes costhawk@latest -- setup codex-desktop
41
+ ```
42
+
43
+ This preflights the local runtime and writable Codex config, opens CostHawk browser login, then configures only Codex Desktop usage tracking. It does not write Claude Code, Gemini CLI, Cursor, or OpenCode config.
44
+
35
45
  ### Manual Token Mode (Fallback)
36
46
 
37
47
  ```bash
@@ -167,10 +177,13 @@ COSTHAWK_GIT_SHA=$(git rev-parse --short HEAD) npm run build
167
177
 
168
178
  | Tool | Description |
169
179
  |------|-------------|
180
+ | `costhawk_get_company_brain_onboarding` | Get the workspace Company Brain destination and per-project sharing policies before writing learnings |
170
181
  | `costhawk_get_usage_summary` | Get usage and costs over a time period (by provider/model) |
171
182
  | `costhawk_get_usage_by_tag` | Get usage grouped by attribution fields and custom metadata (`project`, `feature`, `team`, `environment`, `user_id`, etc.) |
172
183
  | `costhawk_detect_anomalies` | Check for cost anomalies and unusual usage patterns |
173
184
 
185
+ `costhawk_get_company_brain_onboarding` is the policy gate for Company Brain writes. Private projects must not submit shared learnings. Project memory projects can keep local/project-scoped memory. Company Brain projects can submit allowed entry types using the configured review mode.
186
+
174
187
  `costhawk_get_usage_by_tag` has two data sources:
175
188
  - Standard attribution fields like `feature`, `project`, `team`, and `environment` can come from wrapped-key or scoped API key attribution.
176
189
  - Arbitrary custom tag keys only appear when you send request metadata through the proxy, such as `costhawk_metadata.feature` or `costhawk_metadata.user_id`.
@@ -271,10 +284,19 @@ The cache read savings are significant - CostHawk tracks all 4 types to give you
271
284
  | `--tools` | Print the registered tool names and exit |
272
285
  | `--self-test` | Print version + tool list + local env checks and exit |
273
286
  | `--what-we-read` | Print local directories and sample files read by the MCP |
287
+ | `brain status` | Show Company Brain sharing policy for the current repo/workspace |
288
+ | `setup codex-desktop` | Configure Codex Desktop usage tracking only |
289
+ | `--setup-codex-desktop` | Alias for `setup codex-desktop` |
274
290
  | `--setup` | Configure Claude Code, Codex CLI, and Gemini CLI MCP settings |
275
291
  | `--codex` / `--no-codex` | Include or skip Codex CLI config at `~/.codex/config.toml` |
276
292
  | `--opencode` / `--no-opencode` | Include or skip OpenCode config |
277
293
 
294
+ Company Brain CLI example:
295
+
296
+ ```bash
297
+ costhawk brain status --org-slug acme --git-repo github.com/acme/app
298
+ ```
299
+
278
300
  ## Privacy & Trust
279
301
 
280
302
  CostHawk’s MCP server is designed to be **local-first** and transparent:
@@ -1,2 +1,2 @@
1
- export declare const BUILD_COMMIT_SHA = "830fd8b";
1
+ export declare const BUILD_COMMIT_SHA = "6547cd3";
2
2
  //# sourceMappingURL=build-info.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated during release builds. Values may be empty in dev.
2
- export const BUILD_COMMIT_SHA = "830fd8b";
2
+ export const BUILD_COMMIT_SHA = "6547cd3";
3
3
  //# sourceMappingURL=build-info.js.map
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { createInterface } from "readline";
6
6
  import { spawn, spawnSync } from "child_process";
7
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { accessSync, chmodSync, constants, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
8
8
  import { dirname, join } from "path";
9
9
  import { homedir, hostname, platform } from "os";
10
10
  // Claude Code local transcript parsing
@@ -94,6 +94,9 @@ function extractApiErrorMessage(errorText) {
94
94
  catch {
95
95
  // Fall through to the raw response body.
96
96
  }
97
+ if (/^<!doctype html/i.test(trimmed) || /^<html/i.test(trimmed)) {
98
+ return "HTML error page returned. Check COSTHAWK_API_URL and confirm this MCP route is deployed.";
99
+ }
97
100
  return trimmed;
98
101
  }
99
102
  function formatApiErrorMessage(status, errorText) {
@@ -411,6 +414,26 @@ async function apiRequest(endpoint, options) {
411
414
  clearTimeout(timeoutId);
412
415
  }
413
416
  }
417
+ function buildCompanyBrainOnboardingEndpoint(input) {
418
+ const queryParams = new URLSearchParams();
419
+ if (input.orgId)
420
+ queryParams.set("orgId", input.orgId);
421
+ if (input.orgSlug)
422
+ queryParams.set("orgSlug", input.orgSlug);
423
+ if (input.projectId)
424
+ queryParams.set("projectId", input.projectId);
425
+ if (input.projectSlug)
426
+ queryParams.set("projectSlug", input.projectSlug);
427
+ if (input.gitRepo)
428
+ queryParams.set("gitRepo", input.gitRepo);
429
+ const query = queryParams.toString();
430
+ return query ? `/api/mcp/brain/onboarding?${query}` : "/api/mcp/brain/onboarding";
431
+ }
432
+ async function fetchCompanyBrainOnboardingState(input) {
433
+ return apiRequest(buildCompanyBrainOnboardingEndpoint(input), {
434
+ apiKey: input.apiKey,
435
+ });
436
+ }
414
437
  function isModelPricingEntry(value) {
415
438
  if (!value || typeof value !== "object")
416
439
  return false;
@@ -731,6 +754,31 @@ function getArgValue(args, flag) {
731
754
  }
732
755
  return undefined;
733
756
  }
757
+ function normalizeGitRemoteForProject(rawRemote) {
758
+ const trimmed = rawRemote.trim();
759
+ if (!trimmed)
760
+ return null;
761
+ const sshMatch = trimmed.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
762
+ if (sshMatch) {
763
+ return `${sshMatch[1]}/${sshMatch[2].replace(/\.git$/, "")}`;
764
+ }
765
+ try {
766
+ const parsed = new URL(trimmed);
767
+ return `${parsed.hostname}${parsed.pathname.replace(/^\/+/, "").replace(/\.git$/, "")}`;
768
+ }
769
+ catch {
770
+ return trimmed.replace(/\.git$/, "");
771
+ }
772
+ }
773
+ function detectGitRepoFromCurrentDirectory() {
774
+ const result = spawnSync("git", ["config", "--get", "remote.origin.url"], {
775
+ encoding: "utf8",
776
+ stdio: ["ignore", "pipe", "ignore"],
777
+ });
778
+ if (result.status !== 0 || typeof result.stdout !== "string")
779
+ return undefined;
780
+ return normalizeGitRemoteForProject(result.stdout) ?? undefined;
781
+ }
734
782
  function parseBooleanFlag(args, flag) {
735
783
  const negated = `--no-${flag}`;
736
784
  if (args.includes(negated))
@@ -795,8 +843,21 @@ function resolveCodexConfigPath() {
795
843
  return primary;
796
844
  }
797
845
  function resolveNpmCommand() {
846
+ return findNpmCommand() ?? (platform() === "win32" ? "npm.cmd" : "npm");
847
+ }
848
+ function findNpmCommand() {
798
849
  if (platform() === "win32") {
799
- return "npm.cmd";
850
+ const whereResult = spawnSync("where", ["npm.cmd"], { encoding: "utf8" });
851
+ if (whereResult.status === 0 && typeof whereResult.stdout === "string") {
852
+ const npmPath = whereResult.stdout.split(/\r?\n/)[0]?.trim();
853
+ if (npmPath)
854
+ return npmPath;
855
+ }
856
+ const checkResult = spawnSync("npm.cmd", ["--version"], {
857
+ encoding: "utf8",
858
+ stdio: ["ignore", "pipe", "ignore"],
859
+ });
860
+ return checkResult.status === 0 ? "npm.cmd" : null;
800
861
  }
801
862
  const result = spawnSync("which", ["npm"], { encoding: "utf8" });
802
863
  if (result.status === 0 && typeof result.stdout === "string") {
@@ -804,7 +865,7 @@ function resolveNpmCommand() {
804
865
  if (npmPath)
805
866
  return npmPath;
806
867
  }
807
- return "npm";
868
+ return null;
808
869
  }
809
870
  function escapeTomlString(value) {
810
871
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
@@ -1022,6 +1083,32 @@ function persistApiKey(apiKey) {
1022
1083
  savedAt: new Date().toISOString(),
1023
1084
  });
1024
1085
  }
1086
+ function assertWritableTarget(filePath) {
1087
+ const dir = dirname(filePath);
1088
+ if (!existsSync(dir)) {
1089
+ mkdirSync(dir, { recursive: true });
1090
+ }
1091
+ if (existsSync(filePath)) {
1092
+ accessSync(filePath, constants.W_OK);
1093
+ return;
1094
+ }
1095
+ const testPath = join(dir, `.costhawk-write-test-${process.pid}-${Date.now()}`);
1096
+ writeFileSync(testPath, "ok", { mode: SECRET_FILE_MODE });
1097
+ rmSync(testPath, { force: true });
1098
+ }
1099
+ function runCodexDesktopPreflight() {
1100
+ const npmCommand = findNpmCommand();
1101
+ if (!npmCommand) {
1102
+ throw new Error([
1103
+ "Codex Desktop setup needs npm on PATH before CostHawk can install tracking.",
1104
+ "Install Node.js/npm or open Codex from an environment where npm is available, then run setup again.",
1105
+ ].join(" "));
1106
+ }
1107
+ const codexConfigPath = resolveCodexConfigPath();
1108
+ assertWritableTarget(AUTH_STATE_PATH);
1109
+ assertWritableTarget(codexConfigPath);
1110
+ return { codexConfigPath, npmCommand };
1111
+ }
1025
1112
  async function promptInput(question) {
1026
1113
  const rl = createInterface({
1027
1114
  input: process.stdin,
@@ -1143,6 +1230,8 @@ async function runDeviceLoginFlow(args) {
1143
1230
  async function runSetup(args) {
1144
1231
  const apiKeyArg = getArgValue(args, "--api-key") || getArgValue(args, "--key");
1145
1232
  const nonInteractive = args.includes("--non-interactive");
1233
+ const codexDesktopMode = args.includes("--codex-desktop");
1234
+ const codexLabel = codexDesktopMode ? "Codex Desktop" : "OpenAI Codex CLI";
1146
1235
  let writeClaude = parseBooleanFlag(args, "claude");
1147
1236
  if (writeClaude === undefined) {
1148
1237
  writeClaude = true;
@@ -1396,7 +1485,7 @@ async function runSetup(args) {
1396
1485
  console.log("✅ CostHawk MCP server added to Gemini CLI!");
1397
1486
  }
1398
1487
  if (writeCodex) {
1399
- console.log("✅ CostHawk MCP server added to OpenAI Codex CLI!");
1488
+ console.log(`✅ CostHawk MCP server added to ${codexLabel}!`);
1400
1489
  }
1401
1490
  if (writeOpenCode) {
1402
1491
  console.log("✅ CostHawk MCP server added to OpenCode!");
@@ -1409,15 +1498,20 @@ async function runSetup(args) {
1409
1498
  console.log(` OpenCode config: ${openCodePath}`);
1410
1499
  }
1411
1500
  if (codexPath) {
1412
- console.log(` OpenAI Codex CLI config: ${codexPath}`);
1501
+ console.log(` ${codexLabel} config: ${codexPath}`);
1413
1502
  }
1414
1503
  if (geminiPath) {
1415
1504
  console.log(` Gemini CLI config: ${geminiPath}`);
1416
1505
  }
1417
1506
  console.log(` Stored auth: ${AUTH_STATE_PATH}`);
1418
1507
  console.log(` Local tool auto-sync: ${autoSync ? "enabled" : "disabled"}`);
1419
- console.log(` OpenAI Codex CLI auto-sync: ${autoSync && codexSync ? "enabled" : "disabled"}`);
1420
- console.log(" Note: Auto-sync only applies to local Claude Code / Codex CLI / Cursor logs. Gemini CLI gets MCP tools, but Gemini local transcripts are not auto-synced yet.");
1508
+ console.log(` ${codexLabel} auto-sync: ${autoSync && codexSync ? "enabled" : "disabled"}`);
1509
+ if (codexDesktopMode) {
1510
+ console.log(" Note: CostHawk tracks Codex session activity after Codex writes local usage logs.");
1511
+ }
1512
+ else {
1513
+ console.log(" Note: Auto-sync only applies to local Claude Code / Codex CLI / Cursor logs. Gemini CLI gets MCP tools, but Gemini local transcripts are not auto-synced yet.");
1514
+ }
1421
1515
  console.log("");
1422
1516
  if (writeClaude) {
1423
1517
  console.log("👉 Next step: Restart Claude Code, then ask:");
@@ -1428,8 +1522,13 @@ async function runSetup(args) {
1428
1522
  console.log(" gemini mcp list");
1429
1523
  }
1430
1524
  if (writeCodex) {
1431
- console.log("👉 Next step: Restart Codex, then run:");
1432
- console.log(" codex mcp get costhawk");
1525
+ if (codexDesktopMode) {
1526
+ console.log("👉 Next step: Restart Codex Desktop, then start a new Codex chat.");
1527
+ }
1528
+ else {
1529
+ console.log("👉 Next step: Restart Codex, then run:");
1530
+ console.log(" codex mcp get costhawk");
1531
+ }
1433
1532
  }
1434
1533
  if (writeOpenCode) {
1435
1534
  console.log("👉 Next step: Restart OpenCode to load MCP servers.");
@@ -1571,9 +1670,12 @@ async function syncCursorSessionsToApi(sessions, options) {
1571
1670
  };
1572
1671
  }
1573
1672
  // Perform a one-time sync during --login so data appears in the dashboard immediately.
1574
- async function performLoginSync(apiKey) {
1673
+ async function performLoginSync(apiKey, options = {}) {
1575
1674
  let totalSynced = 0;
1576
- if (claudeCodeDirectoryExists()) {
1675
+ const syncClaudeCode = options.claudeCode ?? true;
1676
+ const syncCodex = options.codex ?? true;
1677
+ const syncCursor = options.cursor ?? true;
1678
+ if (syncClaudeCode && claudeCodeDirectoryExists()) {
1577
1679
  try {
1578
1680
  const sessions = parseAllTranscriptsDetailed(LOGIN_SYNC_MAX_AGE_HOURS);
1579
1681
  const result = await syncSessionsToApi(sessions, {
@@ -1590,7 +1692,7 @@ async function performLoginSync(apiKey) {
1590
1692
  console.error(` Warning: Claude Code sync failed: ${error instanceof Error ? error.message : "Unknown error"}`);
1591
1693
  }
1592
1694
  }
1593
- if (codexDirectoryExists()) {
1695
+ if (syncCodex && codexDirectoryExists()) {
1594
1696
  try {
1595
1697
  const sessions = parseAllCodexSessionsDetailed(LOGIN_SYNC_MAX_AGE_HOURS);
1596
1698
  const result = await syncSessionsToApi(sessions, {
@@ -1608,7 +1710,7 @@ async function performLoginSync(apiKey) {
1608
1710
  console.error(` Warning: Codex sync failed: ${error instanceof Error ? error.message : "Unknown error"}`);
1609
1711
  }
1610
1712
  }
1611
- if (cursorDbExists()) {
1713
+ if (syncCursor && cursorDbExists()) {
1612
1714
  try {
1613
1715
  // parseCursorUsage() reads the whole Cursor SQLite in one pass. The
1614
1716
  // LOGIN_SYNC_MAX_AGE_HOURS window is applied client-side below to
@@ -1645,6 +1747,62 @@ async function performLoginSync(apiKey) {
1645
1747
  console.log("\n No local sessions found to sync (this is normal for first-time users).");
1646
1748
  }
1647
1749
  }
1750
+ function getCodexDesktopSetupArgs(apiKey) {
1751
+ return [
1752
+ "--api-key",
1753
+ apiKey,
1754
+ "--codex",
1755
+ "--no-claude",
1756
+ "--no-gemini",
1757
+ "--no-opencode",
1758
+ "--auto-sync",
1759
+ "--codex-sync",
1760
+ "--no-cursor-sync",
1761
+ "--non-interactive",
1762
+ "--codex-desktop",
1763
+ ];
1764
+ }
1765
+ function withDefaultArg(args, flag, value) {
1766
+ if (args.includes(flag) || args.some((arg) => arg.startsWith(`${flag}=`))) {
1767
+ return args;
1768
+ }
1769
+ return [...args, flag, value];
1770
+ }
1771
+ async function runCodexDesktopSetup(args) {
1772
+ let preflight;
1773
+ try {
1774
+ preflight = runCodexDesktopPreflight();
1775
+ }
1776
+ catch (error) {
1777
+ console.error(`Error: ${error instanceof Error ? error.message : "Codex Desktop setup preflight failed."}`);
1778
+ process.exit(1);
1779
+ }
1780
+ console.log("✅ Codex Desktop setup preflight passed.");
1781
+ console.log(` npm: ${preflight.npmCommand}`);
1782
+ console.log(` Codex config: ${preflight.codexConfigPath}`);
1783
+ console.log(` CostHawk auth: ${AUTH_STATE_PATH}`);
1784
+ let apiKey = getArgValue(args, "--api-key") || getArgValue(args, "--key") || DEFAULT_API_KEY || "";
1785
+ if (!apiKey) {
1786
+ const loginArgs = withDefaultArg(args, "--client-name", "Codex Desktop");
1787
+ try {
1788
+ apiKey = await runDeviceLoginFlow(loginArgs);
1789
+ }
1790
+ catch (error) {
1791
+ console.error(`Error: ${error instanceof Error ? error.message : "Browser login failed."}`);
1792
+ process.exit(1);
1793
+ }
1794
+ }
1795
+ if (!apiKey) {
1796
+ console.error("Error: API key is required to complete Codex Desktop setup.");
1797
+ process.exit(1);
1798
+ }
1799
+ persistApiKey(apiKey);
1800
+ await runSetup(getCodexDesktopSetupArgs(apiKey));
1801
+ await performLoginSync(apiKey, { claudeCode: false, codex: true, cursor: false });
1802
+ console.log("✅ CostHawk Codex Desktop tracking is configured.");
1803
+ console.log(" Restart Codex Desktop if the CostHawk MCP server is not visible yet.");
1804
+ console.log("");
1805
+ }
1648
1806
  async function runLoginCommand(args) {
1649
1807
  const shouldRunSetup = parseBooleanFlag(args, "setup") ?? true;
1650
1808
  let apiKey;
@@ -1698,6 +1856,84 @@ async function runLoginCommand(args) {
1698
1856
  await performLoginSync(apiKey);
1699
1857
  }
1700
1858
  // Markdown formatters
1859
+ function formatPolicyMode(mode) {
1860
+ switch (mode) {
1861
+ case "company_brain":
1862
+ return "Company Brain";
1863
+ case "project_memory":
1864
+ return "Project memory";
1865
+ case "private":
1866
+ default:
1867
+ return "Private";
1868
+ }
1869
+ }
1870
+ function formatReviewMode(mode) {
1871
+ switch (mode) {
1872
+ case "auto_all":
1873
+ return "Auto approve all";
1874
+ case "auto_low_risk":
1875
+ return "Auto approve low risk";
1876
+ case "manual":
1877
+ default:
1878
+ return "Manual review";
1879
+ }
1880
+ }
1881
+ function formatCompanyBrainOnboardingMarkdown(data) {
1882
+ let output = `# Company Brain Project Policies\n\n`;
1883
+ output += `**Organization:** ${data.organization.name} (${data.organization.slug})\n`;
1884
+ output += `**Company Brain:** ${data.companyBrain.name}\n`;
1885
+ output += `**Raw content stored by default:** ${data.companyBrain.rawContentStorage ? "yes" : "no"}\n\n`;
1886
+ output += `## Summary\n`;
1887
+ output += `| Metric | Value |\n|--------|-------|\n`;
1888
+ output += `| Active projects | ${formatNumber(data.summary.totalProjects)} |\n`;
1889
+ output += `| Explicit policies | ${formatNumber(data.summary.explicitPolicies)} |\n`;
1890
+ output += `| Private | ${formatNumber(data.summary.privateProjects)} |\n`;
1891
+ output += `| Project memory | ${formatNumber(data.summary.projectMemoryProjects)} |\n`;
1892
+ output += `| Company Brain | ${formatNumber(data.summary.companyBrainProjects)} |\n`;
1893
+ output += `| Needs review | ${formatNumber(data.summary.unreviewedProjects)} |\n`;
1894
+ if (data.summary.matchedProjects !== null) {
1895
+ output += `| Current repo matches | ${formatNumber(data.summary.matchedProjects)} |\n`;
1896
+ }
1897
+ output += `\n## Project Policies\n`;
1898
+ if (data.projects.length === 0) {
1899
+ output += `No active projects found for this organization.\n\n`;
1900
+ }
1901
+ else {
1902
+ output += `| Project | Policy | Review | Sensitivity | Writes | Entries | Match |\n`;
1903
+ output += `|---------|--------|--------|-------------|--------|---------|-------|\n`;
1904
+ for (const project of data.projects) {
1905
+ const policy = project.policy;
1906
+ const writes = policy.canWriteCompanyBrain
1907
+ ? "company brain"
1908
+ : policy.canWriteProjectMemory
1909
+ ? "project memory"
1910
+ : "blocked";
1911
+ const match = project.currentProjectMatch === null ? "" : project.currentProjectMatch ? "current" : "";
1912
+ output += `| ${project.name} | ${formatPolicyMode(policy.mode)}${policy.isExplicit ? "" : " (default)"} | ${formatReviewMode(policy.reviewMode)} | ${policy.defaultSensitivity} | ${writes} | ${policy.allowedEntryTypes.join(", ")} | ${match} |\n`;
1913
+ }
1914
+ }
1915
+ const currentMatches = data.projects.filter((project) => project.currentProjectMatch);
1916
+ if (currentMatches.length > 0) {
1917
+ output += `\n## Current Project\n`;
1918
+ for (const project of currentMatches) {
1919
+ output += `- ${project.name}: ${formatPolicyMode(project.policy.mode)}. `;
1920
+ if (project.policy.canWriteCompanyBrain) {
1921
+ output += `Agents may submit allowed entries to the Company Brain with ${formatReviewMode(project.policy.reviewMode).toLowerCase()}.\n`;
1922
+ }
1923
+ else if (project.policy.canWriteProjectMemory) {
1924
+ output += `Agents may write project memory, but not shared Company Brain entries.\n`;
1925
+ }
1926
+ else {
1927
+ output += `Agents must not write shared learnings from this project.\n`;
1928
+ }
1929
+ }
1930
+ }
1931
+ output += `\n## Next Actions\n`;
1932
+ for (const action of data.nextActions) {
1933
+ output += `- ${action}\n`;
1934
+ }
1935
+ return truncateResponse(output);
1936
+ }
1701
1937
  function formatUsageSummaryMarkdown(data) {
1702
1938
  let output = `# API Usage Summary\n\n`;
1703
1939
  output += `This report covers CostHawk-tracked API requests from wrapped/proxied keys and provider-ingested API usage. It does not include local Claude Code, Codex, or Cursor sessions; use the local usage, sync, ROI, or savings tools for those.\n\n`;
@@ -3066,7 +3302,57 @@ const GetProxyGuideSchema = {
3066
3302
  const ListIntegrationsSchema = {
3067
3303
  apiKey: z.string().optional().describe("CostHawk API key (falls back to COSTHAWK_API_KEY env var)"),
3068
3304
  };
3305
+ const CompanyBrainOnboardingSchema = {
3306
+ apiKey: z.string().optional().describe("CostHawk API key (falls back to COSTHAWK_API_KEY env var)"),
3307
+ orgId: z.string().optional().describe("Organization ID to inspect. Required only when the API key has access to multiple organizations."),
3308
+ orgSlug: z.string().optional().describe("Organization slug to inspect. Required only when the API key has access to multiple organizations."),
3309
+ projectId: z.string().optional().describe("Optional project ID to mark as the current project in the response."),
3310
+ projectSlug: z.string().optional().describe("Optional project slug to mark as the current project in the response."),
3311
+ gitRepo: z
3312
+ .string()
3313
+ .optional()
3314
+ .describe("Optional git repository identifier for the current local project, such as github.com/acme/app or acme/app."),
3315
+ format: z.enum(["markdown", "json"]).optional().default("markdown").describe("Response format"),
3316
+ };
3069
3317
  // Register tools
3318
+ server.registerTool("costhawk_get_company_brain_onboarding", {
3319
+ description: `Get the Company Brain onboarding state for the authenticated workspace. Use this before proposing or writing any Company Brain or project-memory entry. It returns the organization brain destination, active projects, each project's sharing policy (Private, Project memory, or Company Brain), review mode, sensitivity, allowed entry types, and whether the current repo/project is allowed to write. Agents must treat Private projects as a hard stop for shared brain writes.`,
3320
+ inputSchema: CompanyBrainOnboardingSchema,
3321
+ annotations: READ_ONLY_ANNOTATIONS,
3322
+ }, async (args, _extra) => {
3323
+ const apiKey = getApiKey(args.apiKey);
3324
+ if (!apiKey) {
3325
+ return {
3326
+ content: [
3327
+ {
3328
+ type: "text",
3329
+ text: "Error: No API key provided. Run `costhawk --login` or set COSTHAWK_API_KEY in your MCP configuration.",
3330
+ },
3331
+ ],
3332
+ isError: true,
3333
+ };
3334
+ }
3335
+ try {
3336
+ const data = await fetchCompanyBrainOnboardingState({
3337
+ apiKey,
3338
+ orgId: args.orgId,
3339
+ orgSlug: args.orgSlug,
3340
+ projectId: args.projectId,
3341
+ projectSlug: args.projectSlug,
3342
+ gitRepo: args.gitRepo,
3343
+ });
3344
+ const text = args.format === "json" ? JSON.stringify(data, null, 2) : formatCompanyBrainOnboardingMarkdown(data);
3345
+ return {
3346
+ content: [{ type: "text", text }],
3347
+ };
3348
+ }
3349
+ catch (error) {
3350
+ return {
3351
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
3352
+ isError: true,
3353
+ };
3354
+ }
3355
+ });
3070
3356
  server.registerTool("costhawk_get_usage_summary", {
3071
3357
  description: `Get a summary of CostHawk-tracked API request usage over a time period. This covers wrapped/proxied keys and provider-ingested API usage stored as API requests. It does NOT include local Claude Code, Codex, or Cursor sessions; for local AI-tool token usage, use costhawk_get_local_claude_code_usage, costhawk_get_local_codex_usage, costhawk_get_local_cursor_usage, costhawk_get_local_roi_report, or costhawk_get_savings after syncing. Returns total API costs, API calls, and API token usage broken down by provider and model. Supports preset periods (last_24h, today, yesterday, last_7d, last_30d) or custom date ranges.\n\nIMPORTANT: Always display the COMPLETE output in your response using markdown tables. Render every section (Overview, By Provider, By Model). Never summarize or truncate. If this report is zero, do not say the user's local AI-tool usage is zero; explain that local Claude Code, Codex, and Cursor usage is reported separately.`,
3072
3358
  inputSchema: UsageSummarySchema,
@@ -4870,8 +5156,74 @@ function stopAutoSync() {
4870
5156
  console.error("[Auto-sync] Stopped");
4871
5157
  }
4872
5158
  }
5159
+ async function runBrainCommand(args) {
5160
+ const hasExplicitSubcommand = Boolean(args[1] && !args[1].startsWith("-"));
5161
+ const subcommand = hasExplicitSubcommand ? args[1] : "status";
5162
+ const commandArgs = args.slice(hasExplicitSubcommand ? 2 : 1);
5163
+ if (subcommand === "help" || commandArgs.includes("--help") || commandArgs.includes("-h")) {
5164
+ console.log(`CostHawk Company Brain
5165
+
5166
+ USAGE:
5167
+ costhawk brain status [OPTIONS]
5168
+
5169
+ OPTIONS:
5170
+ --org-id Organization ID
5171
+ --org-slug Organization slug
5172
+ --project-id Current project ID
5173
+ --project-slug Current project slug
5174
+ --git-repo Current git repository identifier
5175
+ --no-detect-git Do not auto-detect git remote.origin.url
5176
+ --json Print raw JSON
5177
+ --api-key Provide API key non-interactively
5178
+
5179
+ EXAMPLES:
5180
+ costhawk brain status
5181
+ costhawk brain status --org-slug acme --git-repo github.com/acme/app
5182
+ `);
5183
+ process.exit(0);
5184
+ }
5185
+ if (subcommand !== "status" && subcommand !== "onboarding") {
5186
+ console.error(`Error: unknown brain command "${subcommand}". Use "costhawk brain status".`);
5187
+ process.exit(1);
5188
+ }
5189
+ const apiKey = getApiKey(getArgValue(commandArgs, "--api-key"));
5190
+ if (!apiKey) {
5191
+ console.error("Error: No API key provided. Run `costhawk --login` or set COSTHAWK_API_KEY.");
5192
+ process.exit(1);
5193
+ }
5194
+ const explicitGitRepo = getArgValue(commandArgs, "--git-repo");
5195
+ const gitRepo = explicitGitRepo ?? (commandArgs.includes("--no-detect-git") ? undefined : detectGitRepoFromCurrentDirectory());
5196
+ try {
5197
+ const data = await fetchCompanyBrainOnboardingState({
5198
+ apiKey,
5199
+ orgId: getArgValue(commandArgs, "--org-id"),
5200
+ orgSlug: getArgValue(commandArgs, "--org-slug"),
5201
+ projectId: getArgValue(commandArgs, "--project-id"),
5202
+ projectSlug: getArgValue(commandArgs, "--project-slug"),
5203
+ gitRepo,
5204
+ });
5205
+ if (commandArgs.includes("--json")) {
5206
+ console.log(JSON.stringify(data, null, 2));
5207
+ }
5208
+ else {
5209
+ console.log(formatCompanyBrainOnboardingMarkdown(data));
5210
+ }
5211
+ process.exit(0);
5212
+ }
5213
+ catch (error) {
5214
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
5215
+ process.exit(1);
5216
+ }
5217
+ }
4873
5218
  async function handleCliFlags() {
4874
5219
  const args = process.argv.slice(2);
5220
+ if (args[0] === "brain") {
5221
+ await runBrainCommand(args);
5222
+ }
5223
+ if (args[0] === "setup" && args[1] === "codex-desktop") {
5224
+ await runCodexDesktopSetup(args.slice(2));
5225
+ process.exit(0);
5226
+ }
4875
5227
  if (args.includes("--version") || args.includes("-v")) {
4876
5228
  console.log(formatVersion());
4877
5229
  process.exit(0);
@@ -4888,6 +5240,9 @@ OPTIONS:
4888
5240
  --tools List all available MCP tools
4889
5241
  --self-test Output diagnostic JSON for debugging
4890
5242
  --what-we-read Show local directories and sample files the MCP reads
5243
+ brain status Show Company Brain project sharing policy for this workspace/repo
5244
+ setup codex-desktop
5245
+ Configure Codex Desktop usage tracking only
4891
5246
  --login Browser/device login (recommended)
4892
5247
  --no-setup With --login, skip writing MCP config files
4893
5248
  --open With --login, force opening the browser
@@ -4895,6 +5250,8 @@ OPTIONS:
4895
5250
  --client-name Optional device/client label for login approval screen
4896
5251
  --scopes Optional comma-separated scopes for login token (default: mcp:read,mcp:write,otel:ingest)
4897
5252
  --setup Configure MCP settings (Claude Code / Codex CLI / Gemini CLI / OpenCode)
5253
+ --setup-codex-desktop
5254
+ Alias for "setup codex-desktop"
4898
5255
  --codex Write OpenAI Codex CLI config during setup
4899
5256
  --opencode Also write OpenCode config during setup
4900
5257
  --no-claude Skip writing Claude Code config during setup
@@ -4918,11 +5275,13 @@ ENVIRONMENT VARIABLES:
4918
5275
  SETUP:
4919
5276
  1. Recommended (browser login + auto-setup):
4920
5277
  npm exec --yes costhawk@latest -- --login
4921
- 2. Manual API key mode (Claude Code):
5278
+ 2. Codex Desktop usage tracking only:
5279
+ npm exec --yes costhawk@latest -- setup codex-desktop
5280
+ 3. Manual API key mode (Claude Code):
4922
5281
  claude mcp add -s user -e COSTHAWK_API_KEY=your_key -e COSTHAWK_AUTO_SYNC=true costhawk -- npx --yes costhawk@latest
4923
5282
  Manual API key mode (Gemini CLI):
4924
5283
  gemini mcp add -s user -e COSTHAWK_API_KEY=your_key costhawk npx --yes costhawk@latest
4925
- 3. Interactive setup (writes Claude Code + Codex CLI + Gemini CLI config, optional OpenCode):
5284
+ 4. Interactive setup (writes Claude Code + Codex CLI + Gemini CLI config, optional OpenCode):
4926
5285
  npm exec --yes costhawk@latest -- --setup --opencode
4927
5286
 
4928
5287
  DOCUMENTATION:
@@ -4934,6 +5293,10 @@ DOCUMENTATION:
4934
5293
  await runLoginCommand(args);
4935
5294
  process.exit(0);
4936
5295
  }
5296
+ if (args.includes("--setup-codex-desktop")) {
5297
+ await runCodexDesktopSetup(args);
5298
+ process.exit(0);
5299
+ }
4937
5300
  if (args.includes("--setup")) {
4938
5301
  await runSetup(args);
4939
5302
  process.exit(0);