perchai-cli 2.4.28 → 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.
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
@@ -75918,6 +75918,7 @@ function isTurnAbortedError(error) {
75918
75918
  var TURN_STOPPED_BY_USER_MESSAGE;
75919
75919
  var init_turnAbort = __esm({
75920
75920
  "features/perchTerminal/runtime/turnAbort.ts"() {
75921
+ "use strict";
75921
75922
  TURN_STOPPED_BY_USER_MESSAGE = "Turn stopped by user.";
75922
75923
  }
75923
75924
  });
@@ -86691,6 +86692,7 @@ function truncateHistoryLine(value, max2) {
86691
86692
  }
86692
86693
  var init_operatorTruth = __esm({
86693
86694
  "features/perchTerminal/runtime/operatorTruth.ts"() {
86695
+ "use strict";
86694
86696
  }
86695
86697
  });
86696
86698
 
@@ -221593,13 +221595,282 @@ var init_runFlockTurn = __esm({
221593
221595
  }
221594
221596
  });
221595
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
+
221596
221861
  // features/perchTerminal/runtime/commands/sharedSlashCommands.ts
221597
221862
  function parseSharedSlashCommand(raw) {
221598
- 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);
221599
221869
  }
221600
221870
  function resolveTurnRunnerForPrompt(raw) {
221601
221871
  const parsed = parseSharedSlashCommand(raw);
221602
221872
  if (!parsed) return null;
221873
+ if (parsed.kind === "usage") return runUsageTurn;
221603
221874
  return runFlockTurn;
221604
221875
  }
221605
221876
  var init_sharedSlashCommands = __esm({
@@ -221607,6 +221878,8 @@ var init_sharedSlashCommands = __esm({
221607
221878
  "use strict";
221608
221879
  init_flockCommand();
221609
221880
  init_runFlockTurn();
221881
+ init_usageCommand();
221882
+ init_runUsageTurn();
221610
221883
  }
221611
221884
  });
221612
221885
 
@@ -230206,6 +230479,7 @@ function buildCliTurnInput(input, resolved) {
230206
230479
  userId,
230207
230480
  selectedSourceId: input.selectedSourceId ?? null,
230208
230481
  recentMessages: input.recentMessages ?? [],
230482
+ usageRunIds: input.usageRunIds ?? [],
230209
230483
  sessionContext: emptyPerchTerminalSessionContext({
230210
230484
  userId,
230211
230485
  workspaceId
@@ -285705,7 +285979,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
285705
285979
  if (!prompt) continue;
285706
285980
  if (prompt === "/exit" || prompt === "exit" || prompt === "quit") break;
285707
285981
  const sharedCommand = parseSharedSlashCommand(prompt);
285708
- const runsAsSharedTurn = Boolean(sharedCommand?.task);
285982
+ const runsAsSharedTurn = sharedSlashCommandRunsAsTurn(sharedCommand);
285709
285983
  if (prompt.startsWith("/") && !runsAsSharedTurn) {
285710
285984
  try {
285711
285985
  const commandResult = await runInteractiveSlashCommand({
@@ -285743,6 +286017,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
285743
286017
  permissionMode: state.permissionMode,
285744
286018
  threadId: state.threadId,
285745
286019
  recentMessages: state.recentMessages,
286020
+ usageRunIds: state.usageRunIds,
285746
286021
  userId: hostedContext.userId,
285747
286022
  workspaceId: hostedContext.workspaceId,
285748
286023
  permanentMemories: hostedContext.permanentMemories,
@@ -285761,6 +286036,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
285761
286036
  appendRecentMessage(state.recentMessages, "assistant", result2.assistantText.trim());
285762
286037
  }
285763
286038
  trimRecentMessages(state.recentMessages);
286039
+ recordCliUsageRunId(state, result2.runId);
285764
286040
  state.contextSnapshot = result2.contextSnapshot ?? state.contextSnapshot;
285765
286041
  const admitted = await admitCliLearningMemory(connection, {
285766
286042
  prompt,
@@ -285899,7 +286175,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
285899
286175
  return;
285900
286176
  }
285901
286177
  const sharedTurnCommand = parseSharedSlashCommand(prompt);
285902
- if (prompt.startsWith("/") && !sharedTurnCommand?.task) {
286178
+ if (prompt.startsWith("/") && !sharedSlashCommandRunsAsTurn(sharedTurnCommand)) {
285903
286179
  const commandWriter = {
285904
286180
  stdout: (text) => {
285905
286181
  const clean = text.trim();
@@ -285973,6 +286249,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
285973
286249
  permissionMode: state.permissionMode,
285974
286250
  threadId: state.threadId,
285975
286251
  recentMessages: state.recentMessages,
286252
+ usageRunIds: state.usageRunIds,
285976
286253
  userId: hostedContext.userId,
285977
286254
  workspaceId: hostedContext.workspaceId,
285978
286255
  permanentMemories: hostedContext.permanentMemories,
@@ -286349,6 +286626,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
286349
286626
  appendRecentMessage(state.recentMessages, "user", prompt);
286350
286627
  if (assistantText) appendRecentMessage(state.recentMessages, "assistant", assistantText);
286351
286628
  trimRecentMessages(state.recentMessages);
286629
+ recordCliUsageRunId(state, result2.runId);
286352
286630
  state.contextSnapshot = result2.contextSnapshot ?? state.contextSnapshot;
286353
286631
  const admitted = await admitCliLearningMemory(connection, {
286354
286632
  prompt,
@@ -286740,6 +287018,7 @@ async function runInteractiveSlashCommand(input) {
286740
287018
  }
286741
287019
  input.state.threadId = parsed.value === "new" ? `cli-${Date.now()}` : parsed.value;
286742
287020
  input.state.recentMessages = [];
287021
+ input.state.usageRunIds = [];
286743
287022
  input.state.contextSnapshot = null;
286744
287023
  input.state.persistedThreadUpdatedAt = null;
286745
287024
  await hydrateInteractiveCliState(input.state);
@@ -286747,6 +287026,7 @@ async function runInteractiveSlashCommand(input) {
286747
287026
  return "continue";
286748
287027
  case "clear":
286749
287028
  input.state.recentMessages = [];
287029
+ input.state.usageRunIds = [];
286750
287030
  input.state.contextSnapshot = null;
286751
287031
  input.state.persistedThreadUpdatedAt = null;
286752
287032
  clearThreadSession(input.state.threadId);
@@ -286997,6 +287277,7 @@ function createInteractiveCliState(options) {
286997
287277
  cliLocalTools: options.cliLocalTools ?? true,
286998
287278
  appUrl: resolveCliAppUrl(options.appUrl ?? null, null),
286999
287279
  recentMessages: [],
287280
+ usageRunIds: [],
287000
287281
  contextSnapshot: null,
287001
287282
  persistedThreadUpdatedAt: null
287002
287283
  };
@@ -287013,6 +287294,7 @@ async function syncInteractiveCliThreadScope(state, connection, options = {}) {
287013
287294
  if (!options.force && state.threadScopeKey === nextScopeKey) return;
287014
287295
  state.threadScopeKey = nextScopeKey;
287015
287296
  state.recentMessages = [];
287297
+ state.usageRunIds = [];
287016
287298
  state.contextSnapshot = null;
287017
287299
  state.persistedThreadUpdatedAt = null;
287018
287300
  clearThreadSession(state.threadId);
@@ -287751,6 +288033,11 @@ function trimRecentMessages(messages) {
287751
288033
  messages.splice(0, messages.length - 30);
287752
288034
  }
287753
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
+ }
287754
288041
  function createCliRunId() {
287755
288042
  return `cli-turn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
287756
288043
  }
@@ -288092,6 +288379,7 @@ Commands:
288092
288379
  /help Show this list.
288093
288380
  /status Show cwd, auth, mode, persona, permission, and thread.
288094
288381
  /skills [name] List Perch core skills, or show one skill body.
288382
+ /usage Show tokens and estimated cost for this session.
288095
288383
  /cwd [dir] Show or change the working directory.
288096
288384
  /permission [mode] Show or set default, auto_review, take_the_wheel, or plan.
288097
288385
  /permissions [mode] Alias for /permission.
@@ -288151,6 +288439,7 @@ Commands:
288151
288439
  ];
288152
288440
  PERCH_SPLASH_COMMANDS = [
288153
288441
  ["/status", "show auth, route, tools, and thread"],
288442
+ ["/usage", "show session tokens and estimated cost"],
288154
288443
  ["/skills", "show reusable operator skills"],
288155
288444
  ["/persona", "swap saffron or quill"],
288156
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.28",
3
+ "version": "2.4.29",
4
4
  "description": "Perch AI command-line interface",
5
5
  "bin": {
6
6
  "perch": "bin/perch"