open-grok-build 0.1.0
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/LICENSE +21 -0
- package/README.md +115 -0
- package/package.json +79 -0
- package/src/auth/oauth.ts +529 -0
- package/src/index.ts +5 -0
- package/src/models/catalog.ts +149 -0
- package/src/opencode/billing.ts +57 -0
- package/src/opencode/collectGrokTools.ts +18 -0
- package/src/opencode/grokModels.ts +72 -0
- package/src/opencode/grokToolSchemas.ts +88 -0
- package/src/opencode/plugin.ts +227 -0
- package/src/opencode/tui.tsx +64 -0
- package/src/opencode/usage.ts +82 -0
- package/src/opencode/usageToast.ts +9 -0
- package/src/opencode/version.ts +2 -0
- package/src/payload/sanitize.ts +320 -0
- package/src/shared/errors.ts +44 -0
- package/src/tools/files.ts +538 -0
- package/src/tools/register.ts +31 -0
- package/src/tools/rendering.ts +260 -0
- package/src/tools/search.ts +195 -0
- package/src/tools/shell.ts +142 -0
- package/src/tools/types.ts +29 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getBaseUrl } from '../auth/oauth.js';
|
|
2
|
+
|
|
3
|
+
interface BillingUsage {
|
|
4
|
+
monthlyLimit: number;
|
|
5
|
+
used: number;
|
|
6
|
+
billingPeriodEnd: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseBillingUsage(payload: unknown): BillingUsage {
|
|
10
|
+
if (!payload || typeof payload !== 'object') throw new Error('invalid billing payload');
|
|
11
|
+
const config = (payload as Record<string, unknown>).config;
|
|
12
|
+
if (!config || typeof config !== 'object') throw new Error('invalid billing payload');
|
|
13
|
+
const monthlyLimit = ((config as Record<string, unknown>).monthlyLimit as Record<string, unknown>)
|
|
14
|
+
?.val;
|
|
15
|
+
const used = ((config as Record<string, unknown>).used as Record<string, unknown>)?.val;
|
|
16
|
+
const billingPeriodEnd = (config as Record<string, unknown>).billingPeriodEnd;
|
|
17
|
+
if (
|
|
18
|
+
typeof monthlyLimit !== 'number' ||
|
|
19
|
+
!Number.isFinite(monthlyLimit) ||
|
|
20
|
+
typeof used !== 'number' ||
|
|
21
|
+
!Number.isFinite(used) ||
|
|
22
|
+
typeof billingPeriodEnd !== 'string' ||
|
|
23
|
+
!Number.isFinite(new Date(billingPeriodEnd).getTime())
|
|
24
|
+
) {
|
|
25
|
+
throw new Error('invalid billing payload');
|
|
26
|
+
}
|
|
27
|
+
return { monthlyLimit, used, billingPeriodEnd };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function fetchBillingUsage(token: string): Promise<BillingUsage> {
|
|
31
|
+
const response = await fetch(`${getBaseUrl()}/billing`, {
|
|
32
|
+
headers: {
|
|
33
|
+
authorization: `Bearer ${token}`,
|
|
34
|
+
'x-xai-token-auth': 'xai-grok-cli',
|
|
35
|
+
accept: 'application/json',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) throw new Error(`billing endpoint returned ${response.status}`);
|
|
39
|
+
return parseBillingUsage(await response.json());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatQuota(usage: BillingUsage | undefined) {
|
|
43
|
+
if (!usage) {
|
|
44
|
+
return [
|
|
45
|
+
' Usage:',
|
|
46
|
+
' no billing data available — run /connect grok-build or set GROK_BUILD_OAUTH_TOKEN',
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resetDate = new Date(new Date(usage.billingPeriodEnd).getTime() - 8 * 60 * 60 * 1000);
|
|
51
|
+
return [
|
|
52
|
+
' Usage:',
|
|
53
|
+
` ${usage.used.toLocaleString()} / ${usage.monthlyLimit.toLocaleString()} credits used (${Math.round((usage.used / usage.monthlyLimit) * 100)}%)`,
|
|
54
|
+
` ${(usage.monthlyLimit - usage.used).toLocaleString()} credits remaining`,
|
|
55
|
+
` Resets at ${['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][resetDate.getUTCMonth()]} ${resetDate.getUTCDate()} ${resetDate.getUTCHours().toString().padStart(2, '0')}:${resetDate.getUTCMinutes().toString().padStart(2, '0')} PT`,
|
|
56
|
+
];
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registerGrokTools } from '../tools/register.js';
|
|
2
|
+
import type { ShimRegisteredTool } from '../tools/types.js';
|
|
3
|
+
|
|
4
|
+
export type CollectedGrokTool = Pick<ShimRegisteredTool, 'name' | 'description' | 'execute'>;
|
|
5
|
+
|
|
6
|
+
export function collectGrokShimTools(): CollectedGrokTool[] {
|
|
7
|
+
const tools: CollectedGrokTool[] = [];
|
|
8
|
+
registerGrokTools({
|
|
9
|
+
registerTool(tool) {
|
|
10
|
+
tools.push({
|
|
11
|
+
name: tool.name,
|
|
12
|
+
description: tool.description,
|
|
13
|
+
execute: tool.execute,
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
return tools;
|
|
18
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @ts-nocheck — catalog fields extend SDK Model shape for grok-build provider config.
|
|
2
|
+
import type { Model as ModelV2 } from '@opencode-ai/sdk/v2';
|
|
3
|
+
import { type GrokBuildModelConfig, resolveModels } from '../models/catalog.js';
|
|
4
|
+
|
|
5
|
+
export const GROK_BUILD_PROVIDER_ID = 'grok-build';
|
|
6
|
+
|
|
7
|
+
export function grokBuildProviderConfig() {
|
|
8
|
+
const models = resolveModels();
|
|
9
|
+
return {
|
|
10
|
+
name: 'Grok Build',
|
|
11
|
+
npm: '@ai-sdk/openai-compatible',
|
|
12
|
+
api: 'https://cli-chat-proxy.grok.com/v1',
|
|
13
|
+
models: Object.fromEntries(
|
|
14
|
+
models.map((m) => [
|
|
15
|
+
m.id,
|
|
16
|
+
{
|
|
17
|
+
name: m.name,
|
|
18
|
+
reasoning: m.reasoning,
|
|
19
|
+
modalities: { input: m.input },
|
|
20
|
+
cost: {
|
|
21
|
+
input: m.cost.input,
|
|
22
|
+
output: m.cost.output,
|
|
23
|
+
cache: { read: m.cost.cacheRead, write: m.cost.cacheWrite },
|
|
24
|
+
},
|
|
25
|
+
limit: {
|
|
26
|
+
context: m.contextWindow,
|
|
27
|
+
output: m.maxTokens,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
]),
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function toPluginModels(
|
|
36
|
+
providerModels: Record<string, ModelV2>,
|
|
37
|
+
catalog: GrokBuildModelConfig[],
|
|
38
|
+
): Record<string, ModelV2> {
|
|
39
|
+
const byId = new Map(catalog.map((m) => [m.id, m]));
|
|
40
|
+
const result: Record<string, ModelV2> = {};
|
|
41
|
+
|
|
42
|
+
for (const [modelID, model] of Object.entries(providerModels)) {
|
|
43
|
+
const entry = byId.get(modelID);
|
|
44
|
+
if (!entry) {
|
|
45
|
+
result[modelID] = model;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
result[modelID] = {
|
|
49
|
+
...model,
|
|
50
|
+
name: entry.name,
|
|
51
|
+
reasoning: entry.reasoning,
|
|
52
|
+
modalities: { input: entry.input },
|
|
53
|
+
cost: {
|
|
54
|
+
input: entry.cost.input,
|
|
55
|
+
output: entry.cost.output,
|
|
56
|
+
cache: { read: entry.cost.cacheRead, write: entry.cost.cacheWrite },
|
|
57
|
+
},
|
|
58
|
+
limit: {
|
|
59
|
+
context: entry.contextWindow,
|
|
60
|
+
output: entry.maxTokens,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const entry of catalog) {
|
|
66
|
+
if (!result[entry.id]) {
|
|
67
|
+
result[entry.id] = providerModels[entry.id] ?? ({} as ModelV2);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
|
|
3
|
+
/** Zod arg schemas for Cursor/Grok shim tools (execute logic from registerGrokTools). */
|
|
4
|
+
export const grokToolArgSchemas: Record<string, Record<string, unknown>> = {
|
|
5
|
+
Grep: {
|
|
6
|
+
pattern: tool.schema.string().describe('Regex pattern to search for in file contents'),
|
|
7
|
+
path: tool.schema
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe('Directory or file to search. Defaults to current working directory.'),
|
|
11
|
+
include: tool.schema
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Glob pattern to filter which files are searched (e.g. *.ts, **/*.md)'),
|
|
15
|
+
},
|
|
16
|
+
Glob: {
|
|
17
|
+
pattern: tool.schema
|
|
18
|
+
.string()
|
|
19
|
+
.describe('Glob pattern to match files (e.g. **/*.ts, src/**/*.json)'),
|
|
20
|
+
path: tool.schema
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Directory to search within. Defaults to current working directory.'),
|
|
24
|
+
},
|
|
25
|
+
LS: {
|
|
26
|
+
path: tool.schema.string().describe('Directory path to list'),
|
|
27
|
+
},
|
|
28
|
+
Read: {
|
|
29
|
+
path: tool.schema.string().describe('Path to the file to read'),
|
|
30
|
+
offset: tool.schema
|
|
31
|
+
.number()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Line number to start reading from (0-indexed)'),
|
|
34
|
+
limit: tool.schema.number().optional().describe('Maximum number of lines to read'),
|
|
35
|
+
},
|
|
36
|
+
Write: {
|
|
37
|
+
path: tool.schema.string().describe('Path to the file to write'),
|
|
38
|
+
content: tool.schema.string().describe('Content to write to the file'),
|
|
39
|
+
},
|
|
40
|
+
StrReplace: {
|
|
41
|
+
path: tool.schema.string().describe('Path to the file to modify'),
|
|
42
|
+
old_str: tool.schema.string().describe('String to search for (exact match)'),
|
|
43
|
+
new_str: tool.schema.string().describe('String to replace with'),
|
|
44
|
+
},
|
|
45
|
+
Edit: {
|
|
46
|
+
path: tool.schema.string().describe('Path to the file to modify'),
|
|
47
|
+
edits: tool.schema
|
|
48
|
+
.array(
|
|
49
|
+
tool.schema.object({
|
|
50
|
+
oldText: tool.schema.string(),
|
|
51
|
+
newText: tool.schema.string(),
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Exact text replacements to apply sequentially'),
|
|
56
|
+
applyPatch: tool.schema
|
|
57
|
+
.object({ patchContent: tool.schema.string() })
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Unsupported unified patch content'),
|
|
60
|
+
strReplace: tool.schema
|
|
61
|
+
.object({
|
|
62
|
+
oldText: tool.schema.string(),
|
|
63
|
+
newText: tool.schema.string(),
|
|
64
|
+
})
|
|
65
|
+
.optional(),
|
|
66
|
+
multiStrReplace: tool.schema
|
|
67
|
+
.object({
|
|
68
|
+
edits: tool.schema.array(
|
|
69
|
+
tool.schema.object({
|
|
70
|
+
oldText: tool.schema.string(),
|
|
71
|
+
newText: tool.schema.string(),
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
})
|
|
75
|
+
.optional(),
|
|
76
|
+
},
|
|
77
|
+
Delete: {
|
|
78
|
+
path: tool.schema.string().describe('Path to the file to delete'),
|
|
79
|
+
},
|
|
80
|
+
Shell: {
|
|
81
|
+
command: tool.schema.string().describe('Shell command to execute'),
|
|
82
|
+
working_directory: tool.schema
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Working directory for the command'),
|
|
86
|
+
timeout: tool.schema.number().optional().describe('Timeout in milliseconds (default: 120000)'),
|
|
87
|
+
},
|
|
88
|
+
} as const;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// @ts-nocheck — OpenCode config/auth hook payloads are loosely typed at the host boundary.
|
|
2
|
+
import type { Hooks, Plugin, PluginInput } from '@opencode-ai/plugin';
|
|
3
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
4
|
+
import * as oauth from '../auth/oauth.js';
|
|
5
|
+
import { resolveModels } from '../models/catalog.js';
|
|
6
|
+
import { sanitizePayload } from '../payload/sanitize.js';
|
|
7
|
+
import { collectGrokShimTools } from './collectGrokTools.js';
|
|
8
|
+
import { GROK_BUILD_PROVIDER_ID, grokBuildProviderConfig, toPluginModels } from './grokModels.js';
|
|
9
|
+
import { grokToolArgSchemas } from './grokToolSchemas.js';
|
|
10
|
+
|
|
11
|
+
import { OPENCODE_INSTALLATION_VERSION } from './version.js';
|
|
12
|
+
|
|
13
|
+
const GROK_BUILD_VERSION = '0.2.16';
|
|
14
|
+
const OAUTH_DUMMY_KEY = 'opencode-oauth-dummy-key';
|
|
15
|
+
const ACCESS_TOKEN_REFRESH_SKEW_MS = 120_000;
|
|
16
|
+
|
|
17
|
+
const collectedTools = collectGrokShimTools();
|
|
18
|
+
|
|
19
|
+
function accessTokenIsExpiring(
|
|
20
|
+
token: string | undefined,
|
|
21
|
+
skewMs = ACCESS_TOKEN_REFRESH_SKEW_MS,
|
|
22
|
+
): boolean {
|
|
23
|
+
if (!token || typeof token !== 'string') return false;
|
|
24
|
+
const parts = token.split('.');
|
|
25
|
+
if (parts.length < 2) return false;
|
|
26
|
+
try {
|
|
27
|
+
let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
28
|
+
while (payload.length % 4 !== 0) payload += '=';
|
|
29
|
+
const claims = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
|
|
30
|
+
if (typeof claims?.exp !== 'number') return false;
|
|
31
|
+
return claims.exp * 1000 <= Date.now() + Math.max(0, skewMs);
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isGrokBuildModel(model: { providerID: string }) {
|
|
38
|
+
return model.providerID === GROK_BUILD_PROVIDER_ID;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildGrokToolDefinitions() {
|
|
42
|
+
const defs: NonNullable<Hooks['tool']> = {};
|
|
43
|
+
for (const entry of collectedTools) {
|
|
44
|
+
const args = grokToolArgSchemas[entry.name as keyof typeof grokToolArgSchemas];
|
|
45
|
+
if (!args) continue;
|
|
46
|
+
defs[entry.name] = tool({
|
|
47
|
+
description: entry.description,
|
|
48
|
+
args,
|
|
49
|
+
async execute(params, ctx) {
|
|
50
|
+
const result = await entry.execute(
|
|
51
|
+
'opencode',
|
|
52
|
+
params as Record<string, unknown>,
|
|
53
|
+
ctx.abort,
|
|
54
|
+
undefined,
|
|
55
|
+
{ cwd: ctx.directory },
|
|
56
|
+
);
|
|
57
|
+
const text = result.content
|
|
58
|
+
.filter((part) => part.type === 'text')
|
|
59
|
+
.map((part) => part.text)
|
|
60
|
+
.join('\n');
|
|
61
|
+
return {
|
|
62
|
+
title: entry.name,
|
|
63
|
+
output: text,
|
|
64
|
+
metadata: result.details ?? {},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return defs;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const OpenGrokBuildPlugin: Plugin = async (input: PluginInput) => {
|
|
73
|
+
return {
|
|
74
|
+
config: async (cfg) => {
|
|
75
|
+
const existing = cfg.provider?.[GROK_BUILD_PROVIDER_ID];
|
|
76
|
+
if (!existing) {
|
|
77
|
+
if (!cfg.provider) cfg.provider = {};
|
|
78
|
+
cfg.provider[GROK_BUILD_PROVIDER_ID] = grokBuildProviderConfig();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
provider: {
|
|
83
|
+
id: GROK_BUILD_PROVIDER_ID,
|
|
84
|
+
async models(provider, _ctx) {
|
|
85
|
+
return toPluginModels(provider.models, resolveModels());
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
auth: {
|
|
90
|
+
provider: GROK_BUILD_PROVIDER_ID,
|
|
91
|
+
async loader(getAuth) {
|
|
92
|
+
const auth = await getAuth();
|
|
93
|
+
if (auth.type !== 'oauth') return {};
|
|
94
|
+
|
|
95
|
+
let refreshPromise:
|
|
96
|
+
| Promise<{ access: string; refresh: string; expires: number }>
|
|
97
|
+
| undefined;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
apiKey: OAUTH_DUMMY_KEY,
|
|
101
|
+
async fetch(requestInput: RequestInfo | URL, init?: RequestInit) {
|
|
102
|
+
let currentAuth = await getAuth();
|
|
103
|
+
if (currentAuth.type !== 'oauth') return fetch(requestInput, init);
|
|
104
|
+
|
|
105
|
+
const expiresSoon =
|
|
106
|
+
!currentAuth.expires ||
|
|
107
|
+
currentAuth.expires - Date.now() <= ACCESS_TOKEN_REFRESH_SKEW_MS ||
|
|
108
|
+
accessTokenIsExpiring(currentAuth.access);
|
|
109
|
+
|
|
110
|
+
if (expiresSoon) {
|
|
111
|
+
if (!refreshPromise) {
|
|
112
|
+
const refreshToken = currentAuth.refresh;
|
|
113
|
+
refreshPromise = oauth
|
|
114
|
+
.refresh({
|
|
115
|
+
access: currentAuth.access,
|
|
116
|
+
refresh: refreshToken,
|
|
117
|
+
expires: currentAuth.expires ?? 0,
|
|
118
|
+
})
|
|
119
|
+
.then(async (tokens) => {
|
|
120
|
+
const expires = tokens.expires;
|
|
121
|
+
await input.client.auth
|
|
122
|
+
.set({
|
|
123
|
+
path: { id: GROK_BUILD_PROVIDER_ID },
|
|
124
|
+
body: {
|
|
125
|
+
type: 'oauth',
|
|
126
|
+
access: tokens.access,
|
|
127
|
+
refresh: tokens.refresh,
|
|
128
|
+
expires,
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
.catch(() => undefined);
|
|
132
|
+
return {
|
|
133
|
+
access: tokens.access,
|
|
134
|
+
refresh: tokens.refresh,
|
|
135
|
+
expires,
|
|
136
|
+
};
|
|
137
|
+
})
|
|
138
|
+
.finally(() => {
|
|
139
|
+
refreshPromise = undefined;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const refreshed = await refreshPromise;
|
|
143
|
+
currentAuth = { ...currentAuth, ...refreshed };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const headers = new Headers(
|
|
147
|
+
requestInput instanceof Request ? requestInput.headers : undefined,
|
|
148
|
+
);
|
|
149
|
+
if (init?.headers) {
|
|
150
|
+
const entries =
|
|
151
|
+
init.headers instanceof Headers
|
|
152
|
+
? init.headers.entries()
|
|
153
|
+
: Array.isArray(init.headers)
|
|
154
|
+
? init.headers
|
|
155
|
+
: Object.entries(init.headers as Record<string, string | undefined>);
|
|
156
|
+
for (const [key, value] of entries) {
|
|
157
|
+
if (value !== undefined) headers.set(key, String(value));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
headers.set('authorization', `Bearer ${currentAuth.access}`);
|
|
161
|
+
headers.set('x-grok-client-identifier', 'grok-shell');
|
|
162
|
+
headers.set('x-grok-client-version', GROK_BUILD_VERSION);
|
|
163
|
+
headers.set('x-xai-token-auth', 'xai-grok-cli');
|
|
164
|
+
headers.set('User-Agent', `opencode/${OPENCODE_INSTALLATION_VERSION}`);
|
|
165
|
+
|
|
166
|
+
return fetch(requestInput, { ...init, headers });
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
methods: [
|
|
171
|
+
{
|
|
172
|
+
label: 'Grok Build (cli-chat-proxy)',
|
|
173
|
+
type: 'oauth',
|
|
174
|
+
authorize: async () => {
|
|
175
|
+
const session = await oauth.beginGrokBuildOAuth('open-grok-build');
|
|
176
|
+
return {
|
|
177
|
+
url: session.url,
|
|
178
|
+
instructions: session.instructions,
|
|
179
|
+
method: 'auto' as const,
|
|
180
|
+
callback: async () => {
|
|
181
|
+
try {
|
|
182
|
+
const credentials = await session.finish();
|
|
183
|
+
return {
|
|
184
|
+
type: 'success' as const,
|
|
185
|
+
refresh: credentials.refresh,
|
|
186
|
+
access: credentials.access,
|
|
187
|
+
expires: credentials.expires,
|
|
188
|
+
};
|
|
189
|
+
} catch {
|
|
190
|
+
return { type: 'failed' as const };
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
label: 'Grok Build token bypass (GROK_BUILD_OAUTH_TOKEN)',
|
|
198
|
+
type: 'api',
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
'chat.headers': async (chatInput, output) => {
|
|
204
|
+
if (!isGrokBuildModel(chatInput.model)) return;
|
|
205
|
+
output.headers['x-grok-client-identifier'] = 'open-grok-build';
|
|
206
|
+
output.headers['x-grok-client-version'] = GROK_BUILD_VERSION;
|
|
207
|
+
output.headers['x-xai-token-auth'] = 'xai-grok-cli';
|
|
208
|
+
output.headers['x-grok-model-override'] = chatInput.model.id;
|
|
209
|
+
output.headers['x-grok-conv-id'] = chatInput.sessionID;
|
|
210
|
+
output.headers['User-Agent'] = `opencode/${OPENCODE_INSTALLATION_VERSION}`;
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
'chat.params': async (chatInput, output) => {
|
|
214
|
+
if (!isGrokBuildModel(chatInput.model)) return;
|
|
215
|
+
const cwd = input.directory;
|
|
216
|
+
const sanitized = sanitizePayload(
|
|
217
|
+
{ ...output.options },
|
|
218
|
+
chatInput.model.id,
|
|
219
|
+
chatInput.sessionID,
|
|
220
|
+
cwd,
|
|
221
|
+
);
|
|
222
|
+
output.options = { ...output.options, ...sanitized };
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
tool: buildGrokToolDefinitions(),
|
|
226
|
+
};
|
|
227
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { TuiPluginModule } from '@opencode-ai/plugin/tui';
|
|
3
|
+
import { buildGrokBuildUsageReport } from './usage.js';
|
|
4
|
+
import {
|
|
5
|
+
formatUsageToastMessage,
|
|
6
|
+
GROK_BUILD_USAGE_SLASH,
|
|
7
|
+
GROK_BUILD_USAGE_TUI_COMMAND,
|
|
8
|
+
} from './usageToast.js';
|
|
9
|
+
|
|
10
|
+
async function showUsageToast(api: {
|
|
11
|
+
ui: {
|
|
12
|
+
toast: (input: {
|
|
13
|
+
variant?: 'info' | 'success' | 'warning' | 'error';
|
|
14
|
+
title?: string;
|
|
15
|
+
message: string;
|
|
16
|
+
duration?: number;
|
|
17
|
+
}) => void;
|
|
18
|
+
};
|
|
19
|
+
}) {
|
|
20
|
+
const report = await buildGrokBuildUsageReport();
|
|
21
|
+
api.ui.toast({
|
|
22
|
+
variant: 'info',
|
|
23
|
+
title: 'Grok Build',
|
|
24
|
+
message: formatUsageToastMessage(report),
|
|
25
|
+
duration: 8_000,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
30
|
+
id: 'open-grok-build.tui',
|
|
31
|
+
async tui(api) {
|
|
32
|
+
api.keymap.registerLayer({
|
|
33
|
+
commands: [
|
|
34
|
+
{
|
|
35
|
+
name: GROK_BUILD_USAGE_TUI_COMMAND,
|
|
36
|
+
title: 'Grok Build usage',
|
|
37
|
+
description: 'Show Grok Build billing quota and token health',
|
|
38
|
+
category: 'Grok Build',
|
|
39
|
+
namespace: 'palette',
|
|
40
|
+
slashName: GROK_BUILD_USAGE_SLASH,
|
|
41
|
+
async run() {
|
|
42
|
+
await showUsageToast(api);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
api.command?.register?.(() => [
|
|
49
|
+
{
|
|
50
|
+
title: 'Grok Build usage',
|
|
51
|
+
value: GROK_BUILD_USAGE_TUI_COMMAND,
|
|
52
|
+
description: 'Show Grok Build billing quota and token health',
|
|
53
|
+
category: 'Grok Build',
|
|
54
|
+
slash: { name: GROK_BUILD_USAGE_SLASH },
|
|
55
|
+
async onSelect() {
|
|
56
|
+
api.ui.dialog.clear();
|
|
57
|
+
await showUsageToast(api);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default plugin;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fetchBillingUsage, formatQuota } from './billing.js';
|
|
5
|
+
import { GROK_BUILD_PROVIDER_ID } from './grokModels.js';
|
|
6
|
+
|
|
7
|
+
export const GROK_BUILD_USAGE_COMMAND = 'grok-build-usage';
|
|
8
|
+
|
|
9
|
+
export const GROK_BUILD_USAGE_DESCRIPTION =
|
|
10
|
+
'Show Grok Build provider status, billing quota, and token health';
|
|
11
|
+
|
|
12
|
+
type StoredAuth = {
|
|
13
|
+
type?: string;
|
|
14
|
+
access?: string;
|
|
15
|
+
key?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function opencodeAuthPath() {
|
|
19
|
+
return join(homedir(), '.local', 'share', 'opencode', 'auth.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readOpencodeAuthFile(): Record<string, StoredAuth> {
|
|
23
|
+
const path = opencodeAuthPath();
|
|
24
|
+
if (!existsSync(path)) return {};
|
|
25
|
+
try {
|
|
26
|
+
const payload = JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
|
|
27
|
+
if (!payload || typeof payload !== 'object') return {};
|
|
28
|
+
return payload as Record<string, StoredAuth>;
|
|
29
|
+
} catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readOpencodeAuthFromEnv(): Record<string, StoredAuth> {
|
|
35
|
+
const raw = process.env.OPENCODE_AUTH_CONTENT;
|
|
36
|
+
if (!raw) return {};
|
|
37
|
+
try {
|
|
38
|
+
const payload = JSON.parse(raw) as Record<string, unknown>;
|
|
39
|
+
if (!payload || typeof payload !== 'object') return {};
|
|
40
|
+
return payload as Record<string, StoredAuth>;
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveGrokBuildAccessToken(): string | undefined {
|
|
47
|
+
const envToken = process.env.GROK_BUILD_OAUTH_TOKEN;
|
|
48
|
+
if (envToken) return envToken;
|
|
49
|
+
|
|
50
|
+
const auth = { ...readOpencodeAuthFile(), ...readOpencodeAuthFromEnv() };
|
|
51
|
+
const entry = auth[GROK_BUILD_PROVIDER_ID];
|
|
52
|
+
if (!entry || typeof entry !== 'object') return undefined;
|
|
53
|
+
if (entry.type === 'oauth' && typeof entry.access === 'string' && entry.access) {
|
|
54
|
+
return entry.access;
|
|
55
|
+
}
|
|
56
|
+
if (entry.type === 'api' && typeof entry.key === 'string' && entry.key) return entry.key;
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function buildGrokBuildUsageReport(): Promise<string[]> {
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
|
|
63
|
+
if (process.env.GROK_BUILD_OAUTH_TOKEN) {
|
|
64
|
+
lines.push(' ⚠️ Using GROK_BUILD_OAUTH_TOKEN env bypass — no auto-refresh available');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const token = resolveGrokBuildAccessToken();
|
|
68
|
+
if (!token) {
|
|
69
|
+
lines.push(...formatQuota(undefined));
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
lines.push(...formatQuota(await fetchBillingUsage(token)));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
77
|
+
lines.push(` billing refresh failed: ${message}`);
|
|
78
|
+
lines.push(...formatQuota(undefined));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { GROK_BUILD_USAGE_COMMAND } from './usage.js';
|
|
2
|
+
|
|
3
|
+
export function formatUsageToastMessage(report: string[]): string {
|
|
4
|
+
return report.join('\n');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const GROK_BUILD_USAGE_TUI_COMMAND = 'open-grok-build.grok-build-usage';
|
|
8
|
+
|
|
9
|
+
export const GROK_BUILD_USAGE_SLASH = GROK_BUILD_USAGE_COMMAND;
|