claude-code-infinite 1.0.36 → 1.0.37

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 (69) hide show
  1. package/README.md +100 -1
  2. package/dist/cc-request.d.ts +15 -0
  3. package/dist/cc-request.d.ts.map +1 -1
  4. package/dist/cc-request.js +30 -0
  5. package/dist/cc-request.js.map +1 -1
  6. package/dist/claude-env.d.ts +24 -1
  7. package/dist/claude-env.d.ts.map +1 -1
  8. package/dist/claude-env.js +48 -7
  9. package/dist/claude-env.js.map +1 -1
  10. package/dist/cli-args.d.ts +1 -1
  11. package/dist/cli-args.d.ts.map +1 -1
  12. package/dist/cli-args.js +1 -1
  13. package/dist/cli-args.js.map +1 -1
  14. package/dist/cli-main.js +99 -8
  15. package/dist/cli-main.js.map +1 -1
  16. package/dist/config.d.ts +5 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js.map +1 -1
  19. package/dist/desktop-command.d.ts +38 -0
  20. package/dist/desktop-command.d.ts.map +1 -0
  21. package/dist/desktop-command.js +324 -0
  22. package/dist/desktop-command.js.map +1 -0
  23. package/dist/desktop-service.d.ts +41 -0
  24. package/dist/desktop-service.d.ts.map +1 -0
  25. package/dist/desktop-service.js +449 -0
  26. package/dist/desktop-service.js.map +1 -0
  27. package/dist/desktop-setup.d.ts +94 -0
  28. package/dist/desktop-setup.d.ts.map +1 -0
  29. package/dist/desktop-setup.js +288 -0
  30. package/dist/desktop-setup.js.map +1 -0
  31. package/dist/https-proxy-eligibility.d.ts +31 -0
  32. package/dist/https-proxy-eligibility.d.ts.map +1 -0
  33. package/dist/https-proxy-eligibility.js +204 -0
  34. package/dist/https-proxy-eligibility.js.map +1 -0
  35. package/dist/https-proxy-mode.d.ts +59 -0
  36. package/dist/https-proxy-mode.d.ts.map +1 -0
  37. package/dist/https-proxy-mode.js +255 -0
  38. package/dist/https-proxy-mode.js.map +1 -0
  39. package/dist/https-proxy-session.d.ts +64 -0
  40. package/dist/https-proxy-session.d.ts.map +1 -0
  41. package/dist/https-proxy-session.js +165 -0
  42. package/dist/https-proxy-session.js.map +1 -0
  43. package/dist/memtree-mcp.d.ts +32 -1
  44. package/dist/memtree-mcp.d.ts.map +1 -1
  45. package/dist/memtree-mcp.js +73 -7
  46. package/dist/memtree-mcp.js.map +1 -1
  47. package/dist/memtree.d.ts +10 -0
  48. package/dist/memtree.d.ts.map +1 -1
  49. package/dist/memtree.js +59 -12
  50. package/dist/memtree.js.map +1 -1
  51. package/dist/message-refs.d.ts +81 -0
  52. package/dist/message-refs.d.ts.map +1 -0
  53. package/dist/message-refs.js +378 -0
  54. package/dist/message-refs.js.map +1 -0
  55. package/dist/mitm-ca.d.ts +31 -0
  56. package/dist/mitm-ca.d.ts.map +1 -0
  57. package/dist/mitm-ca.js +236 -0
  58. package/dist/mitm-ca.js.map +1 -0
  59. package/dist/paused-session.d.ts.map +1 -1
  60. package/dist/paused-session.js +9 -2
  61. package/dist/paused-session.js.map +1 -1
  62. package/dist/proxy.d.ts +20 -0
  63. package/dist/proxy.d.ts.map +1 -1
  64. package/dist/proxy.js +101 -16
  65. package/dist/proxy.js.map +1 -1
  66. package/dist/reqlog.d.ts +7 -1
  67. package/dist/reqlog.d.ts.map +1 -1
  68. package/dist/reqlog.js.map +1 -1
  69. package/package.json +1 -1
package/dist/proxy.js CHANGED
@@ -40,6 +40,7 @@
40
40
  */
41
41
  import { UNKNOWN_INDEX_WAIT_SECONDS } from "./index-wait-413.js";
42
42
  import { capCacheBreakpoints } from "./route-cache.js";
43
+ import { attachHttpsProxyMode, } from "./https-proxy-mode.js";
43
44
  import { PromptAccounting } from "./prompt-accounting.js";
44
45
  import { INDEX_WAIT_RETRY_AFTER_SECONDS, RefusalStreaks, RouteFallbackFailures, pausedRefusal, unreachableRefusal } from "./route-fallback.js";
45
46
  import { refusalCause, refusalMessage } from "./refusal-reason.js";
@@ -68,7 +69,7 @@ import { keyInvalidNoticeText, keyInvalidRefusalMessage } from "./key-status.js"
68
69
  import { KEY_INVALID_KIND, ModelContextNotices, PAUSED_KIND, keyInvalidModelContext, lastToldKindInFile, pausedModelContext } from "./model-context.js";
69
70
  import { PaymentNoticeThrottle, pausedCompactionText, pausedRefusalMessage, paymentNoticeText, subscribeUrlFromDetail } from "./payment-notice.js";
70
71
  import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
71
- import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
72
+ import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, notSessionConversation, sessionTag, } from "./cc-request.js";
72
73
  import { isResumableSessionId } from "./resume-hint.js";
73
74
  import { isSubagentCall, postToolUseOutput, SUBAGENT_RESULT_HINT, subagentMemoryHintText, subagentStartOutput, } from "./subagent-hint.js";
74
75
  import { PROJECT_TREES_HOOK_SUFFIX, RESUME_GUIDANCE_HOOK_SUFFIX, SUBAGENT_RESULT_HOOK_SUFFIX, } from "./hooks.js";
@@ -427,10 +428,12 @@ export function startProxy(opts) {
427
428
  // A newer tree from the background lookup reaches the status line without a turn ending.
428
429
  opts.newestTrees?.onTree((sessionId) => publishStatusLink(state, sessionId));
429
430
  state.notices.setFirstTreeIntro(opts.firstTreeIntro ?? null);
430
- const server = http.createServer((req, res) => {
431
+ // One handler for the loopback server and, in HTTPS-proxy mode, for the decrypted
432
+ // api.anthropic.com streams (`intercepted`): both are tracked, drained and cancelled alike.
433
+ const onRequest = (intercepted) => (req, res) => {
431
434
  const accepted = { req, res };
432
435
  acceptedRequests.add(accepted);
433
- const task = handleRequest(req, res, opts, upstream, state, hookPath).catch((err) => {
436
+ const task = handleRequest(req, res, opts, upstream, state, hookPath, intercepted).catch((err) => {
434
437
  try {
435
438
  sendAnthropicError(res, `local proxy error: ${err?.message ?? err}`);
436
439
  }
@@ -448,7 +451,20 @@ export function startProxy(opts) {
448
451
  });
449
452
  if (shutdownAbort.signal.aborted)
450
453
  cancelAcceptedRequest(accepted);
451
- });
454
+ };
455
+ const server = http.createServer(opts.innerToken === undefined ? onRequest(false)
456
+ : (req, res) => {
457
+ const inner = acceptInnerRequest(req, opts.innerToken);
458
+ if (inner === undefined) {
459
+ res.writeHead(403, { "content-type": "application/json" });
460
+ res.end(JSON.stringify({ detail: "ccc inner proxy: not for direct use" }));
461
+ return;
462
+ }
463
+ onRequest(inner.intercepted)(req, res);
464
+ });
465
+ const closeTunnels = opts.httpsProxyMode
466
+ ? attachHttpsProxyMode(server, onRequest(true), interceptUpstream(upstream), opts.httpsProxyMode)
467
+ : { blind: () => { }, all: () => { } };
452
468
  // Long-running SSE responses must not be cut by idle timeouts.
453
469
  server.requestTimeout = 0;
454
470
  server.headersTimeout = 60_000;
@@ -470,6 +486,9 @@ export function startProxy(opts) {
470
486
  ? Math.floor(timeoutMs)
471
487
  : 5_000;
472
488
  backgroundAbort.abort();
489
+ // CONNECT tunnels are raw sockets that keep server.close() waiting: the blind ones
490
+ // (and idle decrypted streams) carry no ccc work, so they go now.
491
+ closeTunnels.blind();
473
492
  const quiesced = (async () => {
474
493
  // Once close completes, every accepted request has entered the tracked
475
494
  // set.
@@ -498,6 +517,7 @@ export function startProxy(opts) {
498
517
  shutdownAbort.abort();
499
518
  for (const accepted of acceptedRequests)
500
519
  cancelAcceptedRequest(accepted);
520
+ closeTunnels.all();
501
521
  await quiesced;
502
522
  sendDeferredAgentIndexes(state);
503
523
  return false;
@@ -532,8 +552,10 @@ function cancelAcceptedRequest(accepted) {
532
552
  if (!res.destroyed && !res.writableEnded)
533
553
  res.destroy();
534
554
  }
535
- async function handleRequest(req, res, opts, upstream, state, hookPath) {
555
+ async function handleRequest(req, res, opts, upstream, state, hookPath, intercepted) {
536
556
  const url = new URL(req.url ?? "/", `http://127.0.0.1`);
557
+ if (intercepted)
558
+ return handleInterceptedRequest(req, res, url, opts, upstream, state);
537
559
  if (!isLocalCaller(req)) {
538
560
  res.writeHead(403, { "content-type": "application/json" });
539
561
  res.end(JSON.stringify({ detail: "The ccc proxy only serves programs on this machine" }));
@@ -578,6 +600,32 @@ async function handleRequest(req, res, opts, upstream, state, hookPath) {
578
600
  }
579
601
  return passThroughStreaming(req, res, upstream, state.shutdownSignal);
580
602
  }
603
+ /**
604
+ * Claude Code's own api.anthropic.com traffic, decrypted (HTTPS-proxy mode): model calls
605
+ * get MemTree exactly as through the base URL; everything else (Remote Control's bridge,
606
+ * sessions, files, OAuth, telemetry) goes verbatim to api.anthropic.com. ccc's
607
+ * loopback-only routes (hooks, /memtree/*) are never answered here: such a path is
608
+ * Anthropic's.
609
+ */
610
+ function handleInterceptedRequest(req, res, url, opts, upstream, state) {
611
+ if (req.method === "POST" && url.pathname === "/v1/messages") {
612
+ try {
613
+ opts.httpsProxyMode?.onModelCall?.();
614
+ }
615
+ catch {
616
+ // A notice hook must never fail a model call.
617
+ }
618
+ return handleMessages(req, res, opts, upstream, state);
619
+ }
620
+ if (req.method === "POST" && url.pathname === "/v1/messages/count_tokens") {
621
+ return handleCountTokens(req, res, opts, upstream, state);
622
+ }
623
+ return passThroughStreaming(req, res, upstream, state.shutdownSignal);
624
+ }
625
+ /** Where a WebSocket upgrade on a decrypted stream is replayed: the same upstream. */
626
+ function interceptUpstream(upstream) {
627
+ return { host: upstream.host, port: upstream.port, secure: upstream.module === https };
628
+ }
581
629
  const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "localhost", "[::1]"]);
582
630
  const BROWSER_SAME_SITE = new Set(["same-origin", "none"]);
583
631
  /**
@@ -1383,6 +1431,12 @@ async function handleMessages(req, res, opts, upstream, state) {
1383
1431
  const { lane: requestRouteLane, key: requestRouteKey } = routeIdentity(req, isAwaySummary);
1384
1432
  const isSubagentRequest = requestRouteLane === "agent";
1385
1433
  const isMainRequest = requestRouteLane === "main";
1434
+ // A side request or a thread continuation carries a fragment, not the conversation:
1435
+ // it is forwarded as sent, with no compress, route, recovery or MemTree upload.
1436
+ // The away summary keeps its own lane and handling.
1437
+ const notConversation = isAwaySummary ? undefined : notSessionConversation(body, clientInfo);
1438
+ if (notConversation)
1439
+ rec.notConversation = notConversation;
1386
1440
  const requestSizeKey = laneSizeKey(req, requestRouteKey, body.model);
1387
1441
  const hold = indexHolds.get(rec);
1388
1442
  if (hold) {
@@ -1449,7 +1503,9 @@ async function handleMessages(req, res, opts, upstream, state) {
1449
1503
  // API identity and validated history alone govern route eligibility.
1450
1504
  const rideableCandidate = getMemoryRoute(state, requestRouteKey);
1451
1505
  const existingRide = rideableCandidate && memoryRoutedToolBody(body, messages, rideableCandidate, state.mainRouteEpoch, requestSessionId(req));
1452
- const isFollowupUserTurn = isUserTurn && hasEarlierNonToolUserMessage(messages);
1506
+ // A fragment is never a follow-up: it must not reserve a route decision (it would never
1507
+ // release it, and an in-flight main compression could not install its route).
1508
+ const isFollowupUserTurn = !notConversation && isUserTurn && hasEarlierNonToolUserMessage(messages);
1453
1509
  const routeEpoch = state.mainRouteEpoch;
1454
1510
  let routeDecisionGeneration = state.mainRouteDecisionGeneration;
1455
1511
  let routeDecisionSettled = false;
@@ -1495,7 +1551,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1495
1551
  // ORIGINAL conversation (the one MemTree indexed), never the ride itself.
1496
1552
  recoverLate: async (_ride, rideTokens, target) => {
1497
1553
  // A compression already made may be redone only to a smaller target.
1498
- if ((rec.compress && target === undefined) ||
1554
+ if (notConversation || (rec.compress && target === undefined) ||
1499
1555
  compactionMode(opts, state, requestSessionKey).mode === "off" ||
1500
1556
  !hasEarlierNonToolUserMessage(messages) ||
1501
1557
  (rec.turnType.startsWith("tool") && opts.toolRouteRecovery === false))
@@ -1518,9 +1574,14 @@ async function handleMessages(req, res, opts, upstream, state) {
1518
1574
  // The main lane's latest conversation: what its final index sends, plus the
1519
1575
  // reply the session ends with (scheduleFinalIndex, on Stop).
1520
1576
  const mainSessionId = isMainRequest ? requestSessionId(req) : undefined;
1521
- if (mainSessionId !== undefined) {
1522
- if (isResumableSessionId(mainSessionId))
1523
- state.resumableMainSession = mainSessionId;
1577
+ if (mainSessionId !== undefined && isResumableSessionId(mainSessionId)) {
1578
+ state.resumableMainSession = mainSessionId;
1579
+ }
1580
+ // A continuation is the session at work (a side request, e.g. a status summary, is not).
1581
+ if (mainSessionId !== undefined && notConversation === "thread-continue") {
1582
+ opts.memtree.noteMainActivity(mainSessionId);
1583
+ }
1584
+ if (mainSessionId !== undefined && !notConversation) {
1524
1585
  opts.memtree.noteMainConversation(mainSessionId, {
1525
1586
  messages: msgsForMemtree,
1526
1587
  modelContextLimit,
@@ -1540,7 +1601,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1540
1601
  // Observation only: which requests carry Claude Code's turn origin.
1541
1602
  if (clientInfo)
1542
1603
  rec.client = clientInfo;
1543
- if (!isFollowupUserTurn) {
1604
+ if (!isFollowupUserTurn || notConversation) {
1544
1605
  // Tool turn or FIRST user turn: keep the index fed off the response path
1545
1606
  // and forward as-is (on the lane's memory route or the session's stable
1546
1607
  // prefix when one applies) — except a tool turn whose estimated size
@@ -1556,7 +1617,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1556
1617
  let routedTool = false;
1557
1618
  let routedCandidate;
1558
1619
  let routeMiss;
1559
- if (isToolResultTurn || isContinuationTurn || isUserTurn) {
1620
+ if (!notConversation && (isToolResultTurn || isContinuationTurn || isUserTurn)) {
1560
1621
  // Reuse only this API session/lane's independently validated prefix.
1561
1622
  const activeRoute = rideableCandidate;
1562
1623
  if (activeRoute) {
@@ -1617,7 +1678,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1617
1678
  rec.continuation = true;
1618
1679
  // Cheap shape check: without an earlier real user message there is
1619
1680
  // nothing MemTree could compress, so no attempt is worth making.
1620
- const canCompress = hasEarlierNonToolUserMessage(messages);
1681
+ const canCompress = !notConversation && hasEarlierNonToolUserMessage(messages);
1621
1682
  let toolRide = routedTool
1622
1683
  ? {
1623
1684
  raw: routedBody,
@@ -1626,7 +1687,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1626
1687
  }
1627
1688
  : undefined;
1628
1689
  let toolNeedsOriginal = false;
1629
- if (opts.toolRouteRecovery === false) {
1690
+ if (notConversation) {
1691
+ // A fragment: forwarded as sent (no route, no stable prefix, no compaction).
1692
+ }
1693
+ else if (opts.toolRouteRecovery === false) {
1630
1694
  // Kill switch: no size check and no compress call on any tool turn.
1631
1695
  // A route still rides; a miss forwards whole and records "disabled".
1632
1696
  if ((routeMiss === "missing" || routeMiss === "rejected") && canCompress) {
@@ -1803,8 +1867,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1803
1867
  requestSendPolicies.get(req)?.allowed.add(toolRide.raw);
1804
1868
  const sendBody = toolRide?.raw ?? (toolNeedsOriginal ? rawBody : forwardBody);
1805
1869
  recordTurn(rec, toolRide ? toolRide.turnType : isUserTurn ? "first-user" : "tool", sendBody);
1806
- indexTurn({ opts, state, req, rec, hash, msgsForMemtree, modelContextLimit,
1807
- sessionId: toolSessionId, clientMeta });
1870
+ if (!notConversation) {
1871
+ indexTurn({ opts, state, req, rec, hash, msgsForMemtree, modelContextLimit,
1872
+ sessionId: toolSessionId, clientMeta });
1873
+ }
1808
1874
  capture(opts, toolRide ? "anthropic-request-memory-tool" : "anthropic-request", sendBody);
1809
1875
  // The size Anthropic reports is the next budget check's anchor: a ride
1810
1876
  // reports to its prefix (or route), a whole request to the session's (or
@@ -6761,4 +6827,23 @@ function transcriptTimesFor(opts, sessionId, agentId) {
6761
6827
  return (messages) => source.timesFor(sessionId, messages, agentId);
6762
6828
  }
6763
6829
  export const __testHoldingLane = holdingLane;
6830
+ /**
6831
+ * The inner-proxy headers (ProxyOptions.innerToken): checked, then stripped so nothing
6832
+ * forwards them. undefined: not the service.
6833
+ */
6834
+ function acceptInnerRequest(req, token) {
6835
+ if (!sameSecret(req.headers[INNER_TOKEN_HEADER], token))
6836
+ return undefined;
6837
+ const intercepted = req.headers[INNER_INTERCEPTED_HEADER] === "1";
6838
+ delete req.headers[INNER_TOKEN_HEADER];
6839
+ delete req.headers[INNER_INTERCEPTED_HEADER];
6840
+ for (let i = req.rawHeaders.length - 2; i >= 0; i -= 2) {
6841
+ const name = req.rawHeaders[i].toLowerCase();
6842
+ if (name === INNER_TOKEN_HEADER || name === INNER_INTERCEPTED_HEADER)
6843
+ req.rawHeaders.splice(i, 2);
6844
+ }
6845
+ return { intercepted };
6846
+ }
6847
+ export const INNER_TOKEN_HEADER = "x-ccc-inner";
6848
+ export const INNER_INTERCEPTED_HEADER = "x-ccc-intercepted";
6764
6849
  //# sourceMappingURL=proxy.js.map