@shanepadgett/tau-agent 0.14.0 → 0.15.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/extensions/attention/README.md +1 -1
- package/extensions/attention/index.ts +1 -1
- package/extensions/context/sync.ts +1 -2
- package/extensions/image-gen/README.md +2 -2
- package/extensions/image-gen/client.ts +1 -1
- package/extensions/image-gen/constants.ts +3 -0
- package/extensions/image-gen/index.ts +3 -3
- package/extensions/run-summary/README.md +1 -1
- package/extensions/run-summary/index.ts +13 -6
- package/extensions/silent-command-runner/index.ts +16 -2
- package/extensions/subagent/run.ts +0 -1
- package/extensions/tau-help/help.md +1 -5
- package/package.json +2 -2
- package/extensions/xai/README.md +0 -7
- package/extensions/xai/auth.ts +0 -40
- package/extensions/xai/constants.ts +0 -11
- package/extensions/xai/index.ts +0 -38
- package/extensions/xai/oauth.ts +0 -342
- package/extensions/xai/payload.ts +0 -68
|
@@ -4,7 +4,7 @@ Sends a terminal-driven attention notification when Tau is ready for input, fini
|
|
|
4
4
|
|
|
5
5
|
## Behavior
|
|
6
6
|
|
|
7
|
-
- Emits an attention notification
|
|
7
|
+
- Emits an attention notification after the agent settles with no automatic continuation pending.
|
|
8
8
|
- Emits an attention notification on `session_compact`.
|
|
9
9
|
- Emits an attention notification on `session_tree` when it includes a branch summary.
|
|
10
10
|
- Listens for shared event `tau:agent.blocked` when Tau is waiting on user input.
|
|
@@ -66,7 +66,7 @@ export default function attentionExtension(pi: ExtensionAPI): void {
|
|
|
66
66
|
|
|
67
67
|
onTauEvent(pi, "attention.agent-blocked", "tau:agent.blocked", notify);
|
|
68
68
|
|
|
69
|
-
pi.on("
|
|
69
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
70
70
|
if (ctx.mode === "print") return;
|
|
71
71
|
notify({ title: DEFAULT_TITLE, body: DEFAULT_BODY });
|
|
72
72
|
});
|
|
@@ -8,7 +8,6 @@ import { createGitRunner, loadRepoStatus, type GitRunner } from "../../shared/gi
|
|
|
8
8
|
import { generateToolValidated, resolveCandidates } from "../../shared/model-fallback/index.ts";
|
|
9
9
|
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
10
10
|
import { truncAt } from "../../shared/text.ts";
|
|
11
|
-
import { XAI_CHAT_MODEL, XAI_PROVIDER } from "../xai/constants.ts";
|
|
12
11
|
import {
|
|
13
12
|
contextEntryPaths,
|
|
14
13
|
isContextEligiblePath,
|
|
@@ -32,7 +31,7 @@ const CONTEXT_SYNC_MODELS: ReadonlyArray<{ provider: string; model: string; reas
|
|
|
32
31
|
{ provider: "openai-codex", model: "gpt-5.6-terra", reasoning: "medium" },
|
|
33
32
|
{ provider: "openai-codex", model: "gpt-5.6-sol", reasoning: "low" },
|
|
34
33
|
{ provider: "anthropic", model: "claude-sonnet-5", reasoning: "low" },
|
|
35
|
-
{ provider:
|
|
34
|
+
{ provider: "xai", model: "grok-4.5", reasoning: "high" },
|
|
36
35
|
];
|
|
37
36
|
|
|
38
37
|
const SUBMIT_TOOL = {
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
`image_gen` generates raster images and edits up to three local raster images with Grok Imagine. It uses `grok-imagine-image-quality` and saves results under `~/.local/share/tau-agent/images/` by default. Pass an explicit path with the expected image extension when the image should be saved in the current repository or another chosen location.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Run `/login xai` and choose either a subscription or API-key login before invoking the tool.
|
|
6
6
|
|
|
7
7
|
Run `/reload` after installing or changing the extension.
|
|
8
8
|
|
|
9
9
|
The model invokes `image_gen` with a prompt. For edits, it also supplies one to three local PNG, JPEG, or WebP paths. Successful images up to 12 MiB are returned inline for inspection; larger results remain available at the saved path.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
xAI controls model availability and subscription entitlements. A successful login does not guarantee that every account can use Grok Imagine.
|
|
@@ -4,8 +4,8 @@ import { link, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
6
6
|
import { type Static, Type } from "typebox";
|
|
7
|
-
import { XAI_IMAGE_MODEL, XAI_PROVIDER } from "../xai/constants.ts";
|
|
8
7
|
import { detectImageMimeType, editImage, generateImage, type EditImage, type GeneratedImage } from "./client.ts";
|
|
8
|
+
import { XAI_IMAGE_MODEL, XAI_PROVIDER } from "./constants.ts";
|
|
9
9
|
|
|
10
10
|
const MAX_INPUT_BYTES = 50 * 1024 * 1024;
|
|
11
11
|
const MAX_INLINE_BYTES = 12 * 1024 * 1024;
|
|
@@ -41,7 +41,7 @@ export default function imageGenExtension(pi: ExtensionAPI): void {
|
|
|
41
41
|
name: "image_gen",
|
|
42
42
|
label: "Image Generation",
|
|
43
43
|
description:
|
|
44
|
-
"Generate a requested raster image or AI-edit existing images with
|
|
44
|
+
"Generate a requested raster image or AI-edit existing images with configured xAI authentication. Omit referenced_image_paths to generate; pass one to three local paths to edit or compose. Omit path to use Tau's external image store; pass path only when the user explicitly requests a repository file or other destination. Returns the image for inspection.",
|
|
45
45
|
parameters: imageGenSchema,
|
|
46
46
|
async execute(_toolCallId, params: ImageGenParams, signal, onUpdate, ctx) {
|
|
47
47
|
signal?.throwIfAborted();
|
|
@@ -63,7 +63,7 @@ export default function imageGenExtension(pi: ExtensionAPI): void {
|
|
|
63
63
|
|
|
64
64
|
const token = await ctx.modelRegistry.getApiKeyForProvider(XAI_PROVIDER);
|
|
65
65
|
if (!token) {
|
|
66
|
-
throw new Error("xAI
|
|
66
|
+
throw new Error("xAI authentication is unavailable. Run /login xai and choose a login method.");
|
|
67
67
|
}
|
|
68
68
|
const images: EditImage[] = [];
|
|
69
69
|
for (const path of params.referenced_image_paths ?? []) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Run Summary
|
|
2
2
|
|
|
3
|
-
Run Summary adds a compact marker after
|
|
3
|
+
Run Summary adds a compact marker after the agent settles with no automatic continuation pending. It shows wall time, direct run cost, delegated subagent cost, and their combined total across the full continuation chain.
|
|
4
4
|
|
|
5
5
|
The marker is stored as a display-only session entry. It does not enter model context or trigger another agent turn.
|
|
@@ -13,6 +13,8 @@ interface RunSummary {
|
|
|
13
13
|
|
|
14
14
|
export default function runSummaryExtension(pi: ExtensionAPI): void {
|
|
15
15
|
let startedAt: number | undefined;
|
|
16
|
+
let runCost = 0;
|
|
17
|
+
let subagentCost = 0;
|
|
16
18
|
|
|
17
19
|
pi.registerEntryRenderer<RunSummary>(ENTRY_TYPE, (entry, _options, theme) => {
|
|
18
20
|
const summary = readRunSummary(entry.data);
|
|
@@ -32,18 +34,15 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
|
|
|
32
34
|
|
|
33
35
|
pi.on("session_start", () => {
|
|
34
36
|
startedAt = undefined;
|
|
37
|
+
runCost = 0;
|
|
38
|
+
subagentCost = 0;
|
|
35
39
|
});
|
|
36
40
|
|
|
37
41
|
pi.on("agent_start", () => {
|
|
38
|
-
startedAt
|
|
42
|
+
startedAt ??= performance.now();
|
|
39
43
|
});
|
|
40
44
|
|
|
41
45
|
pi.on("agent_end", (event) => {
|
|
42
|
-
if (startedAt === undefined) return;
|
|
43
|
-
const wallMs = Math.max(0, performance.now() - startedAt);
|
|
44
|
-
startedAt = undefined;
|
|
45
|
-
let runCost = 0;
|
|
46
|
-
let subagentCost = 0;
|
|
47
46
|
for (const message of event.messages) {
|
|
48
47
|
if (message.role === "assistant") {
|
|
49
48
|
runCost += finiteNonNegative((message as AssistantMessage).usage.cost.total);
|
|
@@ -52,12 +51,20 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
|
|
|
52
51
|
if (message.role !== "toolResult" || message.toolName !== "subagent") continue;
|
|
53
52
|
subagentCost += readSubagentCost(message.details);
|
|
54
53
|
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
pi.on("agent_settled", () => {
|
|
57
|
+
if (startedAt === undefined) return;
|
|
58
|
+
const wallMs = Math.max(0, performance.now() - startedAt);
|
|
59
|
+
startedAt = undefined;
|
|
55
60
|
pi.appendEntry<RunSummary>(ENTRY_TYPE, {
|
|
56
61
|
wallMs,
|
|
57
62
|
runCost,
|
|
58
63
|
subagentCost,
|
|
59
64
|
totalCost: runCost + subagentCost,
|
|
60
65
|
});
|
|
66
|
+
runCost = 0;
|
|
67
|
+
subagentCost = 0;
|
|
61
68
|
});
|
|
62
69
|
}
|
|
63
70
|
|
|
@@ -80,6 +80,8 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
80
80
|
let turnPaths = new Set<string>();
|
|
81
81
|
let run: Promise<void> | undefined;
|
|
82
82
|
let abortController: AbortController | undefined;
|
|
83
|
+
let lastRunAborted = false;
|
|
84
|
+
let chainActive = false;
|
|
83
85
|
|
|
84
86
|
pi.registerMessageRenderer<FailureDetails>(MESSAGE_TYPE, (message, { expanded }, theme) =>
|
|
85
87
|
renderFailure(asFailureDetails(message.details), expanded, theme),
|
|
@@ -89,6 +91,8 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
89
91
|
settings = normalizeSettings(await loadTauExtensionSettings(ctx, silentCommandRunnerSettings));
|
|
90
92
|
turnStart = Date.now();
|
|
91
93
|
turnPaths = new Set();
|
|
94
|
+
lastRunAborted = false;
|
|
95
|
+
chainActive = false;
|
|
92
96
|
});
|
|
93
97
|
|
|
94
98
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -98,6 +102,9 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
98
102
|
});
|
|
99
103
|
|
|
100
104
|
pi.on("agent_start", async (_event, ctx) => {
|
|
105
|
+
lastRunAborted = false;
|
|
106
|
+
if (chainActive) return;
|
|
107
|
+
chainActive = true;
|
|
101
108
|
turnStart = Date.now();
|
|
102
109
|
if (!settings.enabled || settings.commands.length === 0) {
|
|
103
110
|
turnPaths = new Set();
|
|
@@ -107,8 +114,13 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
107
114
|
turnPaths = new Set(await walkFiles(projectRoot));
|
|
108
115
|
});
|
|
109
116
|
|
|
110
|
-
pi.on("agent_end",
|
|
111
|
-
|
|
117
|
+
pi.on("agent_end", (event) => {
|
|
118
|
+
lastRunAborted = hasAbortedAssistantMessage(event.messages);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
122
|
+
chainActive = false;
|
|
123
|
+
if (lastRunAborted) return;
|
|
112
124
|
if (run) return;
|
|
113
125
|
run = runChangedCommands(ctx.cwd, turnStart, ctx.ui.notify)
|
|
114
126
|
.catch((error: unknown) => {
|
|
@@ -125,6 +137,8 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
125
137
|
abortController = undefined;
|
|
126
138
|
run = undefined;
|
|
127
139
|
turnPaths = new Set();
|
|
140
|
+
lastRunAborted = false;
|
|
141
|
+
chainActive = false;
|
|
128
142
|
});
|
|
129
143
|
|
|
130
144
|
async function runChangedCommands(
|
|
@@ -232,7 +232,6 @@ export async function createSubagentThread(options: {
|
|
|
232
232
|
cwd: ctx.cwd,
|
|
233
233
|
model,
|
|
234
234
|
thinkingLevel: thinkingLevel as "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max",
|
|
235
|
-
modelRegistry: ctx.modelRegistry,
|
|
236
235
|
tools: definition.tools,
|
|
237
236
|
excludeTools: ["subagent"],
|
|
238
237
|
resourceLoader,
|
|
@@ -44,11 +44,7 @@ Adds `/ideas` to log rough ideas or open the ideas browser.
|
|
|
44
44
|
|
|
45
45
|
## image-gen
|
|
46
46
|
|
|
47
|
-
Gives the agent
|
|
48
|
-
|
|
49
|
-
## xai
|
|
50
|
-
|
|
51
|
-
Adds Grok 4.5 and Grok Imagine through an xAI Grok subscription OAuth login. Run `/login` and select xAI before use.
|
|
47
|
+
Gives the agent a Grok Imagine generation and editing tool. Run `/login xai` and choose a subscription or API-key login before use. Generated images are saved for inspection.
|
|
52
48
|
|
|
53
49
|
## manage-sessions
|
|
54
50
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shanepadgett/tau-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Tau is a custom agentic harness built with pi extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@shanepadgett/tau-tui": "0.
|
|
31
|
+
"@shanepadgett/tau-tui": "0.15.0",
|
|
32
32
|
"@toon-format/toon": "2.3.0",
|
|
33
33
|
"smol-toml": "1.7.0"
|
|
34
34
|
},
|
package/extensions/xai/README.md
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# xAI OAuth
|
|
2
|
-
|
|
3
|
-
Adds Grok 4.5 using an xAI Grok subscription login. Run `/login`, select **xAI (Grok subscription OAuth)**, then authorize xAI in the browser. An existing official Grok CLI login can also be reused.
|
|
4
|
-
|
|
5
|
-
The same login powers Tau's `image_gen` tool through Grok Imagine. No xAI API key is used.
|
|
6
|
-
|
|
7
|
-
This integration uses xAI's undocumented subscription OAuth access. xAI may change its availability, entitlement rules, or protocol without notice.
|
package/extensions/xai/auth.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { OAuthCredentials } from "@earendil-works/pi-ai";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { XAI_OAUTH_CLIENT_ID, XAI_OAUTH_ISSUER } from "./constants.ts";
|
|
6
|
-
|
|
7
|
-
function expiry(value: unknown): number | undefined {
|
|
8
|
-
if (typeof value === "number" && Number.isInteger(value) && value >= 1_000_000_000_000) return value;
|
|
9
|
-
if (typeof value !== "string") return undefined;
|
|
10
|
-
const parsed = Date.parse(value);
|
|
11
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function parseGrokCredentials(value: unknown): OAuthCredentials | undefined {
|
|
15
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
16
|
-
const entry = (value as Record<string, unknown>)[`${XAI_OAUTH_ISSUER}::${XAI_OAUTH_CLIENT_ID}`];
|
|
17
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return undefined;
|
|
18
|
-
const record = entry as Record<string, unknown>;
|
|
19
|
-
const expires = expiry(record.expires_at);
|
|
20
|
-
if (
|
|
21
|
-
typeof record.key !== "string" ||
|
|
22
|
-
!record.key ||
|
|
23
|
-
typeof record.refresh_token !== "string" ||
|
|
24
|
-
!record.refresh_token ||
|
|
25
|
-
record.oidc_issuer !== XAI_OAUTH_ISSUER ||
|
|
26
|
-
record.oidc_client_id !== XAI_OAUTH_CLIENT_ID ||
|
|
27
|
-
expires === undefined
|
|
28
|
-
) {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
return { access: record.key, refresh: record.refresh_token, expires };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function readGrokCredentials(): Promise<OAuthCredentials | undefined> {
|
|
35
|
-
try {
|
|
36
|
-
return parseGrokCredentials(JSON.parse(await readFile(join(homedir(), ".grok", "auth.json"), "utf8")));
|
|
37
|
-
} catch {
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export const XAI_PROVIDER = "xai-oauth";
|
|
2
|
-
export const XAI_CHAT_MODEL = "grok-4.5";
|
|
3
|
-
export const XAI_IMAGE_MODEL = "grok-imagine-image-quality";
|
|
4
|
-
export const XAI_API_BASE_URL = "https://api.x.ai/v1";
|
|
5
|
-
|
|
6
|
-
export const XAI_OAUTH_ISSUER = "https://auth.x.ai";
|
|
7
|
-
export const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
8
|
-
export const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
9
|
-
export const XAI_OAUTH_CALLBACK_HOST = "127.0.0.1";
|
|
10
|
-
export const XAI_OAUTH_CALLBACK_PORT = 56121;
|
|
11
|
-
export const XAI_OAUTH_CALLBACK_PATH = "/callback";
|
package/extensions/xai/index.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { XAI_API_BASE_URL, XAI_CHAT_MODEL, XAI_PROVIDER } from "./constants.ts";
|
|
3
|
-
import { xaiOAuth } from "./oauth.ts";
|
|
4
|
-
import { rewriteXaiPayload } from "./payload.ts";
|
|
5
|
-
|
|
6
|
-
export default function xaiExtension(pi: ExtensionAPI): void {
|
|
7
|
-
pi.registerProvider(XAI_PROVIDER, {
|
|
8
|
-
name: "xAI (Grok subscription OAuth)",
|
|
9
|
-
baseUrl: XAI_API_BASE_URL,
|
|
10
|
-
api: "openai-responses",
|
|
11
|
-
authHeader: true,
|
|
12
|
-
oauth: xaiOAuth,
|
|
13
|
-
models: [
|
|
14
|
-
{
|
|
15
|
-
id: XAI_CHAT_MODEL,
|
|
16
|
-
name: "Grok 4.5",
|
|
17
|
-
reasoning: true,
|
|
18
|
-
input: ["text", "image"],
|
|
19
|
-
cost: { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
|
|
20
|
-
contextWindow: 500_000,
|
|
21
|
-
maxTokens: 131_072,
|
|
22
|
-
thinkingLevelMap: {
|
|
23
|
-
off: null,
|
|
24
|
-
minimal: "low",
|
|
25
|
-
low: "low",
|
|
26
|
-
medium: "medium",
|
|
27
|
-
high: "high",
|
|
28
|
-
xhigh: null,
|
|
29
|
-
max: null,
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
});
|
|
34
|
-
pi.on("before_provider_request", (event, ctx) => {
|
|
35
|
-
if (ctx.model?.provider !== XAI_PROVIDER) return;
|
|
36
|
-
return rewriteXaiPayload(event.payload);
|
|
37
|
-
});
|
|
38
|
-
}
|
package/extensions/xai/oauth.ts
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { createServer, type Server } from "node:http";
|
|
4
|
-
import { readGrokCredentials } from "./auth.ts";
|
|
5
|
-
import {
|
|
6
|
-
XAI_OAUTH_CALLBACK_HOST,
|
|
7
|
-
XAI_OAUTH_CALLBACK_PATH,
|
|
8
|
-
XAI_OAUTH_CALLBACK_PORT,
|
|
9
|
-
XAI_OAUTH_CLIENT_ID,
|
|
10
|
-
XAI_OAUTH_ISSUER,
|
|
11
|
-
XAI_OAUTH_SCOPE,
|
|
12
|
-
} from "./constants.ts";
|
|
13
|
-
|
|
14
|
-
const DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
|
|
15
|
-
const REQUEST_TIMEOUT_MS = 30_000;
|
|
16
|
-
const LOGIN_TIMEOUT_MS = 180_000;
|
|
17
|
-
const REFRESH_SKEW_MS = 120_000;
|
|
18
|
-
|
|
19
|
-
interface Discovery {
|
|
20
|
-
authorization_endpoint: string;
|
|
21
|
-
token_endpoint: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface TokenPayload {
|
|
25
|
-
access_token?: unknown;
|
|
26
|
-
refresh_token?: unknown;
|
|
27
|
-
id_token?: unknown;
|
|
28
|
-
expires_in?: unknown;
|
|
29
|
-
token_type?: unknown;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface HttpResponse {
|
|
33
|
-
ok: boolean;
|
|
34
|
-
status: number;
|
|
35
|
-
body: { cancel(): Promise<void> } | null;
|
|
36
|
-
json(): Promise<unknown>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface CallbackResult {
|
|
40
|
-
code?: string;
|
|
41
|
-
error?: string;
|
|
42
|
-
errorDescription?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function validatedEndpoint(value: unknown, field: string): string {
|
|
46
|
-
if (typeof value !== "string") throw new Error(`xAI OAuth discovery omitted ${field}`);
|
|
47
|
-
const url = new URL(value);
|
|
48
|
-
const host = url.hostname.toLowerCase();
|
|
49
|
-
if (url.protocol !== "https:" || (host !== "x.ai" && !host.endsWith(".x.ai"))) {
|
|
50
|
-
throw new Error(`xAI OAuth discovery returned an unexpected ${field}`);
|
|
51
|
-
}
|
|
52
|
-
return url.toString();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function requestSignal(parent?: AbortSignal): AbortSignal {
|
|
56
|
-
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
57
|
-
return parent ? AbortSignal.any([parent, timeout]) : timeout;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function discover(signal?: AbortSignal): Promise<Discovery> {
|
|
61
|
-
const response = (await fetch(DISCOVERY_URL, {
|
|
62
|
-
headers: { Accept: "application/json" },
|
|
63
|
-
signal: requestSignal(signal),
|
|
64
|
-
})) as HttpResponse;
|
|
65
|
-
if (!response.ok) throw new Error(`xAI OAuth discovery failed with status ${response.status}`);
|
|
66
|
-
const value: unknown = await response.json();
|
|
67
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
68
|
-
throw new Error("xAI OAuth discovery returned invalid JSON");
|
|
69
|
-
}
|
|
70
|
-
const record = value as Record<string, unknown>;
|
|
71
|
-
return {
|
|
72
|
-
authorization_endpoint: validatedEndpoint(record.authorization_endpoint, "authorization_endpoint"),
|
|
73
|
-
token_endpoint: validatedEndpoint(record.token_endpoint, "token_endpoint"),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function tokenRequest(endpoint: string, body: URLSearchParams, signal?: AbortSignal): Promise<TokenPayload> {
|
|
78
|
-
const response = (await fetch(validatedEndpoint(endpoint, "token_endpoint"), {
|
|
79
|
-
method: "POST",
|
|
80
|
-
headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
|
|
81
|
-
body,
|
|
82
|
-
signal: requestSignal(signal),
|
|
83
|
-
})) as HttpResponse;
|
|
84
|
-
if (!response.ok) {
|
|
85
|
-
await response.body?.cancel().catch(() => undefined);
|
|
86
|
-
throw new Error(`xAI OAuth token request failed with status ${response.status}`);
|
|
87
|
-
}
|
|
88
|
-
return (await response.json()) as TokenPayload;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function jwtClaims(token: string): Record<string, unknown> {
|
|
92
|
-
const segments = token.split(".");
|
|
93
|
-
if (segments.length !== 3 || !segments[1]) throw new Error("xAI OAuth returned an invalid ID token");
|
|
94
|
-
try {
|
|
95
|
-
const value: unknown = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
|
|
96
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error();
|
|
97
|
-
return value as Record<string, unknown>;
|
|
98
|
-
} catch {
|
|
99
|
-
throw new Error("xAI OAuth returned an invalid ID token");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function credentials(payload: TokenPayload, endpoint: string, fallbackRefresh = "", nonce?: string): OAuthCredentials {
|
|
104
|
-
if (typeof payload.access_token !== "string" || !payload.access_token) {
|
|
105
|
-
throw new Error("xAI OAuth token response omitted the access token");
|
|
106
|
-
}
|
|
107
|
-
const refresh =
|
|
108
|
-
typeof payload.refresh_token === "string" && payload.refresh_token ? payload.refresh_token : fallbackRefresh;
|
|
109
|
-
if (!refresh) throw new Error("xAI OAuth token response omitted the refresh token");
|
|
110
|
-
if (nonce !== undefined) {
|
|
111
|
-
if (typeof payload.id_token !== "string" || !payload.id_token)
|
|
112
|
-
throw new Error("xAI OAuth token response omitted the ID token");
|
|
113
|
-
const claims = jwtClaims(payload.id_token);
|
|
114
|
-
const audience = claims.aud;
|
|
115
|
-
const validAudience =
|
|
116
|
-
audience === XAI_OAUTH_CLIENT_ID || (Array.isArray(audience) && audience.includes(XAI_OAUTH_CLIENT_ID));
|
|
117
|
-
if (claims.iss !== XAI_OAUTH_ISSUER || !validAudience || claims.nonce !== nonce) {
|
|
118
|
-
throw new Error("xAI OAuth ID token validation failed");
|
|
119
|
-
}
|
|
120
|
-
if (typeof claims.exp !== "number" || claims.exp * 1000 <= Date.now()) {
|
|
121
|
-
throw new Error("xAI OAuth returned an expired ID token");
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
const expiresIn = typeof payload.expires_in === "number" && payload.expires_in > 0 ? payload.expires_in : 3600;
|
|
125
|
-
return {
|
|
126
|
-
access: payload.access_token,
|
|
127
|
-
refresh,
|
|
128
|
-
expires: Date.now() + expiresIn * 1000 - REFRESH_SKEW_MS,
|
|
129
|
-
tokenEndpoint: endpoint,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function closeServer(server: Server): Promise<void> {
|
|
134
|
-
if (!server.listening) return;
|
|
135
|
-
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function callbackServer(expectedState: string): Promise<{
|
|
139
|
-
redirectUri: string;
|
|
140
|
-
wait(signal?: AbortSignal): Promise<CallbackResult>;
|
|
141
|
-
acceptManual(input: string): string | undefined;
|
|
142
|
-
close(): Promise<void>;
|
|
143
|
-
}> {
|
|
144
|
-
let settle: ((result: CallbackResult) => void) | undefined;
|
|
145
|
-
let reject: ((error: Error) => void) | undefined;
|
|
146
|
-
let settled = false;
|
|
147
|
-
const result = new Promise<CallbackResult>((resolve, rejectResult) => {
|
|
148
|
-
settle = resolve;
|
|
149
|
-
reject = rejectResult;
|
|
150
|
-
});
|
|
151
|
-
const accept = (value: CallbackResult) => {
|
|
152
|
-
if (settled) return;
|
|
153
|
-
settled = true;
|
|
154
|
-
settle?.(value);
|
|
155
|
-
};
|
|
156
|
-
const parse = (params: URLSearchParams): CallbackResult | undefined => {
|
|
157
|
-
if (params.get("state") !== expectedState) return undefined;
|
|
158
|
-
const code = params.get("code") || undefined;
|
|
159
|
-
const error = params.get("error") || undefined;
|
|
160
|
-
if (!code && !error) return undefined;
|
|
161
|
-
return { code, error, errorDescription: params.get("error_description") || undefined };
|
|
162
|
-
};
|
|
163
|
-
const server = createServer((request, response) => {
|
|
164
|
-
const origin = request.headers.origin;
|
|
165
|
-
if (origin === "https://accounts.x.ai" || origin === "https://auth.x.ai") {
|
|
166
|
-
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
167
|
-
response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
168
|
-
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
169
|
-
response.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
170
|
-
response.setHeader("Vary", "Origin");
|
|
171
|
-
}
|
|
172
|
-
if (request.method === "OPTIONS") {
|
|
173
|
-
response.writeHead(204).end();
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const url = new URL(request.url ?? "/", `http://${XAI_OAUTH_CALLBACK_HOST}`);
|
|
177
|
-
if (request.method !== "GET" || url.pathname !== XAI_OAUTH_CALLBACK_PATH) {
|
|
178
|
-
response.writeHead(404).end("Not found");
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const parsed = parse(url.searchParams);
|
|
182
|
-
if (!parsed) {
|
|
183
|
-
response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" }).end("Invalid OAuth callback");
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
response
|
|
187
|
-
.writeHead(parsed.error ? 400 : 200, { "Content-Type": "text/html; charset=utf-8" })
|
|
188
|
-
.end("<html><body><h1>xAI authorization received.</h1>You can close this tab.</body></html>", () =>
|
|
189
|
-
accept(parsed),
|
|
190
|
-
);
|
|
191
|
-
});
|
|
192
|
-
const listen = (port: number) =>
|
|
193
|
-
new Promise<number>((resolve, rejectListen) => {
|
|
194
|
-
server.once("error", rejectListen);
|
|
195
|
-
server.listen(port, XAI_OAUTH_CALLBACK_HOST, () => {
|
|
196
|
-
server.removeListener("error", rejectListen);
|
|
197
|
-
const address = server.address();
|
|
198
|
-
if (!address || typeof address === "string") rejectListen(new Error("Could not determine callback port"));
|
|
199
|
-
else resolve(address.port);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
let port: number;
|
|
203
|
-
try {
|
|
204
|
-
port = await listen(XAI_OAUTH_CALLBACK_PORT);
|
|
205
|
-
} catch {
|
|
206
|
-
port = await listen(0);
|
|
207
|
-
}
|
|
208
|
-
return {
|
|
209
|
-
redirectUri: `http://${XAI_OAUTH_CALLBACK_HOST}:${port}${XAI_OAUTH_CALLBACK_PATH}`,
|
|
210
|
-
acceptManual(input) {
|
|
211
|
-
try {
|
|
212
|
-
const value = input.trim();
|
|
213
|
-
const url = value.startsWith("http")
|
|
214
|
-
? new URL(value)
|
|
215
|
-
: new URL(`http://${XAI_OAUTH_CALLBACK_HOST}${XAI_OAUTH_CALLBACK_PATH}?${value.replace(/^\?/, "")}`);
|
|
216
|
-
if (url.pathname !== XAI_OAUTH_CALLBACK_PATH) return "Callback URL path was not recognized";
|
|
217
|
-
const parsed = parse(url.searchParams);
|
|
218
|
-
if (!parsed) return "Callback state did not match";
|
|
219
|
-
accept(parsed);
|
|
220
|
-
return undefined;
|
|
221
|
-
} catch {
|
|
222
|
-
return "Callback URL was invalid";
|
|
223
|
-
}
|
|
224
|
-
},
|
|
225
|
-
async wait(signal) {
|
|
226
|
-
const timeout = setTimeout(() => {
|
|
227
|
-
if (!settled) {
|
|
228
|
-
settled = true;
|
|
229
|
-
reject?.(new Error("Timed out waiting for xAI OAuth callback"));
|
|
230
|
-
}
|
|
231
|
-
}, LOGIN_TIMEOUT_MS);
|
|
232
|
-
const onAbort = () => {
|
|
233
|
-
if (!settled) {
|
|
234
|
-
settled = true;
|
|
235
|
-
reject?.(new Error("xAI OAuth login was cancelled"));
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
239
|
-
try {
|
|
240
|
-
return await result;
|
|
241
|
-
} finally {
|
|
242
|
-
clearTimeout(timeout);
|
|
243
|
-
signal?.removeEventListener("abort", onAbort);
|
|
244
|
-
await closeServer(server);
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
close: () => closeServer(server),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function refreshXaiCredentials(value: OAuthCredentials): Promise<OAuthCredentials> {
|
|
252
|
-
if (!value.refresh) throw new Error("xAI OAuth credential cannot be refreshed; run /login again");
|
|
253
|
-
const endpoint =
|
|
254
|
-
typeof value.tokenEndpoint === "string" && value.tokenEndpoint
|
|
255
|
-
? validatedEndpoint(value.tokenEndpoint, "token_endpoint")
|
|
256
|
-
: (await discover()).token_endpoint;
|
|
257
|
-
const payload = await tokenRequest(
|
|
258
|
-
endpoint,
|
|
259
|
-
new URLSearchParams({
|
|
260
|
-
grant_type: "refresh_token",
|
|
261
|
-
client_id: XAI_OAUTH_CLIENT_ID,
|
|
262
|
-
refresh_token: value.refresh,
|
|
263
|
-
}),
|
|
264
|
-
);
|
|
265
|
-
return credentials(payload, endpoint, value.refresh);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export const xaiOAuth = {
|
|
269
|
-
name: "xAI (Grok subscription)",
|
|
270
|
-
usesCallbackServer: true,
|
|
271
|
-
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
272
|
-
const existing = await readGrokCredentials();
|
|
273
|
-
if (existing) {
|
|
274
|
-
const method = await callbacks.onSelect({
|
|
275
|
-
message: "Select xAI login method:",
|
|
276
|
-
options: [
|
|
277
|
-
{ id: "browser", label: "Browser login" },
|
|
278
|
-
{ id: "existing", label: "Use existing Grok CLI login" },
|
|
279
|
-
],
|
|
280
|
-
});
|
|
281
|
-
if (!method) throw new Error("Login cancelled");
|
|
282
|
-
if (method === "existing") {
|
|
283
|
-
if (existing.expires > Date.now()) return existing;
|
|
284
|
-
try {
|
|
285
|
-
return await refreshXaiCredentials(existing);
|
|
286
|
-
} catch {
|
|
287
|
-
callbacks.onProgress?.("The existing Grok CLI login could not be refreshed. Starting browser login.");
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
const discovery = await discover(callbacks.signal);
|
|
292
|
-
const verifier = randomBytes(32).toString("base64url");
|
|
293
|
-
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
294
|
-
const state = randomBytes(24).toString("base64url");
|
|
295
|
-
const nonce = randomBytes(24).toString("base64url");
|
|
296
|
-
const callback = await callbackServer(state);
|
|
297
|
-
try {
|
|
298
|
-
const url = new URL(discovery.authorization_endpoint);
|
|
299
|
-
url.search = new URLSearchParams({
|
|
300
|
-
response_type: "code",
|
|
301
|
-
client_id: XAI_OAUTH_CLIENT_ID,
|
|
302
|
-
redirect_uri: callback.redirectUri,
|
|
303
|
-
scope: XAI_OAUTH_SCOPE,
|
|
304
|
-
code_challenge: challenge,
|
|
305
|
-
code_challenge_method: "S256",
|
|
306
|
-
state,
|
|
307
|
-
nonce,
|
|
308
|
-
}).toString();
|
|
309
|
-
callbacks.onAuth({ url: url.toString(), instructions: "Authorize xAI in your browser, then return to Tau." });
|
|
310
|
-
if (callbacks.onManualCodeInput) {
|
|
311
|
-
void callbacks
|
|
312
|
-
.onManualCodeInput()
|
|
313
|
-
.then((input) => {
|
|
314
|
-
const error = callback.acceptManual(input);
|
|
315
|
-
if (error) callbacks.onProgress?.(`Ignored pasted callback: ${error}`);
|
|
316
|
-
})
|
|
317
|
-
.catch(() => undefined);
|
|
318
|
-
}
|
|
319
|
-
const result = await callback.wait(callbacks.signal);
|
|
320
|
-
if (result.error) throw new Error(`xAI authorization failed: ${result.errorDescription ?? result.error}`);
|
|
321
|
-
if (!result.code) throw new Error("xAI authorization did not return a code");
|
|
322
|
-
const payload = await tokenRequest(
|
|
323
|
-
discovery.token_endpoint,
|
|
324
|
-
new URLSearchParams({
|
|
325
|
-
grant_type: "authorization_code",
|
|
326
|
-
client_id: XAI_OAUTH_CLIENT_ID,
|
|
327
|
-
code: result.code,
|
|
328
|
-
redirect_uri: callback.redirectUri,
|
|
329
|
-
code_verifier: verifier,
|
|
330
|
-
}),
|
|
331
|
-
callbacks.signal,
|
|
332
|
-
);
|
|
333
|
-
return credentials(payload, discovery.token_endpoint, "", nonce);
|
|
334
|
-
} finally {
|
|
335
|
-
await callback.close();
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
refreshToken: refreshXaiCredentials,
|
|
339
|
-
getApiKey(value: OAuthCredentials): string {
|
|
340
|
-
return value.access;
|
|
341
|
-
},
|
|
342
|
-
};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function contentText(value: unknown): string {
|
|
6
|
-
if (typeof value === "string") return value;
|
|
7
|
-
if (!Array.isArray(value)) return "";
|
|
8
|
-
return value
|
|
9
|
-
.map((part) => {
|
|
10
|
-
if (!isRecord(part)) return "";
|
|
11
|
-
return typeof part.text === "string" ? part.text : "";
|
|
12
|
-
})
|
|
13
|
-
.filter(Boolean)
|
|
14
|
-
.join("\n");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeToolOutput(item: Record<string, unknown>): unknown[] {
|
|
18
|
-
if (item.type !== "function_call_output" || !Array.isArray(item.output)) return [item];
|
|
19
|
-
const images = item.output.filter((part) => isRecord(part) && part.type === "input_image");
|
|
20
|
-
if (images.length === 0) return [item];
|
|
21
|
-
const text = contentText(item.output) || "(tool returned image output)";
|
|
22
|
-
return [
|
|
23
|
-
{ ...item, output: text },
|
|
24
|
-
{
|
|
25
|
-
role: "user",
|
|
26
|
-
content: [
|
|
27
|
-
{ type: "input_text", text: "The previous tool result included image output. Use the attached image." },
|
|
28
|
-
...images,
|
|
29
|
-
],
|
|
30
|
-
},
|
|
31
|
-
];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function rewriteXaiPayload(value: unknown): unknown {
|
|
35
|
-
if (!isRecord(value)) return value;
|
|
36
|
-
const body = { ...value };
|
|
37
|
-
delete body.prompt_cache_retention;
|
|
38
|
-
if (isRecord(body.reasoning)) {
|
|
39
|
-
const effort = body.reasoning.effort;
|
|
40
|
-
body.reasoning =
|
|
41
|
-
typeof effort === "string" && effort !== "none"
|
|
42
|
-
? { effort: effort === "minimal" ? "low" : effort }
|
|
43
|
-
: undefined;
|
|
44
|
-
}
|
|
45
|
-
if (Array.isArray(body.input)) {
|
|
46
|
-
const instructions: string[] = [];
|
|
47
|
-
const input: unknown[] = [];
|
|
48
|
-
for (const raw of body.input) {
|
|
49
|
-
if (!isRecord(raw)) {
|
|
50
|
-
input.push(raw);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if ((raw.role === "developer" || raw.role === "system") && input.length === 0) {
|
|
54
|
-
const text = contentText(raw.content).trim();
|
|
55
|
-
if (text) instructions.push(text);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
input.push(...normalizeToolOutput(raw));
|
|
59
|
-
}
|
|
60
|
-
body.input = input;
|
|
61
|
-
if (instructions.length > 0) {
|
|
62
|
-
body.instructions = [typeof body.instructions === "string" ? body.instructions : "", ...instructions]
|
|
63
|
-
.filter(Boolean)
|
|
64
|
-
.join("\n\n");
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return body;
|
|
68
|
-
}
|