perchai-cli 2.4.26 → 2.4.29

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/perch.mjs +530 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,6 +19,7 @@ Run `perch`, then use these commands inside the terminal chat:
19
19
 
20
20
  - `/help` — show commands.
21
21
  - `/status` — show cwd, auth, mode, persona, permission, and thread.
22
+ - `/usage` — show tokens and estimated cost for this session.
22
23
  - `/cwd [dir]` — show or change the working directory.
23
24
  - `/permission [default|auto_review|take_the_wheel|plan]` — show or change permission mode.
24
25
  - `/permissions [default|auto_review|take_the_wheel|plan]` — alias for `/permission`.
package/dist/perch.mjs CHANGED
@@ -75566,6 +75566,7 @@ var init_payroll = __esm({
75566
75566
  // lib/perchBusinessTools/index.ts
75567
75567
  var init_perchBusinessTools = __esm({
75568
75568
  "lib/perchBusinessTools/index.ts"() {
75569
+ "use strict";
75569
75570
  init_generateAPAuditPacket();
75570
75571
  init_inventoryFolder();
75571
75572
  init_loadBusinessTables();
@@ -75917,6 +75918,7 @@ function isTurnAbortedError(error) {
75917
75918
  var TURN_STOPPED_BY_USER_MESSAGE;
75918
75919
  var init_turnAbort = __esm({
75919
75920
  "features/perchTerminal/runtime/turnAbort.ts"() {
75921
+ "use strict";
75920
75922
  TURN_STOPPED_BY_USER_MESSAGE = "Turn stopped by user.";
75921
75923
  }
75922
75924
  });
@@ -76921,6 +76923,8 @@ function buildTranscriptSegments(state) {
76921
76923
  const terminalSegmentIdx = /* @__PURE__ */ new Map();
76922
76924
  const workerRunIdx = /* @__PURE__ */ new Map();
76923
76925
  const flockRunIdx = /* @__PURE__ */ new Map();
76926
+ const flockIdByWorkerId = /* @__PURE__ */ new Map();
76927
+ const flockWorkerIdByToolCallId = /* @__PURE__ */ new Map();
76924
76928
  const diagnosticIdxByKey = /* @__PURE__ */ new Map();
76925
76929
  const capabilityIdx = /* @__PURE__ */ new Map();
76926
76930
  const liveCardIdx = /* @__PURE__ */ new Map();
@@ -76944,6 +76948,32 @@ function buildTranscriptSegments(state) {
76944
76948
  if (runId) sandboxRunIdx.set(runId, idx);
76945
76949
  if (toolCallId) sandboxRunIdx.set(toolCallId, idx);
76946
76950
  }
76951
+ function updateFlockWorkerByWorkerId(workerId, updater) {
76952
+ if (!workerId) return false;
76953
+ const flockId = flockIdByWorkerId.get(workerId);
76954
+ if (!flockId) return false;
76955
+ const idx = flockRunIdx.get(flockId);
76956
+ if (idx === void 0) return true;
76957
+ const seg = segments[idx];
76958
+ if (seg?.kind !== "flock_run") return true;
76959
+ segments[idx] = {
76960
+ ...seg,
76961
+ workers: seg.workers.map(
76962
+ (worker) => worker.workerId === workerId ? updater(worker) : worker
76963
+ )
76964
+ };
76965
+ return true;
76966
+ }
76967
+ function isFlockOwnedToolEvent(event) {
76968
+ return Boolean(
76969
+ event.workerId && flockIdByWorkerId.has(event.workerId) || event.toolCallId && flockWorkerIdByToolCallId.has(event.toolCallId)
76970
+ );
76971
+ }
76972
+ function isQuietFlockDiagnostic(event) {
76973
+ if (!event.workerId || !flockIdByWorkerId.has(event.workerId)) return false;
76974
+ if (event.code === "tool_call_budget_exhausted") return false;
76975
+ return event.code === "source_read_budget_reached" || event.code === "source_analysis_max_iteration_closing_attempted" || event.code === "source_analysis_max_iteration_synthesized" || event.code === "source_analysis_max_iteration_closing_fallback" || event.code === "source_analysis_max_iteration_incomplete" || /source-read budget|source-analysis|read-only source/i.test(event.message);
76976
+ }
76947
76977
  function flushReasoning() {
76948
76978
  if (!pendingReasoning) return;
76949
76979
  segments.push({ kind: "reasoning", text: pendingReasoning, seq: seq++, timestamp: pendingReasoningTs });
@@ -77037,6 +77067,15 @@ function buildTranscriptSegments(state) {
77037
77067
  ev.toolCallId ? [ev.toolCallId] : [],
77038
77068
  ev.ts
77039
77069
  );
77070
+ if (ev.workerId && flockIdByWorkerId.has(ev.workerId)) {
77071
+ if (ev.toolCallId) flockWorkerIdByToolCallId.set(ev.toolCallId, ev.workerId);
77072
+ updateFlockWorkerByWorkerId(ev.workerId, (worker) => ({
77073
+ ...worker,
77074
+ toolCalls: (worker.toolCalls ?? 0) + 1,
77075
+ lastToolName: ev.toolName
77076
+ }));
77077
+ break;
77078
+ }
77040
77079
  if (ev.workerId) {
77041
77080
  const idx = segments.length;
77042
77081
  segments.push({
@@ -77091,6 +77130,7 @@ function buildTranscriptSegments(state) {
77091
77130
  }
77092
77131
  case "tool_call_completed": {
77093
77132
  flushReasoning();
77133
+ if (isFlockOwnedToolEvent(ev)) break;
77094
77134
  const id = ev.toolCallId ?? ev.toolName;
77095
77135
  const idx = toolCallIdx.get(id);
77096
77136
  if (idx !== void 0) {
@@ -77107,6 +77147,7 @@ function buildTranscriptSegments(state) {
77107
77147
  }
77108
77148
  case "tool_result": {
77109
77149
  flushReasoning();
77150
+ if (isFlockOwnedToolEvent(ev)) break;
77110
77151
  const id = ev.toolCallId ?? ev.toolName;
77111
77152
  if (ev.toolName === "run_sandbox_code") {
77112
77153
  updateSandboxRun(ev.toolCallId, null, (seg) => ({ ...seg, resultText: ev.text }));
@@ -77135,6 +77176,7 @@ function buildTranscriptSegments(state) {
77135
77176
  case "tool_output_delta": {
77136
77177
  flushReasoning();
77137
77178
  if (!ev.text) break;
77179
+ if (isFlockOwnedToolEvent(ev)) break;
77138
77180
  const id = ev.toolCallId ?? ev.toolName;
77139
77181
  if (ev.toolName === "run_sandbox_code") {
77140
77182
  updateSandboxRun(ev.toolCallId, null, (seg) => ({ ...seg, resultText: `${seg.resultText ?? ""}${ev.text}` }));
@@ -77543,6 +77585,7 @@ function buildTranscriptSegments(state) {
77543
77585
  case "diagnostic": {
77544
77586
  flushReasoning();
77545
77587
  if (!ev.message.trim()) break;
77588
+ if (isQuietFlockDiagnostic(ev)) break;
77546
77589
  if (ev.code === "tool_call_budget_exhausted") {
77547
77590
  let attributed = false;
77548
77591
  if (ev.workerId) {
@@ -77694,6 +77737,7 @@ function buildTranscriptSegments(state) {
77694
77737
  }
77695
77738
  case "tool_call_failed": {
77696
77739
  flushReasoning();
77740
+ if (isFlockOwnedToolEvent(ev)) break;
77697
77741
  const id = ev.toolCallId ?? ev.toolName;
77698
77742
  const idx = toolCallIdx.get(id);
77699
77743
  if (idx !== void 0) {
@@ -77808,6 +77852,12 @@ function buildTranscriptSegments(state) {
77808
77852
  }
77809
77853
  case "worker_run_started": {
77810
77854
  flushReasoning();
77855
+ if (updateFlockWorkerByWorkerId(ev.workerId, (worker) => ({
77856
+ ...worker,
77857
+ status: worker.status === "queued" ? "running" : worker.status
77858
+ }))) {
77859
+ break;
77860
+ }
77811
77861
  const idx = segments.length;
77812
77862
  segments.push({
77813
77863
  kind: "worker_run",
@@ -77824,6 +77874,12 @@ function buildTranscriptSegments(state) {
77824
77874
  }
77825
77875
  case "worker_run_completed": {
77826
77876
  flushReasoning();
77877
+ if (updateFlockWorkerByWorkerId(ev.workerId, (worker) => ({
77878
+ ...worker,
77879
+ status: ev.ok ? "done" : "failed"
77880
+ }))) {
77881
+ break;
77882
+ }
77827
77883
  const runIdx = workerRunIdx.get(ev.workerId);
77828
77884
  if (runIdx !== void 0) {
77829
77885
  const seg = segments[runIdx];
@@ -77876,6 +77932,11 @@ function buildTranscriptSegments(state) {
77876
77932
  if (idx === void 0) break;
77877
77933
  const seg = segments[idx];
77878
77934
  if (seg?.kind !== "flock_run") break;
77935
+ if (ev.accepted) {
77936
+ for (const worker of ev.workers) {
77937
+ flockIdByWorkerId.set(worker.workerId, ev.flockId);
77938
+ }
77939
+ }
77879
77940
  segments[idx] = ev.accepted ? {
77880
77941
  ...seg,
77881
77942
  status: "running",
@@ -77897,7 +77958,11 @@ function buildTranscriptSegments(state) {
77897
77958
  if (seg?.kind !== "flock_run") break;
77898
77959
  const known = seg.workers.some((worker) => worker.flockWorkerId === ev.flockWorkerId);
77899
77960
  const workers = known ? seg.workers.map(
77900
- (worker) => worker.flockWorkerId === ev.flockWorkerId ? { ...worker, status: ev.status } : worker
77961
+ (worker) => worker.flockWorkerId === ev.flockWorkerId ? {
77962
+ ...worker,
77963
+ status: ev.status,
77964
+ note: ev.detail ?? worker.note
77965
+ } : worker
77901
77966
  ) : [
77902
77967
  ...seg.workers,
77903
77968
  {
@@ -77905,7 +77970,8 @@ function buildTranscriptSegments(state) {
77905
77970
  workerId: ev.workerId,
77906
77971
  displayName: ev.displayName,
77907
77972
  nickname: ev.nickname,
77908
- status: ev.status
77973
+ status: ev.status,
77974
+ note: ev.detail
77909
77975
  }
77910
77976
  ];
77911
77977
  segments[idx] = { ...seg, workers };
@@ -78308,9 +78374,13 @@ function segmentToCompact(seg) {
78308
78374
  summary: seg.summary?.slice(0, 300),
78309
78375
  flockWorkers: seg.workers.map((worker) => ({
78310
78376
  flockWorkerId: worker.flockWorkerId,
78377
+ workerId: worker.workerId,
78311
78378
  displayName: worker.displayName,
78312
78379
  nickname: worker.nickname,
78313
- status: worker.status
78380
+ status: worker.status,
78381
+ note: worker.note,
78382
+ toolCalls: worker.toolCalls,
78383
+ lastToolName: worker.lastToolName
78314
78384
  }))
78315
78385
  };
78316
78386
  case "worker_tool_call":
@@ -86622,6 +86692,7 @@ function truncateHistoryLine(value, max2) {
86622
86692
  }
86623
86693
  var init_operatorTruth = __esm({
86624
86694
  "features/perchTerminal/runtime/operatorTruth.ts"() {
86695
+ "use strict";
86625
86696
  }
86626
86697
  });
86627
86698
 
@@ -91587,7 +91658,6 @@ function listFinancialPlaybooks() {
91587
91658
  var AP_AUDIT_PACKET_DEF, PLAYBOOKS;
91588
91659
  var init_registry2 = __esm({
91589
91660
  "features/perchTerminal/runtime/financialPlaybooks/registry.ts"() {
91590
- "use strict";
91591
91661
  init_managedWorkflowRegistry2();
91592
91662
  init_toolNames();
91593
91663
  AP_AUDIT_PACKET_DEF = {
@@ -220709,11 +220779,11 @@ function flockTaskGate(rawTask) {
220709
220779
  if (!task) {
220710
220780
  return { ok: false, task, reason: "No task given. Usage: /flock <task>." };
220711
220781
  }
220712
- if (/\/flock\b/i.test(task)) {
220782
+ if (/(^|[\s([{])\/flock(?:\s|$)/i.test(task)) {
220713
220783
  return {
220714
220784
  ok: false,
220715
220785
  task,
220716
- reason: "A flock cannot gather another flock \u2014 recursive /flock is not allowed."
220786
+ reason: "Subagents cannot start another /flock run from inside a /flock request."
220717
220787
  };
220718
220788
  }
220719
220789
  const wordCount = task.split(/\s+/).length;
@@ -221475,15 +221545,30 @@ function buildSpawnContext(input, flockId, signal, emit, worker) {
221475
221545
  };
221476
221546
  }
221477
221547
  function buildFlockSummary(plan, outcomes, status, toolCallsUsed, wallTimeHit) {
221478
- const header = status === "completed" ? `Flock finished: ${plan.summary}.` : status === "partial" ? `Flock finished with partial results (${plan.summary}).` : status === "cancelled" ? `Flock stopped early${wallTimeHit ? " \u2014 wall-time cap reached" : ""}.` : `Flock failed \u2014 no worker produced a usable result.`;
221548
+ const header = status === "completed" ? `Subagents finished: ${plan.summary}.` : status === "partial" ? `Subagents finished with partial results (${plan.summary}).` : status === "cancelled" ? `Subagents stopped early${wallTimeHit ? " \u2014 wall-time cap reached" : ""}.` : `Subagents failed \u2014 no worker produced a usable result.`;
221479
221549
  const lines = outcomes.map((outcome) => {
221480
221550
  const mark = outcome.status === "done" ? "done" : outcome.status === "failed" ? "failed" : outcome.status;
221481
- const detail = outcome.detail ? ` \u2014 ${clampLine(outcome.detail, 160)}` : "";
221551
+ const detailText = friendlyOutcomeDetail(outcome.detail);
221552
+ const detail = detailText ? ` \u2014 ${clampLine(detailText, 160)}` : "";
221482
221553
  return `- ${outcome.worker.displayName} (${outcome.worker.nickname}): ${mark}${detail}`;
221483
221554
  });
221484
221555
  const footer = `Used ${toolCallsUsed}/${plan.caps.maxTotalToolCalls} tool calls across ${outcomes.length} workers.`;
221485
221556
  return [header, ...lines, footer].join("\n");
221486
221557
  }
221558
+ function friendlyOutcomeDetail(detail) {
221559
+ const clean = clampLine(detail, 220);
221560
+ if (!clean) return "";
221561
+ if (/Reached maximum \d+ tool-call iterations/i.test(clean)) {
221562
+ return "finished from gathered evidence";
221563
+ }
221564
+ if (/source-read budget|read-only source|source-analysis/i.test(clean)) {
221565
+ return "used gathered evidence";
221566
+ }
221567
+ if (/tool-call budget exhausted|Tool budget reached/i.test(clean)) {
221568
+ return "tool limit reached; finished from evidence";
221569
+ }
221570
+ return clean;
221571
+ }
221487
221572
  function clampLine(text, max2) {
221488
221573
  const clean = (text ?? "").replace(/\s+/g, " ").trim();
221489
221574
  return clean.length > max2 ? `${clean.slice(0, max2 - 1)}\u2026` : clean;
@@ -221510,13 +221595,282 @@ var init_runFlockTurn = __esm({
221510
221595
  }
221511
221596
  });
221512
221597
 
221598
+ // features/perchTerminal/runtime/usage/usageCommand.ts
221599
+ function parseUsageCommand(raw) {
221600
+ const trimmed = raw.trim();
221601
+ if (!/^\/usage(?:\s|$)/i.test(trimmed)) return null;
221602
+ const task = trimmed.slice("/usage".length).trim();
221603
+ return task ? { kind: "usage", task } : { kind: "usage" };
221604
+ }
221605
+ var init_usageCommand = __esm({
221606
+ "features/perchTerminal/runtime/usage/usageCommand.ts"() {
221607
+ "use strict";
221608
+ }
221609
+ });
221610
+
221611
+ // lib/perch-ai/access.ts
221612
+ function hasPrivilegedPerchAiRole(memberships) {
221613
+ return memberships.some(
221614
+ (membership) => membership.status === "active" && (membership.role === "admin" || membership.role === "internal")
221615
+ );
221616
+ }
221617
+ function canShowPerchDevTools(membershipRole) {
221618
+ if (!membershipRole) return false;
221619
+ return hasPrivilegedPerchAiRole([
221620
+ {
221621
+ role: membershipRole,
221622
+ status: "active"
221623
+ }
221624
+ ]);
221625
+ }
221626
+ var init_access = __esm({
221627
+ "lib/perch-ai/access.ts"() {
221628
+ }
221629
+ });
221630
+
221631
+ // features/perchTerminal/runtime/usage/usageSummary.ts
221632
+ function collectUsageRunIds(input) {
221633
+ const runIds = /* @__PURE__ */ new Set();
221634
+ for (const runId of input.usageRunIds ?? []) {
221635
+ const cleaned = runId.trim();
221636
+ if (cleaned) runIds.add(cleaned);
221637
+ }
221638
+ for (const message of input.recentMessages ?? []) {
221639
+ if (message.kind !== "text" || message.role !== "assistant") continue;
221640
+ const stateRunId = message.operatorState?.runId?.trim();
221641
+ if (stateRunId) runIds.add(stateRunId);
221642
+ for (const event of message.operatorState?.events ?? []) {
221643
+ if (event.type === "turn_started" && event.runId.trim()) runIds.add(event.runId.trim());
221644
+ }
221645
+ for (const event of message.transcriptEvents ?? []) {
221646
+ if (event.type === "turn_started" && event.runId.trim()) runIds.add(event.runId.trim());
221647
+ }
221648
+ }
221649
+ return Array.from(runIds).slice(-250);
221650
+ }
221651
+ function summarizeInferenceUsageRows(rows, options = {}) {
221652
+ const promptTokens = sumRows(rows, "prompt_tokens");
221653
+ const completionTokens = sumRows(rows, "completion_tokens");
221654
+ const cachedTokens = sumRows(rows, "cached_tokens");
221655
+ const totalTokens = sumRows(rows, "total_tokens");
221656
+ const estimatedCostUsd = rows.reduce((sum, row) => sum + readNumber(row.estimated_cost_usd), 0);
221657
+ const created = rows.map((row) => row.created_at).filter((value) => typeof value === "string" && value.length > 0).sort();
221658
+ const summary = {
221659
+ ok: true,
221660
+ calls: rows.length,
221661
+ promptTokens,
221662
+ completionTokens,
221663
+ cachedTokens,
221664
+ totalTokens,
221665
+ estimatedCostUsd,
221666
+ runCount: new Set(options.runIds?.length ? options.runIds : rows.map((row) => row.run_id).filter(Boolean)).size,
221667
+ since: created[0] ?? null,
221668
+ through: created[created.length - 1] ?? null,
221669
+ showModelDetails: Boolean(options.showModelDetails)
221670
+ };
221671
+ if (summary.showModelDetails) {
221672
+ summary.modelBreakdown = buildModelBreakdown(rows);
221673
+ }
221674
+ return summary;
221675
+ }
221676
+ function formatUsageSummaryText(summary) {
221677
+ const lines = [
221678
+ "Usage this session",
221679
+ `- Calls: ${formatInteger(summary.calls)}`,
221680
+ `- Tokens: ${formatInteger(summary.totalTokens)} total (${formatInteger(summary.promptTokens)} input \xB7 ${formatInteger(summary.completionTokens)} output${summary.cachedTokens ? ` \xB7 ${formatInteger(summary.cachedTokens)} cached` : ""})`,
221681
+ `- Estimated cost: ${formatUsd(summary.estimatedCostUsd)}`
221682
+ ];
221683
+ if (summary.calls === 0) {
221684
+ lines.push("- Nothing has been recorded for this session yet.");
221685
+ }
221686
+ if (summary.showModelDetails && summary.modelBreakdown?.length) {
221687
+ lines.push("", "Internal detail:");
221688
+ for (const row of summary.modelBreakdown.slice(0, 8)) {
221689
+ lines.push(
221690
+ `- ${row.label}: ${formatInteger(row.totalTokens)} tokens \xB7 ${formatUsd(row.estimatedCostUsd)} \xB7 ${formatInteger(row.calls)} calls`
221691
+ );
221692
+ }
221693
+ }
221694
+ return lines.join("\n");
221695
+ }
221696
+ function buildModelBreakdown(rows) {
221697
+ const byModel = /* @__PURE__ */ new Map();
221698
+ for (const row of rows) {
221699
+ const label = [row.model_option_id, row.provider, row.model].filter((value) => typeof value === "string" && value.trim()).join(" \xB7 ") || "unknown model";
221700
+ const current = byModel.get(label) ?? {
221701
+ label,
221702
+ calls: 0,
221703
+ promptTokens: 0,
221704
+ completionTokens: 0,
221705
+ cachedTokens: 0,
221706
+ totalTokens: 0,
221707
+ estimatedCostUsd: 0
221708
+ };
221709
+ current.calls += 1;
221710
+ current.promptTokens += readNumber(row.prompt_tokens);
221711
+ current.completionTokens += readNumber(row.completion_tokens);
221712
+ current.cachedTokens += readNumber(row.cached_tokens);
221713
+ current.totalTokens += readNumber(row.total_tokens);
221714
+ current.estimatedCostUsd += readNumber(row.estimated_cost_usd);
221715
+ byModel.set(label, current);
221716
+ }
221717
+ return Array.from(byModel.values()).sort((a, b2) => b2.estimatedCostUsd - a.estimatedCostUsd);
221718
+ }
221719
+ function sumRows(rows, key) {
221720
+ return rows.reduce((sum, row) => sum + readNumber(row[key]), 0);
221721
+ }
221722
+ function readNumber(value) {
221723
+ if (typeof value === "number" && Number.isFinite(value)) return value;
221724
+ if (typeof value === "string") {
221725
+ const parsed = Number(value);
221726
+ return Number.isFinite(parsed) ? parsed : 0;
221727
+ }
221728
+ return 0;
221729
+ }
221730
+ function formatInteger(value) {
221731
+ return Math.max(0, Math.round(value)).toLocaleString("en-US");
221732
+ }
221733
+ function formatUsd(value) {
221734
+ if (!Number.isFinite(value) || value <= 0) return "$0.00";
221735
+ return value < 0.01 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
221736
+ }
221737
+ var init_usageSummary = __esm({
221738
+ "features/perchTerminal/runtime/usage/usageSummary.ts"() {
221739
+ "use strict";
221740
+ }
221741
+ });
221742
+
221743
+ // features/perchTerminal/runtime/usage/runUsageTurn.ts
221744
+ async function runUsageTurn(input, deps) {
221745
+ const startedAt = Date.now();
221746
+ const runId = input.clientRunId ?? `usage-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
221747
+ const now14 = () => (/* @__PURE__ */ new Date()).toISOString();
221748
+ const events = [];
221749
+ const emit = (event) => {
221750
+ events.push(event);
221751
+ deps.onEvent?.(event);
221752
+ };
221753
+ emit({ type: "turn_started", runId, ts: now14(), isSystemNotification: false });
221754
+ let userMessageId = null;
221755
+ if (input.workspaceId && input.skipUserMessagePersistence !== true) {
221756
+ userMessageId = await deps.persistUserMessage({
221757
+ workspaceId: input.workspaceId,
221758
+ threadId: input.threadId,
221759
+ role: "user",
221760
+ mode: input.chatMode,
221761
+ content: input.trimmedInput
221762
+ });
221763
+ if (userMessageId) {
221764
+ emit({ type: "user_message_persisted", messageId: userMessageId, ts: now14() });
221765
+ }
221766
+ }
221767
+ const runIds = collectUsageRunIds({
221768
+ usageRunIds: input.usageRunIds,
221769
+ recentMessages: input.recentMessages
221770
+ });
221771
+ const showModelDetails = canShowPerchDevTools(input.sessionContext.membershipRole);
221772
+ const summary = await resolveUsageSummary(input, runIds, showModelDetails);
221773
+ const assistantText = formatUsageSummaryText(summary);
221774
+ let assistantMessageId = null;
221775
+ if (input.workspaceId) {
221776
+ assistantMessageId = await deps.persistAssistantMessage({
221777
+ workspaceId: input.workspaceId,
221778
+ threadId: input.threadId,
221779
+ role: "assistant",
221780
+ mode: input.chatMode,
221781
+ content: assistantText,
221782
+ contentJson: {
221783
+ slashCommand: "usage",
221784
+ turnSummary: assistantText,
221785
+ usageSummary: summary,
221786
+ transcriptEvents: events
221787
+ }
221788
+ });
221789
+ }
221790
+ emit({
221791
+ type: "assistant_response",
221792
+ text: assistantText,
221793
+ messageId: assistantMessageId ?? runId,
221794
+ ts: now14()
221795
+ });
221796
+ const durationMs = Date.now() - startedAt;
221797
+ emit({ type: "turn_completed", runId, durationMs, status: "completed", ts: now14() });
221798
+ return {
221799
+ status: "completed",
221800
+ assistantText,
221801
+ messageId: assistantMessageId ?? runId,
221802
+ runId,
221803
+ durationMs
221804
+ };
221805
+ }
221806
+ async function resolveUsageSummary(input, runIds, showModelDetails) {
221807
+ if (runIds.length === 0) {
221808
+ return summarizeInferenceUsageRows([], { showModelDetails, runIds });
221809
+ }
221810
+ if (input.supabase && input.userId && input.workspaceId) {
221811
+ const rows = await fetchUsageRows(input.supabase, {
221812
+ userId: input.userId,
221813
+ workspaceId: input.workspaceId,
221814
+ runIds
221815
+ });
221816
+ return summarizeInferenceUsageRows(rows, { showModelDetails, runIds });
221817
+ }
221818
+ const viaServer = await fetchUsageSummaryFromServer(input, runIds);
221819
+ if (viaServer) return viaServer;
221820
+ return summarizeInferenceUsageRows([], { showModelDetails, runIds });
221821
+ }
221822
+ async function fetchUsageRows(supabase, input) {
221823
+ const { data, error } = await supabase.from("perch_ai_inference_usage").select("*").eq("user_id", input.userId).eq("workspace_id", input.workspaceId).in("run_id", input.runIds).order("created_at", { ascending: true }).limit(1e3);
221824
+ if (error) throw error;
221825
+ return data ?? [];
221826
+ }
221827
+ async function fetchUsageSummaryFromServer(input, runIds) {
221828
+ const appUrl = input.cliServerAppUrl?.trim();
221829
+ const token = input.cliServerAccessToken?.trim();
221830
+ if (!appUrl || !token || typeof fetch !== "function") return null;
221831
+ const controller = new AbortController();
221832
+ const timeout = setTimeout(() => controller.abort(), 5e3);
221833
+ try {
221834
+ const response = await fetch(`${appUrl.replace(/\/+$/, "")}/api/perch-terminal/usage`, {
221835
+ method: "POST",
221836
+ headers: {
221837
+ Accept: "application/json",
221838
+ "Content-Type": "application/json",
221839
+ Authorization: `Bearer ${token}`
221840
+ },
221841
+ body: JSON.stringify({ runIds }),
221842
+ signal: controller.signal
221843
+ });
221844
+ if (!response.ok) return null;
221845
+ const payload = await response.json();
221846
+ return payload.ok ? payload.summary : null;
221847
+ } catch {
221848
+ return null;
221849
+ } finally {
221850
+ clearTimeout(timeout);
221851
+ }
221852
+ }
221853
+ var init_runUsageTurn = __esm({
221854
+ "features/perchTerminal/runtime/usage/runUsageTurn.ts"() {
221855
+ "use strict";
221856
+ init_access();
221857
+ init_usageSummary();
221858
+ }
221859
+ });
221860
+
221513
221861
  // features/perchTerminal/runtime/commands/sharedSlashCommands.ts
221514
221862
  function parseSharedSlashCommand(raw) {
221515
- return parseFlockCommand(raw);
221863
+ return parseUsageCommand(raw) ?? parseFlockCommand(raw);
221864
+ }
221865
+ function sharedSlashCommandRunsAsTurn(command) {
221866
+ if (!command) return false;
221867
+ if (command.kind === "usage") return true;
221868
+ return Boolean(command.task);
221516
221869
  }
221517
221870
  function resolveTurnRunnerForPrompt(raw) {
221518
221871
  const parsed = parseSharedSlashCommand(raw);
221519
221872
  if (!parsed) return null;
221873
+ if (parsed.kind === "usage") return runUsageTurn;
221520
221874
  return runFlockTurn;
221521
221875
  }
221522
221876
  var init_sharedSlashCommands = __esm({
@@ -221524,6 +221878,8 @@ var init_sharedSlashCommands = __esm({
221524
221878
  "use strict";
221525
221879
  init_flockCommand();
221526
221880
  init_runFlockTurn();
221881
+ init_usageCommand();
221882
+ init_runUsageTurn();
221527
221883
  }
221528
221884
  });
221529
221885
 
@@ -230123,6 +230479,7 @@ function buildCliTurnInput(input, resolved) {
230123
230479
  userId,
230124
230480
  selectedSourceId: input.selectedSourceId ?? null,
230125
230481
  recentMessages: input.recentMessages ?? [],
230482
+ usageRunIds: input.usageRunIds ?? [],
230126
230483
  sessionContext: emptyPerchTerminalSessionContext({
230127
230484
  userId,
230128
230485
  workspaceId
@@ -230793,6 +231150,10 @@ var init_cliStandaloneOAuth = __esm({
230793
231150
  });
230794
231151
 
230795
231152
  // features/perchTerminal/runtime/contextMeterDisplay.ts
231153
+ function positiveInt3(value) {
231154
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
231155
+ return Math.floor(value);
231156
+ }
230796
231157
  function formatTokenK(n) {
230797
231158
  if (n < 1e3) return `${Math.max(0, Math.round(n))}`;
230798
231159
  return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
@@ -230811,12 +231172,17 @@ function resolvePrimaryContextTokens(snapshot) {
230811
231172
  if (rowTokens > 0) return rowTokens;
230812
231173
  return 0;
230813
231174
  }
230814
- function resolveContextMeterDisplay(snapshot) {
230815
- const limitTokens = snapshot?.rawContextWindowTokens ?? snapshot?.effectiveLimitTokens ?? snapshot?.contextLimitTokens ?? getEffectiveContextWindow();
231175
+ function resolveContextMeterDisplay(snapshot, activeModel) {
231176
+ const activeRawLimitTokens = positiveInt3(activeModel?.contextWindow) ?? positiveInt3(activeModel?.contextWindowTokens);
231177
+ const activeEffectiveLimitTokens = activeRawLimitTokens ? getEffectiveContextWindow({
231178
+ contextWindow: activeRawLimitTokens,
231179
+ maxOutputTokens: activeModel?.maxOutputTokens ?? null
231180
+ }) : null;
231181
+ const limitTokens = activeRawLimitTokens ?? snapshot?.rawContextWindowTokens ?? activeEffectiveLimitTokens ?? snapshot?.effectiveLimitTokens ?? snapshot?.contextLimitTokens ?? getEffectiveContextWindow();
230816
231182
  const threadTokens = snapshot?.threadContextTokens ?? snapshot?.totalContextTokens ?? snapshot?.latestSendTokens ?? 0;
230817
231183
  const primaryTokens = resolvePrimaryContextTokens(snapshot);
230818
231184
  const rawLimitTokens = Math.max(1, limitTokens);
230819
- const effectiveLimitTokens = snapshot?.effectiveLimitTokens ?? snapshot?.contextLimitTokens ?? rawLimitTokens;
231185
+ const effectiveLimitTokens = activeEffectiveLimitTokens ?? snapshot?.effectiveLimitTokens ?? snapshot?.contextLimitTokens ?? rawLimitTokens;
230820
231186
  const reservedOutputTokens = Math.max(0, rawLimitTokens - effectiveLimitTokens);
230821
231187
  const committedTokens = Math.min(rawLimitTokens, primaryTokens + reservedOutputTokens);
230822
231188
  const workerUsageTokens = snapshot?.workerUsage?.reduce((sum, worker) => sum + Math.max(0, worker.totalTokens), 0) ?? 0;
@@ -230836,6 +231202,8 @@ function resolveContextMeterDisplay(snapshot) {
230836
231202
  workerTokens,
230837
231203
  jobTotalTokens: job,
230838
231204
  limitTokens,
231205
+ effectiveLimitTokens,
231206
+ reservedOutputTokens,
230839
231207
  composerLabel,
230840
231208
  hasWorkers
230841
231209
  };
@@ -285305,7 +285673,8 @@ ${HELP_TEXT}`);
285305
285673
  trimRecentMessages(nextRecentMessages);
285306
285674
  writeCliTurnResult(result3, parsed.json, writer, {
285307
285675
  personaId: parsed.personaId,
285308
- color: shouldUseCliColor()
285676
+ color: shouldUseCliColor(),
285677
+ activeContextModel: resolveCliActiveContextModel(connection)
285309
285678
  });
285310
285679
  await admitCliLearningMemory(connection, {
285311
285680
  prompt: parsed.prompt,
@@ -285610,7 +285979,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
285610
285979
  if (!prompt) continue;
285611
285980
  if (prompt === "/exit" || prompt === "exit" || prompt === "quit") break;
285612
285981
  const sharedCommand = parseSharedSlashCommand(prompt);
285613
- const runsAsSharedTurn = Boolean(sharedCommand?.task);
285982
+ const runsAsSharedTurn = sharedSlashCommandRunsAsTurn(sharedCommand);
285614
285983
  if (prompt.startsWith("/") && !runsAsSharedTurn) {
285615
285984
  try {
285616
285985
  const commandResult = await runInteractiveSlashCommand({
@@ -285648,6 +286017,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
285648
286017
  permissionMode: state.permissionMode,
285649
286018
  threadId: state.threadId,
285650
286019
  recentMessages: state.recentMessages,
286020
+ usageRunIds: state.usageRunIds,
285651
286021
  userId: hostedContext.userId,
285652
286022
  workspaceId: hostedContext.workspaceId,
285653
286023
  permanentMemories: hostedContext.permanentMemories,
@@ -285658,13 +286028,15 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
285658
286028
  });
285659
286029
  writeCliTurnResult(result2, false, writer, {
285660
286030
  personaId: state.personaId,
285661
- color: shouldUseCliColor()
286031
+ color: shouldUseCliColor(),
286032
+ activeContextModel: resolveCliActiveContextModel(connection)
285662
286033
  });
285663
286034
  appendRecentMessage(state.recentMessages, "user", prompt);
285664
286035
  if (result2.assistantText.trim()) {
285665
286036
  appendRecentMessage(state.recentMessages, "assistant", result2.assistantText.trim());
285666
286037
  }
285667
286038
  trimRecentMessages(state.recentMessages);
286039
+ recordCliUsageRunId(state, result2.runId);
285668
286040
  state.contextSnapshot = result2.contextSnapshot ?? state.contextSnapshot;
285669
286041
  const admitted = await admitCliLearningMemory(connection, {
285670
286042
  prompt,
@@ -285803,7 +286175,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
285803
286175
  return;
285804
286176
  }
285805
286177
  const sharedTurnCommand = parseSharedSlashCommand(prompt);
285806
- if (prompt.startsWith("/") && !sharedTurnCommand?.task) {
286178
+ if (prompt.startsWith("/") && !sharedSlashCommandRunsAsTurn(sharedTurnCommand)) {
285807
286179
  const commandWriter = {
285808
286180
  stdout: (text) => {
285809
286181
  const clean = text.trim();
@@ -285844,6 +286216,8 @@ async function runInkInteractivePerchCli(writer, deps, options) {
285844
286216
  const toolNamesById = /* @__PURE__ */ new Map();
285845
286217
  const flockWorkerNames = /* @__PURE__ */ new Map();
285846
286218
  const flockLimitMeta = /* @__PURE__ */ new Map();
286219
+ const aggregateToolIds = /* @__PURE__ */ new Map();
286220
+ const aggregateToolMeta = /* @__PURE__ */ new Map();
285847
286221
  const clientRunId = createCliRunId();
285848
286222
  const externalController = new AbortController();
285849
286223
  const runtimeRun = registerRuntimeRun({
@@ -285875,6 +286249,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
285875
286249
  permissionMode: state.permissionMode,
285876
286250
  threadId: state.threadId,
285877
286251
  recentMessages: state.recentMessages,
286252
+ usageRunIds: state.usageRunIds,
285878
286253
  userId: hostedContext.userId,
285879
286254
  workspaceId: hostedContext.workspaceId,
285880
286255
  permanentMemories: hostedContext.permanentMemories,
@@ -285907,11 +286282,28 @@ async function runInkInteractivePerchCli(writer, deps, options) {
285907
286282
  case "tool_call_started": {
285908
286283
  const toolId = event.toolCallId ?? `${event.toolName}-${Date.now()}`;
285909
286284
  const name = humanizeCliToolName(event.toolName);
286285
+ const aggregateKind = cliToolAggregateKind(event.toolName);
285910
286286
  toolInputsById.current.set(toolId, {
285911
286287
  toolName: event.toolName,
285912
286288
  input: event.input ?? {}
285913
286289
  });
285914
286290
  toolNamesById.set(toolId, name);
286291
+ if (aggregateKind) {
286292
+ aggregateToolIds.set(toolId, aggregateKind);
286293
+ const meta2 = getCliToolAggregateMeta(aggregateToolMeta, aggregateKind);
286294
+ meta2.started += 1;
286295
+ meta2.pending += 1;
286296
+ appendCliToolAggregateDetail(meta2, event.toolName, event.input ?? {}, "started");
286297
+ updateToolItem(cliToolAggregateItemId(aggregateKind), {
286298
+ label: "tool",
286299
+ text: renderCliToolAggregateText(aggregateKind, meta2),
286300
+ tone: "muted",
286301
+ detailLines: meta2.detailLines,
286302
+ expanded: false
286303
+ });
286304
+ setWorkingText(cliToolAggregateWorkingText(aggregateKind));
286305
+ break;
286306
+ }
285915
286307
  const fileStart = buildFileToolDisplay(event.toolName, event.input ?? {}, "running");
285916
286308
  if (fileStart) richToolIds.current.add(toolId);
285917
286309
  updateToolItem(toolId, {
@@ -285927,6 +286319,22 @@ async function runInkInteractivePerchCli(writer, deps, options) {
285927
286319
  case "tool_call_completed": {
285928
286320
  const toolId = event.toolCallId ?? `${event.toolName}-${Date.now()}`;
285929
286321
  const stored = toolInputsById.current.get(toolId);
286322
+ const aggregateKind = aggregateToolIds.get(toolId);
286323
+ if (aggregateKind) {
286324
+ const meta2 = getCliToolAggregateMeta(aggregateToolMeta, aggregateKind);
286325
+ meta2.pending = Math.max(0, meta2.pending - 1);
286326
+ meta2.done += 1;
286327
+ appendCliToolAggregateDetail(meta2, event.toolName, stored?.input ?? {}, "done");
286328
+ updateToolItem(cliToolAggregateItemId(aggregateKind), {
286329
+ label: "tool",
286330
+ text: renderCliToolAggregateText(aggregateKind, meta2),
286331
+ tone: meta2.failed > 0 ? "danger" : "success",
286332
+ detailLines: meta2.detailLines,
286333
+ expanded: false
286334
+ });
286335
+ setWorkingText("thinking");
286336
+ break;
286337
+ }
285930
286338
  const fileDone = buildFileToolDisplay(event.toolName, stored?.input ?? {}, "done");
285931
286339
  if (fileDone) {
285932
286340
  richToolIds.current.add(toolId);
@@ -285956,6 +286364,22 @@ async function runInkInteractivePerchCli(writer, deps, options) {
285956
286364
  case "tool_call_failed": {
285957
286365
  const toolId = event.toolCallId ?? `${event.toolName}-${Date.now()}`;
285958
286366
  const name = toolNamesById.get(toolId) ?? humanizeCliToolName(event.toolName);
286367
+ const stored = toolInputsById.current.get(toolId);
286368
+ const aggregateKind = aggregateToolIds.get(toolId);
286369
+ if (aggregateKind) {
286370
+ const meta2 = getCliToolAggregateMeta(aggregateToolMeta, aggregateKind);
286371
+ meta2.pending = Math.max(0, meta2.pending - 1);
286372
+ meta2.failed += 1;
286373
+ appendCliToolAggregateDetail(meta2, event.toolName, stored?.input ?? {}, "failed", event.error);
286374
+ updateToolItem(cliToolAggregateItemId(aggregateKind), {
286375
+ label: "tool",
286376
+ text: renderCliToolAggregateText(aggregateKind, meta2),
286377
+ tone: "danger",
286378
+ detailLines: meta2.detailLines,
286379
+ expanded: false
286380
+ });
286381
+ break;
286382
+ }
285959
286383
  updateToolItem(toolId, {
285960
286384
  label: "need",
285961
286385
  text: `${name} \xB7 ${event.error ?? "needs attention"}`,
@@ -286202,6 +286626,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
286202
286626
  appendRecentMessage(state.recentMessages, "user", prompt);
286203
286627
  if (assistantText) appendRecentMessage(state.recentMessages, "assistant", assistantText);
286204
286628
  trimRecentMessages(state.recentMessages);
286629
+ recordCliUsageRunId(state, result2.runId);
286205
286630
  state.contextSnapshot = result2.contextSnapshot ?? state.contextSnapshot;
286206
286631
  const admitted = await admitCliLearningMemory(connection, {
286207
286632
  prompt,
@@ -286515,7 +286940,10 @@ async function runInteractiveSlashCommand(input) {
286515
286940
  `);
286516
286941
  return "continue";
286517
286942
  case "context":
286518
- input.writer.stdout(renderCliContextDetails(input.state.contextSnapshot));
286943
+ input.writer.stdout(renderCliContextDetails(
286944
+ input.state.contextSnapshot,
286945
+ resolveCliActiveContextModel(input.getConnection?.() ?? null)
286946
+ ));
286519
286947
  return "continue";
286520
286948
  case "memoryAudit": {
286521
286949
  const audit = explainLearningAdmission({
@@ -286590,6 +287018,7 @@ async function runInteractiveSlashCommand(input) {
286590
287018
  }
286591
287019
  input.state.threadId = parsed.value === "new" ? `cli-${Date.now()}` : parsed.value;
286592
287020
  input.state.recentMessages = [];
287021
+ input.state.usageRunIds = [];
286593
287022
  input.state.contextSnapshot = null;
286594
287023
  input.state.persistedThreadUpdatedAt = null;
286595
287024
  await hydrateInteractiveCliState(input.state);
@@ -286597,6 +287026,7 @@ async function runInteractiveSlashCommand(input) {
286597
287026
  return "continue";
286598
287027
  case "clear":
286599
287028
  input.state.recentMessages = [];
287029
+ input.state.usageRunIds = [];
286600
287030
  input.state.contextSnapshot = null;
286601
287031
  input.state.persistedThreadUpdatedAt = null;
286602
287032
  clearThreadSession(input.state.threadId);
@@ -286685,6 +287115,7 @@ function renderInteractiveStatus(state, connection, session, workspaceId) {
286685
287115
  const signedIn = session === void 0 ? isCliModelConnectionReady(connection) : isStoredCliAuthSessionUsable(session);
286686
287116
  const connectionStatus = isCliModelConnectionReady(connection) ? "connected" : "locked \xB7 run /login";
286687
287117
  const color = shouldUseCliColor();
287118
+ const activeContextModel = resolveCliActiveContextModel(connection);
286688
287119
  const lines = [
286689
287120
  ["version", CLI_PACKAGE_VERSION],
286690
287121
  ["cwd", state.cwd],
@@ -286697,7 +287128,7 @@ function renderInteractiveStatus(state, connection, session, workspaceId) {
286697
287128
  ["mode", state.chatMode],
286698
287129
  ["persona", state.personaId],
286699
287130
  ["thread", state.threadId],
286700
- ["context", renderCliContextMeter(state.contextSnapshot)],
287131
+ ["context", renderCliContextMeter(state.contextSnapshot, activeContextModel)],
286701
287132
  ["saved", state.persistedThreadUpdatedAt ? new Date(state.persistedThreadUpdatedAt).toLocaleString() : "(new thread)"],
286702
287133
  ["local-tools", state.cliLocalTools ? "on" : "off"]
286703
287134
  ];
@@ -286846,6 +287277,7 @@ function createInteractiveCliState(options) {
286846
287277
  cliLocalTools: options.cliLocalTools ?? true,
286847
287278
  appUrl: resolveCliAppUrl(options.appUrl ?? null, null),
286848
287279
  recentMessages: [],
287280
+ usageRunIds: [],
286849
287281
  contextSnapshot: null,
286850
287282
  persistedThreadUpdatedAt: null
286851
287283
  };
@@ -286862,6 +287294,7 @@ async function syncInteractiveCliThreadScope(state, connection, options = {}) {
286862
287294
  if (!options.force && state.threadScopeKey === nextScopeKey) return;
286863
287295
  state.threadScopeKey = nextScopeKey;
286864
287296
  state.recentMessages = [];
287297
+ state.usageRunIds = [];
286865
287298
  state.contextSnapshot = null;
286866
287299
  state.persistedThreadUpdatedAt = null;
286867
287300
  clearThreadSession(state.threadId);
@@ -287045,21 +287478,21 @@ function isPermanentMemoryLike2(value) {
287045
287478
  const memory = value;
287046
287479
  return typeof memory.id === "string" && typeof memory.title === "string" && typeof memory.body === "string";
287047
287480
  }
287048
- function renderCliContextMeter(snapshot) {
287481
+ function renderCliContextMeter(snapshot, activeModel) {
287049
287482
  if (!snapshot) return "Context";
287050
- const meter = resolveContextMeterDisplay(snapshot);
287483
+ const meter = resolveContextMeterDisplay(snapshot, activeModel);
287051
287484
  const percent2 = snapshot.contextPercentage ?? Math.round(meter.committedTokens / Math.max(1, meter.limitTokens) * 100);
287052
287485
  const parts = [`${meter.composerLabel}`, `${Math.max(0, Math.min(100, percent2))}%`];
287053
287486
  const compacted = snapshot.compactedRowCount ?? 0;
287054
287487
  if (snapshot.compacted || compacted > 0) parts.push(`compacted ${compacted}`);
287055
287488
  return parts.join(" \xB7 ");
287056
287489
  }
287057
- function renderCliContextDetails(snapshot) {
287490
+ function renderCliContextDetails(snapshot, activeModel) {
287058
287491
  if (!snapshot) return "context no meter yet\n";
287059
- const meter = resolveContextMeterDisplay(snapshot);
287492
+ const meter = resolveContextMeterDisplay(snapshot, activeModel);
287060
287493
  const percent2 = snapshot.contextPercentage ?? Math.round(meter.committedTokens / Math.max(1, meter.limitTokens) * 100);
287061
287494
  const lines = [
287062
- ["context", renderCliContextMeter(snapshot)],
287495
+ ["context", renderCliContextMeter(snapshot, activeModel)],
287063
287496
  ["thread", `${formatCliTokenCount(meter.threadTokens)} tokens`],
287064
287497
  ["limit", `${formatCliTokenCount(meter.limitTokens)} tokens`],
287065
287498
  ["fill", `${Math.max(0, Math.min(100, percent2))}%`],
@@ -287079,6 +287512,10 @@ function humanizeCliToolName(name) {
287079
287512
  function isCliModelConnectionReady(connection) {
287080
287513
  return Boolean(connection?.authenticated && connection.founderModelSelection);
287081
287514
  }
287515
+ function resolveCliActiveContextModel(connection) {
287516
+ if (!connection?.founderModelSelection) return null;
287517
+ return resolveModelForLane(connection.founderModelSelection, "chat");
287518
+ }
287082
287519
  function renderCliAuthSummary(connection) {
287083
287520
  if (isCliModelConnectionReady(connection)) {
287084
287521
  return `signed in${connection.email ? ` as ${connection.email}` : ""}`;
@@ -287415,6 +287852,67 @@ function buildFileToolDisplay(toolName, input, phase, summary) {
287415
287852
  ]
287416
287853
  };
287417
287854
  }
287855
+ function cliToolAggregateKind(toolName) {
287856
+ const normalized = toolName.toLowerCase();
287857
+ if (normalized === "readlocalfile" || normalized === "readlocalsourcefile") return "read";
287858
+ if (normalized === "glob" || normalized === "grep" || normalized === "listlocalsources" || normalized === "statpath" || normalized === "validateworkspaceroot") return "search";
287859
+ return null;
287860
+ }
287861
+ function getCliToolAggregateMeta(store, kind) {
287862
+ const existing = store.get(kind);
287863
+ if (existing) return existing;
287864
+ const created = {
287865
+ started: 0,
287866
+ done: 0,
287867
+ failed: 0,
287868
+ pending: 0,
287869
+ detailLines: []
287870
+ };
287871
+ store.set(kind, created);
287872
+ return created;
287873
+ }
287874
+ function cliToolAggregateItemId(kind) {
287875
+ return `tool-aggregate-${kind}`;
287876
+ }
287877
+ function cliToolAggregateWorkingText(kind) {
287878
+ return kind === "read" ? "reading files" : "searching";
287879
+ }
287880
+ function renderCliToolAggregateText(kind, meta) {
287881
+ const completed = meta.done + meta.failed;
287882
+ const count = Math.max(completed, meta.started);
287883
+ const noun = kind === "read" ? `file${count === 1 ? "" : "s"}` : `path${count === 1 ? "" : "s"}`;
287884
+ const verb = kind === "read" ? "read" : "searched";
287885
+ const status = meta.pending > 0 ? `${meta.pending} running` : "done";
287886
+ const failed = meta.failed > 0 ? ` \xB7 ${meta.failed} failed` : "";
287887
+ return `${verb} ${count} ${noun} \xB7 ${status}${failed} \xB7 ctrl-e`;
287888
+ }
287889
+ function appendCliToolAggregateDetail(meta, toolName, input, status, error) {
287890
+ const label = humanizeCliToolName(toolName);
287891
+ const target = describeCliToolAggregateInput(toolName, input);
287892
+ const tail = error ? ` \xB7 ${error}` : "";
287893
+ meta.detailLines.push({
287894
+ tone: "meta",
287895
+ text: `${label}${target ? ` \xB7 ${target}` : ""} \xB7 ${status}${tail}`
287896
+ });
287897
+ if (meta.detailLines.length > INK_DETAIL_LINE_LIMIT) {
287898
+ meta.detailLines = meta.detailLines.slice(-INK_DETAIL_LINE_LIMIT);
287899
+ }
287900
+ }
287901
+ function describeCliToolAggregateInput(toolName, input) {
287902
+ const normalized = toolName.toLowerCase();
287903
+ if (normalized === "glob") {
287904
+ const pattern = stringValue8(input.pattern) ?? stringValue8(input.glob) ?? "*";
287905
+ const base = stringValue8(input.path) ?? stringValue8(input.cwd) ?? ".";
287906
+ return `${pattern} in ${shortFilePath(base)}`;
287907
+ }
287908
+ if (normalized === "grep") {
287909
+ const pattern = stringValue8(input.pattern) ?? stringValue8(input.query) ?? "";
287910
+ const base = stringValue8(input.path) ?? stringValue8(input.cwd) ?? ".";
287911
+ return `${pattern || "pattern"} in ${shortFilePath(base)}`;
287912
+ }
287913
+ const filePath = stringValue8(input.path) ?? stringValue8(input.filePath) ?? stringValue8(input.localSourceId) ?? stringValue8(input.cwd);
287914
+ return filePath ? shortFilePath(filePath) : null;
287915
+ }
287418
287916
  function describeChangeSummary(summary) {
287419
287917
  const kind = summary.changeKind ?? "changed";
287420
287918
  const added = summary.linesAdded ?? 0;
@@ -287535,6 +288033,11 @@ function trimRecentMessages(messages) {
287535
288033
  messages.splice(0, messages.length - 30);
287536
288034
  }
287537
288035
  }
288036
+ function recordCliUsageRunId(state, runId) {
288037
+ const cleaned = runId?.trim();
288038
+ if (!cleaned) return;
288039
+ state.usageRunIds = [.../* @__PURE__ */ new Set([...state.usageRunIds, cleaned])].slice(-250);
288040
+ }
287538
288041
  function createCliRunId() {
287539
288042
  return `cli-turn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
287540
288043
  }
@@ -287657,7 +288160,7 @@ function writeCliTurnResult(result2, json, writer, options = {}) {
287657
288160
  color: options.color,
287658
288161
  streamLabels: true
287659
288162
  });
287660
- const contextLine = `${paint("context".padEnd(12), "muted", options.color === true)} ${renderCliContextMeter(result2.contextSnapshot)}
288163
+ const contextLine = `${paint("context".padEnd(12), "muted", options.color === true)} ${renderCliContextMeter(result2.contextSnapshot, options.activeContextModel)}
287661
288164
  `;
287662
288165
  writer.stdout(`${rendered}${rendered.endsWith("\n") ? "" : "\n"}${contextLine}`);
287663
288166
  }
@@ -287813,6 +288316,7 @@ var init_perch_cli = __esm({
287813
288316
  init_cliAuthSession();
287814
288317
  init_cliStandaloneOAuth();
287815
288318
  init_contextMeterDisplay();
288319
+ init_modelRegistry();
287816
288320
  init_threadSession();
287817
288321
  init_runRegistry();
287818
288322
  init_learningMemory();
@@ -287875,6 +288379,7 @@ Commands:
287875
288379
  /help Show this list.
287876
288380
  /status Show cwd, auth, mode, persona, permission, and thread.
287877
288381
  /skills [name] List Perch core skills, or show one skill body.
288382
+ /usage Show tokens and estimated cost for this session.
287878
288383
  /cwd [dir] Show or change the working directory.
287879
288384
  /permission [mode] Show or set default, auto_review, take_the_wheel, or plan.
287880
288385
  /permissions [mode] Alias for /permission.
@@ -287934,6 +288439,7 @@ Commands:
287934
288439
  ];
287935
288440
  PERCH_SPLASH_COMMANDS = [
287936
288441
  ["/status", "show auth, route, tools, and thread"],
288442
+ ["/usage", "show session tokens and estimated cost"],
287937
288443
  ["/skills", "show reusable operator skills"],
287938
288444
  ["/persona", "swap saffron or quill"],
287939
288445
  ["/permission", "change autonomy for the next turns"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perchai-cli",
3
- "version": "2.4.26",
3
+ "version": "2.4.29",
4
4
  "description": "Perch AI command-line interface",
5
5
  "bin": {
6
6
  "perch": "bin/perch"