proxitor 0.5.1 → 0.5.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"config2.mjs","names":["clack.select","isCancel"],"sources":["../src/commands/config.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { isCancel } from '@clack/prompts'\nimport type { OpenRouterClient } from '../openrouter/client.js'\nimport { addOverrideCommand } from './config/add.js'\nimport { browseModelsCommand } from './config/browse.js'\nimport { editOverrideCommand } from './config/edit.js'\nimport { listOverridesCommand } from './config/list.js'\nimport { removeOverrideCommand } from './config/remove.js'\nimport { validateConfigCommand } from './config/validate.js'\n\n/** Run the interactive config manager menu. */\nexport async function runConfigMenu(client: OpenRouterClient): Promise<void> {\n clack.intro('Proxitor Config Manager')\n\n const action = await clack.select({\n message: 'What would you like to do?',\n options: [\n { value: 'add', label: '➕ Add model override' },\n { value: 'edit', label: '✏️ Edit model override' },\n { value: 'remove', label: '🗑 Remove model override' },\n { value: 'list', label: '📋 List current overrides' },\n { value: 'browse', label: '🔍 Browse models' },\n { value: 'validate', label: '✅ Validate config' },\n { value: 'exit', label: '❌ Exit' },\n ],\n })\n\n if (isCancel(action) || action === 'exit') {\n clack.outro('Bye!')\n return\n }\n\n switch (action) {\n case 'add':\n await addOverrideCommand(client)\n break\n case 'edit':\n await editOverrideCommand(client)\n break\n case 'remove':\n await removeOverrideCommand()\n break\n case 'list':\n await listOverridesCommand()\n break\n case 'browse':\n await browseModelsCommand(client)\n break\n case 'validate':\n await validateConfigCommand()\n break\n }\n}\n"],"mappings":";;;;;;;;;AAWA,eAAsB,cAAc,QAAyC;CAC3E,GAAY,yBAAyB;CAErC,MAAM,SAAS,MAAMA,GAAa;EAChC,SAAS;EACT,SAAS;GACP;IAAE,OAAO;IAAO,OAAO;GAAwB;GAC/C;IAAE,OAAO;IAAQ,OAAO;GAA2B;GACnD;IAAE,OAAO;IAAU,OAAO;GAA6B;GACvD;IAAE,OAAO;IAAQ,OAAO;GAA6B;GACrD;IAAE,OAAO;IAAU,OAAO;GAAoB;GAC9C;IAAE,OAAO;IAAY,OAAO;GAAqB;GACjD;IAAE,OAAO;IAAQ,OAAO;GAAU;EACpC;CACF,CAAC;CAED,IAAIC,EAAS,MAAM,KAAK,WAAW,QAAQ;EACzC,GAAY,MAAM;EAClB;CACF;CAEA,QAAQ,QAAR;EACE,KAAK;GACH,MAAM,mBAAmB,MAAM;GAC/B;EACF,KAAK;GACH,MAAM,oBAAoB,MAAM;GAChC;EACF,KAAK;GACH,MAAM,sBAAsB;GAC5B;EACF,KAAK;GACH,MAAM,qBAAqB;GAC3B;EACF,KAAK;GACH,MAAM,oBAAoB,MAAM;GAChC;EACF,KAAK;GACH,MAAM,sBAAsB;GAC5B;CACJ;AACF"}
1
+ {"version":3,"file":"config2.mjs","names":["clack.select","isCancel"],"sources":["../src/commands/config.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { isCancel } from '@clack/prompts'\nimport type { OpenRouterDataClient } from '../openrouter/data-client.js'\nimport { addOverrideCommand } from './config/add.js'\nimport { browseModelsCommand } from './config/browse.js'\nimport { editOverrideCommand } from './config/edit.js'\nimport { listOverridesCommand } from './config/list.js'\nimport { removeOverrideCommand } from './config/remove.js'\nimport { validateConfigCommand } from './config/validate.js'\n\n/** Run the interactive config manager menu. */\nexport async function runConfigMenu(client: OpenRouterDataClient): Promise<void> {\n clack.intro('Proxitor Config Manager')\n\n const action = await clack.select({\n message: 'What would you like to do?',\n options: [\n { value: 'add', label: '➕ Add model override' },\n { value: 'edit', label: '✏️ Edit model override' },\n { value: 'remove', label: '🗑 Remove model override' },\n { value: 'list', label: '📋 List current overrides' },\n { value: 'browse', label: '🔍 Browse models' },\n { value: 'validate', label: '✅ Validate config' },\n { value: 'exit', label: '❌ Exit' },\n ],\n })\n\n if (isCancel(action) || action === 'exit') {\n clack.outro('Bye!')\n return\n }\n\n switch (action) {\n case 'add':\n await addOverrideCommand(client)\n break\n case 'edit':\n await editOverrideCommand(client)\n break\n case 'remove':\n await removeOverrideCommand()\n break\n case 'list':\n await listOverridesCommand()\n break\n case 'browse':\n await browseModelsCommand(client)\n break\n case 'validate':\n await validateConfigCommand()\n break\n }\n}\n"],"mappings":";;;;;;;;;AAWA,eAAsB,cAAc,QAA6C;CAC/E,GAAY,yBAAyB;CAErC,MAAM,SAAS,MAAMA,GAAa;EAChC,SAAS;EACT,SAAS;GACP;IAAE,OAAO;IAAO,OAAO;GAAwB;GAC/C;IAAE,OAAO;IAAQ,OAAO;GAA2B;GACnD;IAAE,OAAO;IAAU,OAAO;GAA6B;GACvD;IAAE,OAAO;IAAQ,OAAO;GAA6B;GACrD;IAAE,OAAO;IAAU,OAAO;GAAoB;GAC9C;IAAE,OAAO;IAAY,OAAO;GAAqB;GACjD;IAAE,OAAO;IAAQ,OAAO;GAAU;EACpC;CACF,CAAC;CAED,IAAIC,EAAS,MAAM,KAAK,WAAW,QAAQ;EACzC,GAAY,MAAM;EAClB;CACF;CAEA,QAAQ,QAAR;EACE,KAAK;GACH,MAAM,mBAAmB,MAAM;GAC/B;EACF,KAAK;GACH,MAAM,oBAAoB,MAAM;GAChC;EACF,KAAK;GACH,MAAM,sBAAsB;GAC5B;EACF,KAAK;GACH,MAAM,qBAAqB;GAC3B;EACF,KAAK;GACH,MAAM,oBAAoB,MAAM;GAChC;EACF,KAAK;GACH,MAAM,sBAAsB;GAC5B;CACJ;AACF"}
package/dist/edit.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"edit.mjs","names":["isCancel","clack.confirm","clack.select"],"sources":["../src/commands/config/edit.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { isCancel } from '@clack/prompts'\nimport type { ModelOverride } from '../../config-schema.js'\nimport type { OpenRouterClient } from '../../openrouter/client.js'\nimport { getModelOverrides, requireConfigPath, setModelOverride } from './config.js'\nimport {\n fetchProvidersForModel,\n selectProvidersByMode,\n selectRoutingMode,\n} from './providers.js'\n\nfunction formatOverrideHint(override: ModelOverride | undefined): string {\n if (!override?.provider) return '(no provider routing)'\n const keys = Object.keys(override.provider)\n return keys\n .map(\n k => `${k}: ${JSON.stringify((override.provider as Record<string, unknown>)?.[k])}`,\n )\n .join(', ')\n}\n\nfunction showCurrentConfig(modelKey: string, current: ModelOverride): void {\n clack.log.info(`Current config for \"${modelKey}\":`)\n if (current.provider) {\n for (const [field, value] of Object.entries(current.provider)) {\n clack.log.info(` provider.${field}: ${JSON.stringify(value)}`)\n }\n }\n if (current.headers) {\n for (const [name, value] of Object.entries(current.headers)) {\n clack.log.info(` headers.${name}: ${value}`)\n }\n }\n}\n\nfunction withoutProvider(current: ModelOverride): ModelOverride {\n return current.headers ? { headers: current.headers } : {}\n}\n\nasync function updateProviderRouting(\n configPath: string,\n modelKey: string,\n current: ModelOverride,\n client: OpenRouterClient,\n): Promise<void> {\n const isPattern = modelKey.includes('*')\n\n const mode = await selectRoutingMode('Routing mode')\n if (isCancel(mode)) return\n\n if (mode === 'skip') {\n setModelOverride(configPath, modelKey, withoutProvider(current))\n clack.outro('✓ Override updated')\n return\n }\n\n const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern)\n if (!providerOptions) return\n\n const override = await selectProvidersByMode(mode as string, providerOptions)\n if (override === null) return\n\n const updated = withoutProvider(current)\n if (override.provider) {\n updated.provider = override.provider as ModelOverride['provider']\n }\n\n const save = await clack.confirm({ message: 'Save changes?' })\n if (isCancel(save) || !save) {\n clack.outro('Cancelled')\n return\n }\n\n setModelOverride(configPath, modelKey, updated)\n clack.outro('✓ Override updated')\n}\n\n/** Run the interactive \"Edit model override\" flow. */\nexport async function editOverrideCommand(client: OpenRouterClient): Promise<void> {\n clack.intro('Edit Model Override')\n\n const configPath = requireConfigPath()\n const overrides = getModelOverrides(configPath)\n const keys = Object.keys(overrides)\n\n if (keys.length === 0) {\n clack.log.warn('No model overrides found. Use Add instead.')\n clack.outro('')\n return\n }\n\n const selected = await clack.select({\n message: 'Select override to edit',\n options: keys.map(k => ({\n value: k,\n label: k,\n hint: formatOverrideHint(overrides[k]),\n })),\n })\n if (isCancel(selected)) return\n\n const modelKey = selected as string\n const current: ModelOverride = overrides[modelKey] ?? {}\n\n showCurrentConfig(modelKey, current)\n\n const target = await clack.select({\n message: 'What to change?',\n options: [\n { value: 'provider', label: 'Provider routing' },\n { value: 'replace', label: 'Replace entirely' },\n ],\n })\n if (isCancel(target)) return\n\n if (target === 'provider' || target === 'replace') {\n await updateProviderRouting(configPath, modelKey, current, client)\n }\n}\n"],"mappings":";;;;AAWA,SAAS,mBAAmB,UAA6C;CACvE,IAAI,CAAC,UAAU,UAAU,OAAO;CAEhC,OADa,OAAO,KAAK,SAAS,QACxB,EACP,KACC,MAAK,GAAG,EAAE,IAAI,KAAK,UAAW,SAAS,WAAuC,EAAE,GAClF,EACC,KAAK,IAAI;AACd;AAEA,SAAS,kBAAkB,UAAkB,SAA8B;CACzE,EAAU,KAAK,uBAAuB,SAAS,GAAG;CAClD,IAAI,QAAQ,UACV,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,QAAQ,QAAQ,GAC1D,EAAU,KAAK,cAAc,MAAM,IAAI,KAAK,UAAU,KAAK,GAAG;CAGlE,IAAI,QAAQ,SACV,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,OAAO,GACxD,EAAU,KAAK,aAAa,KAAK,IAAI,OAAO;AAGlD;AAEA,SAAS,gBAAgB,SAAuC;CAC9D,OAAO,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAC3D;AAEA,eAAe,sBACb,YACA,UACA,SACA,QACe;CACf,MAAM,YAAY,SAAS,SAAS,GAAG;CAEvC,MAAM,OAAO,MAAM,kBAAkB,cAAc;CACnD,IAAIA,IAAS,IAAI,GAAG;CAEpB,IAAI,SAAS,QAAQ;EACnB,iBAAiB,YAAY,UAAU,gBAAgB,OAAO,CAAC;EAC/D,GAAY,oBAAoB;EAChC;CACF;CAEA,MAAM,kBAAkB,MAAM,uBAAuB,QAAQ,UAAU,SAAS;CAChF,IAAI,CAAC,iBAAiB;CAEtB,MAAM,WAAW,MAAM,sBAAsB,MAAgB,eAAe;CAC5E,IAAI,aAAa,MAAM;CAEvB,MAAM,UAAU,gBAAgB,OAAO;CACvC,IAAI,SAAS,UACX,QAAQ,WAAW,SAAS;CAG9B,MAAM,OAAO,MAAMC,GAAc,EAAE,SAAS,gBAAgB,CAAC;CAC7D,IAAID,IAAS,IAAI,KAAK,CAAC,MAAM;EAC3B,GAAY,WAAW;EACvB;CACF;CAEA,iBAAiB,YAAY,UAAU,OAAO;CAC9C,GAAY,oBAAoB;AAClC;;AAGA,eAAsB,oBAAoB,QAAyC;CACjF,GAAY,qBAAqB;CAEjC,MAAM,aAAa,kBAAkB;CACrC,MAAM,YAAY,kBAAkB,UAAU;CAC9C,MAAM,OAAO,OAAO,KAAK,SAAS;CAElC,IAAI,KAAK,WAAW,GAAG;EACrB,EAAU,KAAK,4CAA4C;EAC3D,GAAY,EAAE;EACd;CACF;CAEA,MAAM,WAAW,MAAME,GAAa;EAClC,SAAS;EACT,SAAS,KAAK,KAAI,OAAM;GACtB,OAAO;GACP,OAAO;GACP,MAAM,mBAAmB,UAAU,EAAE;EACvC,EAAE;CACJ,CAAC;CACD,IAAIF,IAAS,QAAQ,GAAG;CAExB,MAAM,WAAW;CACjB,MAAM,UAAyB,UAAU,aAAa,CAAC;CAEvD,kBAAkB,UAAU,OAAO;CAEnC,MAAM,SAAS,MAAME,GAAa;EAChC,SAAS;EACT,SAAS,CACP;GAAE,OAAO;GAAY,OAAO;EAAmB,GAC/C;GAAE,OAAO;GAAW,OAAO;EAAmB,CAChD;CACF,CAAC;CACD,IAAIF,IAAS,MAAM,GAAG;CAEtB,IAAI,WAAW,cAAc,WAAW,WACtC,MAAM,sBAAsB,YAAY,UAAU,SAAS,MAAM;AAErE"}
1
+ {"version":3,"file":"edit.mjs","names":["isCancel","clack.confirm","clack.select"],"sources":["../src/commands/config/edit.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { isCancel } from '@clack/prompts'\nimport type { ModelOverride } from '../../config-schema.js'\nimport type { OpenRouterDataClient } from '../../openrouter/data-client.js'\nimport { getModelOverrides, requireConfigPath, setModelOverride } from './config.js'\nimport {\n fetchProvidersForModel,\n selectProvidersByMode,\n selectRoutingMode,\n} from './providers.js'\n\nfunction formatOverrideHint(override: ModelOverride | undefined): string {\n if (!override?.provider) return '(no provider routing)'\n const keys = Object.keys(override.provider)\n return keys\n .map(\n k => `${k}: ${JSON.stringify((override.provider as Record<string, unknown>)?.[k])}`,\n )\n .join(', ')\n}\n\nfunction showCurrentConfig(modelKey: string, current: ModelOverride): void {\n clack.log.info(`Current config for \"${modelKey}\":`)\n if (current.provider) {\n for (const [field, value] of Object.entries(current.provider)) {\n clack.log.info(` provider.${field}: ${JSON.stringify(value)}`)\n }\n }\n if (current.headers) {\n for (const [name, value] of Object.entries(current.headers)) {\n clack.log.info(` headers.${name}: ${value}`)\n }\n }\n}\n\nfunction withoutProvider(current: ModelOverride): ModelOverride {\n return current.headers ? { headers: current.headers } : {}\n}\n\nasync function updateProviderRouting(\n configPath: string,\n modelKey: string,\n current: ModelOverride,\n client: OpenRouterDataClient,\n): Promise<void> {\n const isPattern = modelKey.includes('*')\n\n const mode = await selectRoutingMode('Routing mode')\n if (isCancel(mode)) return\n\n if (mode === 'skip') {\n setModelOverride(configPath, modelKey, withoutProvider(current))\n clack.outro('✓ Override updated')\n return\n }\n\n const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern)\n if (!providerOptions) return\n\n const override = await selectProvidersByMode(mode as string, providerOptions)\n if (override === null) return\n\n const updated = withoutProvider(current)\n if (override.provider) {\n updated.provider = override.provider as ModelOverride['provider']\n }\n\n const save = await clack.confirm({ message: 'Save changes?' })\n if (isCancel(save) || !save) {\n clack.outro('Cancelled')\n return\n }\n\n setModelOverride(configPath, modelKey, updated)\n clack.outro('✓ Override updated')\n}\n\n/** Run the interactive \"Edit model override\" flow. */\nexport async function editOverrideCommand(client: OpenRouterDataClient): Promise<void> {\n clack.intro('Edit Model Override')\n\n const configPath = requireConfigPath()\n const overrides = getModelOverrides(configPath)\n const keys = Object.keys(overrides)\n\n if (keys.length === 0) {\n clack.log.warn('No model overrides found. Use Add instead.')\n clack.outro('')\n return\n }\n\n const selected = await clack.select({\n message: 'Select override to edit',\n options: keys.map(k => ({\n value: k,\n label: k,\n hint: formatOverrideHint(overrides[k]),\n })),\n })\n if (isCancel(selected)) return\n\n const modelKey = selected as string\n const current: ModelOverride = overrides[modelKey] ?? {}\n\n showCurrentConfig(modelKey, current)\n\n const target = await clack.select({\n message: 'What to change?',\n options: [\n { value: 'provider', label: 'Provider routing' },\n { value: 'replace', label: 'Replace entirely' },\n ],\n })\n if (isCancel(target)) return\n\n if (target === 'provider' || target === 'replace') {\n await updateProviderRouting(configPath, modelKey, current, client)\n }\n}\n"],"mappings":";;;;AAWA,SAAS,mBAAmB,UAA6C;CACvE,IAAI,CAAC,UAAU,UAAU,OAAO;CAEhC,OADa,OAAO,KAAK,SAAS,QACxB,EACP,KACC,MAAK,GAAG,EAAE,IAAI,KAAK,UAAW,SAAS,WAAuC,EAAE,GAClF,EACC,KAAK,IAAI;AACd;AAEA,SAAS,kBAAkB,UAAkB,SAA8B;CACzE,EAAU,KAAK,uBAAuB,SAAS,GAAG;CAClD,IAAI,QAAQ,UACV,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,QAAQ,QAAQ,GAC1D,EAAU,KAAK,cAAc,MAAM,IAAI,KAAK,UAAU,KAAK,GAAG;CAGlE,IAAI,QAAQ,SACV,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,OAAO,GACxD,EAAU,KAAK,aAAa,KAAK,IAAI,OAAO;AAGlD;AAEA,SAAS,gBAAgB,SAAuC;CAC9D,OAAO,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAC3D;AAEA,eAAe,sBACb,YACA,UACA,SACA,QACe;CACf,MAAM,YAAY,SAAS,SAAS,GAAG;CAEvC,MAAM,OAAO,MAAM,kBAAkB,cAAc;CACnD,IAAIA,IAAS,IAAI,GAAG;CAEpB,IAAI,SAAS,QAAQ;EACnB,iBAAiB,YAAY,UAAU,gBAAgB,OAAO,CAAC;EAC/D,GAAY,oBAAoB;EAChC;CACF;CAEA,MAAM,kBAAkB,MAAM,uBAAuB,QAAQ,UAAU,SAAS;CAChF,IAAI,CAAC,iBAAiB;CAEtB,MAAM,WAAW,MAAM,sBAAsB,MAAgB,eAAe;CAC5E,IAAI,aAAa,MAAM;CAEvB,MAAM,UAAU,gBAAgB,OAAO;CACvC,IAAI,SAAS,UACX,QAAQ,WAAW,SAAS;CAG9B,MAAM,OAAO,MAAMC,GAAc,EAAE,SAAS,gBAAgB,CAAC;CAC7D,IAAID,IAAS,IAAI,KAAK,CAAC,MAAM;EAC3B,GAAY,WAAW;EACvB;CACF;CAEA,iBAAiB,YAAY,UAAU,OAAO;CAC9C,GAAY,oBAAoB;AAClC;;AAGA,eAAsB,oBAAoB,QAA6C;CACrF,GAAY,qBAAqB;CAEjC,MAAM,aAAa,kBAAkB;CACrC,MAAM,YAAY,kBAAkB,UAAU;CAC9C,MAAM,OAAO,OAAO,KAAK,SAAS;CAElC,IAAI,KAAK,WAAW,GAAG;EACrB,EAAU,KAAK,4CAA4C;EAC3D,GAAY,EAAE;EACd;CACF;CAEA,MAAM,WAAW,MAAME,GAAa;EAClC,SAAS;EACT,SAAS,KAAK,KAAI,OAAM;GACtB,OAAO;GACP,OAAO;GACP,MAAM,mBAAmB,UAAU,EAAE;EACvC,EAAE;CACJ,CAAC;CACD,IAAIF,IAAS,QAAQ,GAAG;CAExB,MAAM,WAAW;CACjB,MAAM,UAAyB,UAAU,aAAa,CAAC;CAEvD,kBAAkB,UAAU,OAAO;CAEnC,MAAM,SAAS,MAAME,GAAa;EAChC,SAAS;EACT,SAAS,CACP;GAAE,OAAO;GAAY,OAAO;EAAmB,GAC/C;GAAE,OAAO;GAAW,OAAO;EAAmB,CAChD;CACF,CAAC;CACD,IAAIF,IAAS,MAAM,GAAG;CAEtB,IAAI,WAAW,cAAc,WAAW,WACtC,MAAM,sBAAsB,YAAY,UAAU,SAAS,MAAM;AAErE"}
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  //#region src/openrouter/cache.ts
6
- const CACHE_DIR = join(homedir(), ".proxitor", "cache");
6
+ const CACHE_DIR = join(homedir(), ".cache", "proxitor");
7
7
  /** Read a cached value. Returns `null` when missing, expired (older than `ttlMs`), or unparseable. */
8
8
  function readCache(key, ttlMs) {
9
9
  const path = join(CACHE_DIR, `${key}.json`);
@@ -31,9 +31,9 @@ const CACHE_TTL$1 = 3600 * 1e3;
31
31
  async function fetchModels(client) {
32
32
  const cached = readCache(CACHE_KEY$1, CACHE_TTL$1);
33
33
  if (cached) return cached;
34
- const response = await client.get("/models");
35
- writeCache(CACHE_KEY$1, response.data);
36
- return response.data;
34
+ const models = await client.fetchModels();
35
+ writeCache(CACHE_KEY$1, models);
36
+ return models;
37
37
  }
38
38
  /** `"anthropic/claude-sonnet-4"` → `"anthropic"` */
39
39
  function parseModelAuthor(modelId) {
@@ -88,7 +88,7 @@ function formatModelHint(m) {
88
88
  //#endregion
89
89
  //#region src/openrouter/endpoints.ts
90
90
  async function fetchModelEndpoints(client, author, slug) {
91
- return (await client.get(`/models/${author}/${slug}/endpoints`)).data.endpoints ?? [];
91
+ return client.fetchModelEndpoints(author, slug);
92
92
  }
93
93
  function getUniqueProviders(endpoints) {
94
94
  const seen = /* @__PURE__ */ new Set();
@@ -111,9 +111,9 @@ const CACHE_TTL = 1440 * 60 * 1e3;
111
111
  async function fetchProviders(client) {
112
112
  const cached = readCache(CACHE_KEY, CACHE_TTL);
113
113
  if (cached) return cached;
114
- const response = await client.get("/providers");
115
- writeCache(CACHE_KEY, response.data);
116
- return response.data;
114
+ const providers = await client.fetchProviders();
115
+ writeCache(CACHE_KEY, providers);
116
+ return providers;
117
117
  }
118
118
  //#endregion
119
119
  //#region src/commands/config/providers.ts
@@ -1 +1 @@
1
- {"version":3,"file":"providers.mjs","names":["CACHE_KEY","CACHE_TTL","clack.spinner","clack.select","clack.multiselect","clack.isCancel","clack.confirm"],"sources":["../src/openrouter/cache.ts","../src/openrouter/models.ts","../src/commands/config/format.ts","../src/openrouter/endpoints.ts","../src/openrouter/providers.ts","../src/commands/config/providers.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport type CacheEntry<T> = {\n data: T\n fetchedAt: number\n}\n\nexport const CACHE_DIR = join(homedir(), '.proxitor', 'cache')\n\n/** Read a cached value. Returns `null` when missing, expired (older than `ttlMs`), or unparseable. */\nexport function readCache<T>(key: string, ttlMs: number): T | null {\n const path = join(CACHE_DIR, `${key}.json`)\n if (!existsSync(path)) return null\n\n try {\n const entry: CacheEntry<T> = JSON.parse(readFileSync(path, 'utf-8'))\n if (Date.now() - entry.fetchedAt > ttlMs) return null\n return entry.data\n } catch {\n return null\n }\n}\n\nexport function writeCache<T>(key: string, data: T): void {\n mkdirSync(CACHE_DIR, { recursive: true })\n const entry: CacheEntry<T> = { fetchedAt: Date.now(), data }\n writeFileSync(join(CACHE_DIR, `${key}.json`), JSON.stringify(entry))\n}\n\nexport function clearCache(key: string): void {\n const path = join(CACHE_DIR, `${key}.json`)\n if (existsSync(path)) unlinkSync(path)\n}\n","import { readCache, writeCache } from './cache.js'\nimport type { OpenRouterClient } from './client.js'\nimport type { OpenRouterModel, OpenRouterModelsResponse } from './types.js'\n\nconst CACHE_KEY = 'models'\nconst CACHE_TTL = 60 * 60 * 1000 // 1 hour\n\nexport async function fetchModels(client: OpenRouterClient): Promise<OpenRouterModel[]> {\n const cached = readCache<OpenRouterModel[]>(CACHE_KEY, CACHE_TTL)\n if (cached) return cached\n\n const response = await client.get<OpenRouterModelsResponse>('/models')\n writeCache(CACHE_KEY, response.data)\n return response.data\n}\n\n/** `\"anthropic/claude-sonnet-4\"` → `\"anthropic\"` */\nexport function parseModelAuthor(modelId: string): string {\n return modelId.split('/')[0] ?? ''\n}\n\n/** `\"anthropic/claude-sonnet-4\"` → `\"claude-sonnet-4\"` */\nexport function parseModelSlug(modelId: string): string {\n return modelId.split('/').slice(1).join('/')\n}\n\n/** `\"0.000003\"` → `\"$3.00\"`, `\"0\"` → `\"free\"` */\nexport function formatPrice(pricePerToken: string): string {\n const per1M = Number.parseFloat(pricePerToken) * 1_000_000\n if (per1M === 0) return 'free'\n if (per1M < 0.01) return `$${per1M.toFixed(4)}`\n return `$${per1M.toFixed(2)}`\n}\n","import { formatPrice } from '../../openrouter/models.js'\nimport type { OpenRouterModel } from '../../openrouter/types.js'\n\nexport function formatPricing(prompt: string, completion: string): string {\n const fmt = (perToken: string) => {\n const per1M = Number.parseFloat(perToken) * 1_000_000\n if (per1M === 0) return 'free'\n if (per1M < 0.01) return `$${per1M.toFixed(4)}`\n return `$${per1M.toFixed(2)}`\n }\n return `${fmt(prompt)} / ${fmt(completion)}`\n}\n\n/** `200000` → `\"200k\"`, `1000000` → `\"1.0M\"` */\nexport function formatContextLength(tokens: number): string {\n if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`\n if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`\n return `${tokens}`\n}\n\n/** `1137` → `\"1.1s\"`, `null` → `\"N/A\"` */\nexport function formatLatency(ms: number | null): string {\n if (ms === null) return 'N/A'\n if (ms < 1000) return `${Math.round(ms)}ms`\n return `${(ms / 1000).toFixed(1)}s`\n}\n\nexport function formatThroughput(tokensPerSec: number | null): string {\n if (tokensPerSec === null) return 'N/A'\n return `${tokensPerSec.toFixed(0)} t/s`\n}\n\nexport function formatModelLabel(m: OpenRouterModel): string {\n return `${m.name || m.id} — ${formatPrice(m.pricing.prompt)} · ${formatContextLength(m.context_length)}`\n}\n\nexport function formatModelHint(m: OpenRouterModel): string {\n const parts = [`out ${formatPrice(m.pricing.completion)}`]\n if (m.pricing.input_cache_read && m.pricing.input_cache_read !== '0') {\n parts.push(`cache ${formatPrice(m.pricing.input_cache_read)}`)\n }\n return parts.join(' · ')\n}\n","import type { OpenRouterClient } from './client.js'\nimport type { ModelEndpoint, ModelEndpointsResponse } from './types.js'\n\nexport async function fetchModelEndpoints(\n client: OpenRouterClient,\n author: string,\n slug: string,\n): Promise<ModelEndpoint[]> {\n const response = await client.get<ModelEndpointsResponse>(\n `/models/${author}/${slug}/endpoints`,\n )\n return response.data.endpoints ?? []\n}\n\nexport type ProviderOption = {\n providerName: string\n /** Routing slug for `provider.only/order/ignore` (e.g. \"anthropic\", \"google-vertex/global\"). */\n tag: string\n}\n\nexport function getUniqueProviders(endpoints: ModelEndpoint[]): ProviderOption[] {\n const seen = new Set<string>()\n const result: ProviderOption[] = []\n\n for (const ep of endpoints) {\n if (seen.has(ep.tag)) continue\n seen.add(ep.tag)\n result.push({ tag: ep.tag, providerName: ep.provider_name })\n }\n\n result.sort((a, b) => a.providerName.localeCompare(b.providerName))\n return result\n}\n","import { readCache, writeCache } from './cache.js'\nimport type { OpenRouterClient } from './client.js'\nimport type { OpenRouterProvider, OpenRouterProvidersResponse } from './types.js'\n\nconst CACHE_KEY = 'providers'\nconst CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours\n\nexport async function fetchProviders(\n client: OpenRouterClient,\n): Promise<OpenRouterProvider[]> {\n const cached = readCache<OpenRouterProvider[]>(CACHE_KEY, CACHE_TTL)\n if (cached) return cached\n\n const response = await client.get<OpenRouterProvidersResponse>('/providers')\n writeCache(CACHE_KEY, response.data)\n return response.data\n}\n","import * as clack from '@clack/prompts'\nimport type { OpenRouterClient } from '../../openrouter/client.js'\nimport { fetchModelEndpoints, getUniqueProviders } from '../../openrouter/endpoints.js'\nimport { parseModelAuthor, parseModelSlug } from '../../openrouter/models.js'\nimport { fetchProviders } from '../../openrouter/providers.js'\nimport { formatLatency, formatThroughput } from './format.js'\n\nexport async function fetchProvidersForPattern(\n client: OpenRouterClient,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n const s = clack.spinner()\n s.start('Fetching providers...')\n try {\n const providers = await fetchProviders(client)\n const options = providers\n .map(p => ({ value: p.slug, label: p.name }))\n .sort((a, b) => a.label.localeCompare(b.label))\n s.stop(`${providers.length} providers available`)\n return options\n } catch (error) {\n s.stop('Failed to fetch providers')\n clack.log.error(String(error))\n return null\n }\n}\n\nexport async function fetchEndpointsForModel(\n client: OpenRouterClient,\n modelId: string,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n const author = parseModelAuthor(modelId)\n const slug = parseModelSlug(modelId)\n\n const s = clack.spinner()\n s.start('Fetching providers for this model...')\n try {\n const endpoints = await fetchModelEndpoints(client, author, slug)\n const unique = getUniqueProviders(endpoints)\n\n const options = unique.map(p => {\n const ep = endpoints.find(e => e.tag === p.tag)\n const latency = ep?.latency_last_30m?.p50 ?? null\n const throughput = ep?.throughput_last_30m?.p50 ?? null\n return {\n value: p.tag,\n label: `${p.providerName} (${p.tag})`,\n hint: `${formatLatency(latency)} · ${formatThroughput(throughput)}`,\n }\n })\n\n s.stop(`${unique.length} providers available for this model`)\n return options\n } catch (error) {\n s.stop('Failed to fetch providers')\n clack.log.error(String(error))\n return null\n }\n}\n\nexport async function fetchProvidersForModel(\n client: OpenRouterClient,\n modelKey: string,\n isPattern: boolean,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n if (isPattern) return fetchProvidersForPattern(client)\n return fetchEndpointsForModel(client, modelKey)\n}\n\nconst DONE_OPTION = '__done__'\n\nexport async function selectRoutingMode(message: string): Promise<string | symbol> {\n return clack.select({\n message,\n options: [\n { value: 'only', label: 'Use specific providers only' },\n { value: 'order', label: 'Set provider priority order' },\n { value: 'ignore', label: 'Ignore specific providers' },\n { value: 'skip', label: 'Skip provider routing' },\n ],\n })\n}\n\nexport async function selectProvidersByMode(\n mode: string,\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n if (mode === 'only') return selectOnlyProviders(providerOptions)\n if (mode === 'order') return selectOrderedProviders(providerOptions)\n if (mode === 'ignore') return selectIgnoreProviders(providerOptions)\n return null\n}\n\nasync function selectOnlyProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const selected = await clack.multiselect({\n message: 'Select providers',\n options: providerOptions,\n required: false,\n })\n\n if (clack.isCancel(selected)) return null\n\n const values = selected as string[]\n if (values.length === 0) return {}\n\n const only = values.length === 1 ? values[0] : values\n return { provider: { only } }\n}\n\nasync function selectOrderedProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const order: string[] = []\n\n for (let i = 1; ; i++) {\n const remaining = providerOptions.filter(p => !order.includes(p.value))\n if (remaining.length === 0) break\n\n const pick = await clack.select({\n message: `Select provider #${i} (or cancel to finish)`,\n options: [...remaining, { value: DONE_OPTION, label: '✓ Done' }],\n })\n\n if (clack.isCancel(pick) || pick === DONE_OPTION) break\n order.push(pick as string)\n }\n\n if (order.length === 0) {\n clack.log.warn('No providers selected')\n return null\n }\n\n const allowFallbacks = await clack.confirm({\n message: 'Allow fallbacks to other providers?',\n initialValue: true,\n })\n\n return {\n provider: {\n order: order.length === 1 ? order[0] : order,\n allowFallbacks: clack.isCancel(allowFallbacks) ? true : (allowFallbacks as boolean),\n },\n }\n}\n\nasync function selectIgnoreProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const selected = await clack.multiselect({\n message: 'Select providers to ignore',\n options: providerOptions,\n required: false,\n })\n\n if (clack.isCancel(selected)) return null\n\n const values = selected as string[]\n if (values.length === 0) return {}\n\n const ignore = values.length === 1 ? values[0] : values\n return { provider: { ignore } }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"providers.mjs","names":["CACHE_KEY","CACHE_TTL","clack.spinner","clack.select","clack.multiselect","clack.isCancel","clack.confirm"],"sources":["../src/openrouter/cache.ts","../src/openrouter/models.ts","../src/commands/config/format.ts","../src/openrouter/endpoints.ts","../src/openrouter/providers.ts","../src/commands/config/providers.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport type CacheEntry<T> = {\n data: T\n fetchedAt: number\n}\n\nexport const CACHE_DIR = join(homedir(), '.cache', 'proxitor')\n\n/** Read a cached value. Returns `null` when missing, expired (older than `ttlMs`), or unparseable. */\nexport function readCache<T>(key: string, ttlMs: number): T | null {\n const path = join(CACHE_DIR, `${key}.json`)\n if (!existsSync(path)) return null\n\n try {\n const entry: CacheEntry<T> = JSON.parse(readFileSync(path, 'utf-8'))\n if (Date.now() - entry.fetchedAt > ttlMs) return null\n return entry.data\n } catch {\n return null\n }\n}\n\nexport function writeCache<T>(key: string, data: T): void {\n mkdirSync(CACHE_DIR, { recursive: true })\n const entry: CacheEntry<T> = { fetchedAt: Date.now(), data }\n writeFileSync(join(CACHE_DIR, `${key}.json`), JSON.stringify(entry))\n}\n\nexport function clearCache(key: string): void {\n const path = join(CACHE_DIR, `${key}.json`)\n if (existsSync(path)) unlinkSync(path)\n}\n","import { readCache, writeCache } from './cache.js'\nimport type { OpenRouterDataClient } from './data-client.js'\nimport type { OpenRouterModel } from './types.js'\n\nconst CACHE_KEY = 'models'\nconst CACHE_TTL = 60 * 60 * 1000 // 1 hour\n\nexport async function fetchModels(\n client: OpenRouterDataClient,\n): Promise<OpenRouterModel[]> {\n const cached = readCache<OpenRouterModel[]>(CACHE_KEY, CACHE_TTL)\n if (cached) return cached\n\n const models = await client.fetchModels()\n writeCache(CACHE_KEY, models)\n return models\n}\n\n/** `\"anthropic/claude-sonnet-4\"` → `\"anthropic\"` */\nexport function parseModelAuthor(modelId: string): string {\n return modelId.split('/')[0] ?? ''\n}\n\n/** `\"anthropic/claude-sonnet-4\"` → `\"claude-sonnet-4\"` */\nexport function parseModelSlug(modelId: string): string {\n return modelId.split('/').slice(1).join('/')\n}\n\n/** `\"0.000003\"` → `\"$3.00\"`, `\"0\"` → `\"free\"` */\nexport function formatPrice(pricePerToken: string): string {\n const per1M = Number.parseFloat(pricePerToken) * 1_000_000\n if (per1M === 0) return 'free'\n if (per1M < 0.01) return `$${per1M.toFixed(4)}`\n return `$${per1M.toFixed(2)}`\n}\n","import { formatPrice } from '../../openrouter/models.js'\nimport type { OpenRouterModel } from '../../openrouter/types.js'\n\nexport function formatPricing(prompt: string, completion: string): string {\n const fmt = (perToken: string) => {\n const per1M = Number.parseFloat(perToken) * 1_000_000\n if (per1M === 0) return 'free'\n if (per1M < 0.01) return `$${per1M.toFixed(4)}`\n return `$${per1M.toFixed(2)}`\n }\n return `${fmt(prompt)} / ${fmt(completion)}`\n}\n\n/** `200000` → `\"200k\"`, `1000000` → `\"1.0M\"` */\nexport function formatContextLength(tokens: number): string {\n if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`\n if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`\n return `${tokens}`\n}\n\n/** `1137` → `\"1.1s\"`, `null` → `\"N/A\"` */\nexport function formatLatency(ms: number | null): string {\n if (ms === null) return 'N/A'\n if (ms < 1000) return `${Math.round(ms)}ms`\n return `${(ms / 1000).toFixed(1)}s`\n}\n\nexport function formatThroughput(tokensPerSec: number | null): string {\n if (tokensPerSec === null) return 'N/A'\n return `${tokensPerSec.toFixed(0)} t/s`\n}\n\nexport function formatModelLabel(m: OpenRouterModel): string {\n return `${m.name || m.id} — ${formatPrice(m.pricing.prompt)} · ${formatContextLength(m.context_length)}`\n}\n\nexport function formatModelHint(m: OpenRouterModel): string {\n const parts = [`out ${formatPrice(m.pricing.completion)}`]\n if (m.pricing.input_cache_read && m.pricing.input_cache_read !== '0') {\n parts.push(`cache ${formatPrice(m.pricing.input_cache_read)}`)\n }\n return parts.join(' · ')\n}\n","import type { OpenRouterDataClient } from './data-client.js'\nimport type { ModelEndpoint } from './types.js'\n\nexport async function fetchModelEndpoints(\n client: OpenRouterDataClient,\n author: string,\n slug: string,\n): Promise<ModelEndpoint[]> {\n return client.fetchModelEndpoints(author, slug)\n}\n\nexport type ProviderOption = {\n providerName: string\n /** Routing slug for `provider.only/order/ignore` (e.g. \"anthropic\", \"google-vertex/global\"). */\n tag: string\n}\n\nexport function getUniqueProviders(endpoints: ModelEndpoint[]): ProviderOption[] {\n const seen = new Set<string>()\n const result: ProviderOption[] = []\n\n for (const ep of endpoints) {\n if (seen.has(ep.tag)) continue\n seen.add(ep.tag)\n result.push({ tag: ep.tag, providerName: ep.provider_name })\n }\n\n result.sort((a, b) => a.providerName.localeCompare(b.providerName))\n return result\n}\n","import { readCache, writeCache } from './cache.js'\nimport type { OpenRouterDataClient } from './data-client.js'\nimport type { OpenRouterProvider } from './types.js'\n\nconst CACHE_KEY = 'providers'\nconst CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours\n\nexport async function fetchProviders(\n client: OpenRouterDataClient,\n): Promise<OpenRouterProvider[]> {\n const cached = readCache<OpenRouterProvider[]>(CACHE_KEY, CACHE_TTL)\n if (cached) return cached\n\n const providers = await client.fetchProviders()\n writeCache(CACHE_KEY, providers)\n return providers\n}\n","import * as clack from '@clack/prompts'\nimport type { OpenRouterDataClient } from '../../openrouter/data-client.js'\nimport { fetchModelEndpoints, getUniqueProviders } from '../../openrouter/endpoints.js'\nimport { parseModelAuthor, parseModelSlug } from '../../openrouter/models.js'\nimport { fetchProviders } from '../../openrouter/providers.js'\nimport { formatLatency, formatThroughput } from './format.js'\n\nexport async function fetchProvidersForPattern(\n client: OpenRouterDataClient,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n const s = clack.spinner()\n s.start('Fetching providers...')\n try {\n const providers = await fetchProviders(client)\n const options = providers\n .map(p => ({ value: p.slug, label: p.name }))\n .sort((a, b) => a.label.localeCompare(b.label))\n s.stop(`${providers.length} providers available`)\n return options\n } catch (error) {\n s.stop('Failed to fetch providers')\n clack.log.error(String(error))\n return null\n }\n}\n\nexport async function fetchEndpointsForModel(\n client: OpenRouterDataClient,\n modelId: string,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n const author = parseModelAuthor(modelId)\n const slug = parseModelSlug(modelId)\n\n const s = clack.spinner()\n s.start('Fetching providers for this model...')\n try {\n const endpoints = await fetchModelEndpoints(client, author, slug)\n const unique = getUniqueProviders(endpoints)\n\n const options = unique.map(p => {\n const ep = endpoints.find(e => e.tag === p.tag)\n const latency = ep?.latency_last_30m?.p50 ?? null\n const throughput = ep?.throughput_last_30m?.p50 ?? null\n return {\n value: p.tag,\n label: `${p.providerName} (${p.tag})`,\n hint: `${formatLatency(latency)} · ${formatThroughput(throughput)}`,\n }\n })\n\n s.stop(`${unique.length} providers available for this model`)\n return options\n } catch (error) {\n s.stop('Failed to fetch providers')\n clack.log.error(String(error))\n return null\n }\n}\n\nexport async function fetchProvidersForModel(\n client: OpenRouterDataClient,\n modelKey: string,\n isPattern: boolean,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n if (isPattern) return fetchProvidersForPattern(client)\n return fetchEndpointsForModel(client, modelKey)\n}\n\nconst DONE_OPTION = '__done__'\n\nexport async function selectRoutingMode(message: string): Promise<string | symbol> {\n return clack.select({\n message,\n options: [\n { value: 'only', label: 'Use specific providers only' },\n { value: 'order', label: 'Set provider priority order' },\n { value: 'ignore', label: 'Ignore specific providers' },\n { value: 'skip', label: 'Skip provider routing' },\n ],\n })\n}\n\nexport async function selectProvidersByMode(\n mode: string,\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n if (mode === 'only') return selectOnlyProviders(providerOptions)\n if (mode === 'order') return selectOrderedProviders(providerOptions)\n if (mode === 'ignore') return selectIgnoreProviders(providerOptions)\n return null\n}\n\nasync function selectOnlyProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const selected = await clack.multiselect({\n message: 'Select providers',\n options: providerOptions,\n required: false,\n })\n\n if (clack.isCancel(selected)) return null\n\n const values = selected as string[]\n if (values.length === 0) return {}\n\n const only = values.length === 1 ? values[0] : values\n return { provider: { only } }\n}\n\nasync function selectOrderedProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const order: string[] = []\n\n for (let i = 1; ; i++) {\n const remaining = providerOptions.filter(p => !order.includes(p.value))\n if (remaining.length === 0) break\n\n const pick = await clack.select({\n message: `Select provider #${i} (or cancel to finish)`,\n options: [...remaining, { value: DONE_OPTION, label: '✓ Done' }],\n })\n\n if (clack.isCancel(pick) || pick === DONE_OPTION) break\n order.push(pick as string)\n }\n\n if (order.length === 0) {\n clack.log.warn('No providers selected')\n return null\n }\n\n const allowFallbacks = await clack.confirm({\n message: 'Allow fallbacks to other providers?',\n initialValue: true,\n })\n\n return {\n provider: {\n order: order.length === 1 ? order[0] : order,\n allowFallbacks: clack.isCancel(allowFallbacks) ? true : (allowFallbacks as boolean),\n },\n }\n}\n\nasync function selectIgnoreProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const selected = await clack.multiselect({\n message: 'Select providers to ignore',\n options: providerOptions,\n required: false,\n })\n\n if (clack.isCancel(selected)) return null\n\n const values = selected as string[]\n if (values.length === 0) return {}\n\n const ignore = values.length === 1 ? values[0] : values\n return { provider: { ignore } }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proxitor",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "Lightweight proxy for routing CLI requests (claude-code, codex) to OpenRouter",
6
6
  "files": [