@dzhechkov/harness-cli 0.3.198 → 0.3.200

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-cli",
3
- "version": "0.3.198",
3
+ "version": "0.3.200",
4
4
  "description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 35 commands, 13 presets, 6 platform targets.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,15 +37,7 @@
37
37
  "src",
38
38
  "README.md"
39
39
  ],
40
- "scripts": {
41
- "build": "tsc -p tsconfig.json",
42
- "test": "vitest run",
43
- "test:watch": "vitest",
44
- "typecheck": "tsc -p tsconfig.json --noEmit",
45
- "lint": "tsc -p tsconfig.json --noEmit"
46
- },
47
40
  "dependencies": {
48
- "@dzhechkov/harness-core": "workspace:*",
49
41
  "@dzhechkov/harness-presets": "^0.5.0",
50
42
  "@dzhechkov/scout": "^0.8.0",
51
43
  "@dzhechkov/skills-devops": "^0.3.0",
@@ -61,7 +53,8 @@
61
53
  "@dzhechkov/skills-idea2prd": "^0.1.0",
62
54
  "@dzhechkov/skills-reverse-engineering": "^0.1.0",
63
55
  "@dzhechkov/skills-presentation-storyteller": "^0.1.0",
64
- "@dzhechkov/skills-website-cloner": "^0.1.0"
56
+ "@dzhechkov/skills-website-cloner": "^0.1.0",
57
+ "@dzhechkov/harness-core": "0.3.100"
65
58
  },
66
59
  "devDependencies": {
67
60
  "@types/node": "^25.6.0",
@@ -79,5 +72,12 @@
79
72
  "url": "https://github.com/djd1m/dz-harness-hub.git",
80
73
  "directory": "packages/@dzhechkov/harness-cli"
81
74
  },
82
- "homepage": "https://github.com/djd1m/dz-harness-hub/tree/main/packages/@dzhechkov/harness-cli#readme"
83
- }
75
+ "homepage": "https://github.com/djd1m/dz-harness-hub/tree/main/packages/@dzhechkov/harness-cli#readme",
76
+ "scripts": {
77
+ "build": "tsc -p tsconfig.json",
78
+ "test": "vitest run",
79
+ "test:watch": "vitest",
80
+ "typecheck": "tsc -p tsconfig.json --noEmit",
81
+ "lint": "tsc -p tsconfig.json --noEmit"
82
+ }
83
+ }
package/src/cli.ts CHANGED
@@ -67,6 +67,8 @@ import {
67
67
  statuslineData,
68
68
  writeFeatureAdrState,
69
69
  computeUsage,
70
+ deriveUsageCalibration,
71
+ normalizeClaudeUsageModelKey,
70
72
  readUsageLimits,
71
73
  queryBookKnowledge,
72
74
  loadStorePatternsSync,
@@ -84,7 +86,7 @@ import {
84
86
  importBrainSlice,
85
87
  registerKusToBrain,
86
88
  } from '@dzhechkov/harness-core';
87
- import type { PatternRecord, TargetName, BookKU, HarmonizeReport } from '@dzhechkov/harness-core';
89
+ import type { ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan } from '@dzhechkov/harness-core';
88
90
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
89
91
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
90
92
 
@@ -127,7 +129,7 @@ Usage:
127
129
  dz brain init [--project <dir>] [--k <N>] (wire the grounding hook into .claude/settings.json — opt-in)
128
130
  dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
129
131
  dz statusline --fa-record --slug <s> --step "<label>" [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
130
- dz usage [--json] [--project <dir>] (ESTIMATE Claude session (5h-block) + weekly (7d) token usage from local transcripts; exit 0 ALWAYS; pct=null when memory.usage.{sessionTokenLimit,weeklyTokenLimit} unconfigured in .dz/config.json)
132
+ dz usage [--json] [--project <dir>] | dz usage --calibrate --session <pct> --weekly <pct> [--model fable=<pct>] [--project <dir>] (ESTIMATE Claude usage from fixed reset windows; optional per-model weekly binding; exit 0 ALWAYS; pct=null when limits unconfigured)
131
133
  dz pretrain [--project <dir>]
132
134
  dz recommend "<task description>"
133
135
  dz compose <preset1+preset2+...> [--target <name>]
@@ -170,12 +172,14 @@ export interface CliIo {
170
172
  interface ParsedArgs {
171
173
  readonly command: string;
172
174
  readonly options: Map<string, string>;
175
+ readonly optionLists: Map<string, string[]>;
173
176
  readonly flags: Set<string>;
174
177
  }
175
178
 
176
179
  /** Parse `<command> [--key value] [--flag]` argv. */
177
180
  function parseArgs(argv: string[]): ParsedArgs {
178
181
  const options = new Map<string, string>();
182
+ const optionLists = new Map<string, string[]>();
179
183
  const flags = new Set<string>();
180
184
  const positional: string[] = [];
181
185
 
@@ -186,6 +190,9 @@ function parseArgs(argv: string[]): ParsedArgs {
186
190
  const next = argv[index + 1];
187
191
  if (next !== undefined && !next.startsWith('--')) {
188
192
  options.set(key, next);
193
+ const list = optionLists.get(key) ?? [];
194
+ list.push(next);
195
+ optionLists.set(key, list);
189
196
  index += 1;
190
197
  } else {
191
198
  flags.add(key);
@@ -200,7 +207,7 @@ function parseArgs(argv: string[]): ParsedArgs {
200
207
  options.set(`_positional_${pi - 1}`, positional[pi] ?? '');
201
208
  }
202
209
 
203
- return { command: positional[0] ?? '', options, flags };
210
+ return { command: positional[0] ?? '', options, optionLists, flags };
204
211
  }
205
212
 
206
213
  type Write = (line: string) => void;
@@ -1112,18 +1119,187 @@ function cmdStatusline(
1112
1119
  }
1113
1120
  }
1114
1121
 
1122
+ function isJsonRecord(value: unknown): value is Record<string, unknown> {
1123
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1124
+ }
1125
+
1126
+ function usageConfigPath(projectRoot: string): string {
1127
+ return join(projectRoot, '.dz', 'config.json');
1128
+ }
1129
+
1130
+ function readProjectConfigForUsage(projectRoot: string): { config: Record<string, unknown>; warning?: string } {
1131
+ const path = usageConfigPath(projectRoot);
1132
+ try {
1133
+ if (!existsSync(path)) return { config: {} };
1134
+ const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
1135
+ if (isJsonRecord(parsed)) return { config: parsed };
1136
+ return { config: {}, warning: 'existing config is not a JSON object; writing a minimal config' };
1137
+ } catch {
1138
+ return { config: {}, warning: 'existing config could not be parsed; writing a minimal config' };
1139
+ }
1140
+ }
1141
+
1142
+ function applyUsageCalibrationToConfig(config: Record<string, unknown>, plan: UsageCalibrationPlan): Record<string, unknown> {
1143
+ const next: Record<string, unknown> = { ...config };
1144
+ const memory = isJsonRecord(next['memory']) ? { ...next['memory'] } : {};
1145
+ const usage = isJsonRecord(memory['usage']) ? { ...memory['usage'] } : {};
1146
+
1147
+ for (const change of plan.changes) {
1148
+ if (change.key === 'session') {
1149
+ usage['sessionTokenLimit'] = change.after;
1150
+ } else if (change.key === 'weekly') {
1151
+ usage['weeklyTokenLimit'] = change.after;
1152
+ } else {
1153
+ const model = normalizeClaudeUsageModelKey(change.key);
1154
+ if (model) {
1155
+ const existingByModel = isJsonRecord(usage['weeklyTokenLimitByModel']) ? { ...usage['weeklyTokenLimitByModel'] } : {};
1156
+ existingByModel[model] = change.after;
1157
+ usage['weeklyTokenLimitByModel'] = existingByModel;
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ if (plan.changes.length > 0) {
1163
+ usage['calibratedAt'] = plan.after.calibratedAt;
1164
+ usage['source'] = plan.after.source;
1165
+ }
1166
+
1167
+ memory['usage'] = usage;
1168
+ next['memory'] = memory;
1169
+ return next;
1170
+ }
1171
+
1172
+ function parseUsageModelArgs(modelArgs: readonly string[]): { modelPct: Record<string, unknown>; skipped: string[] } {
1173
+ const modelPct: Record<string, unknown> = {};
1174
+ const skipped: string[] = [];
1175
+ for (const raw of modelArgs) {
1176
+ const eq = raw.indexOf('=');
1177
+ if (eq <= 0 || eq === raw.length - 1) {
1178
+ skipped.push(`model ${raw}: skipped malformed model=pct argument`);
1179
+ continue;
1180
+ }
1181
+ const modelName = raw.slice(0, eq).trim();
1182
+ const model = normalizeClaudeUsageModelKey(modelName);
1183
+ if (!model) {
1184
+ skipped.push(`model ${modelName}: skipped unknown model`);
1185
+ continue;
1186
+ }
1187
+ modelPct[model] = raw.slice(eq + 1).trim();
1188
+ }
1189
+ return { modelPct, skipped };
1190
+ }
1191
+
1192
+ function writeUsageCalibrationSummary(opts: {
1193
+ projectRoot: string;
1194
+ plan: UsageCalibrationPlan;
1195
+ preSkipped: readonly string[];
1196
+ configWarning?: string | undefined;
1197
+ wrote: boolean;
1198
+ write: Write;
1199
+ }): void {
1200
+ opts.write('usage calibrate: estimated local transcript counts; claude.ai/settings/usage is authoritative');
1201
+ opts.write(`usage calibrate: project ${opts.projectRoot}`);
1202
+ if (opts.configWarning) opts.write(`usage calibrate: ${opts.configWarning}`);
1203
+ for (const change of opts.plan.changes) {
1204
+ opts.write(
1205
+ `usage calibrate: ${change.key} tokens=${change.tokens} pct=${change.pct}% limit ${change.before ?? 'null'} -> ${change.after}`,
1206
+ );
1207
+ }
1208
+ const skipped = [...opts.preSkipped, ...opts.plan.skipped];
1209
+ for (const item of skipped) opts.write(`usage calibrate: skipped ${item}`);
1210
+ if (opts.wrote) {
1211
+ opts.write('usage calibrate: wrote .dz/config.json with source claude.ai/settings/usage');
1212
+ } else {
1213
+ opts.write('usage calibrate: no config changes written');
1214
+ }
1215
+ }
1216
+
1217
+ function cmdUsageCalibrate(
1218
+ options: Map<string, string>,
1219
+ optionLists: Map<string, string[]>,
1220
+ cwd: string,
1221
+ write: Write,
1222
+ ): number {
1223
+ const projectRoot = resolve(cwd, options.get('project') ?? '.');
1224
+ const suppliedModels = optionLists.get('model') ?? [];
1225
+ const parsedModels = parseUsageModelArgs(suppliedModels);
1226
+ const modelPct = parsedModels.modelPct;
1227
+ const hasModelPct = Object.keys(modelPct).length > 0;
1228
+ const input = {
1229
+ ...(options.has('session') ? { sessionPct: options.get('session') } : {}),
1230
+ ...(options.has('weekly') ? { weeklyPct: options.get('weekly') } : {}),
1231
+ ...(hasModelPct ? { modelPct } : {}),
1232
+ calibratedAt: new Date().toISOString(),
1233
+ source: 'claude.ai/settings/usage' as const,
1234
+ };
1235
+ const missingInputs: string[] = [];
1236
+ if (!options.has('session') && !options.has('weekly') && !hasModelPct) {
1237
+ missingInputs.push('no calibration percentages supplied');
1238
+ }
1239
+
1240
+ try {
1241
+ const current = computeUsage(projectRoot);
1242
+ const before = readUsageLimits(projectRoot);
1243
+ const plan = deriveUsageCalibration(current, before, input);
1244
+ if (plan.changes.length === 0) {
1245
+ writeUsageCalibrationSummary({
1246
+ projectRoot,
1247
+ plan,
1248
+ preSkipped: [...parsedModels.skipped, ...missingInputs],
1249
+ wrote: false,
1250
+ write,
1251
+ });
1252
+ return 0;
1253
+ }
1254
+
1255
+ const existing = readProjectConfigForUsage(projectRoot);
1256
+ const nextConfig = applyUsageCalibrationToConfig(existing.config, plan);
1257
+ try {
1258
+ mkdirSync(join(projectRoot, '.dz'), { recursive: true });
1259
+ writeFileSync(usageConfigPath(projectRoot), JSON.stringify(nextConfig, null, 2) + '\n');
1260
+ writeUsageCalibrationSummary({
1261
+ projectRoot,
1262
+ plan,
1263
+ preSkipped: [...parsedModels.skipped, ...missingInputs],
1264
+ configWarning: existing.warning,
1265
+ wrote: true,
1266
+ write,
1267
+ });
1268
+ } catch {
1269
+ writeUsageCalibrationSummary({
1270
+ projectRoot,
1271
+ plan,
1272
+ preSkipped: [...parsedModels.skipped, ...missingInputs, 'write failed'],
1273
+ configWarning: existing.warning,
1274
+ wrote: false,
1275
+ write,
1276
+ });
1277
+ }
1278
+ return 0;
1279
+ } catch {
1280
+ write('usage calibrate: skipped internal error; no config changes written');
1281
+ return 0;
1282
+ }
1283
+ }
1284
+
1115
1285
  /**
1116
- * `dz usage` — print an ESTIMATE of Claude session (active 5h-block) + weekly (rolling 7d) token
1117
- * usage, aggregated READONLY from the local transcript store (see {@link computeUsage}). `--json`
1118
- * emits the single-line contract the feature-adr usage-probe agent parses; the human path prints a
1119
- * compact `session ~74% (resets 19:00) · week ~52% (resets Sat)` or an unconfigured hint.
1286
+ * `dz usage` — print an ESTIMATE of Claude session + weekly usage from fixed reset windows,
1287
+ * aggregated READONLY from the local transcript store (see {@link computeUsage}). `--json` emits
1288
+ * the single-line contract the feature-adr usage-probe agent parses; `--calibrate` is the only
1289
+ * write path and records human-transcribed claude.ai percentages in `.dz/config.json`.
1120
1290
  *
1121
1291
  * **Exit code is 0 ALWAYS** — including on internal error the whole body is guarded and prints the
1122
1292
  * all-null JSON, so a probe can NEVER distinguish "usage unknown" from "command failed" via a
1123
- * non-zero exit (unknown is a first-class value, INV-3). `--project <dir>` scopes ONLY the
1124
- * `.dz/config.json` limits read; the measurement is account-wide (all projects, FR-1.6).
1293
+ * non-zero exit. `--project <dir>` scopes ONLY the `.dz/config.json` read/write; measurement is
1294
+ * account-wide (all projects).
1125
1295
  */
1126
- function cmdUsage(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
1296
+ function cmdUsage(
1297
+ options: Map<string, string>,
1298
+ optionLists: Map<string, string[]>,
1299
+ flags: Set<string>,
1300
+ cwd: string,
1301
+ write: Write,
1302
+ ): number {
1127
1303
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
1128
1304
  const nullContract = () =>
1129
1305
  JSON.stringify({
@@ -1136,19 +1312,40 @@ function cmdUsage(options: Map<string, string>, flags: Set<string>, cwd: string,
1136
1312
  estimated: true,
1137
1313
  });
1138
1314
  try {
1315
+ if (flags.has('calibrate')) return cmdUsageCalibrate(options, optionLists, cwd, write);
1316
+
1139
1317
  const u = computeUsage(projectRoot);
1140
1318
  const lim = readUsageLimits(projectRoot);
1319
+ const modelLimits = lim.weeklyTokenLimitByModel;
1320
+ const hasModelLimits = modelLimits !== undefined && Object.keys(modelLimits).length > 0;
1141
1321
  if (flags.has('json')) {
1322
+ const limitsPayload: {
1323
+ session: number | null;
1324
+ weekly: number | null;
1325
+ weeklyByModel?: Partial<Record<ClaudeUsageModel, number>>;
1326
+ } = { session: lim.sessionTokenLimit ?? null, weekly: lim.weeklyTokenLimit ?? null };
1327
+ if (hasModelLimits) limitsPayload.weeklyByModel = { ...modelLimits };
1328
+ const payload: {
1329
+ sessionPct: number | null;
1330
+ weeklyPct: number | null;
1331
+ sessionTokens: number;
1332
+ weeklyTokens: number;
1333
+ resetsAt: { session: string | null; weekly: string | null };
1334
+ limits: typeof limitsPayload;
1335
+ weeklyByModel?: typeof u.weeklyByModel;
1336
+ estimated: true;
1337
+ } = {
1338
+ sessionPct: u.sessionPct,
1339
+ weeklyPct: u.weeklyPct,
1340
+ sessionTokens: u.sessionTokens,
1341
+ weeklyTokens: u.weeklyTokens,
1342
+ resetsAt: { session: u.sessionResetsAt, weekly: u.weeklyResetsAt },
1343
+ limits: limitsPayload,
1344
+ estimated: true,
1345
+ };
1346
+ if (hasModelLimits && u.weeklyByModel !== undefined) payload.weeklyByModel = u.weeklyByModel;
1142
1347
  write(
1143
- JSON.stringify({
1144
- sessionPct: u.sessionPct,
1145
- weeklyPct: u.weeklyPct,
1146
- sessionTokens: u.sessionTokens,
1147
- weeklyTokens: u.weeklyTokens,
1148
- resetsAt: { session: u.sessionResetsAt, weekly: u.weeklyResetsAt },
1149
- limits: { session: lim.sessionTokenLimit ?? null, weekly: lim.weeklyTokenLimit ?? null },
1150
- estimated: true,
1151
- }),
1348
+ JSON.stringify(payload),
1152
1349
  );
1153
1350
  return 0;
1154
1351
  }
@@ -1168,7 +1365,8 @@ function cmdUsage(options: Map<string, string>, flags: Set<string>, cwd: string,
1168
1365
  }
1169
1366
  };
1170
1367
  const s = u.sessionPct === null ? 'n/a' : '~' + u.sessionPct + '%';
1171
- const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%';
1368
+ const binding = hasModelLimits && u.weeklyBindingModel !== undefined ? ' ' + u.weeklyBindingModel + '-bound' : '';
1369
+ const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%' + binding;
1172
1370
  write('usage: session ' + s + ' (resets ' + clock(u.sessionResetsAt) + ') · week ' + w + ' (resets ' + clock(u.weeklyResetsAt) + ') · estimated');
1173
1371
  return 0;
1174
1372
  } catch {
@@ -1180,7 +1378,7 @@ function cmdUsage(options: Map<string, string>, flags: Set<string>, cwd: string,
1180
1378
  }
1181
1379
 
1182
1380
  async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): Promise<number> {
1183
- const projectRoot = options.get('project') ?? cwd;
1381
+ const projectRoot = resolve(cwd, options.get('project') ?? '.');
1184
1382
 
1185
1383
  // Vector tier (dz-rvf-vector-bridge FR-1): best-effort mirror AFTER the lexical write is
1186
1384
  // durable (I-3). Auto-gated on the agentdb memory backend / an explicit vector-engine config
@@ -3461,7 +3659,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
3461
3659
  return '';
3462
3660
  }
3463
3661
  };
3464
- const { command, options, flags } = parseArgs(argv);
3662
+ const { command, options, optionLists, flags } = parseArgs(argv);
3465
3663
 
3466
3664
  if (command === '' || command === 'help' || flags.has('help')) {
3467
3665
  write(USAGE);
@@ -3482,7 +3680,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
3482
3680
  case 'create-skill':
3483
3681
  return cmdCreateSkill(options, flags, cwd, write);
3484
3682
  case 'info':
3485
- return cmdInfo(options, { command, options, flags }, cwd, write);
3683
+ return cmdInfo(options, { command, options, optionLists, flags }, cwd, write);
3486
3684
  case 'scout':
3487
3685
  return await cmdScout(options, flags, cwd, write);
3488
3686
  case 'workflow':
@@ -3508,7 +3706,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
3508
3706
  case 'statusline':
3509
3707
  return cmdStatusline(options, flags, cwd, write, readStdin);
3510
3708
  case 'usage':
3511
- return cmdUsage(options, flags, cwd, write);
3709
+ return cmdUsage(options, optionLists, flags, cwd, write);
3512
3710
  case 'setup':
3513
3711
  return await cmdSetup(options, flags, cwd, write);
3514
3712
  case 'pretrain':