@tech-leads-club/harness-toolkit 0.2.4 → 0.3.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/README.md +22 -26
- package/bin/tlc-build.mjs +93 -0
- package/bin/tlc-cli.ts +110 -66
- package/bin/tlc-exec.mjs +16 -13
- package/dist/compact-before.mjs +66 -8
- package/dist/doctor.mjs +178 -41
- package/dist/help-topic.mjs +0 -0
- package/dist/init-project.mjs +2 -7
- package/dist/install-runtime.mjs +100 -17
- package/dist/lessons-cli.mjs +67 -1
- package/dist/obs-cli.mjs +64 -1
- package/dist/price-lookup.mjs +45 -23
- package/dist/prompt-submit.mjs +66 -8
- package/dist/refresh-model-prices.mjs +7190 -46
- package/dist/response-after.mjs +66 -8
- package/dist/run.mjs +66 -8
- package/dist/session-end.mjs +66 -8
- package/dist/session-start.mjs +66 -8
- package/dist/shim.mjs +66 -3
- package/dist/stop.mjs +66 -8
- package/dist/subagent-start.mjs +66 -8
- package/dist/subagent-stop.mjs +66 -8
- package/dist/support.mjs +64 -1
- package/dist/tlc-cli.mjs +193 -87
- package/dist/tool-after.mjs +111 -31
- package/dist/tool-before.mjs +66 -8
- package/dist/tool-failure.mjs +66 -8
- package/dist/uninstall-runtime.mjs +9 -10
- package/docs/log.md +2 -0
- package/docs/measure.md +35 -31
- package/package.json +4 -4
- package/src/core/core.facade.ts +7 -0
- package/src/core/index.ts +1 -0
- package/src/core/pricing/pricing.freshness.ts +118 -0
- package/src/core/skill/skill.link.ts +14 -3
- package/src/entrypoints/shim.ts +8 -2
- package/src/platform/links.ts +73 -0
- package/src/platform/pricing.ts +139 -31
- package/src/providers/cursor/cursor.wiring.ts +11 -8
- package/tools/doctor.ts +78 -8
- package/tools/init-project.ts +7 -7
- package/tools/install-runtime.ts +89 -6
- package/tools/refresh-model-prices.ts +242 -75
- package/tools/uninstall-runtime.ts +23 -19
- package/bin/tlc-build +0 -80
- package/bin/tlc-exec +0 -10
- package/bin/tlc-exec.cmd +0 -4
- package/model-aliases.json +0 -12
- package/model-prices.cursor.json +0 -410
- package/model-prices.json +0 -1
package/dist/tool-after.mjs
CHANGED
|
@@ -5273,6 +5273,62 @@ function release(root, provider, session) {
|
|
|
5273
5273
|
deletePresenceRecord(root, provider, session);
|
|
5274
5274
|
}
|
|
5275
5275
|
|
|
5276
|
+
// src/core/pricing/pricing.freshness.ts
|
|
5277
|
+
var DEFAULT_TTL_DAYS = 7;
|
|
5278
|
+
var MS_PER_DAY = 86400000;
|
|
5279
|
+
function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
|
|
5280
|
+
if (meta === null) {
|
|
5281
|
+
return { state: "absent" };
|
|
5282
|
+
}
|
|
5283
|
+
const stamp = meta.refreshedAt;
|
|
5284
|
+
if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
|
|
5285
|
+
return { state: "undated" };
|
|
5286
|
+
}
|
|
5287
|
+
const ageMs = now.getTime() - Date.parse(stamp);
|
|
5288
|
+
const ageDays = Math.max(0, ageMs / MS_PER_DAY);
|
|
5289
|
+
return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
|
|
5290
|
+
}
|
|
5291
|
+
function shouldRefetch(state) {
|
|
5292
|
+
return state.state === "absent" || state.state === "undated" || state.state === "stale";
|
|
5293
|
+
}
|
|
5294
|
+
function freshnessMessage(state, catalogue) {
|
|
5295
|
+
switch (state.state) {
|
|
5296
|
+
case "absent":
|
|
5297
|
+
return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
|
|
5298
|
+
case "undated":
|
|
5299
|
+
return `${catalogue}: present but carries no date — it will be refetched`;
|
|
5300
|
+
case "fresh":
|
|
5301
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old`;
|
|
5302
|
+
default:
|
|
5303
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
var MIN_RETAINED_RATIO = 0.5;
|
|
5307
|
+
function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
|
|
5308
|
+
if (incomingCount === 0) {
|
|
5309
|
+
return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
|
|
5310
|
+
}
|
|
5311
|
+
if (existingCount === 0) {
|
|
5312
|
+
return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
|
|
5313
|
+
}
|
|
5314
|
+
if (incomingCount >= existingCount) {
|
|
5315
|
+
return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
|
|
5316
|
+
}
|
|
5317
|
+
const retained = incomingCount / existingCount;
|
|
5318
|
+
return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
|
|
5319
|
+
replace: false,
|
|
5320
|
+
reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5323
|
+
function describeAge(ageDays) {
|
|
5324
|
+
if (ageDays < 1) {
|
|
5325
|
+
const hours = Math.max(1, Math.round(ageDays * 24));
|
|
5326
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
5327
|
+
}
|
|
5328
|
+
const days = Math.round(ageDays);
|
|
5329
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5276
5332
|
// src/core/release/release.decisions.ts
|
|
5277
5333
|
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
|
|
5278
5334
|
import { join as join18 } from "node:path";
|
|
@@ -5622,7 +5678,8 @@ function linkHealth(target, runtimeHome2, probe) {
|
|
|
5622
5678
|
if (!probe.exists(resolved)) {
|
|
5623
5679
|
return { state: "dangling", target, resolved };
|
|
5624
5680
|
}
|
|
5625
|
-
const
|
|
5681
|
+
const resolveHome = probe.realpath ?? ((path) => path);
|
|
5682
|
+
const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
|
|
5626
5683
|
return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
|
|
5627
5684
|
}
|
|
5628
5685
|
function linkHealthMessage(health) {
|
|
@@ -6769,6 +6826,12 @@ var coreFacade = {
|
|
|
6769
6826
|
coversHandler,
|
|
6770
6827
|
decideShim
|
|
6771
6828
|
},
|
|
6829
|
+
pricing: {
|
|
6830
|
+
freshness,
|
|
6831
|
+
freshnessMessage,
|
|
6832
|
+
mayReplace,
|
|
6833
|
+
shouldRefetch
|
|
6834
|
+
},
|
|
6772
6835
|
skill: {
|
|
6773
6836
|
linkHealth,
|
|
6774
6837
|
linkHealthMessage,
|
|
@@ -6958,6 +7021,11 @@ var coreFacade = {
|
|
|
6958
7021
|
// src/platform/pricing.ts
|
|
6959
7022
|
import { existsSync as existsSync25, readFileSync as readFileSync27 } from "node:fs";
|
|
6960
7023
|
import { join as join26 } from "node:path";
|
|
7024
|
+
var FALLBACK_PLANE = "litellm";
|
|
7025
|
+
var MODEL_ALIASES = {
|
|
7026
|
+
"cursor-grok-4.5": "grok-4.5",
|
|
7027
|
+
auto: "auto-cost"
|
|
7028
|
+
};
|
|
6961
7029
|
var VENDOR_TO_NEUTRAL_POOL = {
|
|
6962
7030
|
cursor_models: "provider_native",
|
|
6963
7031
|
anthropic_models: "provider_native",
|
|
@@ -6968,16 +7036,6 @@ var VENDOR_TO_NEUTRAL_POOL = {
|
|
|
6968
7036
|
function mapPoolToNeutral(pool) {
|
|
6969
7037
|
return VENDOR_TO_NEUTRAL_POOL[pool];
|
|
6970
7038
|
}
|
|
6971
|
-
function readJsonFile2(path) {
|
|
6972
|
-
if (!existsSync25(path)) {
|
|
6973
|
-
return null;
|
|
6974
|
-
}
|
|
6975
|
-
try {
|
|
6976
|
-
return JSON.parse(readFileSync27(path, "utf8"));
|
|
6977
|
-
} catch {
|
|
6978
|
-
return null;
|
|
6979
|
-
}
|
|
6980
|
-
}
|
|
6981
7039
|
function stripMeta(table) {
|
|
6982
7040
|
if (!table) {
|
|
6983
7041
|
return {};
|
|
@@ -6986,7 +7044,7 @@ function stripMeta(table) {
|
|
|
6986
7044
|
return rest;
|
|
6987
7045
|
}
|
|
6988
7046
|
function slugifyModelName(name) {
|
|
6989
|
-
return name.trim().toLowerCase().replace(
|
|
7047
|
+
return name.trim().toLowerCase().replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/[[\]]/g, "").replace(/[^a-z0-9.+]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6990
7048
|
}
|
|
6991
7049
|
function candidatesFor(model, aliases) {
|
|
6992
7050
|
const trimmed = model.trim();
|
|
@@ -7023,28 +7081,55 @@ function fuzzyFind(table, needle) {
|
|
|
7023
7081
|
}
|
|
7024
7082
|
return;
|
|
7025
7083
|
}
|
|
7026
|
-
function
|
|
7084
|
+
function cataloguePath() {
|
|
7027
7085
|
return join26(runtimeHome(), "model-prices.json");
|
|
7028
7086
|
}
|
|
7029
|
-
function
|
|
7030
|
-
return join26(runtimeHome(),
|
|
7087
|
+
function overridesPath() {
|
|
7088
|
+
return join26(runtimeHome(), "model-prices.local.json");
|
|
7089
|
+
}
|
|
7090
|
+
var cache = new Map;
|
|
7091
|
+
function readCatalogue(path) {
|
|
7092
|
+
if (!existsSync25(path)) {
|
|
7093
|
+
cache.delete(path);
|
|
7094
|
+
return null;
|
|
7095
|
+
}
|
|
7096
|
+
let text;
|
|
7097
|
+
try {
|
|
7098
|
+
text = readFileSync27(path, "utf8");
|
|
7099
|
+
} catch {
|
|
7100
|
+
cache.delete(path);
|
|
7101
|
+
return null;
|
|
7102
|
+
}
|
|
7103
|
+
const hit = cache.get(path);
|
|
7104
|
+
if (hit && hit.text === text) {
|
|
7105
|
+
return hit.value;
|
|
7106
|
+
}
|
|
7107
|
+
let parsed;
|
|
7108
|
+
try {
|
|
7109
|
+
parsed = JSON.parse(text);
|
|
7110
|
+
} catch {
|
|
7111
|
+
cache.delete(path);
|
|
7112
|
+
return null;
|
|
7113
|
+
}
|
|
7114
|
+
cache.set(path, { text, value: parsed });
|
|
7115
|
+
return parsed;
|
|
7031
7116
|
}
|
|
7032
|
-
function
|
|
7033
|
-
return
|
|
7117
|
+
function loadCatalogue() {
|
|
7118
|
+
return readCatalogue(cataloguePath()) ?? {};
|
|
7034
7119
|
}
|
|
7035
|
-
function
|
|
7036
|
-
return
|
|
7120
|
+
function planeFor(catalogue, plane) {
|
|
7121
|
+
return stripMeta(catalogue.planes?.[plane] ?? null);
|
|
7037
7122
|
}
|
|
7038
7123
|
function resolveModelPrice(provider, model) {
|
|
7039
7124
|
const trimmed = model.trim();
|
|
7040
7125
|
if (!trimmed) {
|
|
7041
7126
|
return;
|
|
7042
7127
|
}
|
|
7043
|
-
const
|
|
7044
|
-
const
|
|
7045
|
-
const
|
|
7046
|
-
const
|
|
7047
|
-
const candidates = candidatesFor(trimmed,
|
|
7128
|
+
const catalogue = loadCatalogue();
|
|
7129
|
+
const overrides = stripMeta(readCatalogue(overridesPath()));
|
|
7130
|
+
const native = provider ? planeFor(catalogue, provider) : {};
|
|
7131
|
+
const litellm = planeFor(catalogue, FALLBACK_PLANE);
|
|
7132
|
+
const candidates = candidatesFor(trimmed, MODEL_ALIASES);
|
|
7048
7133
|
for (const id of candidates) {
|
|
7049
7134
|
const entry = overrides[id];
|
|
7050
7135
|
if (entry) {
|
|
@@ -7909,14 +7994,9 @@ var ENTRY_SPECS2 = [
|
|
|
7909
7994
|
{ hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
7910
7995
|
{ hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
|
|
7911
7996
|
];
|
|
7912
|
-
function commandFor(runtime) {
|
|
7913
|
-
if (process.platform === "win32") {
|
|
7914
|
-
return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
|
|
7915
|
-
}
|
|
7916
|
-
return { command: "node", argsPrefix: [runtime.launcherPath] };
|
|
7917
|
-
}
|
|
7918
7997
|
function cursorWiring(runtime) {
|
|
7919
|
-
const
|
|
7998
|
+
const command = "node";
|
|
7999
|
+
const argsPrefix = [runtime.launcherPath];
|
|
7920
8000
|
const entries = ENTRY_SPECS2.map((spec) => ({
|
|
7921
8001
|
hookEvent: spec.hookEvent,
|
|
7922
8002
|
handler: spec.handler,
|
package/dist/tool-before.mjs
CHANGED
|
@@ -5273,6 +5273,62 @@ function release(root, provider, session) {
|
|
|
5273
5273
|
deletePresenceRecord(root, provider, session);
|
|
5274
5274
|
}
|
|
5275
5275
|
|
|
5276
|
+
// src/core/pricing/pricing.freshness.ts
|
|
5277
|
+
var DEFAULT_TTL_DAYS = 7;
|
|
5278
|
+
var MS_PER_DAY = 86400000;
|
|
5279
|
+
function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
|
|
5280
|
+
if (meta === null) {
|
|
5281
|
+
return { state: "absent" };
|
|
5282
|
+
}
|
|
5283
|
+
const stamp = meta.refreshedAt;
|
|
5284
|
+
if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
|
|
5285
|
+
return { state: "undated" };
|
|
5286
|
+
}
|
|
5287
|
+
const ageMs = now.getTime() - Date.parse(stamp);
|
|
5288
|
+
const ageDays = Math.max(0, ageMs / MS_PER_DAY);
|
|
5289
|
+
return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
|
|
5290
|
+
}
|
|
5291
|
+
function shouldRefetch(state) {
|
|
5292
|
+
return state.state === "absent" || state.state === "undated" || state.state === "stale";
|
|
5293
|
+
}
|
|
5294
|
+
function freshnessMessage(state, catalogue) {
|
|
5295
|
+
switch (state.state) {
|
|
5296
|
+
case "absent":
|
|
5297
|
+
return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
|
|
5298
|
+
case "undated":
|
|
5299
|
+
return `${catalogue}: present but carries no date — it will be refetched`;
|
|
5300
|
+
case "fresh":
|
|
5301
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old`;
|
|
5302
|
+
default:
|
|
5303
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
var MIN_RETAINED_RATIO = 0.5;
|
|
5307
|
+
function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
|
|
5308
|
+
if (incomingCount === 0) {
|
|
5309
|
+
return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
|
|
5310
|
+
}
|
|
5311
|
+
if (existingCount === 0) {
|
|
5312
|
+
return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
|
|
5313
|
+
}
|
|
5314
|
+
if (incomingCount >= existingCount) {
|
|
5315
|
+
return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
|
|
5316
|
+
}
|
|
5317
|
+
const retained = incomingCount / existingCount;
|
|
5318
|
+
return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
|
|
5319
|
+
replace: false,
|
|
5320
|
+
reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5323
|
+
function describeAge(ageDays) {
|
|
5324
|
+
if (ageDays < 1) {
|
|
5325
|
+
const hours = Math.max(1, Math.round(ageDays * 24));
|
|
5326
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
5327
|
+
}
|
|
5328
|
+
const days = Math.round(ageDays);
|
|
5329
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5276
5332
|
// src/core/release/release.decisions.ts
|
|
5277
5333
|
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
|
|
5278
5334
|
import { join as join18 } from "node:path";
|
|
@@ -5622,7 +5678,8 @@ function linkHealth(target, runtimeHome2, probe) {
|
|
|
5622
5678
|
if (!probe.exists(resolved)) {
|
|
5623
5679
|
return { state: "dangling", target, resolved };
|
|
5624
5680
|
}
|
|
5625
|
-
const
|
|
5681
|
+
const resolveHome = probe.realpath ?? ((path) => path);
|
|
5682
|
+
const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
|
|
5626
5683
|
return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
|
|
5627
5684
|
}
|
|
5628
5685
|
function linkHealthMessage(health) {
|
|
@@ -6769,6 +6826,12 @@ var coreFacade = {
|
|
|
6769
6826
|
coversHandler,
|
|
6770
6827
|
decideShim
|
|
6771
6828
|
},
|
|
6829
|
+
pricing: {
|
|
6830
|
+
freshness,
|
|
6831
|
+
freshnessMessage,
|
|
6832
|
+
mayReplace,
|
|
6833
|
+
shouldRefetch
|
|
6834
|
+
},
|
|
6772
6835
|
skill: {
|
|
6773
6836
|
linkHealth,
|
|
6774
6837
|
linkHealthMessage,
|
|
@@ -7719,14 +7782,9 @@ var ENTRY_SPECS2 = [
|
|
|
7719
7782
|
{ hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
7720
7783
|
{ hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
|
|
7721
7784
|
];
|
|
7722
|
-
function commandFor(runtime) {
|
|
7723
|
-
if (process.platform === "win32") {
|
|
7724
|
-
return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
|
|
7725
|
-
}
|
|
7726
|
-
return { command: "node", argsPrefix: [runtime.launcherPath] };
|
|
7727
|
-
}
|
|
7728
7785
|
function cursorWiring(runtime) {
|
|
7729
|
-
const
|
|
7786
|
+
const command = "node";
|
|
7787
|
+
const argsPrefix = [runtime.launcherPath];
|
|
7730
7788
|
const entries = ENTRY_SPECS2.map((spec) => ({
|
|
7731
7789
|
hookEvent: spec.hookEvent,
|
|
7732
7790
|
handler: spec.handler,
|
package/dist/tool-failure.mjs
CHANGED
|
@@ -5273,6 +5273,62 @@ function release(root, provider, session) {
|
|
|
5273
5273
|
deletePresenceRecord(root, provider, session);
|
|
5274
5274
|
}
|
|
5275
5275
|
|
|
5276
|
+
// src/core/pricing/pricing.freshness.ts
|
|
5277
|
+
var DEFAULT_TTL_DAYS = 7;
|
|
5278
|
+
var MS_PER_DAY = 86400000;
|
|
5279
|
+
function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
|
|
5280
|
+
if (meta === null) {
|
|
5281
|
+
return { state: "absent" };
|
|
5282
|
+
}
|
|
5283
|
+
const stamp = meta.refreshedAt;
|
|
5284
|
+
if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
|
|
5285
|
+
return { state: "undated" };
|
|
5286
|
+
}
|
|
5287
|
+
const ageMs = now.getTime() - Date.parse(stamp);
|
|
5288
|
+
const ageDays = Math.max(0, ageMs / MS_PER_DAY);
|
|
5289
|
+
return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
|
|
5290
|
+
}
|
|
5291
|
+
function shouldRefetch(state) {
|
|
5292
|
+
return state.state === "absent" || state.state === "undated" || state.state === "stale";
|
|
5293
|
+
}
|
|
5294
|
+
function freshnessMessage(state, catalogue) {
|
|
5295
|
+
switch (state.state) {
|
|
5296
|
+
case "absent":
|
|
5297
|
+
return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
|
|
5298
|
+
case "undated":
|
|
5299
|
+
return `${catalogue}: present but carries no date — it will be refetched`;
|
|
5300
|
+
case "fresh":
|
|
5301
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old`;
|
|
5302
|
+
default:
|
|
5303
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
var MIN_RETAINED_RATIO = 0.5;
|
|
5307
|
+
function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
|
|
5308
|
+
if (incomingCount === 0) {
|
|
5309
|
+
return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
|
|
5310
|
+
}
|
|
5311
|
+
if (existingCount === 0) {
|
|
5312
|
+
return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
|
|
5313
|
+
}
|
|
5314
|
+
if (incomingCount >= existingCount) {
|
|
5315
|
+
return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
|
|
5316
|
+
}
|
|
5317
|
+
const retained = incomingCount / existingCount;
|
|
5318
|
+
return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
|
|
5319
|
+
replace: false,
|
|
5320
|
+
reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5323
|
+
function describeAge(ageDays) {
|
|
5324
|
+
if (ageDays < 1) {
|
|
5325
|
+
const hours = Math.max(1, Math.round(ageDays * 24));
|
|
5326
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
5327
|
+
}
|
|
5328
|
+
const days = Math.round(ageDays);
|
|
5329
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5276
5332
|
// src/core/release/release.decisions.ts
|
|
5277
5333
|
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
|
|
5278
5334
|
import { join as join18 } from "node:path";
|
|
@@ -5622,7 +5678,8 @@ function linkHealth(target, runtimeHome2, probe) {
|
|
|
5622
5678
|
if (!probe.exists(resolved)) {
|
|
5623
5679
|
return { state: "dangling", target, resolved };
|
|
5624
5680
|
}
|
|
5625
|
-
const
|
|
5681
|
+
const resolveHome = probe.realpath ?? ((path) => path);
|
|
5682
|
+
const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
|
|
5626
5683
|
return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
|
|
5627
5684
|
}
|
|
5628
5685
|
function linkHealthMessage(health) {
|
|
@@ -6769,6 +6826,12 @@ var coreFacade = {
|
|
|
6769
6826
|
coversHandler,
|
|
6770
6827
|
decideShim
|
|
6771
6828
|
},
|
|
6829
|
+
pricing: {
|
|
6830
|
+
freshness,
|
|
6831
|
+
freshnessMessage,
|
|
6832
|
+
mayReplace,
|
|
6833
|
+
shouldRefetch
|
|
6834
|
+
},
|
|
6772
6835
|
skill: {
|
|
6773
6836
|
linkHealth,
|
|
6774
6837
|
linkHealthMessage,
|
|
@@ -7719,14 +7782,9 @@ var ENTRY_SPECS2 = [
|
|
|
7719
7782
|
{ hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
7720
7783
|
{ hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
|
|
7721
7784
|
];
|
|
7722
|
-
function commandFor(runtime) {
|
|
7723
|
-
if (process.platform === "win32") {
|
|
7724
|
-
return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
|
|
7725
|
-
}
|
|
7726
|
-
return { command: "node", argsPrefix: [runtime.launcherPath] };
|
|
7727
|
-
}
|
|
7728
7785
|
function cursorWiring(runtime) {
|
|
7729
|
-
const
|
|
7786
|
+
const command = "node";
|
|
7787
|
+
const argsPrefix = [runtime.launcherPath];
|
|
7730
7788
|
const entries = ENTRY_SPECS2.map((spec) => ({
|
|
7731
7789
|
hookEvent: spec.hookEvent,
|
|
7732
7790
|
handler: spec.handler,
|
|
@@ -979,9 +979,6 @@ var RUNTIME_PAYLOAD = [
|
|
|
979
979
|
"src",
|
|
980
980
|
"tools",
|
|
981
981
|
"config.example.json",
|
|
982
|
-
"model-aliases.json",
|
|
983
|
-
"model-prices.cursor.json",
|
|
984
|
-
"model-prices.json",
|
|
985
982
|
"package.json"
|
|
986
983
|
];
|
|
987
984
|
var OPERATOR_OWNED = ["config.json", "state", "flags"];
|
|
@@ -989,18 +986,18 @@ var NOT_SHIPPED = [join2("tools", "dev"), join2("tools", "__test__")];
|
|
|
989
986
|
if (false) {}
|
|
990
987
|
|
|
991
988
|
// tools/uninstall-runtime.ts
|
|
992
|
-
function uninstallTargets(env = process.env
|
|
993
|
-
const
|
|
994
|
-
const userHome = (windows ? env.USERPROFILE : env.HOME)?.trim() || homedir2();
|
|
989
|
+
function uninstallTargets(env = process.env) {
|
|
990
|
+
const userHome = homedir2();
|
|
995
991
|
const binDir = env.TLC_BIN_DIR?.trim() || join3(userHome, ".local", "bin");
|
|
996
992
|
return {
|
|
997
993
|
home: runtimeHome(env),
|
|
998
|
-
|
|
994
|
+
binLinks: [join3(binDir, "tlc"), join3(binDir, "tlc.cmd")],
|
|
999
995
|
claudeSettings: join3(claudeConfigDir(), "settings.json"),
|
|
1000
996
|
cursorHooks: join3(cursorConfigDir(), "hooks.json"),
|
|
1001
|
-
skillLinks:
|
|
997
|
+
skillLinks: [
|
|
1002
998
|
join3(claudeConfigDir(), "skills", "harness-init"),
|
|
1003
|
-
join3(cursorConfigDir(), "skills", "harness-init")
|
|
999
|
+
join3(cursorConfigDir(), "skills", "harness-init"),
|
|
1000
|
+
join3(userHome, ".tlc", "skills", "harness-init")
|
|
1004
1001
|
]
|
|
1005
1002
|
};
|
|
1006
1003
|
}
|
|
@@ -1182,7 +1179,9 @@ function planUninstall(targets, options = {}) {
|
|
|
1182
1179
|
for (const link of targets.skillLinks) {
|
|
1183
1180
|
planLink(items, link, targets.home, "skill link", "location");
|
|
1184
1181
|
}
|
|
1185
|
-
|
|
1182
|
+
for (const link of targets.binLinks) {
|
|
1183
|
+
planLink(items, link, targets.home, "the tlc launcher on PATH", "target");
|
|
1184
|
+
}
|
|
1186
1185
|
const homeIsLink = planRuntime(items, targets.home, purge);
|
|
1187
1186
|
planManual(items, targets.home);
|
|
1188
1187
|
return { items, purge, homeIsLink };
|
package/docs/log.md
CHANGED
|
@@ -23,6 +23,8 @@ newest first. For what landed in which npm release, see `CHANGELOG.md` at the re
|
|
|
23
23
|
- **AD-086** — The write lock read the wrong error code on Windows, in a module that already listed the right ones ([/decisions/ad-086.md](/decisions/ad-086.md))
|
|
24
24
|
- **AD-087** — How the release works, and the six wrong shapes it took first ([/decisions/ad-087.md](/decisions/ad-087.md))
|
|
25
25
|
- **AD-095** — Four defects about where things are written, and one of them made every hook run twice ([/decisions/ad-095.md](/decisions/ad-095.md))
|
|
26
|
+
- **AD-096** — Prices are the machine's, in one file, and the parser that fills it was wrong twice ([/decisions/ad-096.md](/decisions/ad-096.md))
|
|
27
|
+
- **AD-097** — The shell layer goes, and with it every platform branch that only existed because of it ([/decisions/ad-097.md](/decisions/ad-097.md))
|
|
26
28
|
|
|
27
29
|
## 2026-08-17
|
|
28
30
|
|
package/docs/measure.md
CHANGED
|
@@ -114,55 +114,59 @@ full mapping table.
|
|
|
114
114
|
|
|
115
115
|
## Prices
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
Fetched per machine, never versioned ([/decisions/ad-096.md](/decisions/ad-096.md)).
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
tlc harness prices refresh
|
|
121
|
-
tlc harness prices refresh
|
|
122
|
-
tlc harness prices refresh
|
|
123
|
-
tlc harness prices refresh litellm
|
|
120
|
+
tlc harness prices refresh # both planes, now
|
|
121
|
+
tlc harness prices refresh cursor # one plane
|
|
122
|
+
tlc harness prices refresh --if-stale # only past the TTL; what install and update run
|
|
124
123
|
tlc harness prices lookup <model-id> [provider]
|
|
125
124
|
```
|
|
126
125
|
|
|
127
|
-
|
|
|
126
|
+
| Trigger | Effect |
|
|
128
127
|
|---------|--------|
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `lookup <model-id> [provider]` | Resolve catalog key, pool, and USD for 1M input + 1M output |
|
|
128
|
+
| `tlc harness install` | first fetch; a network failure does not fail the install |
|
|
129
|
+
| `tlc harness update` | `--if-stale`, TTL 7 days |
|
|
130
|
+
| `tlc harness doctor` | reports the catalogue's age, or that it is absent |
|
|
133
131
|
|
|
134
|
-
###
|
|
132
|
+
### Files
|
|
135
133
|
|
|
136
134
|
| File | Role | In git |
|
|
137
135
|
|------|------|--------|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
| `~/.tlc/harness/model-prices.json` | the catalogue | No |
|
|
137
|
+
| `~/.tlc/harness/model-prices.local.json` | hand-written overrides | No |
|
|
138
|
+
|
|
139
|
+
### Planes
|
|
140
|
+
|
|
141
|
+
`planes` is keyed by who bills the call. They are not merged: the same model has one rate from its vendor and
|
|
142
|
+
another from a provider reselling it.
|
|
143
|
+
|
|
144
|
+
| Plane | Holds | Source |
|
|
145
|
+
|-------|-------|--------|
|
|
146
|
+
| `cursor` | what that provider charges | its pricing page |
|
|
147
|
+
| `litellm` | vendor list prices | the LiteLLM public JSON |
|
|
148
|
+
|
|
149
|
+
`_meta.planes[<plane>]` records the source, the model count and the fetch time.
|
|
142
150
|
|
|
143
151
|
### Resolution order
|
|
144
152
|
|
|
145
|
-
1. `model-prices.json`
|
|
146
|
-
2. `
|
|
147
|
-
3. `
|
|
153
|
+
1. `model-prices.local.json`
|
|
154
|
+
2. `planes[<asking provider>]`
|
|
155
|
+
3. `planes.litellm`
|
|
148
156
|
4. otherwise `cost_usd: null`
|
|
149
157
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
keys internally (`cursor_models`, `anthropic_models`, …) since pricing must name real vendors — those are
|
|
153
|
-
mapped to the neutral names before they reach `core/`.
|
|
158
|
+
Host ids that differ from a catalogue key are mapped in `MODEL_ALIASES` (`src/platform/pricing.ts`). Add your own
|
|
159
|
+
by writing the key into the overrides file.
|
|
154
160
|
|
|
155
|
-
|
|
161
|
+
Pools (neutral names in observability records; see [/decisions/ad-011.md](/decisions/ad-011.md) item 2):
|
|
162
|
+
`provider_native` | `other` | `auto` | `unknown`. The catalogue uses vendor-named pool keys internally
|
|
163
|
+
(`cursor_models`, `anthropic_models`, …) since pricing must name real vendors — those are mapped to the neutral
|
|
164
|
+
names before they reach `core/`.
|
|
156
165
|
|
|
157
|
-
|
|
158
|
-
|-----------|---------|
|
|
159
|
-
| A provider published new rates or models | `tlc harness prices refresh cursor` (then commit) |
|
|
160
|
-
| Missing LiteLLM file or obscure model | `tlc harness prices refresh litellm` |
|
|
161
|
-
| Update both catalogs | `tlc harness prices refresh` |
|
|
162
|
-
| Inspect one model | `tlc harness prices lookup <model-id> [provider]` |
|
|
166
|
+
### Refusal
|
|
163
167
|
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
A plane is replaced only if the incoming table keeps at least half of what is on disk. Below that the refresh
|
|
169
|
+
refuses, names both counts, and leaves every plane untouched.
|
|
166
170
|
|
|
167
171
|
## Project state files
|
|
168
172
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tech-leads-club/harness-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
|
|
6
6
|
"keywords": [
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"tlc": "bin/tlc.mjs",
|
|
20
20
|
"tlc-exec": "bin/tlc-exec.mjs"
|
|
21
21
|
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"prepack": "node bin/tlc-build.mjs"
|
|
24
|
+
},
|
|
22
25
|
"files": [
|
|
23
26
|
"bin/",
|
|
24
27
|
"capabilities/",
|
|
@@ -33,9 +36,6 @@
|
|
|
33
36
|
"!tools/test-env*",
|
|
34
37
|
"!docs/decisions",
|
|
35
38
|
"config.example.json",
|
|
36
|
-
"model-aliases.json",
|
|
37
|
-
"model-prices.cursor.json",
|
|
38
|
-
"model-prices.json",
|
|
39
39
|
"NOTICE"
|
|
40
40
|
],
|
|
41
41
|
"publishConfig": {
|
package/src/core/core.facade.ts
CHANGED
|
@@ -130,6 +130,7 @@ import { isOperatorMode, OPERATOR_MODES } from "./policy/policy.posture.ts";
|
|
|
130
130
|
import { activeRails } from "./policy/policy.rails.ts";
|
|
131
131
|
import { forProvider } from "./policy/policy.types.ts";
|
|
132
132
|
import { checkCollision, heartbeat, register, release, sweepStale } from "./presence/presence.service.ts";
|
|
133
|
+
import { freshness, freshnessMessage, mayReplace, shouldRefetch } from "./pricing/pricing.freshness.ts";
|
|
133
134
|
import {
|
|
134
135
|
allDecisionFiles,
|
|
135
136
|
formatDecisionDigest,
|
|
@@ -277,6 +278,12 @@ export const coreFacade = {
|
|
|
277
278
|
coversHandler,
|
|
278
279
|
decideShim,
|
|
279
280
|
},
|
|
281
|
+
pricing: {
|
|
282
|
+
freshness,
|
|
283
|
+
freshnessMessage,
|
|
284
|
+
mayReplace,
|
|
285
|
+
shouldRefetch,
|
|
286
|
+
},
|
|
280
287
|
skill: {
|
|
281
288
|
linkHealth,
|
|
282
289
|
linkHealthMessage,
|
package/src/core/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type {
|
|
|
44
44
|
ProviderScoped,
|
|
45
45
|
} from "./policy/policy.types.ts";
|
|
46
46
|
export type { PresenceRecord } from "./presence/presence.types.ts";
|
|
47
|
+
export type { CatalogueMeta, Freshness } from "./pricing/pricing.freshness.ts";
|
|
47
48
|
export type { ShellEffectClass } from "./shell-policy/shell-policy.types.ts";
|
|
48
49
|
export type { ProviderSettings } from "./shim/shim.precedence.ts";
|
|
49
50
|
export type { ShipClaim, ShipClaimKind, ShipLedgerEvent, ShipLedgerRow } from "./ship/ship.types.ts";
|