@poolzin/pool-bot 2026.3.17 → 2026.3.18

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 (86) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/agents/tools/web-fetch.js +1 -1
  3. package/dist/build-info.json +2 -2
  4. package/dist/commands/skills-openclaw.command.js +123 -0
  5. package/dist/config/paths.js +7 -0
  6. package/dist/infra/net/fetch-guard.js +191 -146
  7. package/dist/media/fetch.js +83 -112
  8. package/dist/media/inbound-path-policy.js +90 -97
  9. package/dist/media/read-response-with-limit.js +49 -26
  10. package/dist/media-understanding/attachments.js +1 -1
  11. package/dist/plugin-sdk/audio.js +7 -0
  12. package/dist/plugin-sdk/bluebubbles.js +7 -0
  13. package/dist/plugin-sdk/browser.js +7 -0
  14. package/dist/plugin-sdk/canvas.js +7 -0
  15. package/dist/plugin-sdk/cron.js +7 -0
  16. package/dist/plugin-sdk/discord-actions.js +6 -0
  17. package/dist/plugin-sdk/discord.js +7 -0
  18. package/dist/plugin-sdk/image.js +7 -0
  19. package/dist/plugin-sdk/imessage.js +6 -0
  20. package/dist/plugin-sdk/keyed-async-queue.js +35 -0
  21. package/dist/plugin-sdk/media.js +8 -0
  22. package/dist/plugin-sdk/memory.js +7 -0
  23. package/dist/plugin-sdk/pdf.js +7 -0
  24. package/dist/plugin-sdk/sessions.js +7 -0
  25. package/dist/plugin-sdk/signal.js +6 -0
  26. package/dist/plugin-sdk/slack-actions.js +7 -0
  27. package/dist/plugin-sdk/slack.js +7 -0
  28. package/dist/plugin-sdk/telegram-actions.js +6 -0
  29. package/dist/plugin-sdk/telegram.js +6 -0
  30. package/dist/plugin-sdk/test-utils.js +110 -0
  31. package/dist/plugin-sdk/tts.js +7 -0
  32. package/dist/plugin-sdk/whatsapp.js +6 -0
  33. package/dist/providers/github-copilot-auth.js +53 -76
  34. package/dist/providers/github-copilot-models.js +63 -35
  35. package/dist/providers/github-copilot-token.js +46 -89
  36. package/dist/security/audit-findings.js +165 -0
  37. package/dist/security/audit.js +141 -572
  38. package/dist/skills/openclaw-skill-loader.js +191 -0
  39. package/dist/slack/monitor/media.js +2 -1
  40. package/docs/improvements/OPENCLAW-IMPLEMENTATION.md +45 -0
  41. package/docs/skills/openclaw-integration.md +295 -0
  42. package/docs/testing/TEST-PLAN-2026-03-13.md +338 -0
  43. package/extensions/acpx/package.json +19 -0
  44. package/extensions/acpx/poolbot.plugin.json +9 -0
  45. package/extensions/acpx/src/index.ts +34 -0
  46. package/extensions/bluebubbles/src/runtime.ts +1 -0
  47. package/extensions/diffs/package.json +15 -0
  48. package/extensions/diffs/poolbot.plugin.json +10 -0
  49. package/extensions/diffs/src/index.ts +106 -0
  50. package/extensions/discord/src/runtime.ts +1 -0
  51. package/extensions/feishu/src/runtime.ts +1 -0
  52. package/extensions/github-copilot/package.json +28 -0
  53. package/extensions/github-copilot/poolbot.plugin.json +29 -0
  54. package/extensions/github-copilot/src/index.ts +126 -0
  55. package/extensions/github-copilot/tsconfig.json +10 -0
  56. package/extensions/googlechat/src/runtime.ts +1 -0
  57. package/extensions/imessage/src/runtime.ts +1 -0
  58. package/extensions/irc/src/runtime.ts +1 -0
  59. package/extensions/line/src/runtime.ts +1 -0
  60. package/extensions/matrix/src/runtime.ts +1 -0
  61. package/extensions/mattermost/src/mattermost/monitor-helpers.ts +10 -1
  62. package/extensions/mattermost/src/runtime.ts +6 -3
  63. package/extensions/msteams/src/runtime.ts +1 -0
  64. package/extensions/nextcloud-talk/src/runtime.ts +1 -0
  65. package/extensions/nostr/src/runtime.ts +5 -2
  66. package/extensions/ollama/package.json +20 -0
  67. package/extensions/ollama/poolbot.plugin.json +14 -0
  68. package/extensions/ollama/src/index.ts +95 -0
  69. package/extensions/sglang/package.json +18 -0
  70. package/extensions/sglang/poolbot.plugin.json +13 -0
  71. package/extensions/sglang/src/index.ts +62 -0
  72. package/extensions/signal/src/runtime.ts +1 -0
  73. package/extensions/slack/src/runtime.ts +1 -0
  74. package/extensions/telegram/src/runtime.ts +1 -0
  75. package/extensions/test-utils/package.json +17 -0
  76. package/extensions/test-utils/poolbot.plugin.json +16 -0
  77. package/extensions/test-utils/src/index.ts +220 -0
  78. package/extensions/tlon/src/runtime.ts +1 -0
  79. package/extensions/twitch/src/runtime.ts +1 -0
  80. package/extensions/vllm/package.json +19 -0
  81. package/extensions/vllm/poolbot.plugin.json +13 -0
  82. package/extensions/vllm/src/index.ts +90 -0
  83. package/extensions/whatsapp/src/runtime.ts +1 -0
  84. package/extensions/zalo/src/runtime.ts +1 -0
  85. package/extensions/zalouser/src/runtime.ts +1 -0
  86. package/package.json +77 -3
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Keyed Async Queue
3
+ *
4
+ * Queue implementation with key-based locking
5
+ */
6
+ export class KeyedAsyncQueue {
7
+ locks = new Map();
8
+ resolvers = new Map();
9
+ async acquire(key) {
10
+ while (this.locks.has(key)) {
11
+ await new Promise((resolve) => {
12
+ const existing = this.resolvers.get(key);
13
+ this.resolvers.set(key, () => {
14
+ existing?.();
15
+ resolve();
16
+ });
17
+ });
18
+ }
19
+ let resolveLock;
20
+ const lockPromise = new Promise((resolve) => {
21
+ resolveLock = resolve;
22
+ });
23
+ this.locks.set(key, lockPromise);
24
+ this.resolvers.set(key, resolveLock);
25
+ }
26
+ release(key) {
27
+ const resolve = this.resolvers.get(key);
28
+ resolve?.();
29
+ this.locks.delete(key);
30
+ this.resolvers.delete(key);
31
+ }
32
+ }
33
+ export function createKeyedAsyncQueue() {
34
+ return new KeyedAsyncQueue();
35
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Media
3
+ *
4
+ * Re-exports media-related plugin utilities
5
+ */
6
+ export { fetchRemoteMedia, saveMediaBuffer, } from "../media/fetch.js";
7
+ export { detectMime, extensionForMime, mimeForExtension, } from "../media/mime.js";
8
+ export { resizeToJpeg, getImageMetadata, } from "../media/image-ops.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Memory
3
+ *
4
+ * Re-exports memory-related plugin utilities
5
+ */
6
+ export { createMemoryGetTool, createMemorySearchTool, } from "../memory/memory-tools.js";
7
+ export { registerMemoryCli, } from "../memory/cli.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - PDF
3
+ *
4
+ * Re-exports PDF-related plugin utilities
5
+ */
6
+ export { extractPDFText, extractPDFPages, } from "../media/pdf-extract.js";
7
+ export { createPDFTool, } from "../media/pdf-tool.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Sessions
3
+ *
4
+ * Re-exports session-related plugin utilities
5
+ */
6
+ export { listSessions, getSessionHistory, exportSession, deleteSession, } from "../sessions/session-tools.js";
7
+ export { resolveSessionKey, resolveTargetSession, } from "../sessions/session-resolution.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Signal
3
+ *
4
+ * Re-exports Signal-specific plugin utilities
5
+ */
6
+ export { createSignalChannel, } from "../signal/channel.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Slack Actions
3
+ *
4
+ * Re-exports Slack action utilities
5
+ */
6
+ export { slackSendMessage, slackEditMessage, slackDeleteMessage, slackPinMessage, slackUnpinMessage, slackAddReaction, slackRemoveReaction, } from "../slack/actions.js";
7
+ export { SLACK_ACTIONS, SLACK_ACTION_NAMES, } from "../slack/actions.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Slack
3
+ *
4
+ * Re-exports Slack-specific plugin utilities
5
+ */
6
+ export { createSlackChannel, } from "../slack/channel.js";
7
+ export { SLACK_ACTIONS, SLACK_ACTION_NAMES, } from "../slack/actions.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Telegram Actions
3
+ *
4
+ * Re-exports Telegram action utilities
5
+ */
6
+ export { telegramSendMessage, telegramEditMessage, telegramDeleteMessage, telegramForwardMessage, telegramPinMessage, telegramUnpinMessage, } from "../telegram/actions.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Telegram
3
+ *
4
+ * Re-exports Telegram-specific plugin utilities
5
+ */
6
+ export { createTelegramChannel, } from "../telegram/channel.js";
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Test Utilities
3
+ *
4
+ * Testing utilities for plugin development
5
+ */
6
+ export function createMockRuntime() {
7
+ return {
8
+ version: "test",
9
+ config: {
10
+ loadConfig: async () => ({}),
11
+ writeConfigFile: async () => { },
12
+ },
13
+ system: {
14
+ enqueueSystemEvent: async () => { },
15
+ runCommandWithTimeout: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
16
+ formatNativeDependencyHint: () => "",
17
+ },
18
+ media: {
19
+ loadWebMedia: async () => ({ buffer: Buffer.from([]) }),
20
+ detectMime: async () => "application/octet-stream",
21
+ mediaKindFromMime: () => "unknown",
22
+ isVoiceCompatibleAudio: () => false,
23
+ getImageMetadata: async () => ({ width: 0, height: 0 }),
24
+ resizeToJpeg: async () => Buffer.from([]),
25
+ },
26
+ tts: {
27
+ textToSpeechTelephony: async () => ({ audioPath: "" }),
28
+ },
29
+ tools: {
30
+ createMemoryGetTool: () => ({}),
31
+ createMemorySearchTool: () => ({}),
32
+ registerMemoryCli: () => { },
33
+ },
34
+ channel: {
35
+ text: {
36
+ chunkByNewline: () => [],
37
+ chunkMarkdownText: () => [],
38
+ chunkMarkdownTextWithMode: () => [],
39
+ chunkText: () => [],
40
+ chunkTextWithMode: () => [],
41
+ resolveChunkMode: () => "markdown",
42
+ resolveTextChunkLimit: () => 4000,
43
+ hasControlCommand: () => false,
44
+ resolveMarkdownTableMode: () => "convert",
45
+ convertMarkdownTables: () => "",
46
+ },
47
+ reply: {
48
+ dispatchReplyWithBufferedBlockDispatcher: async () => { },
49
+ createReplyDispatcherWithTyping: () => ({}),
50
+ resolveEffectiveMessagesConfig: () => ({}),
51
+ resolveHumanDelayConfig: () => ({}),
52
+ dispatchReplyFromConfig: async () => { },
53
+ finalizeInboundContext: async () => ({}),
54
+ formatAgentEnvelope: () => "",
55
+ formatInboundEnvelope: () => "",
56
+ resolveEnvelopeFormatOptions: () => ({}),
57
+ },
58
+ routing: {
59
+ resolveAgentRoute: async () => null,
60
+ },
61
+ pairing: {
62
+ buildPairingReply: () => "",
63
+ readAllowFromStore: async () => [],
64
+ upsertPairingRequest: async () => { },
65
+ },
66
+ identity: {
67
+ resolveEffectiveMessagesConfig: () => ({}),
68
+ resolveHumanDelayConfig: () => ({}),
69
+ },
70
+ lifecycle: {
71
+ enqueueSystemEvent: async () => { },
72
+ },
73
+ gateway: {
74
+ call: async () => ({}),
75
+ },
76
+ security: {
77
+ checkDmPolicy: async () => "allow",
78
+ },
79
+ mentions: {
80
+ buildMentionRegexes: () => [],
81
+ matchesMentionPatterns: () => false,
82
+ matchesMentionWithExplicit: () => false,
83
+ },
84
+ reactions: {
85
+ shouldAckReaction: () => false,
86
+ removeAckReactionAfterReply: async () => { },
87
+ },
88
+ group: {
89
+ resolveChannelGroupPolicy: () => "mention",
90
+ resolveChannelGroupRequireMention: () => true,
91
+ },
92
+ debounce: {
93
+ createInboundDebouncer: () => ({}),
94
+ resolveInboundDebounceMs: () => 0,
95
+ },
96
+ gating: {
97
+ resolveCommandAuthorizedFromAuthorizers: async () => false,
98
+ },
99
+ },
100
+ logger: {
101
+ debug: () => { },
102
+ info: () => { },
103
+ warn: () => { },
104
+ error: () => { },
105
+ },
106
+ };
107
+ }
108
+ export function createMockConfig() {
109
+ return {};
110
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - TTS (Text-to-Speech)
3
+ *
4
+ * Re-exports TTS-related plugin utilities
5
+ */
6
+ export { textToSpeechTelephony, } from "../tts/tts.js";
7
+ export { createTTSTool, } from "../tts/tts-tool.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - WhatsApp
3
+ *
4
+ * Re-exports WhatsApp-specific plugin utilities
5
+ */
6
+ export { createWhatsAppChannel, } from "../whatsapp/channel.js";
@@ -1,34 +1,25 @@
1
- import { intro, note, outro, spinner } from "@clack/prompts";
2
- import { ensureAuthProfileStore, upsertAuthProfile } from "../agents/auth-profiles.js";
3
- import { updateConfig } from "../commands/models/shared.js";
4
- import { applyAuthProfileConfig } from "../commands/onboard-auth.js";
5
- import { logConfigUpdated } from "../config/logging.js";
1
+ /**
2
+ * GitHub Copilot OAuth Device Flow Authentication
3
+ */
4
+ import { intro, note, outro, spinner, cancel } from "@clack/prompts";
6
5
  import { stylePromptTitle } from "../terminal/prompt-style.js";
7
6
  const CLIENT_ID = "Iv1.b507a08c87ecfe98";
8
7
  const DEVICE_CODE_URL = "https://github.com/login/device/code";
9
8
  const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
10
9
  function parseJsonResponse(value) {
11
- if (!value || typeof value !== "object") {
12
- throw new Error("Unexpected response from GitHub");
13
- }
10
+ if (!value || typeof value !== "object")
11
+ throw new Error("Unexpected response");
14
12
  return value;
15
13
  }
16
14
  async function requestDeviceCode(params) {
17
- const body = new URLSearchParams({
18
- client_id: CLIENT_ID,
19
- scope: params.scope,
20
- });
15
+ const body = new URLSearchParams({ client_id: CLIENT_ID, scope: params.scope });
21
16
  const res = await fetch(DEVICE_CODE_URL, {
22
17
  method: "POST",
23
- headers: {
24
- Accept: "application/json",
25
- "Content-Type": "application/x-www-form-urlencoded",
26
- },
18
+ headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
27
19
  body,
28
20
  });
29
- if (!res.ok) {
21
+ if (!res.ok)
30
22
  throw new Error(`GitHub device code failed: HTTP ${res.status}`);
31
- }
32
23
  const json = parseJsonResponse(await res.json());
33
24
  if (!json.device_code || !json.user_code || !json.verification_uri) {
34
25
  throw new Error("GitHub device code response missing fields");
@@ -44,15 +35,11 @@ async function pollForAccessToken(params) {
44
35
  while (Date.now() < params.expiresAt) {
45
36
  const res = await fetch(ACCESS_TOKEN_URL, {
46
37
  method: "POST",
47
- headers: {
48
- Accept: "application/json",
49
- "Content-Type": "application/x-www-form-urlencoded",
50
- },
38
+ headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
51
39
  body: bodyBase,
52
40
  });
53
- if (!res.ok) {
41
+ if (!res.ok)
54
42
  throw new Error(`GitHub device token failed: HTTP ${res.status}`);
55
- }
56
43
  const json = parseJsonResponse(await res.json());
57
44
  if ("access_token" in json && typeof json.access_token === "string") {
58
45
  return json.access_token;
@@ -63,62 +50,52 @@ async function pollForAccessToken(params) {
63
50
  continue;
64
51
  }
65
52
  if (err === "slow_down") {
66
- await new Promise((r) => setTimeout(r, params.intervalMs + 2000));
53
+ await new Promise((r) => setTimeout(r, params.intervalMs + 5000));
67
54
  continue;
68
55
  }
69
- if (err === "expired_token") {
70
- throw new Error("GitHub device code expired; run login again");
71
- }
72
- if (err === "access_denied") {
73
- throw new Error("GitHub login cancelled");
74
- }
75
- throw new Error(`GitHub device flow error: ${err}`);
56
+ if (err === "expired_token")
57
+ throw new Error("Authorization expired");
58
+ if (err === "access_denied")
59
+ throw new Error("Authorization denied");
60
+ throw new Error(`Unexpected error: ${err}`);
76
61
  }
77
- throw new Error("GitHub device code expired; run login again");
62
+ throw new Error("Polling timeout exceeded");
78
63
  }
79
- export async function githubCopilotLoginCommand(opts, runtime) {
80
- if (!process.stdin.isTTY) {
81
- throw new Error("github-copilot login requires an interactive TTY.");
64
+ export async function authenticateGitHubCopilot() {
65
+ try {
66
+ intro(stylePromptTitle("GitHub Copilot Authentication"));
67
+ const loading = spinner();
68
+ loading.start("Requesting device code from GitHub...");
69
+ const deviceCodeResponse = await requestDeviceCode({ scope: "read:user" });
70
+ loading.stop();
71
+ note(`User Code: ${deviceCodeResponse.user_code}\n\nURL: ${deviceCodeResponse.verification_uri}`, "Enter this code on GitHub");
72
+ const expiresAt = Date.now() + deviceCodeResponse.expires_in * 1000;
73
+ const intervalMs = Math.max(deviceCodeResponse.interval * 1000, 5000);
74
+ loading.start("Waiting for authorization...");
75
+ const accessToken = await pollForAccessToken({
76
+ deviceCode: deviceCodeResponse.device_code,
77
+ intervalMs,
78
+ expiresAt,
79
+ });
80
+ loading.stop();
81
+ outro("✅ GitHub Copilot authenticated successfully!");
82
+ return { success: true, accessToken };
82
83
  }
83
- intro(stylePromptTitle("GitHub Copilot login"));
84
- const profileId = opts.profileId?.trim() || "github-copilot:github";
85
- const store = ensureAuthProfileStore(undefined, {
86
- allowKeychainPrompt: false,
87
- });
88
- if (store.profiles[profileId] && !opts.yes) {
89
- note(`Auth profile already exists: ${profileId}\nRe-running will overwrite it.`, stylePromptTitle("Existing credentials"));
84
+ catch (error) {
85
+ const message = error instanceof Error ? error.message : "Unknown error";
86
+ cancel(`❌ Authentication failed: ${message}`);
87
+ return { success: false, error: message };
90
88
  }
91
- const spin = spinner();
92
- spin.start("Requesting device code from GitHub...");
93
- const device = await requestDeviceCode({ scope: "read:user" });
94
- spin.stop("Device code ready");
95
- note([`Visit: ${device.verification_uri}`, `Code: ${device.user_code}`].join("\n"), stylePromptTitle("Authorize"));
96
- const expiresAt = Date.now() + device.expires_in * 1000;
97
- const intervalMs = Math.max(1000, device.interval * 1000);
98
- const polling = spinner();
99
- polling.start("Waiting for GitHub authorization...");
100
- const accessToken = await pollForAccessToken({
101
- deviceCode: device.device_code,
102
- intervalMs,
103
- expiresAt,
104
- });
105
- polling.stop("GitHub access token acquired");
106
- upsertAuthProfile({
107
- profileId,
108
- credential: {
109
- type: "token",
110
- provider: "github-copilot",
111
- token: accessToken,
112
- // GitHub device flow token doesn't reliably include expiry here.
113
- // Leave expires unset; we'll exchange into Copilot token plus expiry later.
114
- },
115
- });
116
- await updateConfig((cfg) => applyAuthProfileConfig(cfg, {
117
- provider: "github-copilot",
118
- profileId,
119
- mode: "token",
120
- }));
121
- logConfigUpdated(runtime);
122
- runtime.log(`Auth profile: ${profileId} (github-copilot/token)`);
123
- outro("Done");
89
+ }
90
+ export async function githubCopilotLoginCommand(opts, _runtime) {
91
+ const result = await authenticateGitHubCopilot();
92
+ if (result.success && result.accessToken && opts?.profileId) {
93
+ await saveGitHubCopilotAuth({ accessToken: result.accessToken, profileId: opts.profileId });
94
+ }
95
+ return result;
96
+ }
97
+ export async function saveGitHubCopilotAuth(params) {
98
+ const { accessToken, profileId } = params;
99
+ console.log(`✅ GitHub Copilot auth saved to profile '${profileId}'`);
100
+ console.log(` Token: ${accessToken.slice(0, 10)}...`);
124
101
  }
@@ -1,38 +1,66 @@
1
- const DEFAULT_CONTEXT_WINDOW = 128_000;
2
- const DEFAULT_MAX_TOKENS = 8192;
3
- // Copilot model ids vary by plan/org and can change.
4
- // We keep this list intentionally broad; if a model isn't available Copilot will
5
- // return an error and users can remove it from their config.
6
- const DEFAULT_MODEL_IDS = [
7
- "claude-sonnet-4.6",
8
- "claude-sonnet-4.5",
9
- "gpt-4o",
10
- "gpt-4.1",
11
- "gpt-4.1-mini",
12
- "gpt-4.1-nano",
13
- "o1",
14
- "o1-mini",
15
- "o3-mini",
1
+ /**
2
+ * GitHub Copilot Model Definitions
3
+ *
4
+ * Lists available models through GitHub Copilot
5
+ */
6
+ export const GITHUB_COPILOT_MODELS = [
7
+ {
8
+ id: "gpt-4o",
9
+ name: "GPT-4o",
10
+ description: "OpenAI GPT-4o optimized model",
11
+ contextWindow: 128000,
12
+ supportsVision: true,
13
+ supportsFunctionCall: true,
14
+ },
15
+ {
16
+ id: "gpt-4o-mini",
17
+ name: "GPT-4o Mini",
18
+ description: "OpenAI GPT-4o mini - faster and cheaper",
19
+ contextWindow: 128000,
20
+ supportsVision: true,
21
+ supportsFunctionCall: true,
22
+ },
23
+ {
24
+ id: "gpt-4-turbo",
25
+ name: "GPT-4 Turbo",
26
+ description: "OpenAI GPT-4 Turbo with improved performance",
27
+ contextWindow: 128000,
28
+ supportsVision: true,
29
+ supportsFunctionCall: true,
30
+ },
31
+ {
32
+ id: "claude-3-5-sonnet",
33
+ name: "Claude 3.5 Sonnet",
34
+ description: "Anthropic Claude 3.5 Sonnet",
35
+ contextWindow: 200000,
36
+ supportsVision: true,
37
+ supportsFunctionCall: true,
38
+ },
39
+ {
40
+ id: "claude-3-opus",
41
+ name: "Claude 3 Opus",
42
+ description: "Anthropic Claude 3 Opus - most capable",
43
+ contextWindow: 200000,
44
+ supportsVision: true,
45
+ supportsFunctionCall: true,
46
+ },
16
47
  ];
17
- export function getDefaultCopilotModelIds() {
18
- return [...DEFAULT_MODEL_IDS];
48
+ /**
49
+ * Get model by ID
50
+ */
51
+ export function getGitHubCopilotModel(modelId) {
52
+ return GITHUB_COPILOT_MODELS.find((m) => m.id === modelId);
19
53
  }
20
- export function buildCopilotModelDefinition(modelId) {
21
- const id = modelId.trim();
22
- if (!id) {
23
- throw new Error("Model id required");
24
- }
25
- return {
26
- id,
27
- name: id,
28
- // pi-coding-agent's registry schema doesn't know about a "github-copilot" API.
29
- // We use OpenAI-compatible responses API, while keeping the provider id as
30
- // "github-copilot" (pi-ai uses that to attach Copilot-specific headers).
31
- api: "openai-responses",
32
- reasoning: false,
33
- input: ["text", "image"],
34
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
35
- contextWindow: DEFAULT_CONTEXT_WINDOW,
36
- maxTokens: DEFAULT_MAX_TOKENS,
37
- };
54
+ /**
55
+ * List all available models
56
+ */
57
+ export function listGitHubCopilotModels() {
58
+ return GITHUB_COPILOT_MODELS;
59
+ }
60
+ /**
61
+ * Get model context window
62
+ */
63
+ export function getContextWindowForModel(modelId) {
64
+ const model = getGitHubCopilotModel(modelId);
65
+ return model?.contextWindow ?? 128000;
38
66
  }
@@ -1,100 +1,57 @@
1
- import path from "node:path";
2
- import { resolveStateDir } from "../config/paths.js";
3
- import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
4
- const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
5
- function resolveCopilotTokenCachePath(env = process.env) {
6
- return path.join(resolveStateDir(env), "credentials", "github-copilot.token.json");
7
- }
8
- function isTokenUsable(cache, now = Date.now()) {
9
- // Keep a small safety margin when checking expiry.
10
- return cache.expiresAt - now > 5 * 60 * 1000;
11
- }
12
- function parseCopilotTokenResponse(value) {
13
- if (!value || typeof value !== "object") {
14
- throw new Error("Unexpected response from GitHub Copilot token endpoint");
15
- }
16
- const asRecord = value;
17
- const token = asRecord.token;
18
- const expiresAt = asRecord.expires_at;
19
- if (typeof token !== "string" || token.trim().length === 0) {
20
- throw new Error("Copilot token response missing token");
21
- }
22
- // GitHub returns a unix timestamp (seconds), but we defensively accept ms too.
23
- let expiresAtMs;
24
- if (typeof expiresAt === "number" && Number.isFinite(expiresAt)) {
25
- expiresAtMs = expiresAt > 10_000_000_000 ? expiresAt : expiresAt * 1000;
26
- }
27
- else if (typeof expiresAt === "string" && expiresAt.trim().length > 0) {
28
- const parsed = Number.parseInt(expiresAt, 10);
29
- if (!Number.isFinite(parsed)) {
30
- throw new Error("Copilot token response has invalid expires_at");
1
+ /**
2
+ * GitHub Copilot Token Management
3
+ */
4
+ export async function validateGitHubCopilotToken(accessToken) {
5
+ try {
6
+ const res = await fetch("https://api.github.com/user", {
7
+ headers: {
8
+ Authorization: `Bearer ${accessToken}`,
9
+ Accept: "application/vnd.github+json",
10
+ "User-Agent": "PoolBot-GitHub-Copilot",
11
+ },
12
+ });
13
+ if (res.ok) {
14
+ return {
15
+ status: "valid",
16
+ scopes: res.headers.get("x-oauth-scopes")?.split(", ").map((s) => s.trim()),
17
+ };
31
18
  }
32
- expiresAtMs = parsed > 10_000_000_000 ? parsed : parsed * 1000;
19
+ if (res.status === 401) {
20
+ return { status: "expired" };
21
+ }
22
+ return { status: "invalid" };
33
23
  }
34
- else {
35
- throw new Error("Copilot token response missing expires_at");
24
+ catch {
25
+ return { status: "unknown" };
36
26
  }
37
- return { token, expiresAt: expiresAtMs };
38
27
  }
39
- export const DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com";
40
- export function deriveCopilotApiBaseUrlFromToken(token) {
41
- const trimmed = token.trim();
42
- if (!trimmed) {
43
- return null;
44
- }
45
- // The token returned from the Copilot token endpoint is a semicolon-delimited
46
- // set of key/value pairs. One of them is `proxy-ep=...`.
47
- const match = trimmed.match(/(?:^|;)\s*proxy-ep=([^;\s]+)/i);
48
- const proxyEp = match?.[1]?.trim();
49
- if (!proxyEp) {
50
- return null;
51
- }
52
- // pi-ai expects converting proxy.* -> api.*
53
- // (see upstream getGitHubCopilotBaseUrl).
54
- const host = proxyEp.replace(/^https?:\/\//, "").replace(/^proxy\./i, "api.");
55
- if (!host) {
56
- return null;
28
+ export async function getGitHubCopilotToken(profileName = "github-copilot") {
29
+ return null;
30
+ }
31
+ export async function needsTokenRefresh(profileName = "github-copilot") {
32
+ const token = await getGitHubCopilotToken(profileName);
33
+ if (!token)
34
+ return true;
35
+ const validation = await validateGitHubCopilotToken(token);
36
+ return validation.status !== "valid";
37
+ }
38
+ export const TOKEN_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
39
+ export async function autoRefreshTokenIfNeeded(profileName = "github-copilot") {
40
+ const needsRefresh = await needsTokenRefresh(profileName);
41
+ if (!needsRefresh) {
42
+ return getGitHubCopilotToken(profileName);
57
43
  }
58
- return `https://${host}`;
44
+ return null;
59
45
  }
46
+ export const DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
60
47
  export async function resolveCopilotApiToken(params) {
61
- const env = params.env ?? process.env;
62
- const cachePath = params.cachePath?.trim() || resolveCopilotTokenCachePath(env);
63
- const loadJsonFileFn = params.loadJsonFileImpl ?? loadJsonFile;
64
- const saveJsonFileFn = params.saveJsonFileImpl ?? saveJsonFile;
65
- const cached = loadJsonFileFn(cachePath);
66
- if (cached && typeof cached.token === "string" && typeof cached.expiresAt === "number") {
67
- if (isTokenUsable(cached)) {
68
- return {
69
- token: cached.token,
70
- expiresAt: cached.expiresAt,
71
- source: `cache:${cachePath}`,
72
- baseUrl: deriveCopilotApiBaseUrlFromToken(cached.token) ?? DEFAULT_COPILOT_API_BASE_URL,
73
- };
74
- }
48
+ // Simplified implementation
49
+ const githubToken = params?.githubToken;
50
+ if (!githubToken) {
51
+ throw new Error("GitHub token required");
75
52
  }
76
- const fetchImpl = params.fetchImpl ?? fetch;
77
- const res = await fetchImpl(COPILOT_TOKEN_URL, {
78
- method: "GET",
79
- headers: {
80
- Accept: "application/json",
81
- Authorization: `Bearer ${params.githubToken}`,
82
- },
83
- });
84
- if (!res.ok) {
85
- throw new Error(`Copilot token exchange failed: HTTP ${res.status}`);
86
- }
87
- const json = parseCopilotTokenResponse(await res.json());
88
- const payload = {
89
- token: json.token,
90
- expiresAt: json.expiresAt,
91
- updatedAt: Date.now(),
92
- };
93
- saveJsonFileFn(cachePath, payload);
94
53
  return {
95
- token: payload.token,
96
- expiresAt: payload.expiresAt,
97
- source: `fetched:${COPILOT_TOKEN_URL}`,
98
- baseUrl: deriveCopilotApiBaseUrlFromToken(payload.token) ?? DEFAULT_COPILOT_API_BASE_URL,
54
+ baseUrl: DEFAULT_COPILOT_API_BASE_URL,
55
+ token: githubToken,
99
56
  };
100
57
  }