opencode-copilot-account-switcher 0.12.1 → 0.12.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.
@@ -1,6 +1,7 @@
1
1
  import { type OpenAIOAuthAuth } from "./codex-auth-source.js";
2
2
  import { type CodexStatusFetcherResult } from "./codex-status-fetcher.js";
3
3
  import { type CodexStoreFile } from "./codex-store.js";
4
+ import { type AccountEntry } from "./store.js";
4
5
  type ToastVariant = "info" | "success" | "warning" | "error";
5
6
  type ToastClient = {
6
7
  tui?: {
@@ -36,12 +37,14 @@ type ToastClient = {
36
37
  type AuthPayload = {
37
38
  openai?: OpenAIOAuthAuth;
38
39
  } & Record<string, unknown>;
40
+ type AuthEntries = Record<string, AccountEntry>;
39
41
  export declare class CodexStatusCommandHandledError extends Error {
40
42
  constructor();
41
43
  }
42
44
  export declare function handleCodexStatusCommand(input: {
43
45
  client?: ToastClient;
44
46
  loadAuth?: () => Promise<AuthPayload | undefined>;
47
+ readAuthEntries?: () => Promise<AuthEntries>;
45
48
  persistAuth?: (auth: AuthPayload) => Promise<void>;
46
49
  fetchStatus?: (input: {
47
50
  oauth: OpenAIOAuthAuth;
@@ -1,6 +1,7 @@
1
1
  import { resolveCodexAuthSource } from "./codex-auth-source.js";
2
2
  import { fetchCodexStatus } from "./codex-status-fetcher.js";
3
3
  import { readCodexStore, writeCodexStore } from "./codex-store.js";
4
+ import { readAuth } from "./store.js";
4
5
  export class CodexStatusCommandHandledError extends Error {
5
6
  constructor() {
6
7
  super("codex-status-command-handled");
@@ -78,23 +79,47 @@ function hasCachedStore(store) {
78
79
  || store.status?.premium?.remaining !== undefined);
79
80
  }
80
81
  async function defaultLoadAuth(client) {
82
+ return defaultLoadAuthWithFallback({
83
+ client,
84
+ readAuthEntries: readAuth,
85
+ });
86
+ }
87
+ function mapAuthEntryToOpenAI(entry) {
88
+ if (!entry)
89
+ return undefined;
90
+ return {
91
+ type: "oauth",
92
+ refresh: entry.refresh,
93
+ access: entry.access,
94
+ expires: entry.expires,
95
+ };
96
+ }
97
+ async function defaultLoadAuthWithFallback(input) {
98
+ const client = input.client;
81
99
  const authClient = client?.auth;
82
100
  const getAuth = client?.auth?.get;
83
- if (!getAuth)
84
- return undefined;
85
- try {
86
- const result = await getAuth.call(authClient, { path: { id: "openai" }, throwOnError: true });
87
- const withData = asRecord(result)?.data;
88
- const payload = asRecord(withData) ?? asRecord(result);
89
- if (!payload)
90
- return undefined;
91
- return {
92
- openai: payload,
93
- };
101
+ if (getAuth) {
102
+ try {
103
+ const result = await getAuth.call(authClient, { path: { id: "openai" }, throwOnError: true });
104
+ const withData = asRecord(result)?.data;
105
+ const payload = asRecord(withData) ?? asRecord(result);
106
+ if (payload) {
107
+ return {
108
+ openai: payload,
109
+ };
110
+ }
111
+ }
112
+ catch {
113
+ // fall through to auth.json fallback
114
+ }
94
115
  }
95
- catch {
116
+ const authEntries = await input.readAuthEntries().catch(() => ({}));
117
+ const openai = mapAuthEntryToOpenAI(authEntries.openai);
118
+ if (!openai)
96
119
  return undefined;
97
- }
120
+ return {
121
+ openai,
122
+ };
98
123
  }
99
124
  async function defaultPersistAuth(client, auth) {
100
125
  const authClient = client?.auth;
@@ -131,7 +156,10 @@ function patchAuth(auth, patch) {
131
156
  };
132
157
  }
133
158
  export async function handleCodexStatusCommand(input) {
134
- const loadAuth = input.loadAuth ?? (() => defaultLoadAuth(input.client));
159
+ const loadAuth = input.loadAuth ?? (() => defaultLoadAuthWithFallback({
160
+ client: input.client,
161
+ readAuthEntries: input.readAuthEntries ?? readAuth,
162
+ }));
135
163
  const persistAuth = input.persistAuth ?? ((nextAuth) => defaultPersistAuth(input.client, nextAuth));
136
164
  const fetchStatus = input.fetchStatus ?? ((next) => fetchCodexStatus(next));
137
165
  const readStore = input.readStore ?? (() => readCodexStore());
@@ -1,5 +1,6 @@
1
1
  import { appendFileSync } from "node:fs";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { createHash } from "node:crypto";
3
4
  import { createCompactionLoopSafetyBypass, createLoopSafetySystemTransform, getLoopSafetyProviderScope, } from "./loop-safety-plugin.js";
4
5
  import { COPILOT_PROVIDER_DESCRIPTOR } from "./providers/descriptor.js";
5
6
  import { createCopilotRetryingFetch, cleanupLongIdsForAccountSwitch, detectRateLimitEvidence, INTERNAL_SESSION_CONTEXT_KEY, } from "./copilot-network-retry.js";
@@ -164,9 +165,11 @@ function mergeAndRewriteRequestHeaders(request, init, rewriteHeaders) {
164
165
  headers: normalizedHeaders,
165
166
  };
166
167
  if (request instanceof Request) {
168
+ const requestInit = { ...(init ?? {}) };
169
+ delete requestInit.headers;
167
170
  return {
168
171
  request: rewriteRequestHeaders(request),
169
- init: normalizedInit,
172
+ init: Object.keys(requestInit).length > 0 ? requestInit : undefined,
170
173
  };
171
174
  }
172
175
  return {
@@ -174,6 +177,30 @@ function mergeAndRewriteRequestHeaders(request, init, rewriteHeaders) {
174
177
  init: normalizedInit,
175
178
  };
176
179
  }
180
+ function stripDuplicateHeadersFromRequestWhenInitOverrides(request, init) {
181
+ if (!(request instanceof Request) || init?.headers == null) {
182
+ return { request, init };
183
+ }
184
+ const initHeaders = new Headers(init.headers);
185
+ if ([...initHeaders.keys()].length === 0) {
186
+ return { request, init };
187
+ }
188
+ const requestHeaders = new Headers(request.headers);
189
+ let changed = false;
190
+ for (const name of initHeaders.keys()) {
191
+ if (requestHeaders.has(name)) {
192
+ requestHeaders.delete(name);
193
+ changed = true;
194
+ }
195
+ }
196
+ if (!changed) {
197
+ return { request, init };
198
+ }
199
+ return {
200
+ request: new Request(request, { headers: requestHeaders }),
201
+ init,
202
+ };
203
+ }
177
204
  function getMergedRequestHeader(request, init, name) {
178
205
  const headers = new Headers(request instanceof Request ? request.headers : undefined);
179
206
  for (const [headerName, value] of new Headers(init?.headers).entries()) {
@@ -210,6 +237,11 @@ function sanitizeLoggedRequestHeadersRecord(headers) {
210
237
  }
211
238
  return sanitized;
212
239
  }
240
+ function toAuthFingerprint(value) {
241
+ if (typeof value !== "string" || value.length === 0)
242
+ return undefined;
243
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
244
+ }
213
245
  function getInternalSessionID(request, init) {
214
246
  const headerValue = getMergedRequestHeader(request, init, "x-opencode-session-id");
215
247
  if (typeof headerValue === "string" && headerValue.length > 0)
@@ -661,9 +693,12 @@ export function buildPluginHooks(input) {
661
693
  let decisionTouchWriteOutcome = "skipped-missing-session";
662
694
  let decisionTouchWriteError;
663
695
  let finalChosenAccount = resolved.name;
696
+ let chosenAccountAuthFingerprint = toAuthFingerprint(resolved.entry.refresh);
664
697
  let finalRequestHeaders = getFinalSentRequestHeadersRecord(selectionRequest, selectionInit);
665
698
  let networkRequestHeaders;
699
+ let networkRequestUsedInitHeaders = selectionInit?.headers != null;
666
700
  const previousBindingAccount = sessionID.length > 0 ? sessionAccountBindings.get(sessionID)?.accountName : undefined;
701
+ const debugLinkId = getMergedRequestHeader(selectionRequest, selectionInit, INTERNAL_DEBUG_LINK_HEADER) ?? undefined;
667
702
  if (sessionID.length > 0) {
668
703
  sessionAccountBindings.set(sessionID, {
669
704
  accountName: resolved.name,
@@ -731,10 +766,12 @@ export function buildPluginHooks(input) {
731
766
  expires: candidate.entry.expires,
732
767
  enterpriseUrl: candidate.entry.enterpriseUrl,
733
768
  };
734
- const outbound = stripInternalSessionHeader(requestValue, initValue);
769
+ const deduplicated = stripDuplicateHeadersFromRequestWhenInitOverrides(requestValue, initValue);
770
+ const outbound = stripInternalSessionHeader(deduplicated.request, deduplicated.init);
735
771
  return finalHeaderCapture.run((headers) => {
736
772
  finalRequestHeaders = headers;
737
773
  networkRequestHeaders = headers;
774
+ networkRequestUsedInitHeaders = outbound.init?.headers != null;
738
775
  }, () => authOverride.run(candidateAuth, () => config.fetch(rewriteRequestForAccount(outbound.request, candidate.entry.enterpriseUrl), outbound.init)));
739
776
  };
740
777
  const response = await sendWithAccount(resolved, nextRequest, nextInit);
@@ -858,6 +895,7 @@ export function buildPluginHooks(input) {
858
895
  decisionSwitchFrom = resolved.name;
859
896
  decisionSwitchBlockedBy = undefined;
860
897
  finalChosenAccount = replacement.name;
898
+ chosenAccountAuthFingerprint = toAuthFingerprint(replacement.entry.refresh);
861
899
  modelAccountFirstUse.add(replacement.name);
862
900
  const switchToast = buildConsumptionToast({
863
901
  accountName: replacement.name,
@@ -889,6 +927,9 @@ export function buildPluginHooks(input) {
889
927
  candidateNames,
890
928
  loads: decisionLoads,
891
929
  chosenAccount: finalChosenAccount,
930
+ chosenAccountAuthFingerprint,
931
+ debugLinkId,
932
+ networkRequestUsedInitHeaders,
892
933
  reason: decisionReason,
893
934
  switched: decisionSwitched,
894
935
  switchFrom: decisionSwitchFrom,
@@ -922,6 +963,9 @@ export function buildPluginHooks(input) {
922
963
  candidateNames,
923
964
  loads: decisionLoads,
924
965
  chosenAccount: finalChosenAccount,
966
+ chosenAccountAuthFingerprint,
967
+ debugLinkId,
968
+ networkRequestUsedInitHeaders,
925
969
  reason: decisionReason,
926
970
  switched: decisionSwitched,
927
971
  switchFrom: decisionSwitchFrom,
@@ -1181,6 +1225,8 @@ export function buildPluginHooks(input) {
1181
1225
  injectArmed = false;
1182
1226
  return;
1183
1227
  }
1228
+ if (hookInput.tool === "task")
1229
+ return;
1184
1230
  if (!injectArmed)
1185
1231
  return;
1186
1232
  const begin = "[COPILOT_INJECT_V1_BEGIN]";
@@ -64,6 +64,9 @@ export type RouteDecisionEvent = {
64
64
  at: number;
65
65
  modelID?: string;
66
66
  chosenAccount: string;
67
+ chosenAccountAuthFingerprint?: string;
68
+ debugLinkId?: string;
69
+ networkRequestUsedInitHeaders?: boolean;
67
70
  sessionID?: string;
68
71
  sessionIDPresent: boolean;
69
72
  groupSource: "model" | "active";
@@ -246,26 +246,20 @@ async function patchStoppedToolTranscript(input) {
246
246
  export async function handleCompactCommand(input) {
247
247
  const session = input.client?.session;
248
248
  const summarize = session?.summarize;
249
- const model = input.model ?? getLatestAssistantModel(await getSessionMessages(session, input.sessionID));
250
- if (!model) {
249
+ if (!summarize) {
251
250
  await showToast({
252
251
  client: input.client,
253
- message: "No assistant model context available for compact.",
252
+ message: "Session summarize is unavailable for compact.",
254
253
  variant: "warning",
255
254
  });
256
255
  throw new SessionControlCommandHandledError();
257
256
  }
258
- if (summarize) {
259
- await summarize({
260
- auto: true,
261
- model,
262
- });
263
- throw new SessionControlCommandHandledError();
264
- }
265
- await showToast({
266
- client: input.client,
267
- message: "Session summarize is unavailable for compact.",
268
- variant: "warning",
257
+ const model = input.model ?? getLatestAssistantModel(await getSessionMessages(session, input.sessionID));
258
+ await summarize(model ? {
259
+ auto: true,
260
+ model,
261
+ } : {
262
+ auto: true,
269
263
  });
270
264
  throw new SessionControlCommandHandledError();
271
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-copilot-account-switcher",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "GitHub Copilot account switcher plugin for OpenCode",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",