perchai-cli 2.4.28 → 2.4.30

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 +326 -12
  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,7 +75566,6 @@ 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";
75570
75569
  init_generateAPAuditPacket();
75571
75570
  init_inventoryFolder();
75572
75571
  init_loadBusinessTables();
@@ -83126,6 +83125,7 @@ function listFinancialRoleIds() {
83126
83125
  var FINANCIAL_ROLE_REGISTRY, evidenceScoutManifest;
83127
83126
  var init_financialRoles = __esm({
83128
83127
  "features/perchTerminal/agentPlatform/financialRoles.ts"() {
83128
+ "use strict";
83129
83129
  FINANCIAL_ROLE_REGISTRY = /* @__PURE__ */ new Map();
83130
83130
  evidenceScoutManifest = {
83131
83131
  workerId: "evidence_scout",
@@ -86691,6 +86691,7 @@ function truncateHistoryLine(value, max2) {
86691
86691
  }
86692
86692
  var init_operatorTruth = __esm({
86693
86693
  "features/perchTerminal/runtime/operatorTruth.ts"() {
86694
+ "use strict";
86694
86695
  }
86695
86696
  });
86696
86697
 
@@ -118883,8 +118884,12 @@ var init_reasoningExtractor = __esm({
118883
118884
 
118884
118885
  // features/perchTerminal/runtime/modelRouter.ts
118885
118886
  async function callModelRouter(_config, request, opts) {
118887
+ const explicitProxyUrl = resolveModelCallProxyUrl(opts?.cliServerAppUrl ?? null);
118886
118888
  if (typeof window !== "undefined") {
118887
- return callModelRouterViaServer(request, opts);
118889
+ return callModelRouterViaServer(request, opts, explicitProxyUrl ?? void 0);
118890
+ }
118891
+ if (explicitProxyUrl) {
118892
+ return callModelRouterViaServer(request, opts, explicitProxyUrl);
118888
118893
  }
118889
118894
  const nodeProxyUrl = resolveNodeModelCallProxyUrl();
118890
118895
  if (nodeProxyUrl) {
@@ -119037,7 +119042,7 @@ async function callModelRouterViaServer(request, opts, endpoint = "/api/perch-te
119037
119042
  headers: {
119038
119043
  "Content-Type": "application/json",
119039
119044
  ...hasStreamingHandler ? { Accept: "text/event-stream" } : {},
119040
- ...nodeModelProxyAuthHeader()
119045
+ ...modelProxyAuthHeader(opts?.cliServerAccessToken ?? null)
119041
119046
  },
119042
119047
  body,
119043
119048
  signal: _signal
@@ -119201,7 +119206,10 @@ function waitForModelProxyRetry(attempt, signal) {
119201
119206
  }
119202
119207
  function resolveNodeModelCallProxyUrl() {
119203
119208
  if (typeof process === "undefined") return null;
119204
- const raw = process.env[MODEL_CALL_PROXY_ENV]?.trim();
119209
+ return resolveModelCallProxyUrl(process.env[MODEL_CALL_PROXY_ENV]?.trim() ?? null);
119210
+ }
119211
+ function resolveModelCallProxyUrl(rawInput) {
119212
+ const raw = rawInput?.trim();
119205
119213
  if (!raw) return null;
119206
119214
  if (raw.endsWith("/api/perch-terminal/model-call")) return raw;
119207
119215
  try {
@@ -119214,9 +119222,8 @@ function resolveNodeModelCallProxyUrl() {
119214
119222
  return null;
119215
119223
  }
119216
119224
  }
119217
- function nodeModelProxyAuthHeader() {
119218
- if (typeof process === "undefined") return {};
119219
- const token = process.env[MODEL_CALL_PROXY_TOKEN_ENV]?.trim();
119225
+ function modelProxyAuthHeader(explicitToken) {
119226
+ const token = explicitToken?.trim() || (typeof process !== "undefined" ? process.env[MODEL_CALL_PROXY_TOKEN_ENV]?.trim() : "");
119220
119227
  return token ? { Authorization: `Bearer ${token}` } : {};
119221
119228
  }
119222
119229
  function getRouterAvailabilityReport() {
@@ -217486,7 +217493,9 @@ async function runModelToolLoop(input) {
217486
217493
  runId,
217487
217494
  lane: activeLane,
217488
217495
  source: "tool_loop"
217489
- })
217496
+ }),
217497
+ cliServerAppUrl: input.cliServerAppUrl ?? null,
217498
+ cliServerAccessToken: input.cliServerAccessToken ?? null
217490
217499
  }
217491
217500
  );
217492
217501
  result2 = routerResult;
@@ -217663,6 +217672,8 @@ async function runModelToolLoop(input) {
217663
217672
  runId,
217664
217673
  userId: input.userId,
217665
217674
  workspaceId: input.workspaceId,
217675
+ cliServerAppUrl: input.cliServerAppUrl ?? null,
217676
+ cliServerAccessToken: input.cliServerAccessToken ?? null,
217666
217677
  signal,
217667
217678
  onEvent
217668
217679
  });
@@ -217711,6 +217722,8 @@ async function runModelToolLoop(input) {
217711
217722
  runId,
217712
217723
  userId: input.userId,
217713
217724
  workspaceId: input.workspaceId,
217725
+ cliServerAppUrl: input.cliServerAppUrl ?? null,
217726
+ cliServerAccessToken: input.cliServerAccessToken ?? null,
217714
217727
  signal,
217715
217728
  onEvent
217716
217729
  });
@@ -218241,6 +218254,8 @@ async function runModelToolLoop(input) {
218241
218254
  runId,
218242
218255
  userId: input.userId,
218243
218256
  workspaceId: input.workspaceId,
218257
+ cliServerAppUrl: input.cliServerAppUrl ?? null,
218258
+ cliServerAccessToken: input.cliServerAccessToken ?? null,
218244
218259
  signal,
218245
218260
  onEvent
218246
218261
  });
@@ -218313,7 +218328,9 @@ async function requestClosingResponse(input) {
218313
218328
  runId: input.runId,
218314
218329
  lane: input.lane,
218315
218330
  source: "tool_loop_closing"
218316
- })
218331
+ }),
218332
+ cliServerAppUrl: input.cliServerAppUrl ?? null,
218333
+ cliServerAccessToken: input.cliServerAccessToken ?? null
218317
218334
  }
218318
218335
  );
218319
218336
  if (!routerResult.ok) return null;
@@ -218716,6 +218733,8 @@ async function finalizeChatAnswerAfterReadSuppression(input) {
218716
218733
  runId: input.runId,
218717
218734
  userId: input.userId,
218718
218735
  workspaceId: input.workspaceId,
218736
+ cliServerAppUrl: input.cliServerAppUrl ?? null,
218737
+ cliServerAccessToken: input.cliServerAccessToken ?? null,
218719
218738
  signal: input.signal,
218720
218739
  onEvent: input.onEvent
218721
218740
  }) : null;
@@ -219611,6 +219630,10 @@ ${contextBlock}
219611
219630
  selectedSourceId: ctx.selectedSourceId,
219612
219631
  supabaseConfigured: ctx.supabaseConfigured,
219613
219632
  supabase: ctx.supabase,
219633
+ cliServerAppUrl: ctx.cliServerAppUrl,
219634
+ cliServerAccessToken: ctx.cliServerAccessToken,
219635
+ marketDeskProxyAppUrl: ctx.marketDeskProxyAppUrl,
219636
+ marketDeskProxyAccessToken: ctx.marketDeskProxyAccessToken,
219614
219637
  runId: runtimeWorkerRunId,
219615
219638
  chatMode: "agents",
219616
219639
  forceToolUse: DELIVERY_WORKER_IDS.has(args.workerId),
@@ -221530,6 +221553,10 @@ function buildSpawnContext(input, flockId, signal, emit, worker) {
221530
221553
  selectedSourceId: input.selectedSourceId,
221531
221554
  supabaseConfigured: input.supabaseConfigured,
221532
221555
  supabase: input.supabase ?? null,
221556
+ cliServerAppUrl: input.cliServerAppUrl ?? null,
221557
+ cliServerAccessToken: input.cliServerAccessToken ?? null,
221558
+ marketDeskProxyAppUrl: input.marketDeskProxyAppUrl ?? null,
221559
+ marketDeskProxyAccessToken: input.marketDeskProxyAccessToken ?? null,
221533
221560
  runId: flockId,
221534
221561
  // Default: the currently selected Perch model path. Only an explicit
221535
221562
  // "use <model> as <role>" request in the task pins this worker elsewhere.
@@ -221593,13 +221620,282 @@ var init_runFlockTurn = __esm({
221593
221620
  }
221594
221621
  });
221595
221622
 
221623
+ // features/perchTerminal/runtime/usage/usageCommand.ts
221624
+ function parseUsageCommand(raw) {
221625
+ const trimmed = raw.trim();
221626
+ if (!/^\/usage(?:\s|$)/i.test(trimmed)) return null;
221627
+ const task = trimmed.slice("/usage".length).trim();
221628
+ return task ? { kind: "usage", task } : { kind: "usage" };
221629
+ }
221630
+ var init_usageCommand = __esm({
221631
+ "features/perchTerminal/runtime/usage/usageCommand.ts"() {
221632
+ "use strict";
221633
+ }
221634
+ });
221635
+
221636
+ // lib/perch-ai/access.ts
221637
+ function hasPrivilegedPerchAiRole(memberships) {
221638
+ return memberships.some(
221639
+ (membership) => membership.status === "active" && (membership.role === "admin" || membership.role === "internal")
221640
+ );
221641
+ }
221642
+ function canShowPerchDevTools(membershipRole) {
221643
+ if (!membershipRole) return false;
221644
+ return hasPrivilegedPerchAiRole([
221645
+ {
221646
+ role: membershipRole,
221647
+ status: "active"
221648
+ }
221649
+ ]);
221650
+ }
221651
+ var init_access = __esm({
221652
+ "lib/perch-ai/access.ts"() {
221653
+ }
221654
+ });
221655
+
221656
+ // features/perchTerminal/runtime/usage/usageSummary.ts
221657
+ function collectUsageRunIds(input) {
221658
+ const runIds = /* @__PURE__ */ new Set();
221659
+ for (const runId of input.usageRunIds ?? []) {
221660
+ const cleaned = runId.trim();
221661
+ if (cleaned) runIds.add(cleaned);
221662
+ }
221663
+ for (const message of input.recentMessages ?? []) {
221664
+ if (message.kind !== "text" || message.role !== "assistant") continue;
221665
+ const stateRunId = message.operatorState?.runId?.trim();
221666
+ if (stateRunId) runIds.add(stateRunId);
221667
+ for (const event of message.operatorState?.events ?? []) {
221668
+ if (event.type === "turn_started" && event.runId.trim()) runIds.add(event.runId.trim());
221669
+ }
221670
+ for (const event of message.transcriptEvents ?? []) {
221671
+ if (event.type === "turn_started" && event.runId.trim()) runIds.add(event.runId.trim());
221672
+ }
221673
+ }
221674
+ return Array.from(runIds).slice(-250);
221675
+ }
221676
+ function summarizeInferenceUsageRows(rows, options = {}) {
221677
+ const promptTokens = sumRows(rows, "prompt_tokens");
221678
+ const completionTokens = sumRows(rows, "completion_tokens");
221679
+ const cachedTokens = sumRows(rows, "cached_tokens");
221680
+ const totalTokens = sumRows(rows, "total_tokens");
221681
+ const estimatedCostUsd = rows.reduce((sum, row) => sum + readNumber(row.estimated_cost_usd), 0);
221682
+ const created = rows.map((row) => row.created_at).filter((value) => typeof value === "string" && value.length > 0).sort();
221683
+ const summary = {
221684
+ ok: true,
221685
+ calls: rows.length,
221686
+ promptTokens,
221687
+ completionTokens,
221688
+ cachedTokens,
221689
+ totalTokens,
221690
+ estimatedCostUsd,
221691
+ runCount: new Set(options.runIds?.length ? options.runIds : rows.map((row) => row.run_id).filter(Boolean)).size,
221692
+ since: created[0] ?? null,
221693
+ through: created[created.length - 1] ?? null,
221694
+ showModelDetails: Boolean(options.showModelDetails)
221695
+ };
221696
+ if (summary.showModelDetails) {
221697
+ summary.modelBreakdown = buildModelBreakdown(rows);
221698
+ }
221699
+ return summary;
221700
+ }
221701
+ function formatUsageSummaryText(summary) {
221702
+ const lines = [
221703
+ "Usage this session",
221704
+ `- Calls: ${formatInteger(summary.calls)}`,
221705
+ `- Tokens: ${formatInteger(summary.totalTokens)} total (${formatInteger(summary.promptTokens)} input \xB7 ${formatInteger(summary.completionTokens)} output${summary.cachedTokens ? ` \xB7 ${formatInteger(summary.cachedTokens)} cached` : ""})`,
221706
+ `- Estimated cost: ${formatUsd(summary.estimatedCostUsd)}`
221707
+ ];
221708
+ if (summary.calls === 0) {
221709
+ lines.push("- Nothing has been recorded for this session yet.");
221710
+ }
221711
+ if (summary.showModelDetails && summary.modelBreakdown?.length) {
221712
+ lines.push("", "Internal detail:");
221713
+ for (const row of summary.modelBreakdown.slice(0, 8)) {
221714
+ lines.push(
221715
+ `- ${row.label}: ${formatInteger(row.totalTokens)} tokens \xB7 ${formatUsd(row.estimatedCostUsd)} \xB7 ${formatInteger(row.calls)} calls`
221716
+ );
221717
+ }
221718
+ }
221719
+ return lines.join("\n");
221720
+ }
221721
+ function buildModelBreakdown(rows) {
221722
+ const byModel = /* @__PURE__ */ new Map();
221723
+ for (const row of rows) {
221724
+ const label = [row.model_option_id, row.provider, row.model].filter((value) => typeof value === "string" && value.trim()).join(" \xB7 ") || "unknown model";
221725
+ const current = byModel.get(label) ?? {
221726
+ label,
221727
+ calls: 0,
221728
+ promptTokens: 0,
221729
+ completionTokens: 0,
221730
+ cachedTokens: 0,
221731
+ totalTokens: 0,
221732
+ estimatedCostUsd: 0
221733
+ };
221734
+ current.calls += 1;
221735
+ current.promptTokens += readNumber(row.prompt_tokens);
221736
+ current.completionTokens += readNumber(row.completion_tokens);
221737
+ current.cachedTokens += readNumber(row.cached_tokens);
221738
+ current.totalTokens += readNumber(row.total_tokens);
221739
+ current.estimatedCostUsd += readNumber(row.estimated_cost_usd);
221740
+ byModel.set(label, current);
221741
+ }
221742
+ return Array.from(byModel.values()).sort((a, b2) => b2.estimatedCostUsd - a.estimatedCostUsd);
221743
+ }
221744
+ function sumRows(rows, key) {
221745
+ return rows.reduce((sum, row) => sum + readNumber(row[key]), 0);
221746
+ }
221747
+ function readNumber(value) {
221748
+ if (typeof value === "number" && Number.isFinite(value)) return value;
221749
+ if (typeof value === "string") {
221750
+ const parsed = Number(value);
221751
+ return Number.isFinite(parsed) ? parsed : 0;
221752
+ }
221753
+ return 0;
221754
+ }
221755
+ function formatInteger(value) {
221756
+ return Math.max(0, Math.round(value)).toLocaleString("en-US");
221757
+ }
221758
+ function formatUsd(value) {
221759
+ if (!Number.isFinite(value) || value <= 0) return "$0.00";
221760
+ return value < 0.01 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
221761
+ }
221762
+ var init_usageSummary = __esm({
221763
+ "features/perchTerminal/runtime/usage/usageSummary.ts"() {
221764
+ "use strict";
221765
+ }
221766
+ });
221767
+
221768
+ // features/perchTerminal/runtime/usage/runUsageTurn.ts
221769
+ async function runUsageTurn(input, deps) {
221770
+ const startedAt = Date.now();
221771
+ const runId = input.clientRunId ?? `usage-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
221772
+ const now14 = () => (/* @__PURE__ */ new Date()).toISOString();
221773
+ const events = [];
221774
+ const emit = (event) => {
221775
+ events.push(event);
221776
+ deps.onEvent?.(event);
221777
+ };
221778
+ emit({ type: "turn_started", runId, ts: now14(), isSystemNotification: false });
221779
+ let userMessageId = null;
221780
+ if (input.workspaceId && input.skipUserMessagePersistence !== true) {
221781
+ userMessageId = await deps.persistUserMessage({
221782
+ workspaceId: input.workspaceId,
221783
+ threadId: input.threadId,
221784
+ role: "user",
221785
+ mode: input.chatMode,
221786
+ content: input.trimmedInput
221787
+ });
221788
+ if (userMessageId) {
221789
+ emit({ type: "user_message_persisted", messageId: userMessageId, ts: now14() });
221790
+ }
221791
+ }
221792
+ const runIds = collectUsageRunIds({
221793
+ usageRunIds: input.usageRunIds,
221794
+ recentMessages: input.recentMessages
221795
+ });
221796
+ const showModelDetails = canShowPerchDevTools(input.sessionContext.membershipRole);
221797
+ const summary = await resolveUsageSummary(input, runIds, showModelDetails);
221798
+ const assistantText = formatUsageSummaryText(summary);
221799
+ let assistantMessageId = null;
221800
+ if (input.workspaceId) {
221801
+ assistantMessageId = await deps.persistAssistantMessage({
221802
+ workspaceId: input.workspaceId,
221803
+ threadId: input.threadId,
221804
+ role: "assistant",
221805
+ mode: input.chatMode,
221806
+ content: assistantText,
221807
+ contentJson: {
221808
+ slashCommand: "usage",
221809
+ turnSummary: assistantText,
221810
+ usageSummary: summary,
221811
+ transcriptEvents: events
221812
+ }
221813
+ });
221814
+ }
221815
+ emit({
221816
+ type: "assistant_response",
221817
+ text: assistantText,
221818
+ messageId: assistantMessageId ?? runId,
221819
+ ts: now14()
221820
+ });
221821
+ const durationMs = Date.now() - startedAt;
221822
+ emit({ type: "turn_completed", runId, durationMs, status: "completed", ts: now14() });
221823
+ return {
221824
+ status: "completed",
221825
+ assistantText,
221826
+ messageId: assistantMessageId ?? runId,
221827
+ runId,
221828
+ durationMs
221829
+ };
221830
+ }
221831
+ async function resolveUsageSummary(input, runIds, showModelDetails) {
221832
+ if (runIds.length === 0) {
221833
+ return summarizeInferenceUsageRows([], { showModelDetails, runIds });
221834
+ }
221835
+ if (input.supabase && input.userId && input.workspaceId) {
221836
+ const rows = await fetchUsageRows(input.supabase, {
221837
+ userId: input.userId,
221838
+ workspaceId: input.workspaceId,
221839
+ runIds
221840
+ });
221841
+ return summarizeInferenceUsageRows(rows, { showModelDetails, runIds });
221842
+ }
221843
+ const viaServer = await fetchUsageSummaryFromServer(input, runIds);
221844
+ if (viaServer) return viaServer;
221845
+ return summarizeInferenceUsageRows([], { showModelDetails, runIds });
221846
+ }
221847
+ async function fetchUsageRows(supabase, input) {
221848
+ 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);
221849
+ if (error) throw error;
221850
+ return data ?? [];
221851
+ }
221852
+ async function fetchUsageSummaryFromServer(input, runIds) {
221853
+ const appUrl = input.cliServerAppUrl?.trim();
221854
+ const token = input.cliServerAccessToken?.trim();
221855
+ if (!appUrl || !token || typeof fetch !== "function") return null;
221856
+ const controller = new AbortController();
221857
+ const timeout = setTimeout(() => controller.abort(), 5e3);
221858
+ try {
221859
+ const response = await fetch(`${appUrl.replace(/\/+$/, "")}/api/perch-terminal/usage`, {
221860
+ method: "POST",
221861
+ headers: {
221862
+ Accept: "application/json",
221863
+ "Content-Type": "application/json",
221864
+ Authorization: `Bearer ${token}`
221865
+ },
221866
+ body: JSON.stringify({ runIds }),
221867
+ signal: controller.signal
221868
+ });
221869
+ if (!response.ok) return null;
221870
+ const payload = await response.json();
221871
+ return payload.ok ? payload.summary : null;
221872
+ } catch {
221873
+ return null;
221874
+ } finally {
221875
+ clearTimeout(timeout);
221876
+ }
221877
+ }
221878
+ var init_runUsageTurn = __esm({
221879
+ "features/perchTerminal/runtime/usage/runUsageTurn.ts"() {
221880
+ "use strict";
221881
+ init_access();
221882
+ init_usageSummary();
221883
+ }
221884
+ });
221885
+
221596
221886
  // features/perchTerminal/runtime/commands/sharedSlashCommands.ts
221597
221887
  function parseSharedSlashCommand(raw) {
221598
- return parseFlockCommand(raw);
221888
+ return parseUsageCommand(raw) ?? parseFlockCommand(raw);
221889
+ }
221890
+ function sharedSlashCommandRunsAsTurn(command) {
221891
+ if (!command) return false;
221892
+ if (command.kind === "usage") return true;
221893
+ return Boolean(command.task);
221599
221894
  }
221600
221895
  function resolveTurnRunnerForPrompt(raw) {
221601
221896
  const parsed = parseSharedSlashCommand(raw);
221602
221897
  if (!parsed) return null;
221898
+ if (parsed.kind === "usage") return runUsageTurn;
221603
221899
  return runFlockTurn;
221604
221900
  }
221605
221901
  var init_sharedSlashCommands = __esm({
@@ -221607,6 +221903,8 @@ var init_sharedSlashCommands = __esm({
221607
221903
  "use strict";
221608
221904
  init_flockCommand();
221609
221905
  init_runFlockTurn();
221906
+ init_usageCommand();
221907
+ init_runUsageTurn();
221610
221908
  }
221611
221909
  });
221612
221910
 
@@ -230206,6 +230504,7 @@ function buildCliTurnInput(input, resolved) {
230206
230504
  userId,
230207
230505
  selectedSourceId: input.selectedSourceId ?? null,
230208
230506
  recentMessages: input.recentMessages ?? [],
230507
+ usageRunIds: input.usageRunIds ?? [],
230209
230508
  sessionContext: emptyPerchTerminalSessionContext({
230210
230509
  userId,
230211
230510
  workspaceId
@@ -285705,7 +286004,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
285705
286004
  if (!prompt) continue;
285706
286005
  if (prompt === "/exit" || prompt === "exit" || prompt === "quit") break;
285707
286006
  const sharedCommand = parseSharedSlashCommand(prompt);
285708
- const runsAsSharedTurn = Boolean(sharedCommand?.task);
286007
+ const runsAsSharedTurn = sharedSlashCommandRunsAsTurn(sharedCommand);
285709
286008
  if (prompt.startsWith("/") && !runsAsSharedTurn) {
285710
286009
  try {
285711
286010
  const commandResult = await runInteractiveSlashCommand({
@@ -285743,6 +286042,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
285743
286042
  permissionMode: state.permissionMode,
285744
286043
  threadId: state.threadId,
285745
286044
  recentMessages: state.recentMessages,
286045
+ usageRunIds: state.usageRunIds,
285746
286046
  userId: hostedContext.userId,
285747
286047
  workspaceId: hostedContext.workspaceId,
285748
286048
  permanentMemories: hostedContext.permanentMemories,
@@ -285761,6 +286061,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
285761
286061
  appendRecentMessage(state.recentMessages, "assistant", result2.assistantText.trim());
285762
286062
  }
285763
286063
  trimRecentMessages(state.recentMessages);
286064
+ recordCliUsageRunId(state, result2.runId);
285764
286065
  state.contextSnapshot = result2.contextSnapshot ?? state.contextSnapshot;
285765
286066
  const admitted = await admitCliLearningMemory(connection, {
285766
286067
  prompt,
@@ -285899,7 +286200,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
285899
286200
  return;
285900
286201
  }
285901
286202
  const sharedTurnCommand = parseSharedSlashCommand(prompt);
285902
- if (prompt.startsWith("/") && !sharedTurnCommand?.task) {
286203
+ if (prompt.startsWith("/") && !sharedSlashCommandRunsAsTurn(sharedTurnCommand)) {
285903
286204
  const commandWriter = {
285904
286205
  stdout: (text) => {
285905
286206
  const clean = text.trim();
@@ -285973,6 +286274,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
285973
286274
  permissionMode: state.permissionMode,
285974
286275
  threadId: state.threadId,
285975
286276
  recentMessages: state.recentMessages,
286277
+ usageRunIds: state.usageRunIds,
285976
286278
  userId: hostedContext.userId,
285977
286279
  workspaceId: hostedContext.workspaceId,
285978
286280
  permanentMemories: hostedContext.permanentMemories,
@@ -286349,6 +286651,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
286349
286651
  appendRecentMessage(state.recentMessages, "user", prompt);
286350
286652
  if (assistantText) appendRecentMessage(state.recentMessages, "assistant", assistantText);
286351
286653
  trimRecentMessages(state.recentMessages);
286654
+ recordCliUsageRunId(state, result2.runId);
286352
286655
  state.contextSnapshot = result2.contextSnapshot ?? state.contextSnapshot;
286353
286656
  const admitted = await admitCliLearningMemory(connection, {
286354
286657
  prompt,
@@ -286740,6 +287043,7 @@ async function runInteractiveSlashCommand(input) {
286740
287043
  }
286741
287044
  input.state.threadId = parsed.value === "new" ? `cli-${Date.now()}` : parsed.value;
286742
287045
  input.state.recentMessages = [];
287046
+ input.state.usageRunIds = [];
286743
287047
  input.state.contextSnapshot = null;
286744
287048
  input.state.persistedThreadUpdatedAt = null;
286745
287049
  await hydrateInteractiveCliState(input.state);
@@ -286747,6 +287051,7 @@ async function runInteractiveSlashCommand(input) {
286747
287051
  return "continue";
286748
287052
  case "clear":
286749
287053
  input.state.recentMessages = [];
287054
+ input.state.usageRunIds = [];
286750
287055
  input.state.contextSnapshot = null;
286751
287056
  input.state.persistedThreadUpdatedAt = null;
286752
287057
  clearThreadSession(input.state.threadId);
@@ -286997,6 +287302,7 @@ function createInteractiveCliState(options) {
286997
287302
  cliLocalTools: options.cliLocalTools ?? true,
286998
287303
  appUrl: resolveCliAppUrl(options.appUrl ?? null, null),
286999
287304
  recentMessages: [],
287305
+ usageRunIds: [],
287000
287306
  contextSnapshot: null,
287001
287307
  persistedThreadUpdatedAt: null
287002
287308
  };
@@ -287013,6 +287319,7 @@ async function syncInteractiveCliThreadScope(state, connection, options = {}) {
287013
287319
  if (!options.force && state.threadScopeKey === nextScopeKey) return;
287014
287320
  state.threadScopeKey = nextScopeKey;
287015
287321
  state.recentMessages = [];
287322
+ state.usageRunIds = [];
287016
287323
  state.contextSnapshot = null;
287017
287324
  state.persistedThreadUpdatedAt = null;
287018
287325
  clearThreadSession(state.threadId);
@@ -287751,6 +288058,11 @@ function trimRecentMessages(messages) {
287751
288058
  messages.splice(0, messages.length - 30);
287752
288059
  }
287753
288060
  }
288061
+ function recordCliUsageRunId(state, runId) {
288062
+ const cleaned = runId?.trim();
288063
+ if (!cleaned) return;
288064
+ state.usageRunIds = [.../* @__PURE__ */ new Set([...state.usageRunIds, cleaned])].slice(-250);
288065
+ }
287754
288066
  function createCliRunId() {
287755
288067
  return `cli-turn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
287756
288068
  }
@@ -288092,6 +288404,7 @@ Commands:
288092
288404
  /help Show this list.
288093
288405
  /status Show cwd, auth, mode, persona, permission, and thread.
288094
288406
  /skills [name] List Perch core skills, or show one skill body.
288407
+ /usage Show tokens and estimated cost for this session.
288095
288408
  /cwd [dir] Show or change the working directory.
288096
288409
  /permission [mode] Show or set default, auto_review, take_the_wheel, or plan.
288097
288410
  /permissions [mode] Alias for /permission.
@@ -288151,6 +288464,7 @@ Commands:
288151
288464
  ];
288152
288465
  PERCH_SPLASH_COMMANDS = [
288153
288466
  ["/status", "show auth, route, tools, and thread"],
288467
+ ["/usage", "show session tokens and estimated cost"],
288154
288468
  ["/skills", "show reusable operator skills"],
288155
288469
  ["/persona", "swap saffron or quill"],
288156
288470
  ["/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.28",
3
+ "version": "2.4.30",
4
4
  "description": "Perch AI command-line interface",
5
5
  "bin": {
6
6
  "perch": "bin/perch"