opencode-copilot-account-switcher 0.3.0 → 0.3.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.
@@ -44,6 +44,15 @@ export type CopilotRetryContext = {
44
44
  };
45
45
  }>;
46
46
  };
47
+ part?: {
48
+ update?: (input: {
49
+ sessionID: string;
50
+ messageID: string;
51
+ partID: string;
52
+ directory?: string;
53
+ part?: JsonRecord;
54
+ }) => Promise<unknown>;
55
+ };
47
56
  tui?: {
48
57
  showToast?: (options: {
49
58
  body: {
@@ -318,6 +318,32 @@ async function repairSessionPart(sessionID, failingId, ctx) {
318
318
  },
319
319
  body: JSON.stringify(body),
320
320
  };
321
+ if (ctx?.client?.part?.update) {
322
+ try {
323
+ await ctx.client.part.update({
324
+ sessionID,
325
+ messageID: match.messageID,
326
+ partID: match.partID,
327
+ directory: ctx.directory,
328
+ part: body,
329
+ });
330
+ debugLog("input-id retry session repair", {
331
+ partID: match.partID,
332
+ messageID: match.messageID,
333
+ sessionID,
334
+ });
335
+ return true;
336
+ }
337
+ catch (error) {
338
+ debugLog("input-id retry session repair failed", {
339
+ partID: match.partID,
340
+ messageID: match.messageID,
341
+ sessionID,
342
+ error: String(error instanceof Error ? error.message : error),
343
+ });
344
+ return false;
345
+ }
346
+ }
321
347
  if (ctx?.patchPart) {
322
348
  try {
323
349
  await ctx.patchPart({ url: url.href, init });
package/dist/index.d.ts CHANGED
@@ -1,4 +1 @@
1
1
  export { CopilotAccountSwitcher } from "./plugin.js";
2
- export { applyMenuAction } from "./plugin-actions.js";
3
- export { buildPluginHooks } from "./plugin-hooks.js";
4
- export { createOfficialFetchAdapter, loadOfficialCopilotConfig } from "./upstream/copilot-loader-adapter.js";
package/dist/index.js CHANGED
@@ -1,4 +1 @@
1
1
  export { CopilotAccountSwitcher } from "./plugin.js";
2
- export { applyMenuAction } from "./plugin-actions.js";
3
- export { buildPluginHooks } from "./plugin-hooks.js";
4
- export { createOfficialFetchAdapter, loadOfficialCopilotConfig } from "./upstream/copilot-loader-adapter.js";
@@ -0,0 +1,3 @@
1
+ export { applyMenuAction } from "./plugin-actions.js";
2
+ export { buildPluginHooks } from "./plugin-hooks.js";
3
+ export { createOfficialFetchAdapter, loadOfficialCopilotConfig } from "./upstream/copilot-loader-adapter.js";
@@ -0,0 +1,3 @@
1
+ export { applyMenuAction } from "./plugin-actions.js";
2
+ export { buildPluginHooks } from "./plugin-hooks.js";
3
+ export { createOfficialFetchAdapter, loadOfficialCopilotConfig } from "./upstream/copilot-loader-adapter.js";
@@ -4,10 +4,22 @@ export declare function persistAccountSwitch(input: {
4
4
  store: StoreFile;
5
5
  name: string;
6
6
  at: number;
7
- writeStore: (store: StoreFile) => Promise<void>;
7
+ writeStore: (store: StoreFile, meta?: {
8
+ reason?: string;
9
+ source?: string;
10
+ actionType?: string;
11
+ inputStage?: string;
12
+ parsedKey?: string;
13
+ }) => Promise<void>;
8
14
  }): Promise<void>;
9
15
  export declare function applyMenuAction(input: {
10
16
  action: MenuAction;
11
17
  store: StoreFile;
12
- writeStore: (store: StoreFile) => Promise<void>;
18
+ writeStore: (store: StoreFile, meta?: {
19
+ reason?: string;
20
+ source?: string;
21
+ actionType?: string;
22
+ inputStage?: string;
23
+ parsedKey?: string;
24
+ }) => Promise<void>;
13
25
  }): Promise<boolean>;
@@ -2,17 +2,29 @@ export async function persistAccountSwitch(input) {
2
2
  input.store.active = input.name;
3
3
  input.store.accounts[input.name].lastUsed = input.at;
4
4
  input.store.lastAccountSwitchAt = input.at;
5
- await input.writeStore(input.store);
5
+ await input.writeStore(input.store, {
6
+ reason: "persist-account-switch",
7
+ source: "persistAccountSwitch",
8
+ actionType: "switch",
9
+ });
6
10
  }
7
11
  export async function applyMenuAction(input) {
8
12
  if (input.action.type === "toggle-loop-safety") {
9
13
  input.store.loopSafetyEnabled = input.store.loopSafetyEnabled !== true;
10
- await input.writeStore(input.store);
14
+ await input.writeStore(input.store, {
15
+ reason: "toggle-loop-safety",
16
+ source: "applyMenuAction",
17
+ actionType: "toggle-loop-safety",
18
+ });
11
19
  return true;
12
20
  }
13
21
  if (input.action.type === "toggle-network-retry") {
14
22
  input.store.networkRetryEnabled = input.store.networkRetryEnabled !== true;
15
- await input.writeStore(input.store);
23
+ await input.writeStore(input.store, {
24
+ reason: "toggle-network-retry",
25
+ source: "applyMenuAction",
26
+ actionType: "toggle-network-retry",
27
+ });
16
28
  return true;
17
29
  }
18
30
  return false;
@@ -1,6 +1,6 @@
1
1
  import { type CopilotPluginHooks } from "./loop-safety-plugin.js";
2
2
  import { type CopilotRetryContext, type FetchLike } from "./copilot-network-retry.js";
3
- import { type StoreFile } from "./store.js";
3
+ import { type StoreFile, type StoreWriteDebugMeta } from "./store.js";
4
4
  import { type CopilotAuthState, type CopilotProviderConfig, type OfficialCopilotConfig } from "./upstream/copilot-loader-adapter.js";
5
5
  type ChatHeadersHook = (input: {
6
6
  sessionID: string;
@@ -25,7 +25,7 @@ type CopilotPluginHooksWithChatHeaders = CopilotPluginHooks & {
25
25
  export declare function buildPluginHooks(input: {
26
26
  auth: NonNullable<CopilotPluginHooks["auth"]>;
27
27
  loadStore?: () => Promise<StoreFile | undefined>;
28
- writeStore?: (store: StoreFile) => Promise<void>;
28
+ writeStore?: (store: StoreFile, meta?: StoreWriteDebugMeta) => Promise<void>;
29
29
  loadOfficialConfig?: (input: {
30
30
  getAuth: () => Promise<CopilotAuthState | undefined>;
31
31
  provider?: CopilotProviderConfig;
@@ -31,7 +31,10 @@ export function buildPluginHooks(input) {
31
31
  if (latestStore.lastAccountSwitchAt !== capturedLastAccountSwitchAt)
32
32
  return;
33
33
  delete latestStore.lastAccountSwitchAt;
34
- await persistStore(latestStore);
34
+ await persistStore(latestStore, {
35
+ reason: "clear-account-switch-context",
36
+ source: "plugin-hooks",
37
+ });
35
38
  }
36
39
  catch (error) {
37
40
  console.warn("[plugin-hooks] failed to clear account-switch context", error);
package/dist/plugin.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
- import { type StoreFile } from "./store.js";
2
+ import { type StoreFile, type StoreWriteDebugMeta } from "./store.js";
3
3
  export declare function activateAddedAccount(input: {
4
4
  store: StoreFile;
5
5
  name: string;
6
6
  switchAccount: () => Promise<void>;
7
- writeStore: (store: StoreFile) => Promise<void>;
7
+ writeStore: (store: StoreFile, meta?: StoreWriteDebugMeta) => Promise<void>;
8
8
  now?: () => number;
9
9
  }): Promise<void>;
10
10
  export declare const CopilotAccountSwitcher: Plugin;
package/dist/plugin.js CHANGED
@@ -444,7 +444,11 @@ async function switchAccount(client, entry) {
444
444
  });
445
445
  }
446
446
  export async function activateAddedAccount(input) {
447
- await input.writeStore(input.store);
447
+ await input.writeStore(input.store, {
448
+ reason: "activate-added-account",
449
+ source: "activateAddedAccount",
450
+ actionType: "add",
451
+ });
448
452
  await input.switchAccount();
449
453
  await persistAccountSwitch({
450
454
  store: input.store,
@@ -501,14 +505,17 @@ export const CopilotAccountSwitcher = async (input) => {
501
505
  store,
502
506
  name,
503
507
  switchAccount: () => switchAccount(client, entry),
504
- writeStore,
508
+ writeStore: persistStore,
505
509
  });
506
510
  // fallthrough to menu
507
511
  }
508
512
  if (!Object.values(store.accounts).some((entry) => entry.user || entry.email || (entry.orgs && entry.orgs.length > 0))) {
509
513
  await refreshIdentity(store);
510
514
  dedupe(store);
511
- await writeStore(store);
515
+ await persistStore(store, {
516
+ reason: "refresh-identity-bootstrap",
517
+ source: "plugin.runMenu",
518
+ });
512
519
  }
513
520
  if (!isTTY()) {
514
521
  console.log("Interactive menu requires a TTY terminal");
@@ -528,7 +535,11 @@ export const CopilotAccountSwitcher = async (input) => {
528
535
  store.accounts[item.name] = item.entry;
529
536
  }
530
537
  store.lastQuotaRefresh = now();
531
- await writeStore(store);
538
+ await persistStore(store, {
539
+ reason: "auto-refresh",
540
+ source: "plugin.runMenu",
541
+ actionType: "toggle-refresh",
542
+ });
532
543
  nextRefresh = now() + (store.refreshMinutes ?? 15) * 60_000;
533
544
  }
534
545
  const entries = Object.entries(store.accounts);
@@ -583,7 +594,7 @@ export const CopilotAccountSwitcher = async (input) => {
583
594
  const active = store.active ? store.accounts[store.active] : undefined;
584
595
  return active;
585
596
  }
586
- if (await applyMenuAction({ action, store, writeStore })) {
597
+ if (await applyMenuAction({ action, store, writeStore: persistStore })) {
587
598
  continue;
588
599
  }
589
600
  if (action.type === "add") {
@@ -609,11 +620,15 @@ export const CopilotAccountSwitcher = async (input) => {
609
620
  store,
610
621
  name: entry.name,
611
622
  switchAccount: () => switchAccount(client, entry),
612
- writeStore,
623
+ writeStore: persistStore,
613
624
  });
614
625
  }
615
626
  else {
616
- await writeStore(store);
627
+ await persistStore(store, {
628
+ reason: "add-account-device-login",
629
+ source: "plugin.runMenu",
630
+ actionType: "add",
631
+ });
617
632
  }
618
633
  continue;
619
634
  }
@@ -633,11 +648,15 @@ export const CopilotAccountSwitcher = async (input) => {
633
648
  store,
634
649
  name: manual.entry.name,
635
650
  switchAccount: () => switchAccount(client, manual.entry),
636
- writeStore,
651
+ writeStore: persistStore,
637
652
  });
638
653
  }
639
654
  else {
640
- await writeStore(store);
655
+ await persistStore(store, {
656
+ reason: "add-account-manual",
657
+ source: "plugin.runMenu",
658
+ actionType: "add",
659
+ });
641
660
  }
642
661
  continue;
643
662
  }
@@ -656,19 +675,31 @@ export const CopilotAccountSwitcher = async (input) => {
656
675
  entry.name = buildName(entry, user?.login);
657
676
  }
658
677
  mergeAuth(store, imported);
659
- await writeStore(store);
678
+ await persistStore(store, {
679
+ reason: "import-auth",
680
+ source: "plugin.runMenu",
681
+ actionType: "import",
682
+ });
660
683
  continue;
661
684
  }
662
685
  if (action.type === "refresh-identity") {
663
686
  await refreshIdentity(store);
664
687
  dedupe(store);
665
- await writeStore(store);
688
+ await persistStore(store, {
689
+ reason: "refresh-identity",
690
+ source: "plugin.runMenu",
691
+ actionType: "refresh-identity",
692
+ });
666
693
  continue;
667
694
  }
668
695
  if (action.type === "toggle-refresh") {
669
696
  store.autoRefresh = !store.autoRefresh;
670
697
  store.refreshMinutes = store.refreshMinutes ?? 15;
671
- await writeStore(store);
698
+ await persistStore(store, {
699
+ reason: "toggle-refresh",
700
+ source: "plugin.runMenu",
701
+ actionType: "toggle-refresh",
702
+ });
672
703
  continue;
673
704
  }
674
705
  if (action.type === "set-interval") {
@@ -676,7 +707,11 @@ export const CopilotAccountSwitcher = async (input) => {
676
707
  const minutes = Math.max(1, Math.min(180, Number(value)));
677
708
  if (Number.isFinite(minutes))
678
709
  store.refreshMinutes = minutes;
679
- await writeStore(store);
710
+ await persistStore(store, {
711
+ reason: "set-interval",
712
+ source: "plugin.runMenu",
713
+ actionType: "set-interval",
714
+ });
680
715
  continue;
681
716
  }
682
717
  if (action.type === "quota") {
@@ -691,7 +726,11 @@ export const CopilotAccountSwitcher = async (input) => {
691
726
  store.accounts[item.name] = item.entry;
692
727
  }
693
728
  store.lastQuotaRefresh = now();
694
- await writeStore(store);
729
+ await persistStore(store, {
730
+ reason: "quota-refresh",
731
+ source: "plugin.runMenu",
732
+ actionType: "quota",
733
+ });
695
734
  continue;
696
735
  }
697
736
  if (action.type === "check-models") {
@@ -705,13 +744,21 @@ export const CopilotAccountSwitcher = async (input) => {
705
744
  for (const item of updated) {
706
745
  store.accounts[item.name] = item.entry;
707
746
  }
708
- await writeStore(store);
747
+ await persistStore(store, {
748
+ reason: "check-models",
749
+ source: "plugin.runMenu",
750
+ actionType: "check-models",
751
+ });
709
752
  continue;
710
753
  }
711
754
  if (action.type === "remove-all") {
712
755
  store.accounts = {};
713
756
  store.active = undefined;
714
- await writeStore(store);
757
+ await persistStore(store, {
758
+ reason: "remove-all",
759
+ source: "plugin.runMenu",
760
+ actionType: "remove-all",
761
+ });
715
762
  continue;
716
763
  }
717
764
  if (action.type === "switch") {
@@ -726,7 +773,11 @@ export const CopilotAccountSwitcher = async (input) => {
726
773
  delete store.accounts[name];
727
774
  if (store.active === name)
728
775
  store.active = undefined;
729
- await writeStore(store);
776
+ await persistStore(store, {
777
+ reason: "remove-account",
778
+ source: "plugin.runMenu",
779
+ actionType: "remove",
780
+ });
730
781
  continue;
731
782
  }
732
783
  await switchAccount(client, entry);
@@ -734,7 +785,7 @@ export const CopilotAccountSwitcher = async (input) => {
734
785
  store,
735
786
  name,
736
787
  at: now(),
737
- writeStore,
788
+ writeStore: persistStore,
738
789
  });
739
790
  console.log("Switched account. If a later Copilot session hits input[*].id too long after switching, enable Copilot Network Retry from the menu.");
740
791
  continue;
@@ -751,3 +802,4 @@ export const CopilotAccountSwitcher = async (input) => {
751
802
  serverUrl,
752
803
  });
753
804
  };
805
+ const persistStore = (store, meta) => writeStore(store, { debug: meta });
package/dist/store.d.ts CHANGED
@@ -1,3 +1,10 @@
1
+ export type StoreWriteDebugMeta = {
2
+ reason?: string;
3
+ source?: string;
4
+ actionType?: string;
5
+ inputStage?: string;
6
+ parsedKey?: string;
7
+ };
1
8
  export type AccountEntry = {
2
9
  name: string;
3
10
  refresh: string;
@@ -64,4 +71,7 @@ export declare function parseStore(raw: string): StoreFile;
64
71
  export declare function readStore(filePath?: string): Promise<StoreFile>;
65
72
  export declare function readStoreSafe(filePath?: string): Promise<StoreFile | undefined>;
66
73
  export declare function readAuth(filePath?: string): Promise<Record<string, AccountEntry>>;
67
- export declare function writeStore(store: StoreFile): Promise<void>;
74
+ export declare function writeStore(store: StoreFile, options?: {
75
+ filePath?: string;
76
+ debug?: StoreWriteDebugMeta;
77
+ }): Promise<void>;
package/dist/store.js CHANGED
@@ -4,6 +4,53 @@ import { promises as fs } from "node:fs";
4
4
  import { xdgConfig, xdgData } from "xdg-basedir";
5
5
  const filename = "copilot-accounts.json";
6
6
  const authFile = "auth.json";
7
+ const defaultStoreDebugLogFile = (() => {
8
+ const tmp = process.env.TEMP || process.env.TMP || "/tmp";
9
+ return `${tmp}/opencode-copilot-store-debug.log`;
10
+ })();
11
+ function isStoreDebugEnabled() {
12
+ return process.env.OPENCODE_COPILOT_STORE_DEBUG === "1";
13
+ }
14
+ function buildStoreSnapshot(store) {
15
+ return {
16
+ active: store?.active ?? null,
17
+ accountCount: Object.keys(store?.accounts ?? {}).length,
18
+ loopSafetyEnabled: store?.loopSafetyEnabled ?? null,
19
+ networkRetryEnabled: store?.networkRetryEnabled ?? null,
20
+ lastAccountSwitchAt: store?.lastAccountSwitchAt ?? null,
21
+ };
22
+ }
23
+ function buildCallStack() {
24
+ const stack = new Error().stack?.split("\n") ?? [];
25
+ return stack
26
+ .slice(2)
27
+ .map((line) => line.trim())
28
+ .filter(Boolean)
29
+ .slice(0, 12);
30
+ }
31
+ async function logStoreWrite(input) {
32
+ if (!isStoreDebugEnabled())
33
+ return;
34
+ const filePath = process.env.OPENCODE_COPILOT_STORE_DEBUG_FILE || defaultStoreDebugLogFile;
35
+ const event = {
36
+ kind: "store-write",
37
+ at: new Date().toISOString(),
38
+ targetFile: input.filePath,
39
+ cwd: process.cwd(),
40
+ argv: process.argv.slice(0, 8),
41
+ stack: buildCallStack(),
42
+ ...input.debug,
43
+ before: buildStoreSnapshot(input.before),
44
+ after: buildStoreSnapshot(input.after),
45
+ };
46
+ try {
47
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
48
+ await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, "utf8");
49
+ }
50
+ catch (error) {
51
+ console.warn("[copilot-store-debug] failed to write debug log", error);
52
+ }
53
+ }
7
54
  export function storePath() {
8
55
  const base = xdgConfig ?? path.join(os.homedir(), ".config");
9
56
  return path.join(base, "opencode", filename);
@@ -81,8 +128,15 @@ export async function readAuth(filePath) {
81
128
  return acc;
82
129
  }, {});
83
130
  }
84
- export async function writeStore(store) {
85
- const file = storePath();
131
+ export async function writeStore(store, options) {
132
+ const file = options?.filePath ?? storePath();
133
+ const before = await readStoreSafe(file);
134
+ await logStoreWrite({
135
+ filePath: file,
136
+ before,
137
+ after: store,
138
+ debug: options?.debug,
139
+ });
86
140
  await fs.mkdir(path.dirname(file), { recursive: true });
87
141
  await fs.writeFile(file, JSON.stringify(store, null, 2), { mode: 0o600 });
88
142
  }
@@ -1,3 +1,15 @@
1
+ export declare function buildSelectDebugEvent(input: {
2
+ stage: "key" | "result";
3
+ parsedKey: string | null;
4
+ currentValue?: unknown;
5
+ nextValue?: unknown;
6
+ }): {
7
+ stage: "key" | "result";
8
+ parsedKey: string | null;
9
+ currentActionType: string | null;
10
+ nextActionType: string | null;
11
+ actionType: string;
12
+ } | undefined;
1
13
  export interface MenuItem<T = string> {
2
14
  label: string;
3
15
  value: T;
package/dist/ui/select.js CHANGED
@@ -1,4 +1,46 @@
1
1
  import { ANSI, isTTY, parseKey } from "./ansi.js";
2
+ const defaultSelectDebugLogFile = (() => {
3
+ const tmp = process.env.TEMP || process.env.TMP || "/tmp";
4
+ return `${tmp}/opencode-copilot-store-debug.log`;
5
+ })();
6
+ function shouldLogSuspiciousAction(actionType) {
7
+ return actionType === "toggle-loop-safety" || actionType === "toggle-network-retry";
8
+ }
9
+ function getActionType(value) {
10
+ if (!value || typeof value !== "object")
11
+ return undefined;
12
+ const actionType = value.type;
13
+ return typeof actionType === "string" ? actionType : undefined;
14
+ }
15
+ export function buildSelectDebugEvent(input) {
16
+ const currentActionType = getActionType(input.currentValue);
17
+ const nextActionType = getActionType(input.nextValue);
18
+ const actionType = nextActionType ?? currentActionType;
19
+ if (!shouldLogSuspiciousAction(actionType))
20
+ return undefined;
21
+ return {
22
+ stage: input.stage,
23
+ parsedKey: input.parsedKey,
24
+ currentActionType: currentActionType ?? null,
25
+ nextActionType: nextActionType ?? null,
26
+ actionType,
27
+ };
28
+ }
29
+ async function logSelectDebug(input) {
30
+ const event = buildSelectDebugEvent(input);
31
+ if (!event)
32
+ return;
33
+ const filePath = process.env.OPENCODE_COPILOT_STORE_DEBUG_FILE || defaultSelectDebugLogFile;
34
+ try {
35
+ const { promises: fs } = await import("node:fs");
36
+ const { dirname } = await import("node:path");
37
+ await fs.mkdir(dirname(filePath), { recursive: true });
38
+ await fs.appendFile(filePath, `${JSON.stringify({ kind: "select-action", at: new Date().toISOString(), ...event })}\n`, "utf8");
39
+ }
40
+ catch (error) {
41
+ console.warn("[copilot-store-debug] failed to write select debug log", error);
42
+ }
43
+ }
2
44
  const ESCAPE_TIMEOUT_MS = 50;
3
45
  const ANSI_REGEX = new RegExp("\\x1b\\[[0-9;]*m", "g");
4
46
  const ANSI_LEADING_REGEX = new RegExp("^\\x1b\\[[0-9;]*m");
@@ -195,16 +237,29 @@ export async function select(items, options) {
195
237
  }
196
238
  const action = parseKey(data);
197
239
  if (action === "up") {
240
+ void logSelectDebug({ stage: "key", parsedKey: action, currentValue: items[cursor]?.value });
198
241
  cursor = findNextSelectable(cursor, -1);
199
242
  render();
200
243
  return;
201
244
  }
202
245
  if (action === "down") {
246
+ void logSelectDebug({ stage: "key", parsedKey: action, currentValue: items[cursor]?.value });
203
247
  cursor = findNextSelectable(cursor, 1);
204
248
  render();
205
249
  return;
206
250
  }
207
251
  if (action === "enter") {
252
+ void logSelectDebug({
253
+ stage: "key",
254
+ parsedKey: action,
255
+ currentValue: items[cursor]?.value,
256
+ });
257
+ void logSelectDebug({
258
+ stage: "result",
259
+ parsedKey: action,
260
+ currentValue: items[cursor]?.value,
261
+ nextValue: items[cursor]?.value,
262
+ });
208
263
  finish(items[cursor]?.value ?? null);
209
264
  return;
210
265
  }
package/package.json CHANGED
@@ -1,9 +1,19 @@
1
1
  {
2
2
  "name": "opencode-copilot-account-switcher",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "GitHub Copilot account switcher plugin for OpenCode",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./internal": {
13
+ "types": "./dist/internal.d.ts",
14
+ "default": "./dist/internal.js"
15
+ }
16
+ },
7
17
  "type": "module",
8
18
  "license": "MPL-2.0",
9
19
  "author": "jiwangyihao",