@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0

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 (166) hide show
  1. package/gui/dist/assets/index-CWXut3rG.js +115 -0
  2. package/gui/dist/assets/index-EdoPnm9_.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/gui/dist/provider-icons/devin.svg +49 -0
  5. package/gui/dist/provider-icons/omo.svg +42 -0
  6. package/package.json +3 -1
  7. package/src/AGENTS.md +1 -1
  8. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
  9. package/src/adapters/command-code.ts +0 -1
  10. package/src/adapters/cursor/checkpoint-store.ts +37 -0
  11. package/src/adapters/cursor/live-transport.ts +74 -2
  12. package/src/adapters/cursor.ts +6 -0
  13. package/src/adapters/devin/cloud-direct/auth.ts +264 -0
  14. package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
  15. package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
  16. package/src/adapters/devin/cloud-direct/index.ts +65 -0
  17. package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
  18. package/src/adapters/devin/cloud-direct/wire.ts +206 -0
  19. package/src/adapters/devin/live-models.ts +133 -0
  20. package/src/adapters/devin-cli/acp.ts +204 -0
  21. package/src/adapters/devin-cli/adapter.ts +345 -0
  22. package/src/adapters/devin-cli/binary.ts +69 -0
  23. package/src/adapters/devin-cli/models.ts +57 -0
  24. package/src/adapters/devin.ts +326 -0
  25. package/src/adapters/google.ts +1 -1
  26. package/src/adapters/openai-chat.ts +31 -10
  27. package/src/adapters/openai-responses.ts +16 -1
  28. package/src/adapters/registry.ts +25 -1
  29. package/src/bridge.ts +8 -2
  30. package/src/claude/inbound-cache-stabilize.ts +130 -0
  31. package/src/claude/inbound.ts +45 -5
  32. package/src/cli/account-auth.ts +60 -1
  33. package/src/cli/account-extended.ts +23 -30
  34. package/src/cli/account.ts +2 -1
  35. package/src/cli/capabilities.ts +50 -8
  36. package/src/cli/config-command.ts +2 -2
  37. package/src/cli/dispatch.ts +14 -2
  38. package/src/cli/export-command.ts +11 -25
  39. package/src/cli/help.ts +2 -2
  40. package/src/cli/opencode.ts +5 -0
  41. package/src/cli/registry.ts +15 -4
  42. package/src/clients/config-export/cline.ts +71 -0
  43. package/src/clients/config-export/contracts.ts +10 -1
  44. package/src/clients/config-export/model-metadata.ts +33 -0
  45. package/src/clients/config-export/zcode.ts +23 -12
  46. package/src/clients/config-export.ts +156 -4
  47. package/src/codex/account-store.ts +7 -2
  48. package/src/codex/auth-context.ts +1 -1
  49. package/src/codex/catalog/effort.ts +4 -3
  50. package/src/codex/catalog/metadata.ts +8 -4
  51. package/src/codex/catalog/native-models.ts +24 -4
  52. package/src/codex/catalog/parsing.ts +19 -1
  53. package/src/codex/catalog/provider-fetch.ts +57 -0
  54. package/src/codex/catalog/sync.ts +1 -1
  55. package/src/codex/context-compat.ts +97 -0
  56. package/src/codex/context-owner.ts +201 -0
  57. package/src/codex/history-provider.ts +161 -14
  58. package/src/codex/inject-coordination.ts +3 -2
  59. package/src/codex/inject.ts +128 -9
  60. package/src/codex/pool-rotation.ts +8 -292
  61. package/src/codex/quota.ts +41 -12
  62. package/src/codex/retired-model-migration.ts +41 -0
  63. package/src/codex/routing.ts +327 -22
  64. package/src/codex/warmup.ts +2 -2
  65. package/src/combos/failover.ts +5 -0
  66. package/src/combos/resolve.ts +11 -15
  67. package/src/config.ts +46 -14
  68. package/src/generated/compatibility-version.json +293 -113
  69. package/src/grok/grpc-web.ts +120 -0
  70. package/src/grok/reset-coupon-ledger.ts +139 -0
  71. package/src/grok/reset-coupons.ts +278 -0
  72. package/src/integrations/catalog-refresh.ts +1 -1
  73. package/src/integrations/cline-document.ts +73 -0
  74. package/src/integrations/cline-io.ts +149 -0
  75. package/src/integrations/cline-transaction.ts +42 -0
  76. package/src/integrations/config-io.ts +21 -2
  77. package/src/integrations/journal.ts +43 -1
  78. package/src/integrations/omp-yaml-source.ts +123 -4
  79. package/src/integrations/ownership-policy.ts +7 -0
  80. package/src/integrations/ownership.ts +36 -0
  81. package/src/integrations/registry.ts +27 -0
  82. package/src/integrations/state.ts +5 -2
  83. package/src/integrations/store.ts +6 -0
  84. package/src/integrations/writer.ts +53 -9
  85. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  86. package/src/lib/abort.ts +36 -0
  87. package/src/lib/local-destinations.ts +1 -1
  88. package/src/lib/upstream-retry.ts +36 -1
  89. package/src/oauth/account-quota-rank.ts +11 -0
  90. package/src/oauth/callback-server.ts +10 -5
  91. package/src/oauth/devin/api-base.ts +63 -0
  92. package/src/oauth/devin/login.ts +1 -0
  93. package/src/oauth/devin/register-user.ts +186 -0
  94. package/src/oauth/devin/types.ts +71 -0
  95. package/src/oauth/devin-cli.ts +149 -0
  96. package/src/oauth/devin.ts +170 -0
  97. package/src/oauth/generic-account-failover.ts +169 -1
  98. package/src/oauth/index.ts +20 -1
  99. package/src/oauth/login-cli.ts +19 -5
  100. package/src/oauth/pool-kernel.ts +321 -0
  101. package/src/oauth/pool-settings-capability.ts +127 -9
  102. package/src/oauth/store.ts +7 -3
  103. package/src/oauth/token-guardian.ts +1 -1
  104. package/src/providers/codebuddy-models.ts +0 -3
  105. package/src/providers/command-code-efforts.ts +23 -4
  106. package/src/providers/default-aliases.ts +4 -0
  107. package/src/providers/derive.ts +8 -0
  108. package/src/providers/devin-cli-authmode-migration.ts +57 -0
  109. package/src/providers/key-failover.ts +175 -0
  110. package/src/providers/model-rename-startup.ts +21 -1
  111. package/src/providers/qoder-models.ts +0 -1
  112. package/src/providers/quota-key-accounts.ts +50 -0
  113. package/src/providers/quota-routing-cache.ts +65 -6
  114. package/src/providers/quota-types.ts +7 -0
  115. package/src/providers/quota.ts +113 -30
  116. package/src/providers/registry.ts +237 -77
  117. package/src/providers/stale-context-window-migration.ts +92 -0
  118. package/src/providers/zai-responses-migration.ts +45 -0
  119. package/src/quota/reset-observer.ts +2 -1
  120. package/src/quota/reset-seen-store.ts +9 -1
  121. package/src/remote-control/crypto.ts +442 -0
  122. package/src/remote-control/host.ts +175 -0
  123. package/src/remote-control/index.ts +100 -0
  124. package/src/remote-control/protocol.ts +200 -0
  125. package/src/remote-control/relay.ts +162 -0
  126. package/src/remote-control/workspace-agent-protocol.ts +246 -0
  127. package/src/remote-control/workspace-rpc-framing.ts +128 -0
  128. package/src/remote-control/workspace-tools.ts +237 -0
  129. package/src/remote-control/workspace-utf8.ts +24 -0
  130. package/src/responses/custom-tool-compat.ts +23 -0
  131. package/src/router.ts +6 -1
  132. package/src/routing/compatibility/behavior.ts +3 -0
  133. package/src/server/adapter-resolve.ts +6 -2
  134. package/src/server/auth-cors.ts +71 -6
  135. package/src/server/chat-completions.ts +5 -3
  136. package/src/server/chat-native.ts +9 -0
  137. package/src/server/claude-messages.ts +25 -30
  138. package/src/server/context-history.ts +207 -0
  139. package/src/server/images.ts +28 -1
  140. package/src/server/index.ts +42 -22
  141. package/src/server/live.ts +2 -1
  142. package/src/server/management/agent-settings-routes.ts +7 -1
  143. package/src/server/management/config-routes.ts +3 -3
  144. package/src/server/management/grok-coupon-routes.ts +287 -0
  145. package/src/server/management/integration-routes.ts +6 -1
  146. package/src/server/management/oauth-account-routes.ts +124 -7
  147. package/src/server/management/provider-routes.ts +77 -2
  148. package/src/server/management/route-registry.ts +6 -1
  149. package/src/server/management-api.ts +7 -0
  150. package/src/server/request-log.ts +4 -0
  151. package/src/server/responses/codex-ws-exchange.ts +90 -14
  152. package/src/server/responses/codex-ws-wire.ts +51 -1
  153. package/src/server/responses/compact.ts +8 -1
  154. package/src/server/responses/core.ts +188 -22
  155. package/src/server/responses/ws-upstream.ts +1 -1
  156. package/src/server/responses-undeclared-tool-guard.ts +261 -12
  157. package/src/server/zai-responses-startup.ts +21 -0
  158. package/src/types/config.ts +33 -3
  159. package/src/types/provider.ts +54 -4
  160. package/src/types/request.ts +1 -1
  161. package/src/types/tools.ts +36 -6
  162. package/src/update/job.ts +33 -11
  163. package/src/vision/plan.ts +40 -37
  164. package/src/web-search/index.ts +1 -0
  165. package/gui/dist/assets/index-BoBRSehJ.css +0 -1
  166. package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Minimal, zero-dependency gRPC-Web binary framing encoder and decoder.
3
+ * Supports 5-byte header prefix: 0x00 data frames, 0x80 trailer frames.
4
+ */
5
+
6
+ export interface GrpcWebTrailer {
7
+ status: number;
8
+ statusMessage?: string;
9
+ metadata: Record<string, string>;
10
+ }
11
+
12
+ export interface DecodedGrpcWebResponse {
13
+ messages: Uint8Array[];
14
+ status: number;
15
+ statusMessage?: string;
16
+ trailers?: GrpcWebTrailer;
17
+ }
18
+
19
+ export class GrpcWebError extends Error {
20
+ readonly status: number;
21
+ readonly statusMessage: string;
22
+
23
+ constructor(status: number, statusMessage: string) {
24
+ super(`gRPC-Web call failed with status ${status}: ${statusMessage}`);
25
+ this.name = "GrpcWebError";
26
+ this.status = status;
27
+ this.statusMessage = statusMessage;
28
+ }
29
+ }
30
+
31
+ const FRAME_DATA = 0x00;
32
+ const FRAME_TRAILER = 0x80;
33
+ const HEADER_SIZE = 5;
34
+
35
+ /**
36
+ * Encodes a protobuf payload into a single gRPC-Web binary data frame (flag 0x00).
37
+ */
38
+ export function encodeGrpcWebEnvelope(message: Uint8Array): Uint8Array {
39
+ const envelope = new Uint8Array(HEADER_SIZE + message.length);
40
+ envelope[0] = FRAME_DATA;
41
+ const view = new DataView(envelope.buffer, envelope.byteOffset, envelope.byteLength);
42
+ view.setUint32(1, message.length, false); // Big-endian u32
43
+ envelope.set(message, HEADER_SIZE);
44
+ return envelope;
45
+ }
46
+
47
+ /**
48
+ * Parses ASCII key-value lines from a gRPC-Web trailer frame payload.
49
+ */
50
+ export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer {
51
+ const text = new TextDecoder("utf-8").decode(bytes);
52
+ const lines = text.split(/\r?\n/);
53
+ const metadata: Record<string, string> = {};
54
+ let status = 0;
55
+ let statusMessage: string | undefined;
56
+
57
+ for (const line of lines) {
58
+ const colonIdx = line.indexOf(":");
59
+ if (colonIdx === -1) continue;
60
+ const key = line.slice(0, colonIdx).trim().toLowerCase();
61
+ const value = line.slice(colonIdx + 1).trim();
62
+ if (!key) continue;
63
+ metadata[key] = value;
64
+ if (key === "grpc-status") {
65
+ const parsed = parseInt(value, 10);
66
+ if (!Number.isNaN(parsed)) {
67
+ status = parsed;
68
+ }
69
+ } else if (key === "grpc-message") {
70
+ try {
71
+ statusMessage = decodeURIComponent(value);
72
+ } catch {
73
+ statusMessage = value;
74
+ }
75
+ }
76
+ }
77
+
78
+ return { status, statusMessage, metadata };
79
+ }
80
+
81
+ /**
82
+ * Decodes a contiguous gRPC-Web binary stream into data messages and trailing metadata.
83
+ */
84
+ export function decodeGrpcWebResponse(bytes: Uint8Array): DecodedGrpcWebResponse {
85
+ const messages: Uint8Array[] = [];
86
+ let offset = 0;
87
+ let trailer: GrpcWebTrailer | undefined;
88
+
89
+ while (offset + HEADER_SIZE <= bytes.length) {
90
+ const flag = bytes[offset];
91
+ const view = new DataView(bytes.buffer, bytes.byteOffset + offset, HEADER_SIZE);
92
+ const length = view.getUint32(1, false);
93
+ const frameStart = offset + HEADER_SIZE;
94
+ const frameEnd = frameStart + length;
95
+
96
+ if (frameEnd > bytes.length) {
97
+ throw new Error(`Incomplete gRPC-Web frame at offset ${offset}: expected ${length} bytes, got ${bytes.length - frameStart}`);
98
+ }
99
+
100
+ const payload = bytes.subarray(frameStart, frameEnd);
101
+
102
+ if (flag === FRAME_DATA) {
103
+ messages.push(payload);
104
+ } else if (flag === FRAME_TRAILER) {
105
+ trailer = parseGrpcWebTrailers(payload);
106
+ }
107
+
108
+ offset = frameEnd;
109
+ }
110
+
111
+ const finalStatus = trailer ? trailer.status : 0;
112
+ const finalMessage = trailer?.statusMessage;
113
+
114
+ return {
115
+ messages,
116
+ status: finalStatus,
117
+ statusMessage: finalMessage,
118
+ trailers: trailer,
119
+ };
120
+ }
@@ -0,0 +1,139 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { atomicWriteFile } from "../config/atomic-write";
4
+ import { getConfigDir } from "../config/paths";
5
+
6
+ export type GrokResetCouponOperationKind = "execute" | "replay" | "identity-mismatch" | "capacity";
7
+
8
+ export interface GrokResetCouponOperationIdentity {
9
+ accountId: string;
10
+ tokenId?: string;
11
+ operationId: string;
12
+ }
13
+
14
+ export interface GrokResetCouponOperationRecord {
15
+ kind: GrokResetCouponOperationKind;
16
+ operationId: string;
17
+ accountId?: string;
18
+ tokenId?: string;
19
+ code?: string;
20
+ settledAt?: number;
21
+ }
22
+
23
+ interface GrokResetCouponOperationState {
24
+ accountId: string;
25
+ tokenId?: string;
26
+ status: "open" | "settled" | "failed";
27
+ code?: string;
28
+ createdAt: number;
29
+ updatedAt: number;
30
+ }
31
+
32
+ interface GrokResetCouponLedger {
33
+ version: 1;
34
+ operations: Record<string, GrokResetCouponOperationState>;
35
+ }
36
+
37
+ export function grokCouponJournalPath(customDir?: string): string {
38
+ const dir = customDir ?? getConfigDir();
39
+ return join(dir, "grok-reset-coupon-ledger.json");
40
+ }
41
+
42
+ function readGrokCouponLedger(filePath: string): GrokResetCouponLedger {
43
+ if (!existsSync(filePath)) {
44
+ return { version: 1, operations: {} };
45
+ }
46
+ try {
47
+ const raw = readFileSync(filePath, "utf-8");
48
+ const parsed = JSON.parse(raw) as GrokResetCouponLedger;
49
+ return parsed && parsed.version === 1 && parsed.operations && typeof parsed.operations === "object"
50
+ ? parsed
51
+ : { version: 1, operations: {} };
52
+ } catch {
53
+ return { version: 1, operations: {} };
54
+ }
55
+ }
56
+
57
+ function writeGrokCouponLedger(filePath: string, ledger: GrokResetCouponLedger, now = Date.now()): void {
58
+ // Prune settled/failed operations older than 30 days to avoid unbounded growth
59
+ const retentionCutoff = now - 30 * 24 * 60 * 60_000;
60
+ ledger.operations = Object.fromEntries(
61
+ Object.entries(ledger.operations).filter(
62
+ ([, op]) => op.status === "open" || op.updatedAt > retentionCutoff,
63
+ ),
64
+ );
65
+ atomicWriteFile(filePath, JSON.stringify(ledger, null, 2));
66
+ }
67
+
68
+ const MAX_GROK_RESET_COUPON_OPERATION_IDS = 256;
69
+
70
+ export function openGrokResetCouponOperation(
71
+ identity: GrokResetCouponOperationIdentity,
72
+ now = Date.now(),
73
+ journalPath?: string,
74
+ ): GrokResetCouponOperationRecord {
75
+ const filePath = journalPath ?? grokCouponJournalPath();
76
+ const ledger = readGrokCouponLedger(filePath);
77
+
78
+ if (Object.keys(ledger.operations).length >= MAX_GROK_RESET_COUPON_OPERATION_IDS) {
79
+ return { kind: "capacity", operationId: identity.operationId };
80
+ }
81
+
82
+ const existing = ledger.operations[identity.operationId];
83
+ if (existing) {
84
+ if (existing.accountId !== identity.accountId) {
85
+ return { kind: "identity-mismatch", operationId: identity.operationId };
86
+ }
87
+ if (existing.status !== "open") {
88
+ // Durably settled already: replay the recorded outcome instead of
89
+ // trusting upstream idempotency for an irreversible spend.
90
+ return {
91
+ kind: "replay",
92
+ operationId: identity.operationId,
93
+ accountId: existing.accountId,
94
+ tokenId: existing.tokenId,
95
+ code: existing.code,
96
+ settledAt: existing.updatedAt,
97
+ };
98
+ }
99
+ return {
100
+ kind: "execute",
101
+ operationId: identity.operationId,
102
+ accountId: existing.accountId,
103
+ tokenId: existing.tokenId,
104
+ };
105
+ }
106
+
107
+ ledger.operations[identity.operationId] = {
108
+ accountId: identity.accountId,
109
+ ...(identity.tokenId === undefined ? {} : { tokenId: identity.tokenId }),
110
+ status: "open",
111
+ createdAt: now,
112
+ updatedAt: now,
113
+ };
114
+ writeGrokCouponLedger(filePath, ledger, now);
115
+ return {
116
+ kind: "execute",
117
+ operationId: identity.operationId,
118
+ accountId: identity.accountId,
119
+ tokenId: identity.tokenId,
120
+ };
121
+ }
122
+
123
+ export function recordGrokResetCouponSettlement(
124
+ settlement: { operationId: string; tokenId?: string; code: string; status: "success" | "failed" },
125
+ now = Date.now(),
126
+ journalPath?: string,
127
+ ): void {
128
+ const filePath = journalPath ?? grokCouponJournalPath();
129
+ const ledger = readGrokCouponLedger(filePath);
130
+ const existing = ledger.operations[settlement.operationId];
131
+ if (!existing) return;
132
+
133
+ existing.status = settlement.status === "success" ? "settled" : "failed";
134
+ existing.code = settlement.code;
135
+ if (settlement.tokenId !== undefined) existing.tokenId = settlement.tokenId;
136
+ existing.updatedAt = now;
137
+
138
+ writeGrokCouponLedger(filePath, ledger, now);
139
+ }
@@ -0,0 +1,278 @@
1
+ import { XAI_GROK_COMPATIBILITY } from "../providers/xai-transport";
2
+ import {
3
+ decodeGrpcWebResponse,
4
+ encodeGrpcWebEnvelope,
5
+ GrpcWebError,
6
+ } from "./grpc-web";
7
+
8
+ export const GROK_CONSUMER_UI_BASE_URL = "https://grok.com";
9
+ export const GROK_GET_REMAINING_RESETS_ENDPOINT =
10
+ "https://grok.com/prod_mc_billing.ConsumerUiSvc/GetRemainingResets";
11
+ export const GROK_REDEEM_RESET_ENDPOINT =
12
+ "https://grok.com/prod_mc_billing.ConsumerUiSvc/RedeemReset";
13
+
14
+ export interface GrokResetCoupon {
15
+ tokenId: string;
16
+ validityStart: string;
17
+ validityEnd: string;
18
+ }
19
+
20
+ export interface GetRemainingResetsOptions {
21
+ accessToken: string;
22
+ fetchFn?: typeof globalThis.fetch;
23
+ signal?: AbortSignal;
24
+ endpoint?: string;
25
+ }
26
+
27
+ export interface RedeemResetOptions {
28
+ accessToken: string;
29
+ tokenId: string;
30
+ fetchFn?: typeof globalThis.fetch;
31
+ signal?: AbortSignal;
32
+ endpoint?: string;
33
+ }
34
+
35
+ export interface RedeemResetResult {
36
+ success: boolean;
37
+ status: number;
38
+ statusMessage?: string;
39
+ }
40
+
41
+ /**
42
+ * Encodes a 32/64-bit non-negative integer into protobuf varint wire bytes.
43
+ */
44
+ export function encodeVarint(value: number | bigint): Uint8Array {
45
+ const bytes: number[] = [];
46
+ let val = BigInt(value);
47
+ while (val >= 0x80n) {
48
+ bytes.push(Number((val & 0x7fn) | 0x80n));
49
+ val >>= 7n;
50
+ }
51
+ bytes.push(Number(val & 0x7fn));
52
+ return new Uint8Array(bytes);
53
+ }
54
+
55
+ /**
56
+ * Decodes a protobuf varint from bytes at offset.
57
+ */
58
+ export function decodeVarint(bytes: Uint8Array, offset: number): { value: number; bytesRead: number } {
59
+ let result = 0;
60
+ let shift = 0;
61
+ let count = 0;
62
+
63
+ while (offset + count < bytes.length) {
64
+ const b = bytes[offset + count];
65
+ count++;
66
+ result |= (b & 0x7f) << shift;
67
+ if ((b & 0x80) === 0) break;
68
+ shift += 7;
69
+ if (shift > 35) {
70
+ // For timestamps seconds, JS safe integers suffice.
71
+ break;
72
+ }
73
+ }
74
+
75
+ return { value: result, bytesRead: count };
76
+ }
77
+
78
+ /**
79
+ * Encodes RedeemResetRequest protobuf: field 1 (string token_id).
80
+ */
81
+ export function encodeRedeemResetRequest(tokenId: string): Uint8Array {
82
+ const tokenBytes = new TextEncoder().encode(tokenId);
83
+ const tag = (1 << 3) | 2; // Field 1, Wire Type 2
84
+ const tagBytes = encodeVarint(tag);
85
+ const lenBytes = encodeVarint(tokenBytes.length);
86
+
87
+ const out = new Uint8Array(tagBytes.length + lenBytes.length + tokenBytes.length);
88
+ out.set(tagBytes, 0);
89
+ out.set(lenBytes, tagBytes.length);
90
+ out.set(tokenBytes, tagBytes.length + lenBytes.length);
91
+ return out;
92
+ }
93
+
94
+ /**
95
+ * Decodes a Timestamp submessage (field 1: int64 seconds).
96
+ */
97
+ function decodeTimestamp(bytes: Uint8Array): number {
98
+ let offset = 0;
99
+ let seconds = 0;
100
+
101
+ while (offset < bytes.length) {
102
+ const { value: tag, bytesRead: tagLen } = decodeVarint(bytes, offset);
103
+ offset += tagLen;
104
+ const fieldNum = tag >> 3;
105
+ const wireType = tag & 0x7;
106
+
107
+ if (wireType === 0) {
108
+ const { value, bytesRead } = decodeVarint(bytes, offset);
109
+ offset += bytesRead;
110
+ if (fieldNum === 1) seconds = value;
111
+ } else if (wireType === 2) {
112
+ const { value: len, bytesRead } = decodeVarint(bytes, offset);
113
+ offset += bytesRead + len;
114
+ } else {
115
+ break;
116
+ }
117
+ }
118
+
119
+ return seconds;
120
+ }
121
+
122
+ /**
123
+ * Decodes a ConsumerResetToken submessage.
124
+ */
125
+ function decodeConsumerResetToken(bytes: Uint8Array): GrokResetCoupon | null {
126
+ let offset = 0;
127
+ let tokenId = "";
128
+ let startSec = 0;
129
+ let endSec = 0;
130
+
131
+ while (offset < bytes.length) {
132
+ const { value: tag, bytesRead: tagLen } = decodeVarint(bytes, offset);
133
+ offset += tagLen;
134
+ const fieldNum = tag >> 3;
135
+ const wireType = tag & 0x7;
136
+
137
+ if (wireType === 2) {
138
+ const { value: len, bytesRead: lenRead } = decodeVarint(bytes, offset);
139
+ offset += lenRead;
140
+ const sub = bytes.subarray(offset, offset + len);
141
+ offset += len;
142
+
143
+ if (fieldNum === 10) {
144
+ tokenId = new TextDecoder("utf-8").decode(sub);
145
+ } else if (fieldNum === 20) {
146
+ startSec = decodeTimestamp(sub);
147
+ } else if (fieldNum === 30) {
148
+ endSec = decodeTimestamp(sub);
149
+ }
150
+ } else if (wireType === 0) {
151
+ const { bytesRead } = decodeVarint(bytes, offset);
152
+ offset += bytesRead;
153
+ } else {
154
+ break;
155
+ }
156
+ }
157
+
158
+ if (!tokenId) return null;
159
+
160
+ return {
161
+ tokenId,
162
+ validityStart: startSec > 0 ? new Date(startSec * 1000).toISOString() : "",
163
+ validityEnd: endSec > 0 ? new Date(endSec * 1000).toISOString() : "",
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Decodes GetRemainingResetsResponse protobuf message: field 10 (repeated ConsumerResetToken).
169
+ */
170
+ export function decodeGetRemainingResetsResponse(payload: Uint8Array): GrokResetCoupon[] {
171
+ const tokens: GrokResetCoupon[] = [];
172
+ let offset = 0;
173
+
174
+ while (offset < payload.length) {
175
+ const { value: tag, bytesRead: tagLen } = decodeVarint(payload, offset);
176
+ offset += tagLen;
177
+ const fieldNum = tag >> 3;
178
+ const wireType = tag & 0x7;
179
+
180
+ if (wireType === 2) {
181
+ const { value: len, bytesRead: lenRead } = decodeVarint(payload, offset);
182
+ offset += lenRead;
183
+ const sub = payload.subarray(offset, offset + len);
184
+ offset += len;
185
+
186
+ if (fieldNum === 10) {
187
+ const token = decodeConsumerResetToken(sub);
188
+ if (token) tokens.push(token);
189
+ }
190
+ } else if (wireType === 0) {
191
+ const { bytesRead } = decodeVarint(payload, offset);
192
+ offset += bytesRead;
193
+ } else {
194
+ break;
195
+ }
196
+ }
197
+
198
+ return tokens;
199
+ }
200
+
201
+ function buildGrokHeaders(accessToken: string): Record<string, string> {
202
+ return {
203
+ "Content-Type": "application/grpc-web+proto",
204
+ "X-Grpc-Web": "1",
205
+ "Accept": "application/grpc-web+proto",
206
+ "Authorization": `Bearer ${accessToken}`,
207
+ [XAI_GROK_COMPATIBILITY.headers.tokenAuth]: "xai-grok-cli",
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Reads available Grok reset tokens for the authenticated xAI account.
213
+ */
214
+ export async function getGrokRemainingResets(options: GetRemainingResetsOptions): Promise<{ tokens: GrokResetCoupon[] }> {
215
+ const fetchImpl = options.fetchFn ?? globalThis.fetch;
216
+ const endpoint = options.endpoint ?? GROK_GET_REMAINING_RESETS_ENDPOINT;
217
+ // BodyInit requires a plain ArrayBuffer backing store; the encoder's generic
218
+ // Uint8Array<ArrayBufferLike> is not assignable under TS 5.7 lib.dom types.
219
+ const emptyBody = encodeGrpcWebEnvelope(new Uint8Array(0)) as Uint8Array<ArrayBuffer>;
220
+
221
+ const res = await fetchImpl(endpoint, {
222
+ method: "POST",
223
+ headers: buildGrokHeaders(options.accessToken),
224
+ body: emptyBody,
225
+ signal: options.signal,
226
+ });
227
+
228
+ if (!res.ok) {
229
+ throw new Error(`GetRemainingResets HTTP error ${res.status}: ${res.statusText}`);
230
+ }
231
+
232
+ const rawBytes = new Uint8Array(await res.arrayBuffer());
233
+ const decoded = decodeGrpcWebResponse(rawBytes);
234
+
235
+ if (decoded.status !== 0) {
236
+ throw new GrpcWebError(decoded.status, decoded.statusMessage ?? "Unknown gRPC error");
237
+ }
238
+
239
+ if (decoded.messages.length === 0) {
240
+ return { tokens: [] };
241
+ }
242
+
243
+ return { tokens: decodeGetRemainingResetsResponse(decoded.messages[0]) };
244
+ }
245
+
246
+ /**
247
+ * Redeems a specific Grok reset token by tokenId.
248
+ */
249
+ export async function redeemGrokResetCoupon(options: RedeemResetOptions): Promise<RedeemResetResult> {
250
+ const fetchImpl = options.fetchFn ?? globalThis.fetch;
251
+ const endpoint = options.endpoint ?? GROK_REDEEM_RESET_ENDPOINT;
252
+ const protoMessage = encodeRedeemResetRequest(options.tokenId);
253
+ const envelope = encodeGrpcWebEnvelope(protoMessage) as Uint8Array<ArrayBuffer>;
254
+
255
+ const res = await fetchImpl(endpoint, {
256
+ method: "POST",
257
+ headers: buildGrokHeaders(options.accessToken),
258
+ body: envelope,
259
+ signal: options.signal,
260
+ });
261
+
262
+ if (!res.ok) {
263
+ throw new Error(`RedeemReset HTTP error ${res.status}: ${res.statusText}`);
264
+ }
265
+
266
+ const rawBytes = new Uint8Array(await res.arrayBuffer());
267
+ const decoded = decodeGrpcWebResponse(rawBytes);
268
+
269
+ if (decoded.status !== 0) {
270
+ throw new GrpcWebError(decoded.status, decoded.statusMessage ?? "Unknown gRPC error");
271
+ }
272
+
273
+ return {
274
+ success: true,
275
+ status: decoded.status,
276
+ statusMessage: decoded.statusMessage,
277
+ };
278
+ }
@@ -10,7 +10,7 @@ import {
10
10
  /** Refresh only previously connected clients; a refused file never blocks its peers. */
11
11
  export async function refreshOwnedCatalogIntegrations(
12
12
  input: Omit<OwnedIntegrationRefreshInput, "clientId">,
13
- clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast"],
13
+ clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo"],
14
14
  ): Promise<OwnedIntegrationRefreshOutcome[]> {
15
15
  let models: Promise<readonly ExportModel[]> | undefined;
16
16
  const loadModels = () => models ??= Promise.resolve().then(() =>
@@ -0,0 +1,73 @@
1
+ import { PARSE_FAILED, parseConfig } from "./config-io";
2
+ import { serializeDocument, UnserializableValueError } from "./serialize";
3
+
4
+ /** Private journal representation, never written into either Cline file. */
5
+ export interface ClineRawPair { settings: string | null; catalog: string | null }
6
+
7
+ export function isClineObject(value: unknown): value is Record<string, unknown> {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+
11
+ export function encodeClinePair(pair: ClineRawPair): string | null {
12
+ return pair.settings === null && pair.catalog === null ? null : JSON.stringify(pair);
13
+ }
14
+
15
+ export function decodeClinePair(text: string | null): ClineRawPair {
16
+ if (text === null) return { settings: null, catalog: null };
17
+ let value: unknown;
18
+ try { value = JSON.parse(text); }
19
+ catch { throw new UnserializableValueError("invalid Cline snapshot bundle"); }
20
+ if (!isClineObject(value) || Object.keys(value).length !== 2
21
+ || !(value.settings === null || typeof value.settings === "string")
22
+ || !(value.catalog === null || typeof value.catalog === "string")) {
23
+ throw new UnserializableValueError("invalid Cline snapshot bundle");
24
+ }
25
+ return { settings: value.settings, catalog: value.catalog };
26
+ }
27
+
28
+ function nativeDocument(value: unknown): value is Record<string, unknown> {
29
+ return isClineObject(value) && (value.version === undefined || value.version === 1)
30
+ && (value.providers === undefined || isClineObject(value.providers));
31
+ }
32
+
33
+ export function parseClineDocument(text: string | null): unknown | typeof PARSE_FAILED {
34
+ try {
35
+ const pair = decodeClinePair(text);
36
+ const settings = parseConfig(pair.settings, "json");
37
+ const catalog = parseConfig(pair.catalog, "json");
38
+ if (!nativeDocument(settings) || !nativeDocument(catalog)
39
+ || (settings.modes !== undefined && !isClineObject(settings.modes))) return PARSE_FAILED;
40
+ return { settings, catalog };
41
+ } catch { return PARSE_FAILED; }
42
+ }
43
+
44
+ export function serializeClineDocument(value: unknown): string {
45
+ if (!isClineObject(value)) throw new UnserializableValueError("invalid Cline document");
46
+ const settings = value.settings ?? {};
47
+ const catalog = value.catalog ?? {};
48
+ if (!nativeDocument(settings) || !nativeDocument(catalog)) {
49
+ throw new UnserializableValueError("unsupported Cline document version or providers shape");
50
+ }
51
+ return JSON.stringify({
52
+ settings: serializeDocument({ version: 1, modes: {}, providers: {}, ...settings }, "json"),
53
+ catalog: serializeDocument({ version: 1, providers: {}, ...catalog }, "json"),
54
+ });
55
+ }
56
+
57
+ /** Cline saves these two fields during normal model selection. Keep only a still-routed choice. */
58
+ export function preserveClineSelection(previous: unknown, next: unknown): void {
59
+ if (!isClineObject(previous) || !isClineObject(next)) return;
60
+ const entry = (document: Record<string, unknown>, part: string): Record<string, unknown> | undefined => {
61
+ const root = document[part];
62
+ const providers = isClineObject(root) ? root.providers : undefined;
63
+ return isClineObject(providers) && isClineObject(providers.opencodex) ? providers.opencodex : undefined;
64
+ };
65
+ const old = entry(previous, "settings");
66
+ const current = entry(next, "settings");
67
+ const models = entry(next, "catalog")?.models;
68
+ if (!current || !isClineObject(current.settings) || !old || !isClineObject(old.settings)) return;
69
+ if (typeof old.settings.model === "string" && isClineObject(models) && Object.hasOwn(models, old.settings.model)) {
70
+ current.settings.model = old.settings.model;
71
+ }
72
+ if (typeof old.updatedAt === "string") current.updatedAt = old.updatedAt;
73
+ }