clawmoney 0.14.1 → 0.14.2

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.
@@ -672,7 +672,15 @@ async function doCallClaudeApi(opts) {
672
672
  },
673
673
  ],
674
674
  metadata: { user_id: buildMetadataUserID(fingerprint, sessionId) },
675
- stream: false,
675
+ // Real Claude Code ALWAYS sends stream:true on its main path
676
+ // (claude-code-sourcemap/src/services/api/claude.ts:1824 —
677
+ // `{ ...params, stream: true }`). The non-stream call at line 864 is
678
+ // only the fallback path triggered when the stream fails mid-response.
679
+ // Sending stream:false on every request is a statistical signal that
680
+ // Anthropic could use to identify relay clients vs real CLI — the
681
+ // entire account's traffic would be the opposite polarity of what the
682
+ // CLI ever emits. Switch to streaming to match.
683
+ stream: true,
676
684
  };
677
685
  const bodyJson = JSON.stringify(body);
678
686
  let transientAttempt = 0;
@@ -695,7 +703,10 @@ async function doCallClaudeApi(opts) {
695
703
  if (sessionWin)
696
704
  rateGuard?.setSessionWindow(sessionWin);
697
705
  if (resp.ok) {
698
- const parsed = parseResponse(await resp.json(), opts.model);
706
+ // Stream parser real Claude Code's main path uses stream:true; see
707
+ // body construction above. parseClaudeSseResponse aggregates text
708
+ // deltas + usage until message_stop, matching SDK semantics.
709
+ const parsed = await parseClaudeSseResponse(resp, opts.model);
699
710
  recordSpendFromUsage(parsed, opts.model);
700
711
  return parsed;
701
712
  }
@@ -756,22 +767,152 @@ function recordSpendFromUsage(parsed, model) {
756
767
  // subscription meter and what will actually burn the account.
757
768
  rateGuard.recordSpend(cost.apiCost);
758
769
  }
759
- function parseResponse(data, fallbackModel) {
760
- const text = (data.content ?? [])
761
- .filter((c) => c.type === "text" && typeof c.text === "string")
762
- .map((c) => c.text)
763
- .join("");
764
- const usage = data.usage ?? {};
770
+ /**
771
+ * Parse an Anthropic SSE `/v1/messages` stream response into a ParsedOutput.
772
+ *
773
+ * Wire format (Anthropic docs — beta.messages.create({stream: true})):
774
+ *
775
+ * event: message_start
776
+ * data: {"type":"message_start","message":{"id":"...","model":"...","usage":{"input_tokens":10,...}}}
777
+ *
778
+ * event: content_block_start
779
+ * data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
780
+ *
781
+ * event: content_block_delta
782
+ * data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
783
+ *
784
+ * ... more deltas ...
785
+ *
786
+ * event: content_block_stop
787
+ * data: {"type":"content_block_stop","index":0}
788
+ *
789
+ * event: message_delta
790
+ * data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":42}}
791
+ *
792
+ * event: message_stop
793
+ * data: {"type":"message_stop"}
794
+ *
795
+ * event: ping (keepalive — ignore)
796
+ *
797
+ * event: error (upstream error — throw)
798
+ * data: {"type":"error","error":{"type":"overloaded_error","message":"..."}}
799
+ */
800
+ async function parseClaudeSseResponse(resp, fallbackModel) {
801
+ const reader = resp.body?.getReader();
802
+ if (!reader) {
803
+ throw new Error("Claude streamGenerateContent returned no body");
804
+ }
805
+ const decoder = new TextDecoder("utf-8");
806
+ let buffer = "";
807
+ let text = "";
808
+ let model = fallbackModel;
809
+ let inputTokens = 0;
810
+ let outputTokens = 0;
811
+ let cacheCreation = 0;
812
+ let cacheRead = 0;
813
+ let streamError;
814
+ const processChunk = (jsonStr) => {
815
+ const trimmed = jsonStr.trim();
816
+ if (!trimmed)
817
+ return;
818
+ let chunk;
819
+ try {
820
+ chunk = JSON.parse(trimmed);
821
+ }
822
+ catch {
823
+ return;
824
+ }
825
+ switch (chunk.type) {
826
+ case "message_start": {
827
+ if (chunk.message?.model)
828
+ model = chunk.message.model;
829
+ const u = chunk.message?.usage;
830
+ if (u) {
831
+ if (typeof u.input_tokens === "number")
832
+ inputTokens = u.input_tokens;
833
+ if (typeof u.output_tokens === "number")
834
+ outputTokens = u.output_tokens;
835
+ if (typeof u.cache_creation_input_tokens === "number") {
836
+ cacheCreation = u.cache_creation_input_tokens;
837
+ }
838
+ if (typeof u.cache_read_input_tokens === "number") {
839
+ cacheRead = u.cache_read_input_tokens;
840
+ }
841
+ }
842
+ break;
843
+ }
844
+ case "content_block_delta": {
845
+ // We only accumulate text_delta. input_json_delta is for tool calls,
846
+ // which we don't surface from the relay path (the buyer gets the
847
+ // model's final text response, not in-flight tool plumbing).
848
+ if (chunk.delta?.type === "text_delta" && typeof chunk.delta.text === "string") {
849
+ text += chunk.delta.text;
850
+ }
851
+ break;
852
+ }
853
+ case "message_delta": {
854
+ // message_delta carries the final output_tokens count and
855
+ // potentially an updated usage (e.g. cache hits applied late).
856
+ const u = chunk.usage;
857
+ if (u) {
858
+ if (typeof u.output_tokens === "number")
859
+ outputTokens = u.output_tokens;
860
+ if (typeof u.input_tokens === "number")
861
+ inputTokens = u.input_tokens;
862
+ if (typeof u.cache_creation_input_tokens === "number") {
863
+ cacheCreation = u.cache_creation_input_tokens;
864
+ }
865
+ if (typeof u.cache_read_input_tokens === "number") {
866
+ cacheRead = u.cache_read_input_tokens;
867
+ }
868
+ }
869
+ break;
870
+ }
871
+ case "error": {
872
+ streamError = chunk.error;
873
+ break;
874
+ }
875
+ // message_stop / content_block_start / content_block_stop / ping —
876
+ // structural, nothing to accumulate.
877
+ default:
878
+ break;
879
+ }
880
+ };
881
+ while (true) {
882
+ const { value, done } = await reader.read();
883
+ if (done)
884
+ break;
885
+ buffer += decoder.decode(value, { stream: true });
886
+ let newlineIdx;
887
+ while ((newlineIdx = buffer.indexOf("\n")) >= 0) {
888
+ const line = buffer.slice(0, newlineIdx).replace(/\r$/, "");
889
+ buffer = buffer.slice(newlineIdx + 1);
890
+ if (!line)
891
+ continue;
892
+ // SSE dispatches on `data: ...` lines. `event: ...` names are
893
+ // informational (the chunk JSON's `type` field is authoritative).
894
+ if (line.startsWith("data:")) {
895
+ processChunk(line.slice(5));
896
+ }
897
+ }
898
+ }
899
+ // Flush trailing line (rare — most servers end with a \n\n).
900
+ if (buffer.startsWith("data:")) {
901
+ processChunk(buffer.slice(5));
902
+ }
903
+ if (streamError) {
904
+ throw new Error(`Anthropic stream error: ${streamError.type ?? "unknown"} — ${streamError.message ?? ""}`);
905
+ }
765
906
  return {
766
907
  text,
767
908
  sessionId: "",
768
909
  usage: {
769
- input_tokens: usage.input_tokens ?? 0,
770
- output_tokens: usage.output_tokens ?? 0,
771
- cache_creation_tokens: usage.cache_creation_input_tokens ?? 0,
772
- cache_read_tokens: usage.cache_read_input_tokens ?? 0,
910
+ input_tokens: inputTokens,
911
+ output_tokens: outputTokens,
912
+ cache_creation_tokens: cacheCreation,
913
+ cache_read_tokens: cacheRead,
773
914
  },
774
- model: data.model ?? fallbackModel,
915
+ model,
775
916
  costUsd: 0,
776
917
  };
777
918
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {