modelstat 0.0.6 → 0.0.10

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 CHANGED
@@ -67,6 +67,12 @@ var init_enums = __esm({
67
67
  "raw_sdk_anthropic",
68
68
  "raw_sdk_openai",
69
69
  "raw_sdk_google",
70
+ // Web chat UIs (Chrome-extension companion). Categorically distinct
71
+ // from *_cli / *_desktop tools — same provider, different surface.
72
+ "chatgpt_web",
73
+ "claude_web",
74
+ "gemini_web",
75
+ "grok_web",
70
76
  "unknown"
71
77
  ];
72
78
  PROVIDERS = [
@@ -117,6 +123,7 @@ var init_enums = __esm({
117
123
  "snap",
118
124
  "manual",
119
125
  "docker",
126
+ "chrome_extension",
120
127
  "unknown"
121
128
  ];
122
129
  OS_FAMILIES = ["macos", "linux", "windows", "other"];
@@ -4258,7 +4265,7 @@ var init_zod = __esm({
4258
4265
  });
4259
4266
 
4260
4267
  // ../../packages/core/src/schemas.ts
4261
- var TokenUsage, TaxonomyHint, SessionSegmentSummary, SessionSummary, GitContext, RawEvent, IngestBatch, DeviceEnrollment, DetectedInstallation, DetectedIdentity, DiscoveryReport, ClassificationConfidenceEnum;
4268
+ var TokenUsage, TaxonomyHint, SessionSegmentSummary, SessionSummary, GitContext, RawEvent, IngestBatch, DeviceEnrollment, DeviceSelfRegister, DeviceClaimRequest, ProcessingMetadata, RedactionPolicy, DetectedInstallation, DetectedIdentity, DiscoveryReport, ClassificationConfidenceEnum;
4262
4269
  var init_schemas = __esm({
4263
4270
  "../../packages/core/src/schemas.ts"() {
4264
4271
  "use strict";
@@ -4375,6 +4382,51 @@ var init_schemas = __esm({
4375
4382
  arch: external_exports.enum(["x86_64", "arm64", "other"]),
4376
4383
  agent_version: external_exports.string().max(40)
4377
4384
  });
4385
+ DeviceSelfRegister = external_exports.object({
4386
+ /** Agent-generated UUIDv7 — must pass shape + recent-timestamp checks. */
4387
+ device_uuid: external_exports.string(),
4388
+ /** Base64-encoded ed25519 public key, exactly 32 raw bytes. Optional
4389
+ * but recommended (used for sender-constrained tokens / DPoP later). */
4390
+ public_key: external_exports.string().optional(),
4391
+ /** Free-form snapshot shown to the human on the claim page so they
4392
+ * can sanity-check what they're claiming. */
4393
+ fingerprint: external_exports.object({
4394
+ hostname: external_exports.string().max(120).optional(),
4395
+ os: external_exports.string().max(60).optional(),
4396
+ os_family: external_exports.enum(OS_FAMILIES).optional(),
4397
+ os_version: external_exports.string().max(60).optional(),
4398
+ arch: external_exports.enum(["x86_64", "arm64", "other"]).optional(),
4399
+ agent: external_exports.string().max(80).optional(),
4400
+ agent_version: external_exports.string().max(40).optional()
4401
+ // Allow extra fields for forward-compat without breaking old agents.
4402
+ }).catchall(external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()])).default({})
4403
+ });
4404
+ DeviceClaimRequest = external_exports.object({
4405
+ claim_code: external_exports.string().max(40),
4406
+ /** Optional org_id if the user belongs to multiple orgs and wants to
4407
+ * claim into a specific one. Defaults to personal org. */
4408
+ org_id: external_exports.string().uuid().optional()
4409
+ });
4410
+ ProcessingMetadata = external_exports.object({
4411
+ redacted_by: external_exports.string().max(120).optional(),
4412
+ redaction_policy: external_exports.string().max(80).optional(),
4413
+ redaction_policy_version: external_exports.string().max(20).optional(),
4414
+ redactions_applied: external_exports.number().int().min(0).optional(),
4415
+ compacted: external_exports.boolean().optional(),
4416
+ summarized: external_exports.boolean().optional(),
4417
+ bytes_saved: external_exports.number().int().min(0).optional(),
4418
+ changes_applied: external_exports.number().int().min(0).optional(),
4419
+ original_size_bytes: external_exports.number().int().min(0).optional(),
4420
+ uploaded_size_bytes: external_exports.number().int().min(0).optional()
4421
+ });
4422
+ RedactionPolicy = external_exports.object({
4423
+ name: external_exports.string().max(60),
4424
+ version: external_exports.string().max(20),
4425
+ description: external_exports.string().max(400),
4426
+ redacts: external_exports.array(external_exports.string()).max(40),
4427
+ is_default: external_exports.boolean().default(false),
4428
+ recommended_for: external_exports.string().max(160).optional()
4429
+ });
4378
4430
  DetectedInstallation = external_exports.object({
4379
4431
  tool: external_exports.enum(TOOLS),
4380
4432
  install_method: external_exports.enum(INSTALL_METHODS),
@@ -4424,6 +4476,17 @@ var init_redact = __esm({
4424
4476
  }
4425
4477
  });
4426
4478
 
4479
+ // ../../packages/core/src/billing.ts
4480
+ var MILLION, FREE_INCLUDED_TOKENS, TEAM_INCLUDED_PER_SEAT;
4481
+ var init_billing = __esm({
4482
+ "../../packages/core/src/billing.ts"() {
4483
+ "use strict";
4484
+ MILLION = 1000000n;
4485
+ FREE_INCLUDED_TOKENS = 100n * MILLION;
4486
+ TEAM_INCLUDED_PER_SEAT = 250n * MILLION;
4487
+ }
4488
+ });
4489
+
4427
4490
  // ../../packages/core/src/index.ts
4428
4491
  var init_src = __esm({
4429
4492
  "../../packages/core/src/index.ts"() {
@@ -4432,6 +4495,7 @@ var init_src = __esm({
4432
4495
  init_schemas();
4433
4496
  init_ids();
4434
4497
  init_redact();
4498
+ init_billing();
4435
4499
  }
4436
4500
  });
4437
4501
 
@@ -5137,7 +5201,7 @@ function machineId() {
5137
5201
  function defaultUserEmail() {
5138
5202
  return process.env.AGENT_USER_EMAIL ?? state.userEmail ?? "aram@dev.local";
5139
5203
  }
5140
- var here, store, state;
5204
+ var here, DEFAULT_API_URL, LEGACY_LOCALHOST_API, store, state;
5141
5205
  var init_config = __esm({
5142
5206
  "src/config.ts"() {
5143
5207
  "use strict";
@@ -5150,20 +5214,35 @@ var init_config = __esm({
5150
5214
  }
5151
5215
  if (d === "/") break;
5152
5216
  }
5217
+ DEFAULT_API_URL = "https://modelstat.ai";
5218
+ LEGACY_LOCALHOST_API = "http://localhost:3010";
5153
5219
  store = new Conf({
5154
5220
  projectName: "modelstat-agent-dev",
5155
5221
  defaults: {
5156
- apiUrl: process.env.AGENT_API_URL ?? "http://localhost:3010",
5222
+ // Intentionally empty — the apiUrl getter below computes this
5223
+ // from env + stored value + DEFAULT_API_URL. Keeping the stored
5224
+ // default empty avoids freezing a stale value into the config
5225
+ // file the way 0.0.7 did.
5226
+ apiUrl: "",
5157
5227
  bearerToken: null,
5158
5228
  deviceId: null,
5229
+ deviceUuid: null,
5230
+ claimCode: null,
5231
+ claimUrl: null,
5159
5232
  userEmail: null,
5160
5233
  defaultOrgId: null,
5161
5234
  cursor: {}
5162
5235
  }
5163
5236
  });
5164
5237
  state = {
5238
+ /** Resolution order: env var → stored value (if user ran `setApiUrl`
5239
+ * or paired pre-0.0.8) → production default. The legacy localhost
5240
+ * value is ignored so upgrades from 0.0.7 self-heal. */
5165
5241
  get apiUrl() {
5166
- return store.get("apiUrl");
5242
+ if (process.env.AGENT_API_URL) return process.env.AGENT_API_URL;
5243
+ const stored = store.get("apiUrl");
5244
+ if (stored && stored !== LEGACY_LOCALHOST_API) return stored;
5245
+ return DEFAULT_API_URL;
5167
5246
  },
5168
5247
  setApiUrl(v) {
5169
5248
  store.set("apiUrl", v);
@@ -5186,6 +5265,24 @@ var init_config = __esm({
5186
5265
  setUserEmail(v) {
5187
5266
  store.set("userEmail", v);
5188
5267
  },
5268
+ get deviceUuid() {
5269
+ return store.get("deviceUuid");
5270
+ },
5271
+ setDeviceUuid(v) {
5272
+ store.set("deviceUuid", v);
5273
+ },
5274
+ get claimCode() {
5275
+ return store.get("claimCode");
5276
+ },
5277
+ setClaimCode(v) {
5278
+ store.set("claimCode", v);
5279
+ },
5280
+ get claimUrl() {
5281
+ return store.get("claimUrl");
5282
+ },
5283
+ setClaimUrl(v) {
5284
+ store.set("claimUrl", v);
5285
+ },
5189
5286
  getCursor(path) {
5190
5287
  return store.get("cursor")[path];
5191
5288
  },
@@ -5203,6 +5300,27 @@ var init_config = __esm({
5203
5300
 
5204
5301
  // src/api.ts
5205
5302
  import { request } from "undici";
5303
+ async function selfRegister(input) {
5304
+ const res = await request(`${state.apiUrl}/v1/devices/self-register`, {
5305
+ method: "POST",
5306
+ headers: { "content-type": "application/json" },
5307
+ body: JSON.stringify(input)
5308
+ });
5309
+ if (res.statusCode >= 300) {
5310
+ throw new Error(`self-register failed: ${res.statusCode} ${await res.body.text()}`);
5311
+ }
5312
+ return await res.body.json();
5313
+ }
5314
+ async function fetchDeviceMe(secret) {
5315
+ const res = await request(`${state.apiUrl}/v1/devices/me`, {
5316
+ method: "GET",
5317
+ headers: { authorization: `Bearer ${secret}` }
5318
+ });
5319
+ if (res.statusCode >= 300) {
5320
+ throw new Error(`devices/me failed: ${res.statusCode} ${await res.body.text()}`);
5321
+ }
5322
+ return await res.body.json();
5323
+ }
5206
5324
  async function startDeviceCode() {
5207
5325
  const res = await request(`${state.apiUrl}/v1/auth/device_code`, {
5208
5326
  method: "POST",
@@ -5512,7 +5630,7 @@ __export(daemon_exports, {
5512
5630
  setProgress: () => setProgress,
5513
5631
  setQueue: () => setQueue
5514
5632
  });
5515
- import { existsSync as existsSync4, statSync as statSync2 } from "fs";
5633
+ import { existsSync as existsSync5, statSync as statSync2 } from "fs";
5516
5634
  import { request as request2 } from "undici";
5517
5635
  function setPhase(phase, message) {
5518
5636
  status.phase = phase;
@@ -5616,21 +5734,21 @@ async function runDaemon() {
5616
5734
  await runDiscovery();
5617
5735
  await runScanCycle("startup");
5618
5736
  const chokidar2 = (await import("chokidar")).default;
5619
- const { homedir: homedir4, platform: platform4 } = await import("os");
5620
- const { join: join4 } = await import("path");
5621
- const home = homedir4();
5737
+ const { homedir: homedir5, platform: platform5 } = await import("os");
5738
+ const { join: join5 } = await import("path");
5739
+ const home2 = homedir5();
5622
5740
  const dirs = [
5623
- join4(home, ".claude/projects"),
5624
- join4(home, ".codex/sessions"),
5625
- join4(home, ".cursor/ai-tracking"),
5626
- join4(home, ".gemini"),
5627
- ...platform4() === "darwin" ? [
5628
- join4(home, "Library/Application Support/Cursor/User/workspaceStorage"),
5629
- join4(home, "Library/Application Support/Claude")
5741
+ join5(home2, ".claude/projects"),
5742
+ join5(home2, ".codex/sessions"),
5743
+ join5(home2, ".cursor/ai-tracking"),
5744
+ join5(home2, ".gemini"),
5745
+ ...platform5() === "darwin" ? [
5746
+ join5(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
5747
+ join5(home2, "Library/Application Support/Claude")
5630
5748
  ] : [
5631
- join4(home, ".config/Cursor/User/workspaceStorage")
5749
+ join5(home2, ".config/Cursor/User/workspaceStorage")
5632
5750
  ]
5633
- ].filter((p) => existsSync4(p) && statSync2(p).isDirectory());
5751
+ ].filter((p) => existsSync5(p) && statSync2(p).isDirectory());
5634
5752
  setPhase("watching", `Watching ${dirs.length} directories`);
5635
5753
  const watcher = chokidar2.watch(dirs, {
5636
5754
  persistent: true,
@@ -5695,37 +5813,37 @@ __export(watch_exports, {
5695
5813
  watchForever: () => watchForever
5696
5814
  });
5697
5815
  import chokidar from "chokidar";
5698
- import { existsSync as existsSync5 } from "fs";
5699
- import { homedir as homedir3, platform as platform2 } from "os";
5700
- import { join as join3 } from "path";
5816
+ import { existsSync as existsSync6 } from "fs";
5817
+ import { homedir as homedir4, platform as platform3 } from "os";
5818
+ import { join as join4 } from "path";
5701
5819
  function resolveWatchDirs() {
5702
- const home = homedir3();
5703
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join3(home, ".config");
5704
- const xdgData = process.env.XDG_DATA_HOME ?? join3(home, ".local/share");
5820
+ const home2 = homedir4();
5821
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join4(home2, ".config");
5822
+ const xdgData = process.env.XDG_DATA_HOME ?? join4(home2, ".local/share");
5705
5823
  const candidates = [
5706
5824
  // universal (default HOME-rooted CLI data dirs)
5707
- join3(home, ".claude/projects"),
5708
- join3(home, ".codex/sessions"),
5709
- join3(home, ".cursor/ai-tracking"),
5710
- join3(home, ".gemini"),
5711
- join3(home, ".aider"),
5825
+ join4(home2, ".claude/projects"),
5826
+ join4(home2, ".codex/sessions"),
5827
+ join4(home2, ".cursor/ai-tracking"),
5828
+ join4(home2, ".gemini"),
5829
+ join4(home2, ".aider"),
5712
5830
  // XDG / Linux
5713
- join3(xdgConfig, "claude/projects"),
5714
- join3(xdgConfig, "codex/sessions"),
5715
- join3(xdgConfig, "Cursor/User/workspaceStorage"),
5716
- join3(xdgConfig, "Code/User/workspaceStorage"),
5717
- join3(xdgConfig, "Code - Insiders/User/workspaceStorage"),
5718
- join3(xdgData, "claude/projects"),
5831
+ join4(xdgConfig, "claude/projects"),
5832
+ join4(xdgConfig, "codex/sessions"),
5833
+ join4(xdgConfig, "Cursor/User/workspaceStorage"),
5834
+ join4(xdgConfig, "Code/User/workspaceStorage"),
5835
+ join4(xdgConfig, "Code - Insiders/User/workspaceStorage"),
5836
+ join4(xdgData, "claude/projects"),
5719
5837
  // macOS
5720
- ...platform2() === "darwin" ? [
5721
- join3(home, "Library/Application Support/Cursor/User/workspaceStorage"),
5722
- join3(home, "Library/Application Support/Claude"),
5723
- join3(home, "Library/Application Support/Code/User/workspaceStorage"),
5724
- join3(home, "Library/Application Support/Windsurf/User/workspaceStorage"),
5725
- join3(home, "Library/Application Support/Zed")
5838
+ ...platform3() === "darwin" ? [
5839
+ join4(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
5840
+ join4(home2, "Library/Application Support/Claude"),
5841
+ join4(home2, "Library/Application Support/Code/User/workspaceStorage"),
5842
+ join4(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
5843
+ join4(home2, "Library/Application Support/Zed")
5726
5844
  ] : []
5727
5845
  ];
5728
- return Array.from(new Set(candidates)).filter((p) => existsSync5(p));
5846
+ return Array.from(new Set(candidates)).filter((p) => existsSync6(p));
5729
5847
  }
5730
5848
  async function safeScan(reason) {
5731
5849
  if (scanning) {
@@ -5788,10 +5906,225 @@ init_api();
5788
5906
  init_config();
5789
5907
  init_scan();
5790
5908
  import { spawn } from "child_process";
5791
- import { platform as platform3, release, arch as cpuArch, hostname as hostname2 } from "os";
5909
+ import { randomBytes } from "crypto";
5910
+ import { platform as platform4, release, arch as cpuArch, hostname as hostname2 } from "os";
5792
5911
  import { setTimeout as delay } from "timers/promises";
5912
+
5913
+ // src/service.ts
5914
+ import { spawnSync } from "child_process";
5915
+ import {
5916
+ copyFileSync,
5917
+ existsSync as existsSync4,
5918
+ mkdirSync,
5919
+ unlinkSync,
5920
+ writeFileSync
5921
+ } from "fs";
5922
+ import { homedir as homedir3, platform as platform2, userInfo } from "os";
5923
+ import { dirname as dirname3, join as join3 } from "path";
5924
+ import { fileURLToPath as fileURLToPath2 } from "url";
5925
+ var SERVICE_LABEL = "ai.modelstat.agent";
5926
+ var SYSTEMD_UNIT = "modelstat";
5927
+ function home() {
5928
+ return homedir3();
5929
+ }
5930
+ function stateDir() {
5931
+ return join3(home(), ".modelstat");
5932
+ }
5933
+ function binDir() {
5934
+ return join3(stateDir(), "bin");
5935
+ }
5936
+ function logDir() {
5937
+ return join3(stateDir(), "logs");
5938
+ }
5939
+ function installedCliPath() {
5940
+ return join3(binDir(), "modelstat.mjs");
5941
+ }
5942
+ function runningCliPath() {
5943
+ return fileURLToPath2(import.meta.url).replace(/service\.(mjs|js|ts)$/, "cli.mjs");
5944
+ }
5945
+ function installBundle() {
5946
+ mkdirSync(binDir(), { recursive: true });
5947
+ mkdirSync(logDir(), { recursive: true });
5948
+ const src = runningCliPath();
5949
+ const dest = installedCliPath();
5950
+ if (!existsSync4(src)) {
5951
+ throw new Error(
5952
+ `Can't find the CLI bundle to install from (${src}). Are you running a local dev build?`
5953
+ );
5954
+ }
5955
+ copyFileSync(src, dest);
5956
+ return dest;
5957
+ }
5958
+ function nodeBinary() {
5959
+ return process.execPath;
5960
+ }
5961
+ function plistPath() {
5962
+ return join3(home(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
5963
+ }
5964
+ function writePlist(cliPath) {
5965
+ const p = plistPath();
5966
+ mkdirSync(dirname3(p), { recursive: true });
5967
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
5968
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
5969
+ <plist version="1.0">
5970
+ <dict>
5971
+ <key>Label</key><string>${SERVICE_LABEL}</string>
5972
+ <key>ProgramArguments</key>
5973
+ <array>
5974
+ <string>${nodeBinary()}</string>
5975
+ <string>${cliPath}</string>
5976
+ <string>start</string>
5977
+ </array>
5978
+ <key>RunAtLoad</key><true/>
5979
+ <key>KeepAlive</key>
5980
+ <dict><key>SuccessfulExit</key><false/></dict>
5981
+ <key>ThrottleInterval</key><integer>30</integer>
5982
+ <key>StandardOutPath</key><string>${join3(logDir(), "out.log")}</string>
5983
+ <key>StandardErrorPath</key><string>${join3(logDir(), "err.log")}</string>
5984
+ <key>EnvironmentVariables</key>
5985
+ <dict>
5986
+ <key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
5987
+ </dict>
5988
+ <key>WorkingDirectory</key><string>${home()}</string>
5989
+ </dict>
5990
+ </plist>
5991
+ `;
5992
+ writeFileSync(p, plist, { mode: 420 });
5993
+ return p;
5994
+ }
5995
+ function launchctl(args) {
5996
+ const r = spawnSync("launchctl", args, { encoding: "utf8" });
5997
+ return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "" };
5998
+ }
5999
+ function macInstall() {
6000
+ const cliPath = installBundle();
6001
+ const plist = writePlist(cliPath);
6002
+ const uid = userInfo().uid;
6003
+ const target = `gui/${uid}/${SERVICE_LABEL}`;
6004
+ launchctl(["bootout", target]);
6005
+ const boot = launchctl(["bootstrap", `gui/${uid}`, plist]);
6006
+ if (!boot.ok && !/already loaded|service already bootstrapped/i.test(boot.err)) {
6007
+ const load = launchctl(["load", "-w", plist]);
6008
+ if (!load.ok) {
6009
+ throw new Error(
6010
+ `launchctl load failed:
6011
+ bootstrap: ${boot.err.trim()}
6012
+ load: ${load.err.trim()}`
6013
+ );
6014
+ }
6015
+ }
6016
+ launchctl(["kickstart", "-k", target]);
6017
+ }
6018
+ function macUninstall() {
6019
+ const uid = userInfo().uid;
6020
+ const target = `gui/${uid}/${SERVICE_LABEL}`;
6021
+ launchctl(["bootout", target]);
6022
+ const plist = plistPath();
6023
+ if (existsSync4(plist)) {
6024
+ try {
6025
+ unlinkSync(plist);
6026
+ } catch {
6027
+ }
6028
+ }
6029
+ }
6030
+ function macStatus() {
6031
+ const uid = userInfo().uid;
6032
+ const r = launchctl(["print", `gui/${uid}/${SERVICE_LABEL}`]);
6033
+ return { running: r.ok, hint: r.ok ? "launchd managed" : "not installed" };
6034
+ }
6035
+ function systemdUnitPath() {
6036
+ const xdg = process.env.XDG_CONFIG_HOME ?? join3(home(), ".config");
6037
+ return join3(xdg, "systemd", "user", `${SYSTEMD_UNIT}.service`);
6038
+ }
6039
+ function writeSystemdUnit(cliPath) {
6040
+ const unitPath = systemdUnitPath();
6041
+ mkdirSync(dirname3(unitPath), { recursive: true });
6042
+ const unit = `[Unit]
6043
+ Description=modelstat agent
6044
+ Documentation=https://modelstat.ai
6045
+ After=network-online.target
6046
+ Wants=network-online.target
6047
+
6048
+ [Service]
6049
+ Type=simple
6050
+ ExecStart=${nodeBinary()} ${cliPath} start
6051
+ Restart=always
6052
+ RestartSec=10
6053
+ # Don't restart-storm if the backend is persistently unreachable.
6054
+ StartLimitIntervalSec=300
6055
+ StartLimitBurst=10
6056
+ StandardOutput=append:${join3(logDir(), "out.log")}
6057
+ StandardError=append:${join3(logDir(), "err.log")}
6058
+
6059
+ [Install]
6060
+ WantedBy=default.target
6061
+ `;
6062
+ writeFileSync(unitPath, unit, { mode: 420 });
6063
+ return unitPath;
6064
+ }
6065
+ function systemctl(args) {
6066
+ const r = spawnSync("systemctl", ["--user", ...args], { encoding: "utf8" });
6067
+ return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "" };
6068
+ }
6069
+ function linuxInstall() {
6070
+ const cliPath = installBundle();
6071
+ writeSystemdUnit(cliPath);
6072
+ systemctl(["daemon-reload"]);
6073
+ const en = systemctl(["enable", "--now", `${SYSTEMD_UNIT}.service`]);
6074
+ if (!en.ok) {
6075
+ throw new Error(`systemctl enable failed: ${en.err.trim()}`);
6076
+ }
6077
+ systemctl(["restart", `${SYSTEMD_UNIT}.service`]);
6078
+ }
6079
+ function linuxUninstall() {
6080
+ systemctl(["disable", "--now", `${SYSTEMD_UNIT}.service`]);
6081
+ const unit = systemdUnitPath();
6082
+ if (existsSync4(unit)) {
6083
+ try {
6084
+ unlinkSync(unit);
6085
+ } catch {
6086
+ }
6087
+ }
6088
+ systemctl(["daemon-reload"]);
6089
+ }
6090
+ function linuxStatus() {
6091
+ const r = systemctl(["is-active", `${SYSTEMD_UNIT}.service`]);
6092
+ const active = r.out.trim() === "active";
6093
+ return { running: active, hint: active ? "systemd managed" : "not running" };
6094
+ }
6095
+ function installService() {
6096
+ const p = platform2();
6097
+ if (p === "darwin") {
6098
+ macInstall();
6099
+ return { path: plistPath(), logs: logDir() };
6100
+ }
6101
+ if (p === "linux") {
6102
+ linuxInstall();
6103
+ return { path: systemdUnitPath(), logs: logDir() };
6104
+ }
6105
+ throw new Error(
6106
+ `Service installation isn't supported on ${p}. Run 'modelstat start' manually to keep the agent running.`
6107
+ );
6108
+ }
6109
+ function uninstallService() {
6110
+ const p = platform2();
6111
+ if (p === "darwin") return macUninstall();
6112
+ if (p === "linux") return linuxUninstall();
6113
+ throw new Error(`Service uninstall isn't supported on ${p}.`);
6114
+ }
6115
+ function serviceStatus() {
6116
+ const p = platform2();
6117
+ if (p === "darwin") return macStatus();
6118
+ if (p === "linux") return linuxStatus();
6119
+ return { running: false, hint: `unsupported platform (${p})` };
6120
+ }
6121
+ function logsDir() {
6122
+ return logDir();
6123
+ }
6124
+
6125
+ // src/cli.ts
5793
6126
  function tryOpenBrowser(url) {
5794
- const p = platform3();
6127
+ const p = platform4();
5795
6128
  const cmd = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
5796
6129
  const args = p === "win32" ? ["/c", "start", "", url] : [url];
5797
6130
  try {
@@ -5807,7 +6140,7 @@ function tryOpenBrowser(url) {
5807
6140
  }
5808
6141
  var AGENT_VERSION3 = "agent-dev-0.0.1";
5809
6142
  function osFamily() {
5810
- const p = platform3();
6143
+ const p = platform4();
5811
6144
  if (p === "darwin") return "macos";
5812
6145
  if (p === "linux") return "linux";
5813
6146
  return "other";
@@ -5818,6 +6151,78 @@ function osArch() {
5818
6151
  if (a === "arm64") return "arm64";
5819
6152
  return "other";
5820
6153
  }
6154
+ function generateUuidV7() {
6155
+ const ms = Date.now();
6156
+ const tsHex = ms.toString(16).padStart(12, "0");
6157
+ const rand = randomBytes(10);
6158
+ rand[0] = rand[0] & 15 | 112;
6159
+ rand[2] = rand[2] & 63 | 128;
6160
+ const hex = tsHex + rand.toString("hex");
6161
+ return [
6162
+ hex.slice(0, 8),
6163
+ hex.slice(8, 12),
6164
+ hex.slice(12, 16),
6165
+ hex.slice(16, 20),
6166
+ hex.slice(20, 32)
6167
+ ].join("-");
6168
+ }
6169
+ async function cmdSelfRegister() {
6170
+ const deviceUuid = state.deviceUuid ?? generateUuidV7();
6171
+ const fingerprint = {
6172
+ hostname: hostname2(),
6173
+ os_family: osFamily(),
6174
+ os_version: release(),
6175
+ arch: osArch(),
6176
+ agent: "modelstat-agent-dev",
6177
+ agent_version: AGENT_VERSION3
6178
+ };
6179
+ const res = await selfRegister({
6180
+ device_uuid: deviceUuid,
6181
+ fingerprint
6182
+ });
6183
+ state.setDeviceUuid(res.device_uuid);
6184
+ state.setDeviceId(res.device_id);
6185
+ state.setBearer(res.device_secret);
6186
+ state.setClaimCode(res.claim_code);
6187
+ state.setClaimUrl(res.claim_url);
6188
+ console.log(`\u2713 self-registered`);
6189
+ console.log(` device_uuid: ${res.device_uuid}`);
6190
+ console.log(` device_id: ${res.device_id}`);
6191
+ console.log(` secret_prefix: ${res.secret_prefix}\u2026`);
6192
+ console.log();
6193
+ console.log(` Claim this device to attach it to your account:`);
6194
+ console.log(` ${res.claim_url}`);
6195
+ console.log(` code: ${res.claim_code}`);
6196
+ console.log();
6197
+ console.log(` state: ${state.storePath}`);
6198
+ }
6199
+ async function cmdAwaitClaim() {
6200
+ const secret = state.bearer;
6201
+ if (!secret) {
6202
+ console.error("not registered \u2014 run `modelstat self-register` first");
6203
+ process.exit(1);
6204
+ }
6205
+ const url = state.claimUrl ?? "(visit your dashboard)";
6206
+ console.log(`waiting for human to claim this device:
6207
+ ${url}
6208
+ `);
6209
+ while (true) {
6210
+ let me;
6211
+ try {
6212
+ me = await fetchDeviceMe(secret);
6213
+ } catch (e) {
6214
+ console.error(`poll failed: ${e.message}`);
6215
+ await delay(5e3);
6216
+ continue;
6217
+ }
6218
+ if (me.status === "claimed") {
6219
+ console.log(`\u2713 claimed by user_id=${me.user_id}`);
6220
+ return;
6221
+ }
6222
+ process.stdout.write(".");
6223
+ await delay(2e3);
6224
+ }
6225
+ }
5821
6226
  async function cmdRegister() {
5822
6227
  const email = defaultUserEmail();
5823
6228
  const res = await enrollDevice({
@@ -5829,6 +6234,9 @@ async function cmdRegister() {
5829
6234
  agent_version: AGENT_VERSION3,
5830
6235
  user_email: email
5831
6236
  });
6237
+ if (!res.bearer_token || !res.device_id) {
6238
+ throw new Error("enroll response missing bearer or device_id");
6239
+ }
5832
6240
  state.setBearer(res.bearer_token);
5833
6241
  state.setDeviceId(res.device_id);
5834
6242
  state.setUserEmail(email);
@@ -5838,28 +6246,103 @@ async function cmdRegister() {
5838
6246
  console.log(` bearer: ${res.bearer_token.slice(0, 16)}\u2026`);
5839
6247
  console.log(` state: ${state.storePath}`);
5840
6248
  }
5841
- async function cmdConnect() {
5842
- const init = await startDeviceCode();
5843
- const url = init.verification_url;
5844
- const opened = tryOpenBrowser(url);
5845
- const line = "\u2501".repeat(60);
5846
- console.log();
5847
- console.log(line);
5848
- console.log(
5849
- opened ? " Opening your browser to approve this device\u2026" : " Open this URL in any browser to approve this device:"
6249
+ var DASHBOARD_URL = "https://modelstat.ai/dashboard";
6250
+ function emitEvent(opts, event, fields = {}) {
6251
+ if (!opts.json) return;
6252
+ process.stdout.write(
6253
+ `${JSON.stringify({ v: 1, ts: Date.now(), event, ...fields })}
6254
+ `
5850
6255
  );
6256
+ }
6257
+ async function cmdConnect(opts) {
6258
+ if (!state.deviceUuid || !state.bearer || !state.deviceId) {
6259
+ await cmdSelfRegister();
6260
+ }
6261
+ const claimCode = state.claimCode ?? "(unknown)";
6262
+ const claimUrl = state.claimUrl ?? `https://modelstat.ai/d/${claimCode}`;
6263
+ const agentUrl = `https://modelstat.ai/da/${claimCode}`;
6264
+ emitEvent(opts, "registered", {
6265
+ device_uuid: state.deviceUuid,
6266
+ device_id: state.deviceId,
6267
+ claim_code: claimCode,
6268
+ claim_url: claimUrl,
6269
+ agent_url: agentUrl
6270
+ });
6271
+ let serviceOk = false;
6272
+ try {
6273
+ const svc = installService();
6274
+ serviceOk = true;
6275
+ emitEvent(opts, "service_installed", { path: svc.path, logs: svc.logs });
6276
+ } catch (e) {
6277
+ emitEvent(opts, "service_install_failed", { error: e.message });
6278
+ }
6279
+ if (!opts.json) {
6280
+ const line = "\u2501".repeat(60);
6281
+ console.log();
6282
+ console.log(line);
6283
+ console.log(` \u2713 Device registered \u2014 streaming your AI usage to modelstat.`);
6284
+ console.log();
6285
+ console.log(` Open your dashboard (no sign-up needed):`);
6286
+ console.log(` \x1B[1;36m${claimUrl}\x1B[0m`);
6287
+ console.log();
6288
+ console.log(` Agent-friendly (for LLMs / MCPs):`);
6289
+ console.log(` \x1B[2m${agentUrl}\x1B[0m`);
6290
+ console.log();
6291
+ console.log(` Claim this device so it keeps analyzing past 100M tokens/mo:`);
6292
+ console.log(` \x1B[2m${claimUrl}/claim\x1B[0m`);
6293
+ console.log(line);
6294
+ console.log();
6295
+ }
6296
+ if (!opts.noBrowser) {
6297
+ const opened = tryOpenBrowser(claimUrl);
6298
+ emitEvent(opts, "browser_open_attempted", { opened });
6299
+ }
6300
+ emitEvent(opts, "done", { claim_url: claimUrl, agent_url: agentUrl });
6301
+ if (serviceOk) {
6302
+ return;
6303
+ }
6304
+ if (opts.json) {
6305
+ return;
6306
+ }
6307
+ console.log(" Service install not supported on this platform \u2014 running in foreground.");
6308
+ console.log(" Press Ctrl-C to stop.");
5851
6309
  console.log();
5852
- console.log(` \x1B[1;36m${url}\x1B[0m`);
5853
- console.log();
5854
- console.log(" Pairing code (if you'd rather type it manually):");
5855
- console.log();
5856
- console.log(` \x1B[1;33m${init.user_code}\x1B[0m`);
5857
- console.log();
5858
- console.log(` Waiting for approval (expires in ${init.expires_in}s)`);
5859
- console.log(line);
5860
- console.log();
6310
+ const { runDaemon: runDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
6311
+ await runDaemon2();
6312
+ }
6313
+ async function cmdConnectOAuth(opts) {
6314
+ const init = await startDeviceCode();
6315
+ const url = init.verification_url;
6316
+ emitEvent(opts, "device_code_issued", {
6317
+ verification_url: url,
6318
+ user_code: init.user_code,
6319
+ expires_in: init.expires_in,
6320
+ interval: init.interval
6321
+ });
6322
+ const opened = opts.noBrowser ? false : tryOpenBrowser(url);
6323
+ if (!opts.noBrowser) {
6324
+ emitEvent(opts, "browser_open_attempted", { opened });
6325
+ }
6326
+ if (!opts.json) {
6327
+ const line = "\u2501".repeat(60);
6328
+ console.log();
6329
+ console.log(line);
6330
+ console.log(
6331
+ opened ? " Opening your browser to approve this device\u2026" : " Open this URL in any browser to approve this device:"
6332
+ );
6333
+ console.log();
6334
+ console.log(` \x1B[1;36m${url}\x1B[0m`);
6335
+ console.log();
6336
+ console.log(" Pairing code:");
6337
+ console.log(` \x1B[1;33m${init.user_code}\x1B[0m`);
6338
+ console.log();
6339
+ console.log(` Waiting for approval (expires in ${init.expires_in}s)`);
6340
+ console.log(line);
6341
+ console.log();
6342
+ }
5861
6343
  const deadline = Date.now() + init.expires_in * 1e3;
5862
6344
  let dots = 0;
6345
+ let lastPollEmit = 0;
5863
6346
  while (Date.now() < deadline) {
5864
6347
  await delay(init.interval * 1e3);
5865
6348
  try {
@@ -5867,8 +6350,11 @@ async function cmdConnect() {
5867
6350
  if (res.status === "approved") {
5868
6351
  state.setBearer(res.bearer_token);
5869
6352
  state.setUserEmail(`user:${res.user_id}`);
5870
- console.log();
5871
- console.log("\u2713 approved \u2014 bearer stored");
6353
+ emitEvent(opts, "approved", { user_id: res.user_id });
6354
+ if (!opts.json) {
6355
+ console.log();
6356
+ console.log("\u2713 approved \u2014 bearer stored");
6357
+ }
5872
6358
  let enroll = null;
5873
6359
  try {
5874
6360
  enroll = await enrollDevice({
@@ -5880,39 +6366,106 @@ async function cmdConnect() {
5880
6366
  agent_version: AGENT_VERSION3
5881
6367
  });
5882
6368
  } catch (e) {
5883
- console.error();
5884
- console.error(`\u2717 device enrollment failed: ${e.message}`);
5885
- console.error(
5886
- " The approval went through but the server couldn't register this"
5887
- );
5888
- console.error(
5889
- " machine. Check that your modelstat API is reachable at"
5890
- );
5891
- console.error(` ${state.apiUrl} \u2014 then run \`modelstat connect\` again.`);
6369
+ const message = e.message;
6370
+ emitEvent(opts, "error", { code: "enroll_failed", message });
6371
+ if (!opts.json) {
6372
+ console.error();
6373
+ console.error(`\u2717 device enrollment failed: ${message}`);
6374
+ console.error(
6375
+ " The approval went through but the server couldn't register this"
6376
+ );
6377
+ console.error(
6378
+ " machine. Check that your modelstat API is reachable at"
6379
+ );
6380
+ console.error(` ${state.apiUrl} \u2014 then run \`modelstat connect\` again.`);
6381
+ }
5892
6382
  process.exit(1);
5893
6383
  }
5894
6384
  if (!enroll?.device_id) {
5895
- console.error("\n\u2717 enrollment returned no device_id. Aborting.");
6385
+ emitEvent(opts, "error", {
6386
+ code: "enroll_failed",
6387
+ message: "enrollment returned no device_id"
6388
+ });
6389
+ if (!opts.json) {
6390
+ console.error("\n\u2717 enrollment returned no device_id. Aborting.");
6391
+ }
5896
6392
  process.exit(1);
5897
6393
  }
5898
6394
  state.setDeviceId(enroll.device_id);
5899
- console.log(`\u2713 device registered (${enroll.device_id.slice(0, 8)}\u2026)`);
5900
- console.log();
5901
- console.log("Starting daemon \u2014 the dashboard takes it from here.");
5902
- console.log("Press Ctrl-C to stop.");
5903
- console.log();
5904
- const { runDaemon: runDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
5905
- await runDaemon2();
5906
- return;
6395
+ emitEvent(opts, "device_enrolled", { device_id: enroll.device_id });
6396
+ if (!opts.json) {
6397
+ console.log(`\u2713 device registered (${enroll.device_id.slice(0, 8)}\u2026)`);
6398
+ }
6399
+ try {
6400
+ const svc = installService();
6401
+ emitEvent(opts, "service_installed", {
6402
+ path: svc.path,
6403
+ logs: svc.logs
6404
+ });
6405
+ if (!opts.json) {
6406
+ console.log(`\u2713 service installed (${svc.path})`);
6407
+ console.log(` logs: ${svc.logs}`);
6408
+ console.log();
6409
+ const banner = "\u2501".repeat(60);
6410
+ console.log(banner);
6411
+ console.log(" You're connected. The agent is now running in the");
6412
+ console.log(" background and will start on login.");
6413
+ console.log();
6414
+ console.log(" Manage it from the dashboard:");
6415
+ console.log(` \x1B[1;36m${DASHBOARD_URL}\x1B[0m`);
6416
+ console.log();
6417
+ console.log(" Stop it any time: \x1B[2mmodelstat stop\x1B[0m");
6418
+ console.log(" Uninstall: \x1B[2mmodelstat uninstall\x1B[0m");
6419
+ console.log(banner);
6420
+ }
6421
+ emitEvent(opts, "done", { dashboard_url: DASHBOARD_URL });
6422
+ return;
6423
+ } catch (err) {
6424
+ const message = err.message;
6425
+ emitEvent(opts, "service_install_failed", {
6426
+ error: message,
6427
+ fallback: opts.json ? "manual" : "foreground"
6428
+ });
6429
+ if (opts.json) {
6430
+ emitEvent(opts, "done", { dashboard_url: DASHBOARD_URL });
6431
+ return;
6432
+ }
6433
+ console.warn();
6434
+ console.warn(`\u26A0 couldn't install as a background service: ${message}`);
6435
+ console.warn(" Running in the foreground instead \u2014 press Ctrl-C to stop.");
6436
+ console.warn(" (Install globally with `npm i -g modelstat` then re-run `modelstat connect`.)");
6437
+ console.warn();
6438
+ const { runDaemon: runDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
6439
+ await runDaemon2();
6440
+ return;
6441
+ }
5907
6442
  }
5908
6443
  } catch (e) {
5909
- if (dots % 15 === 0) console.warn(`
6444
+ if (dots % 15 === 0 && !opts.json) {
6445
+ console.warn(`
5910
6446
  [retry] ${e.message}`);
6447
+ }
5911
6448
  }
5912
6449
  dots += 1;
5913
- process.stdout.write(".");
6450
+ if (opts.json) {
6451
+ const now = Date.now();
6452
+ if (now - lastPollEmit >= 1e4) {
6453
+ emitEvent(opts, "polling", {
6454
+ seconds_remaining: Math.max(0, Math.round((deadline - now) / 1e3))
6455
+ });
6456
+ lastPollEmit = now;
6457
+ }
6458
+ } else {
6459
+ process.stdout.write(".");
6460
+ }
6461
+ }
6462
+ emitEvent(opts, "error", {
6463
+ code: "device_code_expired",
6464
+ message: "device_code expired before approval"
6465
+ });
6466
+ if (!opts.json) {
6467
+ console.error("\ndevice_code expired before approval");
5914
6468
  }
5915
- console.error("\ndevice_code expired before approval");
5916
6469
  process.exit(1);
5917
6470
  }
5918
6471
  async function cmdDiscover() {
@@ -5947,33 +6500,109 @@ async function cmdStart() {
5947
6500
  const { runDaemon: runDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
5948
6501
  await runDaemon2();
5949
6502
  }
6503
+ async function cmdStop() {
6504
+ try {
6505
+ uninstallService();
6506
+ console.log("\u2713 service stopped and uninstalled");
6507
+ console.log(` Your device pairing is still in ${state.storePath}`);
6508
+ console.log(" Run `modelstat connect` again to re-enable.");
6509
+ } catch (err) {
6510
+ console.error(`\u2717 ${err.message}`);
6511
+ process.exit(1);
6512
+ }
6513
+ }
6514
+ async function cmdStatus() {
6515
+ const s = serviceStatus();
6516
+ const paired = !!state.bearer && !!state.deviceId;
6517
+ console.log(`paired: ${paired ? "yes" : "no"}`);
6518
+ if (paired) {
6519
+ console.log(` user: ${state.userEmail ?? "(unknown)"}`);
6520
+ console.log(` device: ${state.deviceId}`);
6521
+ console.log(` uuid: ${state.deviceUuid ?? "(not self-registered)"}`);
6522
+ }
6523
+ console.log(`service: ${s.running ? "running" : "stopped"} (${s.hint})`);
6524
+ console.log(`logs: ${logsDir()}`);
6525
+ console.log(`state: ${state.storePath}`);
6526
+ console.log(`api: ${state.apiUrl}`);
6527
+ }
6528
+ function cmdPaths(args) {
6529
+ const data = {
6530
+ state: state.storePath,
6531
+ logs: logsDir(),
6532
+ api: state.apiUrl,
6533
+ paired: !!state.bearer && !!state.deviceId
6534
+ };
6535
+ if (args.includes("--json")) {
6536
+ process.stdout.write(`${JSON.stringify(data)}
6537
+ `);
6538
+ return;
6539
+ }
6540
+ for (const [k, v] of Object.entries(data)) {
6541
+ console.log(`${k.padEnd(8)} ${String(v)}`);
6542
+ }
6543
+ }
6544
+ function parseConnectOpts(argv) {
6545
+ return {
6546
+ json: argv.includes("--json"),
6547
+ noBrowser: argv.includes("--no-browser"),
6548
+ // --oauth selects the legacy device-code flow (for users who
6549
+ // already have an account and want to stream straight into their
6550
+ // personal org). Default is the new self-register flow.
6551
+ oauth: argv.includes("--oauth")
6552
+ };
6553
+ }
5950
6554
  async function main() {
5951
6555
  const cmd = process.argv[2];
6556
+ const rest = process.argv.slice(3);
5952
6557
  switch (cmd) {
5953
6558
  case void 0:
5954
6559
  case "start":
5955
- if (!state.bearer || !state.deviceId) return cmdConnect();
6560
+ if (!state.bearer || !state.deviceId)
6561
+ return cmdConnect(parseConnectOpts(rest));
5956
6562
  return cmdStart();
5957
- case "connect":
5958
- return cmdConnect();
6563
+ case "connect": {
6564
+ const opts = parseConnectOpts(rest);
6565
+ return opts.oauth ? cmdConnectOAuth(opts) : cmdConnect(opts);
6566
+ }
5959
6567
  case "register":
5960
6568
  return cmdRegister();
6569
+ case "self-register":
6570
+ return cmdSelfRegister();
6571
+ case "await-claim":
6572
+ return cmdAwaitClaim();
5961
6573
  case "discover":
5962
6574
  return cmdDiscover();
5963
6575
  case "scan":
5964
6576
  return cmdScan();
5965
6577
  case "watch":
5966
6578
  return cmdWatch();
6579
+ case "stop":
6580
+ case "uninstall":
6581
+ return cmdStop();
6582
+ case "status":
6583
+ return cmdStatus();
6584
+ case "paths":
6585
+ cmdPaths(rest);
6586
+ return;
5967
6587
  default:
5968
- console.log("usage: modelstat [connect|start|discover|scan|watch|register]");
6588
+ console.log(
6589
+ "usage: modelstat [connect|self-register|await-claim|status|paths|stop|start|discover|scan|watch|register]"
6590
+ );
5969
6591
  console.log();
5970
- console.log(" (no args) \u2014 pair if needed, then run the daemon (default)");
5971
- console.log(" connect \u2014 pair + start daemon (recommended first run)");
5972
- console.log(" start \u2014 run the daemon (requires prior pairing)");
5973
- console.log(" discover \u2014 one-shot report of installs/identities");
5974
- console.log(" scan \u2014 one-shot parse + upload of local JSONL");
5975
- console.log(" watch \u2014 continuous (chokidar) with periodic backstop");
5976
- console.log(" register \u2014 DEV shortcut (email-only, no OAuth)");
6592
+ console.log(" (no args) \u2014 pair if needed, then run the daemon in the foreground");
6593
+ console.log(" connect \u2014 OAuth device-code pair + install service, then exit");
6594
+ console.log(" flags: --json (NDJSON events on stdout), --no-browser");
6595
+ console.log(" self-register \u2014 agent-first: generate UUIDv7, get device_secret + claim code");
6596
+ console.log(" (no human required for the first call)");
6597
+ console.log(" await-claim \u2014 block until a human claims this self-registered device");
6598
+ console.log(" status \u2014 show pairing + service state");
6599
+ console.log(" paths \u2014 print state file + log dir + api URL (use --json for machine-readable)");
6600
+ console.log(" stop \u2014 stop and uninstall the background service");
6601
+ console.log(" start \u2014 run the daemon (used by the installed service)");
6602
+ console.log(" discover \u2014 one-shot report of installs/identities");
6603
+ console.log(" scan \u2014 one-shot parse + upload of local JSONL");
6604
+ console.log(" watch \u2014 continuous (chokidar) with periodic backstop");
6605
+ console.log(" register \u2014 DEV shortcut (email-only, no OAuth)");
5977
6606
  process.exit(1);
5978
6607
  }
5979
6608
  }