@leroylabs/cli 0.1.7 → 0.1.9

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Leroy Labs CLI Proprietary License
2
+
3
+ Copyright © 2026 Leroy Labs. All rights reserved.
4
+
5
+ Leroy Labs grants you a limited, revocable, non-exclusive,
6
+ non-transferable, non-sublicensable license to install and use an unmodified
7
+ copy of this package solely to access the Leroy service in accordance with the
8
+ Leroy Terms at https://getleroy.com/legal.
9
+
10
+ Except for that limited license, Leroy Labs retains all right, title, and
11
+ interest in and to this package, including its source code, interfaces,
12
+ documentation, trademarks, and related intellectual property. You may not
13
+ copy, modify, create derivative works from, distribute, publish, sublicense,
14
+ sell, lease, reverse engineer, decompile, disassemble, or otherwise make this
15
+ package or its source code available to any third party, except to the extent
16
+ such restriction is prohibited by applicable law.
17
+
18
+ This package may include third-party components governed by their respective
19
+ license terms. Nothing in this license grants rights to Leroy Labs trademarks
20
+ or branding.
package/README.md CHANGED
@@ -55,23 +55,31 @@ which matches the one-minute tape cadence; use `--interval SECONDS` to choose a
55
55
  different polling interval. Each refresh is an authenticated evaluation request
56
56
  and may count toward the account's lookup allowance.
57
57
 
58
- ## Match strength
58
+ ## Evidence Strength
59
59
 
60
60
  Leroy selects the strongest available evidence tier and does not pool lower
61
61
  tiers into it:
62
62
 
63
- - `5/5`: exact ticker setup
64
- - `4/5`: same setup on other tickers
65
- - `3/5`: same 41-sensor state
66
- - `2/5`: same side with strategy and market context
67
- - `1/5`: same side baseline
63
+ - `5/5`: Exact Setup
64
+ - `4/5`: Cross-Ticker Setup
65
+ - `3/5`: Signal Pattern
66
+ - `2/5`: Contextual Analogue
67
+ - `1/5`: Direction-Only Baseline
68
+ - `0/5`: No Comparable Evidence
68
69
 
69
- The human-readable output shows `Exact Matches` only for `5/5`. Lower tiers
70
- show only their own `Comparable Matches` count. Use `--verbose` for the match
71
- basis, or `--json` for the complete structured response.
70
+ The human-readable output shows a scope-specific observation count according to
71
+ the selected tier; these are observations in that evidence scope, not setup
72
+ matches. Use `--verbose` for the full match basis, or `--json` for the complete
73
+ structured response.
72
74
 
73
75
  The browser reference is available at [getleroy.com/cli](https://getleroy.com/cli).
74
76
 
77
+ ## License
78
+
79
+ Copyright © 2026 Leroy Labs. This package is proprietary software, licensed
80
+ only for use with Leroy under the [Leroy Terms](https://getleroy.com/legal).
81
+ See [LICENSE](./LICENSE) for the package license.
82
+
75
83
  ## Exit codes
76
84
 
77
85
  - `0`: the request completed and returned a valid Leroy response. This includes
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@leroylabs/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Command-line access to Leroy market evidence.",
5
5
  "homepage": "https://getleroy.com",
6
+ "license": "SEE LICENSE IN LICENSE",
6
7
  "type": "module",
7
8
  "files": [
8
9
  "bin",
9
10
  "src",
10
- "README.md"
11
+ "README.md",
12
+ "LICENSE"
11
13
  ],
12
14
  "publishConfig": {
13
15
  "access": "public"
package/src/format.mjs CHANGED
@@ -25,6 +25,32 @@ function record(value) {
25
25
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
26
26
  }
27
27
 
28
+ export function unavailableSymbolGuidance(response) {
29
+ if (response?.status !== "unavailable") return null;
30
+ const assumptions = record(response.assumptions);
31
+ if (assumptions.availability_code !== "symbol_not_covered") return null;
32
+ const request = record(response.request);
33
+ const symbol = typeof request.symbol === "string" ? request.symbol : null;
34
+ if (!symbol) return null;
35
+ const suggestedSymbol = typeof assumptions.suggested_symbol === "string"
36
+ ? assumptions.suggested_symbol.toUpperCase()
37
+ : null;
38
+ return { symbol, suggestedSymbol };
39
+ }
40
+
41
+ function formatUnavailableSymbolGuidance(response, colorEnabled) {
42
+ const guidance = unavailableSymbolGuidance(response);
43
+ if (!guidance) return null;
44
+ return [
45
+ `${style("✕", ["bold", "red"], colorEnabled)} ${style(`${guidance.symbol} is not currently covered by Leroy.`, ["white"], colorEnabled)}`,
46
+ ...(guidance.suggestedSymbol ? [
47
+ "",
48
+ ` ${style(`Did you mean ${guidance.suggestedSymbol}?`, ["white"], colorEnabled)}`,
49
+ ` ${color(`Retry: leroy evaluate ${guidance.suggestedSymbol}`, "neutral", colorEnabled)}`,
50
+ ] : []),
51
+ ].join("\n");
52
+ }
53
+
28
54
  function finite(value) {
29
55
  return typeof value === "number" && Number.isFinite(value) ? value : null;
30
56
  }
@@ -105,7 +131,8 @@ function memoryMatchCounts(response) {
105
131
  const rowCount = finite(record(selected).row_count) ?? 0;
106
132
  return {
107
133
  exact: tier.score === 5 ? rowCount : 0,
108
- comparable: tier.score > 0 && tier.score < 5 ? rowCount : 0,
134
+ comparable: tier.score > 1 && tier.score < 5 ? rowCount : 0,
135
+ baseline: tier.score === 1 ? rowCount : 0,
109
136
  tier,
110
137
  };
111
138
  }
@@ -115,21 +142,67 @@ function matchTier(match) {
115
142
  const kind = value.match_kind;
116
143
  const scope = typeof value.scope === "string" ? value.scope : null;
117
144
  if (scope === "same_ticker_exact" || (kind === "context" && !scope)) {
118
- return { score: 5, basis: "Exact ticker setup" };
145
+ return { score: 5, basis: "Exact Setup" };
119
146
  }
120
147
  if (scope === "cross_ticker_exact_state" || kind === "cross_ticker_context") {
121
- return { score: 4, basis: "Same setup on other tickers" };
148
+ return { score: 4, basis: "Cross-Ticker Setup" };
122
149
  }
123
150
  if (scope === "exact_41_sensor_state" || kind === "strategy") {
124
- return { score: 3, basis: "Same 41-sensor state" };
151
+ return { score: 3, basis: "Signal Pattern" };
125
152
  }
126
153
  if (scope?.startsWith("forward_overlay_") || kind === "forward_overlay_context") {
127
- return { score: 2, basis: "Same side with strategy and market context" };
154
+ return { score: 2, basis: "Contextual Analogue" };
128
155
  }
129
156
  if (scope === "all_market_memory_observations" || kind === "side") {
130
- return { score: 1, basis: "Same side baseline" };
157
+ return { score: 1, basis: "Direction-Only Baseline" };
131
158
  }
132
- return { score: 0, basis: null };
159
+ return { score: 0, basis: "No Comparable Evidence" };
160
+ }
161
+
162
+ function matchShortLabel(score) {
163
+ if (score >= 5) return "Exact Setup";
164
+ if (score === 4) return "Cross-Ticker Setup";
165
+ if (score === 3) return "Signal Pattern";
166
+ if (score === 2) return "Contextual Analogue";
167
+ if (score === 1) return "Direction-Only Baseline";
168
+ return "No Comparable Evidence";
169
+ }
170
+
171
+ function matchTone(score) {
172
+ if (score >= 4) return "green";
173
+ if (score >= 2) return "yellow";
174
+ if (score === 1) return "red";
175
+ return "inactive";
176
+ }
177
+
178
+ function matchCells(score, colorEnabled) {
179
+ const active = Math.max(0, Math.min(5, Number(score) || 0));
180
+ const activeTone = matchTone(active);
181
+ return Array.from({ length: 5 }, (_, index) => color(
182
+ index < active ? "●" : "○",
183
+ index < active ? activeTone : "inactive",
184
+ colorEnabled,
185
+ )).join("");
186
+ }
187
+
188
+ function matchEvidenceLabel(tier, rowCount) {
189
+ const labels = {
190
+ 5: "exact observations",
191
+ 4: "cross-ticker observations",
192
+ 3: "signal-pattern observations",
193
+ 2: "context observations",
194
+ 1: "baseline observations",
195
+ };
196
+ return labels[tier.score] ? `${rowCount} ${labels[tier.score]}` : "No comparable observations";
197
+ }
198
+
199
+ function matchCountMetric(tier, counts) {
200
+ if (tier.score === 5) return { label: "Exact Observations", value: countLabel(counts.exact) };
201
+ if (tier.score === 4) return { label: "Cross-Ticker Sample", value: countLabel(counts.comparable) };
202
+ if (tier.score === 3) return { label: "Signal Pattern Sample", value: countLabel(counts.comparable) };
203
+ if (tier.score === 2) return { label: "Context Sample", value: countLabel(counts.comparable) };
204
+ if (tier.score === 1) return { label: "Baseline Observations", value: countLabel(counts.baseline) };
205
+ return null;
133
206
  }
134
207
 
135
208
  function memoryStatus(response) {
@@ -167,7 +240,7 @@ function lookupMilliseconds(response) {
167
240
  function humanizeStatus(value) {
168
241
  const labels = {
169
242
  historical_match: "Historical Match",
170
- insufficient_context: "Limited Match",
243
+ insufficient_context: "No Comparable Evidence",
171
244
  artifact_unavailable: "Reader Unavailable",
172
245
  };
173
246
  if (typeof value !== "string" || !value) return "Unavailable";
@@ -176,7 +249,7 @@ function humanizeStatus(value) {
176
249
 
177
250
  function memorySummary(status, tier) {
178
251
  if (status === "historical_match" && tier.score === 5) return "Match Found";
179
- if (tier.score > 0) return "Limited Match";
252
+ if (tier.score > 0) return tier.basis;
180
253
  return humanizeStatus(status);
181
254
  }
182
255
 
@@ -307,7 +380,7 @@ function ratioLabel(value) {
307
380
  }
308
381
 
309
382
  function horizonLabel(horizon) {
310
- return `${horizon} Minutes`;
383
+ return `${horizon} Min`;
311
384
  }
312
385
 
313
386
  function setupLine(label, value, tone, enabled) {
@@ -427,17 +500,10 @@ function gridFrameParts(value) {
427
500
 
428
501
  function gridMatchColumns(match, colorEnabled) {
429
502
  const tier = matchTier(match);
430
- const label = tier.score === 5 ? "Exact" : tier.score > 0 ? "Limited" : "No match";
431
503
  const rowCount = countLabel(record(match).row_count);
432
- const countLabelText = tier.score === 5 ? "exact observations" : "comparable observations";
433
- const cells = Array.from({ length: 5 }, (_, index) => color(
434
- index < tier.score ? "■" : "□",
435
- index < tier.score ? "green" : "neutral",
436
- colorEnabled,
437
- )).join("");
438
504
  return {
439
- label: `${cells} ${label}`,
440
- comparable: `${rowCount} ${countLabelText}`,
505
+ label: `${matchCells(tier.score, colorEnabled)} ${matchShortLabel(tier.score)}`,
506
+ comparable: matchEvidenceLabel(tier, rowCount),
441
507
  };
442
508
  }
443
509
 
@@ -514,7 +580,7 @@ export function buildEvaluationViewModel(response) {
514
580
  statusReason: memory.reason,
515
581
  match: {
516
582
  score: tier.score,
517
- label: tier.score === 5 ? "Exact" : tier.score > 0 ? "Limited" : "No match",
583
+ label: matchShortLabel(tier.score),
518
584
  rowCount: countLabel(record(match).row_count),
519
585
  basis: tier.basis,
520
586
  },
@@ -555,7 +621,7 @@ function formatGridEvaluation(response, { colorEnabled = true } = {}) {
555
621
  const topLines = [
556
622
  gridColumns(
557
623
  { text: `${sideText} EVIDENCE`, tone: sideTone(requestedSide) },
558
- { text: "MATCH STRENGTH", tone: "white" },
624
+ { text: "EVIDENCE STRENGTH", tone: "white" },
559
625
  colorEnabled,
560
626
  ),
561
627
  gridColumns(
@@ -586,6 +652,8 @@ function formatGridEvaluation(response, { colorEnabled = true } = {}) {
586
652
  }
587
653
 
588
654
  export function formatEvaluation(response, { colorEnabled = true, setup = false, verbose = false, compact = false, layout = "ledger" } = {}) {
655
+ const unavailableSymbol = formatUnavailableSymbolGuidance(response, colorEnabled);
656
+ if (unavailableSymbol) return unavailableSymbol;
589
657
  if (layout === "grid") {
590
658
  const { status } = memoryStatus(response);
591
659
  return status === "artifact_unavailable"
@@ -672,6 +740,7 @@ export function formatEvaluation(response, { colorEnabled = true, setup = false,
672
740
  ...strategyMixLines(counts.counts, strategyTotal, colorEnabled),
673
741
  sectionBottom(colorEnabled),
674
742
  ];
743
+ const matchCount = matchCountMetric(matchCounts.tier, matchCounts);
675
744
  const outcomeGrid = [
676
745
  sectionRule("Outcome", colorEnabled),
677
746
  outcomeRow("Timeframe", outcomeRows.map((row) => ({ value: horizonLabel(row.horizon), tone: "white" })), colorEnabled),
@@ -705,10 +774,9 @@ export function formatEvaluation(response, { colorEnabled = true, setup = false,
705
774
  "",
706
775
  "Market Memory",
707
776
  compactMetricLine("Status", memorySummary(status, matchCounts.tier), statusTone, colorEnabled),
708
- compactMetricLine("Match Strength", `${matchCounts.tier.score}/5`, matchCounts.tier.score === 5 ? "green" : matchCounts.tier.score > 0 ? "yellow" : "dim", colorEnabled),
709
- ...(matchCounts.tier.score === 5 ? [compactMetricLine("Exact Matches", countLabel(matchCounts.exact), "bold", colorEnabled)] : []),
710
- ...(matchCounts.tier.score > 0 && matchCounts.tier.score < 5 ? [compactMetricLine("Comparable Matches", countLabel(matchCounts.comparable), "bold", colorEnabled)] : []),
711
- ...(verbose && matchCounts.tier.basis ? [compactMetricLine("Match Basis", matchCounts.tier.basis, "dim", colorEnabled)] : []),
777
+ compactMetricLine("Evidence Strength", `${matchCounts.tier.score}/5`, matchCounts.tier.score === 5 ? "green" : matchCounts.tier.score > 0 ? "yellow" : "dim", colorEnabled),
778
+ ...(matchCount ? [compactMetricLine(matchCount.label, matchCount.value, "bold", colorEnabled)] : []),
779
+ ...(verbose && matchCounts.tier.basis ? [compactMetricLine("Evidence Basis", matchCounts.tier.basis, "dim", colorEnabled)] : []),
712
780
  compactMetricLine("Window", coverageWindow(response, match), "dim", colorEnabled),
713
781
  "",
714
782
  "Outcome",
@@ -727,10 +795,9 @@ export function formatEvaluation(response, { colorEnabled = true, setup = false,
727
795
  "",
728
796
  sectionRule("Market Memory", colorEnabled),
729
797
  metricLine("Status", memorySummary(status, matchCounts.tier), statusTone, colorEnabled, 24),
730
- metricLine("Match Strength", `${matchCounts.tier.score}/5`, matchCounts.tier.score === 5 ? "green" : matchCounts.tier.score > 0 ? "yellow" : "dim", colorEnabled, 24),
731
- ...(matchCounts.tier.score === 5 ? [metricLine("Exact Matches", countLabel(matchCounts.exact), "bold", colorEnabled, 24)] : []),
732
- ...(matchCounts.tier.score > 0 && matchCounts.tier.score < 5 ? [metricLine("Comparable Matches", countLabel(matchCounts.comparable), "bold", colorEnabled, 24)] : []),
733
- ...(verbose && matchCounts.tier.basis ? [metricLine("Match Basis", matchCounts.tier.basis, "dim", colorEnabled, 24)] : []),
798
+ metricLine("Evidence Strength", `${matchCounts.tier.score}/5`, matchCounts.tier.score === 5 ? "green" : matchCounts.tier.score > 0 ? "yellow" : "dim", colorEnabled, 24),
799
+ ...(matchCount ? [metricLine(matchCount.label, matchCount.value, "bold", colorEnabled, 24)] : []),
800
+ ...(verbose && matchCounts.tier.basis ? [metricLine("Evidence Basis", matchCounts.tier.basis, "dim", colorEnabled, 24)] : []),
734
801
  metricLine("Window", coverageWindow(response, match), "dim", colorEnabled, 24),
735
802
  sectionBottom(colorEnabled),
736
803
  "",
package/src/live-ui.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { stdin as processInput, stdout as processOutput } from "node:process";
2
2
 
3
3
  import { ansiForeground, LEROY_COLORS } from "./colors.mjs";
4
- import { buildEvaluationViewModel } from "./format.mjs";
4
+ import { buildEvaluationViewModel, unavailableSymbolGuidance } from "./format.mjs";
5
5
 
6
6
  const MAX_CARD_WIDTH = 67;
7
7
  const MIN_CARD_WIDTH = 64;
@@ -235,7 +235,7 @@ function renderOutcomeTable(outcomes, colorEnabled) {
235
235
  const rows = [
236
236
  [
237
237
  tableValue("Timeframe", "muted", colorEnabled, { bold: true }),
238
- ...outcomes.map((outcome) => tableValue(`${outcome.horizon} Minutes`, "white", colorEnabled, { bold: true, hAlign: "right" })),
238
+ ...outcomes.map((outcome) => tableValue(`${outcome.horizon} Min`, "white", colorEnabled, { bold: true, hAlign: "right" })),
239
239
  ],
240
240
  [
241
241
  tableValue("Avg. Return", "muted", colorEnabled, { bold: true }),
@@ -312,24 +312,30 @@ function bodyLine(content, colorEnabled, layout) {
312
312
  }
313
313
 
314
314
  function renderHistoricalEvidence(view, colorEnabled) {
315
- const sampleLabel = view.match.score === 5
316
- ? " exact observations"
317
- : view.match.score > 0
318
- ? " comparable observations"
319
- : " observations";
320
- const matchLabel = view.match.label === "No match" ? "No Match" : `${view.match.label} Match`;
321
- const matchDetails = view.match.score > 0 ? `${view.match.rowCount}${sampleLabel}` : "No comparable observations.";
315
+ const score = view.match.score;
316
+ const activeTone = score >= 4 ? "green" : score >= 2 ? "yellow" : score === 1 ? "red" : "inactive";
317
+ const detailLabels = {
318
+ 5: "exact observations",
319
+ 4: "cross-ticker observations",
320
+ 3: "signal-pattern observations",
321
+ 2: "context observations",
322
+ 1: "baseline observations",
323
+ };
324
+ const matchDetails = detailLabels[score]
325
+ ? `${view.match.rowCount} ${detailLabels[score]}`
326
+ : "No comparable observations";
322
327
  const matchStrength = [
323
328
  Array.from({ length: 5 }, (_, index) => tone(
324
- strengthCellGlyph(index < view.match.score, colorEnabled),
325
- index < view.match.score ? "green" : "neutral",
329
+ index < score ? "●" : "○",
330
+ index < score ? activeTone : "inactive",
326
331
  colorEnabled,
327
- )).join(" "),
328
- tone(matchLabel, view.match.score > 0 ? "white" : "muted", colorEnabled, true),
332
+ )).join(""),
333
+ tone(`${score}/5`, score > 0 ? "white" : "muted", colorEnabled, true),
329
334
  ].join(" ");
330
335
  const rows = [
331
- [tableValue("Match Strength", "muted", colorEnabled, { bold: true }), { content: matchStrength, hAlign: "left" }],
332
- [tableValue("Match Details", "muted", colorEnabled, { bold: true }), tableValue(matchDetails, "neutral", colorEnabled, { bold: true })],
336
+ [tableValue("Evidence Strength", "muted", colorEnabled, { bold: true }), { content: matchStrength, hAlign: "left" }],
337
+ [tableValue("Evidence Basis", "muted", colorEnabled, { bold: true }), tableValue(view.match.basis ?? "No Comparable Evidence", "neutral", colorEnabled)],
338
+ [tableValue("Observations", "muted", colorEnabled, { bold: true }), tableValue(matchDetails, "neutral", colorEnabled, { bold: true })],
333
339
  [tableValue("Date Range", "muted", colorEnabled, { bold: true }), tableValue(view.archiveWindow, "neutral", colorEnabled)],
334
340
  ];
335
341
  return renderTable(rows, tableColumnWidths(rows), colorEnabled, { divider: false });
@@ -366,6 +372,24 @@ export function renderEvidenceView(response, { colorEnabled = true, columns = 80
366
372
  ? new Date(updatedAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
367
373
  : "—";
368
374
  const liveTone = phase === "error" ? "red" : phase === "checking" ? "yellow" : "green";
375
+ const unavailableGuidance = unavailableSymbolGuidance(response);
376
+ if (unavailableGuidance) {
377
+ const lines = renderLoadingSteps({
378
+ symbol: unavailableGuidance.symbol,
379
+ progress,
380
+ colorEnabled,
381
+ columns,
382
+ }).split("\n");
383
+ lines.push("");
384
+ lines.push(`${layout.padding}${tone("✕", "red", colorEnabled, true)} ${tone(`${unavailableGuidance.symbol} is not currently covered by Leroy.`, "white", colorEnabled)}`);
385
+ if (unavailableGuidance.suggestedSymbol) {
386
+ lines.push("");
387
+ lines.push(`${layout.padding} ${tone(`Did you mean ${unavailableGuidance.suggestedSymbol}?`, "white", colorEnabled)}`);
388
+ lines.push(`${layout.padding} ${tone(`Retry: leroy evaluate ${unavailableGuidance.suggestedSymbol}`, "neutral", colorEnabled)}`);
389
+ }
390
+ lines.push("", "");
391
+ return lines.join("\n");
392
+ }
369
393
  if (view.status === "artifact_unavailable") {
370
394
  const lines = ["", ""];
371
395
  lines.push(`${layout.padding}${tone("!", liveTone, colorEnabled, true)} ${tone("Market Memory unavailable", "white", colorEnabled)}`);