opencode-copilot-account-switcher 0.12.0 → 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");
@@ -19,11 +20,12 @@ function pickNumber(value) {
19
20
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
20
21
  }
21
22
  async function showToast(input) {
23
+ const tui = input.client?.tui;
22
24
  const show = input.client?.tui?.showToast;
23
25
  if (!show)
24
26
  return;
25
27
  try {
26
- await show({
28
+ await show.call(tui, {
27
29
  body: {
28
30
  message: input.message,
29
31
  variant: input.variant,
@@ -77,31 +79,57 @@ function hasCachedStore(store) {
77
79
  || store.status?.premium?.remaining !== undefined);
78
80
  }
79
81
  async function defaultLoadAuth(client) {
80
- const getAuth = client?.auth?.get;
81
- if (!getAuth)
82
+ return defaultLoadAuthWithFallback({
83
+ client,
84
+ readAuthEntries: readAuth,
85
+ });
86
+ }
87
+ function mapAuthEntryToOpenAI(entry) {
88
+ if (!entry)
82
89
  return undefined;
83
- try {
84
- const result = await getAuth({ path: { id: "openai" }, throwOnError: true });
85
- const withData = asRecord(result)?.data;
86
- const payload = asRecord(withData) ?? asRecord(result);
87
- if (!payload)
88
- return undefined;
89
- return {
90
- openai: payload,
91
- };
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;
99
+ const authClient = client?.auth;
100
+ const getAuth = client?.auth?.get;
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
+ }
92
115
  }
93
- catch {
116
+ const authEntries = await input.readAuthEntries().catch(() => ({}));
117
+ const openai = mapAuthEntryToOpenAI(authEntries.openai);
118
+ if (!openai)
94
119
  return undefined;
95
- }
120
+ return {
121
+ openai,
122
+ };
96
123
  }
97
124
  async function defaultPersistAuth(client, auth) {
125
+ const authClient = client?.auth;
98
126
  const setAuth = client?.auth?.set;
99
127
  if (!setAuth)
100
128
  return;
101
129
  const openai = asRecord(auth.openai);
102
130
  if (!openai)
103
131
  return;
104
- await setAuth({
132
+ await setAuth.call(authClient, {
105
133
  path: { id: "openai" },
106
134
  body: {
107
135
  type: "oauth",
@@ -128,7 +156,10 @@ function patchAuth(auth, patch) {
128
156
  };
129
157
  }
130
158
  export async function handleCodexStatusCommand(input) {
131
- const loadAuth = input.loadAuth ?? (() => defaultLoadAuth(input.client));
159
+ const loadAuth = input.loadAuth ?? (() => defaultLoadAuthWithFallback({
160
+ client: input.client,
161
+ readAuthEntries: input.readAuthEntries ?? readAuth,
162
+ }));
132
163
  const persistAuth = input.persistAuth ?? ((nextAuth) => defaultPersistAuth(input.client, nextAuth));
133
164
  const fetchStatus = input.fetchStatus ?? ((next) => fetchCodexStatus(next));
134
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,8 +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);
698
+ let networkRequestHeaders;
699
+ let networkRequestUsedInitHeaders = selectionInit?.headers != null;
665
700
  const previousBindingAccount = sessionID.length > 0 ? sessionAccountBindings.get(sessionID)?.accountName : undefined;
701
+ const debugLinkId = getMergedRequestHeader(selectionRequest, selectionInit, INTERNAL_DEBUG_LINK_HEADER) ?? undefined;
666
702
  if (sessionID.length > 0) {
667
703
  sessionAccountBindings.set(sessionID, {
668
704
  accountName: resolved.name,
@@ -730,9 +766,12 @@ export function buildPluginHooks(input) {
730
766
  expires: candidate.entry.expires,
731
767
  enterpriseUrl: candidate.entry.enterpriseUrl,
732
768
  };
733
- const outbound = stripInternalSessionHeader(requestValue, initValue);
769
+ const deduplicated = stripDuplicateHeadersFromRequestWhenInitOverrides(requestValue, initValue);
770
+ const outbound = stripInternalSessionHeader(deduplicated.request, deduplicated.init);
734
771
  return finalHeaderCapture.run((headers) => {
735
772
  finalRequestHeaders = headers;
773
+ networkRequestHeaders = headers;
774
+ networkRequestUsedInitHeaders = outbound.init?.headers != null;
736
775
  }, () => authOverride.run(candidateAuth, () => config.fetch(rewriteRequestForAccount(outbound.request, candidate.entry.enterpriseUrl), outbound.init)));
737
776
  };
738
777
  const response = await sendWithAccount(resolved, nextRequest, nextInit);
@@ -856,6 +895,7 @@ export function buildPluginHooks(input) {
856
895
  decisionSwitchFrom = resolved.name;
857
896
  decisionSwitchBlockedBy = undefined;
858
897
  finalChosenAccount = replacement.name;
898
+ chosenAccountAuthFingerprint = toAuthFingerprint(replacement.entry.refresh);
859
899
  modelAccountFirstUse.add(replacement.name);
860
900
  const switchToast = buildConsumptionToast({
861
901
  accountName: replacement.name,
@@ -887,6 +927,9 @@ export function buildPluginHooks(input) {
887
927
  candidateNames,
888
928
  loads: decisionLoads,
889
929
  chosenAccount: finalChosenAccount,
930
+ chosenAccountAuthFingerprint,
931
+ debugLinkId,
932
+ networkRequestUsedInitHeaders,
890
933
  reason: decisionReason,
891
934
  switched: decisionSwitched,
892
935
  switchFrom: decisionSwitchFrom,
@@ -896,6 +939,7 @@ export function buildPluginHooks(input) {
896
939
  rateLimitMatched: decisionRateLimitMatched,
897
940
  retryAfterMs: decisionRetryAfterMs,
898
941
  finalRequestHeaders,
942
+ networkRequestHeaders,
899
943
  },
900
944
  }).catch(() => undefined);
901
945
  return sendWithAccount(replacement, retriedRequest, retriedInit);
@@ -919,6 +963,9 @@ export function buildPluginHooks(input) {
919
963
  candidateNames,
920
964
  loads: decisionLoads,
921
965
  chosenAccount: finalChosenAccount,
966
+ chosenAccountAuthFingerprint,
967
+ debugLinkId,
968
+ networkRequestUsedInitHeaders,
922
969
  reason: decisionReason,
923
970
  switched: decisionSwitched,
924
971
  switchFrom: decisionSwitchFrom,
@@ -928,6 +975,7 @@ export function buildPluginHooks(input) {
928
975
  rateLimitMatched: decisionRateLimitMatched,
929
976
  retryAfterMs: decisionRetryAfterMs,
930
977
  finalRequestHeaders,
978
+ networkRequestHeaders,
931
979
  },
932
980
  }).catch(() => undefined);
933
981
  if (shouldShowConsumptionToast({ reason: decisionReason, isFirstUse })) {
@@ -1177,6 +1225,8 @@ export function buildPluginHooks(input) {
1177
1225
  injectArmed = false;
1178
1226
  return;
1179
1227
  }
1228
+ if (hookInput.tool === "task")
1229
+ return;
1180
1230
  if (!injectArmed)
1181
1231
  return;
1182
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";
@@ -78,6 +81,7 @@ export type RouteDecisionEvent = {
78
81
  rateLimitMatched: boolean;
79
82
  retryAfterMs?: number;
80
83
  finalRequestHeaders?: Record<string, string>;
84
+ networkRequestHeaders?: Record<string, string>;
81
85
  };
82
86
  export type RoutingEvent = SessionTouchEvent | RateLimitFlaggedEvent;
83
87
  export declare function appendRouteDecisionEvent(input: {
@@ -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.0",
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",