@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/README.md +6 -3
- package/dist/index.js +719 -301
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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.
|
|
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
|
|
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
|
|
212
|
-
const existing = groups.get(
|
|
567
|
+
const key2 = `${file.group}:${file.source}:${file.type}:${dir}`;
|
|
568
|
+
const existing = groups.get(key2) ?? [];
|
|
213
569
|
existing.push(file);
|
|
214
|
-
groups.set(
|
|
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 =
|
|
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 [
|
|
691
|
+
for (const [key2, value] of Object.entries(raw)) {
|
|
330
692
|
if (typeof value === "string") {
|
|
331
|
-
data[
|
|
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[
|
|
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 [
|
|
765
|
-
const at =
|
|
1126
|
+
for (const [key2, entries] of Object.entries(installed.plugins ?? {})) {
|
|
1127
|
+
const at = key2.lastIndexOf("@");
|
|
766
1128
|
if (at <= 0) continue;
|
|
767
|
-
const pluginName =
|
|
768
|
-
const marketplace =
|
|
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
|
|
807
|
-
const mp =
|
|
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 [
|
|
1356
|
-
const prev = existingMap.get(
|
|
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:
|
|
1721
|
+
details.push({ name: key2, status: "added" });
|
|
1360
1722
|
} else if (prev !== content) {
|
|
1361
1723
|
changed++;
|
|
1362
|
-
details.push({ name:
|
|
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
|
|
1369
|
-
if (!currentMap.has(
|
|
1730
|
+
for (const key2 of existingMap.keys()) {
|
|
1731
|
+
if (!currentMap.has(key2)) {
|
|
1370
1732
|
removed++;
|
|
1371
|
-
details.push({ name:
|
|
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 [
|
|
1392
|
-
if (!existingLinks.has(
|
|
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 [
|
|
1398
|
-
if (!currentLinks.has(
|
|
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,
|
|
2478
|
+
function sumBy(rows, key2, add, clone) {
|
|
2297
2479
|
const merged = /* @__PURE__ */ new Map();
|
|
2298
2480
|
for (const row of rows) {
|
|
2299
|
-
const k =
|
|
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:
|
|
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((
|
|
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,
|
|
3682
|
-
map.set(
|
|
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 = (
|
|
3792
|
-
let state = sessions.get(
|
|
3988
|
+
const getSession = (key2) => {
|
|
3989
|
+
let state = sessions.get(key2);
|
|
3793
3990
|
if (!state) {
|
|
3794
3991
|
state = sessionState();
|
|
3795
|
-
sessions.set(
|
|
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 [
|
|
3954
|
-
bump2(day.activity,
|
|
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
|
|
4180
|
+
let active2 = 0;
|
|
3984
4181
|
for (const boundary of boundaries) {
|
|
3985
|
-
|
|
4182
|
+
active2 += boundary.delta;
|
|
3986
4183
|
day.delegation.widestFanOut = Math.max(
|
|
3987
4184
|
day.delegation.widestFanOut,
|
|
3988
|
-
|
|
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(([
|
|
4059
|
-
const [weekdayUtc, hourUtc] =
|
|
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
|
|
5653
|
-
if (fold.seenUsage.has(
|
|
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(
|
|
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
|
|
5860
|
+
const key2 = modelKey ?? "(unknown)";
|
|
5664
5861
|
addModelUsage(
|
|
5665
5862
|
agg,
|
|
5666
|
-
|
|
5863
|
+
key2,
|
|
5667
5864
|
counts,
|
|
5668
|
-
priceable ? apiEquivalentCost(
|
|
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: "
|
|
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: "
|
|
6049
|
-
hint: "
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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).
|
|
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: "
|
|
6576
|
-
hint: "
|
|
6798
|
+
label: "Maybe later",
|
|
6799
|
+
hint: "ask again after your next manual sync"
|
|
6577
6800
|
},
|
|
6578
6801
|
{
|
|
6579
|
-
value: "
|
|
6580
|
-
label: "
|
|
6581
|
-
hint:
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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(([
|
|
7050
|
-
const [weekdayUtc, hourUtc] =
|
|
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}
|
|
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"}
|
|
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: "
|
|
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
|
-
...
|
|
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(
|
|
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
|
-
`${"
|
|
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(
|
|
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
|
-
`
|
|
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
|
|
7515
|
-
for (const adapter of
|
|
7516
|
-
|
|
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
|
|
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
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
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
|
-
|
|
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." :
|
|
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
|
|
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
|
-
|
|
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
|
|
8156
|
+
"This machine is not linked to a destination stack. Opening aistack so you can choose one."
|
|
7776
8157
|
);
|
|
7777
|
-
if (!await performLogin(
|
|
7778
|
-
|
|
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({
|
|
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}
|
|
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}
|
|
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
|
|
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();
|