opencode-copilot-account-switcher 0.1.4 → 0.2.1

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.
package/README.md CHANGED
@@ -12,14 +12,15 @@
12
12
 
13
13
  ## English
14
14
 
15
- Manage and switch between multiple **GitHub Copilot** accounts in **OpenCode**. This plugin adds account switching, quota checks, and an optional **Guided Loop Safety** mode that can help Copilot keep a single premium request working longer with fewer report interruptions before it truly needs user input. It **uses the official `github-copilot` provider** and does **not** require model reconfiguration.
15
+ Manage and switch between multiple **GitHub Copilot** accounts in **OpenCode**. This plugin adds account switching, quota checks, an optional **Guided Loop Safety** mode that can keep a single premium request productive for hours with fewer report interruptions before it truly needs user input, and an optional **Copilot Network Retry** switch for retryable network and certificate failures. It **uses the official `github-copilot` provider** and does **not** require model reconfiguration.
16
16
 
17
17
  ## What You Get
18
18
 
19
19
  - **Multi-account support** — add multiple Copilot accounts and switch anytime
20
20
  - **Quota check** — view remaining quota per account
21
21
  - **Auth import** — import Copilot tokens from OpenCode auth storage
22
- - **Guided Loop Safety** — a stricter Copilot-only question-first policy designed to keep non-blocked work moving, require `question` for user-facing reports when available, and help cut avoidable quota burn caused by repeated status interruptions
22
+ - **Guided Loop Safety** — a stricter Copilot-only question-first policy designed to keep non-blocked work moving, keep one premium request productive for hours, and cut avoidable quota burn by replacing repeated interruption turns with `question`-based waiting
23
+ - **Copilot Network Retry** — optional and off by default; normalizes retryable Copilot network or TLS failures so OpenCode's native retry path can handle them
23
24
  - **Zero model config** — no model changes required (official provider only)
24
25
 
25
26
  ---
@@ -101,12 +102,36 @@ You will see an interactive menu (arrow keys + enter) with actions:
101
102
  - **Add account**
102
103
  - **Import from auth.json**
103
104
  - **Check quotas**
104
- - **Guided Loop Safety** — prompt-guided question-first reporting that requires `question` for user-facing reports when available, keeps non-blocked work moving, and avoids unnecessary subagent calls
105
+ - **Guided Loop Safety** — prompt-guided question-first reporting that requires `question` for user-facing reports when available, keeps non-blocked work moving, reduces repeated interruptions, and avoids unnecessary subagent calls
106
+ - **Copilot Network Retry** — off by default; only affects the Copilot request `fetch` path and only for retryable network/certificate-style failures
105
107
  - **Switch account**
106
108
  - **Remove account**
107
109
  - **Remove all**
108
110
 
109
- If you want GitHub Copilot sessions to stay in a single premium request longer, enable Guided Loop Safety from the account menu. It is a prompt-guided, Copilot-only question-first mode: when `question` is available and permitted, user-facing reports must go through it; if safe non-blocked work remains, Copilot should keep going instead of pausing early; only when no safe action remains should it use `question` to ask for the next task or clarification, while also reducing unnecessary subagent calls.
111
+ If you want GitHub Copilot sessions to stay in a single premium request longer, enable Guided Loop Safety from the account menu. In practice, this can keep one request productive for hours: when `question` is available and permitted, user-facing reports must go through it, so waiting for your reply does not keep burning extra quota the way repeated direct-status interruptions do. Fewer interruptions also means less avoidable quota burn. If safe non-blocked work remains, Copilot should keep going instead of pausing early; only when no safe action remains should it use `question` to ask for the next task or clarification, while also reducing unnecessary subagent calls.
112
+
113
+ If you hit transient Copilot TLS or network failures, you can enable Copilot Network Retry from the same menu. It is off by default. When enabled, the plugin keeps the official Copilot header/baseURL behavior from the upstream loader, only wraps the final Copilot `fetch` path, and converts retryable network-like failures into a shape that OpenCode already treats as retryable. This keeps request retry policy aligned with OpenCode instead of re-implementing a second retry system inside the plugin.
114
+
115
+ ## Copilot Network Retry
116
+
117
+ - Default: **disabled**
118
+ - Scope: only the official Copilot request `fetch` path returned by `auth.loader`
119
+ - Purpose: limited handling for retryable network and certificate-style failures such as `failed to fetch`, `ECONNRESET`, `unknown certificate`, or `self signed certificate`
120
+ - Strategy: preserve official loader behavior, then normalize retryable failures so OpenCode's native retry pipeline can decide whether and when to retry
121
+ - Risk: because the plugin still wraps the official fetch path, upstream internal behavior may change over time and drift is possible
122
+
123
+ ## Upstream Sync
124
+
125
+ The repository includes a committed upstream snapshot at `src/upstream/copilot-plugin.snapshot.ts` plus a sync/check script at `scripts/sync-copilot-upstream.mjs`.
126
+
127
+ Useful commands:
128
+
129
+ ```bash
130
+ npm run sync:copilot-snapshot -- --source <file-or-url> --upstream-commit <sha> --sync-date <YYYY-MM-DD>
131
+ npm run check:copilot-sync -- --source <file-or-url> --upstream-commit <sha> --sync-date <YYYY-MM-DD>
132
+ ```
133
+
134
+ The script generates or checks the committed snapshot, requires upstream metadata for repository snapshot updates, and helps catch drift from the official `opencode` `copilot.ts` implementation.
110
135
 
111
136
  ---
112
137
 
@@ -128,20 +153,24 @@ No. This plugin only manages accounts and works with the official `github-copilo
128
153
  **Does it replace the official provider?**
129
154
  No. It uses the official provider and only adds account switching + quota checks.
130
155
 
156
+ **Does Copilot Network Retry replace OpenCode's retry logic?**
157
+ No. The plugin keeps retry policy inside OpenCode by normalizing retryable Copilot network/TLS failures into a shape that OpenCode already recognizes as retryable.
158
+
131
159
  ---
132
160
 
133
161
  <a name="中文"></a>
134
162
 
135
163
  ## 中文
136
164
 
137
- 在 **OpenCode** 中管理并切换多个 **GitHub Copilot** 账号。本插件提供**账号切换、配额查询**以及可选的 **Guided Loop Safety** 模式,帮助 Copilot 在一次 premium request 里更持续地工作,并尽量减少真正需要你输入之前的汇报打断。**完全依赖官方 `github-copilot` provider**,无需修改模型配置。
165
+ 在 **OpenCode** 中管理并切换多个 **GitHub Copilot** 账号。本插件提供**账号切换、配额查询**、可选的 **Guided Loop Safety** 模式,以及默认关闭的 **Copilot Network Retry** 开关;前者帮助一次 premium request 更容易连续工作好几个小时、减少真正需要你输入之前的汇报打断,后者用于处理可重试的网络与证书类失败。**完全依赖官方 `github-copilot` provider**,无需修改模型配置。
138
166
 
139
167
  ## 功能一览
140
168
 
141
169
  - **多账号管理** — 添加多个 Copilot 账号,随时切换
142
170
  - **配额查询** — 查看每个账号的剩余额度
143
171
  - **导入认证** — 可从 OpenCode 认证存储导入
144
- - **Guided Loop Safety** — 仅对 Copilot 生效的更严格 question-first 提示词策略,推动非阻塞工作持续执行、在可用时要求用户可见汇报走 `question`,并帮助降低因反复中断带来的无谓配额消耗
172
+ - **Guided Loop Safety** — 仅对 Copilot 生效的更严格 question-first 提示词策略,推动非阻塞工作持续执行、让一次 premium request 更容易连续工作好几个小时,并通过减少反复中断来降低无谓配额消耗
173
+ - **Copilot Network Retry** — 默认关闭;把可重试的 Copilot 网络或 TLS 失败归一化成 OpenCode 原生重试链路可识别的形态
145
174
  - **无需模型配置** — 使用官方 provider,无需改模型
146
175
 
147
176
  ---
@@ -223,12 +252,36 @@ opencode auth login --provider github-copilot
223
252
  - **添加账号**
224
253
  - **从 auth.json 导入**
225
254
  - **检查配额**
226
- - **Guided Loop Safety 开关** — 通过提示词引导模型在可用时必须使用 `question` 做用户可见汇报、继续完成非阻塞工作,并避免不必要的子代理调用
255
+ - **Guided Loop Safety 开关** — 通过提示词引导模型在可用时必须使用 `question` 做用户可见汇报、继续完成非阻塞工作、减少反复中断,并避免不必要的子代理调用
256
+ - **Copilot Network Retry 开关** — 默认关闭;仅影响 Copilot 请求的 `fetch` 路径,只处理可重试的网络/证书类失败
227
257
  - **切换账号**
228
258
  - **删除账号**
229
259
  - **全部删除**
230
260
 
231
- 如果你希望 GitHub Copilot 会话在一次 premium request 中尽量持续工作、更少被汇报打断,可以在账号菜单中开启 Guided Loop Safety。它是仅对 Copilot 生效的 prompt 引导式 question-first 模式:当 `question` 工具在当前会话中可用且被允许时,用户可见汇报必须通过它完成;只要还有安全的非阻塞工作可做,Copilot 就应继续执行而不是提前暂停;只有在当前确实没有可安全执行的动作时,才应通过 `question` 询问下一项任务或所需澄清,同时也会减少不必要的子代理调用。
261
+ 如果你希望 GitHub Copilot 会话在一次 premium request 中尽量持续工作、更少被汇报打断,可以在账号菜单中开启 Guided Loop Safety。实际使用中,它可以让一次 request 更容易连续工作好几个小时:当 `question` 工具在当前会话中可用且被允许时,用户可见汇报必须通过它完成,因此等待你的回复本身不会像反复插入直接状态消息那样继续额外消耗配额;少一次中断,本身就少一次无谓的配额消耗。只要还有安全的非阻塞工作可做,Copilot 就应继续执行而不是提前暂停;只有在当前确实没有可安全执行的动作时,才应通过 `question` 询问下一项任务或所需澄清,同时也会减少不必要的子代理调用。
262
+
263
+ 如果你遇到 Copilot 的瞬时 TLS 或网络失败,也可以在同一菜单中开启 Copilot Network Retry。它默认关闭。开启后,插件会先保留 upstream 官方 loader 生成的 `baseURL`、认证头和 `fetch` 行为,只在最后一跳 Copilot `fetch` 路径上做最小包装,把可重试的网络类失败归一化成 OpenCode 已有重试链路能识别的形态,而不是在插件内部重新定义一套独立的请求重试策略。
264
+
265
+ ## Copilot Network Retry
266
+
267
+ - 默认:**关闭**
268
+ - 作用范围:仅影响 `auth.loader` 返回的官方 Copilot 请求 `fetch` 路径
269
+ - 用途:有限处理 `failed to fetch`、`ECONNRESET`、`unknown certificate`、`self signed certificate` 等可重试网络/证书类失败
270
+ - 实现策略:尽量保留官方 loader 行为,再把可重试失败归一化给 OpenCode 原生重试链路判断是否重试
271
+ - 风险提示:因为插件仍然包裹了官方 fetch 路径,若 upstream 后续内部实现变化,仍可能产生行为漂移
272
+
273
+ ## Upstream 同步机制
274
+
275
+ 仓库中提交了一份 upstream 快照 `src/upstream/copilot-plugin.snapshot.ts`,并提供同步/校验脚本 `scripts/sync-copilot-upstream.mjs`。
276
+
277
+ 常用命令:
278
+
279
+ ```bash
280
+ npm run sync:copilot-snapshot -- --source <file-or-url> --upstream-commit <sha> --sync-date <YYYY-MM-DD>
281
+ npm run check:copilot-sync -- --source <file-or-url> --upstream-commit <sha> --sync-date <YYYY-MM-DD>
282
+ ```
283
+
284
+ 该脚本会生成或校验仓库中提交的 snapshot,并要求在更新正式 snapshot 时显式提供 upstream commit 与同步日期,用来尽早发现与官方 `opencode` `copilot.ts` 的行为漂移。
232
285
 
233
286
  ---
234
287
 
@@ -250,6 +303,9 @@ opencode auth login --provider github-copilot
250
303
  **会替换官方 provider 吗?**
251
304
  不会。它只是在官方 provider 基础上增加账号切换和配额查询。
252
305
 
306
+ **Copilot Network Retry 会替代 OpenCode 自己的重试逻辑吗?**
307
+ 不会。插件的目标是把可重试的 Copilot 网络/TLS 失败归一化成 OpenCode 已识别的可重试错误形态,真正的是否重试与如何退避仍由 OpenCode 原生链路决定。
308
+
253
309
  ---
254
310
 
255
311
  ## License
@@ -0,0 +1,6 @@
1
+ type FetchLike = (request: Request | URL | string, init?: RequestInit) => Promise<Response>;
2
+ export declare function isRetryableCopilotFetchError(error: unknown): boolean;
3
+ export declare function createCopilotRetryingFetch(baseFetch: FetchLike, options?: {
4
+ wait?: (ms: number) => Promise<void>;
5
+ }): (request: Request | URL | string, init?: RequestInit) => Promise<Response>;
6
+ export {};
@@ -0,0 +1,61 @@
1
+ const RETRYABLE_MESSAGES = [
2
+ "load failed",
3
+ "failed to fetch",
4
+ "network request failed",
5
+ "econnreset",
6
+ "etimedout",
7
+ "socket hang up",
8
+ "unknown certificate",
9
+ "self signed certificate",
10
+ "unable to verify the first certificate",
11
+ "self-signed certificate in certificate chain",
12
+ ];
13
+ const RETRYABLE_PATH_SEGMENTS = ["/chat/completions", "/responses", "/models", "/token"];
14
+ function isAbortError(error) {
15
+ return error instanceof Error && error.name === "AbortError";
16
+ }
17
+ function getErrorMessage(error) {
18
+ return String(error instanceof Error ? error.message : error).toLowerCase();
19
+ }
20
+ function toRetryableSystemError(error) {
21
+ const base = error instanceof Error ? error : new Error(String(error));
22
+ const wrapped = new Error(`[copilot-network-retry normalized] ${base.message}`);
23
+ wrapped.name = base.name;
24
+ wrapped.code = "ECONNRESET";
25
+ wrapped.syscall = "fetch";
26
+ wrapped.cause = error;
27
+ return wrapped;
28
+ }
29
+ function isCopilotUrl(request) {
30
+ const raw = request instanceof Request ? request.url : request instanceof URL ? request.href : String(request);
31
+ try {
32
+ const url = new URL(raw);
33
+ const isCopilotHost = url.hostname === "api.githubcopilot.com" || url.hostname.startsWith("copilot-api.");
34
+ if (!isCopilotHost)
35
+ return false;
36
+ return RETRYABLE_PATH_SEGMENTS.some((segment) => url.pathname.includes(segment));
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ export function isRetryableCopilotFetchError(error) {
43
+ if (!error || isAbortError(error))
44
+ return false;
45
+ const message = getErrorMessage(error);
46
+ return RETRYABLE_MESSAGES.some((part) => message.includes(part));
47
+ }
48
+ export function createCopilotRetryingFetch(baseFetch, options) {
49
+ void options;
50
+ return async function retryingFetch(request, init) {
51
+ try {
52
+ return await baseFetch(request, init);
53
+ }
54
+ catch (error) {
55
+ if (!isCopilotUrl(request) || !isRetryableCopilotFetchError(error)) {
56
+ throw error;
57
+ }
58
+ throw toRetryableSystemError(error);
59
+ }
60
+ };
61
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { CopilotAccountSwitcher } from "./plugin.js";
2
2
  export { applyMenuAction } from "./plugin-actions.js";
3
3
  export { buildPluginHooks } from "./plugin-hooks.js";
4
+ export { createOfficialFetchAdapter, loadOfficialCopilotConfig } from "./upstream/copilot-loader-adapter.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { CopilotAccountSwitcher } from "./plugin.js";
2
2
  export { applyMenuAction } from "./plugin-actions.js";
3
3
  export { buildPluginHooks } from "./plugin-hooks.js";
4
+ export { createOfficialFetchAdapter, loadOfficialCopilotConfig } from "./upstream/copilot-loader-adapter.js";
@@ -1,6 +1,6 @@
1
1
  import type { Hooks } from "@opencode-ai/plugin";
2
2
  import { type StoreFile } from "./store.js";
3
- export declare const LOOP_SAFETY_POLICY = "Guided Loop Safety Policy\n- Continue working on any remaining non-blocked task before stopping to report or wait for more instructions.\n- If you are not fully blocked, do not stop just because you feel ready to pause; finish the work that can still be done safely.\n- When the question tool is available and permitted in the current session, all user-facing reports must be delivered through the question tool.\n- The question tool is considered available and permitted when it appears in the active tool list and the current session has not denied its use.\n- Direct assistant text is allowed only when the question tool is unavailable, denied, or absent from the current session.\n- When reporting multiple related items, prefer a single question tool call with multiple well-grouped questions instead of multiple separate interruptions.\n- Group related items into clear question batches such as current progress, key findings, and next-step choices.\n- For long or complex reports, split the report into paginated or sequential question batches instead of overloading one large message.\n- Present the highest-priority information first and defer secondary details to later question batches when needed.\n- Even when no explicit decision is required, prefer brief question-tool status updates over direct assistant text whenever the tool is available.\n- Avoid unnecessary question frequency; combine small related updates when a single question call can cover them clearly.\n- When no further action can be taken safely and no non-blocked work remains, use the question tool to ask for the next task or clarification instead of ending with direct assistant text.\n- Dispatching task or subagent work is expensive and should be avoided unless it materially improves the result.\n- Materially improves the result means clearly beneficial cases such as parallel analysis of independent areas; it does not include routine local searches, small file reads, or straightforward edits.\n- If task or subagent delegation is used, keep the number minimal and explain the reason briefly through the question tool when available.";
3
+ export declare const LOOP_SAFETY_POLICY = "Guided Loop Safety Policy\n- Continue working on any remaining non-blocked task before stopping to report or wait for more instructions.\n- If you are not fully blocked, do not stop just because you feel ready to pause; finish the work that can still be done safely.\n- MANDATORY: When the question tool is available and permitted in the current session, ALL user-facing reports MUST be delivered through the question tool.\n- MANDATORY: Treat progress updates, status reports, summaries, explanations, completion notes, and next-step choices as user-facing reports.\n- MANDATORY: In this workflow, the question tool is both the question channel and the report channel for user-visible communication.\n- MANDATORY: Do not reinterpret \"keep working unless blocked\" as permission to send direct assistant text. If you need to report, report through the question tool.\n- MANDATORY: Do not reinterpret \"don't ask unnecessary questions\" as permission to skip the question tool. Use fewer question batches, not a different reporting channel.\n- NO EXCEPTIONS: Do not switch to direct assistant text just because the report is long, detailed, complex, or contains multiple related items.\n- The question tool is considered available and permitted when it appears in the active tool list and the current session has not denied its use.\n- Direct assistant text is allowed only when the question tool is unavailable, denied, or absent from the current session.\n- If a report would otherwise be sent as direct text, convert it into one question batch or a short sequence of question batches instead.\n- IMPORTANT: A report-mode violation usually means the wrong tool was used, not that the report was too long.\n- IMPORTANT: Do not reinterpret a tool-usage violation as a verbosity problem.\n- Long reports are allowed. Use pagination or sequential question batches when needed; do not fall back to direct assistant text.\n- A single question tool call may contain multiple well-grouped questions. Prefer that over multiple small interruptions when it keeps the report clear.\n- Group related items into explicit batches such as current progress, key findings, decisions, and next-step choices.\n- Present the highest-priority information first and defer secondary details to later question batches when needed.\n- Even when no explicit decision is required, use brief question-tool status updates instead of direct assistant text whenever the tool is available.\n- Avoid unnecessary question frequency; combine small related updates when a single question call can cover them clearly.\n- When no further action can be taken safely and no non-blocked work remains, use the question tool to ask for the next task or clarification instead of ending with direct assistant text.\n- Dispatching task or subagent work is expensive and should be avoided unless it materially improves the result.\n- Materially improves the result means clearly beneficial cases such as parallel analysis of independent areas; it does not include routine local searches, small file reads, or straightforward edits.\n- If task or subagent delegation is used, keep the number minimal and explain the reason briefly through the question tool when available.";
4
4
  export type ExperimentalChatSystemTransformHook = (input: {
5
5
  sessionID: string;
6
6
  model: {
@@ -2,14 +2,22 @@ import { readStoreSafe } from "./store.js";
2
2
  export const LOOP_SAFETY_POLICY = `Guided Loop Safety Policy
3
3
  - Continue working on any remaining non-blocked task before stopping to report or wait for more instructions.
4
4
  - If you are not fully blocked, do not stop just because you feel ready to pause; finish the work that can still be done safely.
5
- - When the question tool is available and permitted in the current session, all user-facing reports must be delivered through the question tool.
5
+ - MANDATORY: When the question tool is available and permitted in the current session, ALL user-facing reports MUST be delivered through the question tool.
6
+ - MANDATORY: Treat progress updates, status reports, summaries, explanations, completion notes, and next-step choices as user-facing reports.
7
+ - MANDATORY: In this workflow, the question tool is both the question channel and the report channel for user-visible communication.
8
+ - MANDATORY: Do not reinterpret "keep working unless blocked" as permission to send direct assistant text. If you need to report, report through the question tool.
9
+ - MANDATORY: Do not reinterpret "don't ask unnecessary questions" as permission to skip the question tool. Use fewer question batches, not a different reporting channel.
10
+ - NO EXCEPTIONS: Do not switch to direct assistant text just because the report is long, detailed, complex, or contains multiple related items.
6
11
  - The question tool is considered available and permitted when it appears in the active tool list and the current session has not denied its use.
7
12
  - Direct assistant text is allowed only when the question tool is unavailable, denied, or absent from the current session.
8
- - When reporting multiple related items, prefer a single question tool call with multiple well-grouped questions instead of multiple separate interruptions.
9
- - Group related items into clear question batches such as current progress, key findings, and next-step choices.
10
- - For long or complex reports, split the report into paginated or sequential question batches instead of overloading one large message.
13
+ - If a report would otherwise be sent as direct text, convert it into one question batch or a short sequence of question batches instead.
14
+ - IMPORTANT: A report-mode violation usually means the wrong tool was used, not that the report was too long.
15
+ - IMPORTANT: Do not reinterpret a tool-usage violation as a verbosity problem.
16
+ - Long reports are allowed. Use pagination or sequential question batches when needed; do not fall back to direct assistant text.
17
+ - A single question tool call may contain multiple well-grouped questions. Prefer that over multiple small interruptions when it keeps the report clear.
18
+ - Group related items into explicit batches such as current progress, key findings, decisions, and next-step choices.
11
19
  - Present the highest-priority information first and defer secondary details to later question batches when needed.
12
- - Even when no explicit decision is required, prefer brief question-tool status updates over direct assistant text whenever the tool is available.
20
+ - Even when no explicit decision is required, use brief question-tool status updates instead of direct assistant text whenever the tool is available.
13
21
  - Avoid unnecessary question frequency; combine small related updates when a single question call can cover them clearly.
14
22
  - When no further action can be taken safely and no non-blocked work remains, use the question tool to ask for the next task or clarification instead of ending with direct assistant text.
15
23
  - Dispatching task or subagent work is expensive and should be avoided unless it materially improves the result.
@@ -1,7 +1,13 @@
1
1
  export async function applyMenuAction(input) {
2
- if (input.action.type !== "toggle-loop-safety")
3
- return false;
4
- input.store.loopSafetyEnabled = input.store.loopSafetyEnabled !== true;
5
- await input.writeStore(input.store);
6
- return true;
2
+ if (input.action.type === "toggle-loop-safety") {
3
+ input.store.loopSafetyEnabled = input.store.loopSafetyEnabled !== true;
4
+ await input.writeStore(input.store);
5
+ return true;
6
+ }
7
+ if (input.action.type === "toggle-network-retry") {
8
+ input.store.networkRetryEnabled = input.store.networkRetryEnabled !== true;
9
+ await input.writeStore(input.store);
10
+ return true;
11
+ }
12
+ return false;
7
13
  }
@@ -1,6 +1,13 @@
1
1
  import { type CopilotPluginHooks } from "./loop-safety-plugin.js";
2
+ import { createCopilotRetryingFetch } from "./copilot-network-retry.js";
2
3
  import { type StoreFile } from "./store.js";
4
+ import { type CopilotAuthState, type CopilotProviderConfig, type OfficialCopilotConfig } from "./upstream/copilot-loader-adapter.js";
3
5
  export declare function buildPluginHooks(input: {
4
- auth: CopilotPluginHooks["auth"];
6
+ auth: NonNullable<CopilotPluginHooks["auth"]>;
5
7
  loadStore?: () => Promise<StoreFile | undefined>;
8
+ loadOfficialConfig?: (input: {
9
+ getAuth: () => Promise<CopilotAuthState | undefined>;
10
+ provider?: CopilotProviderConfig;
11
+ }) => Promise<OfficialCopilotConfig | undefined>;
12
+ createRetryFetch?: typeof createCopilotRetryingFetch;
6
13
  }): CopilotPluginHooks;
@@ -1,8 +1,33 @@
1
1
  import { createLoopSafetySystemTransform } from "./loop-safety-plugin.js";
2
+ import { createCopilotRetryingFetch } from "./copilot-network-retry.js";
2
3
  import { readStoreSafe } from "./store.js";
4
+ import { loadOfficialCopilotConfig, } from "./upstream/copilot-loader-adapter.js";
3
5
  export function buildPluginHooks(input) {
6
+ const loadStore = input.loadStore ?? readStoreSafe;
7
+ const loadOfficialConfig = input.loadOfficialConfig ?? loadOfficialCopilotConfig;
8
+ const createRetryFetch = input.createRetryFetch ?? createCopilotRetryingFetch;
9
+ const loader = async (getAuth, provider) => {
10
+ const config = await loadOfficialConfig({
11
+ getAuth: getAuth,
12
+ provider: provider,
13
+ });
14
+ if (!config)
15
+ return {};
16
+ const store = await loadStore().catch(() => undefined);
17
+ if (store?.networkRetryEnabled !== true) {
18
+ return config;
19
+ }
20
+ return {
21
+ ...config,
22
+ fetch: createRetryFetch(config.fetch),
23
+ };
24
+ };
4
25
  return {
5
- auth: input.auth,
6
- "experimental.chat.system.transform": createLoopSafetySystemTransform(input.loadStore ?? readStoreSafe),
26
+ auth: {
27
+ ...input.auth,
28
+ provider: input.auth.provider ?? "github-copilot",
29
+ loader,
30
+ },
31
+ "experimental.chat.system.transform": createLoopSafetySystemTransform(loadStore),
7
32
  };
8
33
  }
package/dist/plugin.js CHANGED
@@ -561,7 +561,7 @@ export const CopilotAccountSwitcher = async ({ client }) => {
561
561
  : undefined,
562
562
  }));
563
563
  const refresh = { enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 };
564
- const action = await showMenu(accounts, refresh, store.lastQuotaRefresh, store.loopSafetyEnabled === true);
564
+ const action = await showMenu(accounts, refresh, store.lastQuotaRefresh, store.loopSafetyEnabled === true, store.networkRetryEnabled === true);
565
565
  if (action.type === "cancel") {
566
566
  const active = store.active ? store.accounts[store.active] : undefined;
567
567
  return active;
package/dist/store.d.ts CHANGED
@@ -55,6 +55,7 @@ export type StoreFile = {
55
55
  refreshMinutes?: number;
56
56
  lastQuotaRefresh?: number;
57
57
  loopSafetyEnabled?: boolean;
58
+ networkRetryEnabled?: boolean;
58
59
  };
59
60
  export declare function storePath(): string;
60
61
  export declare function authPath(): string;
package/dist/store.js CHANGED
@@ -18,6 +18,8 @@ export function parseStore(raw) {
18
18
  data.accounts = {};
19
19
  if (data.loopSafetyEnabled !== true)
20
20
  data.loopSafetyEnabled = false;
21
+ if (data.networkRetryEnabled !== true)
22
+ data.networkRetryEnabled = false;
21
23
  for (const [name, entry] of Object.entries(data.accounts)) {
22
24
  const info = entry;
23
25
  if (!info.name)
package/dist/ui/menu.d.ts CHANGED
@@ -55,6 +55,8 @@ export type MenuAction = {
55
55
  type: "set-interval";
56
56
  } | {
57
57
  type: "toggle-loop-safety";
58
+ } | {
59
+ type: "toggle-network-retry";
58
60
  } | {
59
61
  type: "switch";
60
62
  account: AccountInfo;
@@ -74,9 +76,10 @@ export declare function buildMenuItems(input: {
74
76
  };
75
77
  lastQuotaRefresh?: number;
76
78
  loopSafetyEnabled: boolean;
79
+ networkRetryEnabled: boolean;
77
80
  }): MenuItem<MenuAction>[];
78
81
  export declare function showMenu(accounts: AccountInfo[], refresh?: {
79
82
  enabled: boolean;
80
83
  minutes: number;
81
- }, lastQuotaRefresh?: number, loopSafetyEnabled?: boolean): Promise<MenuAction>;
84
+ }, lastQuotaRefresh?: number, loopSafetyEnabled?: boolean, networkRetryEnabled?: boolean): Promise<MenuAction>;
82
85
  export declare function showAccountActions(account: AccountInfo): Promise<"switch" | "remove" | "back">;
package/dist/ui/menu.js CHANGED
@@ -47,6 +47,12 @@ export function buildMenuItems(input) {
47
47
  color: "cyan",
48
48
  hint: "Prompt-guided: fewer report interruptions, fewer unnecessary subagents",
49
49
  },
50
+ {
51
+ label: input.networkRetryEnabled ? "Disable Copilot network retry" : "Enable Copilot network retry",
52
+ value: { type: "toggle-network-retry" },
53
+ color: "cyan",
54
+ hint: "Overrides official fetch path; may drift from upstream",
55
+ },
50
56
  { label: "", value: { type: "cancel" }, separator: true },
51
57
  { label: "Accounts", value: { type: "cancel" }, kind: "heading" },
52
58
  ...input.accounts.map((account) => {
@@ -76,12 +82,13 @@ export function buildMenuItems(input) {
76
82
  { label: "Remove all accounts", value: { type: "remove-all" }, color: "red" },
77
83
  ];
78
84
  }
79
- export async function showMenu(accounts, refresh, lastQuotaRefresh, loopSafetyEnabled = false) {
85
+ export async function showMenu(accounts, refresh, lastQuotaRefresh, loopSafetyEnabled = false, networkRetryEnabled = false) {
80
86
  const items = buildMenuItems({
81
87
  accounts,
82
88
  refresh,
83
89
  lastQuotaRefresh,
84
90
  loopSafetyEnabled,
91
+ networkRetryEnabled,
85
92
  });
86
93
  while (true) {
87
94
  const result = await select(items, {
@@ -0,0 +1,42 @@
1
+ export type CopilotAuthState = {
2
+ type: string;
3
+ refresh?: string;
4
+ access?: string;
5
+ expires?: number;
6
+ enterpriseUrl?: string;
7
+ };
8
+ export type CopilotProviderModel = {
9
+ id?: string;
10
+ api: {
11
+ url?: string;
12
+ npm?: string;
13
+ };
14
+ cost?: {
15
+ input: number;
16
+ output: number;
17
+ cache: {
18
+ read: number;
19
+ write: number;
20
+ };
21
+ };
22
+ };
23
+ export type CopilotProviderConfig = {
24
+ models?: Record<string, CopilotProviderModel>;
25
+ };
26
+ export type OfficialCopilotConfig = {
27
+ baseURL?: string;
28
+ apiKey: string;
29
+ fetch: (request: Request | URL | string, init?: RequestInit) => Promise<Response>;
30
+ };
31
+ export declare function loadOfficialCopilotConfig(input: {
32
+ getAuth: () => Promise<CopilotAuthState | undefined>;
33
+ baseFetch?: typeof fetch;
34
+ provider?: CopilotProviderConfig;
35
+ version?: string;
36
+ }): Promise<OfficialCopilotConfig | undefined>;
37
+ export declare function createOfficialFetchAdapter(input: {
38
+ getAuth: () => Promise<CopilotAuthState | undefined>;
39
+ baseFetch?: typeof fetch;
40
+ provider?: CopilotProviderConfig;
41
+ version?: string;
42
+ }): (request: Request | URL | string, init?: RequestInit) => Promise<Response>;
@@ -0,0 +1,26 @@
1
+ import { createOfficialCopilotLoader } from "./copilot-plugin.snapshot.js";
2
+ export async function loadOfficialCopilotConfig(input) {
3
+ const loader = createOfficialCopilotLoader({
4
+ fetchImpl: input.baseFetch,
5
+ version: input.version,
6
+ });
7
+ const result = await loader(input.getAuth, input.provider);
8
+ if (!("fetch" in result) || typeof result.fetch !== "function") {
9
+ return undefined;
10
+ }
11
+ return {
12
+ baseURL: result.baseURL,
13
+ apiKey: result.apiKey,
14
+ fetch: result.fetch,
15
+ };
16
+ }
17
+ export function createOfficialFetchAdapter(input) {
18
+ return async function fetchWithOfficialHeaders(request, init) {
19
+ const config = await loadOfficialCopilotConfig(input);
20
+ const fallback = input.baseFetch ?? fetch;
21
+ if (!config) {
22
+ return fallback(request, init);
23
+ }
24
+ return config.fetch(request, init);
25
+ };
26
+ }
@@ -0,0 +1,25 @@
1
+ type RequestInfo = Request | URL | string;
2
+ type Hooks = any;
3
+ type PluginInput = any;
4
+ type OfficialProviderInput = {
5
+ models?: Record<string, {
6
+ id?: string;
7
+ api: {
8
+ url?: string;
9
+ npm?: string;
10
+ };
11
+ cost?: unknown;
12
+ }>;
13
+ };
14
+ type OfficialLoaderResult = {
15
+ baseURL?: string;
16
+ apiKey: string;
17
+ fetch: (request: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
18
+ };
19
+ type OfficialLoader = (getAuth: () => Promise<any>, provider?: OfficialProviderInput) => Promise<OfficialLoaderResult | {}>;
20
+ export declare function CopilotAuthPlugin(input: PluginInput): Promise<Hooks>;
21
+ export declare function createOfficialCopilotLoader(options?: {
22
+ fetchImpl?: typeof fetch;
23
+ version?: string;
24
+ }): OfficialLoader;
25
+ export {};
@@ -0,0 +1,393 @@
1
+ // @ts-nocheck
2
+ /*
3
+ * Upstream snapshot source:
4
+ * - Repository: https://github.com/sst/opencode
5
+ * - Original path: packages/opencode/src/plugin/copilot.ts
6
+ * - Sync date: 2026-03-13
7
+ * - Upstream commit: d1482e148399bfaf808674549199f5f4aa69a22d
8
+ *
9
+ * Generated by scripts/sync-copilot-upstream.mjs.
10
+ * Do not edit this file directly; update the sync script and regenerate it.
11
+ */
12
+ const Installation = {
13
+ VERSION: "snapshot",
14
+ };
15
+ function iife(fn) {
16
+ return fn();
17
+ }
18
+ const Bun = {
19
+ sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ },
22
+ };
23
+ /* LOCAL_SHIMS_END */
24
+ const CLIENT_ID = "Ov23li8tweQw6odWQebz";
25
+ // Add a small safety buffer when polling to avoid hitting the server
26
+ // slightly too early due to clock skew / timer drift.
27
+ const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000; // 3 seconds
28
+ function normalizeDomain(url) {
29
+ return url.replace(/^https?:\/\//, "").replace(/\/$/, "");
30
+ }
31
+ function getUrls(domain) {
32
+ return {
33
+ DEVICE_CODE_URL: `https://${domain}/login/device/code`,
34
+ ACCESS_TOKEN_URL: `https://${domain}/login/oauth/access_token`,
35
+ };
36
+ }
37
+ export async function CopilotAuthPlugin(input) {
38
+ const sdk = input.client;
39
+ return {
40
+ auth: {
41
+ provider: "github-copilot",
42
+ async loader(getAuth, provider) {
43
+ const info = await getAuth();
44
+ if (!info || info.type !== "oauth")
45
+ return {};
46
+ const enterpriseUrl = info.enterpriseUrl;
47
+ const baseURL = enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : undefined;
48
+ if (provider && provider.models) {
49
+ for (const model of Object.values(provider.models)) {
50
+ model.cost = {
51
+ input: 0,
52
+ output: 0,
53
+ cache: {
54
+ read: 0,
55
+ write: 0,
56
+ },
57
+ };
58
+ // TODO: re-enable once messages api has higher rate limits
59
+ // TODO: move some of this hacky-ness to models.dev presets once we have better grasp of things here...
60
+ // const base = baseURL ?? model.api.url
61
+ // const claude = model.id.includes("claude")
62
+ // const url = iife(() => {
63
+ // if (!claude) return base
64
+ // if (base.endsWith("/v1")) return base
65
+ // if (base.endsWith("/")) return `${base}v1`
66
+ // return `${base}/v1`
67
+ // })
68
+ // model.api.url = url
69
+ // model.api.npm = claude ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"
70
+ model.api.npm = "@ai-sdk/github-copilot";
71
+ }
72
+ }
73
+ return {
74
+ baseURL,
75
+ apiKey: "",
76
+ async fetch(request, init) {
77
+ const info = await getAuth();
78
+ if (info.type !== "oauth")
79
+ return fetch(request, init);
80
+ const url = request instanceof URL ? request.href : request.toString();
81
+ const { isVision, isAgent } = iife(() => {
82
+ try {
83
+ const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body;
84
+ // Completions API
85
+ if (body?.messages && url.includes("completions")) {
86
+ const last = body.messages[body.messages.length - 1];
87
+ return {
88
+ isVision: body.messages.some((msg) => Array.isArray(msg.content) && msg.content.some((part) => part.type === "image_url")),
89
+ isAgent: last?.role !== "user",
90
+ };
91
+ }
92
+ // Responses API
93
+ if (body?.input) {
94
+ const last = body.input[body.input.length - 1];
95
+ return {
96
+ isVision: body.input.some((item) => Array.isArray(item?.content) && item.content.some((part) => part.type === "input_image")),
97
+ isAgent: last?.role !== "user",
98
+ };
99
+ }
100
+ // Messages API
101
+ if (body?.messages) {
102
+ const last = body.messages[body.messages.length - 1];
103
+ const hasNonToolCalls = Array.isArray(last?.content) && last.content.some((part) => part?.type !== "tool_result");
104
+ return {
105
+ isVision: body.messages.some((item) => Array.isArray(item?.content) &&
106
+ item.content.some((part) => part?.type === "image" ||
107
+ // images can be nested inside tool_result content
108
+ (part?.type === "tool_result" &&
109
+ Array.isArray(part?.content) &&
110
+ part.content.some((nested) => nested?.type === "image")))),
111
+ isAgent: !(last?.role === "user" && hasNonToolCalls),
112
+ };
113
+ }
114
+ }
115
+ catch { }
116
+ return { isVision: false, isAgent: false };
117
+ });
118
+ const headers = {
119
+ "x-initiator": isAgent ? "agent" : "user",
120
+ ...init?.headers,
121
+ "User-Agent": `opencode/${Installation.VERSION}`,
122
+ Authorization: `Bearer ${info.refresh}`,
123
+ "Openai-Intent": "conversation-edits",
124
+ };
125
+ if (isVision) {
126
+ headers["Copilot-Vision-Request"] = "true";
127
+ }
128
+ delete headers["x-api-key"];
129
+ delete headers["authorization"];
130
+ return fetch(request, {
131
+ ...init,
132
+ headers,
133
+ });
134
+ },
135
+ };
136
+ },
137
+ methods: [
138
+ {
139
+ type: "oauth",
140
+ label: "Login with GitHub Copilot",
141
+ prompts: [
142
+ {
143
+ type: "select",
144
+ key: "deploymentType",
145
+ message: "Select GitHub deployment type",
146
+ options: [
147
+ {
148
+ label: "GitHub.com",
149
+ value: "github.com",
150
+ hint: "Public",
151
+ },
152
+ {
153
+ label: "GitHub Enterprise",
154
+ value: "enterprise",
155
+ hint: "Data residency or self-hosted",
156
+ },
157
+ ],
158
+ },
159
+ {
160
+ type: "text",
161
+ key: "enterpriseUrl",
162
+ message: "Enter your GitHub Enterprise URL or domain",
163
+ placeholder: "company.ghe.com or https://company.ghe.com",
164
+ condition: (inputs) => inputs.deploymentType === "enterprise",
165
+ validate: (value) => {
166
+ if (!value)
167
+ return "URL or domain is required";
168
+ try {
169
+ const url = value.includes("://") ? new URL(value) : new URL(`https://${value}`);
170
+ if (!url.hostname)
171
+ return "Please enter a valid URL or domain";
172
+ return undefined;
173
+ }
174
+ catch {
175
+ return "Please enter a valid URL (e.g., company.ghe.com or https://company.ghe.com)";
176
+ }
177
+ },
178
+ },
179
+ ],
180
+ async authorize(inputs = {}) {
181
+ const deploymentType = inputs.deploymentType || "github.com";
182
+ let domain = "github.com";
183
+ let actualProvider = "github-copilot";
184
+ if (deploymentType === "enterprise") {
185
+ const enterpriseUrl = inputs.enterpriseUrl;
186
+ domain = normalizeDomain(enterpriseUrl);
187
+ actualProvider = "github-copilot-enterprise";
188
+ }
189
+ const urls = getUrls(domain);
190
+ const deviceResponse = await fetch(urls.DEVICE_CODE_URL, {
191
+ method: "POST",
192
+ headers: {
193
+ Accept: "application/json",
194
+ "Content-Type": "application/json",
195
+ "User-Agent": `opencode/${Installation.VERSION}`,
196
+ },
197
+ body: JSON.stringify({
198
+ client_id: CLIENT_ID,
199
+ scope: "read:user",
200
+ }),
201
+ });
202
+ if (!deviceResponse.ok) {
203
+ throw new Error("Failed to initiate device authorization");
204
+ }
205
+ const deviceData = (await deviceResponse.json());
206
+ return {
207
+ url: deviceData.verification_uri,
208
+ instructions: `Enter code: ${deviceData.user_code}`,
209
+ method: "auto",
210
+ async callback() {
211
+ while (true) {
212
+ const response = await fetch(urls.ACCESS_TOKEN_URL, {
213
+ method: "POST",
214
+ headers: {
215
+ Accept: "application/json",
216
+ "Content-Type": "application/json",
217
+ "User-Agent": `opencode/${Installation.VERSION}`,
218
+ },
219
+ body: JSON.stringify({
220
+ client_id: CLIENT_ID,
221
+ device_code: deviceData.device_code,
222
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
223
+ }),
224
+ });
225
+ if (!response.ok)
226
+ return { type: "failed" };
227
+ const data = (await response.json());
228
+ if (data.access_token) {
229
+ const result = {
230
+ type: "success",
231
+ refresh: data.access_token,
232
+ access: data.access_token,
233
+ expires: 0,
234
+ };
235
+ if (actualProvider === "github-copilot-enterprise") {
236
+ result.provider = "github-copilot-enterprise";
237
+ result.enterpriseUrl = domain;
238
+ }
239
+ return result;
240
+ }
241
+ if (data.error === "authorization_pending") {
242
+ await Bun.sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS);
243
+ continue;
244
+ }
245
+ if (data.error === "slow_down") {
246
+ // Based on the RFC spec, we must add 5 seconds to our current polling interval.
247
+ // (See https://www.rfc-editor.org/rfc/rfc8628#section-3.5)
248
+ let newInterval = (deviceData.interval + 5) * 1000;
249
+ // GitHub OAuth API may return the new interval in seconds in the response.
250
+ // We should try to use that if provided with safety margin.
251
+ const serverInterval = data.interval;
252
+ if (serverInterval && typeof serverInterval === "number" && serverInterval > 0) {
253
+ newInterval = serverInterval * 1000;
254
+ }
255
+ await Bun.sleep(newInterval + OAUTH_POLLING_SAFETY_MARGIN_MS);
256
+ continue;
257
+ }
258
+ if (data.error)
259
+ return { type: "failed" };
260
+ await Bun.sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS);
261
+ continue;
262
+ }
263
+ },
264
+ };
265
+ },
266
+ },
267
+ ],
268
+ },
269
+ "chat.headers": async (incoming, output) => {
270
+ if (!incoming.model.providerID.includes("github-copilot"))
271
+ return;
272
+ if (incoming.model.api.npm === "@ai-sdk/anthropic") {
273
+ output.headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
274
+ }
275
+ const session = await sdk.session
276
+ .get({
277
+ path: {
278
+ id: incoming.sessionID,
279
+ },
280
+ query: {
281
+ directory: input.directory,
282
+ },
283
+ throwOnError: true,
284
+ })
285
+ .catch(() => undefined);
286
+ if (!session || !session.data.parentID)
287
+ return;
288
+ // mark subagent sessions as agent initiated matching standard that other copilot tools have
289
+ output.headers["x-initiator"] = "agent";
290
+ },
291
+ };
292
+ }
293
+ /* GENERATED_EXPORTS_START */
294
+ export function createOfficialCopilotLoader(options = {}) {
295
+ const fetchImpl = options.fetchImpl ?? fetch;
296
+ const version = options.version ?? Installation.VERSION;
297
+ return async function loader(getAuth, provider) {
298
+ const info = await getAuth();
299
+ if (!info || info.type !== "oauth")
300
+ return {};
301
+ const enterpriseUrl = info.enterpriseUrl;
302
+ const baseURL = enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : undefined;
303
+ if (provider && provider.models) {
304
+ for (const model of Object.values(provider.models)) {
305
+ model.cost = {
306
+ input: 0,
307
+ output: 0,
308
+ cache: {
309
+ read: 0,
310
+ write: 0,
311
+ },
312
+ };
313
+ // TODO: re-enable once messages api has higher rate limits
314
+ // TODO: move some of this hacky-ness to models.dev presets once we have better grasp of things here...
315
+ // const base = baseURL ?? model.api.url
316
+ // const claude = model.id.includes("claude")
317
+ // const url = iife(() => {
318
+ // if (!claude) return base
319
+ // if (base.endsWith("/v1")) return base
320
+ // if (base.endsWith("/")) return `${base}v1`
321
+ // return `${base}/v1`
322
+ // })
323
+ // model.api.url = url
324
+ // model.api.npm = claude ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"
325
+ model.api.npm = "@ai-sdk/github-copilot";
326
+ }
327
+ }
328
+ return {
329
+ baseURL,
330
+ apiKey: "",
331
+ async fetch(request, init) {
332
+ const info = await getAuth();
333
+ if (info.type !== "oauth")
334
+ return fetchImpl(request, init);
335
+ const url = request instanceof URL ? request.href : request.toString();
336
+ const { isVision, isAgent } = iife(() => {
337
+ try {
338
+ const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body;
339
+ // Completions API
340
+ if (body?.messages && url.includes("completions")) {
341
+ const last = body.messages[body.messages.length - 1];
342
+ return {
343
+ isVision: body.messages.some((msg) => Array.isArray(msg.content) && msg.content.some((part) => part.type === "image_url")),
344
+ isAgent: last?.role !== "user",
345
+ };
346
+ }
347
+ // Responses API
348
+ if (body?.input) {
349
+ const last = body.input[body.input.length - 1];
350
+ return {
351
+ isVision: body.input.some((item) => Array.isArray(item?.content) && item.content.some((part) => part.type === "input_image")),
352
+ isAgent: last?.role !== "user",
353
+ };
354
+ }
355
+ // Messages API
356
+ if (body?.messages) {
357
+ const last = body.messages[body.messages.length - 1];
358
+ const hasNonToolCalls = Array.isArray(last?.content) && last.content.some((part) => part?.type !== "tool_result");
359
+ return {
360
+ isVision: body.messages.some((item) => Array.isArray(item?.content) &&
361
+ item.content.some((part) => part?.type === "image" ||
362
+ // images can be nested inside tool_result content
363
+ (part?.type === "tool_result" &&
364
+ Array.isArray(part?.content) &&
365
+ part.content.some((nested) => nested?.type === "image")))),
366
+ isAgent: !(last?.role === "user" && hasNonToolCalls),
367
+ };
368
+ }
369
+ }
370
+ catch { }
371
+ return { isVision: false, isAgent: false };
372
+ });
373
+ const headers = {
374
+ "x-initiator": isAgent ? "agent" : "user",
375
+ ...init?.headers,
376
+ "User-Agent": `opencode/${version}`,
377
+ Authorization: `Bearer ${info.refresh}`,
378
+ "Openai-Intent": "conversation-edits",
379
+ };
380
+ if (isVision) {
381
+ headers["Copilot-Vision-Request"] = "true";
382
+ }
383
+ delete headers["x-api-key"];
384
+ delete headers["authorization"];
385
+ return fetchImpl(request, {
386
+ ...init,
387
+ headers,
388
+ });
389
+ },
390
+ };
391
+ };
392
+ }
393
+ /* GENERATED_EXPORTS_END */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-copilot-account-switcher",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "GitHub Copilot account switcher plugin for OpenCode",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -32,6 +32,8 @@
32
32
  ],
33
33
  "scripts": {
34
34
  "build": "tsc -p tsconfig.build.json",
35
+ "sync:copilot-snapshot": "node scripts/sync-copilot-upstream.mjs --output src/upstream/copilot-plugin.snapshot.ts",
36
+ "check:copilot-sync": "node scripts/sync-copilot-upstream.mjs --output src/upstream/copilot-plugin.snapshot.ts --check",
35
37
  "test": "npm run build && node --test",
36
38
  "typecheck": "tsc --noEmit",
37
39
  "prepublishOnly": "npm run build"