@meetopenbot/github 0.1.1 → 0.1.3

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/dist/index.js CHANGED
@@ -1,145 +1,19 @@
1
- import { definePlugin, shouldHandleInvoke, agentOutput, uiWidget, } from '@meetopenbot/plugin-sdk';
2
- import { runGithubAgent } from './agent.js';
3
- import { isCloudMode } from './cloud-mode.js';
4
- import { CREDITS_NOT_CONFIGURED_MESSAGE, creditsErrorMessage, resolveCreditsAuthConfig, } from './credits-auth.js';
5
- import { formatMissingCredentials, GITHUB_TOKEN_VAR, readGithubConfig, resolveGithubCredentials, } from './config.js';
6
- const GITHUB_TOKEN_WIDGET_ID = 'github-token-form';
7
- const githubPluginConfigSchema = {
8
- type: 'object',
9
- properties: {
10
- ...(isCloudMode()
11
- ? {
12
- authMode: {
13
- type: 'string',
14
- description: 'Credits uses your workspace credit balance via OpenBot. BYOK uses `OPENAI_API_KEY` from workspace settings.',
15
- enum: ['credits', 'byok'],
16
- default: 'credits',
17
- },
18
- }
19
- : {}),
20
- githubToken: {
21
- type: 'string',
22
- description: 'GitHub Personal Access Token',
23
- format: 'password',
24
- },
25
- model: {
26
- type: 'string',
27
- description: 'OpenAI model for GitHub agent invocations',
28
- default: 'openai/gpt-4o',
29
- },
30
- },
31
- };
32
- export default definePlugin({
1
+ import { defineMcpAgent } from '@meetopenbot/plugin-sdk';
2
+ export const plugin = await defineMcpAgent({
33
3
  name: 'GitHub',
34
4
  description: 'Manage GitHub repositories, issues, and pull requests',
35
- configSchema: githubPluginConfigSchema,
36
- factory: (context) => {
37
- return (builder) => {
38
- builder.on('agent:invoke', async function* (event) {
39
- if (!shouldHandleInvoke(event, context.agentId))
40
- return;
41
- const userMessage = event.data?.content || '';
42
- const threadId = event.meta?.threadId;
43
- if (!userMessage)
44
- return;
45
- const githubConfig = readGithubConfig(context.config);
46
- const auth = await resolveGithubCredentials(githubConfig, context.storage);
47
- if (!auth.ok) {
48
- if (auth.missing.includes('githubToken')) {
49
- yield uiWidget({
50
- agentId: context.agentId,
51
- threadId,
52
- widget: {
53
- kind: 'form',
54
- widgetId: GITHUB_TOKEN_WIDGET_ID,
55
- title: 'GitHub Access Token',
56
- description: 'Enter a GitHub Personal Access Token with repo scope to continue.',
57
- fields: [
58
- {
59
- id: 'githubToken',
60
- label: 'GitHub Access Token',
61
- type: 'password',
62
- placeholder: 'ghp_...',
63
- required: true,
64
- },
65
- ],
66
- submitLabel: 'Save Token',
67
- },
68
- });
69
- return;
70
- }
71
- yield agentOutput({
72
- agentId: context.agentId,
73
- content: formatMissingCredentials(auth.missing, auth.authMode),
74
- threadId,
75
- });
76
- return;
77
- }
78
- if (auth.credentials.authMode === 'credits' &&
79
- !resolveCreditsAuthConfig()) {
80
- yield agentOutput({
81
- agentId: context.agentId,
82
- content: CREDITS_NOT_CONFIGURED_MESSAGE,
83
- threadId,
84
- });
85
- return;
86
- }
87
- try {
88
- for await (const chunk of runGithubAgent({
89
- prompt: userMessage,
90
- githubToken: auth.credentials.githubToken,
91
- authMode: auth.credentials.authMode,
92
- openaiApiKey: auth.credentials.openaiApiKey,
93
- model: auth.credentials.model,
94
- })) {
95
- if (chunk.kind === 'widget') {
96
- yield uiWidget({
97
- agentId: context.agentId,
98
- threadId,
99
- widget: chunk.widget,
100
- meta: event.meta,
101
- });
102
- continue;
103
- }
104
- yield agentOutput({
105
- agentId: context.agentId,
106
- content: chunk.content,
107
- threadId,
108
- meta: event.meta,
109
- });
110
- }
111
- }
112
- catch (error) {
113
- const message = error instanceof Error ? error.message : String(error);
114
- const creditsMessage = auth.credentials.authMode === 'credits'
115
- ? creditsErrorMessage(message)
116
- : undefined;
117
- yield agentOutput({
118
- agentId: context.agentId,
119
- content: (creditsMessage ?? message)
120
- ? `I encountered an error: ${creditsMessage ?? message}`
121
- : 'GitHub agent failed for an unknown reason. Please try again.',
122
- threadId,
123
- });
124
- }
125
- });
126
- builder.on('client:ui:widget:response', async function* (event) {
127
- if (event.data?.widgetId !== GITHUB_TOKEN_WIDGET_ID)
128
- return;
129
- const githubToken = event.data.values?.githubToken;
130
- if (typeof githubToken !== 'string' || !githubToken.trim())
131
- return;
132
- await context.storage.createVariable({
133
- key: GITHUB_TOKEN_VAR,
134
- value: githubToken.trim(),
135
- secret: true,
136
- });
137
- yield agentOutput({
138
- agentId: context.agentId,
139
- content: 'GitHub access token saved. Retry your last request.',
140
- threadId: event.meta?.threadId,
141
- });
142
- });
143
- };
5
+ models: { providers: ['openai'], default: 'openai/gpt-4o-mini' },
6
+ secret: { envKeys: ['GITHUB_TOKEN'] },
7
+ mcp: {
8
+ url: 'https://api.githubcopilot.com/mcp/',
9
+ headers: (token) => ({
10
+ Authorization: `Bearer ${token}`,
11
+ 'X-MCP-Toolsets': 'repos,issues,pull_requests',
12
+ }),
144
13
  },
14
+ system: `You are a GitHub assistant. Use the GitHub MCP tools to help with repositories, issues, and pull requests.
15
+ Parse owner/repo from "owner/repo" when the user provides that format.
16
+ Be concise and helpful.`,
17
+ maxSteps: 5,
145
18
  });
19
+ export default plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/github",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Manage GitHub repositories, issues, and pull requests from OpenBot",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,10 +8,7 @@
8
8
  "access": "public"
9
9
  },
10
10
  "dependencies": {
11
- "@ai-sdk/mcp": "^2.0.14",
12
- "@ai-sdk/openai": "^4.0.15",
13
- "ai": "^7.0.29",
14
- "@meetopenbot/plugin-sdk": "^0.2.0"
11
+ "@meetopenbot/plugin-sdk": "^0.4.0"
15
12
  },
16
13
  "devDependencies": {
17
14
  "@types/node": "^25.9.1",
@@ -33,7 +30,6 @@
33
30
  "build": "tsc && node ../../scripts/write-plugin-declaration.mjs",
34
31
  "start": "node dist/index.js",
35
32
  "dev": "tsc --watch --preserveWatchOutput",
36
- "typecheck": "tsc --noEmit",
37
- "test": "node --experimental-strip-types --test src/diff.test.ts"
33
+ "typecheck": "tsc --noEmit"
38
34
  }
39
35
  }
package/dist/agent.js DELETED
@@ -1,191 +0,0 @@
1
- import { createMCPClient } from '@ai-sdk/mcp';
2
- import { toolTraceWidget } from '@meetopenbot/plugin-sdk';
3
- import { generateText, stepCountIs } from 'ai';
4
- import { resolveOpenAiModel } from './model.js';
5
- import { diffWidgetFromTool, prIdentityFromInput, shouldFollowUpPrFiles, toolInputFrom, unwrapToolOutput, } from './diff.js';
6
- export const GITHUB_MCP_URL = 'https://api.githubcopilot.com/mcp/';
7
- const TOOL_OUTPUT_MAX_LENGTH = 2_000;
8
- function formatToolOutput(output) {
9
- if (output === undefined || output === null)
10
- return 'Done.';
11
- if (typeof output === 'string')
12
- return truncate(output);
13
- try {
14
- return truncate(JSON.stringify(output, null, 2));
15
- }
16
- catch {
17
- return truncate(String(output));
18
- }
19
- }
20
- function truncate(text) {
21
- if (text.length <= TOOL_OUTPUT_MAX_LENGTH)
22
- return text;
23
- return `${text.slice(0, TOOL_OUTPUT_MAX_LENGTH)}…`;
24
- }
25
- function toolCallWidget(args) {
26
- return toolTraceWidget({
27
- widgetId: args.widgetId,
28
- groupId: 'github:tools',
29
- title: args.title,
30
- body: args.body,
31
- });
32
- }
33
- function findMcpTool(tools, name) {
34
- const direct = tools[name];
35
- if (direct)
36
- return direct;
37
- const needle = name.toLowerCase();
38
- for (const [key, tool] of Object.entries(tools)) {
39
- if (key.toLowerCase() === needle || key.toLowerCase().includes(needle)) {
40
- return tool;
41
- }
42
- }
43
- return undefined;
44
- }
45
- async function fetchPrFilesDiffWidget(args) {
46
- const identity = prIdentityFromInput(args.input);
47
- if (!identity)
48
- return null;
49
- const tool = findMcpTool(args.tools, 'pull_request_read');
50
- if (!tool?.execute)
51
- return null;
52
- try {
53
- const output = await tool.execute({
54
- method: 'get_files',
55
- owner: identity.owner,
56
- repo: identity.repo,
57
- pullNumber: identity.pullNumber,
58
- perPage: 100,
59
- }, { toolCallId: args.widgetId, messages: [] });
60
- return diffWidgetFromTool({
61
- widgetId: args.widgetId,
62
- toolName: args.toolName,
63
- input: {
64
- ...(typeof args.input === 'object' && args.input ? args.input : {}),
65
- method: 'get_files',
66
- ...identity,
67
- },
68
- output,
69
- });
70
- }
71
- catch {
72
- return null;
73
- }
74
- }
75
- const SYSTEM_PROMPT = `You are a GitHub assistant. Use the GitHub MCP tools to help with repositories, issues, and pull requests.
76
- Parse owner/repo from "owner/repo" when the user provides that format.
77
- Be concise and helpful.
78
-
79
- When the user asks to inspect, review, or see a pull request, commit, or code change:
80
- - Call pull_request_read with method get for title/author/status if needed.
81
- - Always also call pull_request_read with method get_files (preferred; includes per-file patches) or get_diff.
82
- A Diff widget is rendered from that file/diff result — keep your text reply to a short summary, not a pasted patch.`;
83
- export async function* runGithubAgent(args) {
84
- const mcpClient = await createMCPClient({
85
- transport: {
86
- type: 'http',
87
- url: GITHUB_MCP_URL,
88
- headers: {
89
- Authorization: `Bearer ${args.githubToken}`,
90
- 'X-MCP-Toolsets': 'repos,issues,pull_requests',
91
- },
92
- },
93
- });
94
- const queue = [];
95
- let wake;
96
- let agentDone = false;
97
- let agentError;
98
- const enqueue = (event) => {
99
- queue.push(event);
100
- wake?.();
101
- wake = undefined;
102
- };
103
- const waitForQueue = () => new Promise((resolve) => {
104
- wake = resolve;
105
- });
106
- const agentTask = (async () => {
107
- try {
108
- const tools = await mcpClient.tools();
109
- const model = resolveOpenAiModel(args.model ?? 'openai/gpt-4o', {
110
- authMode: args.authMode,
111
- openaiApiKey: args.openaiApiKey,
112
- });
113
- const result = await generateText({
114
- model,
115
- stopWhen: stepCountIs(5),
116
- system: SYSTEM_PROMPT,
117
- prompt: args.prompt,
118
- tools,
119
- onToolExecutionStart: ({ toolCall }) => {
120
- enqueue({
121
- kind: 'widget',
122
- widget: toolCallWidget({
123
- widgetId: toolCall.toolCallId,
124
- title: toolCall.toolName,
125
- }),
126
- });
127
- },
128
- onToolExecutionEnd: async ({ toolCall, toolOutput }) => {
129
- const input = toolInputFrom(toolCall, toolOutput);
130
- const payload = unwrapToolOutput(toolOutput);
131
- enqueue({
132
- kind: 'widget',
133
- widget: toolCallWidget({
134
- widgetId: toolCall.toolCallId,
135
- title: toolCall.toolName,
136
- body: formatToolOutput(payload),
137
- }),
138
- });
139
- const widgetId = `github-diff:${toolCall.toolCallId}`;
140
- let widget = diffWidgetFromTool({
141
- widgetId,
142
- toolName: toolCall.toolName,
143
- input,
144
- output: payload,
145
- });
146
- if (!widget &&
147
- shouldFollowUpPrFiles({
148
- toolName: toolCall.toolName,
149
- input,
150
- hasDiffWidget: false,
151
- })) {
152
- widget = await fetchPrFilesDiffWidget({
153
- tools: tools,
154
- widgetId,
155
- toolName: toolCall.toolName,
156
- input,
157
- });
158
- }
159
- if (widget)
160
- enqueue({ kind: 'widget', widget });
161
- },
162
- });
163
- if (result.text.trim()) {
164
- enqueue({ kind: 'reply', content: result.text.trim() });
165
- }
166
- }
167
- catch (error) {
168
- agentError = error;
169
- }
170
- finally {
171
- agentDone = true;
172
- wake?.();
173
- wake = undefined;
174
- }
175
- })();
176
- try {
177
- while (!agentDone || queue.length > 0) {
178
- if (queue.length === 0) {
179
- await waitForQueue();
180
- continue;
181
- }
182
- yield queue.shift();
183
- }
184
- await agentTask;
185
- if (agentError)
186
- throw agentError;
187
- }
188
- finally {
189
- await mcpClient.close().catch(() => undefined);
190
- }
191
- }
@@ -1,10 +0,0 @@
1
- /** True when this runtime is a platform-managed cloud deployment. */
2
- export const isCloudMode = () => process.env.OPENBOT_CLOUD_MODE === "1";
3
- /** Default auth mode: Credits on cloud, BYOK locally. */
4
- export const defaultAuthMode = () => isCloudMode() ? "credits" : "byok";
5
- export function resolveAuthMode(config) {
6
- if (config.authMode === "byok" || config.authMode === "credits") {
7
- return config.authMode;
8
- }
9
- return defaultAuthMode();
10
- }
package/dist/config.js DELETED
@@ -1,74 +0,0 @@
1
- import { resolveAuthMode } from "./cloud-mode.js";
2
- import { shouldUseCreditsAuth } from "./credits-auth.js";
3
- export const GITHUB_TOKEN_VAR = "GITHUB_TOKEN";
4
- function variableValue(variables, key) {
5
- const entry = variables[key];
6
- if (typeof entry === "string")
7
- return entry || undefined;
8
- return entry?.value || undefined;
9
- }
10
- export function readGithubConfig(config) {
11
- return {
12
- githubToken: typeof config.githubToken === "string" && config.githubToken.trim()
13
- ? config.githubToken.trim()
14
- : undefined,
15
- authMode: config.authMode === "byok" || config.authMode === "credits"
16
- ? config.authMode
17
- : undefined,
18
- model: typeof config.model === "string" && config.model.trim()
19
- ? config.model.trim()
20
- : undefined,
21
- };
22
- }
23
- export function formatMissingCredentials(missing, authMode) {
24
- const lines = [
25
- "GitHub agent setup is incomplete. Configure the following in plugin config, workspace settings, or environment variables:",
26
- ];
27
- if (missing.includes("githubToken")) {
28
- lines.push("- `githubToken` / `GITHUB_TOKEN` — GitHub Personal Access Token with repo scope");
29
- }
30
- if (missing.includes("openaiApiKey")) {
31
- if (authMode === "credits") {
32
- lines.push("- OpenAI API key is required in BYOK mode — add `OPENAI_API_KEY` under workspace settings, or switch `authMode` to `credits` on cloud");
33
- }
34
- else {
35
- lines.push("- `OPENAI_API_KEY` — OpenAI API key for the agent loop (BYOK mode), or switch `authMode` to `credits` on cloud");
36
- }
37
- }
38
- return lines.join("\n");
39
- }
40
- export async function resolveGithubCredentials(config, storage) {
41
- const authMode = resolveAuthMode({ authMode: config.authMode });
42
- const useCredits = shouldUseCreditsAuth({ authMode });
43
- const variables = (await storage.getVariables().catch(() => ({})));
44
- const resolve = (configKey, envKey) => {
45
- const fromConfig = config[configKey];
46
- if (typeof fromConfig === "string" && fromConfig.trim()) {
47
- return fromConfig.trim();
48
- }
49
- if (process.env[envKey]?.trim())
50
- return process.env[envKey].trim();
51
- return variableValue(variables, envKey)?.trim();
52
- };
53
- const githubToken = resolve("githubToken", GITHUB_TOKEN_VAR);
54
- const openaiApiKey = process.env.OPENAI_API_KEY?.trim() ||
55
- variableValue(variables, "OPENAI_API_KEY");
56
- const model = resolve("model", "OPENAI_MODEL") ?? "openai/gpt-4o";
57
- const missing = [];
58
- if (!githubToken)
59
- missing.push("githubToken");
60
- if (!useCredits && !openaiApiKey)
61
- missing.push("openaiApiKey");
62
- if (missing.length > 0) {
63
- return { ok: false, missing, authMode };
64
- }
65
- return {
66
- ok: true,
67
- credentials: {
68
- githubToken: githubToken,
69
- authMode,
70
- openaiApiKey: openaiApiKey || undefined,
71
- model,
72
- },
73
- };
74
- }
@@ -1,53 +0,0 @@
1
- import { isCloudMode } from "./cloud-mode.js";
2
- export const INTEGRATIONS_TOKEN_HEADER = "x-openbot-integrations-token";
3
- export const CREDITS_API_KEY_PLACEHOLDER = "openbot-credits";
4
- /** Cloud host injects these when routing LLM calls through OpenBot Credits. */
5
- export function resolveCreditsAuthConfig() {
6
- const baseUrl = process.env.OPENBOT_INTEGRATIONS_BASE_URL?.trim();
7
- const token = process.env.OPENBOT_INTEGRATIONS_TOKEN?.trim();
8
- if (!baseUrl || !token)
9
- return undefined;
10
- return { baseUrl: baseUrl.replace(/\/$/, ""), token };
11
- }
12
- export function creditsProviderBaseUrl(config) {
13
- return `${config.baseUrl}/openai/v1`;
14
- }
15
- export function shouldUseCreditsAuth(options) {
16
- if (options?.authMode === "byok")
17
- return false;
18
- if (options?.authMode === "credits")
19
- return true;
20
- return isCloudMode() && resolveCreditsAuthConfig() !== undefined;
21
- }
22
- export function isCreditsErrorMessage(message) {
23
- const lower = message.toLowerCase();
24
- return (lower.includes("insufficient_credits") ||
25
- lower.includes("insufficient credits") ||
26
- lower.includes("402"));
27
- }
28
- export function isAuthErrorMessage(message) {
29
- const lower = message.toLowerCase();
30
- return (lower.includes("api key") ||
31
- lower.includes("401") ||
32
- lower.includes("unauthorized") ||
33
- lower.includes("authentication"));
34
- }
35
- export function isIntegrationsProviderError(message) {
36
- const lower = message.toLowerCase();
37
- return (lower.includes("provider api key not configured") ||
38
- (lower.includes("503") && lower.includes("provider")));
39
- }
40
- export const CREDITS_NOT_CONFIGURED_MESSAGE = "OpenBot Credits is not configured on this runtime. The cloud host must set OPENBOT_INTEGRATIONS_BASE_URL and OPENBOT_INTEGRATIONS_TOKEN (try redeploying the workspace).";
41
- export const CREDITS_PROVIDER_UNAVAILABLE_MESSAGE = "OpenBot Credits could not reach OpenAI — the platform provider API key is not configured yet. Try again later or switch this agent to BYOK mode.";
42
- export const CREDITS_AUTH_FAILED_MESSAGE = "GitHub could not authenticate via OpenBot Credits. Check your workspace credit balance in settings, or switch this agent to BYOK mode.";
43
- export function creditsErrorMessage(message) {
44
- if (isIntegrationsProviderError(message)) {
45
- return CREDITS_PROVIDER_UNAVAILABLE_MESSAGE;
46
- }
47
- if (isCreditsErrorMessage(message)) {
48
- return "Insufficient workspace credits. Add credits in workspace settings or switch this agent to BYOK mode.";
49
- }
50
- if (isAuthErrorMessage(message))
51
- return CREDITS_AUTH_FAILED_MESSAGE;
52
- return undefined;
53
- }
package/dist/diff.js DELETED
@@ -1,346 +0,0 @@
1
- const MAX_FILES = 40;
2
- const MAX_PATCH_CHARS = 48_000;
3
- const LANG_BY_EXT = {
4
- ts: 'typescript',
5
- tsx: 'tsx',
6
- js: 'javascript',
7
- jsx: 'jsx',
8
- mjs: 'javascript',
9
- cjs: 'javascript',
10
- py: 'python',
11
- go: 'go',
12
- rs: 'rust',
13
- rb: 'ruby',
14
- java: 'java',
15
- kt: 'kotlin',
16
- swift: 'swift',
17
- cs: 'csharp',
18
- cpp: 'cpp',
19
- cc: 'cpp',
20
- cxx: 'cpp',
21
- c: 'c',
22
- h: 'c',
23
- hpp: 'cpp',
24
- md: 'markdown',
25
- json: 'json',
26
- css: 'css',
27
- scss: 'scss',
28
- html: 'html',
29
- yml: 'yaml',
30
- yaml: 'yaml',
31
- toml: 'toml',
32
- sh: 'bash',
33
- bash: 'bash',
34
- zsh: 'bash',
35
- sql: 'sql',
36
- };
37
- function isRecord(value) {
38
- return typeof value === 'object' && value !== null && !Array.isArray(value);
39
- }
40
- function asString(value) {
41
- return typeof value === 'string' && value.length > 0 ? value : undefined;
42
- }
43
- function asNumber(value) {
44
- if (typeof value === 'number' && Number.isFinite(value))
45
- return value;
46
- if (typeof value === 'string' && value.trim() !== '') {
47
- const parsed = Number(value);
48
- if (Number.isFinite(parsed))
49
- return parsed;
50
- }
51
- return undefined;
52
- }
53
- function languageFromPath(path) {
54
- const base = path.split('/').pop() ?? path;
55
- const ext = base.includes('.') ? base.slice(base.lastIndexOf('.') + 1).toLowerCase() : '';
56
- return LANG_BY_EXT[ext];
57
- }
58
- function mapStatus(status, oldPath) {
59
- switch (status) {
60
- case 'added':
61
- return 'added';
62
- case 'removed':
63
- case 'deleted':
64
- return 'deleted';
65
- case 'renamed':
66
- case 'copied':
67
- return 'renamed';
68
- default:
69
- return oldPath ? 'renamed' : 'modified';
70
- }
71
- }
72
- function countPatchStats(patch) {
73
- let additions = 0;
74
- let deletions = 0;
75
- for (const line of patch.split('\n')) {
76
- if (line.startsWith('+') && !line.startsWith('+++'))
77
- additions += 1;
78
- else if (line.startsWith('-') && !line.startsWith('---'))
79
- deletions += 1;
80
- }
81
- return { additions, deletions };
82
- }
83
- function capPatch(patch) {
84
- if (!patch)
85
- return {};
86
- if (patch.length <= MAX_PATCH_CHARS)
87
- return { patch };
88
- return { patch: patch.slice(0, MAX_PATCH_CHARS), truncated: true };
89
- }
90
- export function unwrapToolOutput(output) {
91
- if (output == null)
92
- return output;
93
- if (typeof output === 'string') {
94
- const trimmed = output.trim();
95
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
96
- try {
97
- return JSON.parse(trimmed);
98
- }
99
- catch {
100
- return output;
101
- }
102
- }
103
- return output;
104
- }
105
- if (Array.isArray(output)) {
106
- if (output.length > 0 &&
107
- output.every((part) => typeof part === 'string' ||
108
- (isRecord(part) && (typeof part.text === 'string' || typeof part.value === 'string')))) {
109
- const text = output
110
- .map((part) => {
111
- if (typeof part === 'string')
112
- return part;
113
- if (isRecord(part))
114
- return asString(part.text) ?? asString(part.value) ?? '';
115
- return '';
116
- })
117
- .join('\n');
118
- return unwrapToolOutput(text);
119
- }
120
- return output;
121
- }
122
- if (isRecord(output)) {
123
- // AI SDK onToolExecutionEnd passes { type: 'tool-result', output } / { type: 'tool-error', error }.
124
- if (output.type === 'tool-result' && 'output' in output) {
125
- return unwrapToolOutput(output.output);
126
- }
127
- if (output.type === 'tool-error' && 'error' in output) {
128
- return unwrapToolOutput(output.error);
129
- }
130
- if (typeof output.text === 'string' && Object.keys(output).length <= 3) {
131
- return unwrapToolOutput(output.text);
132
- }
133
- if (Array.isArray(output.content))
134
- return unwrapToolOutput(output.content);
135
- if ('value' in output)
136
- return unwrapToolOutput(output.value);
137
- }
138
- return output;
139
- }
140
- /** Prefer the AI SDK tool-result input; fall back to the tool call. */
141
- export function toolInputFrom(toolCall, toolOutput) {
142
- if (isRecord(toolOutput) && 'input' in toolOutput)
143
- return toolOutput.input;
144
- if (isRecord(toolCall) && 'input' in toolCall)
145
- return toolCall.input;
146
- return undefined;
147
- }
148
- export function prIdentityFromInput(input) {
149
- if (!isRecord(input))
150
- return null;
151
- const owner = asString(input.owner);
152
- const repo = asString(input.repo);
153
- const pullNumber = asNumber(input.pullNumber) ?? asNumber(input.pull_number);
154
- if (!owner || !repo || pullNumber == null)
155
- return null;
156
- return { owner, repo, pullNumber };
157
- }
158
- const PR_FILE_FOLLOW_UP_METHODS = new Set(['get', 'get_files', 'get_diff']);
159
- /** True when pull_request_read didn't yield a Diff widget but we can still fetch files. */
160
- export function shouldFollowUpPrFiles(args) {
161
- if (args.hasDiffWidget)
162
- return false;
163
- if (!args.toolName.toLowerCase().includes('pull_request_read'))
164
- return false;
165
- if (!prIdentityFromInput(args.input))
166
- return false;
167
- const method = isRecord(args.input) ? asString(args.input.method)?.toLowerCase() : undefined;
168
- if (!method)
169
- return true;
170
- return PR_FILE_FOLLOW_UP_METHODS.has(method);
171
- }
172
- function toDiffFile(file) {
173
- const path = asString(file.filename);
174
- if (!path)
175
- return null;
176
- const oldPath = asString(file.previous_filename);
177
- const rawPatch = asString(file.patch);
178
- const capped = capPatch(rawPatch);
179
- const stats = rawPatch ? countPatchStats(rawPatch) : { additions: 0, deletions: 0 };
180
- return {
181
- path,
182
- ...(oldPath ? { oldPath } : {}),
183
- status: mapStatus(asString(file.status), oldPath),
184
- ...(languageFromPath(path) ? { language: languageFromPath(path) } : {}),
185
- additions: asNumber(file.additions) ?? (stats.additions || undefined),
186
- deletions: asNumber(file.deletions) ?? (stats.deletions || undefined),
187
- ...capped,
188
- };
189
- }
190
- export function splitUnifiedDiff(raw) {
191
- const text = raw.replace(/\r\n/g, '\n');
192
- const starts = [];
193
- const header = /^diff --git /gm;
194
- let match;
195
- while ((match = header.exec(text)))
196
- starts.push(match.index);
197
- if (starts.length === 0) {
198
- if (!text.trim())
199
- return [];
200
- const stats = countPatchStats(text);
201
- const capped = capPatch(text);
202
- return [
203
- {
204
- path: 'diff',
205
- status: 'modified',
206
- additions: stats.additions || undefined,
207
- deletions: stats.deletions || undefined,
208
- ...capped,
209
- },
210
- ];
211
- }
212
- return starts
213
- .map((start, index) => {
214
- const chunk = text.slice(start, starts[index + 1]);
215
- const names = /^diff --git a\/(.+?) b\/(.+)$/m.exec(chunk);
216
- const oldPath = names?.[1] ?? 'unknown';
217
- const path = names?.[2] ?? oldPath;
218
- let status = 'modified';
219
- if (/^new file mode /m.test(chunk) || oldPath === '/dev/null')
220
- status = 'added';
221
- else if (/^deleted file mode /m.test(chunk) || path === '/dev/null')
222
- status = 'deleted';
223
- else if (/^rename from /m.test(chunk) || oldPath !== path)
224
- status = 'renamed';
225
- const stats = countPatchStats(chunk);
226
- return {
227
- path: path === '/dev/null' ? oldPath : path,
228
- ...(status === 'renamed' && oldPath !== path ? { oldPath } : {}),
229
- status,
230
- ...(languageFromPath(path) ? { language: languageFromPath(path) } : {}),
231
- additions: stats.additions || undefined,
232
- deletions: stats.deletions || undefined,
233
- ...capPatch(chunk),
234
- };
235
- })
236
- .slice(0, MAX_FILES);
237
- }
238
- function githubFileFromUnknown(item) {
239
- if (!isRecord(item))
240
- return null;
241
- const filename = asString(item.filename) ?? asString(item.path) ?? asString(item.name);
242
- if (!filename)
243
- return null;
244
- return {
245
- filename,
246
- previous_filename: asString(item.previous_filename) ??
247
- asString(item.previousFilename) ??
248
- asString(item.oldPath) ??
249
- asString(item.old_path),
250
- status: item.status,
251
- additions: item.additions,
252
- deletions: item.deletions,
253
- patch: item.patch,
254
- };
255
- }
256
- function filesFromUnknown(payload) {
257
- if (Array.isArray(payload)) {
258
- const files = payload
259
- .map(githubFileFromUnknown)
260
- .filter((file) => file != null);
261
- return files.length > 0 ? files : null;
262
- }
263
- if (isRecord(payload) && Array.isArray(payload.files)) {
264
- return filesFromUnknown(payload.files);
265
- }
266
- return null;
267
- }
268
- function isChangedFilePayload(payload) {
269
- const files = filesFromUnknown(payload);
270
- if (!files?.length)
271
- return false;
272
- return files.some((file) => typeof file.patch === 'string' ||
273
- typeof file.status === 'string' ||
274
- typeof file.additions === 'number' ||
275
- typeof file.deletions === 'number');
276
- }
277
- function summarize(files) {
278
- const additions = files.reduce((sum, file) => sum + (file.additions ?? 0), 0);
279
- const deletions = files.reduce((sum, file) => sum + (file.deletions ?? 0), 0);
280
- const fileLabel = files.length === 1 ? '1 file' : `${files.length} files`;
281
- if (!additions && !deletions)
282
- return fileLabel;
283
- return `${fileLabel} · +${additions} −${deletions}`;
284
- }
285
- function titleFromInput(input) {
286
- if (!input)
287
- return 'Diff';
288
- const owner = asString(input.owner);
289
- const repo = asString(input.repo);
290
- const pullNumber = asNumber(input.pullNumber) ?? asNumber(input.pull_number);
291
- const sha = asString(input.sha);
292
- if (owner && repo && pullNumber != null)
293
- return `${owner}/${repo}#${pullNumber}`;
294
- if (owner && repo && sha)
295
- return `${owner}/${repo}@${sha.slice(0, 7)}`;
296
- if (owner && repo)
297
- return `${owner}/${repo}`;
298
- return 'Diff';
299
- }
300
- function filesFromPayload(payload) {
301
- if (typeof payload === 'string') {
302
- const trimmed = payload.trim();
303
- if (trimmed.startsWith('diff --git') || trimmed.startsWith('@@')) {
304
- return splitUnifiedDiff(payload);
305
- }
306
- return null;
307
- }
308
- const githubFiles = filesFromUnknown(payload);
309
- if (!githubFiles)
310
- return null;
311
- const files = githubFiles
312
- .map(toDiffFile)
313
- .filter((file) => file != null)
314
- .slice(0, MAX_FILES);
315
- return files.length > 0 ? files : null;
316
- }
317
- export function diffWidgetFromTool(args) {
318
- const toolName = args.toolName.toLowerCase();
319
- const input = isRecord(args.input) ? args.input : undefined;
320
- const method = asString(input?.method)?.toLowerCase();
321
- const payload = unwrapToolOutput(args.output);
322
- const looksLikeDiffTool = (toolName.includes('pull_request_read') &&
323
- (method === 'get_files' || method === 'get_diff')) ||
324
- toolName.includes('get_commit') ||
325
- toolName.includes('get_diff') ||
326
- (typeof payload === 'string' && payload.trim().startsWith('diff --git')) ||
327
- isChangedFilePayload(payload);
328
- if (!looksLikeDiffTool)
329
- return null;
330
- const files = filesFromPayload(payload);
331
- if (!files || files.length === 0)
332
- return null;
333
- return {
334
- kind: 'diff',
335
- widgetId: args.widgetId,
336
- title: titleFromInput(input),
337
- description: summarize(files),
338
- files,
339
- size: 'full',
340
- display: 'expanded',
341
- metadata: {
342
- toolName: args.toolName,
343
- ...(method ? { method } : {}),
344
- },
345
- };
346
- }
package/dist/model.js DELETED
@@ -1,24 +0,0 @@
1
- import { createOpenAI } from "@ai-sdk/openai";
2
- import { CREDITS_API_KEY_PLACEHOLDER, INTEGRATIONS_TOKEN_HEADER, creditsProviderBaseUrl, resolveCreditsAuthConfig, shouldUseCreditsAuth, } from "./credits-auth.js";
3
- function normalizeOpenAiModelId(model) {
4
- return model.includes("/") ? model.split("/").slice(1).join("/") : model;
5
- }
6
- export function resolveOpenAiModel(model, options) {
7
- const modelId = normalizeOpenAiModelId(model);
8
- const useCredits = shouldUseCreditsAuth(options);
9
- if (useCredits) {
10
- const config = resolveCreditsAuthConfig();
11
- if (!config) {
12
- throw new Error("OpenBot Credits is not configured. The cloud host must set OPENBOT_INTEGRATIONS_BASE_URL and OPENBOT_INTEGRATIONS_TOKEN.");
13
- }
14
- const baseURL = creditsProviderBaseUrl(config);
15
- const headers = { [INTEGRATIONS_TOKEN_HEADER]: config.token };
16
- const apiKey = config.token || CREDITS_API_KEY_PLACEHOLDER;
17
- return createOpenAI({ baseURL, apiKey, headers })(modelId);
18
- }
19
- const apiKey = options?.openaiApiKey?.trim();
20
- if (!apiKey) {
21
- throw new Error("OpenAI API key is required in BYOK mode. Add `OPENAI_API_KEY` under workspace settings or switch `authMode` to `credits` on cloud.");
22
- }
23
- return createOpenAI({ apiKey })(modelId);
24
- }