@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.
Files changed (50) hide show
  1. package/README.md +22 -26
  2. package/bin/tlc-build.mjs +93 -0
  3. package/bin/tlc-cli.ts +110 -66
  4. package/bin/tlc-exec.mjs +16 -13
  5. package/dist/compact-before.mjs +66 -8
  6. package/dist/doctor.mjs +178 -41
  7. package/dist/help-topic.mjs +0 -0
  8. package/dist/init-project.mjs +2 -7
  9. package/dist/install-runtime.mjs +100 -17
  10. package/dist/lessons-cli.mjs +67 -1
  11. package/dist/obs-cli.mjs +64 -1
  12. package/dist/price-lookup.mjs +45 -23
  13. package/dist/prompt-submit.mjs +66 -8
  14. package/dist/refresh-model-prices.mjs +7190 -46
  15. package/dist/response-after.mjs +66 -8
  16. package/dist/run.mjs +66 -8
  17. package/dist/session-end.mjs +66 -8
  18. package/dist/session-start.mjs +66 -8
  19. package/dist/shim.mjs +66 -3
  20. package/dist/stop.mjs +66 -8
  21. package/dist/subagent-start.mjs +66 -8
  22. package/dist/subagent-stop.mjs +66 -8
  23. package/dist/support.mjs +64 -1
  24. package/dist/tlc-cli.mjs +193 -87
  25. package/dist/tool-after.mjs +111 -31
  26. package/dist/tool-before.mjs +66 -8
  27. package/dist/tool-failure.mjs +66 -8
  28. package/dist/uninstall-runtime.mjs +9 -10
  29. package/docs/log.md +2 -0
  30. package/docs/measure.md +35 -31
  31. package/package.json +4 -4
  32. package/src/core/core.facade.ts +7 -0
  33. package/src/core/index.ts +1 -0
  34. package/src/core/pricing/pricing.freshness.ts +118 -0
  35. package/src/core/skill/skill.link.ts +14 -3
  36. package/src/entrypoints/shim.ts +8 -2
  37. package/src/platform/links.ts +73 -0
  38. package/src/platform/pricing.ts +139 -31
  39. package/src/providers/cursor/cursor.wiring.ts +11 -8
  40. package/tools/doctor.ts +78 -8
  41. package/tools/init-project.ts +7 -7
  42. package/tools/install-runtime.ts +89 -6
  43. package/tools/refresh-model-prices.ts +242 -75
  44. package/tools/uninstall-runtime.ts +23 -19
  45. package/bin/tlc-build +0 -80
  46. package/bin/tlc-exec +0 -10
  47. package/bin/tlc-exec.cmd +0 -4
  48. package/model-aliases.json +0 -12
  49. package/model-prices.cursor.json +0 -410
  50. package/model-prices.json +0 -1
package/dist/obs-cli.mjs CHANGED
@@ -5262,6 +5262,62 @@ function release(root, provider, session) {
5262
5262
  deletePresenceRecord(root, provider, session);
5263
5263
  }
5264
5264
 
5265
+ // src/core/pricing/pricing.freshness.ts
5266
+ var DEFAULT_TTL_DAYS = 7;
5267
+ var MS_PER_DAY = 86400000;
5268
+ function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
5269
+ if (meta === null) {
5270
+ return { state: "absent" };
5271
+ }
5272
+ const stamp = meta.refreshedAt;
5273
+ if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
5274
+ return { state: "undated" };
5275
+ }
5276
+ const ageMs = now.getTime() - Date.parse(stamp);
5277
+ const ageDays = Math.max(0, ageMs / MS_PER_DAY);
5278
+ return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
5279
+ }
5280
+ function shouldRefetch(state) {
5281
+ return state.state === "absent" || state.state === "undated" || state.state === "stale";
5282
+ }
5283
+ function freshnessMessage(state, catalogue) {
5284
+ switch (state.state) {
5285
+ case "absent":
5286
+ return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
5287
+ case "undated":
5288
+ return `${catalogue}: present but carries no date — it will be refetched`;
5289
+ case "fresh":
5290
+ return `${catalogue}: ${describeAge(state.ageDays)} old`;
5291
+ default:
5292
+ return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
5293
+ }
5294
+ }
5295
+ var MIN_RETAINED_RATIO = 0.5;
5296
+ function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
5297
+ if (incomingCount === 0) {
5298
+ return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
5299
+ }
5300
+ if (existingCount === 0) {
5301
+ return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
5302
+ }
5303
+ if (incomingCount >= existingCount) {
5304
+ return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
5305
+ }
5306
+ const retained = incomingCount / existingCount;
5307
+ return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
5308
+ replace: false,
5309
+ reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
5310
+ };
5311
+ }
5312
+ function describeAge(ageDays) {
5313
+ if (ageDays < 1) {
5314
+ const hours = Math.max(1, Math.round(ageDays * 24));
5315
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
5316
+ }
5317
+ const days = Math.round(ageDays);
5318
+ return `${days} day${days === 1 ? "" : "s"}`;
5319
+ }
5320
+
5265
5321
  // src/core/release/release.decisions.ts
5266
5322
  import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
5267
5323
  import { join as join18 } from "node:path";
@@ -5611,7 +5667,8 @@ function linkHealth(target, runtimeHome2, probe) {
5611
5667
  if (!probe.exists(resolved)) {
5612
5668
  return { state: "dangling", target, resolved };
5613
5669
  }
5614
- const home = runtimeHome2.replace(/\/+$/, "");
5670
+ const resolveHome = probe.realpath ?? ((path) => path);
5671
+ const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
5615
5672
  return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5616
5673
  }
5617
5674
  function linkHealthMessage(health) {
@@ -6758,6 +6815,12 @@ var coreFacade = {
6758
6815
  coversHandler,
6759
6816
  decideShim
6760
6817
  },
6818
+ pricing: {
6819
+ freshness,
6820
+ freshnessMessage,
6821
+ mayReplace,
6822
+ shouldRefetch
6823
+ },
6761
6824
  skill: {
6762
6825
  linkHealth,
6763
6826
  linkHealthMessage,
@@ -38,16 +38,11 @@ function runtimeHome(env = process.env) {
38
38
  }
39
39
 
40
40
  // src/platform/pricing.ts
41
- function readJsonFile(path) {
42
- if (!existsSync(path)) {
43
- return null;
44
- }
45
- try {
46
- return JSON.parse(readFileSync(path, "utf8"));
47
- } catch {
48
- return null;
49
- }
50
- }
41
+ var FALLBACK_PLANE = "litellm";
42
+ var MODEL_ALIASES = {
43
+ "cursor-grok-4.5": "grok-4.5",
44
+ auto: "auto-cost"
45
+ };
51
46
  function stripMeta(table) {
52
47
  if (!table) {
53
48
  return {};
@@ -56,7 +51,7 @@ function stripMeta(table) {
56
51
  return rest;
57
52
  }
58
53
  function slugifyModelName(name) {
59
- return name.trim().toLowerCase().replace(/[[\]]/g, "").replace(/\(.*?\)/g, "").replace(/[^a-z0-9.+]+/g, "-").replace(/^-+|-+$/g, "");
54
+ return name.trim().toLowerCase().replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/[[\]]/g, "").replace(/[^a-z0-9.+]+/g, "-").replace(/^-+|-+$/g, "");
60
55
  }
61
56
  function candidatesFor(model, aliases) {
62
57
  const trimmed = model.trim();
@@ -93,28 +88,55 @@ function fuzzyFind(table, needle) {
93
88
  }
94
89
  return;
95
90
  }
96
- function overridesPath() {
91
+ function cataloguePath() {
97
92
  return join2(runtimeHome(), "model-prices.json");
98
93
  }
99
- function providerNativePath(provider) {
100
- return join2(runtimeHome(), `model-prices.${provider}.json`);
94
+ function overridesPath() {
95
+ return join2(runtimeHome(), "model-prices.local.json");
96
+ }
97
+ var cache = new Map;
98
+ function readCatalogue(path) {
99
+ if (!existsSync(path)) {
100
+ cache.delete(path);
101
+ return null;
102
+ }
103
+ let text;
104
+ try {
105
+ text = readFileSync(path, "utf8");
106
+ } catch {
107
+ cache.delete(path);
108
+ return null;
109
+ }
110
+ const hit = cache.get(path);
111
+ if (hit && hit.text === text) {
112
+ return hit.value;
113
+ }
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(text);
117
+ } catch {
118
+ cache.delete(path);
119
+ return null;
120
+ }
121
+ cache.set(path, { text, value: parsed });
122
+ return parsed;
101
123
  }
102
- function litellmPath() {
103
- return join2(runtimeHome(), "model-prices.litellm.json");
124
+ function loadCatalogue() {
125
+ return readCatalogue(cataloguePath()) ?? {};
104
126
  }
105
- function aliasesPath() {
106
- return join2(runtimeHome(), "model-aliases.json");
127
+ function planeFor(catalogue, plane) {
128
+ return stripMeta(catalogue.planes?.[plane] ?? null);
107
129
  }
108
130
  function resolveModelPrice(provider, model) {
109
131
  const trimmed = model.trim();
110
132
  if (!trimmed) {
111
133
  return;
112
134
  }
113
- const overrides = stripMeta(readJsonFile(overridesPath()));
114
- const native = stripMeta(readJsonFile(providerNativePath(provider)));
115
- const litellm = stripMeta(readJsonFile(litellmPath()));
116
- const aliases = readJsonFile(aliasesPath()) ?? {};
117
- const candidates = candidatesFor(trimmed, aliases);
135
+ const catalogue = loadCatalogue();
136
+ const overrides = stripMeta(readCatalogue(overridesPath()));
137
+ const native = provider ? planeFor(catalogue, provider) : {};
138
+ const litellm = planeFor(catalogue, FALLBACK_PLANE);
139
+ const candidates = candidatesFor(trimmed, MODEL_ALIASES);
118
140
  for (const id of candidates) {
119
141
  const entry = overrides[id];
120
142
  if (entry) {
@@ -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 home = runtimeHome2.replace(/\/+$/, "");
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 { command, argsPrefix } = commandFor(runtime);
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,