@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.
Files changed (55) hide show
  1. package/README.md +140 -45
  2. package/README.zh-CN.md +140 -45
  3. package/dist/{account-COtMmvzU.js → account-DpW8RaT6.js} +3 -3
  4. package/dist/{account-COtMmvzU.js.map → account-DpW8RaT6.js.map} +1 -1
  5. package/dist/admin/AGENTS.md +19 -0
  6. package/dist/auth-nO-eHeO_.js +327 -0
  7. package/dist/auth-nO-eHeO_.js.map +1 -0
  8. package/dist/{check-usage-DdevqHE5.js → check-usage-ZifYvA3w.js} +4 -42
  9. package/dist/check-usage-ZifYvA3w.js.map +1 -0
  10. package/dist/config-CmhIPHn_.js +578 -0
  11. package/dist/config-CmhIPHn_.js.map +1 -0
  12. package/dist/{debug-BMo6ltbp.js → debug-DvpksqEL.js} +18 -7
  13. package/dist/debug-DvpksqEL.js.map +1 -0
  14. package/dist/main.js +5 -10
  15. package/dist/main.js.map +1 -1
  16. package/dist/mcp-http-BhELuvog.js +2 -0
  17. package/dist/mcp-http-DI4Vz01p.js +82 -0
  18. package/dist/mcp-http-DI4Vz01p.js.map +1 -0
  19. package/dist/mcp-http-config-DMdUDz1D.js +39 -0
  20. package/dist/mcp-http-config-DMdUDz1D.js.map +1 -0
  21. package/dist/mcp-pLTPS0tO.js +79 -0
  22. package/dist/mcp-pLTPS0tO.js.map +1 -0
  23. package/dist/{tool-search-BrN7M0Dd.js → mcp-server-DEqHrXFq.js} +25 -2
  24. package/dist/mcp-server-DEqHrXFq.js.map +1 -0
  25. package/dist/{paths-CclKwouX.js → paths-Bpsb62LK.js} +3 -1
  26. package/dist/paths-Bpsb62LK.js.map +1 -0
  27. package/dist/{poll-access-token-BAgM2-7k.js → poll-access-token-GzVkiTH8.js} +71 -4
  28. package/dist/poll-access-token-GzVkiTH8.js.map +1 -0
  29. package/dist/{request-outbound-BJjWS_jF.js → request-outbound-BkEA8Wgb.js} +1 -1
  30. package/dist/{request-outbound-Pu1kp2x8.js → request-outbound-DZTxxtcx.js} +3 -3
  31. package/dist/{request-outbound-Pu1kp2x8.js.map → request-outbound-DZTxxtcx.js.map} +1 -1
  32. package/dist/{proxy-_U-hgwIn.js → responses-bridge-registry-BJ5Sbh6-.js} +116 -577
  33. package/dist/responses-bridge-registry-BJ5Sbh6-.js.map +1 -0
  34. package/dist/{server-GxNB5Syq.js → server-DJ3_UGc4.js} +313 -165
  35. package/dist/server-DJ3_UGc4.js.map +1 -0
  36. package/dist/start-DaB0AcjZ.js +526 -0
  37. package/dist/start-DaB0AcjZ.js.map +1 -0
  38. package/dist/token-DrFDLVxa.js +365 -0
  39. package/dist/token-DrFDLVxa.js.map +1 -0
  40. package/package.json +1 -1
  41. package/dist/auth-B0y-2njL.js +0 -226
  42. package/dist/auth-B0y-2njL.js.map +0 -1
  43. package/dist/check-usage-DdevqHE5.js.map +0 -1
  44. package/dist/debug-BMo6ltbp.js.map +0 -1
  45. package/dist/get-copilot-token-8Rm-rVsp.js +0 -17
  46. package/dist/get-copilot-token-8Rm-rVsp.js.map +0 -1
  47. package/dist/mcp-9Hgepkc5.js +0 -37
  48. package/dist/mcp-9Hgepkc5.js.map +0 -1
  49. package/dist/paths-CclKwouX.js.map +0 -1
  50. package/dist/poll-access-token-BAgM2-7k.js.map +0 -1
  51. package/dist/proxy-_U-hgwIn.js.map +0 -1
  52. package/dist/server-GxNB5Syq.js.map +0 -1
  53. package/dist/start-DdrurmQ3.js +0 -274
  54. package/dist/start-DdrurmQ3.js.map +0 -1
  55. 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
- > npx @nick3/copilot-api@latest --oauth-app=opencode start
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
- - 如果要通过 `npx` 运行已发布 CLI,需要 Node.js
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
- ## 通过 npx 使用
154
+ ## 通过 Bun 使用已发布 CLI
154
155
 
155
- 你可以直接用 npx 运行本项目:
156
+ 服务端和账号管理命令是 Bun-only,因为 Admin UI 与请求历史使用 `bun:sqlite`。运行已发布 CLI 时请使用 Bun,避免 `#!/usr/bin/env node` shebang 强制走 Node.js:
156
157
 
157
158
  ```sh
158
- npx @nick3/copilot-api@latest start
159
+ bunx --bun @nick3/copilot-api@latest start
159
160
  ```
160
161
 
161
162
  带参数示例:
162
163
 
163
164
  ```sh
164
- npx @nick3/copilot-api@latest start --port 8080
165
+ bunx --bun @nick3/copilot-api@latest start --port 8080
165
166
  ```
166
167
 
167
168
  如果只想做认证:
168
169
 
169
170
  ```sh
170
- npx @nick3/copilot-api@latest auth
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`(默认)时,Responses API 请求会对声明了 `ws:/responses` 的模型使用 Copilot WebSocket transport;仅声明 `/responses` 的模型仍走 HTTP。设为 `false` 可禁用 WebSocket 路由。
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
- 通过 npx 使用:
601
+ 通过 Bun 使用已发布 CLI:
584
602
 
585
603
  ```sh
586
604
  # 基础启动
587
- npx @nick3/copilot-api@latest start
605
+ bunx --bun @nick3/copilot-api@latest start
588
606
 
589
607
  # 自定义端口并开启详细日志
590
- npx @nick3/copilot-api@latest start --port 8080 --verbose
608
+ bunx --bun @nick3/copilot-api@latest start --port 8080 --verbose
591
609
 
592
610
  # 使用 GitHub Business 方案账号
593
- npx @nick3/copilot-api@latest start --account-type business
611
+ bunx --bun @nick3/copilot-api@latest start --account-type business
594
612
 
595
613
  # 使用 GitHub Enterprise 方案账号
596
- npx @nick3/copilot-api@latest start --account-type enterprise
614
+ bunx --bun @nick3/copilot-api@latest start --account-type enterprise
597
615
 
598
616
  # 对每个请求启用手动审批
599
- npx @nick3/copilot-api@latest start --manual
617
+ bunx --bun @nick3/copilot-api@latest start --manual
600
618
 
601
619
  # 将请求间隔限制为 30 秒
602
- npx @nick3/copilot-api@latest start --rate-limit 30
620
+ bunx --bun @nick3/copilot-api@latest start --rate-limit 30
603
621
 
604
622
  # 命中速率限制时等待,而不是直接报错
605
- npx @nick3/copilot-api@latest start --rate-limit 30 --wait
623
+ bunx --bun @nick3/copilot-api@latest start --rate-limit 30 --wait
606
624
 
607
625
  # 直接传入 GitHub token
608
- npx @nick3/copilot-api@latest start --github-token ghp_YOUR_TOKEN_HERE
626
+ bunx --bun @nick3/copilot-api@latest start --github-token ghp_YOUR_TOKEN_HERE
609
627
 
610
628
  # 仅执行认证流程
611
- npx @nick3/copilot-api@latest auth
629
+ bunx --bun @nick3/copilot-api@latest auth
612
630
 
613
631
  # 认证时启用详细日志
614
- npx @nick3/copilot-api@latest auth --verbose
632
+ bunx --bun @nick3/copilot-api@latest auth --verbose
615
633
 
616
634
  # 添加多个账号(账号会按添加顺序记录)
617
- npx @nick3/copilot-api@latest auth add
618
- npx @nick3/copilot-api@latest auth add # 添加第二个账号
635
+ bunx --bun @nick3/copilot-api@latest auth add
636
+ bunx --bun @nick3/copilot-api@latest auth add # 添加第二个账号
619
637
 
620
638
  # 列出所有已注册账号
621
- npx @nick3/copilot-api@latest auth ls
639
+ bunx --bun @nick3/copilot-api@latest auth ls
622
640
 
623
641
  # 列出账号并显示配额信息
624
- npx @nick3/copilot-api@latest auth ls -q
642
+ bunx --bun @nick3/copilot-api@latest auth ls -q
625
643
 
626
644
  # 按索引删除账号(1-based)
627
- npx @nick3/copilot-api@latest auth rm 2
645
+ bunx --bun @nick3/copilot-api@latest auth rm 2
628
646
 
629
647
  # 按 ID 删除账号(GitHub 用户名)
630
- npx @nick3/copilot-api@latest auth rm octocat
648
+ bunx --bun @nick3/copilot-api@latest auth rm octocat
631
649
 
632
650
  # 在终端中查看 Copilot 用量与额度(无需启动服务)
633
- npx @nick3/copilot-api@latest check-usage
651
+ bunx --bun @nick3/copilot-api@latest check-usage
634
652
 
635
653
  # 输出调试信息,便于排障
636
- npx @nick3/copilot-api@latest debug
654
+ bunx --bun @nick3/copilot-api@latest debug
637
655
 
638
656
  # 以 JSON 格式输出调试信息
639
- npx @nick3/copilot-api@latest debug --json
657
+ bunx --bun @nick3/copilot-api@latest debug --json
640
658
 
641
659
  # 从环境变量初始化代理(HTTP_PROXY、HTTPS_PROXY 等)
642
- npx @nick3/copilot-api@latest start --proxy-env
660
+ bunx --bun @nick3/copilot-api@latest start --proxy-env
643
661
 
644
662
  # 使用 opencode GitHub Copilot 认证
645
- COPILOT_API_OAUTH_APP=opencode npx @nick3/copilot-api@latest start
663
+ COPILOT_API_OAUTH_APP=opencode bunx --bun @nick3/copilot-api@latest start
646
664
 
647
665
  # 通过命令行设置自定义 API home 目录
648
- npx @nick3/copilot-api@latest --api-home=/path/to/custom/dir start
666
+ bunx --bun @nick3/copilot-api@latest --api-home=/path/to/custom/dir start
649
667
 
650
668
  # 通过命令行使用 GitHub Enterprise
651
- npx @nick3/copilot-api@latest --enterprise-url=company.ghe.com start
669
+ bunx --bun @nick3/copilot-api@latest --enterprise-url=company.ghe.com start
652
670
 
653
671
  # 通过命令行使用 opencode OAuth
654
- npx @nick3/copilot-api@latest --oauth-app=opencode start
672
+ bunx --bun @nick3/copilot-api@latest --oauth-app=opencode start
655
673
 
656
674
  # 组合多个全局选项
657
- npx @nick3/copilot-api@latest --api-home=/custom/path --oauth-app=opencode --enterprise-url=company.ghe.com start
675
+ bunx --bun @nick3/copilot-api@latest --api-home=/custom/path --oauth-app=opencode --enterprise-url=company.ghe.com start
676
+ ```
658
677
 
659
- # Bun 而不是 Node.js 运行已发布 CLI
660
- bunx --bun @nick3/copilot-api@latest start
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
- npx @nick3/copilot-api@latest start
673
- npx @nick3/copilot-api@latest auth
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 npx @nick3/copilot-api@latest start
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
- npx @nick3/copilot-api@latest start --claude-code
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
- 请把 tool search bridge 加到 Claude Code 使用的 MCP 配置中:
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
- npx @nick3/copilot-api@latest --oauth-app=opencode start
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 npx @nick3/copilot-api@latest start
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. 启动服务。例如使用 npx
996
+ 1. 启动服务。例如使用 Bun
902
997
  ```sh
903
- npx @nick3/copilot-api@latest start
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 { T as normalizeDomain } from "./poll-access-token-BAgM2-7k.js";
2
- import { n as accountTokenPath, t as PATHS } from "./paths-CclKwouX.js";
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-COtMmvzU.js.map
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