@use-aistack/cli 0.6.1 → 0.6.3

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 CHANGED
@@ -18,7 +18,7 @@ The default command. Scan your local transcripts (rolling 30 days), review the f
18
18
  npx @use-aistack/cli sync
19
19
  ```
20
20
 
21
- Requires a one-time `login` first.
21
+ On an unlinked machine, `sync` starts the login flow inline — your browser opens to approve the machine, then the sync continues. One command is the whole onboarding.
22
22
 
23
23
  ### `npx @use-aistack/cli sync --auto on` / `off`
24
24
 
@@ -34,7 +34,7 @@ The silent run (`sync --auto`) never prompts and publishes only under this opt-i
34
34
 
35
35
  ### `npx @use-aistack/cli login`
36
36
 
37
- Link this machine to your AI Stack account via browser.
37
+ Link this machine to your AI Stack account via browser. Optional — `sync` runs this inline when the machine is not linked yet.
38
38
 
39
39
  ```sh
40
40
  npx @use-aistack/cli login
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
5
 
6
+ // src/version.ts
7
+ var CLI_VERSION = true ? "0.6.3" : "0.0.0-dev";
8
+
6
9
  // src/api.ts
7
10
  var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
8
11
  async function request(path3, options = {}) {
@@ -34,7 +37,12 @@ function failure(what, res) {
34
37
  async function authStart(machineName) {
35
38
  const res = await request("/api/cli/auth/start", {
36
39
  method: "POST",
37
- body: JSON.stringify(machineName ? { machineName } : {})
40
+ // `cliVersion` rides along so `cli_login_completed` can report which
41
+ // version linked the machine (#78). The server carries it on the pending
42
+ // session and reads it at the token exchange.
43
+ body: JSON.stringify(
44
+ machineName ? { machineName, cliVersion: CLI_VERSION } : { cliVersion: CLI_VERSION }
45
+ )
38
46
  });
39
47
  if (!res.ok) throw failure("Auth start failed", res);
40
48
  return res.json();
@@ -1572,8 +1580,7 @@ function proposedMachineName(read = hostname) {
1572
1580
  return void 0;
1573
1581
  }
1574
1582
  }
1575
- async function loginCommand() {
1576
- intro2("login");
1583
+ async function performLogin() {
1577
1584
  const s = p5.spinner();
1578
1585
  s.start("Starting authentication...");
1579
1586
  let session;
@@ -1583,8 +1590,7 @@ async function loginCommand() {
1583
1590
  } catch (err) {
1584
1591
  s.stop("Failed to start authentication");
1585
1592
  p5.log.error(err instanceof Error ? err.message : String(err));
1586
- outroError("error");
1587
- process.exit(1);
1593
+ return false;
1588
1594
  }
1589
1595
  p5.log.info(`${dim("CODE")} ${limeBold(session.userCode)}`);
1590
1596
  p5.log.info(`${dim("OPEN")} ${dim(session.authUrl)}`);
@@ -1604,29 +1610,33 @@ async function loginCommand() {
1604
1610
  if (result.status === "approved" && result.token) {
1605
1611
  s.stop(lime("Authenticated"));
1606
1612
  saveToken(result.token, result.userId);
1607
- p5.log.success(
1608
- `Token saved. Run ${limeBold("npx @use-aistack/cli collect")} to get started.`
1609
- );
1610
- outro2(lime("done"));
1611
- return;
1613
+ return true;
1612
1614
  }
1613
1615
  if (result.status === "expired") {
1614
1616
  s.stop("Session expired");
1615
1617
  p5.log.error("Authentication session expired. Please try again.");
1616
- outroError("expired");
1617
- process.exit(1);
1618
+ return false;
1618
1619
  }
1619
1620
  } catch (err) {
1620
1621
  s.stop("Error polling");
1621
1622
  p5.log.error(err instanceof Error ? err.message : String(err));
1622
- outroError("error");
1623
- process.exit(1);
1623
+ return false;
1624
1624
  }
1625
1625
  }
1626
1626
  s.stop("Timed out");
1627
1627
  p5.log.error("Authentication timed out after 3 minutes. Please try again.");
1628
- outroError("timed out");
1629
- process.exit(1);
1628
+ return false;
1629
+ }
1630
+ async function loginCommand() {
1631
+ intro2("login");
1632
+ if (!await performLogin()) {
1633
+ outroError("error");
1634
+ process.exit(1);
1635
+ }
1636
+ p5.log.success(
1637
+ `Token saved. Run ${limeBold("npx @use-aistack/cli sync")} to publish your usage.`
1638
+ );
1639
+ outro2(lime("done"));
1630
1640
  }
1631
1641
 
1632
1642
  // src/commands/sync.ts
@@ -2748,7 +2758,11 @@ function emptyScanStats() {
2748
2758
  filesRead: 0,
2749
2759
  filesSkippedByMtime: 0,
2750
2760
  filesSkippedAsDuplicate: 0,
2751
- filesUnreadable: 0
2761
+ filesUnreadable: 0,
2762
+ filesForeign: 0,
2763
+ foreignOriginators: /* @__PURE__ */ new Map(),
2764
+ unreadableFiles: [],
2765
+ filesZstdUnsupported: 0
2752
2766
  };
2753
2767
  }
2754
2768
 
@@ -3040,11 +3054,12 @@ async function scan2(agg, opts = {}) {
3040
3054
  } catch {
3041
3055
  resolved = file;
3042
3056
  }
3043
- if (visited.has(resolved)) {
3057
+ const dedupKey = resolved.endsWith(".zst") ? resolved.slice(0, -".zst".length) : resolved;
3058
+ if (visited.has(dedupKey)) {
3044
3059
  stats.filesSkippedAsDuplicate++;
3045
3060
  continue;
3046
3061
  }
3047
- visited.add(resolved);
3062
+ visited.add(dedupKey);
3048
3063
  if (opts.sinceMs !== void 0) {
3049
3064
  try {
3050
3065
  const st = await stat3(file);
@@ -3058,11 +3073,20 @@ async function scan2(agg, opts = {}) {
3058
3073
  agg.files++;
3059
3074
  stats.filesRead++;
3060
3075
  if (opts.onProgress && agg.files % 200 === 0) opts.onProgress(agg.files);
3061
- try {
3062
- ingestFile2(agg, file, opts.sinceMs);
3063
- } catch {
3076
+ const outcome = ingestWithRetry(agg, file, opts);
3077
+ if (!outcome.ok) {
3064
3078
  stats.filesUnreadable++;
3065
3079
  stats.filesRead--;
3080
+ if (outcome.reason === "zstd-unsupported") stats.filesZstdUnsupported++;
3081
+ stats.unreadableFiles.push({
3082
+ path: path2.relative(root, file),
3083
+ reason: outcome.reason
3084
+ });
3085
+ } else if (!outcome.genuine) {
3086
+ stats.filesForeign++;
3087
+ stats.filesRead--;
3088
+ const seen = stats.foreignOriginators.get(outcome.originator) ?? 0;
3089
+ stats.foreignOriginators.set(outcome.originator, seen + 1);
3066
3090
  }
3067
3091
  }
3068
3092
  }
@@ -3077,29 +3101,81 @@ async function exists3(p8) {
3077
3101
  return false;
3078
3102
  }
3079
3103
  }
3080
- function ingestFile2(agg, file, sinceMs) {
3104
+ function errorClass(e) {
3105
+ const code = e?.code;
3106
+ if (typeof code === "string" && code.length > 0) return code;
3107
+ return e instanceof Error ? e.constructor.name : "unknown";
3108
+ }
3109
+ var readError = (reason) => Object.assign(new Error(reason), { code: reason });
3110
+ function ingestWithRetry(agg, file, opts) {
3111
+ try {
3112
+ return ingestFile2(agg, file, opts);
3113
+ } catch (e) {
3114
+ if (errorClass(e) === "ENOENT" && !file.endsWith(".zst")) {
3115
+ try {
3116
+ return ingestFile2(agg, `${file}.zst`, opts);
3117
+ } catch (e2) {
3118
+ return { ok: false, reason: errorClass(e2) };
3119
+ }
3120
+ }
3121
+ return { ok: false, reason: errorClass(e) };
3122
+ }
3123
+ }
3124
+ function ingestFile2(agg, file, opts) {
3125
+ const readFile = opts.readFileImpl ?? readFileSync9;
3081
3126
  let text;
3082
3127
  if (file.endsWith(".zst")) {
3083
- if (zstdDecompress === null) {
3084
- throw new Error("zstd not supported by this Node runtime");
3128
+ if (zstdDecompress === null) throw readError("zstd-unsupported");
3129
+ const raw = readFile(file);
3130
+ try {
3131
+ text = zstdDecompress(
3132
+ Buffer.isBuffer(raw) ? raw : Buffer.from(raw)
3133
+ ).toString("utf8");
3134
+ } catch {
3135
+ throw readError("zstd-corrupt");
3085
3136
  }
3086
- text = zstdDecompress(readFileSync9(file)).toString("utf8");
3087
3137
  } else {
3088
- text = readFileSync9(file, "utf8");
3138
+ text = readFile(file).toString("utf8");
3089
3139
  }
3090
- const state = createFileState();
3140
+ const records = [];
3141
+ let nonEmptyLines = 0;
3142
+ let parseErrors = 0;
3091
3143
  for (const line of text.split("\n")) {
3092
3144
  if (!line) continue;
3093
- agg.lines++;
3094
- let rec;
3145
+ nonEmptyLines++;
3095
3146
  try {
3096
- rec = JSON.parse(line);
3147
+ records.push(JSON.parse(line));
3097
3148
  } catch {
3098
- agg.parseErrors++;
3099
- continue;
3149
+ parseErrors++;
3100
3150
  }
3101
- ingestLine(agg, rec, state, sinceMs);
3102
3151
  }
3152
+ const verdict = classifyRollout(records);
3153
+ if (!verdict.genuine) return { ok: true, ...verdict };
3154
+ agg.lines += nonEmptyLines;
3155
+ agg.parseErrors += parseErrors;
3156
+ const state = createFileState();
3157
+ for (const rec of records) ingestLine(agg, rec, state, opts.sinceMs);
3158
+ return { ok: true, genuine: true };
3159
+ }
3160
+ function classifyRollout(records) {
3161
+ let originator = null;
3162
+ let sawTurnContext = false;
3163
+ let genuine = records.length > 0;
3164
+ for (const [i, raw] of records.entries()) {
3165
+ const rec = asObj(raw);
3166
+ const type = rec ? asStr(rec.type) : null;
3167
+ const payload = rec ? asObj(rec.payload) : null;
3168
+ if (i === 0 && type !== "session_meta") genuine = false;
3169
+ if (type === "session_meta" && payload && originator === null) {
3170
+ originator = asStr(payload.originator);
3171
+ } else if (type === "turn_context") {
3172
+ sawTurnContext = true;
3173
+ } else if (type === "event_msg" && payload && asStr(payload.type) === "token_count" && !sawTurnContext) {
3174
+ genuine = false;
3175
+ }
3176
+ }
3177
+ if (genuine) return { genuine: true };
3178
+ return { genuine: false, originator: originator ?? "(none)" };
3103
3179
  }
3104
3180
  function readConfiguredMcpServers(agg, configFile) {
3105
3181
  const file = configFile ?? path2.join(codexHome2(), "config.toml");
@@ -3368,11 +3444,12 @@ function mergeKeptPrivate(halves) {
3368
3444
  }
3369
3445
  return out;
3370
3446
  }
3371
- function buildSyncBody(built, syncConfig) {
3447
+ function buildSyncBody(built, syncConfig, autoSync) {
3372
3448
  const payloads = built.map((b) => b.payload);
3373
- if (!syncConfig.reviewKeptPrivate) return { payloads };
3449
+ const base = autoSync ? { payloads, autoSync } : { payloads };
3450
+ if (!syncConfig.reviewKeptPrivate) return base;
3374
3451
  return {
3375
- payloads,
3452
+ ...base,
3376
3453
  keptPrivate: mergeKeptPrivate(built.map((b) => b.keptPrivate))
3377
3454
  };
3378
3455
  }
@@ -3468,7 +3545,32 @@ function harnessLabel(name) {
3468
3545
  if (name === "codex") return "Codex";
3469
3546
  return name;
3470
3547
  }
3471
- function payloadBlock(payload, showHeader) {
3548
+ var UNREADABLE_FILES_SHOWN = 5;
3549
+ function scanNoteLines(stats, label) {
3550
+ const out = [];
3551
+ const shown = stats.unreadableFiles.slice(0, UNREADABLE_FILES_SHOWN);
3552
+ for (const f of shown) {
3553
+ out.push(` ${f.path} (${f.reason})`);
3554
+ }
3555
+ if (stats.unreadableFiles.length > shown.length) {
3556
+ out.push(
3557
+ ` ...${stats.unreadableFiles.length - shown.length} more`
3558
+ );
3559
+ }
3560
+ if (stats.filesZstdUnsupported > 0) {
3561
+ out.push(
3562
+ ` ${stats.filesZstdUnsupported} compressed rollout${stats.filesZstdUnsupported === 1 ? "" : "s"} need Node 22.15 or newer`
3563
+ );
3564
+ }
3565
+ if (stats.filesForeign > 0) {
3566
+ const origins = [...stats.foreignOriginators].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([name, n]) => n > 1 ? `${name} \xD7${n}` : name).join(", ");
3567
+ out.push(
3568
+ `skipped ${stats.filesForeign} file${stats.filesForeign === 1 ? "" : "s"} not written by ${label} \u2014 left out (originators: ${origins})`
3569
+ );
3570
+ }
3571
+ return out;
3572
+ }
3573
+ function payloadBlock(payload, showHeader, stats) {
3472
3574
  const out = [];
3473
3575
  if (showHeader) {
3474
3576
  out.push(
@@ -3491,6 +3593,9 @@ function payloadBlock(payload, showHeader) {
3491
3593
  `coverage ${cov.filesUnreadable} files unreadable \xB7 ${cov.linesFailed} lines failed \u2014 this reading is a floor`
3492
3594
  );
3493
3595
  }
3596
+ if (stats) {
3597
+ out.push(...scanNoteLines(stats, harnessLabel(payload.harness.name)));
3598
+ }
3494
3599
  out.push("");
3495
3600
  out.push("models");
3496
3601
  for (const m of payload.models) {
@@ -3522,7 +3627,8 @@ function buildGateSummary(ctx) {
3522
3627
  );
3523
3628
  }
3524
3629
  for (const payload of payloads) {
3525
- out.push(...payloadBlock(payload, payloads.length > 1));
3630
+ const stats = ctx.scanStats?.[payload.harness.name];
3631
+ out.push(...payloadBlock(payload, payloads.length > 1, stats));
3526
3632
  out.push("");
3527
3633
  }
3528
3634
  if (out[out.length - 1] === "") out.pop();
@@ -3548,6 +3654,12 @@ function buildGateSummary(ctx) {
3548
3654
  out.push(" they stay on this machine");
3549
3655
  }
3550
3656
  }
3657
+ if (body.autoSync !== void 0) {
3658
+ out.push("");
3659
+ out.push(
3660
+ `auto-sync ${body.autoSync.enabled ? `on, about every ${body.autoSync.frequencyHours}h` : "off"}`
3661
+ );
3662
+ }
3551
3663
  if (source === "bundled") {
3552
3664
  out.push("");
3553
3665
  out.push(
@@ -3575,9 +3687,11 @@ async function stageSync(deps) {
3575
3687
  ...token ? { token } : {}
3576
3688
  });
3577
3689
  const built = [];
3690
+ const scanStats = {};
3578
3691
  const sinceMs = windowStartMs(now, windowDays);
3579
3692
  for (const adapter of await adapters()) {
3580
3693
  const { aggregate, stats } = await adapter.scan({ sinceMs });
3694
+ scanStats[adapter.name] = stats;
3581
3695
  built.push(
3582
3696
  buildPayload({
3583
3697
  aggregate,
@@ -3591,7 +3705,8 @@ async function stageSync(deps) {
3591
3705
  })
3592
3706
  );
3593
3707
  }
3594
- const body = buildSyncBody(built, config);
3708
+ const settings = (deps.getSettingsImpl ?? getSettings)();
3709
+ const body = buildSyncBody(built, config, settings.autoSync);
3595
3710
  const bodyJson = JSON.stringify(body);
3596
3711
  const keptPrivate = mergeKeptPrivate(built.map((b) => b.keptPrivate));
3597
3712
  const ctx = {
@@ -3599,7 +3714,8 @@ async function stageSync(deps) {
3599
3714
  keptPrivate,
3600
3715
  config,
3601
3716
  source,
3602
- baseUrl: deps.baseUrl
3717
+ baseUrl: deps.baseUrl,
3718
+ scanStats
3603
3719
  };
3604
3720
  let blockedReason = null;
3605
3721
  if (built.length === 0) {
@@ -3607,7 +3723,7 @@ async function stageSync(deps) {
3607
3723
  } else if (token === null) {
3608
3724
  blockedReason = "This machine is not linked. Run `npx @use-aistack/cli login` first.";
3609
3725
  } else if (config.stack === null) {
3610
- blockedReason = source === "bundled" ? "Could not fetch your settings from aistack, so the destination stack is unknown. Publish needs it. Check the network and preview again." : "The token resolves no destination stack. Run `npx @use-aistack/cli login` again to re-link this machine.";
3726
+ blockedReason = source === "bundled" ? "Could not fetch your settings from aistack, so the destination stack is unknown. Publish needs it. Check the network and preview again." : `The token resolves no destination stack. Create one at ${deps.baseUrl}/stacks/new, then sync again.`;
3611
3727
  }
3612
3728
  return {
3613
3729
  id: stageId(bodyJson),
@@ -3751,6 +3867,16 @@ async function syncCommand(options = {}) {
3751
3867
  process.exitCode = 1;
3752
3868
  return;
3753
3869
  }
3870
+ if (getToken() === null) {
3871
+ p7.log.message(
3872
+ "This machine is not linked to an aistack account yet \u2014 linking it first."
3873
+ );
3874
+ if (!await performLogin()) {
3875
+ outroError("login failed \u2014 nothing was sent");
3876
+ process.exitCode = 1;
3877
+ return;
3878
+ }
3879
+ }
3754
3880
  const s = p7.spinner();
3755
3881
  s.start("Scanning local agent transcripts");
3756
3882
  let staged;
@@ -4060,7 +4186,7 @@ function runStdioSyncServer(deps) {
4060
4186
 
4061
4187
  // src/index.ts
4062
4188
  var program = new Command();
4063
- program.name("aistack").description("Measure and share your AI stack from your terminal").version("0.5.0");
4189
+ program.name("aistack").description("Measure and share your AI stack from your terminal").version(CLI_VERSION);
4064
4190
  program.command("login").description("Authenticate with AI Stack").action(loginCommand);
4065
4191
  program.command("collect").description("Scan and upload AI config files from your project").option("--no-global", "Exclude global config files (~/.claude, etc.)").action((options) => collectCommand({ global: options.global ?? true }));
4066
4192
  program.command("create").description("Download and write your stack's AI config files").action(createCommand);