sigrank 0.0.188 → 0.0.189
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 +5 -4
- package/adapters/index.mjs +64 -0
- package/lib/constants.mjs +19 -0
- package/package.json +1 -1
- package/presentation/cli.mjs +19 -9
- package/presentation/tui.mjs +10 -3
- package/submit/index.mjs +1 -0
- package/tools/index.mjs +24 -7
package/README.md
CHANGED
|
@@ -342,6 +342,7 @@ All adapters are token-only (no message content, no cost fields, no credentials)
|
|
|
342
342
|
| Amp | ✅ `~/.local/share/amp/threads` | Native 4-pillar; per-message |
|
|
343
343
|
| Kimi | ✅ `~/.kimi/sessions` | Native 4-pillar; `StatusUpdate` lines only |
|
|
344
344
|
| pi-agent | ✅ `~/.pi/agent/sessions` | Native 4-pillar; per-message JSONL |
|
|
345
|
+
| oh-my-pi (`omp`) | ✅ `~/.omp/agent/sessions` (recursive, incl. nested subagent transcripts) | Native 4-pillar from `.message.usage` on `type:"message"` entries (`cacheWrite`→`cacheCreate`); **`reasoningTokens` is already inside `output`** — never added, or it double-counts; `usage.cost` reuses the same four key names for USD floats and is dropped; dedup by (session header `id`, entry `id`). Separate harness from pi-agent |
|
|
345
346
|
| OpenClaw | ✅ `~/.openclaw` | Native 4-pillar; per-message JSONL |
|
|
346
347
|
| Droid | ✅ `~/.factory/sessions/*.settings.json` | Native 4-pillar; `thinking_tokens`→output |
|
|
347
348
|
| Codebuff | ✅ `~/.config/manicode` | Native 4-pillar; `chat-messages.json` |
|
|
@@ -382,19 +383,19 @@ All adapters are token-only (no message content, no cost fields, no credentials)
|
|
|
382
383
|
## Dev / test
|
|
383
384
|
|
|
384
385
|
```bash
|
|
385
|
-
node test.mjs #
|
|
386
|
+
node test.mjs # 14 test groups, 313 assertions (no network; fs writes confined to OS-tmpdir fixtures)
|
|
386
387
|
node sign.test.mjs # ed25519 signing + canon parity
|
|
387
388
|
node index.mjs # stdio MCP server directly (pipe to MCP client)
|
|
388
389
|
```
|
|
389
390
|
|
|
390
|
-
Tests verify (
|
|
391
|
+
Tests verify (14 groups, 313 assertions):
|
|
391
392
|
|
|
392
393
|
- `rank_paste` canon: MO§ES `(1251211, 11296121, 128196310, 2555179769)` → Υ 18436.98 · TRANSMITTER
|
|
393
394
|
- `submit_paste` preview (no codename) + POST shape (injected fetch, no live writes)
|
|
394
395
|
- `tokenpull` dedup, window slicing, 4-window pillars (mock adapter)
|
|
395
396
|
- `tokenpull_submit` all 4 windows POST, sha256 hash, ddmmyy stamp
|
|
396
397
|
- `tokenpullCodex` io_ratio conversion per-window
|
|
397
|
-
- Adapter registry (
|
|
398
|
+
- Adapter registry (16 platforms) + per-adapter shape contracts
|
|
398
399
|
- `rank_windows` 4-window paste scoring, partial input, no-network
|
|
399
400
|
- `watch_tokenpull` cascade snapshot, interval_s, submit path
|
|
400
401
|
- `enroll` posts identity (public key only), maps 201 enrolled + 410 code_invalid
|
|
@@ -414,7 +415,7 @@ Tests verify (13 groups, 200 assertions):
|
|
|
414
415
|
| `tui.mjs` | Full tabbed TUI: Dashboard / Trends / Compare / Board / Watch / Connect |
|
|
415
416
|
| `cascade.mjs` | Pure cascade math (Υ, SNR, leverage, velocity, 10xDEV, class) |
|
|
416
417
|
| `tokenpull.mjs` | On-device log scanner — Claude, Codex, multi-platform |
|
|
417
|
-
| `adapters.mjs` | Platform adapter registry (
|
|
418
|
+
| `adapters.mjs` | Platform adapter registry (16+ platforms) |
|
|
418
419
|
| `tools.mjs` | MCP tool table + dispatcher |
|
|
419
420
|
| `connect.mjs` | Connect-code enrollment + device identity |
|
|
420
421
|
| `keystore.mjs` | Local key management (paste-keys, not API keys) |
|
package/adapters/index.mjs
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* tokenpullCodex(); adapters that yield native 4-pillar via messages() go through
|
|
25
25
|
* tokenpull() directly. The split happens at the tokenpullAny() routing layer.)
|
|
26
26
|
* - Reasoning / thinking tokens → folded into `output` (they are output-side spend)
|
|
27
|
+
* EXCEPTION: omp already reports reasoningTokens INSIDE output — see adapter #16
|
|
27
28
|
* - No cache-creation data available → cacheCreate: 0 + adapter sets `estimated: true`
|
|
28
29
|
* - Cost fields (USD) → NEVER used or forwarded (SigRank scores cost efficiency from
|
|
29
30
|
* token ratios, not from dollar amounts — cost efficiency is derived, not ingested)
|
|
@@ -875,6 +876,68 @@ export const otherAdapter = {
|
|
|
875
876
|
},
|
|
876
877
|
};
|
|
877
878
|
|
|
879
|
+
// ── 16. oh-my-pi (omp) ───────────────────────────────────────────────────────
|
|
880
|
+
// ~/.omp/agent/sessions/<bucket>/<timestamp>_<sessionId>/<name>.jsonl (recursive)
|
|
881
|
+
// NOT pi-agent (#4): oh-my-pi forked from pi long ago and is a separate harness
|
|
882
|
+
// with its own on-disk format. Line 1 is a fixed-width 256-byte {"type":"title"}
|
|
883
|
+
// slot; line 2 is {"type":"session"} and carries the session `.id`. Neither line
|
|
884
|
+
// holds usage. Other observed `.type` values (all usage-free): custom,
|
|
885
|
+
// custom_message, model_change, thinking_level_change, session_init, credential_pin.
|
|
886
|
+
// Usage lives at `.message.usage` on `.type === "message"` entries:
|
|
887
|
+
// { input, output, cacheRead, cacheWrite, totalTokens, reasoningTokens, cost:{…} }
|
|
888
|
+
// Native 4-pillar (no estimation): input→input, output→output,
|
|
889
|
+
// cacheWrite→cacheCreate, cacheRead→cacheRead.
|
|
890
|
+
// TRAP 1 — reasoningTokens is ALREADY INSIDE output. Do NOT fold it in. Every other
|
|
891
|
+
// adapter here folds reasoning into output, so the reflex is wrong for omp; adding
|
|
892
|
+
// it double-counts. Proof: totalTokens === input+output+cacheRead+cacheWrite
|
|
893
|
+
// exactly, and reasoningTokens is always ≤ output.
|
|
894
|
+
// TRAP 2 — usage.cost reuses the SAME four key names (input/output/cacheRead/
|
|
895
|
+
// cacheWrite, plus total) for USD floats. Read the four counts straight off
|
|
896
|
+
// `usage`; never descend into `cost`. Cost fields are never forwarded (file header).
|
|
897
|
+
// Nested subagent transcripts (critic.jsonl beside critic/__advisor.jsonl) are real
|
|
898
|
+
// operator work and ARE counted — same policy as Claude's `subagents/`.
|
|
899
|
+
// Dedup key is (session header `.id`, entry top-level `.id`) — `.message` has no id.
|
|
900
|
+
// File cap: a real omp tree runs to 20k+ transcripts, past walkFiles' 10_000 default,
|
|
901
|
+
// which would silently drop half the operator's tokens — pass an explicit cap.
|
|
902
|
+
const OMP_MAX_FILES = 500_000;
|
|
903
|
+
export const ompAdapter = {
|
|
904
|
+
platform: "omp",
|
|
905
|
+
defaultRoot: () => join(homedir(), ".omp", "agent", "sessions"),
|
|
906
|
+
async *messages(root) {
|
|
907
|
+
for (const r of roots("OMP_DATA_DIR", root)) {
|
|
908
|
+
for await (const path of walkFiles(r, isJsonl, { n: 0 }, OMP_MAX_FILES)) {
|
|
909
|
+
const text = await readUtf8(path);
|
|
910
|
+
let sid = null;
|
|
911
|
+
for (const [ev] of parseJsonl(text, path)) {
|
|
912
|
+
if (!ev) continue;
|
|
913
|
+
if (ev.type === "session") {
|
|
914
|
+
sid = ev.id || null; // line-2 header: session half of the dedup key
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (ev.type !== "message") continue;
|
|
918
|
+
const u = ev.message && ev.message.usage;
|
|
919
|
+
if (!u) continue;
|
|
920
|
+
const input = Number(u.input || 0);
|
|
921
|
+
const output = Number(u.output || 0); // reasoningTokens ALREADY inside — TRAP 1
|
|
922
|
+
const cacheCreate = Number(u.cacheWrite || 0);
|
|
923
|
+
const cacheRead = Number(u.cacheRead || 0);
|
|
924
|
+
if (input + output + cacheCreate + cacheRead === 0) continue;
|
|
925
|
+
yield {
|
|
926
|
+
id: ev.id || null,
|
|
927
|
+
sid,
|
|
928
|
+
ts: ev.timestamp || null,
|
|
929
|
+
input,
|
|
930
|
+
output,
|
|
931
|
+
cacheCreate,
|
|
932
|
+
cacheRead,
|
|
933
|
+
file: path,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
|
|
878
941
|
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
879
942
|
/** All non-Claude, non-Codex adapters keyed by platform ID. */
|
|
880
943
|
export const ADAPTERS = {
|
|
@@ -893,6 +956,7 @@ export const ADAPTERS = {
|
|
|
893
956
|
hermes: hermesAdapter,
|
|
894
957
|
devin: devinAdapter,
|
|
895
958
|
other: otherAdapter,
|
|
959
|
+
omp: ompAdapter,
|
|
896
960
|
};
|
|
897
961
|
|
|
898
962
|
export const ALL_PLATFORMS = Object.keys(ADAPTERS).concat(["claude", "codex"]);
|
package/lib/constants.mjs
CHANGED
|
@@ -62,6 +62,8 @@ export const TOKSCALE_CLIENT_MAP = {
|
|
|
62
62
|
opencode: "opencode",
|
|
63
63
|
openclaw: "openclaw",
|
|
64
64
|
pi: "pi",
|
|
65
|
+
omp: "omp",
|
|
66
|
+
"oh-my-pi": "omp",
|
|
65
67
|
cursor: "other",
|
|
66
68
|
roocode: "other",
|
|
67
69
|
mux: "other",
|
|
@@ -87,6 +89,23 @@ export const TOKSCALE_CLIENT_MAP = {
|
|
|
87
89
|
synthetic: null,
|
|
88
90
|
};
|
|
89
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Platforms whose local data tokscale CANNOT see, so `tokscale models --json`
|
|
94
|
+
* never lists them as an active client. pullActivePlatforms() auto-detect must
|
|
95
|
+
* probe these directly or their cascade row can never appear, even with GBs of
|
|
96
|
+
* native local telemetry (that was exactly the omp bug: tokscale reports
|
|
97
|
+
* claude/codex/copilot/gemini/grok/kimi/kiro/opencode/pi and nothing else, so
|
|
98
|
+
* a tokscale-detected machine silently dropped oh-my-pi entirely).
|
|
99
|
+
*
|
|
100
|
+
* The adapter pull itself filters out platforms with no data, so probing a
|
|
101
|
+
* platform the operator doesn't use costs one cheap missing-directory walk.
|
|
102
|
+
*
|
|
103
|
+
* As of 2026-08-08: oh-my-pi. Delete an entry once tokscale reports that client
|
|
104
|
+
* (TOKSCALE_CLIENT_MAP already maps the `omp` / `oh-my-pi` client names for
|
|
105
|
+
* when that happens).
|
|
106
|
+
*/
|
|
107
|
+
export const TOKSCALE_BLIND_PLATFORMS = ["omp"];
|
|
108
|
+
|
|
90
109
|
/**
|
|
91
110
|
* The "actually on the board" predicate. A submission is ranked only when the
|
|
92
111
|
* server returns HTTP ok AND verification_tier === "verified" AND persisted
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.189",
|
|
4
4
|
"author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
|
|
5
5
|
"description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
|
|
6
6
|
"mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
|
package/presentation/cli.mjs
CHANGED
|
@@ -782,10 +782,17 @@ async function runCompare({ platform = "claude" } = {}) {
|
|
|
782
782
|
const WATCH_WINDOWS = ["7d", "30d", "90d", "all"];
|
|
783
783
|
|
|
784
784
|
// Detect which platforms have real local data (input+output > 0 in any window) via tokenpull.
|
|
785
|
-
// Returns an array of pull results (the same shape tokenpullAny yields), one per active
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
785
|
+
// Returns an array of pull results (the same shape tokenpullAny yields), one per active
|
|
786
|
+
// platform, through the shared loader used by `me` / `watch` / the TUI Dashboard.
|
|
787
|
+
//
|
|
788
|
+
// `platform` scopes the PULL, not just the render: the watcher re-pulls every `refresh`
|
|
789
|
+
// seconds (default 30) and an unscoped auto-detect also probes the tokscale-blind ~10 GiB
|
|
790
|
+
// oh-my-pi tree (~46s), so `watch --platform claude` used to scan everything and then
|
|
791
|
+
// display one row — scan longer than refresh interval, permanently saturated.
|
|
792
|
+
// null / '' / 'all' still mean "everything", keeping withTokscaleBlind's omp row.
|
|
793
|
+
export async function detectActivePulls(platform, opts = {}) {
|
|
794
|
+
const scoped = platform && platform !== "all" ? [platform] : undefined;
|
|
795
|
+
return pullActivePlatforms({ platforms: scoped }, opts).catch(() => []);
|
|
789
796
|
}
|
|
790
797
|
|
|
791
798
|
async function runWatch({
|
|
@@ -815,9 +822,9 @@ async function runWatch({
|
|
|
815
822
|
write(HIDE_CURSOR);
|
|
816
823
|
|
|
817
824
|
const draw = async () => {
|
|
818
|
-
// Re-
|
|
819
|
-
|
|
820
|
-
|
|
825
|
+
// Re-pull each tick so a platform that just started writing logs appears automatically.
|
|
826
|
+
// Scoped by platFilter, so a focused watch never scans platforms it cannot display.
|
|
827
|
+
const pulls = await detectActivePulls(platFilter);
|
|
821
828
|
|
|
822
829
|
if (lines > 0) write(CURSOR_UP(lines));
|
|
823
830
|
|
|
@@ -961,8 +968,11 @@ async function runSigRank() {
|
|
|
961
968
|
writeln(` ${gold("⊙ SigRank")} ${bold("Operator Dashboard")}`);
|
|
962
969
|
writeln(` ${dim("reading local data…")}`);
|
|
963
970
|
|
|
971
|
+
// ../tokenpull.mjs — cli.mjs lives in presentation/, so "./tokenpull.mjs" resolved to
|
|
972
|
+
// presentation/tokenpull.mjs (nonexistent) and threw ERR_MODULE_NOT_FOUND, taking the
|
|
973
|
+
// whole dashboard read down. Matches the `../adapters.mjs` static import above.
|
|
964
974
|
const { tokenpullAny, freshVerifierPillars } =
|
|
965
|
-
await import("
|
|
975
|
+
await import("../tokenpull.mjs");
|
|
966
976
|
|
|
967
977
|
// Local sources first (fast) — board fetch with 5s timeout runs in parallel
|
|
968
978
|
const boardPromise = Promise.race([
|
|
@@ -971,7 +981,7 @@ async function runSigRank() {
|
|
|
971
981
|
]);
|
|
972
982
|
|
|
973
983
|
// FIX A1-CLI (2026-06-27): verifiers (ccusage/tokscale/tokendash) are fetched
|
|
974
|
-
// ON-DEMAND for ACTIVE platforms only — not synchronously for
|
|
984
|
+
// ON-DEMAND for ACTIVE platforms only — not synchronously for every ALL_PLATFORMS entry
|
|
975
985
|
// upfront (which blocked Dashboard paint up to ~40s, same class of bug as TUI
|
|
976
986
|
// FIX A1). The sync ccusagePillars()/tokscalePillars()/tokenDashPillars() loop
|
|
977
987
|
// over ALL_PLATFORMS is replaced by parallel freshVerifierPillars() calls after
|
package/presentation/tui.mjs
CHANGED
|
@@ -644,8 +644,10 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
|
|
|
644
644
|
emit(` ${dim("·".repeat(Math.max(0, Math.min(w - 4, narrow ? 96 : 114))))}`);
|
|
645
645
|
|
|
646
646
|
if (active.length === 0) {
|
|
647
|
+
// Count is derived from the registry — a hardcoded number drifts every time an
|
|
648
|
+
// adapter is added (it read "14 platforms" while ALL_PLATFORMS held 17).
|
|
647
649
|
emit(
|
|
648
|
-
` ${dim(
|
|
650
|
+
` ${dim(` reading token logs… (${ALL_PLATFORMS.length} platforms, ~5s · press [R] to refresh)`)}`,
|
|
649
651
|
);
|
|
650
652
|
}
|
|
651
653
|
|
|
@@ -1586,7 +1588,10 @@ function renderConnect(id, codeBuf = "", msg = "") {
|
|
|
1586
1588
|
// null/'all'/'all-windows' means "everything". Each row is a (platform, window) cascade cell:
|
|
1587
1589
|
// Υ Yield + SNR/Lev/Vel/class. Iterates active platforms (input+output > 0 in any window).
|
|
1588
1590
|
async function renderWatch(platform = "all", win = "all-windows") {
|
|
1589
|
-
|
|
1591
|
+
// ../tokenpull.mjs — this file lives in presentation/, so "./tokenpull.mjs" resolved
|
|
1592
|
+
// to presentation/tokenpull.mjs (nonexistent) and threw ERR_MODULE_NOT_FOUND for
|
|
1593
|
+
// every row. Matches the static `../tokenpull.mjs` import at the top of this file.
|
|
1594
|
+
const { tokenpullAny } = await import("../tokenpull.mjs");
|
|
1590
1595
|
const ALL_PLATFORMS = [
|
|
1591
1596
|
"claude",
|
|
1592
1597
|
"codex",
|
|
@@ -1602,6 +1607,7 @@ async function renderWatch(platform = "all", win = "all-windows") {
|
|
|
1602
1607
|
"copilot",
|
|
1603
1608
|
"openclaw",
|
|
1604
1609
|
"pi",
|
|
1610
|
+
"omp",
|
|
1605
1611
|
];
|
|
1606
1612
|
const platFilter = platform && platform !== "all" ? platform : null;
|
|
1607
1613
|
const winFilter = win && win !== "all-windows" ? win : null;
|
|
@@ -2064,7 +2070,7 @@ export async function runTui({
|
|
|
2064
2070
|
dashData = { active: [], _loading: true };
|
|
2065
2071
|
await redraw();
|
|
2066
2072
|
// FIX 0: progressive Dashboard load — claude first (fast), render it, THEN fill
|
|
2067
|
-
// the
|
|
2073
|
+
// the remaining ALL_PLATFORMS entries in the background + redraw. The user sees their cascade
|
|
2068
2074
|
// within ~1 read instead of a 7s blank "loading dashboard…".
|
|
2069
2075
|
dashData = await loadDashboardData().catch((e) => {
|
|
2070
2076
|
status = `dashboard error: ${e.message}`;
|
|
@@ -2481,6 +2487,7 @@ export async function runTui({
|
|
|
2481
2487
|
"copilot",
|
|
2482
2488
|
"openclaw",
|
|
2483
2489
|
"pi",
|
|
2490
|
+
"omp",
|
|
2484
2491
|
];
|
|
2485
2492
|
if (k === "p" && (activeTab === 2 || activeTab === 4)) {
|
|
2486
2493
|
if (activeTab === 2) {
|
package/submit/index.mjs
CHANGED
package/tools/index.mjs
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
import { ALL_PLATFORMS } from "../adapters/index.mjs";
|
|
10
10
|
import { execFileAsync } from "./_helpers.mjs";
|
|
11
11
|
import { buildFetch, DEFAULT_API_BASE, DEFAULT_FETCH_TIMEOUT } from "./_helpers.mjs";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
TOKSCALE_CLIENT_MAP,
|
|
14
|
+
TOKSCALE_BLIND_PLATFORMS,
|
|
15
|
+
} from "../lib/constants.mjs";
|
|
13
16
|
|
|
14
17
|
import { TOOL_DEF as rankPasteTool, handleRankPaste } from "./rank-paste.mjs";
|
|
15
18
|
import { TOOL_DEF as getLeaderboardTool, handleGetLeaderboard } from "./get-leaderboard.mjs";
|
|
@@ -98,10 +101,22 @@ async function _tokscaleDetectClients() {
|
|
|
98
101
|
return [...clients];
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
/**
|
|
104
|
+
/** Detection targets = what tokscale saw ∪ the platforms tokscale is blind to.
|
|
105
|
+
* Without the union, an operator with GBs of oh-my-pi telemetry gets no omp row
|
|
106
|
+
* on any machine where tokscale succeeds, because tokscale never names that
|
|
107
|
+
* client. Pure + exported so the union is unit-testable without shelling out. */
|
|
108
|
+
export function withTokscaleBlind(detected) {
|
|
109
|
+
return [...new Set([...(detected || []), ...TOKSCALE_BLIND_PLATFORMS])];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Pull a specific set of platforms in parallel, filter to active ones.
|
|
113
|
+
* opts.callTool overrides the per-platform pull for tests (same opts-injection
|
|
114
|
+
* convention as opts.adapter in pullByPlatform) so the target-set logic is
|
|
115
|
+
* assertable without a real filesystem scan. */
|
|
102
116
|
async function _pullExplicit(platforms, opts = {}) {
|
|
117
|
+
const pull = opts.callTool || callTool;
|
|
103
118
|
const settled = await Promise.allSettled(
|
|
104
|
-
platforms.map((p) =>
|
|
119
|
+
platforms.map((p) => pull("tokenpull", { platform: p }, opts)),
|
|
105
120
|
);
|
|
106
121
|
const active = settled
|
|
107
122
|
.filter((r) => r.status === "fulfilled" && r.value)
|
|
@@ -129,15 +144,17 @@ export async function pullActivePlatforms({ platforms } = {}, opts = {}) {
|
|
|
129
144
|
return _pullExplicit(platforms, opts);
|
|
130
145
|
}
|
|
131
146
|
// Auto-detect: run tokscale once to discover all active clients, then pull
|
|
132
|
-
// only those. This is much faster than trying
|
|
133
|
-
// silently on a machine that doesn't have that tool
|
|
147
|
+
// only those (plus the tokscale-blind platforms). This is much faster than trying
|
|
148
|
+
// all adapters (most fail silently on a machine that doesn't have that tool).
|
|
134
149
|
//
|
|
135
150
|
// Flow: tokscale surfaces ALL clients → map to our platform names →
|
|
136
151
|
// ccusage is primary for Claude (more accurate) → other platforms use
|
|
137
152
|
// their adapters. Anything in tokscale NOT in ccusage gets included.
|
|
138
|
-
|
|
153
|
+
// opts.detectClients overrides the tokscale probe for tests (no shelling out).
|
|
154
|
+
const detect = opts.detectClients || _tokscaleDetectClients;
|
|
155
|
+
const detected = await detect().catch(() => null);
|
|
139
156
|
if (detected && detected.length > 0) {
|
|
140
|
-
return _pullExplicit(detected, opts);
|
|
157
|
+
return _pullExplicit(withTokscaleBlind(detected), opts);
|
|
141
158
|
}
|
|
142
159
|
// Fallback: tokscale not installed or failed → try all adapters (old behavior)
|
|
143
160
|
return _pullExplicit(ALL_PLATFORMS, opts);
|