modelstat 0.4.1 → 0.5.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/dist/cli.mjs +199 -23
- package/dist/cli.mjs.map +1 -1
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -34544,7 +34544,8 @@ function load() {
|
|
|
34544
34544
|
apiUrl: obj.apiUrl ?? DEFAULTS.apiUrl,
|
|
34545
34545
|
cursor: obj.cursor ?? {},
|
|
34546
34546
|
segmentsSent: obj.segmentsSent ?? 0,
|
|
34547
|
-
processingVersion: obj.processingVersion ?? null
|
|
34547
|
+
processingVersion: obj.processingVersion ?? null,
|
|
34548
|
+
reconcileCache: obj.reconcileCache ?? {}
|
|
34548
34549
|
};
|
|
34549
34550
|
} catch {
|
|
34550
34551
|
cache2 = { ...DEFAULTS, cursor: {} };
|
|
@@ -34566,7 +34567,8 @@ var init_runtime_state = __esm({
|
|
|
34566
34567
|
apiUrl: "",
|
|
34567
34568
|
cursor: {},
|
|
34568
34569
|
segmentsSent: 0,
|
|
34569
|
-
processingVersion: null
|
|
34570
|
+
processingVersion: null,
|
|
34571
|
+
reconcileCache: {}
|
|
34570
34572
|
};
|
|
34571
34573
|
cache2 = null;
|
|
34572
34574
|
runtimeState = {
|
|
@@ -34593,6 +34595,29 @@ var init_runtime_state = __esm({
|
|
|
34593
34595
|
s.cursor = {};
|
|
34594
34596
|
persist(s);
|
|
34595
34597
|
},
|
|
34598
|
+
/** Drop ONE file's cursor so the next scan re-reads it — the precise lever the
|
|
34599
|
+
* self-healing reconcile pulls for the files of sessions the server is missing. */
|
|
34600
|
+
clearCursor(path) {
|
|
34601
|
+
const s = load();
|
|
34602
|
+
if (path in s.cursor) {
|
|
34603
|
+
delete s.cursor[path];
|
|
34604
|
+
persist(s);
|
|
34605
|
+
}
|
|
34606
|
+
},
|
|
34607
|
+
/** Drop cursors for files no longer present so the map tracks the CURRENT file
|
|
34608
|
+
* set, not every file ever seen. Returns how many were pruned. */
|
|
34609
|
+
pruneCursors(present) {
|
|
34610
|
+
const s = load();
|
|
34611
|
+
let removed = 0;
|
|
34612
|
+
for (const p of Object.keys(s.cursor)) {
|
|
34613
|
+
if (!present.has(p)) {
|
|
34614
|
+
delete s.cursor[p];
|
|
34615
|
+
removed += 1;
|
|
34616
|
+
}
|
|
34617
|
+
}
|
|
34618
|
+
if (removed) persist(s);
|
|
34619
|
+
return removed;
|
|
34620
|
+
},
|
|
34596
34621
|
getSegmentsSent() {
|
|
34597
34622
|
return load().segmentsSent;
|
|
34598
34623
|
},
|
|
@@ -34610,6 +34635,15 @@ var init_runtime_state = __esm({
|
|
|
34610
34635
|
s.processingVersion = v;
|
|
34611
34636
|
persist(s);
|
|
34612
34637
|
},
|
|
34638
|
+
/** Self-healing reconcile cache (see {@link RuntimeState.reconcileCache}). */
|
|
34639
|
+
getReconcileCache() {
|
|
34640
|
+
return load().reconcileCache;
|
|
34641
|
+
},
|
|
34642
|
+
setReconcileCache(c) {
|
|
34643
|
+
const s = load();
|
|
34644
|
+
s.reconcileCache = c;
|
|
34645
|
+
persist(s);
|
|
34646
|
+
},
|
|
34613
34647
|
/** Test-only: drop the in-memory cache so the next read hits disk. */
|
|
34614
34648
|
_resetCacheForTests() {
|
|
34615
34649
|
cache2 = null;
|
|
@@ -34884,6 +34918,25 @@ async function uploadBatch(batch) {
|
|
|
34884
34918
|
batch_id: result2.response.batch_id
|
|
34885
34919
|
};
|
|
34886
34920
|
}
|
|
34921
|
+
async function backfillGet(query) {
|
|
34922
|
+
const bearer = state.bearer;
|
|
34923
|
+
if (!bearer) return null;
|
|
34924
|
+
const res = await (0, import_undici.request)(`${state.apiUrl}/v1/backfill/digests${query}`, {
|
|
34925
|
+
method: "GET",
|
|
34926
|
+
headers: { authorization: `Bearer ${bearer}` }
|
|
34927
|
+
});
|
|
34928
|
+
if (res.statusCode >= 300) {
|
|
34929
|
+
await res.body.dump();
|
|
34930
|
+
return null;
|
|
34931
|
+
}
|
|
34932
|
+
return await res.body.json();
|
|
34933
|
+
}
|
|
34934
|
+
function fetchBackfillDays() {
|
|
34935
|
+
return backfillGet("");
|
|
34936
|
+
}
|
|
34937
|
+
function fetchBackfillDaySessions(day) {
|
|
34938
|
+
return backfillGet(`?day=${encodeURIComponent(day)}`);
|
|
34939
|
+
}
|
|
34887
34940
|
var import_undici, DeviceMeUnauthorized, _ingest;
|
|
34888
34941
|
var init_api = __esm({
|
|
34889
34942
|
"src/api.ts"() {
|
|
@@ -36836,9 +36889,7 @@ import { join as join7 } from "path";
|
|
|
36836
36889
|
function withNonNullTokens(e) {
|
|
36837
36890
|
return e.tokens ? e : { ...e, tokens: { ...ZERO_TOKENS } };
|
|
36838
36891
|
}
|
|
36839
|
-
async function
|
|
36840
|
-
const deviceId = state.deviceId;
|
|
36841
|
-
if (!deviceId) throw new Error("daemon not enrolled \u2014 run `register` first");
|
|
36892
|
+
async function discoverJobs(deviceId) {
|
|
36842
36893
|
const jobs = [];
|
|
36843
36894
|
try {
|
|
36844
36895
|
const base = join7(homedir5(), ".claude/projects");
|
|
@@ -36853,8 +36904,8 @@ async function scanAll(cb = {}) {
|
|
|
36853
36904
|
const full = join7(dir, f);
|
|
36854
36905
|
jobs.push({
|
|
36855
36906
|
path: full,
|
|
36856
|
-
parse: async (
|
|
36857
|
-
const r = await parseClaudeCodeJsonl({ deviceId, sourceFile: full, onEvents:
|
|
36907
|
+
parse: async (sink) => {
|
|
36908
|
+
const r = await parseClaudeCodeJsonl({ deviceId, sourceFile: full, onEvents: sink });
|
|
36858
36909
|
return { toolCalls: r.toolCalls ?? [], scriptContexts: r.scriptContexts ?? [] };
|
|
36859
36910
|
}
|
|
36860
36911
|
});
|
|
@@ -36877,8 +36928,8 @@ async function scanAll(cb = {}) {
|
|
|
36877
36928
|
const full = join7(base, y, m, d, f);
|
|
36878
36929
|
jobs.push({
|
|
36879
36930
|
path: full,
|
|
36880
|
-
parse: async (
|
|
36881
|
-
const r = await parseCodexRollout({ deviceId, sourceFile: full, onEvents:
|
|
36931
|
+
parse: async (sink) => {
|
|
36932
|
+
const r = await parseCodexRollout({ deviceId, sourceFile: full, onEvents: sink });
|
|
36882
36933
|
return { toolCalls: r.toolCalls ?? [], scriptContexts: r.scriptContexts ?? [] };
|
|
36883
36934
|
}
|
|
36884
36935
|
});
|
|
@@ -36889,6 +36940,12 @@ async function scanAll(cb = {}) {
|
|
|
36889
36940
|
} catch (e) {
|
|
36890
36941
|
console.warn("codex scan skipped:", e.message);
|
|
36891
36942
|
}
|
|
36943
|
+
return jobs;
|
|
36944
|
+
}
|
|
36945
|
+
async function scanAll(cb = {}) {
|
|
36946
|
+
const deviceId = state.deviceId;
|
|
36947
|
+
if (!deviceId) throw new Error("daemon not enrolled \u2014 run `register` first");
|
|
36948
|
+
const jobs = await discoverJobs(deviceId);
|
|
36892
36949
|
const ordered = (await Promise.all(
|
|
36893
36950
|
jobs.map(async (j) => ({
|
|
36894
36951
|
job: j,
|
|
@@ -37029,7 +37086,7 @@ var init_scan = __esm({
|
|
|
37029
37086
|
init_api();
|
|
37030
37087
|
init_config2();
|
|
37031
37088
|
init_pipeline2();
|
|
37032
|
-
DAEMON_VERSION = true ? "daemon-0.
|
|
37089
|
+
DAEMON_VERSION = true ? "daemon-0.5.0" : "daemon-dev";
|
|
37033
37090
|
BATCH_MAX_EVENTS = INGEST_BATCH_MAX_EVENTS;
|
|
37034
37091
|
BATCH_MAX_TOOL_CALLS = 2e4;
|
|
37035
37092
|
BATCH_BUFFER_HARD_CAP = BATCH_MAX_EVENTS * 2;
|
|
@@ -37182,6 +37239,117 @@ var init_lock = __esm({
|
|
|
37182
37239
|
}
|
|
37183
37240
|
});
|
|
37184
37241
|
|
|
37242
|
+
// src/reconcile.ts
|
|
37243
|
+
import { stat as stat3 } from "fs/promises";
|
|
37244
|
+
function utcDay(ts) {
|
|
37245
|
+
const d = new Date(ts);
|
|
37246
|
+
return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
|
|
37247
|
+
}
|
|
37248
|
+
async function reconcileBackfill(requestScan2) {
|
|
37249
|
+
const deviceId = state.deviceId;
|
|
37250
|
+
if (!deviceId) return null;
|
|
37251
|
+
const jobs = await discoverJobs(deviceId);
|
|
37252
|
+
const present = new Set(jobs.map((j) => j.path));
|
|
37253
|
+
const cache3 = runtimeState.getReconcileCache();
|
|
37254
|
+
let filesParsed = 0;
|
|
37255
|
+
for (const job of jobs) {
|
|
37256
|
+
const mtime = (await stat3(job.path).catch(() => null))?.mtimeMs ?? 0;
|
|
37257
|
+
const hit = cache3[job.path];
|
|
37258
|
+
if (hit && hit.mtime === mtime) continue;
|
|
37259
|
+
const perDaySession = {};
|
|
37260
|
+
try {
|
|
37261
|
+
await job.parse(async (chunk) => {
|
|
37262
|
+
for (const e of chunk) {
|
|
37263
|
+
const bySession = perDaySession[utcDay(e.ts)] ??= {};
|
|
37264
|
+
bySession[e.session_id] = (bySession[e.session_id] ?? 0) + 1;
|
|
37265
|
+
}
|
|
37266
|
+
});
|
|
37267
|
+
cache3[job.path] = { mtime, perDaySession };
|
|
37268
|
+
filesParsed += 1;
|
|
37269
|
+
} catch (e) {
|
|
37270
|
+
console.warn(` ! reconcile parse failed for ${job.path}:`, e.message);
|
|
37271
|
+
}
|
|
37272
|
+
}
|
|
37273
|
+
for (const p of Object.keys(cache3)) if (!present.has(p)) delete cache3[p];
|
|
37274
|
+
runtimeState.setReconcileCache(cache3);
|
|
37275
|
+
runtimeState.pruneCursors(present);
|
|
37276
|
+
const localDay = /* @__PURE__ */ new Map();
|
|
37277
|
+
const localDaySession = /* @__PURE__ */ new Map();
|
|
37278
|
+
const filesOf = /* @__PURE__ */ new Map();
|
|
37279
|
+
let localEvents = 0;
|
|
37280
|
+
for (const [path, entry] of Object.entries(cache3)) {
|
|
37281
|
+
for (const [day, sessions] of Object.entries(entry.perDaySession)) {
|
|
37282
|
+
let ds = localDaySession.get(day);
|
|
37283
|
+
if (!ds) {
|
|
37284
|
+
ds = /* @__PURE__ */ new Map();
|
|
37285
|
+
localDaySession.set(day, ds);
|
|
37286
|
+
}
|
|
37287
|
+
for (const [sid, n] of Object.entries(sessions)) {
|
|
37288
|
+
localEvents += n;
|
|
37289
|
+
localDay.set(day, (localDay.get(day) ?? 0) + n);
|
|
37290
|
+
ds.set(sid, (ds.get(sid) ?? 0) + n);
|
|
37291
|
+
const key = `${day}\0${sid}`;
|
|
37292
|
+
let fs2 = filesOf.get(key);
|
|
37293
|
+
if (!fs2) {
|
|
37294
|
+
fs2 = /* @__PURE__ */ new Set();
|
|
37295
|
+
filesOf.set(key, fs2);
|
|
37296
|
+
}
|
|
37297
|
+
fs2.add(path);
|
|
37298
|
+
}
|
|
37299
|
+
}
|
|
37300
|
+
}
|
|
37301
|
+
const serverDays = await fetchBackfillDays();
|
|
37302
|
+
if (!serverDays) return null;
|
|
37303
|
+
const base = {
|
|
37304
|
+
inSync: true,
|
|
37305
|
+
localEvents,
|
|
37306
|
+
serverEvents: serverDays.total_events,
|
|
37307
|
+
filesParsed,
|
|
37308
|
+
daysChecked: 0,
|
|
37309
|
+
sessionsShort: 0,
|
|
37310
|
+
filesInvalidated: 0
|
|
37311
|
+
};
|
|
37312
|
+
if (localEvents <= serverDays.total_events) return base;
|
|
37313
|
+
const serverDayMap = new Map(serverDays.days.map((d) => [d.day, d.events]));
|
|
37314
|
+
const filesToReship = /* @__PURE__ */ new Set();
|
|
37315
|
+
let sessionsShort = 0;
|
|
37316
|
+
let daysChecked = 0;
|
|
37317
|
+
for (const [day, localCount] of localDay) {
|
|
37318
|
+
if (localCount <= (serverDayMap.get(day) ?? 0)) continue;
|
|
37319
|
+
daysChecked += 1;
|
|
37320
|
+
const serverSessions = await fetchBackfillDaySessions(day);
|
|
37321
|
+
const have = new Map((serverSessions?.sessions ?? []).map((s) => [s.session_id, s.events]));
|
|
37322
|
+
for (const [sid, n] of localDaySession.get(day) ?? []) {
|
|
37323
|
+
if (n > (have.get(sid) ?? 0)) {
|
|
37324
|
+
sessionsShort += 1;
|
|
37325
|
+
for (const f of filesOf.get(`${day}\0${sid}`) ?? []) filesToReship.add(f);
|
|
37326
|
+
}
|
|
37327
|
+
}
|
|
37328
|
+
}
|
|
37329
|
+
if (filesToReship.size === 0) return { ...base, daysChecked };
|
|
37330
|
+
for (const f of filesToReship) runtimeState.clearCursor(f);
|
|
37331
|
+
console.log(
|
|
37332
|
+
`[modelstat] self-heal: server short ${sessionsShort} session(s) across ${daysChecked} day(s); re-shipping ${filesToReship.size} file(s) from local logs`
|
|
37333
|
+
);
|
|
37334
|
+
await requestScan2("self-heal");
|
|
37335
|
+
return {
|
|
37336
|
+
...base,
|
|
37337
|
+
inSync: false,
|
|
37338
|
+
daysChecked,
|
|
37339
|
+
sessionsShort,
|
|
37340
|
+
filesInvalidated: filesToReship.size
|
|
37341
|
+
};
|
|
37342
|
+
}
|
|
37343
|
+
var init_reconcile = __esm({
|
|
37344
|
+
"src/reconcile.ts"() {
|
|
37345
|
+
"use strict";
|
|
37346
|
+
init_api();
|
|
37347
|
+
init_config2();
|
|
37348
|
+
init_runtime_state();
|
|
37349
|
+
init_scan();
|
|
37350
|
+
}
|
|
37351
|
+
});
|
|
37352
|
+
|
|
37185
37353
|
// src/single-flight.ts
|
|
37186
37354
|
function createCoalescingRunner(task) {
|
|
37187
37355
|
let running = false;
|
|
@@ -37491,7 +37659,7 @@ var init_policies2 = __esm({
|
|
|
37491
37659
|
});
|
|
37492
37660
|
|
|
37493
37661
|
// ../../node_modules/.pnpm/readdirp@4.1.2/node_modules/readdirp/esm/index.js
|
|
37494
|
-
import { stat as
|
|
37662
|
+
import { stat as stat4, lstat, readdir as readdir2, realpath } from "fs/promises";
|
|
37495
37663
|
import { Readable } from "stream";
|
|
37496
37664
|
import { resolve as presolve, relative as prelative, join as pjoin, sep as psep } from "path";
|
|
37497
37665
|
function readdirp(root, options = {}) {
|
|
@@ -37578,7 +37746,7 @@ var init_esm = __esm({
|
|
|
37578
37746
|
const { root, type } = opts;
|
|
37579
37747
|
this._fileFilter = normalizeFilter(opts.fileFilter);
|
|
37580
37748
|
this._directoryFilter = normalizeFilter(opts.directoryFilter);
|
|
37581
|
-
const statMethod = opts.lstat ? lstat :
|
|
37749
|
+
const statMethod = opts.lstat ? lstat : stat4;
|
|
37582
37750
|
if (wantBigintFsStats) {
|
|
37583
37751
|
this._stat = (path) => statMethod(path, { bigint: true });
|
|
37584
37752
|
} else {
|
|
@@ -37717,7 +37885,7 @@ var init_esm = __esm({
|
|
|
37717
37885
|
|
|
37718
37886
|
// ../../node_modules/.pnpm/chokidar@4.0.3/node_modules/chokidar/esm/handler.js
|
|
37719
37887
|
import { watchFile, unwatchFile, watch as fs_watch } from "fs";
|
|
37720
|
-
import { open, stat as
|
|
37888
|
+
import { open, stat as stat5, lstat as lstat2, realpath as fsrealpath } from "fs/promises";
|
|
37721
37889
|
import * as sysPath from "path";
|
|
37722
37890
|
import { type as osType } from "os";
|
|
37723
37891
|
function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
|
|
@@ -37765,7 +37933,7 @@ var init_handler = __esm({
|
|
|
37765
37933
|
};
|
|
37766
37934
|
EV = EVENTS;
|
|
37767
37935
|
THROTTLE_MODE_WATCH = "watch";
|
|
37768
|
-
statMethods = { lstat: lstat2, stat:
|
|
37936
|
+
statMethods = { lstat: lstat2, stat: stat5 };
|
|
37769
37937
|
KEY_LISTENERS = "listeners";
|
|
37770
37938
|
KEY_ERR = "errHandlers";
|
|
37771
37939
|
KEY_RAW = "rawEmitters";
|
|
@@ -38234,7 +38402,7 @@ var init_handler = __esm({
|
|
|
38234
38402
|
return;
|
|
38235
38403
|
if (!newStats || newStats.mtimeMs === 0) {
|
|
38236
38404
|
try {
|
|
38237
|
-
const newStats2 = await
|
|
38405
|
+
const newStats2 = await stat5(file);
|
|
38238
38406
|
if (this.fsw.closed)
|
|
38239
38407
|
return;
|
|
38240
38408
|
const at = newStats2.atimeMs;
|
|
@@ -38486,7 +38654,7 @@ __export(esm_exports, {
|
|
|
38486
38654
|
watch: () => watch
|
|
38487
38655
|
});
|
|
38488
38656
|
import { stat as statcb } from "fs";
|
|
38489
|
-
import { stat as
|
|
38657
|
+
import { stat as stat6, readdir as readdir3 } from "fs/promises";
|
|
38490
38658
|
import { EventEmitter } from "events";
|
|
38491
38659
|
import * as sysPath2 from "path";
|
|
38492
38660
|
function arrify(item) {
|
|
@@ -38957,7 +39125,7 @@ var init_esm2 = __esm({
|
|
|
38957
39125
|
const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path) : path;
|
|
38958
39126
|
let stats2;
|
|
38959
39127
|
try {
|
|
38960
|
-
stats2 = await
|
|
39128
|
+
stats2 = await stat6(fullPath);
|
|
38961
39129
|
} catch (err) {
|
|
38962
39130
|
}
|
|
38963
39131
|
if (!stats2 || this.closed)
|
|
@@ -39084,8 +39252,8 @@ var init_esm2 = __esm({
|
|
|
39084
39252
|
}
|
|
39085
39253
|
return this._userIgnored(path, stats);
|
|
39086
39254
|
}
|
|
39087
|
-
_isntIgnored(path,
|
|
39088
|
-
return !this._isIgnored(path,
|
|
39255
|
+
_isntIgnored(path, stat7) {
|
|
39256
|
+
return !this._isIgnored(path, stat7);
|
|
39089
39257
|
}
|
|
39090
39258
|
/**
|
|
39091
39259
|
* Provides a set of common helpers and properties relating to symlink handling.
|
|
@@ -39300,12 +39468,12 @@ async function sendHeartbeat() {
|
|
|
39300
39468
|
async function rotateRunawayLogs() {
|
|
39301
39469
|
const { homedir: homedir9 } = await import("os");
|
|
39302
39470
|
const { join: join14 } = await import("path");
|
|
39303
|
-
const { open: open2, stat:
|
|
39471
|
+
const { open: open2, stat: stat7, truncate, writeFile: writeFile2 } = await import("fs/promises");
|
|
39304
39472
|
const dir = join14(homedir9(), ".modelstat", "logs");
|
|
39305
39473
|
for (const name of ["out.log", "err.log"]) {
|
|
39306
39474
|
const p = join14(dir, name);
|
|
39307
39475
|
try {
|
|
39308
|
-
const st = await
|
|
39476
|
+
const st = await stat7(p);
|
|
39309
39477
|
if (st.size <= LOG_MAX_BYTES) continue;
|
|
39310
39478
|
const keep = Math.min(LOG_TAIL_KEEP_BYTES, st.size);
|
|
39311
39479
|
const fh = await open2(p, "r");
|
|
@@ -39510,6 +39678,13 @@ async function runDaemon(opts = {}) {
|
|
|
39510
39678
|
backstop.unref();
|
|
39511
39679
|
const discoveryTimer = setInterval(() => void runDiscovery(), DISCOVERY_INTERVAL_MS);
|
|
39512
39680
|
discoveryTimer.unref();
|
|
39681
|
+
const RECONCILE_INTERVAL_MS = 30 * 6e4;
|
|
39682
|
+
const reconcileTimer = setInterval(
|
|
39683
|
+
() => void reconcileBackfill(requestScan),
|
|
39684
|
+
RECONCILE_INTERVAL_MS
|
|
39685
|
+
);
|
|
39686
|
+
reconcileTimer.unref();
|
|
39687
|
+
setTimeout(() => void reconcileBackfill(requestScan), 6e4).unref();
|
|
39513
39688
|
const shutdown = async () => {
|
|
39514
39689
|
setPhase("offline", "Shutting down");
|
|
39515
39690
|
await sendHeartbeat();
|
|
@@ -39537,9 +39712,10 @@ var init_daemon = __esm({
|
|
|
39537
39712
|
init_config2();
|
|
39538
39713
|
init_lock();
|
|
39539
39714
|
init_machine_key();
|
|
39715
|
+
init_reconcile();
|
|
39540
39716
|
init_scan();
|
|
39541
39717
|
init_single_flight();
|
|
39542
|
-
DAEMON_VERSION2 = true ? "daemon-0.
|
|
39718
|
+
DAEMON_VERSION2 = true ? "daemon-0.5.0" : "daemon-dev";
|
|
39543
39719
|
HEARTBEAT_INTERVAL_MS = 1e4;
|
|
39544
39720
|
SCAN_INTERVAL_MS = 5 * 60 * 1e3;
|
|
39545
39721
|
DISCOVERY_INTERVAL_MS = 6e4;
|
|
@@ -40140,7 +40316,7 @@ function tryOpenBrowser(url) {
|
|
|
40140
40316
|
return false;
|
|
40141
40317
|
}
|
|
40142
40318
|
}
|
|
40143
|
-
var DAEMON_VERSION3 = true ? "daemon-0.
|
|
40319
|
+
var DAEMON_VERSION3 = true ? "daemon-0.5.0" : "daemon-dev";
|
|
40144
40320
|
function osFamily() {
|
|
40145
40321
|
const p = platform5();
|
|
40146
40322
|
if (p === "darwin") return "macos";
|