@tech-leads-club/harness-toolkit 0.2.1 → 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 +117 -58
- package/bin/tlc-exec.mjs +16 -13
- package/dist/compact-before.mjs +151 -15
- package/dist/doctor.mjs +288 -45
- package/dist/help-topic.mjs +0 -0
- package/dist/init-project.mjs +15 -14
- package/dist/install-runtime.mjs +100 -17
- package/dist/lessons-cli.mjs +152 -8
- package/dist/obs-cli.mjs +149 -8
- package/dist/price-lookup.mjs +45 -23
- package/dist/prompt-submit.mjs +151 -15
- package/dist/refresh-model-prices.mjs +7190 -46
- package/dist/response-after.mjs +151 -15
- package/dist/run.mjs +151 -15
- package/dist/session-end.mjs +151 -15
- package/dist/session-start.mjs +151 -15
- package/dist/shim.mjs +7040 -16
- package/dist/stop.mjs +151 -15
- package/dist/subagent-start.mjs +151 -15
- package/dist/subagent-stop.mjs +151 -15
- package/dist/support.mjs +149 -8
- package/dist/tlc-cli.mjs +289 -89
- package/dist/tool-after.mjs +196 -38
- package/dist/tool-before.mjs +151 -15
- package/dist/tool-failure.mjs +151 -15
- package/dist/uninstall-runtime.mjs +9 -10
- package/docs/log.md +7 -0
- package/docs/measure.md +35 -31
- package/package.json +6 -5
- package/src/core/core.facade.ts +18 -0
- package/src/core/index.ts +3 -0
- package/src/core/pricing/pricing.freshness.ts +118 -0
- package/src/core/release/release.version.ts +147 -0
- package/src/core/shim/shim.precedence.ts +72 -0
- package/src/core/skill/skill.link.ts +92 -0
- package/src/entrypoints/shim.ts +49 -11
- package/src/platform/fs-atomic.ts +61 -23
- package/src/platform/links.ts +73 -0
- package/src/platform/paths.ts +27 -0
- package/src/platform/pricing.ts +139 -31
- package/src/providers/cursor/cursor.wiring.ts +11 -8
- package/tools/doctor.ts +127 -10
- package/tools/init-project.ts +34 -14
- package/tools/install-runtime.ts +89 -6
- package/tools/refresh-model-prices.ts +242 -75
- package/tools/uninstall-runtime.ts +23 -19
- package/CHANGELOG.md +0 -95
- package/bin/tlc-build +0 -80
- package/bin/tlc-exec +0 -10
- package/bin/tlc-exec.cmd +0 -4
- package/docs/decisions/ad-001.md +0 -32
- package/docs/decisions/ad-002.md +0 -51
- package/docs/decisions/ad-003.md +0 -30
- package/docs/decisions/ad-004.md +0 -37
- package/docs/decisions/ad-005.md +0 -36
- package/docs/decisions/ad-006.md +0 -49
- package/docs/decisions/ad-007.md +0 -36
- package/docs/decisions/ad-008.md +0 -54
- package/docs/decisions/ad-009.md +0 -61
- package/docs/decisions/ad-010.md +0 -45
- package/docs/decisions/ad-011.md +0 -59
- package/docs/decisions/ad-012.md +0 -71
- package/docs/decisions/ad-013.md +0 -87
- package/docs/decisions/ad-014.md +0 -56
- package/docs/decisions/ad-015.md +0 -33
- package/docs/decisions/ad-016.md +0 -98
- package/docs/decisions/ad-017.md +0 -65
- package/docs/decisions/ad-018.md +0 -77
- package/docs/decisions/ad-019.md +0 -75
- package/docs/decisions/ad-020.md +0 -88
- package/docs/decisions/ad-021.md +0 -57
- package/docs/decisions/ad-022.md +0 -120
- package/docs/decisions/ad-023.md +0 -87
- package/docs/decisions/ad-024.md +0 -85
- package/docs/decisions/ad-025.md +0 -92
- package/docs/decisions/ad-026.md +0 -131
- package/docs/decisions/ad-027.md +0 -90
- package/docs/decisions/ad-028.md +0 -90
- package/docs/decisions/ad-029.md +0 -73
- package/docs/decisions/ad-030.md +0 -97
- package/docs/decisions/ad-031.md +0 -94
- package/docs/decisions/ad-032.md +0 -83
- package/docs/decisions/ad-033.md +0 -89
- package/docs/decisions/ad-034.md +0 -86
- package/docs/decisions/ad-035.md +0 -86
- package/docs/decisions/ad-036.md +0 -68
- package/docs/decisions/ad-037.md +0 -47
- package/docs/decisions/ad-038.md +0 -52
- package/docs/decisions/ad-039.md +0 -69
- package/docs/decisions/ad-040.md +0 -89
- package/docs/decisions/ad-041.md +0 -98
- package/docs/decisions/ad-042.md +0 -82
- package/docs/decisions/ad-043.md +0 -79
- package/docs/decisions/ad-044.md +0 -61
- package/docs/decisions/ad-045.md +0 -94
- package/docs/decisions/ad-046.md +0 -111
- package/docs/decisions/ad-047.md +0 -96
- package/docs/decisions/ad-048.md +0 -85
- package/docs/decisions/ad-049.md +0 -66
- package/docs/decisions/ad-050.md +0 -94
- package/docs/decisions/ad-051.md +0 -69
- package/docs/decisions/ad-052.md +0 -69
- package/docs/decisions/ad-053.md +0 -78
- package/docs/decisions/ad-054.md +0 -98
- package/docs/decisions/ad-055.md +0 -74
- package/docs/decisions/ad-056.md +0 -85
- package/docs/decisions/ad-057.md +0 -68
- package/docs/decisions/ad-058.md +0 -97
- package/docs/decisions/ad-059.md +0 -82
- package/docs/decisions/ad-060.md +0 -75
- package/docs/decisions/ad-061.md +0 -68
- package/docs/decisions/ad-062.md +0 -72
- package/docs/decisions/ad-063.md +0 -84
- package/docs/decisions/ad-064.md +0 -79
- package/docs/decisions/ad-065.md +0 -81
- package/docs/decisions/ad-066.md +0 -111
- package/docs/decisions/ad-067.md +0 -64
- package/docs/decisions/ad-068.md +0 -79
- package/docs/decisions/ad-069.md +0 -74
- package/docs/decisions/ad-070.md +0 -86
- package/docs/decisions/ad-071.md +0 -93
- package/docs/decisions/ad-072.md +0 -82
- package/docs/decisions/ad-073.md +0 -102
- package/docs/decisions/ad-074.md +0 -91
- package/docs/decisions/ad-075.md +0 -79
- package/docs/decisions/ad-076.md +0 -102
- package/docs/decisions/ad-077.md +0 -94
- package/docs/decisions/ad-078.md +0 -84
- package/docs/decisions/ad-079.md +0 -73
- package/docs/decisions/ad-080.md +0 -86
- package/docs/decisions/ad-081.md +0 -70
- package/docs/decisions/ad-082.md +0 -79
- package/docs/decisions/ad-083.md +0 -88
- package/docs/decisions/index.md +0 -111
- package/model-aliases.json +0 -12
- package/model-prices.cursor.json +0 -410
- package/model-prices.json +0 -1
- package/tools/test-env.mjs +0 -28
- package/tools/test-env.names.d.mts +0 -1
- package/tools/test-env.names.mjs +0 -14
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
|
|
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 { dirname as dirname9, join as
|
|
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 {
|
|
@@ -97,6 +97,9 @@ function cursorConfigDir() {
|
|
|
97
97
|
const custom = process.env.CURSOR_CONFIG_DIR?.trim();
|
|
98
98
|
return custom && custom.length > 0 ? custom : join(homedir(), ".cursor");
|
|
99
99
|
}
|
|
100
|
+
function providerConfigDirs() {
|
|
101
|
+
return [cursorConfigDir(), claudeConfigDir()];
|
|
102
|
+
}
|
|
100
103
|
|
|
101
104
|
// src/core/attest/attest.service.ts
|
|
102
105
|
var CHAIN_ROOT = "genesis";
|
|
@@ -295,20 +298,27 @@ function readJson(path) {
|
|
|
295
298
|
return null;
|
|
296
299
|
}
|
|
297
300
|
}
|
|
298
|
-
|
|
301
|
+
function isContention(error) {
|
|
302
|
+
return errorCode(error) === "EEXIST" || isRetryableFsError(error);
|
|
303
|
+
}
|
|
304
|
+
async function withFileLock(lockPath, fn, options = {}) {
|
|
305
|
+
const {
|
|
306
|
+
openLock = (path) => closeSync(openSync(path, "wx")),
|
|
307
|
+
lockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
308
|
+
lockAttempts = 200
|
|
309
|
+
} = options;
|
|
299
310
|
mkdirSync2(dirname2(lockPath), { recursive: true });
|
|
300
|
-
const attempts = 200;
|
|
301
311
|
let acquired = false;
|
|
302
|
-
for (let attempt = 0;attempt <
|
|
312
|
+
for (let attempt = 0;attempt < lockAttempts; attempt++) {
|
|
303
313
|
try {
|
|
304
|
-
|
|
314
|
+
openLock(lockPath);
|
|
305
315
|
acquired = true;
|
|
306
316
|
break;
|
|
307
317
|
} catch (error) {
|
|
308
|
-
if (
|
|
318
|
+
if (!isContention(error)) {
|
|
309
319
|
throw error;
|
|
310
320
|
}
|
|
311
|
-
await
|
|
321
|
+
await lockSleep(nextDelay({ attempt, baseMs: 10, capMs: 200 }));
|
|
312
322
|
}
|
|
313
323
|
}
|
|
314
324
|
if (!acquired) {
|
|
@@ -323,14 +333,14 @@ async function withFileLock(lockPath, fn) {
|
|
|
323
333
|
}
|
|
324
334
|
}
|
|
325
335
|
async function updateJsonAtomic(path, mutator, options) {
|
|
326
|
-
const { lockPath, afterWrite, ...atomicOptions } = options;
|
|
336
|
+
const { lockPath, afterWrite, openLock, lockSleep, lockAttempts, ...atomicOptions } = options;
|
|
327
337
|
return withFileLock(lockPath, async () => {
|
|
328
338
|
const current = readJson(path);
|
|
329
339
|
const next = mutator(current);
|
|
330
340
|
await writeJsonAtomic(path, next, atomicOptions);
|
|
331
341
|
afterWrite?.(path);
|
|
332
342
|
return next;
|
|
333
|
-
});
|
|
343
|
+
}, { openLock, lockSleep, lockAttempts });
|
|
334
344
|
}
|
|
335
345
|
|
|
336
346
|
// src/core/capability/capability.store.ts
|
|
@@ -5278,6 +5288,62 @@ function release(root, provider, session) {
|
|
|
5278
5288
|
deletePresenceRecord(root, provider, session);
|
|
5279
5289
|
}
|
|
5280
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
|
+
|
|
5281
5347
|
// src/core/release/release.decisions.ts
|
|
5282
5348
|
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
|
|
5283
5349
|
import { join as join18 } from "node:path";
|
|
@@ -5581,6 +5647,69 @@ function evaluateShellCommand(args) {
|
|
|
5581
5647
|
return { kind: "allow" };
|
|
5582
5648
|
}
|
|
5583
5649
|
|
|
5650
|
+
// src/core/shim/shim.precedence.ts
|
|
5651
|
+
var LAUNCHER = "tlc-exec";
|
|
5652
|
+
function invocationText(entry) {
|
|
5653
|
+
return [entry.command ?? "", ...entry.args ?? []].join(" ");
|
|
5654
|
+
}
|
|
5655
|
+
function coversHandler(settings, handler) {
|
|
5656
|
+
for (const matchers of Object.values(settings.hooks ?? {})) {
|
|
5657
|
+
for (const matcher of matchers) {
|
|
5658
|
+
for (const entry of matcher.hooks ?? []) {
|
|
5659
|
+
const text = invocationText(entry);
|
|
5660
|
+
if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
|
|
5661
|
+
return true;
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
return false;
|
|
5667
|
+
}
|
|
5668
|
+
function decideShim(userSettings, handler) {
|
|
5669
|
+
if (userSettings === null) {
|
|
5670
|
+
return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
|
|
5671
|
+
}
|
|
5672
|
+
return coversHandler(userSettings, handler) ? {
|
|
5673
|
+
run: false,
|
|
5674
|
+
reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
|
|
5675
|
+
} : { run: true, reason: `no user-level hook runs ${handler}` };
|
|
5676
|
+
}
|
|
5677
|
+
|
|
5678
|
+
// src/core/skill/skill.link.ts
|
|
5679
|
+
var SKILL_NAME = "harness-init";
|
|
5680
|
+
function skillLinks(runtimeHome2, providerDirs, present) {
|
|
5681
|
+
const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
|
|
5682
|
+
return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
|
|
5683
|
+
providerDir,
|
|
5684
|
+
source,
|
|
5685
|
+
target: `${providerDir}/skills/${SKILL_NAME}`
|
|
5686
|
+
}));
|
|
5687
|
+
}
|
|
5688
|
+
function linkHealth(target, runtimeHome2, probe) {
|
|
5689
|
+
const resolved = probe.linkTarget(target);
|
|
5690
|
+
if (resolved === null) {
|
|
5691
|
+
return { state: "absent", target };
|
|
5692
|
+
}
|
|
5693
|
+
if (!probe.exists(resolved)) {
|
|
5694
|
+
return { state: "dangling", target, resolved };
|
|
5695
|
+
}
|
|
5696
|
+
const resolveHome = probe.realpath ?? ((path) => path);
|
|
5697
|
+
const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
|
|
5698
|
+
return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
|
|
5699
|
+
}
|
|
5700
|
+
function linkHealthMessage(health) {
|
|
5701
|
+
switch (health.state) {
|
|
5702
|
+
case "ok":
|
|
5703
|
+
return `linked → ${health.resolved}`;
|
|
5704
|
+
case "dangling":
|
|
5705
|
+
return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
|
|
5706
|
+
case "outside-runtime":
|
|
5707
|
+
return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
|
|
5708
|
+
default:
|
|
5709
|
+
return "not linked — the provider cannot see the init skill";
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5584
5713
|
// src/core/stagnation/stagnation.resolution.ts
|
|
5585
5714
|
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5586
5715
|
import { join as join21 } from "node:path";
|
|
@@ -6708,6 +6837,21 @@ var coreFacade = {
|
|
|
6708
6837
|
readHandoffFile,
|
|
6709
6838
|
readForeignSlices
|
|
6710
6839
|
},
|
|
6840
|
+
shim: {
|
|
6841
|
+
coversHandler,
|
|
6842
|
+
decideShim
|
|
6843
|
+
},
|
|
6844
|
+
pricing: {
|
|
6845
|
+
freshness,
|
|
6846
|
+
freshnessMessage,
|
|
6847
|
+
mayReplace,
|
|
6848
|
+
shouldRefetch
|
|
6849
|
+
},
|
|
6850
|
+
skill: {
|
|
6851
|
+
linkHealth,
|
|
6852
|
+
linkHealthMessage,
|
|
6853
|
+
skillLinks
|
|
6854
|
+
},
|
|
6711
6855
|
lesson: {
|
|
6712
6856
|
projectLessonsInjectable,
|
|
6713
6857
|
recordLessonFromFailure,
|
|
@@ -6912,6 +7056,7 @@ function writeStdout(text) {
|
|
|
6912
7056
|
}
|
|
6913
7057
|
|
|
6914
7058
|
// bin/tlc-cli.ts
|
|
7059
|
+
var NPM_PACKAGE = "@tech-leads-club/harness-toolkit";
|
|
6915
7060
|
var NPM_MARKER = "installed-from-npm";
|
|
6916
7061
|
function classifyRuntimePath(dest, probe) {
|
|
6917
7062
|
if (probe.isSymlink(dest)) {
|
|
@@ -6945,19 +7090,17 @@ import { delimiter as delimiter2, dirname as dirname7, join as join27 } from "no
|
|
|
6945
7090
|
function isPackagedCopy(candidate) {
|
|
6946
7091
|
return candidate.split(/[/\\]/).includes("node_modules");
|
|
6947
7092
|
}
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
function findBunOnPath(env = process.env, platform = process.platform) {
|
|
6952
|
-
const pathValue = env.PATH ?? "";
|
|
6953
|
-
const bunName = bunExecutableName(platform);
|
|
6954
|
-
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)) {
|
|
6955
7096
|
if (!dir) {
|
|
6956
7097
|
continue;
|
|
6957
7098
|
}
|
|
6958
|
-
const
|
|
6959
|
-
|
|
6960
|
-
|
|
7099
|
+
for (const name of BUN_EXECUTABLE_NAMES) {
|
|
7100
|
+
const candidate = join27(dir, name);
|
|
7101
|
+
if (existsSync26(candidate)) {
|
|
7102
|
+
return candidate;
|
|
7103
|
+
}
|
|
6961
7104
|
}
|
|
6962
7105
|
}
|
|
6963
7106
|
return null;
|
|
@@ -7733,14 +7876,9 @@ var ENTRY_SPECS2 = [
|
|
|
7733
7876
|
{ hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
7734
7877
|
{ hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
|
|
7735
7878
|
];
|
|
7736
|
-
function commandFor(runtime) {
|
|
7737
|
-
if (process.platform === "win32") {
|
|
7738
|
-
return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
|
|
7739
|
-
}
|
|
7740
|
-
return { command: "node", argsPrefix: [runtime.launcherPath] };
|
|
7741
|
-
}
|
|
7742
7879
|
function cursorWiring(runtime) {
|
|
7743
|
-
const
|
|
7880
|
+
const command = "node";
|
|
7881
|
+
const argsPrefix = [runtime.launcherPath];
|
|
7744
7882
|
const entries = ENTRY_SPECS2.map((spec) => ({
|
|
7745
7883
|
hookEvent: spec.hookEvent,
|
|
7746
7884
|
handler: spec.handler,
|
|
@@ -7832,6 +7970,50 @@ function isCursorWired(targetPath) {
|
|
|
7832
7970
|
}
|
|
7833
7971
|
if (false) {}
|
|
7834
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
|
+
|
|
7835
8017
|
// tools/doctor.ts
|
|
7836
8018
|
function plural(count, word) {
|
|
7837
8019
|
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
@@ -7893,24 +8075,80 @@ function runtimeOwnershipCheck(home) {
|
|
|
7893
8075
|
detail: detail[kind]
|
|
7894
8076
|
};
|
|
7895
8077
|
}
|
|
8078
|
+
function checkSkillLinks(home, providerDirs = providerConfigDirs(), probe = {
|
|
8079
|
+
linkTarget: (path) => {
|
|
8080
|
+
try {
|
|
8081
|
+
return realpathSync4(path);
|
|
8082
|
+
} catch {
|
|
8083
|
+
try {
|
|
8084
|
+
return readlinkSync(path);
|
|
8085
|
+
} catch {
|
|
8086
|
+
return null;
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
},
|
|
8090
|
+
exists: existsSync30,
|
|
8091
|
+
realpath: (path) => {
|
|
8092
|
+
try {
|
|
8093
|
+
return realpathSync4(path);
|
|
8094
|
+
} catch {
|
|
8095
|
+
return path;
|
|
8096
|
+
}
|
|
8097
|
+
}
|
|
8098
|
+
}) {
|
|
8099
|
+
return providerDirs.filter((dir) => existsSync30(dir)).map((dir) => {
|
|
8100
|
+
const health = coreFacade.skill.linkHealth(join31(dir, "skills", "harness-init"), home, probe);
|
|
8101
|
+
return {
|
|
8102
|
+
level: health.state === "ok" ? "ok" : "fail",
|
|
8103
|
+
name: `init skill (${basename5(dir)})`,
|
|
8104
|
+
detail: coreFacade.skill.linkHealthMessage(health)
|
|
8105
|
+
};
|
|
8106
|
+
});
|
|
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
|
+
}
|
|
7896
8134
|
function checkRuntimePaths(home, platform) {
|
|
7897
|
-
const launcher =
|
|
7898
|
-
const distSample =
|
|
7899
|
-
const
|
|
8135
|
+
const launcher = join31(home, "bin", "tlc-exec.mjs");
|
|
8136
|
+
const distSample = join31(home, "dist", "stop.mjs");
|
|
8137
|
+
const onPath = resolveOnPath("tlc");
|
|
7900
8138
|
return [
|
|
7901
8139
|
{ level: "ok", name: "platform", detail: platform },
|
|
7902
|
-
{ level:
|
|
8140
|
+
{ level: existsSync30(launcher) ? "ok" : "fail", name: "global runtime", detail: home },
|
|
7903
8141
|
runtimeOwnershipCheck(home),
|
|
7904
8142
|
{
|
|
7905
|
-
level:
|
|
8143
|
+
level: existsSync30(distSample) ? "ok" : "fail",
|
|
7906
8144
|
name: "dist bundles",
|
|
7907
|
-
detail:
|
|
8145
|
+
detail: existsSync30(distSample) ? join31(home, "dist") : "missing — run: tlc harness build"
|
|
7908
8146
|
},
|
|
7909
|
-
{ level:
|
|
8147
|
+
{ level: existsSync30(launcher) ? "ok" : "fail", name: "portable launcher", detail: launcher },
|
|
7910
8148
|
{
|
|
7911
|
-
level:
|
|
8149
|
+
level: onPath === null ? "fail" : "ok",
|
|
7912
8150
|
name: "CLI on PATH",
|
|
7913
|
-
detail:
|
|
8151
|
+
detail: onPath ?? `no \`tlc\` on PATH — npm i -g ${NPM_PACKAGE}, or \`npm link\` from a clone`
|
|
7914
8152
|
}
|
|
7915
8153
|
];
|
|
7916
8154
|
}
|
|
@@ -7929,15 +8167,15 @@ function wiringProblems(wiring) {
|
|
|
7929
8167
|
if (wiring.strategy !== "replace") {
|
|
7930
8168
|
return [];
|
|
7931
8169
|
}
|
|
7932
|
-
const text =
|
|
7933
|
-
return cursorWiringProblems(text, { launcherPath: launcherPathOf(wiring) },
|
|
8170
|
+
const text = existsSync30(wiring.target) ? readFileSync32(wiring.target, "utf8") : null;
|
|
8171
|
+
return cursorWiringProblems(text, { launcherPath: launcherPathOf(wiring) }, existsSync30);
|
|
7934
8172
|
}
|
|
7935
8173
|
function launcherPathOf(wiring) {
|
|
7936
8174
|
const first = wiring.entries[0];
|
|
7937
8175
|
return first?.args.find((arg) => arg.endsWith(".mjs")) ?? "";
|
|
7938
8176
|
}
|
|
7939
8177
|
function providerWiringStatus(wiring) {
|
|
7940
|
-
if (!
|
|
8178
|
+
if (!existsSync30(dirname9(wiring.target))) {
|
|
7941
8179
|
return "not-installed";
|
|
7942
8180
|
}
|
|
7943
8181
|
if (wiring.strategy === "replace") {
|
|
@@ -7946,12 +8184,12 @@ function providerWiringStatus(wiring) {
|
|
|
7946
8184
|
}
|
|
7947
8185
|
return wiringProblems(wiring).length === 0 ? "wired" : "detected-but-unwired";
|
|
7948
8186
|
}
|
|
7949
|
-
const existingText =
|
|
8187
|
+
const existingText = existsSync30(wiring.target) ? readFileSync32(wiring.target, "utf8") : null;
|
|
7950
8188
|
const result = mergeClaudeSettings(existingText, wiring.entries);
|
|
7951
8189
|
return result.ok && !result.changed ? "wired" : "detected-but-unwired";
|
|
7952
8190
|
}
|
|
7953
8191
|
function checkProviders(registry, home) {
|
|
7954
|
-
const launcherPath =
|
|
8192
|
+
const launcherPath = join31(home, "bin", "tlc-exec.mjs");
|
|
7955
8193
|
return registry.map((provider) => {
|
|
7956
8194
|
const wiring = provider.wiring({ launcherPath });
|
|
7957
8195
|
const status = providerWiringStatus(wiring);
|
|
@@ -8135,12 +8373,12 @@ function checkProjectPolicy(root) {
|
|
|
8135
8373
|
{
|
|
8136
8374
|
level: "ok",
|
|
8137
8375
|
name: "project policy",
|
|
8138
|
-
detail:
|
|
8376
|
+
detail: existsSync30(configPath) ? configPath : "missing — run: tlc harness init"
|
|
8139
8377
|
},
|
|
8140
8378
|
{
|
|
8141
8379
|
level: "ok",
|
|
8142
8380
|
name: "state dir",
|
|
8143
|
-
detail:
|
|
8381
|
+
detail: existsSync30(stateDir) ? stateDir : `${stateDir} (created on first session)`
|
|
8144
8382
|
},
|
|
8145
8383
|
checkPosture(root),
|
|
8146
8384
|
...checkObservedRails(root),
|
|
@@ -8151,8 +8389,8 @@ function checkProjectPolicy(root) {
|
|
|
8151
8389
|
];
|
|
8152
8390
|
}
|
|
8153
8391
|
function checkGlobalCommands(home) {
|
|
8154
|
-
const globalCommands =
|
|
8155
|
-
if (!
|
|
8392
|
+
const globalCommands = join31(home, ".cursor", "commands");
|
|
8393
|
+
if (!existsSync30(globalCommands)) {
|
|
8156
8394
|
return {
|
|
8157
8395
|
level: "ok",
|
|
8158
8396
|
name: "global commands dir",
|
|
@@ -8171,10 +8409,12 @@ function runChecks(ctx) {
|
|
|
8171
8409
|
return [
|
|
8172
8410
|
...checkNodeVersion(ctx.nodeVersion, ctx.bunPath),
|
|
8173
8411
|
...checkRuntimePaths(ctx.runtimeHome, ctx.platform),
|
|
8412
|
+
...checkSkillLinks(ctx.runtimeHome),
|
|
8174
8413
|
checkHookRuntime(ctx.runtimeHome, ctx.bunPath),
|
|
8175
8414
|
...checkProviders(ctx.registry, ctx.runtimeHome),
|
|
8176
8415
|
...checkProjectPolicy(ctx.root),
|
|
8177
8416
|
...checkCapabilities(ctx.root, ctx.runtimeHome),
|
|
8417
|
+
...checkPrices(),
|
|
8178
8418
|
checkGlobalCommands(ctx.home)
|
|
8179
8419
|
];
|
|
8180
8420
|
}
|
|
@@ -8244,6 +8484,7 @@ export {
|
|
|
8244
8484
|
toReport,
|
|
8245
8485
|
runtimeOwnershipCheck,
|
|
8246
8486
|
runChecks,
|
|
8487
|
+
resolveOnPath,
|
|
8247
8488
|
providerWiringStatus,
|
|
8248
8489
|
plural,
|
|
8249
8490
|
medianMs,
|
|
@@ -8251,9 +8492,11 @@ export {
|
|
|
8251
8492
|
formatReport,
|
|
8252
8493
|
exitCodeFor,
|
|
8253
8494
|
checkSubagentAllowlist,
|
|
8495
|
+
checkSkillLinks,
|
|
8254
8496
|
checkRuntimePaths,
|
|
8255
8497
|
checkProviders,
|
|
8256
8498
|
checkProjectPolicy,
|
|
8499
|
+
checkPrices,
|
|
8257
8500
|
checkNodeVersion,
|
|
8258
8501
|
checkLessonHealth,
|
|
8259
8502
|
checkId,
|
package/dist/help-topic.mjs
CHANGED
|
File without changes
|
package/dist/init-project.mjs
CHANGED
|
@@ -3,7 +3,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
3
3
|
|
|
4
4
|
// tools/init-project.ts
|
|
5
5
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6
|
-
import { dirname as dirname3, join as join4 } from "node:path";
|
|
6
|
+
import { dirname as dirname3, join as join4, sep } from "node:path";
|
|
7
7
|
|
|
8
8
|
// bin/write-user-hooks.mjs
|
|
9
9
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -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
|
-
|
|
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 } =
|
|
835
|
+
const { command, argsPrefix } = SHIM_COMMAND;
|
|
841
836
|
return CURSOR_SHIM_SPECS.map((spec) => ({
|
|
842
837
|
hookEvent: spec.hookEvent,
|
|
843
838
|
handler: spec.handler,
|
|
@@ -858,19 +853,23 @@ function claudeShimEntries(launcher) {
|
|
|
858
853
|
...spec.loopLimit !== undefined ? { loopLimit: spec.loopLimit } : {}
|
|
859
854
|
}));
|
|
860
855
|
}
|
|
861
|
-
var
|
|
856
|
+
var PROJECT_SHIMS = [join4(".cursor", "hooks.json"), join4(".claude", "settings.json")];
|
|
857
|
+
var GITIGNORE_STATE = ".tlc/harness/state/";
|
|
858
|
+
function gitignoreEntries() {
|
|
859
|
+
return [GITIGNORE_STATE, ...PROJECT_SHIMS.map((path) => path.split(sep).join("/"))];
|
|
860
|
+
}
|
|
862
861
|
function mergeGitignore(root) {
|
|
863
862
|
const path = join4(root, ".gitignore");
|
|
864
863
|
const existing = existsSync3(path) ? readFileSync3(path, "utf8") : "";
|
|
865
864
|
const lines = existing.split(`
|
|
866
865
|
`);
|
|
867
|
-
const
|
|
868
|
-
if (
|
|
866
|
+
const missing = gitignoreEntries().filter((entry) => !lines.includes(entry));
|
|
867
|
+
if (missing.length === 0) {
|
|
869
868
|
return { text: existing.endsWith(`
|
|
870
869
|
`) || existing === "" ? existing : `${existing}
|
|
871
870
|
`, changed: false };
|
|
872
871
|
}
|
|
873
|
-
lines.push(
|
|
872
|
+
lines.push(...missing);
|
|
874
873
|
const withoutTrailingBlank = lines.filter((line, index, all) => line.length > 0 || index < all.length - 1);
|
|
875
874
|
return { text: `${withoutTrailingBlank.join(`
|
|
876
875
|
`).replace(/\n+$/, "")}
|
|
@@ -901,7 +900,7 @@ function buildPlan(root, flags, stdinText, presence) {
|
|
|
901
900
|
policy,
|
|
902
901
|
cursorHooksDocument: presence.cursor ? renderCursorHooksDocument(cursorShimEntries(launcher)) : null,
|
|
903
902
|
claudeHooksPreview: presence.claude ? claudeShimEntries(launcher) : null,
|
|
904
|
-
|
|
903
|
+
gitignoreEntries: gitignoreEntries()
|
|
905
904
|
};
|
|
906
905
|
}
|
|
907
906
|
function applyPlan(root, flags, presence, stdinText) {
|
|
@@ -983,11 +982,13 @@ export {
|
|
|
983
982
|
mergeGitignore,
|
|
984
983
|
main,
|
|
985
984
|
launcherPath,
|
|
985
|
+
gitignoreEntries,
|
|
986
986
|
detectProviders,
|
|
987
987
|
cursorShimEntries,
|
|
988
988
|
claudeShimEntries,
|
|
989
989
|
buildPlan,
|
|
990
990
|
applyPlan,
|
|
991
991
|
UsageError,
|
|
992
|
-
|
|
992
|
+
PROJECT_SHIMS,
|
|
993
|
+
GITIGNORE_STATE
|
|
993
994
|
};
|