@use-aistack/cli 0.9.0 → 0.10.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
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/version.ts
7
- var CLI_VERSION = true ? "0.9.0" : "0.0.0-dev";
7
+ var CLI_VERSION = true ? "0.10.0" : "0.0.0-dev";
8
8
 
9
9
  // src/api.ts
10
10
  var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
@@ -122,6 +122,29 @@ async function syncPublish(token, bodyJson) {
122
122
  }
123
123
  return res.json();
124
124
  }
125
+ async function fetchDayManifest(baseUrl, token) {
126
+ const res = await fetch(`${baseUrl}/api/cli/sync-manifest`, {
127
+ headers: { "Content-Type": "application/json", ...authHeaders(token) }
128
+ });
129
+ if (res.status === 404) return null;
130
+ if (res.status === 401)
131
+ throw new Error(
132
+ "Authentication expired. Run `npx @use-aistack/cli login` again."
133
+ );
134
+ if (res.status === 403 || res.status === 429)
135
+ throw failure("Manifest fetch failed", res);
136
+ if (!res.ok) {
137
+ throw new Error(await formatHttpError(res, "Manifest fetch failed"));
138
+ }
139
+ const body = await res.json();
140
+ const retentionDays = typeof body.retentionDays === "number" && body.retentionDays > 0 ? body.retentionDays : 400;
141
+ const aggregateVersion = typeof body.aggregateVersion === "string" ? body.aggregateVersion : "";
142
+ const days = Array.isArray(body.days) ? body.days.flatMap((d) => {
143
+ const row = d;
144
+ return typeof row?.date === "string" && typeof row?.fingerprint === "string" ? [{ date: row.date, fingerprint: row.fingerprint }] : [];
145
+ }) : [];
146
+ return { retentionDays, aggregateVersion, days };
147
+ }
125
148
  async function setAutoSync(token, flag) {
126
149
  const res = await request("/api/cli/auto-sync", {
127
150
  method: "POST",
@@ -1592,6 +1615,64 @@ var asName = (v) => {
1592
1615
  const s = asStr(v);
1593
1616
  return s === null ? null : cleanName(s);
1594
1617
  };
1618
+ var utcDateOf = (ms) => new Date(ms).toISOString().slice(0, 10);
1619
+ function usageDayAcc(agg, tsMs) {
1620
+ const date = utcDateOf(tsMs);
1621
+ let day = agg.usageDays.get(date);
1622
+ if (!day) {
1623
+ day = {
1624
+ models: /* @__PURE__ */ new Map(),
1625
+ subagentTokens: 0,
1626
+ syntheticTokens: 0,
1627
+ projectDirs: /* @__PURE__ */ new Set()
1628
+ };
1629
+ agg.usageDays.set(date, day);
1630
+ }
1631
+ return day;
1632
+ }
1633
+ function noteUsageResponse(agg, response, sign = 1) {
1634
+ if (response.tsMs === null) return;
1635
+ const day = usageDayAcc(agg, response.tsMs);
1636
+ let m = day.models.get(response.modelKey);
1637
+ if (!m) {
1638
+ m = {
1639
+ counts: {
1640
+ input: 0,
1641
+ output: 0,
1642
+ cacheWrite5m: 0,
1643
+ cacheWrite1h: 0,
1644
+ cacheWriteUnsplit: 0,
1645
+ cacheRead: 0
1646
+ },
1647
+ costUSD: 0,
1648
+ unpricedTokens: 0
1649
+ };
1650
+ day.models.set(response.modelKey, m);
1651
+ }
1652
+ const c = response.counts;
1653
+ m.counts.input += sign * c.input;
1654
+ m.counts.output += sign * c.output;
1655
+ m.counts.cacheWrite5m += sign * c.cacheWrite5m;
1656
+ m.counts.cacheWrite1h += sign * c.cacheWrite1h;
1657
+ m.counts.cacheWriteUnsplit += sign * c.cacheWriteUnsplit;
1658
+ m.counts.cacheRead += sign * c.cacheRead;
1659
+ if (response.costUSD === null) m.unpricedTokens += sign * countsTotal(c);
1660
+ else m.costUSD += sign * response.costUSD;
1661
+ if (response.sidechain) day.subagentTokens += sign * countsTotal(c);
1662
+ }
1663
+ function noteSyntheticTokens(agg, tsMs, tokens) {
1664
+ if (tsMs === null) return;
1665
+ usageDayAcc(agg, tsMs).syntheticTokens += tokens;
1666
+ }
1667
+ function noteSessionStart(agg, sessionId, tsMs) {
1668
+ if (tsMs === null) return;
1669
+ const held = agg.sessionStarts.get(sessionId);
1670
+ if (held === void 0 || tsMs < held) agg.sessionStarts.set(sessionId, tsMs);
1671
+ }
1672
+ function noteProjectDay(agg, directory, tsMs) {
1673
+ if (tsMs === null) return;
1674
+ usageDayAcc(agg, tsMs).projectDirs.add(directory);
1675
+ }
1595
1676
  function createAggregate() {
1596
1677
  return {
1597
1678
  files: 0,
@@ -1631,7 +1712,9 @@ function createAggregate() {
1631
1712
  textBlocks: 0,
1632
1713
  webSearchRequests: 0,
1633
1714
  webFetchRequests: 0,
1634
- seen: /* @__PURE__ */ new Map()
1715
+ seen: /* @__PURE__ */ new Map(),
1716
+ usageDays: /* @__PURE__ */ new Map(),
1717
+ sessionStarts: /* @__PURE__ */ new Map()
1635
1718
  };
1636
1719
  }
1637
1720
  var bump = (m, k, n = 1) => m.set(k, (m.get(k) ?? 0) + n);
@@ -1649,7 +1732,16 @@ function emptyUsage() {
1649
1732
  };
1650
1733
  }
1651
1734
  var countsTotal = (t) => t.input + t.output + t.cacheWrite5m + t.cacheWrite1h + t.cacheWriteUnsplit + t.cacheRead;
1652
- function addModelUsage(agg, modelKey, counts, costUSD, messages = 1) {
1735
+ function addModelUsage(agg, modelKey, counts, costUSD, messages = 1, at) {
1736
+ if (at) {
1737
+ noteUsageResponse(agg, {
1738
+ tsMs: at.tsMs,
1739
+ modelKey,
1740
+ counts,
1741
+ costUSD,
1742
+ ...at.sidechain ? { sidechain: true } : {}
1743
+ });
1744
+ }
1653
1745
  let m = agg.byModel.get(modelKey);
1654
1746
  if (!m) {
1655
1747
  m = emptyUsage();
@@ -2423,6 +2515,12 @@ function foldWorkflowDays(days, options) {
2423
2515
  git: foldGitDays(days.map((day) => day.git)),
2424
2516
  ...parallelProjectDays.length === 0 ? {} : { parallelProjects: Math.max(...parallelProjectDays) },
2425
2517
  parallelProjectDays,
2518
+ gitDays: [...days].sort((a, b) => a.date.localeCompare(b.date)).map((day) => ({
2519
+ date: day.date,
2520
+ additions: day.git.additions,
2521
+ removals: day.git.removals,
2522
+ commits: day.git.commits
2523
+ })),
2426
2524
  webSearchDays
2427
2525
  };
2428
2526
  }
@@ -3181,6 +3279,38 @@ function deriveSessionPhases(events, ruleSet = PHASE_RULES_V1, harness) {
3181
3279
  };
3182
3280
  }
3183
3281
 
3282
+ // ../workflow-rules/src/usage.ts
3283
+ var MEASURED_DAYS_V1 = "measured-days/v1";
3284
+ function canonicalJson(value) {
3285
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
3286
+ if (value && typeof value === "object") {
3287
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
3288
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
3289
+ }
3290
+ return JSON.stringify(value) ?? "null";
3291
+ }
3292
+ function fnv1a64(text) {
3293
+ const prime = 0x100000001b3n;
3294
+ const mask = 0xffffffffffffffffn;
3295
+ let hash = 0xcbf29ce484222325n;
3296
+ const bytes = new TextEncoder().encode(text);
3297
+ for (const byte of bytes) {
3298
+ hash ^= BigInt(byte);
3299
+ hash = hash * prime & mask;
3300
+ }
3301
+ return hash.toString(16).padStart(16, "0");
3302
+ }
3303
+ function dayFingerprint(day) {
3304
+ return fnv1a64(
3305
+ canonicalJson({
3306
+ version: MEASURED_DAYS_V1,
3307
+ date: day.date,
3308
+ usage: day.usage,
3309
+ workflow: day.workflow
3310
+ })
3311
+ );
3312
+ }
3313
+
3184
3314
  // ../workflow-rules/src/workflowRows.ts
3185
3315
  function metricRowId(metricId) {
3186
3316
  return `metric:${metricId}`;
@@ -3481,12 +3611,23 @@ function buildPayload(input) {
3481
3611
  }
3482
3612
  };
3483
3613
  }
3484
- function toPayloadWorkflow(extraction) {
3485
- return {
3486
- aggregateVersion: extraction.aggregateVersion,
3487
- utcOffsetMinutes: extraction.utcOffsetMinutes,
3488
- days: extraction.days
3489
- };
3614
+ function applyDayConsent(days, syncConfig) {
3615
+ return days.map((day) => {
3616
+ const { workflow, usage, ...rest } = day;
3617
+ const out = { ...rest };
3618
+ if (usage) {
3619
+ out.usage = syncConfig.publishCost ? usage : {
3620
+ harnesses: usage.harnesses.map((h) => ({
3621
+ ...h,
3622
+ models: h.models.map(
3623
+ ({ usd: _usd, pricingTable: _table, ...model }) => model
3624
+ )
3625
+ }))
3626
+ };
3627
+ }
3628
+ if (workflow && syncConfig.publishWorkflow) out.workflow = workflow;
3629
+ return out;
3630
+ });
3490
3631
  }
3491
3632
  function mergeKeptPrivate(halves) {
3492
3633
  const out = {};
@@ -3505,11 +3646,18 @@ function mergeKeptPrivate(halves) {
3505
3646
  }
3506
3647
  return out;
3507
3648
  }
3508
- function buildSyncBody(built, syncConfig, autoSync, trigger = "manual", workflow, cliVersion) {
3649
+ function buildSyncBody(built, syncConfig, autoSync, trigger = "manual", measuredDays, cliVersion) {
3509
3650
  const payloads = built.map((b) => b.payload);
3510
3651
  const base = autoSync ? { payloads, autoSync, trigger } : { payloads, trigger };
3511
- const withWorkflow = workflow && syncConfig.publishWorkflow ? { ...base, workflow: toPayloadWorkflow(workflow) } : base;
3512
- const withVersion = cliVersion ? { ...withWorkflow, cliVersion } : withWorkflow;
3652
+ const withDays = measuredDays ? {
3653
+ ...base,
3654
+ measuredDays: {
3655
+ aggregateVersion: MEASURED_DAYS_V1,
3656
+ utcOffsetMinutes: measuredDays.utcOffsetMinutes,
3657
+ days: applyDayConsent(measuredDays.days, syncConfig)
3658
+ }
3659
+ } : base;
3660
+ const withVersion = cliVersion ? { ...withDays, cliVersion } : withDays;
3513
3661
  if (!syncConfig.reviewKeptPrivate) return withVersion;
3514
3662
  return {
3515
3663
  ...withVersion,
@@ -3533,7 +3681,7 @@ var finiteNonnegative = (value) => value !== void 0 && Number.isFinite(value) &&
3533
3681
  var bump2 = (map, key, amount = 1) => {
3534
3682
  map.set(key, (map.get(key) ?? 0) + amount);
3535
3683
  };
3536
- var utcDateOf = (ms) => new Date(ms).toISOString().slice(0, 10);
3684
+ var utcDateOf2 = (ms) => new Date(ms).toISOString().slice(0, 10);
3537
3685
  var PHASE_RANK = {
3538
3686
  verify: 4,
3539
3687
  handoff: 3,
@@ -3658,7 +3806,7 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3658
3806
  state.parentSession ??= observation.parentSession;
3659
3807
  state.sidechain ||= observation.sidechain === true;
3660
3808
  const at = new Date(observation.tsMs);
3661
- const date = utcDateOf(observation.tsMs);
3809
+ const date = utcDateOf2(observation.tsMs);
3662
3810
  if (observation.projectWorkspace) {
3663
3811
  state.projectWorkspaces.add(observation.projectWorkspace);
3664
3812
  localSources.projectWorkspaces.add(observation.projectWorkspace);
@@ -3711,7 +3859,7 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3711
3859
  let phaseSessionCount = 0;
3712
3860
  for (const state of sessions.values()) {
3713
3861
  if (state.firstTs === void 0) continue;
3714
- const day = dayOf(utcDateOf(state.firstTs));
3862
+ const day = dayOf(utcDateOf2(state.firstTs));
3715
3863
  const events = reduceEventBatches(state.events, harness);
3716
3864
  const responses = [...state.responses.values()];
3717
3865
  day.sessions++;
@@ -3821,7 +3969,7 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3821
3969
  const parent = sessions.get(parentKey);
3822
3970
  const anchor = parent?.firstTs ?? Math.min(...children.map((child) => child.firstTs ?? Infinity));
3823
3971
  if (!Number.isFinite(anchor)) continue;
3824
- const day = dayOf(utcDateOf(anchor));
3972
+ const day = dayOf(utcDateOf2(anchor));
3825
3973
  day.hasDelegation = true;
3826
3974
  day.delegation.mostSubagents = Math.max(
3827
3975
  day.delegation.mostSubagents,
@@ -3844,10 +3992,10 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3844
3992
  localSources.activeProjectDays.clear();
3845
3993
  for (const state of sessions.values()) {
3846
3994
  if (state.firstTs === void 0 || state.lastTs === void 0) continue;
3847
- let day = Date.parse(`${utcDateOf(state.firstTs)}T00:00:00Z`);
3848
- const lastDay = Date.parse(`${utcDateOf(state.lastTs)}T00:00:00Z`);
3995
+ let day = Date.parse(`${utcDateOf2(state.firstTs)}T00:00:00Z`);
3996
+ const lastDay = Date.parse(`${utcDateOf2(state.lastTs)}T00:00:00Z`);
3849
3997
  while (day <= lastDay) {
3850
- const date = utcDateOf(day);
3998
+ const date = utcDateOf2(day);
3851
3999
  const projects = localSources.activeProjectDays.get(date) ?? /* @__PURE__ */ new Set();
3852
4000
  for (const project of state.projectWorkspaces) projects.add(project);
3853
4001
  if (projects.size > 0)
@@ -3961,7 +4109,8 @@ function ingestRecord(agg, raw, ctx) {
3961
4109
  const rec = asObj(raw);
3962
4110
  if (!rec) return;
3963
4111
  agg.records++;
3964
- agg.projectDirs.add(projectWorkspaceDirectory(rec) ?? ctx.projectDir);
4112
+ const projectDir = projectWorkspaceDirectory(rec) ?? ctx.projectDir;
4113
+ agg.projectDirs.add(projectDir);
3965
4114
  const version = asStr(rec.version);
3966
4115
  if (version) agg.ccVersions.add(cleanName(version));
3967
4116
  const sessionId = asStr(rec.sessionId);
@@ -3977,6 +4126,8 @@ function ingestRecord(agg, raw, ctx) {
3977
4126
  agg.lastTs = agg.lastTs === null ? ts : Math.max(agg.lastTs, ts);
3978
4127
  }
3979
4128
  }
4129
+ if (sessionId) noteSessionStart(agg, sessionId, tsMs);
4130
+ noteProjectDay(agg, projectDir, tsMs);
3980
4131
  const type = asStr(rec.type);
3981
4132
  if (type === "assistant") {
3982
4133
  ingestClaudeWorkflow(agg, rec, ctx, tsMs);
@@ -4088,7 +4239,9 @@ function ingestAssistant(agg, rec, tsMs) {
4088
4239
  const model = asName(msg.model) ?? "(unknown)";
4089
4240
  if (model.startsWith("<")) {
4090
4241
  agg.syntheticRecords++;
4091
- agg.syntheticTokens += countsTotal(readCounts(usage));
4242
+ const synthetic = countsTotal(readCounts(usage));
4243
+ agg.syntheticTokens += synthetic;
4244
+ noteSyntheticTokens(agg, tsMs, synthetic);
4092
4245
  return;
4093
4246
  }
4094
4247
  if (tsMs === null) agg.untimestampedResponses++;
@@ -4188,6 +4341,7 @@ function buildContribution(usage, model, sidechain, tsMs) {
4188
4341
  entries,
4189
4342
  total: entries.reduce((a, e) => a + countsTotal(e.counts), 0),
4190
4343
  sidechain,
4344
+ tsMs,
4191
4345
  webSearch: serverTools ? asNum(serverTools.web_search_requests) : 0,
4192
4346
  webFetch: serverTools ? asNum(serverTools.web_fetch_requests) : 0,
4193
4347
  mirroredIterationTypes: [...mirrored],
@@ -4211,6 +4365,11 @@ function applyContribution(agg, c, sign) {
4211
4365
  m.cacheRead += sign * counts.cacheRead;
4212
4366
  if (costUSD === null) m.unpricedTokens += sign * countsTotal(counts);
4213
4367
  else m.costUSD += sign * costUSD;
4368
+ noteUsageResponse(
4369
+ agg,
4370
+ { tsMs: c.tsMs, modelKey, counts, costUSD, sidechain: c.sidechain },
4371
+ sign
4372
+ );
4214
4373
  });
4215
4374
  if (c.sidechain) agg.sidechainTokens += sign * c.total;
4216
4375
  else agg.mainTokens += sign * c.total;
@@ -4477,12 +4636,14 @@ function ingestLine(agg, raw, state, sinceMs) {
4477
4636
  agg.firstTs = agg.firstTs === null ? tsMs : Math.min(agg.firstTs, tsMs);
4478
4637
  agg.lastTs = agg.lastTs === null ? tsMs : Math.max(agg.lastTs, tsMs);
4479
4638
  }
4480
- noteActivity(agg, state);
4639
+ noteActivity(agg, state, tsMs);
4640
+ noteProjectDay(agg, state.cwd ?? "(unknown)", tsMs);
4481
4641
  if (type === "event_msg" && payload) ingestEvent(agg, payload, state, tsMs);
4482
4642
  else if (type === "response_item" && payload)
4483
4643
  ingestItem(agg, payload, state, tsMs);
4484
4644
  }
4485
- function noteActivity(agg, state) {
4645
+ function noteActivity(agg, state, tsMs) {
4646
+ if (state.sessionId) noteSessionStart(agg, state.sessionId, tsMs);
4486
4647
  if (state.counted) return;
4487
4648
  state.counted = true;
4488
4649
  if (state.sessionId) agg.sessions.add(state.sessionId);
@@ -4513,7 +4674,9 @@ function ingestEvent(agg, payload, state, tsMs) {
4513
4674
  agg,
4514
4675
  modelKey,
4515
4676
  counts,
4516
- apiEquivalentCost(modelKey, counts, tsMs)
4677
+ apiEquivalentCost(modelKey, counts, tsMs),
4678
+ 1,
4679
+ { tsMs }
4517
4680
  );
4518
4681
  agg.mainTokens += total;
4519
4682
  if (tsMs !== null && state.sessionId) {
@@ -4916,10 +5079,14 @@ function ingestMessageRow(agg, state, row) {
4916
5079
  const session = sessionId ? state.sessions.get(sessionId) : void 0;
4917
5080
  if (sessionId) {
4918
5081
  agg.sessions.add(sessionId);
5082
+ noteSessionStart(agg, sessionId, tsMs);
4919
5083
  if (session?.version) agg.ccVersions.add(cleanName(session.version));
4920
5084
  }
4921
5085
  const cwd = asStr(row.cwd);
4922
- if (cwd) agg.projectDirs.add(cwd);
5086
+ if (cwd) {
5087
+ agg.projectDirs.add(cwd);
5088
+ noteProjectDay(agg, cwd, tsMs);
5089
+ }
4923
5090
  if (asStr(row.role) !== "assistant") return;
4924
5091
  agg.assistantRecords++;
4925
5092
  agg.distinctResponses++;
@@ -4942,7 +5109,9 @@ function ingestMessageRow(agg, state, row) {
4942
5109
  agg,
4943
5110
  modelKey,
4944
5111
  counts,
4945
- apiEquivalentCost(modelKey, counts, tsMs)
5112
+ apiEquivalentCost(modelKey, counts, tsMs),
5113
+ 1,
5114
+ { tsMs, sidechain: Boolean(session?.parentId) }
4946
5115
  );
4947
5116
  if (sessionId && tsMs !== null) {
4948
5117
  const completed = asNum(row.completedTsMs);
@@ -5418,7 +5587,8 @@ function ingestEntry(agg, raw, state, fold, sinceMs) {
5418
5587
  agg.firstTs = agg.firstTs === null ? tsMs : Math.min(agg.firstTs, tsMs);
5419
5588
  agg.lastTs = agg.lastTs === null ? tsMs : Math.max(agg.lastTs, tsMs);
5420
5589
  }
5421
- noteActivity2(agg, state);
5590
+ noteActivity2(agg, state, tsMs);
5591
+ noteProjectDay(agg, state.cwd ?? "(unknown)", tsMs);
5422
5592
  if (role === "assistant" && message) {
5423
5593
  agg.assistantRecords++;
5424
5594
  const served = asStr(message.responseModel);
@@ -5465,7 +5635,8 @@ function ingestEntry(agg, raw, state, fold, sinceMs) {
5465
5635
  countUsage(agg, fold, rec, 0, rec.usage, state.modelKey, tsMs);
5466
5636
  }
5467
5637
  }
5468
- function noteActivity2(agg, state) {
5638
+ function noteActivity2(agg, state, tsMs) {
5639
+ if (state.sessionId) noteSessionStart(agg, state.sessionId, tsMs);
5469
5640
  if (state.counted) return;
5470
5641
  state.counted = true;
5471
5642
  if (state.sessionId) agg.sessions.add(state.sessionId);
@@ -5494,7 +5665,9 @@ function countUsage(agg, fold, rec, msgTsMs, usageRaw, modelKey, tsMs, priceable
5494
5665
  agg,
5495
5666
  key,
5496
5667
  counts,
5497
- priceable ? apiEquivalentCost(key, counts, tsMs) : null
5668
+ priceable ? apiEquivalentCost(key, counts, tsMs) : null,
5669
+ 1,
5670
+ { tsMs }
5498
5671
  );
5499
5672
  agg.mainTokens += total;
5500
5673
  return "counted";
@@ -6445,6 +6618,152 @@ import { dirname as dirname7, join as join10 } from "path";
6445
6618
  // src/sync/stage.ts
6446
6619
  import { createHash as createHash2 } from "crypto";
6447
6620
 
6621
+ // src/usage/days.ts
6622
+ var round6 = (n) => Math.round(n * 1e6) / 1e6;
6623
+ function buildUsageDays(input) {
6624
+ const { aggregate: agg, harness, publishCost, projectWorkspaceId } = input;
6625
+ const sessionsByDay = /* @__PURE__ */ new Map();
6626
+ for (const startMs of agg.sessionStarts.values()) {
6627
+ const date = utcDateOf(startMs);
6628
+ sessionsByDay.set(date, (sessionsByDay.get(date) ?? 0) + 1);
6629
+ }
6630
+ const dates = [
6631
+ .../* @__PURE__ */ new Set([...agg.usageDays.keys(), ...sessionsByDay.keys()])
6632
+ ].sort();
6633
+ const out = /* @__PURE__ */ new Map();
6634
+ for (const date of dates) {
6635
+ const acc = agg.usageDays.get(date);
6636
+ const groups = /* @__PURE__ */ new Map();
6637
+ let unpriced = 0;
6638
+ for (const [modelKey, m] of acc?.models ?? []) {
6639
+ const id = sanitizeModelId(baseModelId(modelKey));
6640
+ let g = groups.get(id);
6641
+ if (!g) {
6642
+ g = {
6643
+ tokens: {
6644
+ input: 0,
6645
+ output: 0,
6646
+ cacheWrite: 0,
6647
+ cacheRead: 0,
6648
+ cacheWriteTtl: { fiveMinute: 0, oneHour: 0, unsplit: 0 }
6649
+ },
6650
+ costUSD: 0,
6651
+ unpricedTokens: 0,
6652
+ table: null
6653
+ };
6654
+ groups.set(id, g);
6655
+ }
6656
+ g.table ??= pricingTableFor(modelKey);
6657
+ g.tokens.input += m.counts.input;
6658
+ g.tokens.output += m.counts.output;
6659
+ g.tokens.cacheRead += m.counts.cacheRead;
6660
+ g.tokens.cacheWrite += m.counts.cacheWrite5m + m.counts.cacheWrite1h + m.counts.cacheWriteUnsplit;
6661
+ g.tokens.cacheWriteTtl.fiveMinute += m.counts.cacheWrite5m;
6662
+ g.tokens.cacheWriteTtl.oneHour += m.counts.cacheWrite1h;
6663
+ g.tokens.cacheWriteTtl.unsplit += m.counts.cacheWriteUnsplit;
6664
+ g.costUSD += m.costUSD;
6665
+ g.unpricedTokens += m.unpricedTokens;
6666
+ unpriced += m.unpricedTokens;
6667
+ }
6668
+ const models = [...groups.entries()].map(([model, g]) => {
6669
+ const { cacheWriteTtl, ...plain } = g.tokens;
6670
+ const tokens = g.tokens.cacheWrite > 0 ? { ...plain, cacheWriteTtl } : plain;
6671
+ const row = { model, tokens };
6672
+ if (publishCost && g.unpricedTokens === 0 && g.table !== null && countsTotalOf(tokens) > 0) {
6673
+ row.usd = round6(g.costUSD);
6674
+ row.pricingTable = g.table;
6675
+ }
6676
+ return row;
6677
+ }).filter((row) => countsTotalOf(row.tokens) > 0).sort(
6678
+ (a, b) => countsTotalOf(b.tokens) - countsTotalOf(a.tokens) || a.model.localeCompare(b.model)
6679
+ );
6680
+ out.set(date, {
6681
+ harness,
6682
+ sessions: sessionsByDay.get(date) ?? 0,
6683
+ projectKeys: [
6684
+ ...new Set([...acc?.projectDirs ?? []].map(projectWorkspaceId))
6685
+ ].sort(),
6686
+ models,
6687
+ subagentTokens: acc?.subagentTokens ?? 0,
6688
+ excludedTokens: { unpriced, synthetic: acc?.syntheticTokens ?? 0 }
6689
+ });
6690
+ }
6691
+ return out;
6692
+ }
6693
+ var countsTotalOf = (t) => t.input + t.output + t.cacheWrite + t.cacheRead;
6694
+ function mergeUsageDays(perHarness) {
6695
+ const out = /* @__PURE__ */ new Map();
6696
+ const dates = [
6697
+ ...new Set(perHarness.flatMap((days) => [...days.keys()]))
6698
+ ].sort();
6699
+ for (const date of dates) {
6700
+ const harnesses = [];
6701
+ for (const days of perHarness) {
6702
+ const day = days.get(date);
6703
+ if (day) harnesses.push(day);
6704
+ }
6705
+ out.set(date, { harnesses });
6706
+ }
6707
+ return out;
6708
+ }
6709
+ function buildMeasuredDays(input) {
6710
+ const workflowByDate = /* @__PURE__ */ new Map();
6711
+ for (const day of input.workflow ?? []) workflowByDate.set(day.date, day);
6712
+ const dates = [.../* @__PURE__ */ new Set([...input.usage.keys(), ...workflowByDate.keys()])].filter(
6713
+ (d) => /^\d{4}-\d{2}-\d{2}$/.test(d) && d >= input.from && d <= input.to
6714
+ ).sort();
6715
+ return dates.map((date) => {
6716
+ const usage = input.usage.get(date);
6717
+ const workflow = workflowByDate.get(date);
6718
+ return {
6719
+ date,
6720
+ ...usage ? { usage } : {},
6721
+ ...workflow ? { workflow } : {}
6722
+ };
6723
+ });
6724
+ }
6725
+
6726
+ // src/usage/diff.ts
6727
+ var MAX_DAY_WINDOW = 400;
6728
+ var DAY_MS = 864e5;
6729
+ function retentionFloor(todayUtc, days) {
6730
+ const span = Math.max(1, Math.min(days, MAX_DAY_WINDOW));
6731
+ const todayMs = Date.parse(`${todayUtc}T00:00:00.000Z`);
6732
+ return new Date(todayMs - (span - 1) * DAY_MS).toISOString().slice(0, 10);
6733
+ }
6734
+ function selectDaysToPublish(input) {
6735
+ const { local, manifest, todayUtc } = input;
6736
+ const retention = manifest?.retentionDays ?? MAX_DAY_WINDOW;
6737
+ const floor = retentionFloor(todayUtc, retention);
6738
+ const comparable = manifest !== null && manifest.aggregateVersion === MEASURED_DAYS_V1;
6739
+ const held = /* @__PURE__ */ new Map();
6740
+ if (comparable) {
6741
+ for (const day of manifest.days) held.set(day.date, day.fingerprint);
6742
+ }
6743
+ const send = [];
6744
+ const skipped = [];
6745
+ for (const day of [...local].sort((a, b) => a.date.localeCompare(b.date))) {
6746
+ if (day.date < floor) {
6747
+ skipped.push({ date: day.date, reason: "expired" });
6748
+ continue;
6749
+ }
6750
+ if (comparable && day.date !== todayUtc) {
6751
+ const fingerprint = held.get(day.date);
6752
+ if (fingerprint !== void 0 && fingerprint === dayFingerprint(day)) {
6753
+ skipped.push({ date: day.date, reason: "unchanged" });
6754
+ continue;
6755
+ }
6756
+ }
6757
+ send.push(day);
6758
+ }
6759
+ return {
6760
+ send,
6761
+ unchanged: skipped.filter((s) => s.reason === "unchanged").length,
6762
+ skipped,
6763
+ mode: comparable ? "diff" : "full"
6764
+ };
6765
+ }
6766
+
6448
6767
  // src/workflow/git.ts
6449
6768
  import { execFileSync as execFileSync2 } from "child_process";
6450
6769
  import path6 from "path";
@@ -7006,11 +7325,11 @@ function payloadBlock(payload, width, ownWindow, stats) {
7006
7325
  }
7007
7326
  var MODEL_ROLLUP = 0.01;
7008
7327
  var PHASE_ORDER = ["scout", "build", "verify", "handoff", "unknown"];
7009
- function workflowBlock(workflow, host) {
7328
+ function workflowBlock(workflowDays, utcOffsetMinutes, host) {
7010
7329
  const out = [];
7011
- const folded = foldWorkflowDays(workflow.days, {
7012
- aggregateVersion: workflow.aggregateVersion,
7013
- utcOffsetMinutes: workflow.utcOffsetMinutes
7330
+ const folded = foldWorkflowDays(workflowDays, {
7331
+ aggregateVersion: WORKFLOW_AGGREGATES_V2,
7332
+ utcOffsetMinutes
7014
7333
  });
7015
7334
  const harnesses = folded?.harnesses ?? [];
7016
7335
  const withPlaybook = harnesses.filter((h) => h.phase);
@@ -7019,12 +7338,12 @@ function workflowBlock(workflow, host) {
7019
7338
  ...new Set(withPlaybook.map((h) => h.phase?.ruleVersion ?? ""))
7020
7339
  ].filter(Boolean);
7021
7340
  out.push(
7022
- `workflow ${harnesses.length} harness${harnesses.length === 1 ? "" : "es"} \xB7 ${sessions} sessions \xB7 ${workflow.aggregateVersion}`
7341
+ `workflow ${harnesses.length} harness${harnesses.length === 1 ? "" : "es"} \xB7 ${sessions} sessions \xB7 ${WORKFLOW_AGGREGATES_V2}`
7023
7342
  );
7024
7343
  const first = folded?.dates[0];
7025
7344
  const last = folded?.dates.at(-1);
7026
7345
  out.push(
7027
- `days ${workflow.days.length} day${workflow.days.length === 1 ? "" : "s"}${first && last ? ` \xB7 ${first} to ${last}` : ""}`
7346
+ ` ${workflowDays.length} day${workflowDays.length === 1 ? "" : "s"}${first && last ? ` \xB7 ${first} to ${last}` : ""}`
7028
7347
  );
7029
7348
  const seconds = PHASE_ORDER.map(
7030
7349
  (phase) => withPlaybook.reduce((a, h) => a + (h.phase?.phaseSec[phase] ?? 0), 0)
@@ -7043,6 +7362,24 @@ function workflowBlock(workflow, host) {
7043
7362
  out.push(` (Publish workflow is on for ${host})`);
7044
7363
  return out;
7045
7364
  }
7365
+ function daysLine(measuredDays, selection) {
7366
+ const n = measuredDays.days.length;
7367
+ const head = `${n} day${n === 1 ? "" : "s"} to publish`;
7368
+ const unchanged = selection?.unchanged ?? 0;
7369
+ return unchanged > 0 ? `${head}, ${unchanged} unchanged` : head;
7370
+ }
7371
+ function daysBlock(measuredDays, selection) {
7372
+ const out = [`days ${daysLine(measuredDays, selection)}`];
7373
+ const first = measuredDays.days[0]?.date;
7374
+ const last = measuredDays.days.at(-1)?.date;
7375
+ const usageDays = measuredDays.days.filter((d) => d.usage).length;
7376
+ if (first && last) {
7377
+ out.push(
7378
+ ` ${first} to ${last} \xB7 ${usageDays} with usage \xB7 ${measuredDays.aggregateVersion}`
7379
+ );
7380
+ }
7381
+ return out;
7382
+ }
7046
7383
  function buildGateSummary(ctx) {
7047
7384
  const { body, keptPrivate, config, source, baseUrl } = ctx;
7048
7385
  const { payloads } = body;
@@ -7073,10 +7410,23 @@ function buildGateSummary(ctx) {
7073
7410
  out.push(...payloadBlock(payload, width, windows.size > 1, stats));
7074
7411
  }
7075
7412
  if (out[out.length - 1] === "") out.pop();
7413
+ if (body.measuredDays) {
7414
+ out.push("");
7415
+ out.push(...daysBlock(body.measuredDays, ctx.days));
7416
+ }
7076
7417
  out.push("");
7077
- out.push(
7078
- body.workflow ? workflowBlock(body.workflow, host).join("\n") : "workflow not published"
7418
+ const workflowDays = (body.measuredDays?.days ?? []).flatMap(
7419
+ (d) => d.workflow ? [d.workflow] : []
7079
7420
  );
7421
+ if (!config.publishWorkflow || !body.measuredDays) {
7422
+ out.push("workflow not published");
7423
+ } else if (workflowDays.length === 0) {
7424
+ out.push("workflow on, no changed day to publish");
7425
+ } else {
7426
+ out.push(
7427
+ ...workflowBlock(workflowDays, body.measuredDays.utcOffsetMinutes, host)
7428
+ );
7429
+ }
7080
7430
  const n = payloads.reduce((a, p8) => a + withheldCount(p8), 0);
7081
7431
  if (n > 0) {
7082
7432
  out.push("");
@@ -7118,6 +7468,7 @@ function buildGateSummary(ctx) {
7118
7468
  }
7119
7469
 
7120
7470
  // src/sync/stage.ts
7471
+ var utcDate2 = (ms) => new Date(ms).toISOString().slice(0, 10);
7121
7472
  function stageId(bodyJson) {
7122
7473
  return createHash2("sha256").update(bodyJson).digest("hex").slice(0, 12);
7123
7474
  }
@@ -7128,20 +7479,33 @@ async function stageSync(deps) {
7128
7479
  const adapters = deps.adaptersImpl ?? detectedAdapters;
7129
7480
  const windowDays = deps.windowDays ?? DEFAULT_WINDOW_DAYS;
7130
7481
  const projectWorkspaceId = deps.getProjectWorkspaceIdImpl ?? getProjectWorkspaceId;
7482
+ const fetchManifest = deps.fetchManifestImpl ?? fetchDayManifest;
7131
7483
  const { config, source } = await loadConfig({
7132
7484
  baseUrl: deps.baseUrl,
7133
7485
  ...token ? { token } : {}
7134
7486
  });
7487
+ let manifest = null;
7488
+ if (token) {
7489
+ try {
7490
+ manifest = await fetchManifest(deps.baseUrl, token);
7491
+ } catch {
7492
+ manifest = null;
7493
+ }
7494
+ }
7495
+ const retentionDays = Math.max(
7496
+ 1,
7497
+ Math.min(manifest?.retentionDays ?? MAX_DAY_WINDOW, MAX_DAY_WINDOW)
7498
+ );
7135
7499
  const built = [];
7136
7500
  const scanStats = {};
7137
7501
  const workflowScans = [];
7502
+ const usageScans = [];
7138
7503
  const sinceMs = windowStartMs(now, windowDays);
7139
- for (const adapter of await adapters(sinceMs)) {
7140
- const { aggregate, stats, workflow: workflow2, workflowLocal } = await adapter.scan({
7141
- sinceMs
7142
- });
7504
+ const daysSinceMs = windowStartMs(now, retentionDays);
7505
+ const active = await adapters(sinceMs);
7506
+ for (const adapter of active) {
7507
+ const { aggregate, stats } = await adapter.scan({ sinceMs });
7143
7508
  scanStats[adapter.name] = stats;
7144
- workflowScans.push({ aggregate: workflow2, local: workflowLocal });
7145
7509
  built.push(
7146
7510
  buildPayload({
7147
7511
  aggregate,
@@ -7155,19 +7519,51 @@ async function stageSync(deps) {
7155
7519
  })
7156
7520
  );
7157
7521
  }
7522
+ for (const adapter of active) {
7523
+ const { aggregate, workflow: workflow2, workflowLocal } = await adapter.scan({
7524
+ sinceMs: daysSinceMs
7525
+ });
7526
+ workflowScans.push({ aggregate: workflow2, local: workflowLocal });
7527
+ usageScans.push(
7528
+ buildUsageDays({
7529
+ harness: adapter.name,
7530
+ aggregate,
7531
+ publishCost: config.publishCost,
7532
+ projectWorkspaceId
7533
+ })
7534
+ );
7535
+ }
7158
7536
  const settings = (deps.getSettingsImpl ?? getSettings)();
7159
7537
  const workflow = workflowScans.length > 0 && config.publishWorkflow ? extractLocalWorkflow({
7160
7538
  harnesses: workflowScans,
7161
- fromMs: sinceMs,
7539
+ fromMs: daysSinceMs,
7162
7540
  toMs: now,
7163
7541
  ...deps.gitRunnerImpl ? { run: deps.gitRunnerImpl } : {}
7164
7542
  }) : void 0;
7543
+ const localDays = applyDayConsent(
7544
+ buildMeasuredDays({
7545
+ usage: mergeUsageDays(usageScans),
7546
+ ...workflow ? { workflow: workflow.days } : {},
7547
+ from: utcDate2(daysSinceMs),
7548
+ to: utcDate2(now)
7549
+ }),
7550
+ config
7551
+ );
7552
+ const days = selectDaysToPublish({
7553
+ local: localDays,
7554
+ manifest,
7555
+ todayUtc: utcDate2(now)
7556
+ });
7165
7557
  const body = buildSyncBody(
7166
7558
  built,
7167
7559
  config,
7168
7560
  settings.autoSync,
7169
7561
  deps.trigger,
7170
- workflow,
7562
+ active.length > 0 ? {
7563
+ aggregateVersion: MEASURED_DAYS_V1,
7564
+ utcOffsetMinutes: workflow?.utcOffsetMinutes ?? machineUtcOffsetMinutes(),
7565
+ days: days.send
7566
+ } : void 0,
7171
7567
  CLI_VERSION
7172
7568
  );
7173
7569
  const bodyJson = JSON.stringify(body);
@@ -7179,6 +7575,7 @@ async function stageSync(deps) {
7179
7575
  source,
7180
7576
  baseUrl: deps.baseUrl,
7181
7577
  scanStats,
7578
+ days,
7182
7579
  // The real terminal, so the inventory rows break where this window ends
7183
7580
  // (#217). A pipe reports nothing and the preview falls back to 80.
7184
7581
  width: process.stdout.columns
@@ -7201,7 +7598,8 @@ async function stageSync(deps) {
7201
7598
  config,
7202
7599
  token,
7203
7600
  stagedAt: now,
7204
- blockedReason
7601
+ blockedReason,
7602
+ days
7205
7603
  };
7206
7604
  }
7207
7605