claudish 7.19.1 → 7.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +190 -107
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -651,7 +651,7 @@ var init_onepassword_config = __esm(() => {
|
|
|
651
651
|
});
|
|
652
652
|
|
|
653
653
|
// src/version.ts
|
|
654
|
-
var VERSION = "7.
|
|
654
|
+
var VERSION = "7.20.0";
|
|
655
655
|
|
|
656
656
|
// src/logger.ts
|
|
657
657
|
var exports_logger = {};
|
|
@@ -27650,9 +27650,6 @@ function loadConfig() {
|
|
|
27650
27650
|
if (config2.onepasswordEnvironments !== undefined) {
|
|
27651
27651
|
merged.onepasswordEnvironments = config2.onepasswordEnvironments;
|
|
27652
27652
|
}
|
|
27653
|
-
if (config2.anthropicApiBilling !== undefined) {
|
|
27654
|
-
merged.anthropicApiBilling = config2.anthropicApiBilling;
|
|
27655
|
-
}
|
|
27656
27653
|
if (config2.localProviders !== undefined) {
|
|
27657
27654
|
merged.localProviders = Array.from(new Set(config2.localProviders)).sort();
|
|
27658
27655
|
}
|
|
@@ -28326,8 +28323,8 @@ var init_provider_definitions = __esm(() => {
|
|
|
28326
28323
|
shortcuts: ["kc"],
|
|
28327
28324
|
shortestPrefix: "kc",
|
|
28328
28325
|
legacyPrefixes: [{ prefix: "kc/", stripPrefix: true }],
|
|
28329
|
-
nativeModelPatterns: [{ pattern: /^kimi-for-coding
|
|
28330
|
-
|
|
28326
|
+
nativeModelPatterns: [{ pattern: /^kimi-for-coding/i }, { pattern: /^k3(-|$)/i }],
|
|
28327
|
+
modelDiscovery: { path: "/models", format: "openai-models-list" },
|
|
28331
28328
|
isDirectApi: true,
|
|
28332
28329
|
description: "Kimi Coding Plan (kc@)"
|
|
28333
28330
|
},
|
|
@@ -30475,6 +30472,22 @@ function lookupModelForProvider(modelId, provider, cachePath) {
|
|
|
30475
30472
|
return;
|
|
30476
30473
|
return entry.aggregators?.find((a) => a.provider === provider)?.contextWindow ?? entry.contextWindow;
|
|
30477
30474
|
}
|
|
30475
|
+
function resolveSubscriptionRouting(modelId, provider, cachePath) {
|
|
30476
|
+
const entry = findCacheEntry(modelId, cachePath);
|
|
30477
|
+
if (!entry)
|
|
30478
|
+
return { kind: "unknown" };
|
|
30479
|
+
if (entry.subscriptionPlans?.includes(provider)) {
|
|
30480
|
+
const agg = entry.aggregators?.find((a) => a.provider === provider);
|
|
30481
|
+
return agg?.externalId ? { kind: "serves", externalId: agg.externalId } : { kind: "unknown" };
|
|
30482
|
+
}
|
|
30483
|
+
return isSubscriptionPlan(provider, cachePath) ? { kind: "not-served" } : { kind: "unknown" };
|
|
30484
|
+
}
|
|
30485
|
+
function isSubscriptionPlan(provider, cachePath) {
|
|
30486
|
+
const cache = readAllModelsCache(cachePath);
|
|
30487
|
+
if (!cache)
|
|
30488
|
+
return false;
|
|
30489
|
+
return cache.entries.some((e) => e.subscriptionPlans?.includes(provider));
|
|
30490
|
+
}
|
|
30478
30491
|
function findCacheEntry(modelId, cachePath) {
|
|
30479
30492
|
if (modelId.includes("@")) {
|
|
30480
30493
|
throw new Error(`model-catalog lookup received provider-routed ID "${modelId}" \u2014 callers must strip the "@" prefix before calling`);
|
|
@@ -31745,14 +31758,6 @@ var init_web_search_detector = __esm(() => {
|
|
|
31745
31758
|
WEB_SEARCH_NAMES = new Set(["web_search", "brave_web_search", "tavily_search"]);
|
|
31746
31759
|
});
|
|
31747
31760
|
|
|
31748
|
-
// src/handlers/shared/stream-parsers/message-start-usage.ts
|
|
31749
|
-
function messageStartUsage(priorInputTokens) {
|
|
31750
|
-
return {
|
|
31751
|
-
input_tokens: priorInputTokens && priorInputTokens > 0 ? priorInputTokens : 100,
|
|
31752
|
-
output_tokens: 1
|
|
31753
|
-
};
|
|
31754
|
-
}
|
|
31755
|
-
|
|
31756
31761
|
// src/handlers/shared/stream-parsers/openai-sse.ts
|
|
31757
31762
|
function validateToolArguments(toolName, argsStr, toolSchemas, textContent) {
|
|
31758
31763
|
const result = validateAndRepairToolCall(toolName, argsStr, toolSchemas, textContent);
|
|
@@ -31782,7 +31787,7 @@ function createStreamingState() {
|
|
|
31782
31787
|
accumulatedText: ""
|
|
31783
31788
|
};
|
|
31784
31789
|
}
|
|
31785
|
-
function createStreamingResponseHandler(c, response, adapter, target, middlewareManager, onTokenUpdate, toolSchemas, toolNameMap
|
|
31790
|
+
function createStreamingResponseHandler(c, response, adapter, target, middlewareManager, onTokenUpdate, toolSchemas, toolNameMap) {
|
|
31786
31791
|
log(`[Streaming] ===== HANDLER STARTED for ${target} =====`);
|
|
31787
31792
|
let isClosed = false;
|
|
31788
31793
|
let ping = null;
|
|
@@ -31811,7 +31816,7 @@ data: ${JSON.stringify(d)}
|
|
|
31811
31816
|
model: target,
|
|
31812
31817
|
stop_reason: null,
|
|
31813
31818
|
stop_sequence: null,
|
|
31814
|
-
usage:
|
|
31819
|
+
usage: { input_tokens: 100, output_tokens: 1 }
|
|
31815
31820
|
}
|
|
31816
31821
|
});
|
|
31817
31822
|
send("ping", { type: "ping" });
|
|
@@ -31924,10 +31929,7 @@ data: ${JSON.stringify(d)}
|
|
|
31924
31929
|
send("message_delta", {
|
|
31925
31930
|
type: "message_delta",
|
|
31926
31931
|
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
31927
|
-
usage: {
|
|
31928
|
-
...state.usage?.prompt_tokens ? { input_tokens: state.usage.prompt_tokens } : {},
|
|
31929
|
-
output_tokens: state.usage?.completion_tokens || 0
|
|
31930
|
-
}
|
|
31932
|
+
usage: { output_tokens: state.usage?.completion_tokens || 0 }
|
|
31931
31933
|
});
|
|
31932
31934
|
send("message_stop", { type: "message_stop" });
|
|
31933
31935
|
}
|
|
@@ -31938,7 +31940,7 @@ data: ${JSON.stringify(d)}
|
|
|
31938
31940
|
} else {
|
|
31939
31941
|
const estimatedOutputTokens = Math.ceil(state.accumulatedText.length / 4);
|
|
31940
31942
|
log(`[Streaming] No usage data from provider, estimating: ~${estimatedOutputTokens} output tokens`);
|
|
31941
|
-
onTokenUpdate(
|
|
31943
|
+
onTokenUpdate(100, estimatedOutputTokens);
|
|
31942
31944
|
}
|
|
31943
31945
|
}
|
|
31944
31946
|
if (!isClosed) {
|
|
@@ -37556,7 +37558,7 @@ data: ${JSON.stringify(data)}
|
|
|
37556
37558
|
model: opts.modelName,
|
|
37557
37559
|
stop_reason: null,
|
|
37558
37560
|
stop_sequence: null,
|
|
37559
|
-
usage:
|
|
37561
|
+
usage: { input_tokens: 100, output_tokens: 1 }
|
|
37560
37562
|
}
|
|
37561
37563
|
});
|
|
37562
37564
|
send("ping", { type: "ping" });
|
|
@@ -37600,10 +37602,7 @@ data: ${JSON.stringify(data)}
|
|
|
37600
37602
|
send("message_delta", {
|
|
37601
37603
|
type: "message_delta",
|
|
37602
37604
|
delta: { stop_reason: hasToolCalls ? "tool_use" : "end_turn", stop_sequence: null },
|
|
37603
|
-
usage: {
|
|
37604
|
-
...inputTokens > 0 ? { input_tokens: inputTokens } : {},
|
|
37605
|
-
output_tokens: outputTokens
|
|
37606
|
-
}
|
|
37605
|
+
usage: { output_tokens: outputTokens }
|
|
37607
37606
|
});
|
|
37608
37607
|
send("message_stop", { type: "message_stop" });
|
|
37609
37608
|
}
|
|
@@ -37806,7 +37805,7 @@ data: ${JSON.stringify(data)}
|
|
|
37806
37805
|
model: opts.modelName,
|
|
37807
37806
|
stop_reason: null,
|
|
37808
37807
|
stop_sequence: null,
|
|
37809
|
-
usage:
|
|
37808
|
+
usage: { input_tokens: 100, output_tokens: 1 }
|
|
37810
37809
|
}
|
|
37811
37810
|
});
|
|
37812
37811
|
send("ping", { type: "ping" });
|
|
@@ -37827,10 +37826,7 @@ data: ${JSON.stringify(data)}
|
|
|
37827
37826
|
send("message_delta", {
|
|
37828
37827
|
type: "message_delta",
|
|
37829
37828
|
delta: { stop_reason: "end_turn", stop_sequence: null },
|
|
37830
|
-
usage: {
|
|
37831
|
-
...promptTokens > 0 ? { input_tokens: promptTokens } : {},
|
|
37832
|
-
output_tokens: completionTokens
|
|
37833
|
-
}
|
|
37829
|
+
usage: { output_tokens: completionTokens }
|
|
37834
37830
|
});
|
|
37835
37831
|
send("message_stop", { type: "message_stop" });
|
|
37836
37832
|
}
|
|
@@ -37982,7 +37978,7 @@ data: ${JSON.stringify(data)}
|
|
|
37982
37978
|
model: opts.modelName,
|
|
37983
37979
|
stop_reason: null,
|
|
37984
37980
|
stop_sequence: null,
|
|
37985
|
-
usage:
|
|
37981
|
+
usage: { input_tokens: 100, output_tokens: 1 }
|
|
37986
37982
|
}
|
|
37987
37983
|
});
|
|
37988
37984
|
send("ping", { type: "ping" });
|
|
@@ -38272,7 +38268,6 @@ class TokenTracker {
|
|
|
38272
38268
|
sessionTotalCost = 0;
|
|
38273
38269
|
sessionInputTokens = 0;
|
|
38274
38270
|
sessionOutputTokens = 0;
|
|
38275
|
-
lastInputTokens = 0;
|
|
38276
38271
|
modelNameOverride;
|
|
38277
38272
|
quotaRemaining;
|
|
38278
38273
|
constructor(port, config2) {
|
|
@@ -38289,11 +38284,10 @@ class TokenTracker {
|
|
|
38289
38284
|
this.quotaRemaining = fraction;
|
|
38290
38285
|
}
|
|
38291
38286
|
rewrite() {
|
|
38292
|
-
this.writeFile(this.
|
|
38287
|
+
this.writeFile(this.sessionInputTokens, this.sessionOutputTokens);
|
|
38293
38288
|
}
|
|
38294
38289
|
update(inputTokens, outputTokens) {
|
|
38295
38290
|
this.sessionInputTokens = inputTokens;
|
|
38296
|
-
this.lastInputTokens = inputTokens;
|
|
38297
38291
|
this.sessionOutputTokens += outputTokens;
|
|
38298
38292
|
const pricing = this.getPricing();
|
|
38299
38293
|
const cost = inputTokens / 1e6 * pricing.inputCostPer1M + outputTokens / 1e6 * pricing.outputCostPer1M;
|
|
@@ -38302,7 +38296,6 @@ class TokenTracker {
|
|
|
38302
38296
|
}
|
|
38303
38297
|
accumulateBoth(inputTokens, outputTokens) {
|
|
38304
38298
|
this.sessionInputTokens += inputTokens;
|
|
38305
|
-
this.lastInputTokens = this.sessionInputTokens;
|
|
38306
38299
|
this.sessionOutputTokens += outputTokens;
|
|
38307
38300
|
const pricing = this.getPricing();
|
|
38308
38301
|
const cost = this.sessionInputTokens / 1e6 * pricing.inputCostPer1M + this.sessionOutputTokens / 1e6 * pricing.outputCostPer1M;
|
|
@@ -38311,7 +38304,6 @@ class TokenTracker {
|
|
|
38311
38304
|
}
|
|
38312
38305
|
updateWithDelta(inputTokens, outputTokens) {
|
|
38313
38306
|
let incrementalInputTokens;
|
|
38314
|
-
this.lastInputTokens = inputTokens;
|
|
38315
38307
|
if (inputTokens >= this.sessionInputTokens) {
|
|
38316
38308
|
incrementalInputTokens = inputTokens - this.sessionInputTokens;
|
|
38317
38309
|
this.sessionInputTokens = inputTokens;
|
|
@@ -38327,11 +38319,10 @@ class TokenTracker {
|
|
|
38327
38319
|
const pricing = this.getPricing();
|
|
38328
38320
|
const cost = incrementalInputTokens / 1e6 * pricing.inputCostPer1M + outputTokens / 1e6 * pricing.outputCostPer1M;
|
|
38329
38321
|
this.sessionTotalCost += cost;
|
|
38330
|
-
this.writeFile(inputTokens, this.sessionOutputTokens, pricing.isEstimate);
|
|
38322
|
+
this.writeFile(Math.max(inputTokens, this.sessionInputTokens), this.sessionOutputTokens, pricing.isEstimate);
|
|
38331
38323
|
}
|
|
38332
38324
|
updateWithActualCost(inputTokens, outputTokens, actualCost) {
|
|
38333
38325
|
this.sessionInputTokens = inputTokens;
|
|
38334
|
-
this.lastInputTokens = inputTokens;
|
|
38335
38326
|
this.sessionOutputTokens += outputTokens;
|
|
38336
38327
|
if (typeof actualCost === "number" && actualCost > 0) {
|
|
38337
38328
|
this.sessionTotalCost += actualCost;
|
|
@@ -38347,7 +38338,6 @@ class TokenTracker {
|
|
|
38347
38338
|
updateLocal(inputTokens, outputTokens) {
|
|
38348
38339
|
if (inputTokens > 0) {
|
|
38349
38340
|
this.sessionInputTokens = inputTokens;
|
|
38350
|
-
this.lastInputTokens = inputTokens;
|
|
38351
38341
|
}
|
|
38352
38342
|
this.sessionOutputTokens += outputTokens;
|
|
38353
38343
|
this.writeFile(this.sessionInputTokens, this.sessionOutputTokens);
|
|
@@ -38361,9 +38351,6 @@ class TokenTracker {
|
|
|
38361
38351
|
getInputTokens() {
|
|
38362
38352
|
return this.sessionInputTokens;
|
|
38363
38353
|
}
|
|
38364
|
-
getLastInputTokens() {
|
|
38365
|
-
return this.lastInputTokens || this.sessionInputTokens;
|
|
38366
|
-
}
|
|
38367
38354
|
getOutputTokens() {
|
|
38368
38355
|
return this.sessionOutputTokens;
|
|
38369
38356
|
}
|
|
@@ -38905,18 +38892,16 @@ class ComposedHandler {
|
|
|
38905
38892
|
}
|
|
38906
38893
|
};
|
|
38907
38894
|
const streamFormat = this.provider.overrideStreamFormat?.() ?? this.explicitAdapter?.getStreamFormat() ?? this.modelAdapter?.getStreamFormat() ?? this.getAdapter().getStreamFormat();
|
|
38908
|
-
const priorInputTokens = this.tokenTracker.getLastInputTokens();
|
|
38909
38895
|
switch (streamFormat) {
|
|
38910
38896
|
case "openai-sse":
|
|
38911
|
-
return createStreamingResponseHandler(c, response, adapter, this.bareModelName, this.middlewareManager, onTokenUpdate, claudeRequest.tools, toolNameMap
|
|
38897
|
+
return createStreamingResponseHandler(c, response, adapter, this.bareModelName, this.middlewareManager, onTokenUpdate, claudeRequest.tools, toolNameMap);
|
|
38912
38898
|
case "openai-responses-sse":
|
|
38913
38899
|
return createResponsesStreamHandler(c, response, {
|
|
38914
38900
|
modelName: this.bareModelName,
|
|
38915
38901
|
onTokenUpdate,
|
|
38916
38902
|
toolNameMap: adapter.getToolNameMap(),
|
|
38917
38903
|
contextWindow: lookupModelForProvider(this.bareModelName, this.provider.name),
|
|
38918
|
-
onApiError
|
|
38919
|
-
priorInputTokens
|
|
38904
|
+
onApiError
|
|
38920
38905
|
});
|
|
38921
38906
|
case "anthropic-sse":
|
|
38922
38907
|
return createAnthropicPassthroughStream(c, response, {
|
|
@@ -38936,15 +38921,13 @@ class ComposedHandler {
|
|
|
38936
38921
|
middlewareManager: this.middlewareManager,
|
|
38937
38922
|
onTokenUpdate,
|
|
38938
38923
|
onToolCall,
|
|
38939
|
-
unwrapResponse: this.options.unwrapGeminiResponse
|
|
38940
|
-
priorInputTokens
|
|
38924
|
+
unwrapResponse: this.options.unwrapGeminiResponse
|
|
38941
38925
|
});
|
|
38942
38926
|
}
|
|
38943
38927
|
case "ollama-jsonl":
|
|
38944
38928
|
return createOllamaJsonlStream(c, response, {
|
|
38945
38929
|
modelName: this.bareModelName,
|
|
38946
|
-
onTokenUpdate
|
|
38947
|
-
priorInputTokens
|
|
38930
|
+
onTokenUpdate
|
|
38948
38931
|
});
|
|
38949
38932
|
default:
|
|
38950
38933
|
throw new Error(`Unknown stream format: ${streamFormat}`);
|
|
@@ -42584,7 +42567,8 @@ var init_default_routing_rules = __esm(() => {
|
|
|
42584
42567
|
"o3-*": ["openai-codex", "openai", "openrouter"],
|
|
42585
42568
|
"gemini-*": ["gemini-codeassist", "google", "openrouter"],
|
|
42586
42569
|
"grok-*": ["x-ai", "openrouter"],
|
|
42587
|
-
"kimi-*": ["kimi-coding
|
|
42570
|
+
"kimi-*": ["kimi-coding", "kimi", "openrouter"],
|
|
42571
|
+
"k3*": ["kimi-coding", "kimi", "openrouter"],
|
|
42588
42572
|
"minimax-*": ["minimax-coding", "minimax", "openrouter"],
|
|
42589
42573
|
"glm-*": ["glm-coding", "glm", "openrouter"],
|
|
42590
42574
|
"z-ai-*": ["z-ai", "openrouter"],
|
|
@@ -42638,7 +42622,7 @@ function matchRoutingRule(modelName, rules) {
|
|
|
42638
42622
|
return rules["*"];
|
|
42639
42623
|
return null;
|
|
42640
42624
|
}
|
|
42641
|
-
function buildRoutingChain(entries, originalModelName) {
|
|
42625
|
+
function buildRoutingChain(entries, originalModelName, cachePath) {
|
|
42642
42626
|
const routes = [];
|
|
42643
42627
|
for (const entry of entries) {
|
|
42644
42628
|
const atIdx = entry.indexOf("@");
|
|
@@ -42652,6 +42636,13 @@ function buildRoutingChain(entries, originalModelName) {
|
|
|
42652
42636
|
modelName = originalModelName;
|
|
42653
42637
|
}
|
|
42654
42638
|
const provider = PROVIDER_SHORTCUTS[providerRaw.toLowerCase()] ?? providerRaw.toLowerCase();
|
|
42639
|
+
if (atIdx === -1) {
|
|
42640
|
+
const routing = resolveSubscriptionRouting(modelName, provider, cachePath);
|
|
42641
|
+
if (routing.kind === "not-served")
|
|
42642
|
+
continue;
|
|
42643
|
+
if (routing.kind === "serves")
|
|
42644
|
+
modelName = routing.externalId;
|
|
42645
|
+
}
|
|
42655
42646
|
let modelSpec;
|
|
42656
42647
|
if (provider === "openrouter") {
|
|
42657
42648
|
const resolution = resolveModelNameSync(modelName, "openrouter");
|
|
@@ -42678,7 +42669,7 @@ function globMatch(pattern, value) {
|
|
|
42678
42669
|
async function hasCredentialsForProvider(provider) {
|
|
42679
42670
|
return credentials.isAvailable(provider);
|
|
42680
42671
|
}
|
|
42681
|
-
async function routeExplicit(modelSpec, model, provider) {
|
|
42672
|
+
async function routeExplicit(modelSpec, model, provider, cachePath) {
|
|
42682
42673
|
if (!await hasCredentialsForProvider(provider)) {
|
|
42683
42674
|
return {
|
|
42684
42675
|
kind: "no-route",
|
|
@@ -42686,7 +42677,7 @@ async function routeExplicit(modelSpec, model, provider) {
|
|
|
42686
42677
|
hint: buildCredentialHint(model, [provider]) ?? undefined
|
|
42687
42678
|
};
|
|
42688
42679
|
}
|
|
42689
|
-
const built = buildRoutingChain([modelSpec], model)[0];
|
|
42680
|
+
const built = buildRoutingChain([modelSpec], model, cachePath)[0];
|
|
42690
42681
|
if (!built) {
|
|
42691
42682
|
return {
|
|
42692
42683
|
kind: "no-route",
|
|
@@ -42695,7 +42686,7 @@ async function routeExplicit(modelSpec, model, provider) {
|
|
|
42695
42686
|
}
|
|
42696
42687
|
return { kind: "ok", primary: built, fallbacks: [] };
|
|
42697
42688
|
}
|
|
42698
|
-
async function routeBare(model, nativeProvider, rules, defaultProvider) {
|
|
42689
|
+
async function routeBare(model, nativeProvider, rules, defaultProvider, cachePath) {
|
|
42699
42690
|
const matched = matchRoutingRule(model, rules) ?? [];
|
|
42700
42691
|
const entries = [...matched];
|
|
42701
42692
|
if (defaultProvider && defaultProvider.length > 0) {
|
|
@@ -42716,7 +42707,7 @@ async function routeBare(model, nativeProvider, rules, defaultProvider) {
|
|
|
42716
42707
|
hint: buildCredentialHint(model, [nativeProvider]) ?? undefined
|
|
42717
42708
|
};
|
|
42718
42709
|
}
|
|
42719
|
-
const candidates = buildRoutingChain(entries, model);
|
|
42710
|
+
const candidates = buildRoutingChain(entries, model, cachePath);
|
|
42720
42711
|
const credentialed = [];
|
|
42721
42712
|
const skipped = [];
|
|
42722
42713
|
const checks4 = await Promise.all(candidates.map((candidate) => hasCredentialsForProvider(candidate.provider)));
|
|
@@ -42737,16 +42728,17 @@ async function routeBare(model, nativeProvider, rules, defaultProvider) {
|
|
|
42737
42728
|
const [primary, ...fallbacks] = credentialed;
|
|
42738
42729
|
return { kind: "ok", primary, fallbacks };
|
|
42739
42730
|
}
|
|
42740
|
-
async function route(modelSpec, rulesOverride, defaultProviderOverride) {
|
|
42731
|
+
async function route(modelSpec, rulesOverride, defaultProviderOverride, cachePath) {
|
|
42741
42732
|
const parsed = parseModelSpec(modelSpec);
|
|
42742
42733
|
if (parsed.isExplicitProvider) {
|
|
42743
|
-
return routeExplicit(modelSpec, parsed.model, parsed.provider);
|
|
42734
|
+
return routeExplicit(modelSpec, parsed.model, parsed.provider, cachePath);
|
|
42744
42735
|
}
|
|
42745
42736
|
const rules = rulesOverride ?? loadRoutingRules();
|
|
42746
42737
|
const defaultProvider = defaultProviderOverride !== undefined ? defaultProviderOverride : rulesOverride !== undefined ? undefined : loadConfig().defaultProvider;
|
|
42747
|
-
return routeBare(parsed.model, parsed.provider, rules, defaultProvider);
|
|
42738
|
+
return routeBare(parsed.model, parsed.provider, rules, defaultProvider, cachePath);
|
|
42748
42739
|
}
|
|
42749
42740
|
var init_routing_rules = __esm(() => {
|
|
42741
|
+
init_model_catalog();
|
|
42750
42742
|
init_authority();
|
|
42751
42743
|
init_profile_config();
|
|
42752
42744
|
init_auto_route();
|
|
@@ -58419,8 +58411,7 @@ var init_config = __esm(() => {
|
|
|
58419
58411
|
OPENAI_BASE_URL: "OPENAI_BASE_URL",
|
|
58420
58412
|
CLAUDISH_SUMMARIZE_TOOLS: "CLAUDISH_SUMMARIZE_TOOLS",
|
|
58421
58413
|
CLAUDISH_DIAG_MODE: "CLAUDISH_DIAG_MODE",
|
|
58422
|
-
CLAUDISH_DEBUG: "CLAUDISH_DEBUG"
|
|
58423
|
-
CLAUDISH_ANTHROPIC_API_BILLING: "CLAUDISH_ANTHROPIC_API_BILLING"
|
|
58414
|
+
CLAUDISH_DEBUG: "CLAUDISH_DEBUG"
|
|
58424
58415
|
};
|
|
58425
58416
|
OPENROUTER_HEADERS = {
|
|
58426
58417
|
"HTTP-Referer": "https://claudish.com",
|
|
@@ -58577,6 +58568,114 @@ var init_model_catalog2 = __esm(() => {
|
|
|
58577
58568
|
NO_CATALOG_VENDOR_SLUGS = new Set(["litellm", "ollama", "lmstudio", "lm-studio"]);
|
|
58578
58569
|
});
|
|
58579
58570
|
|
|
58571
|
+
// src/providers/model-discovery.ts
|
|
58572
|
+
function resolveBaseUrl2(catalogName) {
|
|
58573
|
+
const def = getProviderByName(catalogName);
|
|
58574
|
+
if (!def)
|
|
58575
|
+
return null;
|
|
58576
|
+
for (const envVar of def.baseUrlEnvVars ?? []) {
|
|
58577
|
+
const v = process.env[envVar];
|
|
58578
|
+
if (v)
|
|
58579
|
+
return v.replace(/\/+$/, "");
|
|
58580
|
+
}
|
|
58581
|
+
return (def.baseUrl || "").replace(/\/+$/, "") || null;
|
|
58582
|
+
}
|
|
58583
|
+
function readContextWindow(row) {
|
|
58584
|
+
for (const field of ["context_length", "context_window", "max_context_length"]) {
|
|
58585
|
+
const v = row[field];
|
|
58586
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0)
|
|
58587
|
+
return v;
|
|
58588
|
+
}
|
|
58589
|
+
return;
|
|
58590
|
+
}
|
|
58591
|
+
function parseOpenAIModelsList(body) {
|
|
58592
|
+
const data = body?.data;
|
|
58593
|
+
if (!Array.isArray(data))
|
|
58594
|
+
return [];
|
|
58595
|
+
const models = [];
|
|
58596
|
+
for (const raw2 of data) {
|
|
58597
|
+
if (!raw2 || typeof raw2 !== "object")
|
|
58598
|
+
continue;
|
|
58599
|
+
const row = raw2;
|
|
58600
|
+
const id = row.id;
|
|
58601
|
+
if (typeof id !== "string" || id.trim().length === 0)
|
|
58602
|
+
continue;
|
|
58603
|
+
const displayName = typeof row.display_name === "string" ? row.display_name : undefined;
|
|
58604
|
+
models.push({ id, displayName, contextWindow: readContextWindow(row) });
|
|
58605
|
+
}
|
|
58606
|
+
return models;
|
|
58607
|
+
}
|
|
58608
|
+
async function discoverProviderModels(providerName) {
|
|
58609
|
+
const cached2 = _cache2.get(providerName);
|
|
58610
|
+
if (cached2 && cached2.expiresAt > Date.now())
|
|
58611
|
+
return cached2.models;
|
|
58612
|
+
const def = getProviderByName(providerName);
|
|
58613
|
+
const descriptor = def?.modelDiscovery;
|
|
58614
|
+
if (!def || !descriptor)
|
|
58615
|
+
return [];
|
|
58616
|
+
const baseUrl = resolveBaseUrl2(providerName);
|
|
58617
|
+
if (!baseUrl)
|
|
58618
|
+
return [];
|
|
58619
|
+
const endpoint = `${baseUrl}${descriptor.path}`;
|
|
58620
|
+
let headers = {};
|
|
58621
|
+
try {
|
|
58622
|
+
const auth = await credentials.getRequestAuth(providerName, { model: "" });
|
|
58623
|
+
headers = { ...auth.headers };
|
|
58624
|
+
} catch (e) {
|
|
58625
|
+
log(`[model-discovery:${providerName}] no credentials, skipping: ${e?.message}`);
|
|
58626
|
+
return [];
|
|
58627
|
+
}
|
|
58628
|
+
let response;
|
|
58629
|
+
try {
|
|
58630
|
+
response = await fetch(endpoint, {
|
|
58631
|
+
method: "GET",
|
|
58632
|
+
headers,
|
|
58633
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS2)
|
|
58634
|
+
});
|
|
58635
|
+
} catch (e) {
|
|
58636
|
+
log(`[model-discovery:${providerName}] fetch failed: ${e?.message}`);
|
|
58637
|
+
return [];
|
|
58638
|
+
}
|
|
58639
|
+
if (!response.ok) {
|
|
58640
|
+
log(`[model-discovery:${providerName}] HTTP ${response.status} from ${endpoint}`);
|
|
58641
|
+
return [];
|
|
58642
|
+
}
|
|
58643
|
+
let body;
|
|
58644
|
+
try {
|
|
58645
|
+
body = await response.json();
|
|
58646
|
+
} catch {
|
|
58647
|
+
log(`[model-discovery:${providerName}] response was not JSON`);
|
|
58648
|
+
return [];
|
|
58649
|
+
}
|
|
58650
|
+
const models = parseOpenAIModelsList(body);
|
|
58651
|
+
if (models.length === 0) {
|
|
58652
|
+
log(`[model-discovery:${providerName}] endpoint reachable but listed no models`);
|
|
58653
|
+
return [];
|
|
58654
|
+
}
|
|
58655
|
+
log(`[model-discovery:${providerName}] discovered ${models.length} models: ` + models.map((m) => `${m.id}(${m.contextWindow ?? "?"})`).join(", "));
|
|
58656
|
+
_cache2.set(providerName, { models, expiresAt: Date.now() + CACHE_TTL_MS3 });
|
|
58657
|
+
return models;
|
|
58658
|
+
}
|
|
58659
|
+
async function discoverContextWindow(providerName, modelId) {
|
|
58660
|
+
const models = await discoverProviderModels(providerName);
|
|
58661
|
+
const match2 = models.find((m) => m.id.toLowerCase() === modelId.toLowerCase());
|
|
58662
|
+
return match2?.contextWindow;
|
|
58663
|
+
}
|
|
58664
|
+
function rankDiscoveredModels(models) {
|
|
58665
|
+
return [...models].sort((a, b) => {
|
|
58666
|
+
const diff = (b.contextWindow ?? 0) - (a.contextWindow ?? 0);
|
|
58667
|
+
return diff !== 0 ? diff : a.id.localeCompare(b.id);
|
|
58668
|
+
});
|
|
58669
|
+
}
|
|
58670
|
+
var CACHE_TTL_MS3, FETCH_TIMEOUT_MS2 = 5000, _cache2;
|
|
58671
|
+
var init_model_discovery = __esm(() => {
|
|
58672
|
+
init_authority();
|
|
58673
|
+
init_logger();
|
|
58674
|
+
init_provider_definitions();
|
|
58675
|
+
CACHE_TTL_MS3 = 5 * 60 * 1000;
|
|
58676
|
+
_cache2 = new Map;
|
|
58677
|
+
});
|
|
58678
|
+
|
|
58580
58679
|
// src/providers/ollama-discovery.ts
|
|
58581
58680
|
function ollamaBaseUrl() {
|
|
58582
58681
|
return process.env.OLLAMA_HOST || process.env.OLLAMA_BASE_URL || "http://localhost:11434";
|
|
@@ -59146,8 +59245,22 @@ async function selectModelFromProvider(provider, tierName, recommendedModels, _f
|
|
|
59146
59245
|
const prefix = PROVIDER_MODEL_PREFIX[provider] || `${provider}@`;
|
|
59147
59246
|
const displayName = getPickerDisplayName(provider);
|
|
59148
59247
|
const def = getProviderByName(provider);
|
|
59149
|
-
if (def?.
|
|
59150
|
-
|
|
59248
|
+
if (def?.modelDiscovery) {
|
|
59249
|
+
const discovered = rankDiscoveredModels(await discoverProviderModels(provider));
|
|
59250
|
+
if (discovered.length > 0) {
|
|
59251
|
+
const discoveredModels = discovered.map((m) => ({
|
|
59252
|
+
id: m.id,
|
|
59253
|
+
name: m.displayName || m.id,
|
|
59254
|
+
description: m.contextWindow ? `${m.displayName ?? m.id} \xB7 ${Math.round(m.contextWindow / 1024)}K context` : m.displayName ?? m.id,
|
|
59255
|
+
provider: displayName,
|
|
59256
|
+
supportsTools: true,
|
|
59257
|
+
isFree: true,
|
|
59258
|
+
source: displayName
|
|
59259
|
+
}));
|
|
59260
|
+
const picked = await pickModelFromList(provider, displayName, tierName, discoveredModels);
|
|
59261
|
+
if (picked)
|
|
59262
|
+
return picked;
|
|
59263
|
+
}
|
|
59151
59264
|
}
|
|
59152
59265
|
if (provider === "ollama") {
|
|
59153
59266
|
const ollamaModels = await fetchOllamaModels2({ enrichCapabilities: false });
|
|
@@ -59326,6 +59439,7 @@ var init_model_selector = __esm(() => {
|
|
|
59326
59439
|
init_authority();
|
|
59327
59440
|
init_model_loader();
|
|
59328
59441
|
init_model_catalog2();
|
|
59442
|
+
init_model_discovery();
|
|
59329
59443
|
init_provider_definitions();
|
|
59330
59444
|
pickerProviderToFirebaseSlug = {
|
|
59331
59445
|
openrouter: "openrouter",
|
|
@@ -62377,8 +62491,6 @@ async function parseArgs(args) {
|
|
|
62377
62491
|
process.exit(1);
|
|
62378
62492
|
}
|
|
62379
62493
|
config3.defaultProvider = dpArg;
|
|
62380
|
-
} else if (arg === "--anthropic-api-billing") {
|
|
62381
|
-
config3.anthropicApiBilling = true;
|
|
62382
62494
|
} else if (arg === "--op-env" || arg.startsWith("--op-env=")) {
|
|
62383
62495
|
const v = arg.startsWith("--op-env=") ? arg.slice("--op-env=".length) : args[++i];
|
|
62384
62496
|
if (!v) {
|
|
@@ -63428,10 +63540,6 @@ ${h("OPTIONS")}
|
|
|
63428
63540
|
${green("--profile")} ${yellow("<name>")} Use named profile for model mapping (default profile if omitted)
|
|
63429
63541
|
${green("--default-provider")} ${yellow("<name>")} Fallback provider for bare model names (builtin or customEndpoints key)
|
|
63430
63542
|
${dim("Precedence: this flag > CLAUDISH_DEFAULT_PROVIDER env > config.json")}
|
|
63431
|
-
${green("--anthropic-api-billing")} Use your real ANTHROPIC_API_KEY for native Claude models
|
|
63432
|
-
${dim("(metered API billing). Default: the key is hidden so Claude Code")}
|
|
63433
|
-
${dim("uses your claude.ai subscription. Env: CLAUDISH_ANTHROPIC_API_BILLING")}
|
|
63434
|
-
${dim("Config: anthropicApiBilling: true")}
|
|
63435
63543
|
${green("--config")} ${yellow("<file>")} Use THIS config file for the run, fully replacing the machine
|
|
63436
63544
|
${dim("global (~/.claudish/config.json) AND project (.claudish.json).")}
|
|
63437
63545
|
${dim("A file naming no op:// source never touches 1Password (no prompt).")}
|
|
@@ -63607,7 +63715,6 @@ ${h("ENVIRONMENT VARIABLES")}
|
|
|
63607
63715
|
${blue("CLAUDISH_CONTEXT_WINDOW")} Override context window size
|
|
63608
63716
|
${blue("CLAUDISH_DIAG_MODE")} Diagnostic output: auto / logfile / off
|
|
63609
63717
|
${blue("CLAUDISH_DEBUG")} Always enable debug logging: 1 / true ${dim("(same as -d)")}
|
|
63610
|
-
${blue("CLAUDISH_ANTHROPIC_API_BILLING")} Bill native Claude to your API key ${dim("(see --anthropic-api-billing)")}
|
|
63611
63718
|
${blue("CLAUDISH_MCP_TOOLS")} MCP tool gating: all / low-level / agentic / channel
|
|
63612
63719
|
${blue("CLAUDISH_MODEL_OPUS")} Override model for Opus role
|
|
63613
63720
|
${blue("CLAUDISH_MODEL_SONNET")} Override model for Sonnet role
|
|
@@ -64733,7 +64840,7 @@ function writeProbeModelsCache(data, path2 = PROBE_MODELS_CACHE_PATH) {
|
|
|
64733
64840
|
mkdirSync14(dirname7(path2), { recursive: true });
|
|
64734
64841
|
writeFileSync16(path2, JSON.stringify(data), "utf-8");
|
|
64735
64842
|
}
|
|
64736
|
-
function isCacheFresh(data, ttlMs =
|
|
64843
|
+
function isCacheFresh(data, ttlMs = CACHE_TTL_MS4) {
|
|
64737
64844
|
if (!data?.generatedAt)
|
|
64738
64845
|
return false;
|
|
64739
64846
|
const generatedMs = Date.parse(data.generatedAt);
|
|
@@ -64741,7 +64848,7 @@ function isCacheFresh(data, ttlMs = CACHE_TTL_MS3) {
|
|
|
64741
64848
|
return false;
|
|
64742
64849
|
return Date.now() - generatedMs < ttlMs;
|
|
64743
64850
|
}
|
|
64744
|
-
async function fetchProbeModels(url2 = PROBE_MODELS_URL, timeoutMs =
|
|
64851
|
+
async function fetchProbeModels(url2 = PROBE_MODELS_URL, timeoutMs = FETCH_TIMEOUT_MS3) {
|
|
64745
64852
|
let response;
|
|
64746
64853
|
try {
|
|
64747
64854
|
response = await fetch(url2, { signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -64847,9 +64954,9 @@ function isValidResponse(raw2) {
|
|
|
64847
64954
|
return false;
|
|
64848
64955
|
return true;
|
|
64849
64956
|
}
|
|
64850
|
-
var PROBE_MODELS_URL = "https://us-central1-claudish-6da10.cloudfunctions.net/probeModels",
|
|
64957
|
+
var PROBE_MODELS_URL = "https://us-central1-claudish-6da10.cloudfunctions.net/probeModels", CACHE_TTL_MS4, FETCH_TIMEOUT_MS3 = 15000, PROBE_MODELS_CACHE_PATH, _inFlight = null;
|
|
64851
64958
|
var init_probe_catalog = __esm(() => {
|
|
64852
|
-
|
|
64959
|
+
CACHE_TTL_MS4 = 60 * 60 * 1000;
|
|
64853
64960
|
PROBE_MODELS_CACHE_PATH = join24(homedir23(), ".claudish", "probe-models.json");
|
|
64854
64961
|
});
|
|
64855
64962
|
|
|
@@ -71177,8 +71284,7 @@ __export(exports_claude_runner, {
|
|
|
71177
71284
|
isProxyAuthMode: () => isProxyAuthMode,
|
|
71178
71285
|
computeMainThreadContextWindow: () => computeMainThreadContextWindow,
|
|
71179
71286
|
checkClaudeInstalled: () => checkClaudeInstalled,
|
|
71180
|
-
buildClaudishSettingsOverlay: () => buildClaudishSettingsOverlay
|
|
71181
|
-
MIN_AUTO_COMPACT_WINDOW: () => MIN_AUTO_COMPACT_WINDOW
|
|
71287
|
+
buildClaudishSettingsOverlay: () => buildClaudishSettingsOverlay
|
|
71182
71288
|
});
|
|
71183
71289
|
import { spawn as spawn4 } from "child_process";
|
|
71184
71290
|
import {
|
|
@@ -71209,18 +71315,6 @@ function hasNativeAnthropicMapping(config3) {
|
|
|
71209
71315
|
];
|
|
71210
71316
|
return models.some((m) => m && parseModelSpec(m).provider === "native-anthropic");
|
|
71211
71317
|
}
|
|
71212
|
-
function wantsAnthropicApiBilling(config3) {
|
|
71213
|
-
if (config3.anthropicApiBilling)
|
|
71214
|
-
return true;
|
|
71215
|
-
const raw2 = process.env[ENV.CLAUDISH_ANTHROPIC_API_BILLING];
|
|
71216
|
-
if (raw2 !== undefined && raw2 !== "" && raw2 !== "0" && raw2.toLowerCase() !== "false")
|
|
71217
|
-
return true;
|
|
71218
|
-
try {
|
|
71219
|
-
return loadConfig().anthropicApiBilling === true;
|
|
71220
|
-
} catch {
|
|
71221
|
-
return false;
|
|
71222
|
-
}
|
|
71223
|
-
}
|
|
71224
71318
|
function isProxyAuthMode(config3) {
|
|
71225
71319
|
return !config3.monitor && !hasNativeAnthropicMapping(config3);
|
|
71226
71320
|
}
|
|
@@ -71428,7 +71522,7 @@ async function computeMainThreadContextWindow(config3, cachePath) {
|
|
|
71428
71522
|
continue;
|
|
71429
71523
|
provider = plan.primary.provider;
|
|
71430
71524
|
}
|
|
71431
|
-
const win = lookupModelForProvider(parsed.model, provider, cachePath);
|
|
71525
|
+
const win = await discoverContextWindow(provider, parsed.model) ?? lookupModelForProvider(parsed.model, provider, cachePath);
|
|
71432
71526
|
if (typeof win === "number" && win > 0) {
|
|
71433
71527
|
min = Math.min(min, win);
|
|
71434
71528
|
}
|
|
@@ -71484,7 +71578,6 @@ async function runClaudeWithProxy(config3, proxyUrl, onCleanup) {
|
|
|
71484
71578
|
[ENV.CLAUDISH_ACTIVE_MODEL_NAME]: modelDisplayName,
|
|
71485
71579
|
CLAUDISH_IS_LOCAL: isLocalModel2 ? "true" : "false"
|
|
71486
71580
|
};
|
|
71487
|
-
let hidAnthropicApiKey = false;
|
|
71488
71581
|
delete env.CLAUDECODE;
|
|
71489
71582
|
if (config3.monitor) {
|
|
71490
71583
|
delete env.ANTHROPIC_API_KEY;
|
|
@@ -71498,23 +71591,16 @@ async function runClaudeWithProxy(config3, proxyUrl, onCleanup) {
|
|
|
71498
71591
|
env[ENV.ANTHROPIC_MODEL] = modelId;
|
|
71499
71592
|
env[ENV.ANTHROPIC_SMALL_FAST_MODEL] = modelId;
|
|
71500
71593
|
}
|
|
71501
|
-
if (hasNativeAnthropicMapping(config3)) {
|
|
71502
|
-
|
|
71503
|
-
|
|
71504
|
-
hidAnthropicApiKey = true;
|
|
71505
|
-
}
|
|
71506
|
-
} else {
|
|
71507
|
-
env.ANTHROPIC_API_KEY = "sk-ant-api03-placeholder-not-used-proxy-handles-auth-with-openrouter-key-xxxxxxxxxxxxxxxxxxxxx";
|
|
71508
|
-
env.ANTHROPIC_AUTH_TOKEN = "placeholder-token-not-used-proxy-handles-auth";
|
|
71594
|
+
if (hasNativeAnthropicMapping(config3)) {} else {
|
|
71595
|
+
env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "sk-ant-api03-placeholder-not-used-proxy-handles-auth-with-openrouter-key-xxxxxxxxxxxxxxxxxxxxx";
|
|
71596
|
+
env.ANTHROPIC_AUTH_TOKEN = process.env.ANTHROPIC_AUTH_TOKEN || "placeholder-token-not-used-proxy-handles-auth";
|
|
71509
71597
|
if (!process.env[ENV.CLAUDE_CODE_AUTO_COMPACT_WINDOW]) {
|
|
71510
71598
|
const autoCompactWindow = await computeMainThreadContextWindow(config3);
|
|
71511
|
-
if (autoCompactWindow
|
|
71599
|
+
if (autoCompactWindow > 0) {
|
|
71512
71600
|
env[ENV.CLAUDE_CODE_AUTO_COMPACT_WINDOW] = String(autoCompactWindow);
|
|
71513
71601
|
if (!config3.quiet) {
|
|
71514
71602
|
console.error(`[claudish] Auto-compact window: ${autoCompactWindow.toLocaleString()} tokens ` + "(Claude Code compacts before the backend's real limit)");
|
|
71515
71603
|
}
|
|
71516
|
-
} else if (autoCompactWindow > 0 && !config3.quiet) {
|
|
71517
|
-
console.error(`[claudish] Model's real context window (${autoCompactWindow.toLocaleString()}) is below ` + `Claude Code's ${MIN_AUTO_COMPACT_WINDOW.toLocaleString()}-token auto-compact floor \u2014 ` + "leaving CLAUDE_CODE_AUTO_COMPACT_WINDOW unset so native auto-compaction stays on.");
|
|
71518
71604
|
}
|
|
71519
71605
|
}
|
|
71520
71606
|
}
|
|
@@ -71530,9 +71616,6 @@ async function runClaudeWithProxy(config3, proxyUrl, onCleanup) {
|
|
|
71530
71616
|
};
|
|
71531
71617
|
if (!config3.monitor && hasNativeAnthropicMapping(config3)) {
|
|
71532
71618
|
log2("[claudish] Native Claude model detected \u2014 using Claude Code subscription credentials");
|
|
71533
|
-
if (hidAnthropicApiKey) {
|
|
71534
|
-
log2("[claudish] ANTHROPIC_API_KEY found but hidden so it can't override that subscription \xB7 " + "use --anthropic-api-billing (or anthropicApiBilling: true) to bill the API instead");
|
|
71535
|
-
}
|
|
71536
71619
|
}
|
|
71537
71620
|
if (config3.interactive) {
|
|
71538
71621
|
log2(`
|
|
@@ -71699,12 +71782,12 @@ async function checkClaudeInstalled() {
|
|
|
71699
71782
|
const binary = await findClaudeBinary();
|
|
71700
71783
|
return binary !== null;
|
|
71701
71784
|
}
|
|
71702
|
-
var restoreTerminal = null
|
|
71785
|
+
var restoreTerminal = null;
|
|
71703
71786
|
var init_claude_runner = __esm(() => {
|
|
71704
71787
|
init_model_catalog();
|
|
71705
71788
|
init_config();
|
|
71706
71789
|
init_logger();
|
|
71707
|
-
|
|
71790
|
+
init_model_discovery();
|
|
71708
71791
|
init_model_parser();
|
|
71709
71792
|
init_routing_rules();
|
|
71710
71793
|
init_telemetry();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudish",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.20.0",
|
|
4
4
|
"description": "Run Claude Code with any model - OpenRouter, Ollama, LM Studio & local models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"ai"
|
|
61
61
|
],
|
|
62
62
|
"optionalDependencies": {
|
|
63
|
-
"@claudish/magmux-darwin-arm64": "7.
|
|
64
|
-
"@claudish/magmux-darwin-x64": "7.
|
|
65
|
-
"@claudish/magmux-linux-arm64": "7.
|
|
66
|
-
"@claudish/magmux-linux-x64": "7.
|
|
63
|
+
"@claudish/magmux-darwin-arm64": "7.20.0",
|
|
64
|
+
"@claudish/magmux-darwin-x64": "7.20.0",
|
|
65
|
+
"@claudish/magmux-linux-arm64": "7.20.0",
|
|
66
|
+
"@claudish/magmux-linux-x64": "7.20.0"
|
|
67
67
|
},
|
|
68
68
|
"author": "Jack Rudenko <i@madappgang.com>",
|
|
69
69
|
"license": "MIT",
|