@use-aistack/cli 0.10.0 → 0.10.2

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/index.js CHANGED
@@ -3,8 +3,352 @@
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
5
 
6
+ // ../pricing/src/table.ts
7
+ var PROVIDER_SEPARATOR = ":";
8
+ var LOCAL_PRICING_TABLE_VERSION = "local-no-charge";
9
+ var PROVIDER_VENDOR = {
10
+ anthropic: "anthropic",
11
+ openai: "openai",
12
+ google: "google"
13
+ };
14
+ var LOCAL_PROVIDERS = /* @__PURE__ */ new Set([
15
+ "ollama",
16
+ "lmstudio",
17
+ "llama.cpp",
18
+ "llamacpp",
19
+ "local"
20
+ ]);
21
+ var FREE_PERIOD = {
22
+ from: null,
23
+ to: null,
24
+ input: 0,
25
+ output: 0,
26
+ cacheRead: 0,
27
+ cacheWrite5m: 0,
28
+ cacheWrite1h: 0,
29
+ source: LOCAL_PRICING_TABLE_VERSION
30
+ };
31
+ function splitModelKey(modelKey) {
32
+ const at = modelKey.indexOf(PROVIDER_SEPARATOR);
33
+ if (at === -1) return { provider: null, model: modelKey };
34
+ return {
35
+ provider: modelKey.slice(0, at),
36
+ model: modelKey.slice(at + PROVIDER_SEPARATOR.length)
37
+ };
38
+ }
39
+ var key = (slug, provider) => `${provider ?? ""}\0${slug}`;
40
+ var PriceIndex = class {
41
+ periods = /* @__PURE__ */ new Map();
42
+ vendors = /* @__PURE__ */ new Map();
43
+ id;
44
+ constructor(table) {
45
+ this.id = table.id;
46
+ const groups = /* @__PURE__ */ new Map();
47
+ for (const row of table.rows) {
48
+ const k = key(row.modelSlug, row.provider);
49
+ const g = groups.get(k) ?? [];
50
+ g.push(row);
51
+ groups.set(k, g);
52
+ if (row.vendor && row.provider === void 0) {
53
+ this.vendors.set(row.modelSlug, row.vendor);
54
+ }
55
+ }
56
+ for (const [k, rows] of groups) {
57
+ rows.sort((a, b) => a.from - b.from);
58
+ this.periods.set(
59
+ k,
60
+ rows.map((r, i) => ({
61
+ from: r.from === 0 ? null : r.from,
62
+ to: i + 1 < rows.length ? rows[i + 1].from : null,
63
+ input: r.input,
64
+ output: r.output,
65
+ cacheRead: r.cacheRead ?? 0,
66
+ cacheWrite5m: r.cacheWrite5m ?? 0,
67
+ cacheWrite1h: r.cacheWrite1h ?? 0,
68
+ source: r.source
69
+ }))
70
+ );
71
+ }
72
+ }
73
+ /** The vendor a bare row belongs to, when the table says. */
74
+ vendorOf(slug) {
75
+ return this.vendors.get(slug) ?? null;
76
+ }
77
+ /** True when the table holds any row at all for this (model, provider). */
78
+ has(slug, provider) {
79
+ return this.periods.has(key(slug, provider));
80
+ }
81
+ /** The periods for exactly this (model, provider). */
82
+ rowsFor(slug, provider) {
83
+ return this.periods.get(key(slug, provider)) ?? [];
84
+ }
85
+ get size() {
86
+ return this.periods.size;
87
+ }
88
+ };
89
+ var Pricer = class {
90
+ constructor(layers, vendorHint = () => null) {
91
+ this.layers = layers;
92
+ this.vendorHint = vendorHint;
93
+ }
94
+ /** The ids of the layers, in lookup order. */
95
+ get tableIds() {
96
+ return this.layers.map((l) => l.id);
97
+ }
98
+ vendorOf(slug) {
99
+ for (const layer of this.layers) {
100
+ const v = layer.vendorOf(slug);
101
+ if (v) return v;
102
+ }
103
+ return this.vendorHint(slug);
104
+ }
105
+ firstLayerWith(slug, provider) {
106
+ return this.layers.find((l) => l.has(slug, provider)) ?? null;
107
+ }
108
+ /**
109
+ * Every period that applies to a pricing key, or an empty list when none
110
+ * can be cited.
111
+ *
112
+ * A bare key is the vendor's own rate. A local provider is free. A provider
113
+ * with rows of its own uses them. A provider that IS the vendor reaches the
114
+ * vendor's bare rows. Anything else (a gateway, an unknown provider) holds
115
+ * no rate.
116
+ */
117
+ periodsFor(modelKey) {
118
+ const { provider, model } = splitModelKey(modelKey);
119
+ if (provider === null) {
120
+ return this.firstLayerWith(model, null)?.rowsFor(model, null) ?? [];
121
+ }
122
+ if (LOCAL_PROVIDERS.has(provider)) return [FREE_PERIOD];
123
+ const own = this.firstLayerWith(model, provider);
124
+ if (own) return own.rowsFor(model, provider);
125
+ const vendor = PROVIDER_VENDOR[provider];
126
+ if (!vendor || this.vendorOf(model) !== vendor) return [];
127
+ return this.firstLayerWith(model, null)?.rowsFor(model, null) ?? [];
128
+ }
129
+ isLocal(modelKey) {
130
+ const { provider } = splitModelKey(modelKey);
131
+ return provider !== null && LOCAL_PROVIDERS.has(provider);
132
+ }
133
+ isPriced(modelKey) {
134
+ return this.periodsFor(modelKey).length > 0;
135
+ }
136
+ /**
137
+ * The rate in effect at `atMs`, or `null` when the model is unknown or the
138
+ * timestamp predates every period. A `null` timestamp also yields `null`:
139
+ * inventing a price for an undated record would attribute the wrong rate.
140
+ */
141
+ priceAt(modelKey, atMs) {
142
+ if (atMs === null) return null;
143
+ for (const p8 of this.periodsFor(modelKey)) {
144
+ if ((p8.from === null || atMs >= p8.from) && (p8.to === null || atMs < p8.to)) {
145
+ return p8;
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ /** Every rate that applies anywhere inside `[fromMs, toMs]`. */
151
+ periodsInWindow(modelKey, fromMs, toMs) {
152
+ return this.periodsFor(modelKey).filter(
153
+ (p8) => (p8.from === null || p8.from <= toMs) && (p8.to === null || p8.to > fromMs)
154
+ );
155
+ }
156
+ /**
157
+ * The citation for this key: the source of the period in effect at `atMs`,
158
+ * or of the latest period when no time is given. `null` when unpriced.
159
+ */
160
+ tableFor(modelKey, atMs) {
161
+ const periods = this.periodsFor(modelKey);
162
+ if (periods.length === 0) return null;
163
+ if (atMs !== void 0) return this.priceAt(modelKey, atMs)?.source ?? null;
164
+ return periods[periods.length - 1].source;
165
+ }
166
+ };
167
+ function parsePriceTable(body) {
168
+ if (typeof body !== "object" || body === null) return null;
169
+ const b = body;
170
+ if (typeof b.id !== "string" || !Array.isArray(b.rows)) return null;
171
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
172
+ const opt = (v) => num(v) ? v : void 0;
173
+ const rows = [];
174
+ for (const raw of b.rows) {
175
+ const r = raw;
176
+ if (typeof r?.modelSlug !== "string" || r.modelSlug.length === 0 || !num(r.from) || !num(r.input) || !num(r.output) || typeof r.source !== "string") {
177
+ continue;
178
+ }
179
+ const vendor = r.vendor === "anthropic" || r.vendor === "openai" || r.vendor === "google" || r.vendor === "local" ? r.vendor : void 0;
180
+ rows.push({
181
+ modelSlug: r.modelSlug,
182
+ ...typeof r.provider === "string" && r.provider.length > 0 ? { provider: r.provider } : {},
183
+ from: r.from,
184
+ input: r.input,
185
+ output: r.output,
186
+ ...opt(r.cacheRead) !== void 0 ? { cacheRead: r.cacheRead } : {},
187
+ ...opt(r.cacheWrite5m) !== void 0 ? { cacheWrite5m: r.cacheWrite5m } : {},
188
+ ...opt(r.cacheWrite1h) !== void 0 ? { cacheWrite1h: r.cacheWrite1h } : {},
189
+ source: r.source,
190
+ ...vendor ? { vendor } : {}
191
+ });
192
+ }
193
+ if (rows.length === 0) return null;
194
+ return { id: b.id, rows };
195
+ }
196
+
197
+ // ../pricing/src/index.ts
198
+ var PRICING_TABLE_VERSION = "anthropic-list-2026-07-25";
199
+ var OPENAI_PRICING_TABLE_VERSION = "openai-list-2026-08-02";
200
+ var GOOGLE_PRICING_TABLE_VERSION = "google-list-2026-08-09";
201
+ var BUNDLED_PRICE_TABLE_ID = "bundled-2026-08-29";
202
+ var CACHE_WRITE_5M_MULTIPLIER = 1.25;
203
+ var CACHE_WRITE_1H_MULTIPLIER = 2;
204
+ var CACHE_READ_MULTIPLIER = 0.1;
205
+ var SONNET_5_INTRO_ENDS_MS = Date.UTC(2026, 8, 1);
206
+ var DEFAULT_CACHE_MULTIPLIERS = {
207
+ write5m: CACHE_WRITE_5M_MULTIPLIER,
208
+ write1h: CACHE_WRITE_1H_MULTIPLIER,
209
+ read: CACHE_READ_MULTIPLIER
210
+ };
211
+ var GOOGLE_CACHE_MULTIPLIERS = {
212
+ write5m: 1,
213
+ write1h: 1,
214
+ read: 0.1
215
+ };
216
+ var anthropic = (periods) => ({
217
+ vendor: "anthropic",
218
+ table: PRICING_TABLE_VERSION,
219
+ periods,
220
+ cache: DEFAULT_CACHE_MULTIPLIERS
221
+ });
222
+ var openai = (periods) => ({
223
+ vendor: "openai",
224
+ table: OPENAI_PRICING_TABLE_VERSION,
225
+ periods,
226
+ cache: DEFAULT_CACHE_MULTIPLIERS
227
+ });
228
+ var google = (periods) => ({
229
+ vendor: "google",
230
+ table: GOOGLE_PRICING_TABLE_VERSION,
231
+ periods,
232
+ cache: GOOGLE_CACHE_MULTIPLIERS
233
+ });
234
+ var flat = (input, output) => [
235
+ { from: null, input, output }
236
+ ];
237
+ var PRICES = {
238
+ "claude-fable-5": anthropic(flat(10, 50)),
239
+ "claude-mythos-5": anthropic(flat(10, 50)),
240
+ "claude-opus-5": anthropic(flat(5, 25)),
241
+ "claude-opus-4-8": anthropic(flat(5, 25)),
242
+ "claude-opus-4-7": anthropic(flat(5, 25)),
243
+ "claude-opus-4-6": anthropic(flat(5, 25)),
244
+ "claude-sonnet-5": anthropic([
245
+ { from: null, input: 2, output: 10 },
246
+ { from: SONNET_5_INTRO_ENDS_MS, input: 3, output: 15 }
247
+ ]),
248
+ "claude-sonnet-4-6": anthropic(flat(3, 15)),
249
+ "claude-haiku-4-5": anthropic(flat(1, 5)),
250
+ // Fast mode (research preview) - Claude API only, Opus 5 / Opus 4.8 only.
251
+ // Opus 4.7 fast mode was removed, so there is deliberately no 4-7 entry.
252
+ "claude-opus-5#fast": anthropic(flat(10, 50)),
253
+ "claude-opus-4-8#fast": anthropic(flat(10, 50)),
254
+ // OpenAI (Codex) - standard-context tier (<272K; observed context window is
255
+ // 258,400).
256
+ "gpt-5.5": openai(flat(5, 30)),
257
+ "gpt-5.4": openai(flat(2.5, 15)),
258
+ "gpt-5.4-mini": openai(flat(0.75, 4.5)),
259
+ "gpt-5.3-codex": openai(flat(1.75, 14)),
260
+ // The gpt-5.6 family launched 2026-07-29; Terra and Luna were repriced on
261
+ // 2026-07-30 (-20% / -80%). The one-day launch rates are not on the list
262
+ // page and are NOT encoded - a July-29 Terra/Luna record underprices for
263
+ // one day rather than carrying a rate we cannot cite (#72).
264
+ // Sol: models.dev reports $4 / $20 on 2026-08-29; the earlier $5 / $30 is
265
+ // not dated, so the lower rate prices the whole period (lower bound).
266
+ "gpt-5.6-sol": openai(flat(4, 20)),
267
+ "gpt-5.6-terra": openai(flat(2, 12)),
268
+ "gpt-5.6-luna": openai(flat(0.2, 1.2)),
269
+ // NOT on OpenAI's list page - an internal Codex routing label with no
270
+ // official price (openai/codex#20981). Rate is the aggregator consensus
271
+ // ($2.50 / $15.00), scoped in explicitly by ticket #72 because it carries
272
+ // real token volume in Codex rollouts. A priced lane, see PRICED_LANES.
273
+ "codex-auto-review": openai(flat(2.5, 15)),
274
+ // Google (opencode, pi-mono) - Standard tier. Where a model is
275
+ // context-tiered, the <=200K rate is encoded, exactly as the OpenAI rows
276
+ // encode the standard-context tier: the payload carries no per-response
277
+ // context length, so the cheaper side keeps the figure a lower bound.
278
+ "gemini-3.1-pro-preview": google(flat(2, 12)),
279
+ // models.dev reports $0.75 / $3.75 on 2026-08-29 (was $1.5 / $7.5).
280
+ "gemini-3.6-flash": google(flat(0.75, 3.75)),
281
+ "gemini-3.5-flash": google(flat(1.5, 9)),
282
+ "gemini-3-flash-preview": google(flat(0.5, 3)),
283
+ "gemini-2.5-pro": google(flat(1.25, 10)),
284
+ "gemini-2.5-flash": google(flat(0.3, 2.5)),
285
+ // RETIRED from Google's list page by 2026-08-09, and still the largest
286
+ // single block of Google tokens measured in #122. Encoded at its launch
287
+ // rate: real volume, a rate we can name. Announcement rate, <=200K tier.
288
+ "gemini-3-pro-preview": google(flat(2, 12)),
289
+ // A real Anthropic model with no row until #123. Measured in #122 as
290
+ // `claude-opus-4-5-20251101`, which the dated-suffix rule strips to this key.
291
+ "claude-opus-4-5": anthropic(flat(5, 25))
292
+ };
293
+ function bundledPriceTable() {
294
+ const rows = [];
295
+ for (const [modelSlug, entry] of Object.entries(PRICES)) {
296
+ for (const p8 of entry.periods) {
297
+ rows.push({
298
+ modelSlug,
299
+ from: p8.from ?? 0,
300
+ input: p8.input,
301
+ output: p8.output,
302
+ cacheRead: p8.input * entry.cache.read,
303
+ cacheWrite5m: p8.input * entry.cache.write5m,
304
+ cacheWrite1h: p8.input * entry.cache.write1h,
305
+ source: entry.table,
306
+ vendor: entry.vendor
307
+ });
308
+ }
309
+ }
310
+ return { id: BUNDLED_PRICE_TABLE_ID, rows };
311
+ }
312
+ var BUNDLED_INDEX = new PriceIndex(bundledPriceTable());
313
+ var BUNDLED_PRICER = new Pricer([BUNDLED_INDEX]);
314
+ function layeredPricer(table, vendorHint) {
315
+ return new Pricer([new PriceIndex(table), BUNDLED_INDEX], vendorHint);
316
+ }
317
+ var active = BUNDLED_PRICER;
318
+ function setActivePricer(pricer) {
319
+ active = pricer ?? BUNDLED_PRICER;
320
+ }
321
+ function modelKeyFor(provider, model) {
322
+ return `${provider}${PROVIDER_SEPARATOR}${model}`;
323
+ }
324
+ function normalizeModel(model) {
325
+ const { provider, model: bare } = splitModelKey(model);
326
+ const [base, suffix] = bare.split("#");
327
+ const stripped = base.replace(/-\d{8}$/, "");
328
+ const normalized = suffix ? `${stripped}#${suffix}` : stripped;
329
+ return provider === null ? normalized : modelKeyFor(provider, normalized);
330
+ }
331
+ function baseModelId(modelKey) {
332
+ return modelKey.split("#")[0];
333
+ }
334
+ function isPricedModel(modelKey) {
335
+ return active.isPriced(modelKey);
336
+ }
337
+ function pricingTableFor(modelKey, atMs) {
338
+ return active.tableFor(modelKey, atMs);
339
+ }
340
+ function costAtPeriod(p8, t) {
341
+ const M = 1e6;
342
+ return (t.input * p8.input + t.output * p8.output + (t.cacheWrite5m + t.cacheWriteUnsplit) * p8.cacheWrite5m + t.cacheWrite1h * p8.cacheWrite1h + t.cacheRead * p8.cacheRead) / M;
343
+ }
344
+ function apiEquivalentCost(modelKey, t, atMs) {
345
+ const p8 = active.priceAt(modelKey, atMs);
346
+ if (!p8) return null;
347
+ return costAtPeriod(p8, t);
348
+ }
349
+
6
350
  // src/version.ts
7
- var CLI_VERSION = true ? "0.10.0" : "0.0.0-dev";
351
+ var CLI_VERSION = true ? "0.10.2" : "0.0.0-dev";
8
352
 
9
353
  // src/api.ts
10
354
  var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
@@ -34,15 +378,19 @@ function failure(what, res) {
34
378
  }
35
379
  return new Error(`${what}: ${res.status}`);
36
380
  }
37
- async function authStart(machineName) {
381
+ async function authStart(machineName, machineNameReadOnly = false, options = {}) {
38
382
  const res = await request("/api/cli/auth/start", {
39
383
  method: "POST",
384
+ ...options.replaceToken ? { headers: authHeaders(options.replaceToken) } : {},
40
385
  // `cliVersion` rides along so `cli_login_completed` can report which
41
386
  // version linked the machine (#78). The server carries it on the pending
42
387
  // session and reads it at the token exchange.
43
- body: JSON.stringify(
44
- machineName ? { machineName, cliVersion: CLI_VERSION } : { cliVersion: CLI_VERSION }
45
- )
388
+ body: JSON.stringify({
389
+ ...machineName ? { machineName } : {},
390
+ ...machineNameReadOnly ? { machineNameReadOnly: true } : {},
391
+ cliVersion: CLI_VERSION,
392
+ ...options.destinationRequired ? { destinationRequired: true } : {}
393
+ })
46
394
  });
47
395
  if (!res.ok) throw failure("Auth start failed", res);
48
396
  return res.json();
@@ -145,6 +493,14 @@ async function fetchDayManifest(baseUrl, token) {
145
493
  }) : [];
146
494
  return { retentionDays, aggregateVersion, days };
147
495
  }
496
+ async function fetchPriceTable(baseUrl) {
497
+ const res = await fetch(`${baseUrl}/api/prices`, {
498
+ headers: { Accept: "application/json" }
499
+ });
500
+ if (res.status === 404) return null;
501
+ if (!res.ok) throw failure("Price table fetch failed", res);
502
+ return parsePriceTable(await res.json());
503
+ }
148
504
  async function setAutoSync(token, flag) {
149
505
  const res = await request("/api/cli/auto-sync", {
150
506
  method: "POST",
@@ -208,10 +564,10 @@ function classify(files) {
208
564
  if (isSingleton) {
209
565
  singletons.push(file);
210
566
  } else {
211
- const key = `${file.group}:${file.source}:${file.type}:${dir}`;
212
- const existing = groups.get(key) ?? [];
567
+ const key2 = `${file.group}:${file.source}:${file.type}:${dir}`;
568
+ const existing = groups.get(key2) ?? [];
213
569
  existing.push(file);
214
- groups.set(key, existing);
570
+ groups.set(key2, existing);
215
571
  }
216
572
  }
217
573
  const items = [];
@@ -302,7 +658,13 @@ function saveToken(token, userId, serverUrl = BASE_URL, file = CREDENTIALS_FILE)
302
658
  writeCredentials(file, data);
303
659
  }
304
660
  var SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
305
- var DEFAULT_FREQUENCY_HOURS = 24;
661
+ var DEFAULT_FREQUENCY_HOURS = 6;
662
+ var MAX_FREQUENCY_HOURS = 24;
663
+ function normalizeFrequencyHours(value) {
664
+ if (value === void 0 || !Number.isFinite(value))
665
+ return DEFAULT_FREQUENCY_HOURS;
666
+ return Math.min(MAX_FREQUENCY_HOURS, Math.max(1, Math.round(value)));
667
+ }
306
668
  function getSettings(file = SETTINGS_FILE) {
307
669
  if (!existsSync(file)) return {};
308
670
  try {
@@ -326,13 +688,13 @@ function readProjects(file = PROJECTS_FILE) {
326
688
  try {
327
689
  const raw = JSON.parse(readFileSync(file, "utf-8"));
328
690
  const data = {};
329
- for (const [key, value] of Object.entries(raw)) {
691
+ for (const [key2, value] of Object.entries(raw)) {
330
692
  if (typeof value === "string") {
331
- data[key] = {};
693
+ data[key2] = {};
332
694
  } else if (value && typeof value === "object") {
333
695
  const excluded = value.excluded;
334
696
  const workspaceId = value.workspaceId;
335
- data[key] = {
697
+ data[key2] = {
336
698
  ...Array.isArray(excluded) ? { excluded } : {},
337
699
  ...typeof workspaceId === "string" ? { workspaceId } : {}
338
700
  };
@@ -761,11 +1123,11 @@ function resolveSource(entry, mpRepoUrl) {
761
1123
  }
762
1124
  function resolvePluginLinks(installed, marketplaces, manifests) {
763
1125
  const out = [];
764
- for (const [key, entries] of Object.entries(installed.plugins ?? {})) {
765
- const at = key.lastIndexOf("@");
1126
+ for (const [key2, entries] of Object.entries(installed.plugins ?? {})) {
1127
+ const at = key2.lastIndexOf("@");
766
1128
  if (at <= 0) continue;
767
- const pluginName = key.slice(0, at);
768
- const marketplace = key.slice(at + 1);
1129
+ const pluginName = key2.slice(0, at);
1130
+ const marketplace = key2.slice(at + 1);
769
1131
  const mpRepoUrl = marketplaceRepoUrl(marketplaces[marketplace]);
770
1132
  const entry = manifests[marketplace]?.plugins?.find(
771
1133
  (p8) => p8.name === pluginName
@@ -803,8 +1165,8 @@ function detectInstalledPlugins(pluginsDir = join4(homedir4(), ".claude", "plugi
803
1165
  if (!installed?.plugins) return [];
804
1166
  const marketplaces = readJson3(join4(pluginsDir, "known_marketplaces.json")) ?? {};
805
1167
  const manifests = {};
806
- for (const key of Object.keys(installed.plugins)) {
807
- const mp = key.slice(key.lastIndexOf("@") + 1);
1168
+ for (const key2 of Object.keys(installed.plugins)) {
1169
+ const mp = key2.slice(key2.lastIndexOf("@") + 1);
808
1170
  if (!mp || manifests[mp]) continue;
809
1171
  const installLocation = marketplaces[mp]?.installLocation ?? join4(pluginsDir, "marketplaces", mp);
810
1172
  const manifest = readJson3(
@@ -1352,23 +1714,23 @@ function diffResources(current, existing) {
1352
1714
  let added = 0;
1353
1715
  let changed = 0;
1354
1716
  let unchanged = 0;
1355
- for (const [key, content] of currentMap) {
1356
- const prev = existingMap.get(key);
1717
+ for (const [key2, content] of currentMap) {
1718
+ const prev = existingMap.get(key2);
1357
1719
  if (prev === void 0) {
1358
1720
  added++;
1359
- details.push({ name: key, status: "added" });
1721
+ details.push({ name: key2, status: "added" });
1360
1722
  } else if (prev !== content) {
1361
1723
  changed++;
1362
- details.push({ name: key, status: "changed" });
1724
+ details.push({ name: key2, status: "changed" });
1363
1725
  } else {
1364
1726
  unchanged++;
1365
1727
  }
1366
1728
  }
1367
1729
  let removed = 0;
1368
- for (const key of existingMap.keys()) {
1369
- if (!currentMap.has(key)) {
1730
+ for (const key2 of existingMap.keys()) {
1731
+ if (!currentMap.has(key2)) {
1370
1732
  removed++;
1371
- details.push({ name: key, status: "removed" });
1733
+ details.push({ name: key2, status: "removed" });
1372
1734
  }
1373
1735
  }
1374
1736
  const linkLabel = (item) => {
@@ -1388,14 +1750,14 @@ function diffResources(current, existing) {
1388
1750
  };
1389
1751
  const existingLinks = linkMap(existing);
1390
1752
  const currentLinks = linkMap(current);
1391
- for (const [key, item] of currentLinks) {
1392
- if (!existingLinks.has(key)) {
1753
+ for (const [key2, item] of currentLinks) {
1754
+ if (!existingLinks.has(key2)) {
1393
1755
  added++;
1394
1756
  details.push({ name: linkLabel(item), status: "added" });
1395
1757
  }
1396
1758
  }
1397
- for (const [key, item] of existingLinks) {
1398
- if (!currentLinks.has(key)) {
1759
+ for (const [key2, item] of existingLinks) {
1760
+ if (!currentLinks.has(key2)) {
1399
1761
  removed++;
1400
1762
  details.push({ name: linkLabel(item), status: "removed" });
1401
1763
  }
@@ -1411,186 +1773,6 @@ import { dirname as dirname3, join as join6 } from "path";
1411
1773
  import { fileURLToPath } from "url";
1412
1774
  import * as p3 from "@clack/prompts";
1413
1775
 
1414
- // ../pricing/src/index.ts
1415
- var PRICING_TABLE_VERSION = "anthropic-list-2026-07-25";
1416
- var OPENAI_PRICING_TABLE_VERSION = "openai-list-2026-08-02";
1417
- var GOOGLE_PRICING_TABLE_VERSION = "google-list-2026-08-09";
1418
- var LOCAL_PRICING_TABLE_VERSION = "local-no-charge";
1419
- var CACHE_WRITE_5M_MULTIPLIER = 1.25;
1420
- var CACHE_WRITE_1H_MULTIPLIER = 2;
1421
- var CACHE_READ_MULTIPLIER = 0.1;
1422
- var PROVIDER_SEPARATOR = ":";
1423
- var SONNET_5_INTRO_ENDS_MS = Date.UTC(2026, 8, 1);
1424
- var DEFAULT_CACHE_MULTIPLIERS = {
1425
- write5m: CACHE_WRITE_5M_MULTIPLIER,
1426
- write1h: CACHE_WRITE_1H_MULTIPLIER,
1427
- read: CACHE_READ_MULTIPLIER
1428
- };
1429
- var GOOGLE_CACHE_MULTIPLIERS = {
1430
- write5m: 1,
1431
- write1h: 1,
1432
- read: 0.1
1433
- };
1434
- var FREE_CACHE_MULTIPLIERS = {
1435
- write5m: 0,
1436
- write1h: 0,
1437
- read: 0
1438
- };
1439
- var anthropic = (periods) => ({
1440
- vendor: "anthropic",
1441
- table: PRICING_TABLE_VERSION,
1442
- periods,
1443
- cache: DEFAULT_CACHE_MULTIPLIERS
1444
- });
1445
- var openai = (periods) => ({
1446
- vendor: "openai",
1447
- table: OPENAI_PRICING_TABLE_VERSION,
1448
- periods,
1449
- cache: DEFAULT_CACHE_MULTIPLIERS
1450
- });
1451
- var google = (periods) => ({
1452
- vendor: "google",
1453
- table: GOOGLE_PRICING_TABLE_VERSION,
1454
- periods,
1455
- cache: GOOGLE_CACHE_MULTIPLIERS
1456
- });
1457
- var flat = (input, output) => [
1458
- { from: null, to: null, input, output }
1459
- ];
1460
- var FREE = {
1461
- vendor: "local",
1462
- table: LOCAL_PRICING_TABLE_VERSION,
1463
- periods: flat(0, 0),
1464
- cache: FREE_CACHE_MULTIPLIERS
1465
- };
1466
- var PROVIDER_VENDOR = {
1467
- anthropic: "anthropic",
1468
- openai: "openai",
1469
- google: "google"
1470
- };
1471
- var LOCAL_PROVIDERS = /* @__PURE__ */ new Set([
1472
- "ollama",
1473
- "lmstudio",
1474
- "llama.cpp",
1475
- "llamacpp",
1476
- "local"
1477
- ]);
1478
- var PRICES = {
1479
- "claude-fable-5": anthropic(flat(10, 50)),
1480
- "claude-mythos-5": anthropic(flat(10, 50)),
1481
- "claude-opus-5": anthropic(flat(5, 25)),
1482
- "claude-opus-4-8": anthropic(flat(5, 25)),
1483
- "claude-opus-4-7": anthropic(flat(5, 25)),
1484
- "claude-opus-4-6": anthropic(flat(5, 25)),
1485
- "claude-sonnet-5": anthropic([
1486
- { from: null, to: SONNET_5_INTRO_ENDS_MS, input: 2, output: 10 },
1487
- { from: SONNET_5_INTRO_ENDS_MS, to: null, input: 3, output: 15 }
1488
- ]),
1489
- "claude-sonnet-4-6": anthropic(flat(3, 15)),
1490
- "claude-haiku-4-5": anthropic(flat(1, 5)),
1491
- // Fast mode (research preview) - Claude API only, Opus 5 / Opus 4.8 only.
1492
- // Opus 4.7 fast mode was removed, so there is deliberately no 4-7 entry.
1493
- "claude-opus-5#fast": anthropic(flat(10, 50)),
1494
- "claude-opus-4-8#fast": anthropic(flat(10, 50)),
1495
- // OpenAI (Codex) - standard-context tier (<272K; observed context window is
1496
- // 258,400).
1497
- "gpt-5.5": openai(flat(5, 30)),
1498
- "gpt-5.4": openai(flat(2.5, 15)),
1499
- "gpt-5.4-mini": openai(flat(0.75, 4.5)),
1500
- "gpt-5.3-codex": openai(flat(1.75, 14)),
1501
- // The gpt-5.6 family launched 2026-07-29; Terra and Luna were repriced on
1502
- // 2026-07-30 (-20% / -80%). The one-day launch rates are not on the list
1503
- // page and are NOT encoded - a July-29 Terra/Luna record underprices for
1504
- // one day rather than carrying a rate we cannot cite (#72).
1505
- "gpt-5.6-sol": openai(flat(5, 30)),
1506
- "gpt-5.6-terra": openai(flat(2, 12)),
1507
- "gpt-5.6-luna": openai(flat(0.2, 1.2)),
1508
- // NOT on OpenAI's list page - an internal Codex routing label with no
1509
- // official price (openai/codex#20981). Rate is the aggregator consensus
1510
- // ($2.50 / $15.00), scoped in explicitly by ticket #72 because it carries
1511
- // real token volume in Codex rollouts.
1512
- "codex-auto-review": openai(flat(2.5, 15)),
1513
- // Google (opencode, pi-mono) - Standard tier. Where a model is
1514
- // context-tiered, the <=200K rate is encoded, exactly as the OpenAI rows
1515
- // encode the standard-context tier: the payload carries no per-response
1516
- // context length, so the cheaper side keeps the figure a lower bound. Only
1517
- // the Pro and Flash families are here - image, TTS, embedding, Live and
1518
- // robotics models are not what a coding harness selects.
1519
- "gemini-3.1-pro-preview": google(flat(2, 12)),
1520
- "gemini-3.6-flash": google(flat(1.5, 7.5)),
1521
- "gemini-3.5-flash": google(flat(1.5, 9)),
1522
- "gemini-3-flash-preview": google(flat(0.5, 3)),
1523
- "gemini-2.5-pro": google(flat(1.25, 10)),
1524
- "gemini-2.5-flash": google(flat(0.3, 2.5)),
1525
- // RETIRED from Google's list page by 2026-08-09, and still the largest
1526
- // single block of Google tokens measured in #122 (10.9M, 20.5% of that
1527
- // machine's total). Encoded at its launch rate on the same footing as
1528
- // `codex-auto-review`: real volume, a rate we can name, and a note saying
1529
- // where it came from. Announcement rate, <=200K tier: $2.00 / $12.00.
1530
- "gemini-3-pro-preview": google(flat(2, 12)),
1531
- // A real Anthropic model with no row until #123. Measured in #122 as
1532
- // `claude-opus-4-5-20251101`, which the dated-suffix rule strips to this key.
1533
- // Same $5/$25 as the rest of the Opus 4 line on the 2026-07-25 list.
1534
- "claude-opus-4-5": anthropic(flat(5, 25))
1535
- };
1536
- function splitModelKey(modelKey) {
1537
- const at = modelKey.indexOf(PROVIDER_SEPARATOR);
1538
- if (at === -1) return { provider: null, model: modelKey };
1539
- return {
1540
- provider: modelKey.slice(0, at),
1541
- model: modelKey.slice(at + PROVIDER_SEPARATOR.length)
1542
- };
1543
- }
1544
- function modelKeyFor(provider, model) {
1545
- return `${provider}${PROVIDER_SEPARATOR}${model}`;
1546
- }
1547
- function entryFor(modelKey) {
1548
- const { provider, model } = splitModelKey(modelKey);
1549
- if (provider === null) return PRICES[model] ?? null;
1550
- if (LOCAL_PROVIDERS.has(provider)) return FREE;
1551
- const vendor = PROVIDER_VENDOR[provider];
1552
- if (!vendor) return null;
1553
- const entry = PRICES[model];
1554
- return entry && entry.vendor === vendor ? entry : null;
1555
- }
1556
- function normalizeModel(model) {
1557
- const { provider, model: bare } = splitModelKey(model);
1558
- const [base, suffix] = bare.split("#");
1559
- const stripped = base.replace(/-\d{8}$/, "");
1560
- const normalized = suffix ? `${stripped}#${suffix}` : stripped;
1561
- return provider === null ? normalized : modelKeyFor(provider, normalized);
1562
- }
1563
- function baseModelId(modelKey) {
1564
- return modelKey.split("#")[0];
1565
- }
1566
- function cacheMultipliersFor(modelKey) {
1567
- return entryFor(modelKey)?.cache ?? DEFAULT_CACHE_MULTIPLIERS;
1568
- }
1569
- function priceAt(modelKey, atMs) {
1570
- if (atMs === null) return null;
1571
- const entry = entryFor(modelKey);
1572
- if (!entry) return null;
1573
- for (const p8 of entry.periods) {
1574
- if ((p8.from === null || atMs >= p8.from) && (p8.to === null || atMs < p8.to)) {
1575
- return p8;
1576
- }
1577
- }
1578
- return null;
1579
- }
1580
- function isPricedModel(modelKey) {
1581
- return entryFor(modelKey) !== null;
1582
- }
1583
- function pricingTableFor(modelKey) {
1584
- return entryFor(modelKey)?.table ?? null;
1585
- }
1586
- function apiEquivalentCost(modelKey, t, atMs) {
1587
- const p8 = priceAt(modelKey, atMs);
1588
- if (!p8) return null;
1589
- const c = cacheMultipliersFor(modelKey);
1590
- const M = 1e6;
1591
- return (t.input * p8.input + t.output * p8.output + (t.cacheWrite5m + t.cacheWriteUnsplit) * p8.input * c.write5m + t.cacheWrite1h * p8.input * c.write1h + t.cacheRead * p8.input * c.read) / M;
1592
- }
1593
-
1594
1776
  // src/harness/shared/aggregate.ts
1595
1777
  var asObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
1596
1778
  var asStr = (v) => typeof v === "string" && v.length > 0 ? v : null;
@@ -2293,10 +2475,10 @@ function addPhaseTotals(into, from) {
2293
2475
  into[phase] += from[phase] ?? 0;
2294
2476
  }
2295
2477
  }
2296
- function sumBy(rows, key, add, clone) {
2478
+ function sumBy(rows, key2, add, clone) {
2297
2479
  const merged = /* @__PURE__ */ new Map();
2298
2480
  for (const row of rows) {
2299
- const k = key(row);
2481
+ const k = key2(row);
2300
2482
  const held = merged.get(k);
2301
2483
  if (held) add(held, row);
2302
2484
  else merged.set(k, clone(row));
@@ -2461,6 +2643,18 @@ function foldHarnessDays(days) {
2461
2643
  }
2462
2644
  return out;
2463
2645
  }
2646
+ var MAX_CHANGED_LINES_PER_COMMIT = 4096;
2647
+ function sampleSorted(values, max) {
2648
+ const sorted = [...values].sort((a, b) => a - b);
2649
+ if (sorted.length <= max) return sorted;
2650
+ const out = [];
2651
+ for (let i = 0; i < max; i++) {
2652
+ out.push(
2653
+ sorted[Math.floor(i * (sorted.length - 1) / (max - 1))]
2654
+ );
2655
+ }
2656
+ return out;
2657
+ }
2464
2658
  function foldGitDays(days) {
2465
2659
  const versions = (values) => [...new Set(values)].sort().join(" \xB7 ");
2466
2660
  return {
@@ -2471,7 +2665,10 @@ function foldGitDays(days) {
2471
2665
  lateNightCommits: days.reduce((sum, d) => sum + d.lateNightCommits, 0),
2472
2666
  additions: days.reduce((sum, d) => sum + d.additions, 0),
2473
2667
  removals: days.reduce((sum, d) => sum + d.removals, 0),
2474
- changedLinesPerCommit: days.flatMap((d) => [...d.changedLinesPerCommit]),
2668
+ changedLinesPerCommit: sampleSorted(
2669
+ days.flatMap((d) => [...d.changedLinesPerCommit]),
2670
+ MAX_CHANGED_LINES_PER_COMMIT
2671
+ ),
2475
2672
  testFileCommits: days.reduce((sum, d) => sum + d.testFileCommits, 0),
2476
2673
  changedLinesByExtension: sumBy(
2477
2674
  days.flatMap((d) => d.changedLinesByExtension),
@@ -3505,7 +3702,7 @@ function buildPayload(input) {
3505
3702
  [...agg.projectDirs].map((directory) => projectWorkspaceId(directory))
3506
3703
  )
3507
3704
  ].sort();
3508
- if (projectKeys.length > 1e3 || projectKeys.some((key) => !/^[A-Za-z0-9_-]{22}$/.test(key))) {
3705
+ if (projectKeys.length > 1e3 || projectKeys.some((key2) => !/^[A-Za-z0-9_-]{22}$/.test(key2))) {
3509
3706
  throw new Error(
3510
3707
  "Project workspace identifiers must be 22-character base64url strings"
3511
3708
  );
@@ -3678,8 +3875,8 @@ var emptyPhase = () => ({
3678
3875
  unknown: 0
3679
3876
  });
3680
3877
  var finiteNonnegative = (value) => value !== void 0 && Number.isFinite(value) && value > 0 ? value : 0;
3681
- var bump2 = (map, key, amount = 1) => {
3682
- map.set(key, (map.get(key) ?? 0) + amount);
3878
+ var bump2 = (map, key2, amount = 1) => {
3879
+ map.set(key2, (map.get(key2) ?? 0) + amount);
3683
3880
  };
3684
3881
  var utcDateOf2 = (ms) => new Date(ms).toISOString().slice(0, 10);
3685
3882
  var PHASE_RANK = {
@@ -3788,11 +3985,11 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3788
3985
  const webSearchesByDate = /* @__PURE__ */ new Map();
3789
3986
  const eventDates = /* @__PURE__ */ new Set();
3790
3987
  let finished;
3791
- const getSession = (key) => {
3792
- let state = sessions.get(key);
3988
+ const getSession = (key2) => {
3989
+ let state = sessions.get(key2);
3793
3990
  if (!state) {
3794
3991
  state = sessionState();
3795
- sessions.set(key, state);
3992
+ sessions.set(key2, state);
3796
3993
  }
3797
3994
  return state;
3798
3995
  };
@@ -3950,8 +4147,8 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3950
4147
  }
3951
4148
  for (const date of eventDates) {
3952
4149
  const day = dayOf(date);
3953
- for (const [key, events] of eventCells.get(date) ?? []) {
3954
- bump2(day.activity, key, events);
4150
+ for (const [key2, events] of eventCells.get(date) ?? []) {
4151
+ bump2(day.activity, key2, events);
3955
4152
  }
3956
4153
  if (harness !== "pi-mono") {
3957
4154
  day.hasWebSearches = true;
@@ -3980,12 +4177,12 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3980
4177
  { ts: child.lastTs ?? child.firstTs ?? 0, delta: -1 }
3981
4178
  ]);
3982
4179
  boundaries.sort((a, b) => a.ts - b.ts || b.delta - a.delta);
3983
- let active = 0;
4180
+ let active2 = 0;
3984
4181
  for (const boundary of boundaries) {
3985
- active += boundary.delta;
4182
+ active2 += boundary.delta;
3986
4183
  day.delegation.widestFanOut = Math.max(
3987
4184
  day.delegation.widestFanOut,
3988
- active
4185
+ active2
3989
4186
  );
3990
4187
  }
3991
4188
  }
@@ -4055,8 +4252,8 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
4055
4252
  }
4056
4253
  } : {},
4057
4254
  ...day.hasDelegation ? { delegation: day.delegation } : {},
4058
- activity: [...day.activity].map(([key, events]) => {
4059
- const [weekdayUtc, hourUtc] = key.split(":").map(Number);
4255
+ activity: [...day.activity].map(([key2, events]) => {
4256
+ const [weekdayUtc, hourUtc] = key2.split(":").map(Number);
4060
4257
  return {
4061
4258
  weekdayUtc: weekdayUtc ?? 0,
4062
4259
  hourUtc: hourUtc ?? 0,
@@ -5649,23 +5846,23 @@ function countUsage(agg, fold, rec, msgTsMs, usageRaw, modelKey, tsMs, priceable
5649
5846
  if (total === 0) return "none";
5650
5847
  const id = asStr(rec.id);
5651
5848
  if (id) {
5652
- const key2 = `${id}:${asStr(rec.timestamp) ?? ""}:${msgTsMs}:${total}`;
5653
- if (fold.seenUsage.has(key2)) {
5849
+ const key3 = `${id}:${asStr(rec.timestamp) ?? ""}:${msgTsMs}:${total}`;
5850
+ if (fold.seenUsage.has(key3)) {
5654
5851
  agg.continuationsFolded++;
5655
5852
  return "duplicate";
5656
5853
  }
5657
- fold.seenUsage.add(key2);
5854
+ fold.seenUsage.add(key3);
5658
5855
  } else {
5659
5856
  agg.unkeyedResponses++;
5660
5857
  }
5661
5858
  if (tsMs === null) agg.untimestampedResponses++;
5662
5859
  agg.distinctResponses++;
5663
- const key = modelKey ?? "(unknown)";
5860
+ const key2 = modelKey ?? "(unknown)";
5664
5861
  addModelUsage(
5665
5862
  agg,
5666
- key,
5863
+ key2,
5667
5864
  counts,
5668
- priceable ? apiEquivalentCost(key, counts, tsMs) : null,
5865
+ priceable ? apiEquivalentCost(key2, counts, tsMs) : null,
5669
5866
  1,
5670
5867
  { tsMs }
5671
5868
  );
@@ -6187,12 +6384,29 @@ function proposedMachineName(read = hostname) {
6187
6384
  return void 0;
6188
6385
  }
6189
6386
  }
6190
- async function performLogin() {
6387
+ function requestedMachineLabel(label) {
6388
+ const trimmed = label.trim();
6389
+ if (!isDisplaySafeName(trimmed)) {
6390
+ throw new Error(
6391
+ "Machine label must be 64 characters or fewer and contain only printable characters."
6392
+ );
6393
+ }
6394
+ return trimmed;
6395
+ }
6396
+ async function performLogin(options = {}) {
6191
6397
  const s = p5.spinner();
6192
6398
  s.start("Starting authentication...");
6193
6399
  let session;
6194
6400
  try {
6195
- session = await authStart(proposedMachineName());
6401
+ const requestedLabel = options.label === void 0 ? void 0 : requestedMachineLabel(options.label);
6402
+ session = await authStart(
6403
+ requestedLabel ?? proposedMachineName(),
6404
+ requestedLabel !== void 0,
6405
+ {
6406
+ ...options.replaceToken ? { replaceToken: options.replaceToken } : {},
6407
+ ...options.destinationRequired ? { destinationRequired: true } : {}
6408
+ }
6409
+ );
6196
6410
  s.stop("Session created");
6197
6411
  } catch (err) {
6198
6412
  s.stop("Failed to start authentication");
@@ -6234,9 +6448,9 @@ async function performLogin() {
6234
6448
  p5.log.error("Authentication timed out after 3 minutes. Please try again.");
6235
6449
  return false;
6236
6450
  }
6237
- async function loginCommand() {
6451
+ async function loginCommand(options = {}) {
6238
6452
  intro2("login");
6239
- if (!await performLogin()) {
6453
+ if (!await performLogin(options)) {
6240
6454
  outroError("error");
6241
6455
  process.exit(1);
6242
6456
  }
@@ -6430,6 +6644,7 @@ function autoSyncHookInstalled(file = CLAUDE_SETTINGS_FILE) {
6430
6644
  var NOT_LINKED = "This machine is not linked to an aistack account, and the auto-sync permission lives on your stack. Run `npx @use-aistack/cli sync` first.";
6431
6645
  var NOTHING_TO_TRIGGER = `No Claude Code or Codex session on this machine in the last ${DEFAULT_WINDOW_DAYS} days, so nothing would trigger an auto-sync. Nothing was changed.`;
6432
6646
  async function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {}) {
6647
+ frequencyHours = normalizeFrequencyHours(frequencyHours);
6433
6648
  const detected = await (deps.detectedImpl ?? detectedAdapters)();
6434
6649
  if (detected.length === 0) {
6435
6650
  return { ok: false, message: NOTHING_TO_TRIGGER };
@@ -6480,7 +6695,9 @@ async function disableAutoSync(deps = {}) {
6480
6695
  autoSyncAnswered: true,
6481
6696
  autoSync: {
6482
6697
  enabled: false,
6483
- frequencyHours: settings.autoSync?.frequencyHours ?? DEFAULT_FREQUENCY_HOURS
6698
+ frequencyHours: normalizeFrequencyHours(
6699
+ settings.autoSync?.frequencyHours
6700
+ )
6484
6701
  }
6485
6702
  },
6486
6703
  deps.settingsFile
@@ -6568,20 +6785,20 @@ async function offerAutoSyncOptIn(deps = {}) {
6568
6785
  const detected = await (deps.detectedImpl ?? detectedAdapters)();
6569
6786
  if (detected.length === 0) return false;
6570
6787
  const answer = await p6.select({
6571
- message: "Keep this stack fresh automatically?",
6788
+ message: "Keep this stack fresh automatically every 6 hours?",
6572
6789
  options: [
6573
- {
6574
- value: "later",
6575
- label: "Not now",
6576
- hint: "this question will not come back"
6577
- },
6578
6790
  {
6579
6791
  value: "enable",
6580
6792
  label: "Enable",
6581
- hint: `a silent daily sync when a ${harnessListLabel(detected)} session starts`
6793
+ hint: `a silent sync at most every 6 hours when a ${harnessListLabel(detected)} session starts`
6794
+ },
6795
+ {
6796
+ value: "later",
6797
+ label: "No thanks",
6798
+ hint: "you can enable it later"
6582
6799
  }
6583
6800
  ],
6584
- initialValue: "later"
6801
+ initialValue: "enable"
6585
6802
  });
6586
6803
  if (p6.isCancel(answer)) return true;
6587
6804
  if (answer === "enable") {
@@ -7046,8 +7263,8 @@ function extractGitWorkflow(options) {
7046
7263
  date,
7047
7264
  ...rest,
7048
7265
  changedLinesByExtension: [...extensionLines].map(([extension, changedLines]) => ({ extension, changedLines })).sort((a, b) => a.extension.localeCompare(b.extension)),
7049
- weekdayHourCells: [...cells].map(([key, commits]) => {
7050
- const [weekdayUtc, hourUtc] = key.split(":").map(Number);
7266
+ weekdayHourCells: [...cells].map(([key2, commits]) => {
7267
+ const [weekdayUtc, hourUtc] = key2.split(":").map(Number);
7051
7268
  return {
7052
7269
  weekdayUtc: weekdayUtc ?? 0,
7053
7270
  hourUtc: hourUtc ?? 0,
@@ -7254,10 +7471,18 @@ function payloadBlock(payload, width, ownWindow, stats) {
7254
7471
  `${days} active day${days === 1 ? "" : "s"}`,
7255
7472
  `${fmtTokens(payload.activity.totalTokens)} tokens`
7256
7473
  ];
7257
- out.push(...wrapRow("", " ", `- ${label} \xB7 ${totals.join(" \xB7 ")}`, width));
7258
- if (payload.activity.totalTokens === 0) return out;
7474
+ out.push(`${label.toUpperCase()} ${payload.activity.sessions}`);
7475
+ if (payload.activity.totalTokens === 0) {
7476
+ out.push(`usage ${totals.slice(1).join(" \xB7 ")}`);
7477
+ return out;
7478
+ }
7259
7479
  out.push(
7260
- usd === null ? "cost not published" : `cost ${fmtUSD(usd)} at API prices`
7480
+ ...wrapRow(
7481
+ "usage ",
7482
+ " ".repeat(LABEL_WIDTH),
7483
+ `${totals.slice(1).join(" \xB7 ")} \xB7 ${usd === null ? "cost not published" : `${fmtUSD(usd)} at API prices`}`,
7484
+ width
7485
+ )
7261
7486
  );
7262
7487
  if (ownWindow) {
7263
7488
  out.push(
@@ -7273,24 +7498,29 @@ function payloadBlock(payload, width, ownWindow, stats) {
7273
7498
  if (stats) {
7274
7499
  out.push(...scanNoteLines(stats, harnessLabel2(payload.harness.name)));
7275
7500
  }
7276
- const indent = " ".repeat(LABEL_WIDTH);
7277
7501
  const shown = payload.models.filter((m) => m.tokenShare >= MODEL_ROLLUP);
7278
7502
  const rolled = payload.models.filter((m) => m.tokenShare < MODEL_ROLLUP);
7279
- const modelWidth = Math.max(0, ...shown.map((m) => m.id.length), 8);
7280
- const row = (i, name, share, dollars) => `${i === 0 ? "models".padEnd(LABEL_WIDTH) : indent}${name.padEnd(modelWidth)} ${fmtPct(share).padStart(5)}${dollars}`;
7281
- shown.forEach((m, i) => {
7282
- const dollars = usd !== null && m.apiEquivalentUSD !== void 0 ? ` ${fmtUSD(m.apiEquivalentUSD)}` : "";
7283
- out.push(row(i, m.id, m.tokenShare, dollars));
7284
- });
7503
+ const entry = (name, share, dollars) => `${name} ${fmtPct(share)}${usd !== null && dollars !== void 0 ? ` ${fmtUSD(dollars)}` : ""}`;
7504
+ const entries = shown.map(
7505
+ (m) => entry(m.id, m.tokenShare, m.apiEquivalentUSD)
7506
+ );
7285
7507
  if (rolled.length > 0) {
7286
7508
  const priced = rolled.every((m) => m.apiEquivalentUSD !== void 0);
7287
- const sum = rolled.reduce((a, m) => a + (m.apiEquivalentUSD ?? 0), 0);
7288
- out.push(
7289
- row(
7290
- shown.length,
7509
+ entries.push(
7510
+ entry(
7291
7511
  `+${rolled.length} more`,
7292
7512
  rolled.reduce((a, m) => a + m.tokenShare, 0),
7293
- usd !== null && priced ? ` ${fmtUSD(sum)}` : ""
7513
+ priced ? rolled.reduce((a, m) => a + (m.apiEquivalentUSD ?? 0), 0) : void 0
7514
+ )
7515
+ );
7516
+ }
7517
+ if (entries.length > 0) {
7518
+ out.push(
7519
+ ...wrapRow(
7520
+ "models ",
7521
+ " ".repeat(LABEL_WIDTH),
7522
+ entries.join(" \xB7 "),
7523
+ width
7294
7524
  )
7295
7525
  );
7296
7526
  }
@@ -7323,6 +7553,7 @@ function payloadBlock(payload, width, ownWindow, stats) {
7323
7553
  }
7324
7554
  return out;
7325
7555
  }
7556
+ var DIVIDER = "\u2500".repeat(40);
7326
7557
  var MODEL_ROLLUP = 0.01;
7327
7558
  var PHASE_ORDER = ["scout", "build", "verify", "handoff", "unknown"];
7328
7559
  function workflowBlock(workflowDays, utcOffsetMinutes, host) {
@@ -7385,8 +7616,6 @@ function buildGateSummary(ctx) {
7385
7616
  const { payloads } = body;
7386
7617
  const host = baseUrl.replace(/^https?:\/\//, "");
7387
7618
  const out = [];
7388
- out.push("from your machine \xB7 sync preview");
7389
- out.push("");
7390
7619
  if (config.stack === null) {
7391
7620
  out.push("to (no linked stack; publish is unavailable)");
7392
7621
  } else {
@@ -7395,26 +7624,39 @@ function buildGateSummary(ctx) {
7395
7624
  );
7396
7625
  }
7397
7626
  out.push(
7398
- `searched ${HARNESS_ADAPTERS.map((a) => harnessLabel2(a.name).toLowerCase()).join(", ")}${body.cliVersion ? ` \xB7 aistack ${body.cliVersion}` : ""}`
7627
+ `searched ${HARNESS_ADAPTERS.map((a) => harnessLabel2(a.name).toLowerCase()).join(", ")}`
7399
7628
  );
7400
7629
  const windows = new Set(
7401
7630
  payloads.map(
7402
7631
  (p8) => `${p8.window.days} days \xB7 ${p8.window.from} \u2192 ${p8.window.to}`
7403
7632
  )
7404
7633
  );
7405
- if (windows.size === 1) out.push(`window ${[...windows][0]}`);
7634
+ if (windows.size === 1) {
7635
+ out.push(
7636
+ `window ${[...windows][0]}${body.cliVersion ? ` \xB7 aistack ${body.cliVersion}` : ""}`
7637
+ );
7638
+ }
7639
+ const cited = [
7640
+ ...new Set(
7641
+ payloads.flatMap(
7642
+ (p8) => p8.models.flatMap((m) => m.pricingTable ? [m.pricingTable] : [])
7643
+ )
7644
+ )
7645
+ ];
7646
+ if (ctx.prices && cited.length > 0) {
7647
+ const origin = ctx.prices.origin === "served" ? `${ctx.prices.id} from ${host}` : `${ctx.prices.id} (bundled; the server table was unavailable)`;
7648
+ out.push(`prices ${origin} \xB7 cites ${cited.join(", ")}`);
7649
+ }
7406
7650
  const width = wrapWidth(ctx.width);
7407
7651
  for (const payload of payloads) {
7408
7652
  const stats = ctx.scanStats?.[payload.harness.name];
7409
- out.push("");
7653
+ out.push("", DIVIDER, "");
7410
7654
  out.push(...payloadBlock(payload, width, windows.size > 1, stats));
7411
7655
  }
7412
- if (out[out.length - 1] === "") out.pop();
7656
+ out.push("", DIVIDER, "", "ALSO PUBLISHING");
7413
7657
  if (body.measuredDays) {
7414
- out.push("");
7415
7658
  out.push(...daysBlock(body.measuredDays, ctx.days));
7416
7659
  }
7417
- out.push("");
7418
7660
  const workflowDays = (body.measuredDays?.days ?? []).flatMap(
7419
7661
  (d) => d.workflow ? [d.workflow] : []
7420
7662
  );
@@ -7429,28 +7671,23 @@ function buildGateSummary(ctx) {
7429
7671
  }
7430
7672
  const n = payloads.reduce((a, p8) => a + withheldCount(p8), 0);
7431
7673
  if (n > 0) {
7432
- out.push("");
7433
- out.push(`kept private: ${n} name${n === 1 ? "" : "s"}`);
7434
7674
  const rows = keptPrivateRows(keptPrivate);
7435
7675
  const shown = rows.slice(0, KEPT_PRIVATE_ROWS_SHOWN);
7436
- const width2 = Math.max(...shown.map((r) => r.label.length));
7437
- for (const row of shown) {
7438
- out.push(` ${row.label.padEnd(width2)} ${row.names}`);
7439
- }
7440
- if (rows.length > shown.length) {
7441
- out.push(` ...${rows.length - shown.length} more`);
7442
- }
7676
+ const examples = shown.map((r) => r.names > 1 ? `${r.label} \xD7${r.names}` : r.label).join(", ");
7677
+ const more = rows.length > shown.length ? `, ...${rows.length - shown.length} more` : "";
7678
+ out.push(`private ${n} name${n === 1 ? "" : "s"} \xB7 ${examples}${more}`);
7443
7679
  if (body.keptPrivate !== void 0 && config.stack !== null) {
7444
- out.push(` publish them at ${host}/stacks/${config.stack.slug}/changes`);
7445
7680
  out.push(
7446
- " (they go up for you to review - turn off: Review kept-private names, on your stack)"
7681
+ ` they go up for you to review at ${host}/stacks/${config.stack.slug}/changes`
7682
+ );
7683
+ out.push(
7684
+ " (turn off: Review kept-private names, on your stack)"
7447
7685
  );
7448
7686
  } else {
7449
- out.push(" they stay on this machine");
7687
+ out.push(" they stay on this machine");
7450
7688
  }
7451
7689
  }
7452
7690
  if (body.autoSync !== void 0) {
7453
- out.push("");
7454
7691
  out.push(
7455
7692
  `auto-sync ${body.autoSync.enabled ? `on, about every ${body.autoSync.frequencyHours}h` : "off"}`
7456
7693
  );
@@ -7480,6 +7717,22 @@ async function stageSync(deps) {
7480
7717
  const windowDays = deps.windowDays ?? DEFAULT_WINDOW_DAYS;
7481
7718
  const projectWorkspaceId = deps.getProjectWorkspaceIdImpl ?? getProjectWorkspaceId;
7482
7719
  const fetchManifest = deps.fetchManifestImpl ?? fetchDayManifest;
7720
+ const fetchPrices = deps.fetchPricesImpl ?? fetchPriceTable;
7721
+ let prices = {
7722
+ id: BUNDLED_PRICE_TABLE_ID,
7723
+ origin: "bundled"
7724
+ };
7725
+ try {
7726
+ const table = await fetchPrices(deps.baseUrl);
7727
+ if (table) {
7728
+ setActivePricer(layeredPricer(table));
7729
+ prices = { id: table.id, origin: "served" };
7730
+ } else {
7731
+ setActivePricer(null);
7732
+ }
7733
+ } catch {
7734
+ setActivePricer(null);
7735
+ }
7483
7736
  const { config, source } = await loadConfig({
7484
7737
  baseUrl: deps.baseUrl,
7485
7738
  ...token ? { token } : {}
@@ -7502,8 +7755,8 @@ async function stageSync(deps) {
7502
7755
  const usageScans = [];
7503
7756
  const sinceMs = windowStartMs(now, windowDays);
7504
7757
  const daysSinceMs = windowStartMs(now, retentionDays);
7505
- const active = await adapters(sinceMs);
7506
- for (const adapter of active) {
7758
+ const active2 = await adapters(sinceMs);
7759
+ for (const adapter of active2) {
7507
7760
  const { aggregate, stats } = await adapter.scan({ sinceMs });
7508
7761
  scanStats[adapter.name] = stats;
7509
7762
  built.push(
@@ -7519,7 +7772,7 @@ async function stageSync(deps) {
7519
7772
  })
7520
7773
  );
7521
7774
  }
7522
- for (const adapter of active) {
7775
+ for (const adapter of active2) {
7523
7776
  const { aggregate, workflow: workflow2, workflowLocal } = await adapter.scan({
7524
7777
  sinceMs: daysSinceMs
7525
7778
  });
@@ -7559,7 +7812,7 @@ async function stageSync(deps) {
7559
7812
  config,
7560
7813
  settings.autoSync,
7561
7814
  deps.trigger,
7562
- active.length > 0 ? {
7815
+ active2.length > 0 ? {
7563
7816
  aggregateVersion: MEASURED_DAYS_V1,
7564
7817
  utcOffsetMinutes: workflow?.utcOffsetMinutes ?? machineUtcOffsetMinutes(),
7565
7818
  days: days.send
@@ -7576,6 +7829,7 @@ async function stageSync(deps) {
7576
7829
  baseUrl: deps.baseUrl,
7577
7830
  scanStats,
7578
7831
  days,
7832
+ prices,
7579
7833
  // The real terminal, so the inventory rows break where this window ends
7580
7834
  // (#217). A pipe reports nothing and the preview falls back to 80.
7581
7835
  width: process.stdout.columns
@@ -7586,7 +7840,7 @@ async function stageSync(deps) {
7586
7840
  } else if (token === null) {
7587
7841
  blockedReason = "This machine is not linked. Run `npx @use-aistack/cli login` first.";
7588
7842
  } else if (config.stack === null) {
7589
- 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.`;
7843
+ 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." : "This machine has no destination stack. Run `npx @use-aistack/cli sync` in an interactive terminal to choose one.";
7590
7844
  }
7591
7845
  return {
7592
7846
  id: stageId(bodyJson),
@@ -7599,7 +7853,8 @@ async function stageSync(deps) {
7599
7853
  token,
7600
7854
  stagedAt: now,
7601
7855
  blockedReason,
7602
- days
7856
+ days,
7857
+ prices
7603
7858
  };
7604
7859
  }
7605
7860
 
@@ -7631,7 +7886,7 @@ async function runAutoSync(deps) {
7631
7886
  appendLogLine(logFile, `${stamp} skipped - auto-sync is not enabled`);
7632
7887
  return;
7633
7888
  }
7634
- const frequencyHours = config.frequencyHours || DEFAULT_FREQUENCY_HOURS;
7889
+ const frequencyHours = normalizeFrequencyHours(config.frequencyHours);
7635
7890
  const state = settings.autoSyncState ?? {};
7636
7891
  const lastRunAt = state.lastRunAt ?? 0;
7637
7892
  if (now - lastRunAt < frequencyHours * 36e5) return;
@@ -7650,6 +7905,18 @@ async function runAutoSync(deps) {
7650
7905
  if (loaded.config.autoSync?.enabled === false) {
7651
7906
  revoked = true;
7652
7907
  } else {
7908
+ const serverFrequency = loaded.config.autoSync?.frequencyHours;
7909
+ if (loaded.config.autoSync?.enabled === true && serverFrequency !== void 0 && normalizeFrequencyHours(serverFrequency) !== frequencyHours) {
7910
+ saveSettings(
7911
+ {
7912
+ autoSync: {
7913
+ enabled: true,
7914
+ frequencyHours: normalizeFrequencyHours(serverFrequency)
7915
+ }
7916
+ },
7917
+ settingsFile
7918
+ );
7919
+ }
7653
7920
  const staged = await stage({
7654
7921
  baseUrl: deps.baseUrl,
7655
7922
  now: () => now,
@@ -7761,21 +8028,70 @@ async function syncCommand(options = {}) {
7761
8028
  process.exitCode = 1;
7762
8029
  return;
7763
8030
  }
7764
- if (getToken() === null) {
8031
+ let token = getToken();
8032
+ if (token === null) {
8033
+ p7.log.message("This machine needs a destination stack. Linking it now.");
8034
+ if (!await performLogin({ destinationRequired: true })) {
8035
+ outroError("login failed. Nothing was sent.");
8036
+ process.exitCode = 1;
8037
+ return;
8038
+ }
8039
+ token = getToken();
8040
+ }
8041
+ if (token === null) {
8042
+ outroError("login completed without a saved credential. Nothing was sent.");
8043
+ process.exitCode = 1;
8044
+ return;
8045
+ }
8046
+ let loaded = await loadSyncConfig({ baseUrl: BASE_URL, token });
8047
+ if (loaded.source === "bundled") {
8048
+ outroError(
8049
+ "Could not fetch your settings from aistack, so the destination stack is unknown. Check the network and sync again."
8050
+ );
8051
+ process.exitCode = 1;
8052
+ return;
8053
+ }
8054
+ if (loaded.config.stack === null) {
7765
8055
  p7.log.message(
7766
- "This machine is not linked to an aistack account yet. Linking it now."
8056
+ "This machine is not linked to a destination stack. Opening aistack so you can choose one."
7767
8057
  );
7768
- if (!await performLogin()) {
7769
- outroError("login failed. Nothing was sent.");
8058
+ if (!await performLogin({
8059
+ destinationRequired: true,
8060
+ replaceToken: token
8061
+ })) {
8062
+ outroError("linking failed. Nothing was sent.");
8063
+ process.exitCode = 1;
8064
+ return;
8065
+ }
8066
+ token = getToken();
8067
+ if (token === null) {
8068
+ outroError(
8069
+ "linking completed without a saved credential. Nothing was sent."
8070
+ );
8071
+ process.exitCode = 1;
8072
+ return;
8073
+ }
8074
+ loaded = await loadSyncConfig({ baseUrl: BASE_URL, token });
8075
+ if (loaded.source === "bundled" || loaded.config.stack === null) {
8076
+ outroError(
8077
+ "The destination stack could not be confirmed. Nothing was sent."
8078
+ );
7770
8079
  process.exitCode = 1;
7771
8080
  return;
7772
8081
  }
8082
+ p7.log.success(`Linked this machine to ${loaded.config.stack.name}`);
7773
8083
  }
8084
+ const destinationToken = token;
8085
+ const destinationConfig = loaded;
7774
8086
  const s = p7.spinner();
7775
8087
  s.start("Scanning local agent transcripts");
7776
8088
  let staged;
7777
8089
  try {
7778
- staged = await stageSync({ baseUrl: BASE_URL });
8090
+ staged = await stageSync({
8091
+ baseUrl: BASE_URL,
8092
+ getTokenImpl: () => destinationToken,
8093
+ loadConfigImpl: async () => destinationConfig
8094
+ });
7779
8095
  } catch (e) {
7780
8096
  s.stop("Scan failed");
7781
8097
  outroError(e instanceof Error ? e.message : String(e));
@@ -7783,7 +8099,7 @@ async function syncCommand(options = {}) {
7783
8099
  return;
7784
8100
  }
7785
8101
  s.stop("Scan complete");
7786
- p7.log.message(staged.summary.split("\n").join("\n"));
8102
+ p7.log.message(staged.summary.split("\n").map(styleSummaryLine).join("\n"));
7787
8103
  if (staged.blockedReason !== null) {
7788
8104
  outroError(staged.blockedReason);
7789
8105
  process.exitCode = 1;
@@ -7830,6 +8146,21 @@ async function syncCommand(options = {}) {
7830
8146
  process.exitCode = 1;
7831
8147
  }
7832
8148
  }
8149
+ function styleSummaryLine(line) {
8150
+ if (line.startsWith("\u2500")) return dim(line);
8151
+ const section2 = /^([A-Z][A-Z0-9 .-]+?)( \d+)?$/.exec(line);
8152
+ if (section2) return `${bold(section2[1] ?? "")}${dim(section2[2] ?? "")}`;
8153
+ const labelled = /^([a-z-]+)( +)(.*)$/.exec(line);
8154
+ if (labelled) {
8155
+ const [, label = "", gap = "", rest = ""] = labelled;
8156
+ const body = label === "skipped" ? yellow(rest) : rest.replace(/≈\$[\d,]+/g, (m) => lime(m));
8157
+ return `${dim(label)}${gap}${body}`;
8158
+ }
8159
+ const sub = /^( {2}[a-z]+ +)(.*)$/.exec(line);
8160
+ if (sub) return `${lime(sub[1] ?? "")}${dim(sub[2] ?? "")}`;
8161
+ if (/^ {10}\S/.test(line)) return dim(line);
8162
+ return line;
8163
+ }
7833
8164
 
7834
8165
  // src/sync/server.ts
7835
8166
  var SERVER_NAME = "aistack";
@@ -8085,7 +8416,7 @@ function runStdioSyncServer(deps) {
8085
8416
  // src/index.ts
8086
8417
  var program = new Command();
8087
8418
  program.name("aistack").description("Measure and share your AI stack from your terminal").version(CLI_VERSION);
8088
- program.command("login").description("Authenticate with AI Stack").action(loginCommand);
8419
+ program.command("login").description("Authenticate with AI Stack").option("--label <label>", "Set the machine label").action((options) => loginCommand(options));
8089
8420
  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 }));
8090
8421
  program.command("create").description("Download and write your stack's AI config files").action(createCommand);
8091
8422
  program.command("mcp").description(
@@ -8102,7 +8433,7 @@ program.command("sync").description("Scan, preview, and publish measured usage (
8102
8433
  "silent background sync; 'on' asks your stack for the permission and installs the SessionStart hooks, 'off' revokes both"
8103
8434
  ).option(
8104
8435
  "--every <hours>",
8105
- "with --auto on: hours between auto-syncs (default 24)"
8436
+ "with --auto on: hours between auto-syncs (default 6)"
8106
8437
  ).action((options) => syncCommand(options));
8107
8438
  program.command("connect").description("Install the in-session sync surface (MCP server + Skill)").argument("<harness>", 'the harness to connect ("claude")').action(connectCommand);
8108
8439
  program.parse();