@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/doctor.mjs CHANGED
@@ -3,9 +3,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
4
  // tools/doctor.ts
5
5
  import { spawnSync } from "node:child_process";
6
- import { existsSync as existsSync29, lstatSync as lstatSync2, readFileSync as readFileSync31, readlinkSync, realpathSync as realpathSync4 } from "node:fs";
6
+ import { existsSync as existsSync30, lstatSync as lstatSync2, readFileSync as readFileSync32, readlinkSync, realpathSync as realpathSync4 } from "node:fs";
7
7
  import { homedir as homedir3, platform as osPlatform } from "node:os";
8
- import { basename as basename5, dirname as dirname9, join as join30 } from "node:path";
8
+ import { basename as basename5, delimiter as delimiter3, dirname as dirname9, join as join31 } from "node:path";
9
9
 
10
10
  // bin/tlc-cli.ts
11
11
  import {
@@ -5288,6 +5288,62 @@ function release(root, provider, session) {
5288
5288
  deletePresenceRecord(root, provider, session);
5289
5289
  }
5290
5290
 
5291
+ // src/core/pricing/pricing.freshness.ts
5292
+ var DEFAULT_TTL_DAYS = 7;
5293
+ var MS_PER_DAY = 86400000;
5294
+ function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
5295
+ if (meta === null) {
5296
+ return { state: "absent" };
5297
+ }
5298
+ const stamp = meta.refreshedAt;
5299
+ if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
5300
+ return { state: "undated" };
5301
+ }
5302
+ const ageMs = now.getTime() - Date.parse(stamp);
5303
+ const ageDays = Math.max(0, ageMs / MS_PER_DAY);
5304
+ return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
5305
+ }
5306
+ function shouldRefetch(state) {
5307
+ return state.state === "absent" || state.state === "undated" || state.state === "stale";
5308
+ }
5309
+ function freshnessMessage(state, catalogue) {
5310
+ switch (state.state) {
5311
+ case "absent":
5312
+ return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
5313
+ case "undated":
5314
+ return `${catalogue}: present but carries no date — it will be refetched`;
5315
+ case "fresh":
5316
+ return `${catalogue}: ${describeAge(state.ageDays)} old`;
5317
+ default:
5318
+ return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
5319
+ }
5320
+ }
5321
+ var MIN_RETAINED_RATIO = 0.5;
5322
+ function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
5323
+ if (incomingCount === 0) {
5324
+ return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
5325
+ }
5326
+ if (existingCount === 0) {
5327
+ return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
5328
+ }
5329
+ if (incomingCount >= existingCount) {
5330
+ return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
5331
+ }
5332
+ const retained = incomingCount / existingCount;
5333
+ return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
5334
+ replace: false,
5335
+ reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
5336
+ };
5337
+ }
5338
+ function describeAge(ageDays) {
5339
+ if (ageDays < 1) {
5340
+ const hours = Math.max(1, Math.round(ageDays * 24));
5341
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
5342
+ }
5343
+ const days = Math.round(ageDays);
5344
+ return `${days} day${days === 1 ? "" : "s"}`;
5345
+ }
5346
+
5291
5347
  // src/core/release/release.decisions.ts
5292
5348
  import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
5293
5349
  import { join as join18 } from "node:path";
@@ -5637,7 +5693,8 @@ function linkHealth(target, runtimeHome2, probe) {
5637
5693
  if (!probe.exists(resolved)) {
5638
5694
  return { state: "dangling", target, resolved };
5639
5695
  }
5640
- const home = runtimeHome2.replace(/\/+$/, "");
5696
+ const resolveHome = probe.realpath ?? ((path) => path);
5697
+ const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
5641
5698
  return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5642
5699
  }
5643
5700
  function linkHealthMessage(health) {
@@ -6784,6 +6841,12 @@ var coreFacade = {
6784
6841
  coversHandler,
6785
6842
  decideShim
6786
6843
  },
6844
+ pricing: {
6845
+ freshness,
6846
+ freshnessMessage,
6847
+ mayReplace,
6848
+ shouldRefetch
6849
+ },
6787
6850
  skill: {
6788
6851
  linkHealth,
6789
6852
  linkHealthMessage,
@@ -6993,6 +7056,7 @@ function writeStdout(text) {
6993
7056
  }
6994
7057
 
6995
7058
  // bin/tlc-cli.ts
7059
+ var NPM_PACKAGE = "@tech-leads-club/harness-toolkit";
6996
7060
  var NPM_MARKER = "installed-from-npm";
6997
7061
  function classifyRuntimePath(dest, probe) {
6998
7062
  if (probe.isSymlink(dest)) {
@@ -7026,19 +7090,17 @@ import { delimiter as delimiter2, dirname as dirname7, join as join27 } from "no
7026
7090
  function isPackagedCopy(candidate) {
7027
7091
  return candidate.split(/[/\\]/).includes("node_modules");
7028
7092
  }
7029
- function bunExecutableName(platform = process.platform) {
7030
- return platform === "win32" ? "bun.exe" : "bun";
7031
- }
7032
- function findBunOnPath(env = process.env, platform = process.platform) {
7033
- const pathValue = env.PATH ?? "";
7034
- const bunName = bunExecutableName(platform);
7035
- for (const dir of pathValue.split(delimiter2)) {
7093
+ var BUN_EXECUTABLE_NAMES = ["bun", "bun.exe"];
7094
+ function findBunOnPath(env = process.env) {
7095
+ for (const dir of (env.PATH ?? "").split(delimiter2)) {
7036
7096
  if (!dir) {
7037
7097
  continue;
7038
7098
  }
7039
- const candidate = join27(dir, bunName);
7040
- if (existsSync26(candidate)) {
7041
- return candidate;
7099
+ for (const name of BUN_EXECUTABLE_NAMES) {
7100
+ const candidate = join27(dir, name);
7101
+ if (existsSync26(candidate)) {
7102
+ return candidate;
7103
+ }
7042
7104
  }
7043
7105
  }
7044
7106
  return null;
@@ -7814,14 +7876,9 @@ var ENTRY_SPECS2 = [
7814
7876
  { hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
7815
7877
  { hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
7816
7878
  ];
7817
- function commandFor(runtime) {
7818
- if (process.platform === "win32") {
7819
- return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
7820
- }
7821
- return { command: "node", argsPrefix: [runtime.launcherPath] };
7822
- }
7823
7879
  function cursorWiring(runtime) {
7824
- const { command, argsPrefix } = commandFor(runtime);
7880
+ const command = "node";
7881
+ const argsPrefix = [runtime.launcherPath];
7825
7882
  const entries = ENTRY_SPECS2.map((spec) => ({
7826
7883
  hookEvent: spec.hookEvent,
7827
7884
  handler: spec.handler,
@@ -7913,6 +7970,50 @@ function isCursorWired(targetPath) {
7913
7970
  }
7914
7971
  if (false) {}
7915
7972
 
7973
+ // src/platform/pricing.ts
7974
+ import { existsSync as existsSync29, readFileSync as readFileSync31 } from "node:fs";
7975
+ import { join as join30 } from "node:path";
7976
+ function cataloguePath() {
7977
+ return join30(runtimeHome(), "model-prices.json");
7978
+ }
7979
+ var cache = new Map;
7980
+ function readCatalogue(path) {
7981
+ if (!existsSync29(path)) {
7982
+ cache.delete(path);
7983
+ return null;
7984
+ }
7985
+ let text;
7986
+ try {
7987
+ text = readFileSync31(path, "utf8");
7988
+ } catch {
7989
+ cache.delete(path);
7990
+ return null;
7991
+ }
7992
+ const hit = cache.get(path);
7993
+ if (hit && hit.text === text) {
7994
+ return hit.value;
7995
+ }
7996
+ let parsed;
7997
+ try {
7998
+ parsed = JSON.parse(text);
7999
+ } catch {
8000
+ cache.delete(path);
8001
+ return null;
8002
+ }
8003
+ cache.set(path, { text, value: parsed });
8004
+ return parsed;
8005
+ }
8006
+ function loadCatalogue() {
8007
+ return readCatalogue(cataloguePath()) ?? {};
8008
+ }
8009
+ function planeMeta() {
8010
+ return loadCatalogue()._meta?.planes ?? {};
8011
+ }
8012
+ function catalogueMeta() {
8013
+ const parsed = readCatalogue(cataloguePath());
8014
+ return parsed === null ? null : parsed._meta ?? {};
8015
+ }
8016
+
7916
8017
  // tools/doctor.ts
7917
8018
  function plural(count, word) {
7918
8019
  return `${count} ${word}${count === 1 ? "" : "s"}`;
@@ -7986,10 +8087,17 @@ function checkSkillLinks(home, providerDirs = providerConfigDirs(), probe = {
7986
8087
  }
7987
8088
  }
7988
8089
  },
7989
- exists: existsSync29
8090
+ exists: existsSync30,
8091
+ realpath: (path) => {
8092
+ try {
8093
+ return realpathSync4(path);
8094
+ } catch {
8095
+ return path;
8096
+ }
8097
+ }
7990
8098
  }) {
7991
- return providerDirs.filter((dir) => existsSync29(dir)).map((dir) => {
7992
- const health = coreFacade.skill.linkHealth(join30(dir, "skills", "harness-init"), home, probe);
8099
+ return providerDirs.filter((dir) => existsSync30(dir)).map((dir) => {
8100
+ const health = coreFacade.skill.linkHealth(join31(dir, "skills", "harness-init"), home, probe);
7993
8101
  return {
7994
8102
  level: health.state === "ok" ? "ok" : "fail",
7995
8103
  name: `init skill (${basename5(dir)})`,
@@ -7997,24 +8105,50 @@ function checkSkillLinks(home, providerDirs = providerConfigDirs(), probe = {
7997
8105
  };
7998
8106
  });
7999
8107
  }
8108
+ function checkPrices(now = new Date, read = { meta: catalogueMeta, planes: planeMeta }) {
8109
+ const state = coreFacade.pricing.freshness(read.meta(), now);
8110
+ const planes = read.planes();
8111
+ const named = Object.entries(planes).map(([plane, meta]) => `${plane} ${meta.count ?? 0}`).join(", ");
8112
+ return [
8113
+ {
8114
+ level: state.state === "fresh" ? "ok" : "warn",
8115
+ name: "prices",
8116
+ detail: state.state === "fresh" ? `${coreFacade.pricing.freshnessMessage(state, "catalogue")}${named ? ` (${named})` : ""}` : coreFacade.pricing.freshnessMessage(state, "catalogue")
8117
+ }
8118
+ ];
8119
+ }
8120
+ function resolveOnPath(command, env = process.env, exists = existsSync30) {
8121
+ for (const dir of (env.PATH ?? "").split(delimiter3)) {
8122
+ if (!dir) {
8123
+ continue;
8124
+ }
8125
+ for (const name of [command, `${command}.cmd`, `${command}.exe`, `${command}.ps1`]) {
8126
+ const candidate = join31(dir, name);
8127
+ if (exists(candidate)) {
8128
+ return candidate;
8129
+ }
8130
+ }
8131
+ }
8132
+ return null;
8133
+ }
8000
8134
  function checkRuntimePaths(home, platform) {
8001
- const launcher = join30(home, "bin", "tlc-exec.mjs");
8002
- const distSample = join30(home, "dist", "stop.mjs");
8003
- const cliLink = join30(homedir3(), ".local", "bin", platform === "win32" ? "tlc.cmd" : "tlc");
8135
+ const launcher = join31(home, "bin", "tlc-exec.mjs");
8136
+ const distSample = join31(home, "dist", "stop.mjs");
8137
+ const onPath = resolveOnPath("tlc");
8004
8138
  return [
8005
8139
  { level: "ok", name: "platform", detail: platform },
8006
- { level: existsSync29(launcher) ? "ok" : "fail", name: "global runtime", detail: home },
8140
+ { level: existsSync30(launcher) ? "ok" : "fail", name: "global runtime", detail: home },
8007
8141
  runtimeOwnershipCheck(home),
8008
8142
  {
8009
- level: existsSync29(distSample) ? "ok" : "fail",
8143
+ level: existsSync30(distSample) ? "ok" : "fail",
8010
8144
  name: "dist bundles",
8011
- detail: existsSync29(distSample) ? join30(home, "dist") : "missing — run: tlc harness build"
8145
+ detail: existsSync30(distSample) ? join31(home, "dist") : "missing — run: tlc harness build"
8012
8146
  },
8013
- { level: existsSync29(launcher) ? "ok" : "fail", name: "portable launcher", detail: launcher },
8147
+ { level: existsSync30(launcher) ? "ok" : "fail", name: "portable launcher", detail: launcher },
8014
8148
  {
8015
- level: existsSync29(cliLink) || existsSync29(join30(home, "bin", platform === "win32" ? "tlc.cmd" : "tlc")) ? "ok" : "fail",
8149
+ level: onPath === null ? "fail" : "ok",
8016
8150
  name: "CLI on PATH",
8017
- detail: cliLink
8151
+ detail: onPath ?? `no \`tlc\` on PATH — npm i -g ${NPM_PACKAGE}, or \`npm link\` from a clone`
8018
8152
  }
8019
8153
  ];
8020
8154
  }
@@ -8033,15 +8167,15 @@ function wiringProblems(wiring) {
8033
8167
  if (wiring.strategy !== "replace") {
8034
8168
  return [];
8035
8169
  }
8036
- const text = existsSync29(wiring.target) ? readFileSync31(wiring.target, "utf8") : null;
8037
- return cursorWiringProblems(text, { launcherPath: launcherPathOf(wiring) }, existsSync29);
8170
+ const text = existsSync30(wiring.target) ? readFileSync32(wiring.target, "utf8") : null;
8171
+ return cursorWiringProblems(text, { launcherPath: launcherPathOf(wiring) }, existsSync30);
8038
8172
  }
8039
8173
  function launcherPathOf(wiring) {
8040
8174
  const first = wiring.entries[0];
8041
8175
  return first?.args.find((arg) => arg.endsWith(".mjs")) ?? "";
8042
8176
  }
8043
8177
  function providerWiringStatus(wiring) {
8044
- if (!existsSync29(dirname9(wiring.target))) {
8178
+ if (!existsSync30(dirname9(wiring.target))) {
8045
8179
  return "not-installed";
8046
8180
  }
8047
8181
  if (wiring.strategy === "replace") {
@@ -8050,12 +8184,12 @@ function providerWiringStatus(wiring) {
8050
8184
  }
8051
8185
  return wiringProblems(wiring).length === 0 ? "wired" : "detected-but-unwired";
8052
8186
  }
8053
- const existingText = existsSync29(wiring.target) ? readFileSync31(wiring.target, "utf8") : null;
8187
+ const existingText = existsSync30(wiring.target) ? readFileSync32(wiring.target, "utf8") : null;
8054
8188
  const result = mergeClaudeSettings(existingText, wiring.entries);
8055
8189
  return result.ok && !result.changed ? "wired" : "detected-but-unwired";
8056
8190
  }
8057
8191
  function checkProviders(registry, home) {
8058
- const launcherPath = join30(home, "bin", "tlc-exec.mjs");
8192
+ const launcherPath = join31(home, "bin", "tlc-exec.mjs");
8059
8193
  return registry.map((provider) => {
8060
8194
  const wiring = provider.wiring({ launcherPath });
8061
8195
  const status = providerWiringStatus(wiring);
@@ -8239,12 +8373,12 @@ function checkProjectPolicy(root) {
8239
8373
  {
8240
8374
  level: "ok",
8241
8375
  name: "project policy",
8242
- detail: existsSync29(configPath) ? configPath : "missing — run: tlc harness init"
8376
+ detail: existsSync30(configPath) ? configPath : "missing — run: tlc harness init"
8243
8377
  },
8244
8378
  {
8245
8379
  level: "ok",
8246
8380
  name: "state dir",
8247
- detail: existsSync29(stateDir) ? stateDir : `${stateDir} (created on first session)`
8381
+ detail: existsSync30(stateDir) ? stateDir : `${stateDir} (created on first session)`
8248
8382
  },
8249
8383
  checkPosture(root),
8250
8384
  ...checkObservedRails(root),
@@ -8255,8 +8389,8 @@ function checkProjectPolicy(root) {
8255
8389
  ];
8256
8390
  }
8257
8391
  function checkGlobalCommands(home) {
8258
- const globalCommands = join30(home, ".cursor", "commands");
8259
- if (!existsSync29(globalCommands)) {
8392
+ const globalCommands = join31(home, ".cursor", "commands");
8393
+ if (!existsSync30(globalCommands)) {
8260
8394
  return {
8261
8395
  level: "ok",
8262
8396
  name: "global commands dir",
@@ -8280,6 +8414,7 @@ function runChecks(ctx) {
8280
8414
  ...checkProviders(ctx.registry, ctx.runtimeHome),
8281
8415
  ...checkProjectPolicy(ctx.root),
8282
8416
  ...checkCapabilities(ctx.root, ctx.runtimeHome),
8417
+ ...checkPrices(),
8283
8418
  checkGlobalCommands(ctx.home)
8284
8419
  ];
8285
8420
  }
@@ -8349,6 +8484,7 @@ export {
8349
8484
  toReport,
8350
8485
  runtimeOwnershipCheck,
8351
8486
  runChecks,
8487
+ resolveOnPath,
8352
8488
  providerWiringStatus,
8353
8489
  plural,
8354
8490
  medianMs,
@@ -8360,6 +8496,7 @@ export {
8360
8496
  checkRuntimePaths,
8361
8497
  checkProviders,
8362
8498
  checkProjectPolicy,
8499
+ checkPrices,
8363
8500
  checkNodeVersion,
8364
8501
  checkLessonHealth,
8365
8502
  checkId,
File without changes
@@ -811,12 +811,7 @@ function usageText(style = PLAIN) {
811
811
  function launcherPath(home = runtimeHome()) {
812
812
  return join4(home, "bin", "tlc-exec.mjs");
813
813
  }
814
- function shimCommand(platform = process.platform) {
815
- if (platform === "win32") {
816
- return { command: "cmd", argsPrefix: ["/c", "node"] };
817
- }
818
- return { command: "node", argsPrefix: [] };
819
- }
814
+ var SHIM_COMMAND = { command: "node", argsPrefix: [] };
820
815
  var CURSOR_SHIM_SPECS = [
821
816
  { hookEvent: "sessionStart", handler: "session-start", timeoutSeconds: 10 },
822
817
  { hookEvent: "sessionEnd", handler: "session-end", timeoutSeconds: 10 },
@@ -837,7 +832,7 @@ var CLAUDE_SHIM_SPECS = [
837
832
  { hookEvent: "MessageDisplay", handler: "response-after", timeoutSeconds: 5 }
838
833
  ];
839
834
  function cursorShimEntries(launcher) {
840
- const { command, argsPrefix } = shimCommand();
835
+ const { command, argsPrefix } = SHIM_COMMAND;
841
836
  return CURSOR_SHIM_SPECS.map((spec) => ({
842
837
  hookEvent: spec.hookEvent,
843
838
  handler: spec.handler,
@@ -2,8 +2,9 @@ import { createRequire } from "node:module";
2
2
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
4
  // tools/install-runtime.ts
5
- import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
- import { join as join2, relative, resolve, sep } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, rmSync as rmSync2, writeFileSync } from "node:fs";
7
+ import { join as join3, relative, resolve, sep } from "node:path";
7
8
 
8
9
  // src/platform/paths.ts
9
10
  import { homedir } from "node:os";
@@ -849,6 +850,34 @@ var TOOL_KINDS = new Set([
849
850
  "file.edit",
850
851
  "file.read"
851
852
  ]);
853
+ // src/platform/links.ts
854
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
855
+ import { dirname, join as join2 } from "node:path";
856
+ var LINK_TYPE = "junction";
857
+ function linkDir(source, target) {
858
+ let replaced = false;
859
+ if (isLink(target)) {
860
+ rmSync(target, { recursive: true, force: true });
861
+ replaced = true;
862
+ } else if (existsSync(target)) {
863
+ return {
864
+ kind: "refused",
865
+ target,
866
+ reason: `${target} exists and is not a link — move it aside and re-run`
867
+ };
868
+ }
869
+ mkdirSync(dirname(target), { recursive: true });
870
+ symlinkSync(source, target, LINK_TYPE);
871
+ return { kind: replaced ? "relinked" : "linked", target, source };
872
+ }
873
+ function isLink(path) {
874
+ try {
875
+ return lstatSync(path).isSymbolicLink();
876
+ } catch {
877
+ return false;
878
+ }
879
+ }
880
+
852
881
  // bin/tlc-cli.ts
853
882
  var NPM_PACKAGE = "@tech-leads-club/harness-toolkit";
854
883
  var NPM_MARKER = "installed-from-npm";
@@ -864,13 +893,10 @@ var RUNTIME_PAYLOAD = [
864
893
  "src",
865
894
  "tools",
866
895
  "config.example.json",
867
- "model-aliases.json",
868
- "model-prices.cursor.json",
869
- "model-prices.json",
870
896
  "package.json"
871
897
  ];
872
898
  var OPERATOR_OWNED = ["config.json", "state", "flags"];
873
- var NOT_SHIPPED = [join2("tools", "dev"), join2("tools", "__test__")];
899
+ var NOT_SHIPPED = [join3("tools", "dev"), join3("tools", "__test__")];
874
900
  function isShipped(relativePath) {
875
901
  const normalised = relativePath.split(sep).join("/");
876
902
  return !NOT_SHIPPED.some((excluded) => {
@@ -890,34 +916,75 @@ function installRuntime(source, dest) {
890
916
  if (resolve(source) === resolve(dest)) {
891
917
  return { kind: "in-place", source, dest, entries: [], missing: [] };
892
918
  }
893
- mkdirSync(dest, { recursive: true });
919
+ mkdirSync2(dest, { recursive: true });
894
920
  const entries = [];
895
921
  const missing = [];
896
922
  for (const entry of RUNTIME_PAYLOAD) {
897
- const from = join2(source, entry);
898
- if (!existsSync(from)) {
923
+ const from = join3(source, entry);
924
+ if (!existsSync2(from)) {
899
925
  missing.push(entry);
900
926
  continue;
901
927
  }
902
- const to = join2(dest, entry);
903
- rmSync(to, { recursive: true, force: true });
928
+ const to = join3(dest, entry);
929
+ rmSync2(to, { recursive: true, force: true });
904
930
  cpSync(from, to, {
905
931
  recursive: true,
906
932
  filter: (src) => isShipped(relative(source, src))
907
933
  });
908
934
  entries.push(entry);
909
935
  }
910
- writeFileSync(join2(dest, NPM_MARKER), `Installed by \`tlc harness install\` from ${source}.
936
+ writeFileSync(join3(dest, NPM_MARKER), `Installed by \`tlc harness install\` from ${source}.
911
937
  Update with: npm i -g ${NPM_PACKAGE}@latest && tlc harness install
912
938
  `, "utf8");
913
- const config = join2(dest, "config.json");
914
- const example = join2(dest, "config.example.json");
915
- if (!existsSync(config) && existsSync(example)) {
939
+ const config = join3(dest, "config.json");
940
+ const example = join3(dest, "config.example.json");
941
+ if (!existsSync2(config) && existsSync2(example)) {
916
942
  writeFileSync(config, readFileSync(example, "utf8"), "utf8");
917
943
  }
918
944
  return { kind: "copied", source, dest, entries, missing };
919
945
  }
946
+ function linkRuntime(source, dest) {
947
+ if (resolve(source) === resolve(dest)) {
948
+ return { kind: "in-place", source, dest, entries: [], missing: [] };
949
+ }
950
+ const outcome = linkDir(resolve(source), dest);
951
+ if (outcome.kind === "refused") {
952
+ return { kind: "refused", source, dest, entries: [], missing: [], reason: outcome.reason };
953
+ }
954
+ const missing = RUNTIME_PAYLOAD.filter((entry) => !existsSync2(join3(dest, entry)));
955
+ return { kind: outcome.kind === "relinked" ? "relinked" : "linked", source, dest, entries: [], missing };
956
+ }
920
957
  function installScreen(report) {
958
+ if (report.kind === "refused") {
959
+ return {
960
+ title: "harness install",
961
+ sections: [{ rows: [{ label: "refused", value: report.reason ?? "", level: "fail" }] }]
962
+ };
963
+ }
964
+ if (report.kind === "linked" || report.kind === "relinked") {
965
+ return {
966
+ title: "harness install",
967
+ sections: [
968
+ {
969
+ rows: [
970
+ {
971
+ label: report.kind === "linked" ? "linked" : "relinked",
972
+ value: `${report.dest} → ${report.source}`,
973
+ level: "ok"
974
+ },
975
+ ...report.missing.length > 0 ? [
976
+ {
977
+ label: "incomplete",
978
+ value: `the checkout has no ${report.missing.join(", ")} — run the build`,
979
+ level: "fail"
980
+ }
981
+ ] : []
982
+ ]
983
+ }
984
+ ],
985
+ footer: "an edit in the checkout is live in the next hook · `npm link` puts `tlc` on PATH"
986
+ };
987
+ }
921
988
  if (report.kind === "in-place") {
922
989
  return {
923
990
  title: "harness install",
@@ -949,20 +1016,36 @@ function installDest(env = process.env) {
949
1016
  }
950
1017
  return runtimeHomeWasChosen(env) ? runtimeHome(env) : conventionalRuntimeHome();
951
1018
  }
1019
+ function fetchPrices(dest, spawn = spawnSync) {
1020
+ const result = spawn(process.execPath, [join3(dest, "bin", "tlc-exec.mjs"), "refresh-model-prices"], {
1021
+ stdio: "inherit",
1022
+ env: { ...process.env, TLC_HOME: dest }
1023
+ });
1024
+ if ((result.status ?? 1) !== 0) {
1025
+ console.log("install: prices not fetched — cost estimates stay empty until `tlc harness prices refresh`");
1026
+ }
1027
+ }
952
1028
  if (__require.main == __require.module) {
953
- const source = originRoot();
1029
+ const link = process.argv.includes("--link");
1030
+ const source = link ? process.cwd() : originRoot();
954
1031
  const dest = installDest();
955
- const report = installRuntime(source, dest);
1032
+ const report = link ? linkRuntime(source, dest) : installRuntime(source, dest);
956
1033
  console.log(installReportText(report, createStyle()));
1034
+ if (report.kind === "refused") {
1035
+ process.exit(1);
1036
+ }
1037
+ fetchPrices(dest);
957
1038
  process.exit(report.missing.length > 0 ? 1 : 0);
958
1039
  }
959
1040
  export {
960
1041
  originRoot,
1042
+ linkRuntime,
961
1043
  isShipped,
962
1044
  installScreen,
963
1045
  installRuntime,
964
1046
  installReportText,
965
1047
  installDest,
1048
+ fetchPrices,
966
1049
  RUNTIME_PAYLOAD,
967
1050
  OPERATOR_OWNED,
968
1051
  NOT_SHIPPED
@@ -5258,6 +5258,62 @@ function release(root, provider, session) {
5258
5258
  deletePresenceRecord(root, provider, session);
5259
5259
  }
5260
5260
 
5261
+ // src/core/pricing/pricing.freshness.ts
5262
+ var DEFAULT_TTL_DAYS = 7;
5263
+ var MS_PER_DAY = 86400000;
5264
+ function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
5265
+ if (meta === null) {
5266
+ return { state: "absent" };
5267
+ }
5268
+ const stamp = meta.refreshedAt;
5269
+ if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
5270
+ return { state: "undated" };
5271
+ }
5272
+ const ageMs = now.getTime() - Date.parse(stamp);
5273
+ const ageDays = Math.max(0, ageMs / MS_PER_DAY);
5274
+ return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
5275
+ }
5276
+ function shouldRefetch(state) {
5277
+ return state.state === "absent" || state.state === "undated" || state.state === "stale";
5278
+ }
5279
+ function freshnessMessage(state, catalogue) {
5280
+ switch (state.state) {
5281
+ case "absent":
5282
+ return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
5283
+ case "undated":
5284
+ return `${catalogue}: present but carries no date — it will be refetched`;
5285
+ case "fresh":
5286
+ return `${catalogue}: ${describeAge(state.ageDays)} old`;
5287
+ default:
5288
+ return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
5289
+ }
5290
+ }
5291
+ var MIN_RETAINED_RATIO = 0.5;
5292
+ function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
5293
+ if (incomingCount === 0) {
5294
+ return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
5295
+ }
5296
+ if (existingCount === 0) {
5297
+ return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
5298
+ }
5299
+ if (incomingCount >= existingCount) {
5300
+ return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
5301
+ }
5302
+ const retained = incomingCount / existingCount;
5303
+ return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
5304
+ replace: false,
5305
+ reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
5306
+ };
5307
+ }
5308
+ function describeAge(ageDays) {
5309
+ if (ageDays < 1) {
5310
+ const hours = Math.max(1, Math.round(ageDays * 24));
5311
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
5312
+ }
5313
+ const days = Math.round(ageDays);
5314
+ return `${days} day${days === 1 ? "" : "s"}`;
5315
+ }
5316
+
5261
5317
  // src/core/release/release.decisions.ts
5262
5318
  import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
5263
5319
  import { join as join18 } from "node:path";
@@ -5607,7 +5663,8 @@ function linkHealth(target, runtimeHome2, probe) {
5607
5663
  if (!probe.exists(resolved)) {
5608
5664
  return { state: "dangling", target, resolved };
5609
5665
  }
5610
- const home = runtimeHome2.replace(/\/+$/, "");
5666
+ const resolveHome = probe.realpath ?? ((path) => path);
5667
+ const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
5611
5668
  return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5612
5669
  }
5613
5670
  function linkHealthMessage(health) {
@@ -6754,6 +6811,12 @@ var coreFacade = {
6754
6811
  coversHandler,
6755
6812
  decideShim
6756
6813
  },
6814
+ pricing: {
6815
+ freshness,
6816
+ freshnessMessage,
6817
+ mayReplace,
6818
+ shouldRefetch
6819
+ },
6757
6820
  skill: {
6758
6821
  linkHealth,
6759
6822
  linkHealthMessage,
@@ -6972,6 +7035,9 @@ var NO_HUMAN_MODES = new Set(["bypassPermissions", "dontAsk"]);
6972
7035
  // bin/write-user-hooks.mjs
6973
7036
  if (false) {}
6974
7037
 
7038
+ // src/platform/pricing.ts
7039
+ var cache = new Map;
7040
+
6975
7041
  // tools/doctor.ts
6976
7042
  function plural(count, word) {
6977
7043
  return `${count} ${word}${count === 1 ? "" : "s"}`;