@use-aistack/cli 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.1" : "0.0.0-dev";
351
+ var CLI_VERSION = true ? "0.11.0" : "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
  );
@@ -6005,7 +6202,7 @@ ${e instanceof Error ? e.message : String(e)}`
6005
6202
  }
6006
6203
  return {
6007
6204
  ok: true,
6008
- message: `Installed. Say ${limeBold('"sync my stack"')} in any Claude Code session.`
6205
+ message: `Installed the user-scoped aistack MCP server and Skill. Say ${limeBold('"sync my stack"')} in any Claude Code session. Every send still requires your confirmation. Remove it with: claude mcp remove --scope user aistack`
6009
6206
  };
6010
6207
  }
6011
6208
  async function connectCommand(harness) {
@@ -6041,17 +6238,17 @@ async function offerConnectUpsell(deps = {}) {
6041
6238
  if (!await (deps.claudeActiveImpl ?? claudeRecentlyActive)()) return;
6042
6239
  if (!(deps.claudeOnPathImpl ?? claudeOnPath)()) return;
6043
6240
  const answer = await p3.select({
6044
- message: "Sync from inside Claude Code too?",
6241
+ message: "Add AI Stack commands to Claude Code? Every send still asks first.",
6045
6242
  options: [
6046
6243
  {
6047
6244
  value: "later",
6048
- label: "Not now",
6049
- hint: "this question will not come back"
6245
+ label: "No, don't ask again",
6246
+ hint: "you can install later with aistack connect claude"
6050
6247
  },
6051
6248
  {
6052
6249
  value: "install",
6053
- label: "Install",
6054
- hint: "adds the aistack MCP server + Skill to Claude Code"
6250
+ label: "Install MCP + Skill",
6251
+ hint: "user scope; enables preview and confirmed send commands"
6055
6252
  }
6056
6253
  ],
6057
6254
  initialValue: "later"
@@ -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
@@ -6564,24 +6781,30 @@ async function settleAutoSync(permission, deps = {}) {
6564
6781
  return offerAutoSyncOptIn(deps);
6565
6782
  }
6566
6783
  async function offerAutoSyncOptIn(deps = {}) {
6567
- if (getSettings(deps.settingsFile).autoSyncAnswered === true) return false;
6784
+ if (getSettings(deps.settingsFile).autoSyncNeverAskAgain === true)
6785
+ return false;
6568
6786
  const detected = await (deps.detectedImpl ?? detectedAdapters)();
6569
6787
  if (detected.length === 0) return false;
6570
6788
  const answer = await p6.select({
6571
- message: "Keep this stack fresh automatically?",
6789
+ message: "Keep this stack fresh automatically every 6 hours?",
6572
6790
  options: [
6791
+ {
6792
+ value: "enable",
6793
+ label: "Enable",
6794
+ hint: `a silent sync at most every 6 hours when a ${harnessListLabel(detected)} session starts`
6795
+ },
6573
6796
  {
6574
6797
  value: "later",
6575
- label: "Not now",
6576
- hint: "this question will not come back"
6798
+ label: "Maybe later",
6799
+ hint: "ask again after your next manual sync"
6577
6800
  },
6578
6801
  {
6579
- value: "enable",
6580
- label: "Enable",
6581
- hint: `a silent daily sync when a ${harnessListLabel(detected)} session starts`
6802
+ value: "never",
6803
+ label: "Never ask again",
6804
+ hint: "you can still enable it with sync --auto on"
6582
6805
  }
6583
6806
  ],
6584
- initialValue: "later"
6807
+ initialValue: "enable"
6585
6808
  });
6586
6809
  if (p6.isCancel(answer)) return true;
6587
6810
  if (answer === "enable") {
@@ -6596,7 +6819,9 @@ async function offerAutoSyncOptIn(deps = {}) {
6596
6819
  }
6597
6820
  return true;
6598
6821
  }
6599
- saveSettings({ autoSyncAnswered: true }, deps.settingsFile);
6822
+ if (answer === "never") {
6823
+ saveSettings({ autoSyncNeverAskAgain: true }, deps.settingsFile);
6824
+ }
6600
6825
  p6.log.message(
6601
6826
  `If you change your mind: ${limeBold("npx @use-aistack/cli sync --auto on")} ${dim(
6602
6827
  "(and --auto off to revoke)"
@@ -6765,7 +6990,7 @@ function selectDaysToPublish(input) {
6765
6990
  }
6766
6991
 
6767
6992
  // src/workflow/git.ts
6768
- import { execFileSync as execFileSync2 } from "child_process";
6993
+ import { execFile, execFileSync as execFileSync2 } from "child_process";
6769
6994
  import path6 from "path";
6770
6995
  var TEST_FILE_RULE_VERSION = "test-files/v2";
6771
6996
  var FILE_TYPE_RULE_VERSION = "file-types/v2";
@@ -6782,6 +7007,18 @@ var defaultRunner = (cwd, args) => {
6782
7007
  return null;
6783
7008
  }
6784
7009
  };
7010
+ var defaultAsyncRunner = (cwd, args) => new Promise((resolve) => {
7011
+ execFile(
7012
+ "git",
7013
+ [...args],
7014
+ {
7015
+ cwd,
7016
+ encoding: "utf8",
7017
+ maxBuffer: 64 * 1024 * 1024
7018
+ },
7019
+ (error, stdout) => resolve(error ? null : stdout)
7020
+ );
7021
+ });
6785
7022
  var emptyGitDay = () => ({
6786
7023
  testFileRuleVersion: TEST_FILE_RULE_VERSION,
6787
7024
  fileTypeRuleVersion: FILE_TYPE_RULE_VERSION,
@@ -6936,6 +7173,39 @@ function extractGitWorkflow(options) {
6936
7173
  const root = run(directory, ["rev-parse", "--show-toplevel"])?.trim();
6937
7174
  if (root) roots.add(root);
6938
7175
  }
7176
+ const histories = [];
7177
+ for (const root of roots) {
7178
+ const history = run(root, gitLogArgs());
7179
+ if (history) histories.push(history);
7180
+ }
7181
+ return reduceGitHistories(histories, options);
7182
+ }
7183
+ async function extractGitWorkflowAsync(options) {
7184
+ const run = options.run ?? defaultAsyncRunner;
7185
+ const roots = /* @__PURE__ */ new Set();
7186
+ for (const directory of options.workingDirectories) {
7187
+ const root = (await run(directory, ["rev-parse", "--show-toplevel"]))?.trim();
7188
+ if (root) roots.add(root);
7189
+ }
7190
+ const histories = await Promise.all(
7191
+ [...roots].map((root) => run(root, gitLogArgs()))
7192
+ );
7193
+ return reduceGitHistories(
7194
+ histories.filter((history) => history !== null),
7195
+ options
7196
+ );
7197
+ }
7198
+ function gitLogArgs() {
7199
+ return [
7200
+ "log",
7201
+ "--all",
7202
+ "--no-merges",
7203
+ `--format=%x00${COMMIT_MARKER}%x00%H%x00%aI%x00`,
7204
+ "--numstat",
7205
+ "-z"
7206
+ ];
7207
+ }
7208
+ function reduceGitHistories(histories, options) {
6939
7209
  const days = /* @__PURE__ */ new Map();
6940
7210
  const dayOf = (date) => {
6941
7211
  let day = days.get(date);
@@ -6956,16 +7226,7 @@ function extractGitWorkflow(options) {
6956
7226
  return day;
6957
7227
  };
6958
7228
  const seenCommits = /* @__PURE__ */ new Set();
6959
- for (const root of roots) {
6960
- const history = run(root, [
6961
- "log",
6962
- "--all",
6963
- "--no-merges",
6964
- `--format=%x00${COMMIT_MARKER}%x00%H%x00%aI%x00`,
6965
- "--numstat",
6966
- "-z"
6967
- ]);
6968
- if (!history) continue;
7229
+ for (const history of histories) {
6969
7230
  let current;
6970
7231
  const finishCommit = () => {
6971
7232
  if (!current?.included || !current.authored) return;
@@ -7046,8 +7307,8 @@ function extractGitWorkflow(options) {
7046
7307
  date,
7047
7308
  ...rest,
7048
7309
  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);
7310
+ weekdayHourCells: [...cells].map(([key2, commits]) => {
7311
+ const [weekdayUtc, hourUtc] = key2.split(":").map(Number);
7051
7312
  return {
7052
7313
  weekdayUtc: weekdayUtc ?? 0,
7053
7314
  hourUtc: hourUtc ?? 0,
@@ -7075,6 +7336,18 @@ function extractLocalWorkflow(options) {
7075
7336
  });
7076
7337
  return buildWorkflowExtraction(options.harnesses, git, utcOffsetMinutes);
7077
7338
  }
7339
+ async function extractLocalWorkflowAsync(options) {
7340
+ const utcOffsetMinutes = options.utcOffsetMinutes ?? machineUtcOffsetMinutes();
7341
+ const git = await extractGitWorkflowAsync({
7342
+ workingDirectories: options.harnesses.flatMap(({ local }) => [
7343
+ ...local.projectWorkspaces
7344
+ ]),
7345
+ fromMs: options.fromMs,
7346
+ toMs: options.toMs,
7347
+ utcOffsetMinutes
7348
+ });
7349
+ return buildWorkflowExtraction(options.harnesses, git, utcOffsetMinutes);
7350
+ }
7078
7351
  function machineUtcOffsetMinutes(now = /* @__PURE__ */ new Date()) {
7079
7352
  return -now.getTimezoneOffset();
7080
7353
  }
@@ -7161,20 +7434,21 @@ function buildGateDialog(ctx) {
7161
7434
  const days = payloads[0]?.window.days ?? 0;
7162
7435
  const facts = [
7163
7436
  `${fmtTokens(tokens)} tokens`,
7164
- `${days} days`,
7437
+ `${days}-day profile`,
7438
+ ...ctx.body.measuredDays && ctx.body.measuredDays.days.length !== days ? [`${ctx.body.measuredDays.days.length} historical days`] : [],
7165
7439
  ...usd === null ? [] : [fmtUSD(usd)]
7166
7440
  ].join(" \xB7 ");
7167
7441
  const n = payloads.reduce((a, p8) => a + withheldCount(p8), 0);
7168
7442
  const lines2 = [`Publish to aistack? ${facts}`];
7169
7443
  if (n > 0) {
7170
7444
  lines2.push(
7171
- keptPrivate === void 0 ? `${n} name${n === 1 ? "" : "s"} stay${n === 1 ? "s" : ""} on this machine` : `${n} name${n === 1 ? "" : "s"} go${n === 1 ? "es" : ""} up for you to review`
7445
+ keptPrivate === void 0 ? `${n} name${n === 1 ? "" : "s"} stay${n === 1 ? "s" : ""} on this machine` : `${n} private review name${n === 1 ? "" : "s"} will be stored`
7172
7446
  );
7173
7447
  }
7174
7448
  return lines2.join("\n");
7175
7449
  }
7176
7450
  var CATEGORY_LABEL = {
7177
- builtinTools: "tools",
7451
+ builtinTools: "actions",
7178
7452
  mcpServers: "mcp",
7179
7453
  skills: "skills",
7180
7454
  subagents: "agents",
@@ -7204,6 +7478,24 @@ function wrapRow(head, continuation, text, width) {
7204
7478
  if (line !== "") lines2.push(line);
7205
7479
  return lines2.map((l, i) => (i === 0 ? head : continuation) + l);
7206
7480
  }
7481
+ function wrapEntries(head, continuation, entries, width) {
7482
+ const limit = Math.max(24, width - continuation.length);
7483
+ const lines2 = [];
7484
+ let line = "";
7485
+ for (const entry of entries) {
7486
+ const next = line ? `${line} \xB7 ${entry}` : entry;
7487
+ if (line && next.length > limit) {
7488
+ lines2.push(line);
7489
+ line = entry;
7490
+ } else {
7491
+ line = next;
7492
+ }
7493
+ }
7494
+ if (line) lines2.push(line);
7495
+ return lines2.map(
7496
+ (line2, index) => `${index === 0 ? head : continuation}${line2}`
7497
+ );
7498
+ }
7207
7499
  function keptPrivateRows(keptPrivate) {
7208
7500
  const groups = /* @__PURE__ */ new Map();
7209
7501
  const singles = [];
@@ -7299,23 +7591,20 @@ function payloadBlock(payload, width, ownWindow, stats) {
7299
7591
  }
7300
7592
  if (entries.length > 0) {
7301
7593
  out.push(
7302
- ...wrapRow(
7303
- "models ",
7304
- " ".repeat(LABEL_WIDTH),
7305
- entries.join(" \xB7 "),
7306
- width
7307
- )
7594
+ ...wrapEntries("models ", " ".repeat(LABEL_WIDTH), entries, width)
7308
7595
  );
7309
7596
  }
7310
7597
  const filled = NAME_CATEGORIES.filter(
7311
7598
  (category) => payload.inventory[category].length > 0
7312
7599
  );
7313
7600
  if (filled.length === 0) {
7314
- out.push(`${"publishes".padEnd(LABEL_WIDTH)}no names from this harness`);
7601
+ out.push(
7602
+ `${"sends".padEnd(LABEL_WIDTH)}no inventory names from this harness`
7603
+ );
7315
7604
  return out;
7316
7605
  }
7317
7606
  out.push(
7318
- `${"publishes".padEnd(LABEL_WIDTH)}${filled.map(
7607
+ `${"sends".padEnd(LABEL_WIDTH)}${filled.map(
7319
7608
  (category) => `${payload.inventory[category].length} ${CATEGORY_LABEL[category]}`
7320
7609
  ).join(" \xB7 ")}`
7321
7610
  );
@@ -7419,7 +7708,26 @@ function buildGateSummary(ctx) {
7419
7708
  `window ${[...windows][0]}${body.cliVersion ? ` \xB7 aistack ${body.cliVersion}` : ""}`
7420
7709
  );
7421
7710
  }
7711
+ const cited = [
7712
+ ...new Set(
7713
+ payloads.flatMap(
7714
+ (p8) => p8.models.flatMap((m) => m.pricingTable ? [m.pricingTable] : [])
7715
+ )
7716
+ )
7717
+ ];
7718
+ if (ctx.prices && cited.length > 0) {
7719
+ const origin = ctx.prices.origin === "served" ? `${ctx.prices.id} from ${host}` : `${ctx.prices.id} (bundled; the server table was unavailable)`;
7720
+ out.push(`prices ${origin} \xB7 cites ${cited.join(", ")}`);
7721
+ }
7422
7722
  const width = wrapWidth(ctx.width);
7723
+ out.push(
7724
+ ...wrapRow(
7725
+ "privacy ",
7726
+ " ".repeat(LABEL_WIDTH),
7727
+ "raw conversation text, paths, repo names, and command arguments stay local",
7728
+ width
7729
+ )
7730
+ );
7423
7731
  for (const payload of payloads) {
7424
7732
  const stats = ctx.scanStats?.[payload.harness.name];
7425
7733
  out.push("", DIVIDER, "");
@@ -7447,10 +7755,12 @@ function buildGateSummary(ctx) {
7447
7755
  const shown = rows.slice(0, KEPT_PRIVATE_ROWS_SHOWN);
7448
7756
  const examples = shown.map((r) => r.names > 1 ? `${r.label} \xD7${r.names}` : r.label).join(", ");
7449
7757
  const more = rows.length > shown.length ? `, ...${rows.length - shown.length} more` : "";
7450
- out.push(`private ${n} name${n === 1 ? "" : "s"} \xB7 ${examples}${more}`);
7758
+ out.push(
7759
+ `private ${n} review name${n === 1 ? "" : "s"} \xB7 ${examples}${more}`
7760
+ );
7451
7761
  if (body.keptPrivate !== void 0 && config.stack !== null) {
7452
7762
  out.push(
7453
- ` they go up for you to review at ${host}/stacks/${config.stack.slug}/changes`
7763
+ ` stored privately for your review at ${host}/stacks/${config.stack.slug}/changes`
7454
7764
  );
7455
7765
  out.push(
7456
7766
  " (turn off: Review kept-private names, on your stack)"
@@ -7489,6 +7799,25 @@ async function stageSync(deps) {
7489
7799
  const windowDays = deps.windowDays ?? DEFAULT_WINDOW_DAYS;
7490
7800
  const projectWorkspaceId = deps.getProjectWorkspaceIdImpl ?? getProjectWorkspaceId;
7491
7801
  const fetchManifest = deps.fetchManifestImpl ?? fetchDayManifest;
7802
+ const fetchPrices = deps.fetchPricesImpl ?? fetchPriceTable;
7803
+ const progress = deps.onProgress ?? (() => {
7804
+ });
7805
+ let prices = {
7806
+ id: BUNDLED_PRICE_TABLE_ID,
7807
+ origin: "bundled"
7808
+ };
7809
+ progress("Checking prices and stack settings");
7810
+ try {
7811
+ const table = await fetchPrices(deps.baseUrl);
7812
+ if (table) {
7813
+ setActivePricer(layeredPricer(table));
7814
+ prices = { id: table.id, origin: "served" };
7815
+ } else {
7816
+ setActivePricer(null);
7817
+ }
7818
+ } catch {
7819
+ setActivePricer(null);
7820
+ }
7492
7821
  const { config, source } = await loadConfig({
7493
7822
  baseUrl: deps.baseUrl,
7494
7823
  ...token ? { token } : {}
@@ -7511,9 +7840,13 @@ async function stageSync(deps) {
7511
7840
  const usageScans = [];
7512
7841
  const sinceMs = windowStartMs(now, windowDays);
7513
7842
  const daysSinceMs = windowStartMs(now, retentionDays);
7514
- const active = await adapters(sinceMs);
7515
- for (const adapter of active) {
7516
- const { aggregate, stats } = await adapter.scan({ sinceMs });
7843
+ const active2 = await adapters(sinceMs);
7844
+ for (const adapter of active2) {
7845
+ progress(`Scanning recent ${adapter.name} usage`);
7846
+ const { aggregate, stats } = await adapter.scan({
7847
+ sinceMs,
7848
+ onProgress: (files) => progress(`Scanning recent ${adapter.name} usage \xB7 ${files} files`)
7849
+ });
7517
7850
  scanStats[adapter.name] = stats;
7518
7851
  built.push(
7519
7852
  buildPayload({
@@ -7528,9 +7861,11 @@ async function stageSync(deps) {
7528
7861
  })
7529
7862
  );
7530
7863
  }
7531
- for (const adapter of active) {
7864
+ for (const adapter of active2) {
7865
+ progress(`Reading historical ${adapter.name} days`);
7532
7866
  const { aggregate, workflow: workflow2, workflowLocal } = await adapter.scan({
7533
- sinceMs: daysSinceMs
7867
+ sinceMs: daysSinceMs,
7868
+ onProgress: (files) => progress(`Reading historical ${adapter.name} days \xB7 ${files} files`)
7534
7869
  });
7535
7870
  workflowScans.push({ aggregate: workflow2, local: workflowLocal });
7536
7871
  usageScans.push(
@@ -7543,12 +7878,20 @@ async function stageSync(deps) {
7543
7878
  );
7544
7879
  }
7545
7880
  const settings = (deps.getSettingsImpl ?? getSettings)();
7546
- const workflow = workflowScans.length > 0 && config.publishWorkflow ? extractLocalWorkflow({
7547
- harnesses: workflowScans,
7548
- fromMs: daysSinceMs,
7549
- toMs: now,
7550
- ...deps.gitRunnerImpl ? { run: deps.gitRunnerImpl } : {}
7551
- }) : void 0;
7881
+ let workflow;
7882
+ if (workflowScans.length > 0 && config.publishWorkflow) {
7883
+ progress("Reading Git history");
7884
+ workflow = deps.gitRunnerImpl ? extractLocalWorkflow({
7885
+ harnesses: workflowScans,
7886
+ fromMs: daysSinceMs,
7887
+ toMs: now,
7888
+ run: deps.gitRunnerImpl
7889
+ }) : await extractLocalWorkflowAsync({
7890
+ harnesses: workflowScans,
7891
+ fromMs: daysSinceMs,
7892
+ toMs: now
7893
+ });
7894
+ }
7552
7895
  const localDays = applyDayConsent(
7553
7896
  buildMeasuredDays({
7554
7897
  usage: mergeUsageDays(usageScans),
@@ -7568,13 +7911,14 @@ async function stageSync(deps) {
7568
7911
  config,
7569
7912
  settings.autoSync,
7570
7913
  deps.trigger,
7571
- active.length > 0 ? {
7914
+ active2.length > 0 ? {
7572
7915
  aggregateVersion: MEASURED_DAYS_V1,
7573
7916
  utcOffsetMinutes: workflow?.utcOffsetMinutes ?? machineUtcOffsetMinutes(),
7574
7917
  days: days.send
7575
7918
  } : void 0,
7576
7919
  CLI_VERSION
7577
7920
  );
7921
+ progress("Preparing review");
7578
7922
  const bodyJson = JSON.stringify(body);
7579
7923
  const keptPrivate = mergeKeptPrivate(built.map((b) => b.keptPrivate));
7580
7924
  const ctx = {
@@ -7585,6 +7929,7 @@ async function stageSync(deps) {
7585
7929
  baseUrl: deps.baseUrl,
7586
7930
  scanStats,
7587
7931
  days,
7932
+ prices,
7588
7933
  // The real terminal, so the inventory rows break where this window ends
7589
7934
  // (#217). A pipe reports nothing and the preview falls back to 80.
7590
7935
  width: process.stdout.columns
@@ -7595,7 +7940,7 @@ async function stageSync(deps) {
7595
7940
  } else if (token === null) {
7596
7941
  blockedReason = "This machine is not linked. Run `npx @use-aistack/cli login` first.";
7597
7942
  } else if (config.stack === null) {
7598
- 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.`;
7943
+ 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.";
7599
7944
  }
7600
7945
  return {
7601
7946
  id: stageId(bodyJson),
@@ -7608,7 +7953,8 @@ async function stageSync(deps) {
7608
7953
  token,
7609
7954
  stagedAt: now,
7610
7955
  blockedReason,
7611
- days
7956
+ days,
7957
+ prices
7612
7958
  };
7613
7959
  }
7614
7960
 
@@ -7640,7 +7986,7 @@ async function runAutoSync(deps) {
7640
7986
  appendLogLine(logFile, `${stamp} skipped - auto-sync is not enabled`);
7641
7987
  return;
7642
7988
  }
7643
- const frequencyHours = config.frequencyHours || DEFAULT_FREQUENCY_HOURS;
7989
+ const frequencyHours = normalizeFrequencyHours(config.frequencyHours);
7644
7990
  const state = settings.autoSyncState ?? {};
7645
7991
  const lastRunAt = state.lastRunAt ?? 0;
7646
7992
  if (now - lastRunAt < frequencyHours * 36e5) return;
@@ -7659,6 +8005,18 @@ async function runAutoSync(deps) {
7659
8005
  if (loaded.config.autoSync?.enabled === false) {
7660
8006
  revoked = true;
7661
8007
  } else {
8008
+ const serverFrequency = loaded.config.autoSync?.frequencyHours;
8009
+ if (loaded.config.autoSync?.enabled === true && serverFrequency !== void 0 && normalizeFrequencyHours(serverFrequency) !== frequencyHours) {
8010
+ saveSettings(
8011
+ {
8012
+ autoSync: {
8013
+ enabled: true,
8014
+ frequencyHours: normalizeFrequencyHours(serverFrequency)
8015
+ }
8016
+ },
8017
+ settingsFile
8018
+ );
8019
+ }
7662
8020
  const staged = await stage({
7663
8021
  baseUrl: deps.baseUrl,
7664
8022
  now: () => now,
@@ -7770,21 +8128,71 @@ async function syncCommand(options = {}) {
7770
8128
  process.exitCode = 1;
7771
8129
  return;
7772
8130
  }
7773
- if (getToken() === null) {
8131
+ let token = getToken();
8132
+ if (token === null) {
8133
+ p7.log.message("This machine needs a destination stack. Linking it now.");
8134
+ if (!await performLogin({ destinationRequired: true })) {
8135
+ outroError("login failed. Nothing was sent.");
8136
+ process.exitCode = 1;
8137
+ return;
8138
+ }
8139
+ token = getToken();
8140
+ }
8141
+ if (token === null) {
8142
+ outroError("login completed without a saved credential. Nothing was sent.");
8143
+ process.exitCode = 1;
8144
+ return;
8145
+ }
8146
+ let loaded = await loadSyncConfig({ baseUrl: BASE_URL, token });
8147
+ if (loaded.source === "bundled") {
8148
+ outroError(
8149
+ "Could not fetch your settings from aistack, so the destination stack is unknown. Check the network and sync again."
8150
+ );
8151
+ process.exitCode = 1;
8152
+ return;
8153
+ }
8154
+ if (loaded.config.stack === null) {
7774
8155
  p7.log.message(
7775
- "This machine is not linked to an aistack account yet. Linking it now."
8156
+ "This machine is not linked to a destination stack. Opening aistack so you can choose one."
7776
8157
  );
7777
- if (!await performLogin()) {
7778
- outroError("login failed. Nothing was sent.");
8158
+ if (!await performLogin({
8159
+ destinationRequired: true,
8160
+ replaceToken: token
8161
+ })) {
8162
+ outroError("linking failed. Nothing was sent.");
8163
+ process.exitCode = 1;
8164
+ return;
8165
+ }
8166
+ token = getToken();
8167
+ if (token === null) {
8168
+ outroError(
8169
+ "linking completed without a saved credential. Nothing was sent."
8170
+ );
7779
8171
  process.exitCode = 1;
7780
8172
  return;
7781
8173
  }
8174
+ loaded = await loadSyncConfig({ baseUrl: BASE_URL, token });
8175
+ if (loaded.source === "bundled" || loaded.config.stack === null) {
8176
+ outroError(
8177
+ "The destination stack could not be confirmed. Nothing was sent."
8178
+ );
8179
+ process.exitCode = 1;
8180
+ return;
8181
+ }
8182
+ p7.log.success(`Linked this machine to ${loaded.config.stack.name}`);
7782
8183
  }
8184
+ const destinationToken = token;
8185
+ const destinationConfig = loaded;
7783
8186
  const s = p7.spinner();
7784
8187
  s.start("Scanning local agent transcripts");
7785
8188
  let staged;
7786
8189
  try {
7787
- staged = await stageSync({ baseUrl: BASE_URL });
8190
+ staged = await stageSync({
8191
+ baseUrl: BASE_URL,
8192
+ getTokenImpl: () => destinationToken,
8193
+ loadConfigImpl: async () => destinationConfig,
8194
+ onProgress: (message) => s.message(message)
8195
+ });
7788
8196
  } catch (e) {
7789
8197
  s.stop("Scan failed");
7790
8198
  outroError(e instanceof Error ? e.message : String(e));
@@ -7826,7 +8234,12 @@ async function syncCommand(options = {}) {
7826
8234
  );
7827
8235
  } else if (res.keptPrivate.stored > 0) {
7828
8236
  lines2.push(
7829
- `${res.keptPrivate.stored} kept-private names went up for your review at ${res.url}/changes`
8237
+ `${res.keptPrivate.stored} private review name${res.keptPrivate.stored === 1 ? "" : "s"} stored at ${res.url}/changes`
8238
+ );
8239
+ }
8240
+ if (res.keptPrivate.machineStored > 0) {
8241
+ lines2.push(
8242
+ "This machine's private label was stored for the same review."
7830
8243
  );
7831
8244
  }
7832
8245
  p7.log.message(lines2.join("\n"));
@@ -8025,7 +8438,12 @@ function createSyncServer(deps, send) {
8025
8438
  );
8026
8439
  } else if (res.keptPrivate.stored > 0) {
8027
8440
  lines2.push(
8028
- `${res.keptPrivate.stored} kept-private names went up for your review at ${res.url}/changes`
8441
+ `${res.keptPrivate.stored} private review name${res.keptPrivate.stored === 1 ? "" : "s"} stored at ${res.url}/changes`
8442
+ );
8443
+ }
8444
+ if (res.keptPrivate.machineStored > 0) {
8445
+ lines2.push(
8446
+ "This machine's private label was stored for the same review."
8029
8447
  );
8030
8448
  }
8031
8449
  ok(id, textResult(lines2.join("\n")));
@@ -8109,7 +8527,7 @@ function runStdioSyncServer(deps) {
8109
8527
  // src/index.ts
8110
8528
  var program = new Command();
8111
8529
  program.name("aistack").description("Measure and share your AI stack from your terminal").version(CLI_VERSION);
8112
- program.command("login").description("Authenticate with AI Stack").action(loginCommand);
8530
+ program.command("login").description("Authenticate with AI Stack").option("--label <label>", "Set the machine label").action((options) => loginCommand(options));
8113
8531
  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 }));
8114
8532
  program.command("create").description("Download and write your stack's AI config files").action(createCommand);
8115
8533
  program.command("mcp").description(
@@ -8126,7 +8544,7 @@ program.command("sync").description("Scan, preview, and publish measured usage (
8126
8544
  "silent background sync; 'on' asks your stack for the permission and installs the SessionStart hooks, 'off' revokes both"
8127
8545
  ).option(
8128
8546
  "--every <hours>",
8129
- "with --auto on: hours between auto-syncs (default 24)"
8547
+ "with --auto on: hours between auto-syncs (default 6)"
8130
8548
  ).action((options) => syncCommand(options));
8131
8549
  program.command("connect").description("Install the in-session sync surface (MCP server + Skill)").argument("<harness>", 'the harness to connect ("claude")').action(connectCommand);
8132
8550
  program.parse();