@nick3/copilot-api 1.10.9 → 1.10.29
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 +140 -45
- package/README.zh-CN.md +140 -45
- package/dist/{account-COtMmvzU.js → account-DpW8RaT6.js} +3 -3
- package/dist/{account-COtMmvzU.js.map → account-DpW8RaT6.js.map} +1 -1
- package/dist/admin/AGENTS.md +19 -0
- package/dist/auth-nO-eHeO_.js +327 -0
- package/dist/auth-nO-eHeO_.js.map +1 -0
- package/dist/{check-usage-DdevqHE5.js → check-usage-ZifYvA3w.js} +4 -42
- package/dist/check-usage-ZifYvA3w.js.map +1 -0
- package/dist/config-CmhIPHn_.js +578 -0
- package/dist/config-CmhIPHn_.js.map +1 -0
- package/dist/{debug-BMo6ltbp.js → debug-DvpksqEL.js} +18 -7
- package/dist/debug-DvpksqEL.js.map +1 -0
- package/dist/main.js +5 -10
- package/dist/main.js.map +1 -1
- package/dist/mcp-http-BhELuvog.js +2 -0
- package/dist/mcp-http-DI4Vz01p.js +82 -0
- package/dist/mcp-http-DI4Vz01p.js.map +1 -0
- package/dist/mcp-http-config-DMdUDz1D.js +39 -0
- package/dist/mcp-http-config-DMdUDz1D.js.map +1 -0
- package/dist/mcp-pLTPS0tO.js +79 -0
- package/dist/mcp-pLTPS0tO.js.map +1 -0
- package/dist/{tool-search-BrN7M0Dd.js → mcp-server-DEqHrXFq.js} +25 -2
- package/dist/mcp-server-DEqHrXFq.js.map +1 -0
- package/dist/{paths-CclKwouX.js → paths-Bpsb62LK.js} +3 -1
- package/dist/paths-Bpsb62LK.js.map +1 -0
- package/dist/{poll-access-token-BAgM2-7k.js → poll-access-token-GzVkiTH8.js} +71 -4
- package/dist/poll-access-token-GzVkiTH8.js.map +1 -0
- package/dist/{request-outbound-BJjWS_jF.js → request-outbound-BkEA8Wgb.js} +1 -1
- package/dist/{request-outbound-Pu1kp2x8.js → request-outbound-DZTxxtcx.js} +3 -3
- package/dist/{request-outbound-Pu1kp2x8.js.map → request-outbound-DZTxxtcx.js.map} +1 -1
- package/dist/{proxy-_U-hgwIn.js → responses-bridge-registry-BJ5Sbh6-.js} +116 -577
- package/dist/responses-bridge-registry-BJ5Sbh6-.js.map +1 -0
- package/dist/{server-GxNB5Syq.js → server-DJ3_UGc4.js} +313 -165
- package/dist/server-DJ3_UGc4.js.map +1 -0
- package/dist/start-DaB0AcjZ.js +526 -0
- package/dist/start-DaB0AcjZ.js.map +1 -0
- package/dist/token-DrFDLVxa.js +365 -0
- package/dist/token-DrFDLVxa.js.map +1 -0
- package/package.json +1 -1
- package/dist/auth-B0y-2njL.js +0 -226
- package/dist/auth-B0y-2njL.js.map +0 -1
- package/dist/check-usage-DdevqHE5.js.map +0 -1
- package/dist/debug-BMo6ltbp.js.map +0 -1
- package/dist/get-copilot-token-8Rm-rVsp.js +0 -17
- package/dist/get-copilot-token-8Rm-rVsp.js.map +0 -1
- package/dist/mcp-9Hgepkc5.js +0 -37
- package/dist/mcp-9Hgepkc5.js.map +0 -1
- package/dist/paths-CclKwouX.js.map +0 -1
- package/dist/poll-access-token-BAgM2-7k.js.map +0 -1
- package/dist/proxy-_U-hgwIn.js.map +0 -1
- package/dist/server-GxNB5Syq.js.map +0 -1
- package/dist/start-DdrurmQ3.js +0 -274
- package/dist/start-DdrurmQ3.js.map +0 -1
- package/dist/tool-search-BrN7M0Dd.js.map +0 -1
package/README.zh-CN.md
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
>
|
|
36
36
|
> 2. **推荐给 opencode 用户:** 与 opencode 搭配时,推荐优先使用 opencode OAuth app 启动。该方式与 opencode 内置的 GitHub Copilot provider 行为一致,且不存在 Terms of Service 风险:
|
|
37
37
|
> ```sh
|
|
38
|
-
>
|
|
38
|
+
> bunx --bun @nick3/copilot-api@latest --oauth-app=opencode start
|
|
39
39
|
> ```
|
|
40
40
|
>
|
|
41
41
|
> 3. **通过 codex 使用时请关闭 multi agent:** 如果你是通过 GitHub Copilot 使用 codex,建议关闭 multi agent 功能。目前 GitHub Copilot 在 codex 场景下会按最后一条消息是否为 user role 计费,而这部分计费逻辑尚未调整。
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
## 功能特性
|
|
52
52
|
|
|
53
53
|
- **OpenAI 与 Anthropic 双兼容**:以 OpenAI 兼容接口(`/v1/responses`、`/v1/chat/completions`、`/v1/models`、`/v1/embeddings`)和 Anthropic 兼容接口(`/v1/messages`)对外暴露 GitHub Copilot。
|
|
54
|
+
- **Codex Responses WebSocket 兼容**:在 `/v1/responses` 接受 Codex 偏好的 Responses WebSocket transport,并桥接到现有 Responses handler。
|
|
54
55
|
- **Claude 模型优先走 Anthropic 原生路由**:当模型支持 Copilot 原生 `/v1/messages` 端点时,代理会优先使用它,而不是 `/responses` 或 `/chat/completions`,从而保留 Anthropic 风格的 `tool_use` / `tool_result` 流程以及更原生的 Claude 行为。
|
|
55
56
|
- **减少不必要的 Premium 请求**:通过把预热请求路由到 `smallModel`、将 `tool_result` 的后续消息重新并入工具流,以及把恢复的工具轮次视为延续流量而非全新高级交互,减少浪费的 premium 使用量。
|
|
56
57
|
- **分阶段的 `gpt-5.4` 与 `gpt-5.3-codex`**:这些模型可以在更深入推理或调用工具前先发出面向用户的 commentary,让长时间运行的编码操作更容易理解,而不是突然开始一串工具调用。
|
|
@@ -133,7 +134,7 @@
|
|
|
133
134
|
## 前置要求
|
|
134
135
|
|
|
135
136
|
- Bun(>= 1.2.x)
|
|
136
|
-
-
|
|
137
|
+
- 只有通过 `npx` 运行轻量 MCP bridge 时才需要 Node.js
|
|
137
138
|
- 已订阅 Copilot 的 GitHub 账号(个人版、Business 或 Enterprise)
|
|
138
139
|
|
|
139
140
|
## 安装
|
|
@@ -150,26 +151,28 @@ bun install
|
|
|
150
151
|
bun run start start
|
|
151
152
|
```
|
|
152
153
|
|
|
153
|
-
## 通过
|
|
154
|
+
## 通过 Bun 使用已发布 CLI
|
|
154
155
|
|
|
155
|
-
|
|
156
|
+
服务端和账号管理命令是 Bun-only,因为 Admin UI 与请求历史使用 `bun:sqlite`。运行已发布 CLI 时请使用 Bun,避免 `#!/usr/bin/env node` shebang 强制走 Node.js:
|
|
156
157
|
|
|
157
158
|
```sh
|
|
158
|
-
|
|
159
|
+
bunx --bun @nick3/copilot-api@latest start
|
|
159
160
|
```
|
|
160
161
|
|
|
161
162
|
带参数示例:
|
|
162
163
|
|
|
163
164
|
```sh
|
|
164
|
-
|
|
165
|
+
bunx --bun @nick3/copilot-api@latest start --port 8080
|
|
165
166
|
```
|
|
166
167
|
|
|
167
168
|
如果只想做认证:
|
|
168
169
|
|
|
169
170
|
```sh
|
|
170
|
-
|
|
171
|
+
bunx --bun @nick3/copilot-api@latest auth
|
|
171
172
|
```
|
|
172
173
|
|
|
174
|
+
轻量 MCP bridge 是例外,仍可通过 `npx` 启动;见 [GPT Tool Search](#gpt-tool-search)。
|
|
175
|
+
|
|
173
176
|
## 配合 Docker 使用
|
|
174
177
|
|
|
175
178
|
构建镜像:
|
|
@@ -300,6 +303,20 @@ Copilot API 现在使用子命令结构,主要命令包括:
|
|
|
300
303
|
| `--claude-code` | 生成一个使用 Copilot API 配置启动 Claude Code 的命令 | `false` | `-c` |
|
|
301
304
|
| `--show-token` | 在获取和刷新时显示 GitHub 与 Copilot token | `false` | 无 |
|
|
302
305
|
| `--proxy-env` | 从环境变量初始化代理 | `false` | 无 |
|
|
306
|
+
| `--enable-mcp-http` | 在 `/mcp` 暴露未认证的 MCP Streamable HTTP 端点 | `false` | 无 |
|
|
307
|
+
|
|
308
|
+
### MCP 命令选项
|
|
309
|
+
|
|
310
|
+
`mcp` 命令默认使用 stdio,以保持本地 Claude Code 兼容性。只有当你的 MCP 客户端支持 Streamable HTTP 时,才使用 `--transport http`。
|
|
311
|
+
|
|
312
|
+
| 选项 | 说明 | 默认值 |
|
|
313
|
+
| --- | --- | --- |
|
|
314
|
+
| `--transport` | 使用的 transport:`stdio` 或 `http` | `stdio` |
|
|
315
|
+
| `--host` | HTTP transport host | `127.0.0.1` |
|
|
316
|
+
| `--port` | HTTP transport port | `4142` |
|
|
317
|
+
| `--path` | HTTP transport path | `/mcp` |
|
|
318
|
+
|
|
319
|
+
MCP HTTP 的浏览器 CORS 默认只允许 loopback origin。可设置 `COPILOT_API_MCP_HTTP_ALLOWED_ORIGINS=https://client.example.com,https://admin.example.com` 允许额外浏览器 origin,或显式设置为 `*` 启用 wildcard CORS。
|
|
303
320
|
|
|
304
321
|
### Auth 命令选项
|
|
305
322
|
|
|
@@ -460,7 +477,7 @@ Copilot API 现在使用子命令结构,主要命令包括:
|
|
|
460
477
|
- **modelRefreshIntervalHours:** 后台刷新账号模型列表的间隔小时数。设为 `0` 可关闭自动刷新。默认值为 `24`。
|
|
461
478
|
- **sessionAffinityRetentionDays:** session affinity 绑定的保留天数。默认值为 `7`。
|
|
462
479
|
- **useMessagesApi:** 当为 `true`(默认)时,支持 Copilot 原生 `/v1/messages` 端点的 Claude 系模型会走 Messages API 路径。设为 `false` 时,将跳过 Messages API 候选,回退到 `/responses`(如支持)或 `/chat/completions`。
|
|
463
|
-
- **useResponsesApiWebSocket:** 当为 `true
|
|
480
|
+
- **useResponsesApiWebSocket:** 当为 `true`(默认)时,发往上游 Copilot Responses API 的请求会对声明了 `ws:/responses` 的模型使用 Copilot WebSocket transport;仅声明 `/responses` 的模型仍走 HTTP。设为 `false` 可禁用上游 WebSocket 路由。该配置不会禁用 `/v1/responses` 上面向 Codex 的入站 WebSocket listener。
|
|
464
481
|
- **useResponsesApiWebSearch:** 当为 `true`(默认)时,`/v1/responses` 会保留 `type: "web_search"` 的工具并转发到上游。设为 `false` 则会在发送 Copilot 请求之前将其剥离。
|
|
465
482
|
- **logLevel:** 控制 `logs/*.log` 下 handler 文件日志的详细级别。可选值:`error`、`warn`、`info`、`debug`。默认值为 `info`。如果你需要把 payload 级或 stream 级的调试内容写入文件日志,请显式设置为 `debug`。
|
|
466
483
|
- **anthropicApiKey:** 可选的 Anthropic API key,用于精确的 Claude token 计数(见下文 [精确的 Claude Token 计数](#accurate-claude-token-counting))。也可通过环境变量 `ANTHROPIC_API_KEY` 设置。未配置时会回退到 GPT tokenizer 估算。
|
|
@@ -498,6 +515,7 @@ curl http://localhost:4141/v1/models \
|
|
|
498
515
|
| 端点 | 方法 | 说明 |
|
|
499
516
|
| --- | --- | --- |
|
|
500
517
|
| `POST /v1/responses` | `POST` | OpenAI 中用于生成模型响应的高级接口。 |
|
|
518
|
+
| `GET /v1/responses` | `WS` | Codex 兼容的 Responses WebSocket transport。 |
|
|
501
519
|
| `POST /v1/chat/completions` | `POST` | 为给定聊天对话创建模型响应。 |
|
|
502
520
|
| `GET /v1/models` | `GET` | 列出当前可用模型。 |
|
|
503
521
|
| `POST /v1/embeddings` | `POST` | 创建表示输入文本的向量嵌入。 |
|
|
@@ -580,86 +598,98 @@ curl "http://localhost:4141/usage/0"
|
|
|
580
598
|
|
|
581
599
|
## 使用示例
|
|
582
600
|
|
|
583
|
-
通过
|
|
601
|
+
通过 Bun 使用已发布 CLI:
|
|
584
602
|
|
|
585
603
|
```sh
|
|
586
604
|
# 基础启动
|
|
587
|
-
|
|
605
|
+
bunx --bun @nick3/copilot-api@latest start
|
|
588
606
|
|
|
589
607
|
# 自定义端口并开启详细日志
|
|
590
|
-
|
|
608
|
+
bunx --bun @nick3/copilot-api@latest start --port 8080 --verbose
|
|
591
609
|
|
|
592
610
|
# 使用 GitHub Business 方案账号
|
|
593
|
-
|
|
611
|
+
bunx --bun @nick3/copilot-api@latest start --account-type business
|
|
594
612
|
|
|
595
613
|
# 使用 GitHub Enterprise 方案账号
|
|
596
|
-
|
|
614
|
+
bunx --bun @nick3/copilot-api@latest start --account-type enterprise
|
|
597
615
|
|
|
598
616
|
# 对每个请求启用手动审批
|
|
599
|
-
|
|
617
|
+
bunx --bun @nick3/copilot-api@latest start --manual
|
|
600
618
|
|
|
601
619
|
# 将请求间隔限制为 30 秒
|
|
602
|
-
|
|
620
|
+
bunx --bun @nick3/copilot-api@latest start --rate-limit 30
|
|
603
621
|
|
|
604
622
|
# 命中速率限制时等待,而不是直接报错
|
|
605
|
-
|
|
623
|
+
bunx --bun @nick3/copilot-api@latest start --rate-limit 30 --wait
|
|
606
624
|
|
|
607
625
|
# 直接传入 GitHub token
|
|
608
|
-
|
|
626
|
+
bunx --bun @nick3/copilot-api@latest start --github-token ghp_YOUR_TOKEN_HERE
|
|
609
627
|
|
|
610
628
|
# 仅执行认证流程
|
|
611
|
-
|
|
629
|
+
bunx --bun @nick3/copilot-api@latest auth
|
|
612
630
|
|
|
613
631
|
# 认证时启用详细日志
|
|
614
|
-
|
|
632
|
+
bunx --bun @nick3/copilot-api@latest auth --verbose
|
|
615
633
|
|
|
616
634
|
# 添加多个账号(账号会按添加顺序记录)
|
|
617
|
-
|
|
618
|
-
|
|
635
|
+
bunx --bun @nick3/copilot-api@latest auth add
|
|
636
|
+
bunx --bun @nick3/copilot-api@latest auth add # 添加第二个账号
|
|
619
637
|
|
|
620
638
|
# 列出所有已注册账号
|
|
621
|
-
|
|
639
|
+
bunx --bun @nick3/copilot-api@latest auth ls
|
|
622
640
|
|
|
623
641
|
# 列出账号并显示配额信息
|
|
624
|
-
|
|
642
|
+
bunx --bun @nick3/copilot-api@latest auth ls -q
|
|
625
643
|
|
|
626
644
|
# 按索引删除账号(1-based)
|
|
627
|
-
|
|
645
|
+
bunx --bun @nick3/copilot-api@latest auth rm 2
|
|
628
646
|
|
|
629
647
|
# 按 ID 删除账号(GitHub 用户名)
|
|
630
|
-
|
|
648
|
+
bunx --bun @nick3/copilot-api@latest auth rm octocat
|
|
631
649
|
|
|
632
650
|
# 在终端中查看 Copilot 用量与额度(无需启动服务)
|
|
633
|
-
|
|
651
|
+
bunx --bun @nick3/copilot-api@latest check-usage
|
|
634
652
|
|
|
635
653
|
# 输出调试信息,便于排障
|
|
636
|
-
|
|
654
|
+
bunx --bun @nick3/copilot-api@latest debug
|
|
637
655
|
|
|
638
656
|
# 以 JSON 格式输出调试信息
|
|
639
|
-
|
|
657
|
+
bunx --bun @nick3/copilot-api@latest debug --json
|
|
640
658
|
|
|
641
659
|
# 从环境变量初始化代理(HTTP_PROXY、HTTPS_PROXY 等)
|
|
642
|
-
|
|
660
|
+
bunx --bun @nick3/copilot-api@latest start --proxy-env
|
|
643
661
|
|
|
644
662
|
# 使用 opencode GitHub Copilot 认证
|
|
645
|
-
COPILOT_API_OAUTH_APP=opencode
|
|
663
|
+
COPILOT_API_OAUTH_APP=opencode bunx --bun @nick3/copilot-api@latest start
|
|
646
664
|
|
|
647
665
|
# 通过命令行设置自定义 API home 目录
|
|
648
|
-
|
|
666
|
+
bunx --bun @nick3/copilot-api@latest --api-home=/path/to/custom/dir start
|
|
649
667
|
|
|
650
668
|
# 通过命令行使用 GitHub Enterprise
|
|
651
|
-
|
|
669
|
+
bunx --bun @nick3/copilot-api@latest --enterprise-url=company.ghe.com start
|
|
652
670
|
|
|
653
671
|
# 通过命令行使用 opencode OAuth
|
|
654
|
-
|
|
672
|
+
bunx --bun @nick3/copilot-api@latest --oauth-app=opencode start
|
|
655
673
|
|
|
656
674
|
# 组合多个全局选项
|
|
657
|
-
|
|
675
|
+
bunx --bun @nick3/copilot-api@latest --api-home=/custom/path --oauth-app=opencode --enterprise-url=company.ghe.com start
|
|
676
|
+
```
|
|
658
677
|
|
|
659
|
-
|
|
660
|
-
|
|
678
|
+
只有 MCP tool-search bridge 仍支持 `npx`:
|
|
679
|
+
|
|
680
|
+
```sh
|
|
681
|
+
# 本地 stdio MCP bridge,行为保持不变
|
|
682
|
+
npx -y @nick3/copilot-api@latest mcp
|
|
683
|
+
|
|
684
|
+
# 独立 Streamable HTTP MCP bridge
|
|
685
|
+
npx -y @nick3/copilot-api@latest mcp --transport http --host 127.0.0.1 --port 4142 --path /mcp
|
|
686
|
+
|
|
687
|
+
# 在主代理服务中显式启用 /mcp
|
|
688
|
+
bunx --bun @nick3/copilot-api@latest start --enable-mcp-http
|
|
661
689
|
```
|
|
662
690
|
|
|
691
|
+
HTTP MCP 端点不带认证。独立模式请尽量保持默认 loopback host;浏览器 CORS 默认只允许 loopback origin,仅为可信客户端设置 `COPILOT_API_MCP_HTTP_ALLOWED_ORIGINS`。不要把 `/mcp` 暴露到不可信网络,除非外层反向代理、防火墙或 tunnel access policy 已经保护它。
|
|
692
|
+
|
|
663
693
|
### Opencode OAuth 认证
|
|
664
694
|
|
|
665
695
|
你可以使用 opencode GitHub Copilot 认证,替代默认的认证方式:
|
|
@@ -669,16 +699,52 @@ bunx --bun @nick3/copilot-api@latest start
|
|
|
669
699
|
export COPILOT_API_OAUTH_APP=opencode
|
|
670
700
|
|
|
671
701
|
# 然后执行 start 或 auth 命令
|
|
672
|
-
|
|
673
|
-
|
|
702
|
+
bunx --bun @nick3/copilot-api@latest start
|
|
703
|
+
bunx --bun @nick3/copilot-api@latest auth
|
|
674
704
|
```
|
|
675
705
|
|
|
676
706
|
也可以使用内联环境变量:
|
|
677
707
|
|
|
678
708
|
```sh
|
|
679
|
-
COPILOT_API_OAUTH_APP=opencode
|
|
709
|
+
COPILOT_API_OAUTH_APP=opencode bunx --bun @nick3/copilot-api@latest start
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
## 与 Codex CLI 一起使用
|
|
713
|
+
|
|
714
|
+
Codex 可以把本代理作为 OpenAI 兼容的 Responses API provider 使用。本代理同时支持 Codex 的 HTTP `POST /v1/responses` 路径,以及它偏好的 `GET /v1/responses` WebSocket upgrade。
|
|
715
|
+
|
|
716
|
+
启动代理:
|
|
717
|
+
|
|
718
|
+
```sh
|
|
719
|
+
bunx --bun @nick3/copilot-api@latest start
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
> **注意:** `GET /v1/responses` 上的入站 Codex WebSocket listener 需要 Bun 服务端运行时,因此请使用 `bunx --bun`(或本地安装的 Bun)启动代理。`npx` 路径仅支持轻量级 MCP bridge,不会运行该 WebSocket listener。
|
|
723
|
+
|
|
724
|
+
在 `~/.codex/config.toml` 中添加 provider:
|
|
725
|
+
|
|
726
|
+
```toml
|
|
727
|
+
[model_providers.copilot-api]
|
|
728
|
+
name = "copilot-api"
|
|
729
|
+
base_url = "http://localhost:4141/v1"
|
|
730
|
+
wire_api = "responses"
|
|
731
|
+
supports_websockets = true
|
|
732
|
+
|
|
733
|
+
[profiles.copilot-api]
|
|
734
|
+
model_provider = "copilot-api"
|
|
735
|
+
model = "gpt-5.4"
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
然后使用该 profile 启动 Codex:
|
|
739
|
+
|
|
740
|
+
```sh
|
|
741
|
+
codex -p copilot-api
|
|
680
742
|
```
|
|
681
743
|
|
|
744
|
+
如果你配置了 `auth.apiKeys`,需要在 Codex 的 provider headers 或 bearer-token 配置里使用同一个 key,这样 HTTP 与 WebSocket 请求都能通过认证。仅在排障时,可以在 Codex 中设置 `supports_websockets = false` 来强制走 HTTP fallback。
|
|
745
|
+
|
|
746
|
+
> **注意:** 通过 GitHub Copilot 使用 Codex 时,目前建议关闭 Codex multi-agent 功能,因为 Copilot 可能会根据最后一条 user-role 消息来计算 Codex 流量。
|
|
747
|
+
|
|
682
748
|
## 与 Claude Code 一起使用
|
|
683
749
|
|
|
684
750
|
这个代理可以为 [Claude Code](https://docs.anthropic.com/en/claude-code) 提供后端能力。Claude Code 是 Anthropic 提供的实验性面向开发者的对话式 AI 助手。
|
|
@@ -690,7 +756,7 @@ COPILOT_API_OAUTH_APP=opencode npx @nick3/copilot-api@latest start
|
|
|
690
756
|
执行带 `--claude-code` 的 `start` 命令开始:
|
|
691
757
|
|
|
692
758
|
```sh
|
|
693
|
-
|
|
759
|
+
bunx --bun @nick3/copilot-api@latest start --claude-code
|
|
694
760
|
```
|
|
695
761
|
|
|
696
762
|
你会被提示选择一个主模型,以及一个用于后台任务的 “small, fast” 模型。选择完成后,会有一条命令被复制到剪贴板中。该命令会设置 Claude Code 使用该代理所需的环境变量。
|
|
@@ -748,7 +814,9 @@ GPT 模型不要设置 Claude Code 原生的 `ENABLE_TOOL_SEARCH`。这个开关
|
|
|
748
814
|
|
|
749
815
|
如果你安装了 `tool-search@copilot-api-marketplace`,Claude Code 会自动带上这个 MCP bridge,可以跳过下面这段 Claude Code MCP 手动配置。
|
|
750
816
|
|
|
751
|
-
|
|
817
|
+
这个 MCP bridge 很小,并且不会加载服务端或 SQLite 代码,因此仍可安全地通过 `npx` 运行。主 `start`、`auth`、`check-usage` 和 `debug` 命令请使用 Bun。
|
|
818
|
+
|
|
819
|
+
通过 stdio 使用时,请把 tool search bridge 加到 Claude Code 使用的 MCP 配置中:
|
|
752
820
|
|
|
753
821
|
```json
|
|
754
822
|
{
|
|
@@ -762,6 +830,33 @@ GPT 模型不要设置 Claude Code 原生的 `ENABLE_TOOL_SEARCH`。这个开关
|
|
|
762
830
|
}
|
|
763
831
|
```
|
|
764
832
|
|
|
833
|
+
如果要改用 Streamable HTTP,先在一个终端中启动 MCP HTTP bridge:
|
|
834
|
+
|
|
835
|
+
```sh
|
|
836
|
+
npx -y @nick3/copilot-api@latest mcp --transport http --host 127.0.0.1 --port 4142 --path /mcp
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
然后把这个 HTTP MCP server 加到 Claude Code:
|
|
840
|
+
|
|
841
|
+
```sh
|
|
842
|
+
claude mcp add --transport http tool_search http://127.0.0.1:4142/mcp
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
等价的手动 MCP 配置如下:
|
|
846
|
+
|
|
847
|
+
```json
|
|
848
|
+
{
|
|
849
|
+
"mcpServers": {
|
|
850
|
+
"tool_search": {
|
|
851
|
+
"type": "http",
|
|
852
|
+
"url": "http://127.0.0.1:4142/mcp"
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
如果希望由主代理进程暴露同一个 MCP server,请用 `--enable-mcp-http` 启动主服务,并在 Claude Code MCP URL 中使用 `http://127.0.0.1:4141/mcp`。`tool_search` 请在 stdio 配置和 HTTP 配置中二选一,不要同时启用。
|
|
859
|
+
|
|
765
860
|
请把 tool search bridge 加到 opencode 使用的 MCP 配置中:
|
|
766
861
|
|
|
767
862
|
```json
|
|
@@ -790,7 +885,7 @@ OpenCode 已经内置了 GitHub Copilot provider。本节适用于你希望让 O
|
|
|
790
885
|
使用 OpenCode OAuth app 启动代理:
|
|
791
886
|
|
|
792
887
|
```sh
|
|
793
|
-
|
|
888
|
+
bunx --bun @nick3/copilot-api@latest --oauth-app=opencode start
|
|
794
889
|
```
|
|
795
890
|
|
|
796
891
|
然后让 OpenCode 通过 `@ai-sdk/anthropic` 指向该代理。
|
|
@@ -880,7 +975,7 @@ npx @nick3/copilot-api@latest --oauth-app=opencode start
|
|
|
880
975
|
curl "http://localhost:4141/api/admin/meta"
|
|
881
976
|
|
|
882
977
|
# 启用远程 Admin UI/API 访问(服务端)
|
|
883
|
-
# ADMIN_TOKEN=your_admin_token_here
|
|
978
|
+
# ADMIN_TOKEN=your_admin_token_here bunx --bun @nick3/copilot-api@latest start
|
|
884
979
|
|
|
885
980
|
# 远程访问(需要 token)
|
|
886
981
|
curl -H "x-admin-token: your_admin_token_here" "http://localhost:4141/api/admin/accounts?include_stats=1"
|
|
@@ -898,9 +993,9 @@ curl "http://localhost:4141/api/admin/requests/<requestId>"
|
|
|
898
993
|
|
|
899
994
|
代理内置了一个随服务实例一起提供的 Admin UI。你可以用它查看代理捕获的账号状态与请求历史(模型/端点、tokens/usage、耗时以及错误摘要等)。
|
|
900
995
|
|
|
901
|
-
1. 启动服务。例如使用
|
|
996
|
+
1. 启动服务。例如使用 Bun:
|
|
902
997
|
```sh
|
|
903
|
-
|
|
998
|
+
bunx --bun @nick3/copilot-api@latest start
|
|
904
999
|
```
|
|
905
1000
|
2. 在浏览器中打开:
|
|
906
1001
|
- `http://localhost:4141/admin`(如果改过端口,请替换为对应端口)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as accountTokenPath, t as PATHS } from "./paths-
|
|
1
|
+
import { E as normalizeDomain } from "./poll-access-token-GzVkiTH8.js";
|
|
2
|
+
import { n as accountTokenPath, t as PATHS } from "./paths-Bpsb62LK.js";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -363,4 +363,4 @@ function parseAccountType(value) {
|
|
|
363
363
|
//#endregion
|
|
364
364
|
export { DEFAULT_IDENTITY_ENTERPRISE_DOMAIN as _, getAccountClientIdentityByLoginAndApp as a, getCurrentIdentityEnvironment as b, isAccountEnabled as c, loadRegistry as d, readLegacyToken as f, saveRegistry as g, saveAccountToken as h, ensureAccountClientIdentity as i, listAccountsFromRegistry as l, removeAccountToken as m, parseAccountType as n, hasLegacyToken as o, removeAccountFromRegistry as p, addAccountToRegistry as r, hasRegistry as s, isAccountType as t, loadAccountToken as u, buildIdentityKey as v, createAccountSessionId as y };
|
|
365
365
|
|
|
366
|
-
//# sourceMappingURL=account-
|
|
366
|
+
//# sourceMappingURL=account-DpW8RaT6.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-COtMmvzU.js","names":[],"sources":["../src/lib/account-client-identity.ts","../src/lib/accounts-registry.ts","../src/lib/types/account.ts"],"sourcesContent":["import { createHash, randomUUID } from \"node:crypto\"\n\nimport { normalizeDomain } from \"./api-config\"\n\nexport const DEFAULT_IDENTITY_OAUTH_APP = \"default\"\nexport const DEFAULT_IDENTITY_ENTERPRISE_DOMAIN = \"public\"\n\nexport interface AccountIdentityEnvironment {\n oauthApp: string\n enterpriseDomain: string\n}\n\nexport const getCurrentIdentityEnvironment = (): AccountIdentityEnvironment => {\n const rawOauthApp = process.env.COPILOT_API_OAUTH_APP?.trim().toLowerCase()\n const rawEnterpriseDomain = process.env.COPILOT_API_ENTERPRISE_URL?.trim()\n\n return {\n oauthApp: rawOauthApp || DEFAULT_IDENTITY_OAUTH_APP,\n enterpriseDomain:\n rawEnterpriseDomain ?\n normalizeDomain(rawEnterpriseDomain).toLowerCase()\n : DEFAULT_IDENTITY_ENTERPRISE_DOMAIN,\n }\n}\n\nexport const buildIdentityKey = ({\n login,\n oauthApp,\n enterpriseDomain,\n}: {\n login: string\n oauthApp: string\n enterpriseDomain: string\n}): string => `${enterpriseDomain}:${oauthApp}:${login}`\n\nexport const createAccountDeviceId = (): string => randomUUID().toLowerCase()\n\nexport const createAccountMachineId = (): string =>\n createHash(\"sha256\").update(randomUUID(), \"utf8\").digest(\"hex\")\n\nexport const createAccountSessionId = (): string =>\n randomUUID() + Date.now().toString()\n","import fs from \"node:fs/promises\"\nimport { z } from \"zod\"\n\nimport type {\n AccountClientIdentity,\n AccountMeta,\n AccountRegistry,\n} from \"~/lib/types/account\"\n\nimport {\n DEFAULT_IDENTITY_ENTERPRISE_DOMAIN,\n buildIdentityKey,\n createAccountDeviceId,\n createAccountMachineId,\n getCurrentIdentityEnvironment,\n} from \"~/lib/account-client-identity\"\nimport { accountTokenPath, PATHS } from \"~/lib/paths\"\n\n/**\n * Validate account ID (GitHub login).\n * Rules:\n * - 1-39 chars\n * - Alphanumeric segments may be separated by single hyphens or underscores\n * - Cannot begin or end with a separator\n * - No consecutive separators\n */\nexport function validateAccountId(id: string): boolean {\n if (id.length === 0 || id.length > 39) return false\n return /^[A-Za-z0-9]+(?:[-_][A-Za-z0-9]+)*$/u.test(id)\n}\n\nfunction assertValidAccountId(id: string): void {\n if (!validateAccountId(id)) {\n throw new Error(`Invalid account ID: ${id}`)\n }\n}\n\nconst ACCOUNT_ID_VALIDATION_RULES =\n \"1-39 chars, alphanumeric with optional single hyphen/underscore separators, no leading/trailing separator, no consecutive separators.\"\n\nconst accountMetaSchema = z.object({\n id: z.string().refine(validateAccountId, {\n message: `Invalid account id. Expected a GitHub login (${ACCOUNT_ID_VALIDATION_RULES})`,\n }),\n accountType: z.enum([\"individual\", \"business\", \"enterprise\"]),\n addedAt: z.number(),\n enabled: z.boolean().optional(),\n})\n\n/**\n * Check whether an account is enabled for request routing.\n * Treats `undefined` and `true` as enabled (backward compatible).\n */\nexport function isAccountEnabled(meta: AccountMeta): boolean {\n return meta.enabled !== false\n}\n\nconst accountClientIdentitySchema = z.object({\n login: z.string().refine(validateAccountId, {\n message: `Invalid client identity login. Expected a GitHub login (${ACCOUNT_ID_VALIDATION_RULES})`,\n }),\n oauthApp: z.string().min(1),\n enterpriseDomain: z.string().min(1),\n deviceId: z\n .string()\n .regex(\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u,\n \"Invalid device ID format. Expected a lowercase UUID.\",\n ),\n machineId: z\n .string()\n .regex(\n /^[0-9a-f]{64}$/u,\n \"Invalid machine ID format. Expected 64 lowercase hexadecimal characters.\",\n ),\n createdAt: z.number(),\n})\n\nconst accountRegistryV1Schema = z.object({\n version: z.literal(1),\n accounts: z.array(accountMetaSchema),\n})\n\nconst accountRegistryV2Schema = z.object({\n version: z.literal(2),\n accounts: z.array(accountMetaSchema),\n clientIdentities: z.record(z.string(), accountClientIdentitySchema),\n})\n\nconst identityLocks = new Map<string, Promise<AccountClientIdentity>>()\nlet registryLock: Promise<void> = Promise.resolve()\n\nconst runWithRegistryLock = async <T>(\n operation: () => Promise<T>,\n): Promise<T> => {\n const previousLock = registryLock\n let releaseLock!: () => void\n registryLock = new Promise<void>((resolve) => {\n releaseLock = resolve\n })\n\n await previousLock\n\n try {\n return await operation()\n } finally {\n releaseLock()\n }\n}\n\n/**\n * Create an empty registry with the current schema version.\n */\nfunction createEmptyRegistry(): AccountRegistry {\n return {\n version: 2,\n accounts: [],\n clientIdentities: {},\n }\n}\n\nconst createClientIdentity = ({\n login,\n oauthApp,\n enterpriseDomain,\n}: {\n login: string\n oauthApp: string\n enterpriseDomain: string\n}): AccountClientIdentity => ({\n login,\n oauthApp,\n enterpriseDomain,\n deviceId: createAccountDeviceId(),\n machineId: createAccountMachineId(),\n createdAt: Date.now(),\n})\n\nconst ensureRegistryIdentity = (\n registry: AccountRegistry,\n {\n login,\n oauthApp,\n enterpriseDomain,\n }: {\n login: string\n oauthApp: string\n enterpriseDomain: string\n },\n): AccountClientIdentity => {\n const identityKey = buildIdentityKey({ login, oauthApp, enterpriseDomain })\n const existing = registry.clientIdentities[identityKey]\n if (existing) {\n return existing\n }\n\n const created = createClientIdentity({\n login,\n oauthApp,\n enterpriseDomain,\n })\n registry.clientIdentities[identityKey] = created\n return created\n}\n\nconst ensureClientIdentitiesForAccounts = (\n registry: AccountRegistry,\n): boolean => {\n const { oauthApp, enterpriseDomain } = getCurrentIdentityEnvironment()\n const countBefore = Object.keys(registry.clientIdentities).length\n\n for (const account of registry.accounts) {\n ensureRegistryIdentity(registry, {\n login: account.id,\n oauthApp,\n enterpriseDomain,\n })\n }\n\n return Object.keys(registry.clientIdentities).length !== countBefore\n}\n\nconst assertNoDuplicateAccounts = (registry: {\n accounts: Array<AccountMeta>\n}) => {\n const seen = new Set<string>()\n for (const account of registry.accounts) {\n if (seen.has(account.id)) {\n throw new Error(\n `Invalid accounts registry at ${PATHS.ACCOUNTS_REGISTRY_PATH}: duplicate account id \"${account.id}\"`,\n )\n }\n seen.add(account.id)\n }\n}\n\nconst loadRegistrySnapshot = async (): Promise<{\n registry: AccountRegistry\n shouldPersist: boolean\n}> => {\n try {\n const content = await fs.readFile(PATHS.ACCOUNTS_REGISTRY_PATH, \"utf8\")\n if (!content.trim()) {\n return {\n registry: createEmptyRegistry(),\n shouldPersist: false,\n }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(content) as unknown\n } catch (error) {\n throw new Error(\n `Invalid accounts registry JSON at ${PATHS.ACCOUNTS_REGISTRY_PATH}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n )\n }\n\n const isVersion2Record =\n typeof parsed === \"object\"\n && parsed !== null\n && \"version\" in parsed\n && parsed.version === 2\n const result =\n isVersion2Record ?\n accountRegistryV2Schema.safeParse(parsed)\n : accountRegistryV1Schema.safeParse(parsed)\n if (!result.success) {\n const issues = result.error.issues\n .map((issue) => `${issue.path.join(\".\")}: ${issue.message}`)\n .join(\"; \")\n\n throw new Error(\n `Invalid accounts registry at ${PATHS.ACCOUNTS_REGISTRY_PATH}: ${issues}`,\n )\n }\n\n const parsedRegistry = result.data\n const registry: AccountRegistry =\n parsedRegistry.version === 2 ?\n parsedRegistry\n : {\n version: 2,\n accounts: parsedRegistry.accounts,\n clientIdentities: {},\n }\n\n assertNoDuplicateAccounts(registry)\n\n const identitiesBackfilled = ensureClientIdentitiesForAccounts(registry)\n\n return {\n registry,\n shouldPersist: parsedRegistry.version !== 2 || identitiesBackfilled,\n }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return {\n registry: createEmptyRegistry(),\n shouldPersist: false,\n }\n }\n throw error\n }\n}\n\nconst saveRegistryUnlocked = async (\n registry: AccountRegistry,\n): Promise<void> => {\n const content = JSON.stringify(registry, null, 2)\n await fs.writeFile(PATHS.ACCOUNTS_REGISTRY_PATH, content, { mode: 0o600 })\n}\n\n/**\n * Load the accounts registry from disk.\n * Returns an empty registry if the file doesn't exist.\n */\nexport async function loadRegistry(): Promise<AccountRegistry> {\n return runWithRegistryLock(async () => {\n const { registry, shouldPersist } = await loadRegistrySnapshot()\n if (shouldPersist) {\n await saveRegistryUnlocked(registry)\n }\n return registry\n })\n}\n\n/**\n * Save the accounts registry to disk with secure permissions.\n */\nexport async function saveRegistry(registry: AccountRegistry): Promise<void> {\n await runWithRegistryLock(async () => {\n await saveRegistryUnlocked(registry)\n })\n}\n\nexport async function getAccountClientIdentity(\n identityKey: string,\n): Promise<AccountClientIdentity | null> {\n const registry = await loadRegistry()\n return registry.clientIdentities[identityKey] ?? null\n}\n\nexport async function getAccountClientIdentityByLoginAndApp(\n login: string,\n oauthApp: string,\n): Promise<AccountClientIdentity | null> {\n const registry = await loadRegistry()\n\n const candidates = Object.values(registry.clientIdentities).filter(\n (identity): identity is AccountClientIdentity =>\n identity !== undefined\n && identity.login === login\n && identity.oauthApp === oauthApp,\n )\n\n const preferredCandidates = candidates.filter(\n (identity) =>\n identity.enterpriseDomain !== DEFAULT_IDENTITY_ENTERPRISE_DOMAIN,\n )\n const selectionPool =\n preferredCandidates.length > 0 ? preferredCandidates : candidates\n\n return selectionPool.reduce<AccountClientIdentity | null>(\n (latest, current) => {\n if (!latest || current.createdAt > latest.createdAt) return current\n return latest\n },\n null,\n )\n}\n\nexport async function ensureAccountClientIdentity({\n login,\n oauthApp,\n enterpriseDomain,\n}: {\n login: string\n oauthApp: string\n enterpriseDomain: string\n}): Promise<AccountClientIdentity> {\n assertValidAccountId(login)\n\n const normalizedOauthApp = oauthApp.trim()\n if (!normalizedOauthApp) {\n throw new Error(\"OAuth app namespace must not be empty\")\n }\n\n const normalizedEnterpriseDomain = enterpriseDomain.trim()\n if (!normalizedEnterpriseDomain) {\n throw new Error(\"Enterprise domain namespace must not be empty\")\n }\n\n const identityKey = buildIdentityKey({\n login,\n oauthApp: normalizedOauthApp,\n enterpriseDomain: normalizedEnterpriseDomain,\n })\n const existingLock = identityLocks.get(identityKey)\n if (existingLock) {\n return existingLock\n }\n\n const identityPromise = runWithRegistryLock(\n async (): Promise<AccountClientIdentity> => {\n const { registry, shouldPersist } = await loadRegistrySnapshot()\n const existing = registry.clientIdentities[identityKey]\n if (existing) {\n if (shouldPersist) {\n await saveRegistryUnlocked(registry)\n }\n return existing\n }\n\n const created = createClientIdentity({\n login,\n oauthApp: normalizedOauthApp,\n enterpriseDomain: normalizedEnterpriseDomain,\n })\n registry.clientIdentities[identityKey] = created\n await saveRegistryUnlocked(registry)\n return created\n },\n )\n\n identityLocks.set(identityKey, identityPromise)\n\n try {\n return await identityPromise\n } finally {\n if (identityLocks.get(identityKey) === identityPromise) {\n identityLocks.delete(identityKey)\n }\n }\n}\n\n/**\n * Add an account to the registry.\n * The account is appended to the end of the list (lowest priority).\n */\nexport async function addAccountToRegistry(meta: AccountMeta): Promise<void> {\n assertValidAccountId(meta.id)\n\n await runWithRegistryLock(async () => {\n const { registry } = await loadRegistrySnapshot()\n\n // Check for duplicate\n if (registry.accounts.some((a) => a.id === meta.id)) {\n throw new Error(`Account already exists: ${meta.id}`)\n }\n\n registry.accounts.push(meta)\n const { oauthApp, enterpriseDomain } = getCurrentIdentityEnvironment()\n ensureRegistryIdentity(registry, {\n login: meta.id,\n oauthApp,\n enterpriseDomain,\n })\n await saveRegistryUnlocked(registry)\n })\n}\n\n/**\n * Remove an account from the registry by ID or index (1-based).\n * Returns the removed account metadata.\n */\nexport async function removeAccountFromRegistry(\n idOrIndex: string | number,\n): Promise<AccountMeta> {\n return runWithRegistryLock(async () => {\n const { registry } = await loadRegistrySnapshot()\n let index: number\n\n if (typeof idOrIndex === \"number\") {\n // 1-based index\n index = idOrIndex - 1\n if (index < 0 || index >= registry.accounts.length) {\n throw new Error(`Invalid account index: ${idOrIndex}`)\n }\n } else {\n index = registry.accounts.findIndex((a) => a.id === idOrIndex)\n if (index === -1) {\n throw new Error(`Account not found: ${idOrIndex}`)\n }\n }\n\n const [removed] = registry.accounts.splice(index, 1)\n await saveRegistryUnlocked(registry)\n return removed\n })\n}\n\n/**\n * List all accounts from the registry.\n */\nexport async function listAccountsFromRegistry(): Promise<Array<AccountMeta>> {\n const registry = await loadRegistry()\n return registry.accounts\n}\n\n/**\n * Load the GitHub token for a specific account.\n * Returns null if the token file doesn't exist.\n */\nexport async function loadAccountToken(id: string): Promise<string | null> {\n assertValidAccountId(id)\n\n try {\n const tokenPath = accountTokenPath(id)\n const token = await fs.readFile(tokenPath, \"utf8\")\n return token.trim() || null\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null\n }\n throw error\n }\n}\n\n/**\n * Save the GitHub token for a specific account with secure permissions.\n */\nexport async function saveAccountToken(\n id: string,\n token: string,\n): Promise<void> {\n assertValidAccountId(id)\n\n const tokenPath = accountTokenPath(id)\n await fs.writeFile(tokenPath, token, { mode: 0o600 })\n}\n\n/**\n * Remove the GitHub token file for a specific account.\n */\nexport async function removeAccountToken(id: string): Promise<void> {\n assertValidAccountId(id)\n\n const tokenPath = accountTokenPath(id)\n try {\n await fs.unlink(tokenPath)\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw error\n }\n // File doesn't exist, nothing to remove\n }\n}\n\n/**\n * Check if the legacy github_token file exists.\n */\nexport async function hasLegacyToken(): Promise<boolean> {\n try {\n const content = await fs.readFile(PATHS.GITHUB_TOKEN_PATH, \"utf8\")\n return content.trim().length > 0\n } catch {\n return false\n }\n}\n\n/**\n * Read the legacy github_token file.\n * Returns null if the file doesn't exist or is empty.\n */\nexport async function readLegacyToken(): Promise<string | null> {\n try {\n const content = await fs.readFile(PATHS.GITHUB_TOKEN_PATH, \"utf8\")\n return content.trim() || null\n } catch {\n return null\n }\n}\n\n/**\n * Check if the registry file exists and has accounts.\n */\nexport async function hasRegistry(): Promise<boolean> {\n const registry = await loadRegistry()\n return registry.accounts.length > 0\n}\n","import type { ModelsResponse } from \"~/services/copilot/get-models\"\n\n/**\n * Account type for GitHub Copilot subscription.\n */\nexport type AccountType = \"individual\" | \"business\" | \"enterprise\"\n\nexport const ACCOUNT_TYPE_VALUES: ReadonlyArray<AccountType> = [\n \"individual\",\n \"business\",\n \"enterprise\",\n]\n\nexport function isAccountType(value: unknown): value is AccountType {\n return (\n typeof value === \"string\"\n && (ACCOUNT_TYPE_VALUES as ReadonlyArray<string>).includes(value)\n )\n}\n\nexport function parseAccountType(value: unknown): AccountType {\n if (!isAccountType(value)) {\n throw new Error(\n `Invalid account type: ${String(value)}. Valid values: ${ACCOUNT_TYPE_VALUES.join(\n \", \",\n )}`,\n )\n }\n return value\n}\n\n/**\n * Metadata for a registered account, stored in the registry file.\n */\nexport interface AccountMeta {\n /** GitHub login */\n id: string\n /** Account subscription type */\n accountType: AccountType\n /** Timestamp when the account was added */\n addedAt: number\n /** Whether the account is enabled for request routing (undefined = true) */\n enabled?: boolean\n}\n\nexport interface AccountClientIdentity {\n /** Real GitHub login */\n login: string\n /** OAuth app namespace */\n oauthApp: string\n /** Enterprise domain namespace (\"public\" for github.com) */\n enterpriseDomain: string\n /** Account-scoped upstream device identifier */\n deviceId: string\n /** Account-scoped upstream machine identifier */\n machineId: string\n /** Creation timestamp for debugging/auditing */\n createdAt: number\n}\n\n/**\n * Registry file structure for storing account metadata.\n */\nexport interface AccountRegistry {\n /** Schema version for future migrations */\n version: 2\n /** Ordered list of accounts (order = priority) */\n accounts: Array<AccountMeta>\n /** Persistent client identities keyed by logical environment + login */\n clientIdentities: Partial<Record<string, AccountClientIdentity>>\n}\n\n/**\n * Runtime state for an account, including tokens and quota information.\n */\nexport interface AccountRuntime extends AccountMeta {\n /** Real GitHub login, used to resolve account-scoped identity */\n accountLogin?: string\n /** Persistent identity key used to load/store account-scoped identifiers */\n identityKey?: string\n /** GitHub personal access token */\n githubToken: string\n /** Copilot API token (obtained from GitHub) */\n copilotToken?: string\n /** Account-specific Copilot API base URL returned by GitHub */\n copilotApiUrl?: string\n /** VS Code version for API headers */\n vsCodeVersion?: string\n /** Account-scoped device identifier sent upstream */\n clientDeviceId?: string\n /** Account-scoped machine identifier sent upstream */\n clientMachineId?: string\n /** Account-scoped session identifier sent upstream */\n clientSessionId?: string\n /** Session refresh timer reference */\n sessionRefreshTimer?: ReturnType<typeof setTimeout>\n /** Cached available models for this account */\n models?: ModelsResponse\n /** Timestamp of last models fetch */\n lastModelsFetch?: number\n /** Whether models refresh is in progress */\n isRefreshingModels?: boolean\n /** Promise for an in-flight models refresh */\n modelsRefreshPromise?: Promise<void>\n /** Total premium interactions quota entitlement */\n premiumEntitlement?: number\n /** Remaining premium interactions quota */\n premiumRemaining?: number\n /** Reserved premium interaction units for in-flight requests */\n premiumReserved?: number\n /** Internal reservation map for idempotent release */\n premiumReservations?: Map<symbol, number>\n /** Whether this account has unlimited quota */\n unlimited?: boolean\n /** Whether this account allows overage billing (enterprise feature) */\n overagePermitted?: boolean\n /** Timestamp of last quota fetch */\n lastQuotaFetch?: number\n /** Token refresh timer reference */\n refreshTimer?: ReturnType<typeof setInterval>\n /** Whether this account has failed (e.g., 401 error) */\n failed?: boolean\n /** Failure reason if failed */\n failureReason?: string\n /** Whether quota refresh is in progress (prevents concurrent refreshes) */\n isRefreshingQuota?: boolean\n /** Promise for an in-flight quota refresh (allows concurrent callers to await the same refresh) */\n quotaRefreshPromise?: Promise<void>\n}\n\n/**\n * Context required for making API calls on behalf of an account.\n * This is a subset of AccountRuntime used by service functions.\n */\nexport interface AccountContext {\n /** Real GitHub login */\n accountLogin?: string\n /** GitHub personal access token */\n githubToken: string\n /** Copilot API token */\n copilotToken?: string\n /** Account-specific Copilot API base URL */\n copilotApiUrl?: string\n /** Account subscription type */\n accountType: AccountType\n /** VS Code version for API headers */\n vsCodeVersion?: string\n /** Account-scoped device identifier */\n clientDeviceId?: string\n /** Account-scoped machine identifier */\n clientMachineId?: string\n /** Account-scoped session identifier */\n clientSessionId?: string\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"account-DpW8RaT6.js","names":[],"sources":["../src/lib/account-client-identity.ts","../src/lib/accounts-registry.ts","../src/lib/types/account.ts"],"sourcesContent":["import { createHash, randomUUID } from \"node:crypto\"\n\nimport { normalizeDomain } from \"./api-config\"\n\nexport const DEFAULT_IDENTITY_OAUTH_APP = \"default\"\nexport const DEFAULT_IDENTITY_ENTERPRISE_DOMAIN = \"public\"\n\nexport interface AccountIdentityEnvironment {\n oauthApp: string\n enterpriseDomain: string\n}\n\nexport const getCurrentIdentityEnvironment = (): AccountIdentityEnvironment => {\n const rawOauthApp = process.env.COPILOT_API_OAUTH_APP?.trim().toLowerCase()\n const rawEnterpriseDomain = process.env.COPILOT_API_ENTERPRISE_URL?.trim()\n\n return {\n oauthApp: rawOauthApp || DEFAULT_IDENTITY_OAUTH_APP,\n enterpriseDomain:\n rawEnterpriseDomain ?\n normalizeDomain(rawEnterpriseDomain).toLowerCase()\n : DEFAULT_IDENTITY_ENTERPRISE_DOMAIN,\n }\n}\n\nexport const buildIdentityKey = ({\n login,\n oauthApp,\n enterpriseDomain,\n}: {\n login: string\n oauthApp: string\n enterpriseDomain: string\n}): string => `${enterpriseDomain}:${oauthApp}:${login}`\n\nexport const createAccountDeviceId = (): string => randomUUID().toLowerCase()\n\nexport const createAccountMachineId = (): string =>\n createHash(\"sha256\").update(randomUUID(), \"utf8\").digest(\"hex\")\n\nexport const createAccountSessionId = (): string =>\n randomUUID() + Date.now().toString()\n","import fs from \"node:fs/promises\"\nimport { z } from \"zod\"\n\nimport type {\n AccountClientIdentity,\n AccountMeta,\n AccountRegistry,\n} from \"~/lib/types/account\"\n\nimport {\n DEFAULT_IDENTITY_ENTERPRISE_DOMAIN,\n buildIdentityKey,\n createAccountDeviceId,\n createAccountMachineId,\n getCurrentIdentityEnvironment,\n} from \"~/lib/account-client-identity\"\nimport { accountTokenPath, PATHS } from \"~/lib/paths\"\n\n/**\n * Validate account ID (GitHub login).\n * Rules:\n * - 1-39 chars\n * - Alphanumeric segments may be separated by single hyphens or underscores\n * - Cannot begin or end with a separator\n * - No consecutive separators\n */\nexport function validateAccountId(id: string): boolean {\n if (id.length === 0 || id.length > 39) return false\n return /^[A-Za-z0-9]+(?:[-_][A-Za-z0-9]+)*$/u.test(id)\n}\n\nfunction assertValidAccountId(id: string): void {\n if (!validateAccountId(id)) {\n throw new Error(`Invalid account ID: ${id}`)\n }\n}\n\nconst ACCOUNT_ID_VALIDATION_RULES =\n \"1-39 chars, alphanumeric with optional single hyphen/underscore separators, no leading/trailing separator, no consecutive separators.\"\n\nconst accountMetaSchema = z.object({\n id: z.string().refine(validateAccountId, {\n message: `Invalid account id. Expected a GitHub login (${ACCOUNT_ID_VALIDATION_RULES})`,\n }),\n accountType: z.enum([\"individual\", \"business\", \"enterprise\"]),\n addedAt: z.number(),\n enabled: z.boolean().optional(),\n})\n\n/**\n * Check whether an account is enabled for request routing.\n * Treats `undefined` and `true` as enabled (backward compatible).\n */\nexport function isAccountEnabled(meta: AccountMeta): boolean {\n return meta.enabled !== false\n}\n\nconst accountClientIdentitySchema = z.object({\n login: z.string().refine(validateAccountId, {\n message: `Invalid client identity login. Expected a GitHub login (${ACCOUNT_ID_VALIDATION_RULES})`,\n }),\n oauthApp: z.string().min(1),\n enterpriseDomain: z.string().min(1),\n deviceId: z\n .string()\n .regex(\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u,\n \"Invalid device ID format. Expected a lowercase UUID.\",\n ),\n machineId: z\n .string()\n .regex(\n /^[0-9a-f]{64}$/u,\n \"Invalid machine ID format. Expected 64 lowercase hexadecimal characters.\",\n ),\n createdAt: z.number(),\n})\n\nconst accountRegistryV1Schema = z.object({\n version: z.literal(1),\n accounts: z.array(accountMetaSchema),\n})\n\nconst accountRegistryV2Schema = z.object({\n version: z.literal(2),\n accounts: z.array(accountMetaSchema),\n clientIdentities: z.record(z.string(), accountClientIdentitySchema),\n})\n\nconst identityLocks = new Map<string, Promise<AccountClientIdentity>>()\nlet registryLock: Promise<void> = Promise.resolve()\n\nconst runWithRegistryLock = async <T>(\n operation: () => Promise<T>,\n): Promise<T> => {\n const previousLock = registryLock\n let releaseLock!: () => void\n registryLock = new Promise<void>((resolve) => {\n releaseLock = resolve\n })\n\n await previousLock\n\n try {\n return await operation()\n } finally {\n releaseLock()\n }\n}\n\n/**\n * Create an empty registry with the current schema version.\n */\nfunction createEmptyRegistry(): AccountRegistry {\n return {\n version: 2,\n accounts: [],\n clientIdentities: {},\n }\n}\n\nconst createClientIdentity = ({\n login,\n oauthApp,\n enterpriseDomain,\n}: {\n login: string\n oauthApp: string\n enterpriseDomain: string\n}): AccountClientIdentity => ({\n login,\n oauthApp,\n enterpriseDomain,\n deviceId: createAccountDeviceId(),\n machineId: createAccountMachineId(),\n createdAt: Date.now(),\n})\n\nconst ensureRegistryIdentity = (\n registry: AccountRegistry,\n {\n login,\n oauthApp,\n enterpriseDomain,\n }: {\n login: string\n oauthApp: string\n enterpriseDomain: string\n },\n): AccountClientIdentity => {\n const identityKey = buildIdentityKey({ login, oauthApp, enterpriseDomain })\n const existing = registry.clientIdentities[identityKey]\n if (existing) {\n return existing\n }\n\n const created = createClientIdentity({\n login,\n oauthApp,\n enterpriseDomain,\n })\n registry.clientIdentities[identityKey] = created\n return created\n}\n\nconst ensureClientIdentitiesForAccounts = (\n registry: AccountRegistry,\n): boolean => {\n const { oauthApp, enterpriseDomain } = getCurrentIdentityEnvironment()\n const countBefore = Object.keys(registry.clientIdentities).length\n\n for (const account of registry.accounts) {\n ensureRegistryIdentity(registry, {\n login: account.id,\n oauthApp,\n enterpriseDomain,\n })\n }\n\n return Object.keys(registry.clientIdentities).length !== countBefore\n}\n\nconst assertNoDuplicateAccounts = (registry: {\n accounts: Array<AccountMeta>\n}) => {\n const seen = new Set<string>()\n for (const account of registry.accounts) {\n if (seen.has(account.id)) {\n throw new Error(\n `Invalid accounts registry at ${PATHS.ACCOUNTS_REGISTRY_PATH}: duplicate account id \"${account.id}\"`,\n )\n }\n seen.add(account.id)\n }\n}\n\nconst loadRegistrySnapshot = async (): Promise<{\n registry: AccountRegistry\n shouldPersist: boolean\n}> => {\n try {\n const content = await fs.readFile(PATHS.ACCOUNTS_REGISTRY_PATH, \"utf8\")\n if (!content.trim()) {\n return {\n registry: createEmptyRegistry(),\n shouldPersist: false,\n }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(content) as unknown\n } catch (error) {\n throw new Error(\n `Invalid accounts registry JSON at ${PATHS.ACCOUNTS_REGISTRY_PATH}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n )\n }\n\n const isVersion2Record =\n typeof parsed === \"object\"\n && parsed !== null\n && \"version\" in parsed\n && parsed.version === 2\n const result =\n isVersion2Record ?\n accountRegistryV2Schema.safeParse(parsed)\n : accountRegistryV1Schema.safeParse(parsed)\n if (!result.success) {\n const issues = result.error.issues\n .map((issue) => `${issue.path.join(\".\")}: ${issue.message}`)\n .join(\"; \")\n\n throw new Error(\n `Invalid accounts registry at ${PATHS.ACCOUNTS_REGISTRY_PATH}: ${issues}`,\n )\n }\n\n const parsedRegistry = result.data\n const registry: AccountRegistry =\n parsedRegistry.version === 2 ?\n parsedRegistry\n : {\n version: 2,\n accounts: parsedRegistry.accounts,\n clientIdentities: {},\n }\n\n assertNoDuplicateAccounts(registry)\n\n const identitiesBackfilled = ensureClientIdentitiesForAccounts(registry)\n\n return {\n registry,\n shouldPersist: parsedRegistry.version !== 2 || identitiesBackfilled,\n }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return {\n registry: createEmptyRegistry(),\n shouldPersist: false,\n }\n }\n throw error\n }\n}\n\nconst saveRegistryUnlocked = async (\n registry: AccountRegistry,\n): Promise<void> => {\n const content = JSON.stringify(registry, null, 2)\n await fs.writeFile(PATHS.ACCOUNTS_REGISTRY_PATH, content, { mode: 0o600 })\n}\n\n/**\n * Load the accounts registry from disk.\n * Returns an empty registry if the file doesn't exist.\n */\nexport async function loadRegistry(): Promise<AccountRegistry> {\n return runWithRegistryLock(async () => {\n const { registry, shouldPersist } = await loadRegistrySnapshot()\n if (shouldPersist) {\n await saveRegistryUnlocked(registry)\n }\n return registry\n })\n}\n\n/**\n * Save the accounts registry to disk with secure permissions.\n */\nexport async function saveRegistry(registry: AccountRegistry): Promise<void> {\n await runWithRegistryLock(async () => {\n await saveRegistryUnlocked(registry)\n })\n}\n\nexport async function getAccountClientIdentity(\n identityKey: string,\n): Promise<AccountClientIdentity | null> {\n const registry = await loadRegistry()\n return registry.clientIdentities[identityKey] ?? null\n}\n\nexport async function getAccountClientIdentityByLoginAndApp(\n login: string,\n oauthApp: string,\n): Promise<AccountClientIdentity | null> {\n const registry = await loadRegistry()\n\n const candidates = Object.values(registry.clientIdentities).filter(\n (identity): identity is AccountClientIdentity =>\n identity !== undefined\n && identity.login === login\n && identity.oauthApp === oauthApp,\n )\n\n const preferredCandidates = candidates.filter(\n (identity) =>\n identity.enterpriseDomain !== DEFAULT_IDENTITY_ENTERPRISE_DOMAIN,\n )\n const selectionPool =\n preferredCandidates.length > 0 ? preferredCandidates : candidates\n\n return selectionPool.reduce<AccountClientIdentity | null>(\n (latest, current) => {\n if (!latest || current.createdAt > latest.createdAt) return current\n return latest\n },\n null,\n )\n}\n\nexport async function ensureAccountClientIdentity({\n login,\n oauthApp,\n enterpriseDomain,\n}: {\n login: string\n oauthApp: string\n enterpriseDomain: string\n}): Promise<AccountClientIdentity> {\n assertValidAccountId(login)\n\n const normalizedOauthApp = oauthApp.trim()\n if (!normalizedOauthApp) {\n throw new Error(\"OAuth app namespace must not be empty\")\n }\n\n const normalizedEnterpriseDomain = enterpriseDomain.trim()\n if (!normalizedEnterpriseDomain) {\n throw new Error(\"Enterprise domain namespace must not be empty\")\n }\n\n const identityKey = buildIdentityKey({\n login,\n oauthApp: normalizedOauthApp,\n enterpriseDomain: normalizedEnterpriseDomain,\n })\n const existingLock = identityLocks.get(identityKey)\n if (existingLock) {\n return existingLock\n }\n\n const identityPromise = runWithRegistryLock(\n async (): Promise<AccountClientIdentity> => {\n const { registry, shouldPersist } = await loadRegistrySnapshot()\n const existing = registry.clientIdentities[identityKey]\n if (existing) {\n if (shouldPersist) {\n await saveRegistryUnlocked(registry)\n }\n return existing\n }\n\n const created = createClientIdentity({\n login,\n oauthApp: normalizedOauthApp,\n enterpriseDomain: normalizedEnterpriseDomain,\n })\n registry.clientIdentities[identityKey] = created\n await saveRegistryUnlocked(registry)\n return created\n },\n )\n\n identityLocks.set(identityKey, identityPromise)\n\n try {\n return await identityPromise\n } finally {\n if (identityLocks.get(identityKey) === identityPromise) {\n identityLocks.delete(identityKey)\n }\n }\n}\n\n/**\n * Add an account to the registry.\n * The account is appended to the end of the list (lowest priority).\n */\nexport async function addAccountToRegistry(meta: AccountMeta): Promise<void> {\n assertValidAccountId(meta.id)\n\n await runWithRegistryLock(async () => {\n const { registry } = await loadRegistrySnapshot()\n\n // Check for duplicate\n if (registry.accounts.some((a) => a.id === meta.id)) {\n throw new Error(`Account already exists: ${meta.id}`)\n }\n\n registry.accounts.push(meta)\n const { oauthApp, enterpriseDomain } = getCurrentIdentityEnvironment()\n ensureRegistryIdentity(registry, {\n login: meta.id,\n oauthApp,\n enterpriseDomain,\n })\n await saveRegistryUnlocked(registry)\n })\n}\n\n/**\n * Remove an account from the registry by ID or index (1-based).\n * Returns the removed account metadata.\n */\nexport async function removeAccountFromRegistry(\n idOrIndex: string | number,\n): Promise<AccountMeta> {\n return runWithRegistryLock(async () => {\n const { registry } = await loadRegistrySnapshot()\n let index: number\n\n if (typeof idOrIndex === \"number\") {\n // 1-based index\n index = idOrIndex - 1\n if (index < 0 || index >= registry.accounts.length) {\n throw new Error(`Invalid account index: ${idOrIndex}`)\n }\n } else {\n index = registry.accounts.findIndex((a) => a.id === idOrIndex)\n if (index === -1) {\n throw new Error(`Account not found: ${idOrIndex}`)\n }\n }\n\n const [removed] = registry.accounts.splice(index, 1)\n await saveRegistryUnlocked(registry)\n return removed\n })\n}\n\n/**\n * List all accounts from the registry.\n */\nexport async function listAccountsFromRegistry(): Promise<Array<AccountMeta>> {\n const registry = await loadRegistry()\n return registry.accounts\n}\n\n/**\n * Load the GitHub token for a specific account.\n * Returns null if the token file doesn't exist.\n */\nexport async function loadAccountToken(id: string): Promise<string | null> {\n assertValidAccountId(id)\n\n try {\n const tokenPath = accountTokenPath(id)\n const token = await fs.readFile(tokenPath, \"utf8\")\n return token.trim() || null\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null\n }\n throw error\n }\n}\n\n/**\n * Save the GitHub token for a specific account with secure permissions.\n */\nexport async function saveAccountToken(\n id: string,\n token: string,\n): Promise<void> {\n assertValidAccountId(id)\n\n const tokenPath = accountTokenPath(id)\n await fs.writeFile(tokenPath, token, { mode: 0o600 })\n}\n\n/**\n * Remove the GitHub token file for a specific account.\n */\nexport async function removeAccountToken(id: string): Promise<void> {\n assertValidAccountId(id)\n\n const tokenPath = accountTokenPath(id)\n try {\n await fs.unlink(tokenPath)\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw error\n }\n // File doesn't exist, nothing to remove\n }\n}\n\n/**\n * Check if the legacy github_token file exists.\n */\nexport async function hasLegacyToken(): Promise<boolean> {\n try {\n const content = await fs.readFile(PATHS.GITHUB_TOKEN_PATH, \"utf8\")\n return content.trim().length > 0\n } catch {\n return false\n }\n}\n\n/**\n * Read the legacy github_token file.\n * Returns null if the file doesn't exist or is empty.\n */\nexport async function readLegacyToken(): Promise<string | null> {\n try {\n const content = await fs.readFile(PATHS.GITHUB_TOKEN_PATH, \"utf8\")\n return content.trim() || null\n } catch {\n return null\n }\n}\n\n/**\n * Check if the registry file exists and has accounts.\n */\nexport async function hasRegistry(): Promise<boolean> {\n const registry = await loadRegistry()\n return registry.accounts.length > 0\n}\n","import type { ModelsResponse } from \"~/services/copilot/get-models\"\n\n/**\n * Account type for GitHub Copilot subscription.\n */\nexport type AccountType = \"individual\" | \"business\" | \"enterprise\"\n\nexport const ACCOUNT_TYPE_VALUES: ReadonlyArray<AccountType> = [\n \"individual\",\n \"business\",\n \"enterprise\",\n]\n\nexport function isAccountType(value: unknown): value is AccountType {\n return (\n typeof value === \"string\"\n && (ACCOUNT_TYPE_VALUES as ReadonlyArray<string>).includes(value)\n )\n}\n\nexport function parseAccountType(value: unknown): AccountType {\n if (!isAccountType(value)) {\n throw new Error(\n `Invalid account type: ${String(value)}. Valid values: ${ACCOUNT_TYPE_VALUES.join(\n \", \",\n )}`,\n )\n }\n return value\n}\n\n/**\n * Metadata for a registered account, stored in the registry file.\n */\nexport interface AccountMeta {\n /** GitHub login */\n id: string\n /** Account subscription type */\n accountType: AccountType\n /** Timestamp when the account was added */\n addedAt: number\n /** Whether the account is enabled for request routing (undefined = true) */\n enabled?: boolean\n}\n\nexport interface AccountClientIdentity {\n /** Real GitHub login */\n login: string\n /** OAuth app namespace */\n oauthApp: string\n /** Enterprise domain namespace (\"public\" for github.com) */\n enterpriseDomain: string\n /** Account-scoped upstream device identifier */\n deviceId: string\n /** Account-scoped upstream machine identifier */\n machineId: string\n /** Creation timestamp for debugging/auditing */\n createdAt: number\n}\n\n/**\n * Registry file structure for storing account metadata.\n */\nexport interface AccountRegistry {\n /** Schema version for future migrations */\n version: 2\n /** Ordered list of accounts (order = priority) */\n accounts: Array<AccountMeta>\n /** Persistent client identities keyed by logical environment + login */\n clientIdentities: Partial<Record<string, AccountClientIdentity>>\n}\n\n/**\n * Runtime state for an account, including tokens and quota information.\n */\nexport interface AccountRuntime extends AccountMeta {\n /** Real GitHub login, used to resolve account-scoped identity */\n accountLogin?: string\n /** Persistent identity key used to load/store account-scoped identifiers */\n identityKey?: string\n /** GitHub personal access token */\n githubToken: string\n /** Copilot API token (obtained from GitHub) */\n copilotToken?: string\n /** Account-specific Copilot API base URL returned by GitHub */\n copilotApiUrl?: string\n /** VS Code version for API headers */\n vsCodeVersion?: string\n /** Account-scoped device identifier sent upstream */\n clientDeviceId?: string\n /** Account-scoped machine identifier sent upstream */\n clientMachineId?: string\n /** Account-scoped session identifier sent upstream */\n clientSessionId?: string\n /** Session refresh timer reference */\n sessionRefreshTimer?: ReturnType<typeof setTimeout>\n /** Cached available models for this account */\n models?: ModelsResponse\n /** Timestamp of last models fetch */\n lastModelsFetch?: number\n /** Whether models refresh is in progress */\n isRefreshingModels?: boolean\n /** Promise for an in-flight models refresh */\n modelsRefreshPromise?: Promise<void>\n /** Total premium interactions quota entitlement */\n premiumEntitlement?: number\n /** Remaining premium interactions quota */\n premiumRemaining?: number\n /** Reserved premium interaction units for in-flight requests */\n premiumReserved?: number\n /** Internal reservation map for idempotent release */\n premiumReservations?: Map<symbol, number>\n /** Whether this account has unlimited quota */\n unlimited?: boolean\n /** Whether this account allows overage billing (enterprise feature) */\n overagePermitted?: boolean\n /** Timestamp of last quota fetch */\n lastQuotaFetch?: number\n /** Token refresh timer reference */\n refreshTimer?: ReturnType<typeof setInterval>\n /** Whether this account has failed (e.g., 401 error) */\n failed?: boolean\n /** Failure reason if failed */\n failureReason?: string\n /** Whether quota refresh is in progress (prevents concurrent refreshes) */\n isRefreshingQuota?: boolean\n /** Promise for an in-flight quota refresh (allows concurrent callers to await the same refresh) */\n quotaRefreshPromise?: Promise<void>\n}\n\n/**\n * Context required for making API calls on behalf of an account.\n * This is a subset of AccountRuntime used by service functions.\n */\nexport interface AccountContext {\n /** Real GitHub login */\n accountLogin?: string\n /** GitHub personal access token */\n githubToken: string\n /** Copilot API token */\n copilotToken?: string\n /** Account-specific Copilot API base URL */\n copilotApiUrl?: string\n /** Account subscription type */\n accountType: AccountType\n /** VS Code version for API headers */\n vsCodeVersion?: string\n /** Account-scoped device identifier */\n clientDeviceId?: string\n /** Account-scoped machine identifier */\n clientMachineId?: string\n /** Account-scoped session identifier */\n clientSessionId?: string\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-05-02 | Updated: 2026-05-02 -->
|
|
3
|
+
|
|
4
|
+
# public
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Static assets served directly by Vite for the admin SPA.
|
|
8
|
+
|
|
9
|
+
## Key Files
|
|
10
|
+
|
|
11
|
+
| File | Description |
|
|
12
|
+
|------|-------------|
|
|
13
|
+
| (varies) | Static assets like favicon, manifest, etc. |
|
|
14
|
+
|
|
15
|
+
## For AI Agents
|
|
16
|
+
|
|
17
|
+
### Working In This Directory
|
|
18
|
+
- Files here are copied verbatim to the build output
|
|
19
|
+
- No transformation or bundling applied by Vite
|