jeo-code 0.1.0 → 0.4.4
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/README.ja.md +160 -0
- package/README.ko.md +160 -0
- package/README.md +115 -297
- package/README.zh.md +160 -0
- package/package.json +11 -6
- package/scripts/install.sh +28 -28
- package/scripts/uninstall.sh +17 -15
- package/src/AGENTS.md +50 -0
- package/src/agent/AGENTS.md +49 -0
- package/src/agent/bash-fixups.ts +103 -0
- package/src/agent/compaction.ts +410 -19
- package/src/agent/config-schema.ts +119 -5
- package/src/agent/context-files.ts +314 -17
- package/src/agent/dev/AGENTS.md +36 -0
- package/src/agent/dev/advanced-analyzer.ts +12 -0
- package/src/agent/dev/evolution-bridge.ts +82 -0
- package/src/agent/dev/evolution-logger.ts +41 -0
- package/src/agent/dev/self-analysis.ts +64 -0
- package/src/agent/dev/self-improve.ts +24 -0
- package/src/agent/dev/spec-automation.ts +49 -0
- package/src/agent/engine.ts +804 -54
- package/src/agent/hooks.ts +273 -0
- package/src/agent/loop.ts +21 -1
- package/src/agent/memory.ts +201 -0
- package/src/agent/model-recency.ts +32 -0
- package/src/agent/output-minimizer.ts +108 -0
- package/src/agent/output-util.ts +64 -0
- package/src/agent/plan.ts +187 -0
- package/src/agent/seed.ts +52 -0
- package/src/agent/session.ts +235 -21
- package/src/agent/state.ts +286 -39
- package/src/agent/step-budget.ts +232 -0
- package/src/agent/subagents.ts +223 -26
- package/src/agent/task-tool.ts +272 -0
- package/src/agent/todo-tool.ts +87 -0
- package/src/agent/tokenizer.ts +117 -0
- package/src/agent/tool-registry.ts +54 -0
- package/src/agent/tools.ts +562 -103
- package/src/agent/web-search.ts +538 -0
- package/src/ai/AGENTS.md +44 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog-compat.ts +3 -1
- package/src/ai/model-catalog.ts +74 -9
- package/src/ai/model-discovery.ts +215 -17
- package/src/ai/model-manager.ts +346 -32
- package/src/ai/model-picker.ts +1 -1
- package/src/ai/model-registry.ts +4 -2
- package/src/ai/pricing.ts +84 -0
- package/src/ai/provider-registry.ts +23 -0
- package/src/ai/provider-status.ts +60 -16
- package/src/ai/providers/AGENTS.md +42 -0
- package/src/ai/providers/anthropic.ts +250 -31
- package/src/ai/providers/antigravity.ts +219 -0
- package/src/ai/providers/errors.ts +15 -1
- package/src/ai/providers/gemini.ts +196 -13
- package/src/ai/providers/ollama.ts +37 -7
- package/src/ai/providers/openai-responses.ts +173 -0
- package/src/ai/providers/openai.ts +64 -12
- package/src/ai/sse.ts +4 -1
- package/src/ai/types.ts +18 -1
- package/src/auth/AGENTS.md +41 -0
- package/src/auth/callback-server.ts +6 -1
- package/src/auth/flows/AGENTS.md +32 -0
- package/src/auth/flows/antigravity.ts +151 -0
- package/src/auth/flows/google-project.ts +190 -0
- package/src/auth/flows/google.ts +39 -18
- package/src/auth/flows/index.ts +15 -5
- package/src/auth/flows/openai.ts +2 -2
- package/src/auth/oauth.ts +8 -0
- package/src/auth/refresh.ts +44 -27
- package/src/auth/storage.ts +149 -26
- package/src/auth/types.ts +1 -1
- package/src/autopilot.ts +362 -0
- package/src/bun-imports.d.ts +4 -0
- package/src/cli/AGENTS.md +39 -0
- package/src/cli/runner.ts +148 -14
- package/src/cli.ts +13 -4
- package/src/commands/AGENTS.md +40 -0
- package/src/commands/approve.ts +62 -3
- package/src/commands/auth.ts +167 -25
- package/src/commands/chat.ts +37 -8
- package/src/commands/deep-interview.ts +633 -175
- package/src/commands/doctor.ts +84 -37
- package/src/commands/evolve-core.ts +18 -0
- package/src/commands/evolve.ts +2 -1
- package/src/commands/export.ts +176 -0
- package/src/commands/gjc.ts +52 -0
- package/src/commands/launch.ts +3549 -240
- package/src/commands/mcp.ts +3 -3
- package/src/commands/ooo-seed.ts +19 -0
- package/src/commands/ralplan.ts +253 -35
- package/src/commands/resume.ts +1 -1
- package/src/commands/session.ts +183 -0
- package/src/commands/setup-helpers.ts +10 -3
- package/src/commands/setup.ts +57 -16
- package/src/commands/skills.ts +78 -18
- package/src/commands/state.ts +198 -0
- package/src/commands/status.ts +84 -0
- package/src/commands/team.ts +340 -212
- package/src/commands/ultragoal.ts +122 -61
- package/src/commands/update.ts +244 -0
- package/src/ledger.ts +270 -0
- package/src/mcp/AGENTS.md +38 -0
- package/src/mcp/server.ts +115 -14
- package/src/mcp/tools.ts +42 -22
- package/src/md-modules.d.ts +4 -0
- package/src/prompts/AGENTS.md +41 -0
- package/src/prompts/agents/AGENTS.md +35 -0
- package/src/prompts/agents/architect.md +35 -0
- package/src/prompts/agents/critic.md +37 -0
- package/src/prompts/agents/executor.md +36 -0
- package/src/prompts/agents/planner.md +37 -0
- package/src/prompts/skills/AGENTS.md +36 -0
- package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
- package/src/prompts/skills/deep-dive/SKILL.md +13 -0
- package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
- package/src/prompts/skills/deep-interview/SKILL.md +12 -0
- package/src/prompts/skills/gjc/AGENTS.md +31 -0
- package/src/prompts/skills/gjc/SKILL.md +15 -0
- package/src/prompts/skills/ralplan/AGENTS.md +31 -0
- package/src/prompts/skills/ralplan/SKILL.md +11 -0
- package/src/prompts/skills/team/AGENTS.md +31 -0
- package/src/prompts/skills/team/SKILL.md +11 -0
- package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
- package/src/prompts/skills/ultragoal/SKILL.md +11 -0
- package/src/skills/AGENTS.md +38 -0
- package/src/skills/catalog.ts +565 -31
- package/src/tui/AGENTS.md +43 -0
- package/src/tui/app.ts +1181 -92
- package/src/tui/components/AGENTS.md +42 -0
- package/src/tui/components/ascii-art.ts +257 -15
- package/src/tui/components/autocomplete.ts +98 -16
- package/src/tui/components/autopilot-status.ts +65 -0
- package/src/tui/components/category-index.ts +49 -0
- package/src/tui/components/code-view.ts +54 -11
- package/src/tui/components/color.ts +171 -2
- package/src/tui/components/config-panel.ts +82 -15
- package/src/tui/components/duration.ts +38 -0
- package/src/tui/components/evolution.ts +3 -3
- package/src/tui/components/footer.ts +91 -42
- package/src/tui/components/forge.ts +426 -31
- package/src/tui/components/hints.ts +54 -0
- package/src/tui/components/hud.ts +73 -0
- package/src/tui/components/index.ts +4 -0
- package/src/tui/components/input-box.ts +150 -0
- package/src/tui/components/layout.ts +11 -3
- package/src/tui/components/live-model-picker.ts +108 -0
- package/src/tui/components/markdown-table.ts +140 -0
- package/src/tui/components/markdown-text.ts +97 -0
- package/src/tui/components/meter.ts +4 -1
- package/src/tui/components/model-picker.ts +3 -2
- package/src/tui/components/provider-picker.ts +3 -2
- package/src/tui/components/section.ts +70 -0
- package/src/tui/components/select-list.ts +40 -10
- package/src/tui/components/skill-picker.ts +25 -0
- package/src/tui/components/slash.ts +244 -21
- package/src/tui/components/status.ts +272 -11
- package/src/tui/components/step-timeline.ts +218 -0
- package/src/tui/components/stream.ts +26 -9
- package/src/tui/components/themes.ts +212 -6
- package/src/tui/components/todo-card.ts +47 -0
- package/src/tui/components/tool-list.ts +58 -12
- package/src/tui/components/transcript.ts +120 -0
- package/src/tui/components/update-box.ts +31 -0
- package/src/tui/components/welcome.ts +162 -0
- package/src/tui/components/width.ts +163 -0
- package/src/tui/monitoring/AGENTS.md +31 -0
- package/src/tui/monitoring/hud-view.ts +55 -0
- package/src/tui/renderer.ts +112 -3
- package/src/tui/terminal.ts +40 -33
- package/src/util/AGENTS.md +39 -0
- package/src/util/clipboard-image.ts +118 -0
- package/src/util/env.ts +12 -0
- package/src/util/provider-error.ts +78 -0
- package/src/util/retry.ts +91 -6
- package/src/util/update-check.ts +64 -0
- package/src/commands/models.ts +0 -104
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI ChatGPT/Codex OAuth path — the Codex subscription backend.
|
|
3
|
+
*
|
|
4
|
+
* ChatGPT/Codex OAuth tokens are rejected by `api.openai.com/v1/chat/completions`
|
|
5
|
+
* (that endpoint wants an `OPENAI_API_KEY`). The Codex CLI instead routes through
|
|
6
|
+
* `https://chatgpt.com/backend-api/codex/responses` using the Responses API schema,
|
|
7
|
+
* authenticated by the OAuth bearer + the `chatgpt-account-id` claimed in the JWT.
|
|
8
|
+
* This module builds that request and parses its SSE so an OAuth-only ChatGPT/Codex
|
|
9
|
+
* login can actually serve a turn (verified end-to-end against a live ChatGPT account).
|
|
10
|
+
*
|
|
11
|
+
* Note: this backend is undocumented and unstable; it can change without notice.
|
|
12
|
+
*/
|
|
13
|
+
import type { Credential } from "../../auth";
|
|
14
|
+
import type { CallOptions, Message } from "../types";
|
|
15
|
+
import { readSse } from "../sse";
|
|
16
|
+
import { providerHttpError } from "./errors";
|
|
17
|
+
|
|
18
|
+
export const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
19
|
+
|
|
20
|
+
export const VALID_REASONING_EFFORTS = new Set(["minimal", "low", "medium", "high"]);
|
|
21
|
+
|
|
22
|
+
/** Extract `chatgpt_account_id` from a ChatGPT/Codex OAuth access JWT. */
|
|
23
|
+
export function extractChatgptAccountId(token: string): string | undefined {
|
|
24
|
+
const parts = token.split(".");
|
|
25
|
+
if (parts.length < 2) return undefined;
|
|
26
|
+
try {
|
|
27
|
+
const payload = JSON.parse(Buffer.from(parts[1]!, "base64url").toString("utf-8")) as {
|
|
28
|
+
["https://api.openai.com/auth"]?: { chatgpt_account_id?: unknown };
|
|
29
|
+
};
|
|
30
|
+
const id = payload["https://api.openai.com/auth"]?.chatgpt_account_id;
|
|
31
|
+
return typeof id === "string" ? id : undefined;
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Build the Codex Responses request (url + headers + body) for an OAuth credential. */
|
|
38
|
+
export function codexResponsesRequest(
|
|
39
|
+
messages: Message[],
|
|
40
|
+
options: CallOptions,
|
|
41
|
+
credential: Credential,
|
|
42
|
+
): { url: string; headers: Record<string, string>; body: string } {
|
|
43
|
+
const model = options.model.startsWith("openai/") ? options.model.slice(7) : options.model;
|
|
44
|
+
const token = credential.kind === "none" ? "" : credential.token;
|
|
45
|
+
const systemPrompt = options.systemPrompt ?? messages.find(m => m.role === "system")?.content;
|
|
46
|
+
const input = messages
|
|
47
|
+
.filter(m => m.role !== "system")
|
|
48
|
+
.map(m => ({
|
|
49
|
+
role: m.role,
|
|
50
|
+
content: [
|
|
51
|
+
{ type: m.role === "assistant" ? "output_text" : "input_text", text: m.content },
|
|
52
|
+
// Clipboard-pasted images ride along as input_image data URLs (user turns only —
|
|
53
|
+
// assistant history is always text in jeo).
|
|
54
|
+
...(m.role !== "assistant" && m.images?.length
|
|
55
|
+
? m.images.map(img => ({ type: "input_image", image_url: `data:${img.mediaType};base64,${img.data}` }))
|
|
56
|
+
: []),
|
|
57
|
+
],
|
|
58
|
+
}));
|
|
59
|
+
const payload: Record<string, unknown> = {
|
|
60
|
+
model,
|
|
61
|
+
instructions: systemPrompt ?? "You are a helpful coding assistant.",
|
|
62
|
+
input,
|
|
63
|
+
stream: true, // the Codex backend only streams
|
|
64
|
+
store: false,
|
|
65
|
+
};
|
|
66
|
+
// Map thinkingLevel → reasoning effort for Codex reasoning models (gjc parity).
|
|
67
|
+
// Drop out-of-enum values instead of forwarding them — the backend 400s on unknown efforts.
|
|
68
|
+
if (options.reasoningEffort && VALID_REASONING_EFFORTS.has(options.reasoningEffort)) {
|
|
69
|
+
payload.reasoning = { effort: options.reasoningEffort };
|
|
70
|
+
}
|
|
71
|
+
const accountId = extractChatgptAccountId(token);
|
|
72
|
+
const headers: Record<string, string> = {
|
|
73
|
+
"content-type": "application/json",
|
|
74
|
+
authorization: `Bearer ${token}`,
|
|
75
|
+
"OpenAI-Beta": "responses=experimental",
|
|
76
|
+
originator: "codex_cli_rs",
|
|
77
|
+
accept: "text/event-stream",
|
|
78
|
+
};
|
|
79
|
+
if (accountId) headers["chatgpt-account-id"] = accountId;
|
|
80
|
+
return { url: CODEX_RESPONSES_URL, headers, body: JSON.stringify(payload) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ResponsesEvent {
|
|
84
|
+
delta?: string;
|
|
85
|
+
usage?: { inputTokens?: number; outputTokens?: number };
|
|
86
|
+
error?: string;
|
|
87
|
+
/** `response.incomplete` cause (e.g. max_output_tokens) — surfaced when the
|
|
88
|
+
* whole response produced no text (round-5 #1). */
|
|
89
|
+
incompleteReason?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Parse one Responses SSE `data:` payload into a delta / usage / error. */
|
|
93
|
+
export function parseResponsesEvent(data: string): ResponsesEvent {
|
|
94
|
+
let o: {
|
|
95
|
+
type?: string;
|
|
96
|
+
delta?: unknown;
|
|
97
|
+
response?: {
|
|
98
|
+
usage?: { input_tokens?: number; output_tokens?: number };
|
|
99
|
+
error?: { message?: string };
|
|
100
|
+
incomplete_details?: { reason?: string };
|
|
101
|
+
};
|
|
102
|
+
error?: { message?: string };
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
o = JSON.parse(data);
|
|
106
|
+
} catch {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
if (o.type === "response.output_text.delta" && typeof o.delta === "string") return { delta: o.delta };
|
|
110
|
+
// `response.incomplete` (max_output_tokens / content filter) also carries usage — don't drop it.
|
|
111
|
+
if ((o.type === "response.completed" || o.type === "response.incomplete") && o.response?.usage) {
|
|
112
|
+
return {
|
|
113
|
+
usage: { inputTokens: o.response.usage.input_tokens, outputTokens: o.response.usage.output_tokens },
|
|
114
|
+
...(o.type === "response.incomplete" ? { incompleteReason: o.response.incomplete_details?.reason ?? "incomplete" } : {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (o.type === "response.failed" || o.type === "error") {
|
|
118
|
+
return { error: o.response?.error?.message ?? o.error?.message ?? "Codex response failed" };
|
|
119
|
+
}
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Round-5 #1: no-text completions surface their cause instead of returning "". */
|
|
124
|
+
function emptyCompletionError(reason: string | undefined): Error {
|
|
125
|
+
const hint = reason === "max_output_tokens"
|
|
126
|
+
? " — output budget exhausted before any text (often reasoning tokens); raise maxTokens or lower reasoning effort"
|
|
127
|
+
: "";
|
|
128
|
+
return new Error(`OpenAI Codex returned no content${reason ? ` (${reason})` : ""}${hint}.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Non-streaming call over the Codex backend (collects the streamed output). */
|
|
132
|
+
export async function codexResponsesCall(messages: Message[], options: CallOptions, credential: Credential): Promise<string> {
|
|
133
|
+
const { url, headers, body } = codexResponsesRequest(messages, options, credential);
|
|
134
|
+
const response = await fetch(url, { method: "POST", headers, body, signal: options.signal });
|
|
135
|
+
if (!response.ok) throw await providerHttpError("OpenAI", response);
|
|
136
|
+
if (!response.body) return "";
|
|
137
|
+
let out = "";
|
|
138
|
+
let incompleteReason: string | undefined;
|
|
139
|
+
for await (const data of readSse(response.body)) {
|
|
140
|
+
const ev = parseResponsesEvent(data);
|
|
141
|
+
if (ev.delta) out += ev.delta;
|
|
142
|
+
if (ev.usage) options.onUsage?.(ev.usage);
|
|
143
|
+
if (ev.incompleteReason) incompleteReason = ev.incompleteReason;
|
|
144
|
+
if (ev.error) throw new Error(`OpenAI Codex response failed: ${ev.error}`);
|
|
145
|
+
}
|
|
146
|
+
if (!out) throw emptyCompletionError(incompleteReason);
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Streaming call over the Codex backend. */
|
|
151
|
+
export async function* codexResponsesStream(
|
|
152
|
+
messages: Message[],
|
|
153
|
+
options: CallOptions,
|
|
154
|
+
credential: Credential,
|
|
155
|
+
): AsyncGenerator<string> {
|
|
156
|
+
const { url, headers, body } = codexResponsesRequest(messages, options, credential);
|
|
157
|
+
const response = await fetch(url, { method: "POST", headers, body, signal: options.signal });
|
|
158
|
+
if (!response.ok) throw await providerHttpError("OpenAI", response, "(stream)");
|
|
159
|
+
if (!response.body) return;
|
|
160
|
+
let yieldedAny = false;
|
|
161
|
+
let incompleteReason: string | undefined;
|
|
162
|
+
for await (const data of readSse(response.body)) {
|
|
163
|
+
const ev = parseResponsesEvent(data);
|
|
164
|
+
if (ev.delta) {
|
|
165
|
+
yieldedAny = true;
|
|
166
|
+
yield ev.delta;
|
|
167
|
+
}
|
|
168
|
+
if (ev.usage) options.onUsage?.(ev.usage);
|
|
169
|
+
if (ev.incompleteReason) incompleteReason = ev.incompleteReason;
|
|
170
|
+
if (ev.error) throw new Error(`OpenAI Codex response failed: ${ev.error}`);
|
|
171
|
+
}
|
|
172
|
+
if (!yieldedAny) throw emptyCompletionError(incompleteReason);
|
|
173
|
+
}
|
|
@@ -2,22 +2,39 @@ import type { Credential } from "../../auth";
|
|
|
2
2
|
import type { CallOptions, Message, ProviderAdapter } from "../types";
|
|
3
3
|
import { readSse } from "../sse";
|
|
4
4
|
import { providerHttpError } from "./errors";
|
|
5
|
+
import { codexResponsesCall, codexResponsesStream } from "./openai-responses";
|
|
5
6
|
|
|
6
|
-
function openaiRequest(messages: Message[], options: CallOptions, credential: Credential, stream: boolean): { url: string; headers: Record<string, string>; body: string } {
|
|
7
|
-
const
|
|
8
|
-
const model = resolvedModel.includes("gpt-4o") ? "gpt-4o" : resolvedModel;
|
|
7
|
+
export function openaiRequest(messages: Message[], options: CallOptions, credential: Credential, stream: boolean): { url: string; headers: Record<string, string>; body: string } {
|
|
8
|
+
const model = options.model.startsWith("openai/") ? options.model.slice(7) : options.model;
|
|
9
9
|
const systemPrompt = options.systemPrompt ?? messages.find(m => m.role === "system")?.content;
|
|
10
|
-
const openaiMessages: { role: string; content:
|
|
10
|
+
const openaiMessages: { role: string; content: unknown }[] = [];
|
|
11
11
|
if (systemPrompt) openaiMessages.push({ role: "system", content: systemPrompt });
|
|
12
12
|
for (const msg of messages) {
|
|
13
|
-
if (msg.role
|
|
13
|
+
if (msg.role === "system") continue;
|
|
14
|
+
// Image attachments (clipboard paste) use the content-parts form with data URLs;
|
|
15
|
+
// text-only messages keep the plain-string form every OpenAI-compat server accepts.
|
|
16
|
+
const content = msg.images?.length
|
|
17
|
+
? [
|
|
18
|
+
...(msg.content ? [{ type: "text", text: msg.content }] : []),
|
|
19
|
+
...msg.images.map(img => ({ type: "image_url", image_url: { url: `data:${img.mediaType};base64,${img.data}` } })),
|
|
20
|
+
]
|
|
21
|
+
: msg.content;
|
|
22
|
+
openaiMessages.push({ role: msg.role, content });
|
|
14
23
|
}
|
|
24
|
+
// Reasoning models (o-series, gpt-5 family) take max_completion_tokens + reasoning_effort
|
|
25
|
+
// and reject temperature; classic chat models (gpt-4o, …) take max_tokens + temperature.
|
|
26
|
+
const isReasoning = /^o\d/.test(model) || /^gpt-5/.test(model);
|
|
15
27
|
const payload: Record<string, unknown> = {
|
|
16
28
|
model,
|
|
17
29
|
messages: openaiMessages,
|
|
18
|
-
temperature: options.temperature ?? 0.2,
|
|
19
|
-
max_tokens: options.maxTokens ?? 4000,
|
|
20
30
|
};
|
|
31
|
+
if (isReasoning) {
|
|
32
|
+
payload.max_completion_tokens = options.maxTokens ?? 4000;
|
|
33
|
+
if (options.reasoningEffort) payload.reasoning_effort = options.reasoningEffort;
|
|
34
|
+
} else {
|
|
35
|
+
payload.temperature = options.temperature ?? 0.2;
|
|
36
|
+
payload.max_tokens = options.maxTokens ?? 4000;
|
|
37
|
+
}
|
|
21
38
|
if (stream) {
|
|
22
39
|
payload.stream = true;
|
|
23
40
|
payload.stream_options = { include_usage: true };
|
|
@@ -31,32 +48,67 @@ function openaiRequest(messages: Message[], options: CallOptions, credential: Cr
|
|
|
31
48
|
};
|
|
32
49
|
}
|
|
33
50
|
|
|
51
|
+
/** Round-5 #1: surface the finish_reason when a 200 carries no text — an empty
|
|
52
|
+
* reply only bounces in the JSON loop (billed) until the step budget dies. */
|
|
53
|
+
function emptyCompletionError(finishReason: string | undefined): Error {
|
|
54
|
+
const hint = finishReason === "length"
|
|
55
|
+
? " — output budget exhausted before any text (often reasoning tokens); raise maxTokens or lower reasoning effort"
|
|
56
|
+
: "";
|
|
57
|
+
return new Error(`OpenAI returned no content${finishReason ? ` (finish_reason=${finishReason})` : ""}${hint}.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
export const openaiAdapter: ProviderAdapter = {
|
|
35
61
|
name: "openai",
|
|
36
62
|
async call(messages, options, credential) {
|
|
63
|
+
// ChatGPT/Codex OAuth can't use /chat/completions — route to the Codex Responses backend.
|
|
64
|
+
if (credential.kind === "oauth") return codexResponsesCall(messages, options, credential);
|
|
37
65
|
const { url, headers, body } = openaiRequest(messages, options, credential, false);
|
|
38
66
|
const response = await fetch(url, { method: "POST", headers, body, signal: options.signal });
|
|
39
67
|
if (!response.ok) throw await providerHttpError("OpenAI", response);
|
|
40
|
-
const result = (await response.json()) as { choices: { message: { content: string } }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
|
|
68
|
+
const result = (await response.json()) as { choices: { message: { content: string }; finish_reason?: string }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
|
|
41
69
|
if (result.usage) options.onUsage?.({ inputTokens: result.usage.prompt_tokens, outputTokens: result.usage.completion_tokens });
|
|
42
|
-
|
|
70
|
+
const text = result.choices[0]?.message?.content ?? "";
|
|
71
|
+
if (!text) throw emptyCompletionError(result.choices[0]?.finish_reason);
|
|
72
|
+
return text;
|
|
43
73
|
},
|
|
44
74
|
async *stream(messages, options, credential) {
|
|
75
|
+
if (credential.kind === "oauth") {
|
|
76
|
+
yield* codexResponsesStream(messages, options, credential);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
45
79
|
const { url, headers, body } = openaiRequest(messages, options, credential, true);
|
|
46
|
-
|
|
80
|
+
let response = await fetch(url, { method: "POST", headers, body, signal: options.signal });
|
|
81
|
+
if (response.status === 400) {
|
|
82
|
+
// Compat retry (round-5 #5): some OpenAI-compatible backends (llama.cpp,
|
|
83
|
+
// LM Studio, older vLLM) 400 on the OPTIONAL `stream_options` usage nicety.
|
|
84
|
+
// Retry once without it instead of killing the turn over a nicety.
|
|
85
|
+
const errBody = await response.clone().text().catch(() => "");
|
|
86
|
+
if (/stream_options/i.test(errBody)) {
|
|
87
|
+
const stripped = JSON.parse(body) as Record<string, unknown>;
|
|
88
|
+
delete stripped.stream_options;
|
|
89
|
+
response = await fetch(url, { method: "POST", headers, body: JSON.stringify(stripped), signal: options.signal });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
47
92
|
if (!response.ok) throw await providerHttpError("OpenAI", response, "(stream)");
|
|
48
93
|
if (!response.body) return;
|
|
94
|
+
let yieldedAny = false;
|
|
95
|
+
let finishReason: string | undefined;
|
|
49
96
|
for await (const data of readSse(response.body)) {
|
|
50
|
-
let chunk: { choices?: { delta?: { content?: string } }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
|
|
97
|
+
let chunk: { choices?: { delta?: { content?: string }; finish_reason?: string }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
|
|
51
98
|
try {
|
|
52
99
|
chunk = JSON.parse(data);
|
|
53
100
|
} catch {
|
|
54
101
|
continue;
|
|
55
102
|
}
|
|
56
103
|
const delta = chunk.choices?.[0]?.delta?.content;
|
|
57
|
-
if (delta)
|
|
104
|
+
if (delta) {
|
|
105
|
+
yieldedAny = true;
|
|
106
|
+
yield delta;
|
|
107
|
+
}
|
|
108
|
+
if (chunk.choices?.[0]?.finish_reason) finishReason = chunk.choices[0].finish_reason;
|
|
58
109
|
if (chunk.usage) options.onUsage?.({ inputTokens: chunk.usage.prompt_tokens, outputTokens: chunk.usage.completion_tokens });
|
|
59
110
|
}
|
|
111
|
+
if (!yieldedAny) throw emptyCompletionError(finishReason);
|
|
60
112
|
},
|
|
61
113
|
};
|
|
62
114
|
|
package/src/ai/sse.ts
CHANGED
|
@@ -27,7 +27,10 @@ export async function* readLines(stream: ReadableStream<Uint8Array>): AsyncGener
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
} finally {
|
|
30
|
-
|
|
30
|
+
// cancel() frees the underlying HTTP connection on early generator return
|
|
31
|
+
// (consumer break) — releaseLock() alone leaks the socket until GC. No-op on a
|
|
32
|
+
// normally-drained stream.
|
|
33
|
+
await reader.cancel().catch(() => {});
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
|
package/src/ai/types.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import type { Credential } from "../auth";
|
|
2
2
|
|
|
3
|
-
export type ProviderName = "anthropic" | "openai" | "gemini" | "ollama";
|
|
3
|
+
export type ProviderName = "anthropic" | "openai" | "gemini" | "antigravity" | "ollama";
|
|
4
|
+
|
|
5
|
+
/** An image attached to a (user) message — base64 payload + IANA media type. */
|
|
6
|
+
export interface ImageAttachment {
|
|
7
|
+
/** e.g. "image/png", "image/jpeg" */
|
|
8
|
+
mediaType: string;
|
|
9
|
+
/** Raw base64 (no data: URL prefix). */
|
|
10
|
+
data: string;
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
export interface Message {
|
|
6
14
|
role: "system" | "user" | "assistant";
|
|
7
15
|
content: string;
|
|
16
|
+
/** Optional image attachments (clipboard paste). Multimodal providers render
|
|
17
|
+
* these alongside `content`; history bookkeeping (compaction, transcripts)
|
|
18
|
+
* keeps treating `content` as the message body. */
|
|
19
|
+
images?: ImageAttachment[];
|
|
8
20
|
}
|
|
9
21
|
|
|
10
22
|
export interface Usage {
|
|
@@ -26,6 +38,11 @@ export interface CallOptions {
|
|
|
26
38
|
onUsage?: (usage: Usage) => void;
|
|
27
39
|
/** Abort in-flight provider requests (Ctrl-C / timeout / supersede). */
|
|
28
40
|
signal?: AbortSignal;
|
|
41
|
+
/** Reasoning effort for reasoning models (o-series / gpt-5), mapped from thinkingLevel. */
|
|
42
|
+
reasoningEffort?: "minimal" | "low" | "medium" | "high";
|
|
43
|
+
/** Notified before each auto-retry backoff wait (rate limits / transient errors).
|
|
44
|
+
* NOT forwarded to provider adapters — consumed by the manager's retry layer. */
|
|
45
|
+
onRetry?: (attempt: number, err: unknown, delayMs: number) => void;
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
export interface ProviderAdapter {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-06-11 | Updated: 2026-06-11 -->
|
|
3
|
+
|
|
4
|
+
# auth
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Authentication and credential management for OAuth flows and API keys. Ensures secure storage and retrieval of provider credentials.
|
|
8
|
+
|
|
9
|
+
## Key Files
|
|
10
|
+
| File | Description |
|
|
11
|
+
|------|-------------|
|
|
12
|
+
| `store.ts` | Secure credential storage mechanism |
|
|
13
|
+
| `config.ts` | Resolution of keys from environment variables and config files |
|
|
14
|
+
|
|
15
|
+
## Subdirectories
|
|
16
|
+
| Directory | Purpose |
|
|
17
|
+
|-----------|---------|
|
|
18
|
+
| `flows/` | Specific OAuth implementations (see `flows/AGENTS.md`) |
|
|
19
|
+
|
|
20
|
+
## For AI Agents
|
|
21
|
+
|
|
22
|
+
### Working In This Directory
|
|
23
|
+
- NEVER log credentials or sensitive tokens.
|
|
24
|
+
- Handle token refresh transparently.
|
|
25
|
+
- Ensure atomic file writes when updating local credential caches.
|
|
26
|
+
|
|
27
|
+
### Testing Requirements
|
|
28
|
+
- Mock the filesystem when testing the credential store.
|
|
29
|
+
|
|
30
|
+
### Common Patterns
|
|
31
|
+
- Fallback chains: Memory cache -> Config File -> Environment Variables.
|
|
32
|
+
|
|
33
|
+
## Dependencies
|
|
34
|
+
|
|
35
|
+
### Internal
|
|
36
|
+
- Used by `src/ai/providers/` to authenticate requests.
|
|
37
|
+
|
|
38
|
+
### External
|
|
39
|
+
- OS-level secure storage if applicable, or local encrypted files.
|
|
40
|
+
|
|
41
|
+
<!-- MANUAL: -->
|
|
@@ -24,7 +24,7 @@ export interface OAuthCallbackFlowOptions {
|
|
|
24
24
|
|
|
25
25
|
export type CallbackResult = { code: string; state: string };
|
|
26
26
|
|
|
27
|
-
const SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>
|
|
27
|
+
const SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>jeo — login complete</title>
|
|
28
28
|
<style>body{font-family:system-ui,sans-serif;background:#0d1117;color:#e6edf3;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
|
29
29
|
.card{text-align:center;padding:2rem 3rem;border:1px solid #30363d;border-radius:12px;background:#161b22}
|
|
30
30
|
h1{margin:0 0 .5rem;font-size:1.4rem}p{margin:0;color:#8b949e}</style></head>
|
|
@@ -152,6 +152,11 @@ export abstract class OAuthCallbackFlow {
|
|
|
152
152
|
const ask = this.ctrl.onManualCodeInput;
|
|
153
153
|
const manualPromise = (async (): Promise<CallbackResult> => {
|
|
154
154
|
while (true) {
|
|
155
|
+
// Cooperative cancellation: once the controller signal aborts (the
|
|
156
|
+
// caller finished or failed the login), STOP re-prompting. Without
|
|
157
|
+
// this guard an aborted `ask()` rejects instantly, the catch maps it
|
|
158
|
+
// to null, and the loop spins re-asking forever.
|
|
159
|
+
if (signal.aborted) return callbackPromise;
|
|
155
160
|
const result = await Promise.race<CallbackResult | null>([
|
|
156
161
|
callbackPromise,
|
|
157
162
|
ask()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!-- Parent: ../../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-06-11 | Updated: 2026-06-11 -->
|
|
3
|
+
|
|
4
|
+
# flows
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Specific OAuth flow implementations for various providers.
|
|
8
|
+
|
|
9
|
+
## Key Files
|
|
10
|
+
| File | Description |
|
|
11
|
+
|------|-------------|
|
|
12
|
+
| `oauth.ts` / `pkce.ts` | Generic OAuth and PKCE utilities |
|
|
13
|
+
| `*.ts` | Provider-specific login flows (e.g., anthropic, openai, gemini) |
|
|
14
|
+
|
|
15
|
+
## Subdirectories
|
|
16
|
+
*(None)*
|
|
17
|
+
|
|
18
|
+
## For AI Agents
|
|
19
|
+
|
|
20
|
+
### Working In This Directory
|
|
21
|
+
- Handle browser launching and local callback servers securely.
|
|
22
|
+
|
|
23
|
+
### Testing Requirements
|
|
24
|
+
- Ensure ports and servers are closed cleanly in tests.
|
|
25
|
+
|
|
26
|
+
### Common Patterns
|
|
27
|
+
- Loopback HTTP servers for receiving OAuth callbacks.
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
*(None)*
|
|
31
|
+
|
|
32
|
+
<!-- MANUAL: -->
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity OAuth — Google authorization-code flow with the Antigravity
|
|
3
|
+
* desktop-app client (gjc parity). The Antigravity/Cloud Code Assist agent
|
|
4
|
+
* backend rejects gemini-cli client tokens (PERMISSION_DENIED), so Antigravity
|
|
5
|
+
* models need this dedicated login: different client id/secret, extra scopes
|
|
6
|
+
* (cclog, experimentsandconfigs), and ANTIGRAVITY discovery metadata.
|
|
7
|
+
*
|
|
8
|
+
* Like the Google installed-app secret in `google.ts`, the client secret ships
|
|
9
|
+
* publicly in the Antigravity app (RFC 8252 §8.5: installed-app secrets are not
|
|
10
|
+
* confidential) and is stored base64-encoded only to avoid secret scanners.
|
|
11
|
+
* `ANTIGRAVITY_OAUTH_CLIENT_SECRET` overrides it for self-provisioned clients.
|
|
12
|
+
*/
|
|
13
|
+
import { OAuthCallbackFlow } from "../callback-server";
|
|
14
|
+
import { discoverGoogleProjectId, ANTIGRAVITY_DISCOVERY_METADATA } from "./google-project";
|
|
15
|
+
import { getAntigravityUserAgent } from "../../ai/providers/antigravity";
|
|
16
|
+
import type { OAuthController, OAuthCredentials } from "../types";
|
|
17
|
+
|
|
18
|
+
const decode = (s: string) => atob(s);
|
|
19
|
+
const CLIENT_ID = decode(
|
|
20
|
+
[
|
|
21
|
+
"MTA3MTAwNjA2MDU5MS10",
|
|
22
|
+
"bWhzc2luMmgyMWxjcmUy",
|
|
23
|
+
"MzV2dG9sb2poNGc0MDNl",
|
|
24
|
+
"cC5hcHBzLmdvb2dsZXVz",
|
|
25
|
+
"ZXJjb250ZW50LmNvbQ==",
|
|
26
|
+
].join("")
|
|
27
|
+
);
|
|
28
|
+
const DEFAULT_CLIENT_SECRET_B64 = [
|
|
29
|
+
"R09DU1BYLUs1OEZX",
|
|
30
|
+
"UjQ4NkxkTEoxbUxC",
|
|
31
|
+
"OHNYQzR6NnFEQWY=",
|
|
32
|
+
].join("");
|
|
33
|
+
|
|
34
|
+
/** Effective Antigravity OAuth client secret: env override → bundled default. */
|
|
35
|
+
export function antigravityClientSecret(env: Record<string, string | undefined> = process.env): string {
|
|
36
|
+
return env.ANTIGRAVITY_OAUTH_CLIENT_SECRET || decode(DEFAULT_CLIENT_SECRET_B64);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
40
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
41
|
+
const CALLBACK_PORT = 51121;
|
|
42
|
+
const CALLBACK_PATH = "/oauth-callback";
|
|
43
|
+
const SCOPES = [
|
|
44
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
45
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
46
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
47
|
+
"https://www.googleapis.com/auth/cclog",
|
|
48
|
+
"https://www.googleapis.com/auth/experimentsandconfigs",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/** Discover (or provision) the Antigravity Cloud Code Assist project for an access token. */
|
|
52
|
+
export function discoverAntigravityProjectId(
|
|
53
|
+
accessToken: string,
|
|
54
|
+
opts: { onProgress?: (message: string) => void } = {},
|
|
55
|
+
): Promise<string> {
|
|
56
|
+
return discoverGoogleProjectId(accessToken, {
|
|
57
|
+
metadata: { ...ANTIGRAVITY_DISCOVERY_METADATA },
|
|
58
|
+
extraHeaders: { "User-Agent": getAntigravityUserAgent() },
|
|
59
|
+
onProgress: opts.onProgress,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function getUserEmail(access: string): Promise<string | undefined> {
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
|
66
|
+
headers: { authorization: `Bearer ${access}` },
|
|
67
|
+
});
|
|
68
|
+
if (res.ok) return ((await res.json()) as { email?: string }).email;
|
|
69
|
+
} catch {
|
|
70
|
+
/* email is optional */
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
76
|
+
constructor(ctrl: OAuthController) {
|
|
77
|
+
super(ctrl, { preferredPort: CALLBACK_PORT, callbackPath: CALLBACK_PATH });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async generateAuthUrl(state: string, redirectUri: string) {
|
|
81
|
+
const params = new URLSearchParams({
|
|
82
|
+
client_id: CLIENT_ID,
|
|
83
|
+
response_type: "code",
|
|
84
|
+
redirect_uri: redirectUri,
|
|
85
|
+
scope: SCOPES.join(" "),
|
|
86
|
+
state,
|
|
87
|
+
access_type: "offline",
|
|
88
|
+
prompt: "consent",
|
|
89
|
+
});
|
|
90
|
+
return { url: `${AUTH_URL}?${params.toString()}`, instructions: "Complete the Antigravity sign-in in your browser." };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
94
|
+
const res = await fetch(TOKEN_URL, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
97
|
+
body: new URLSearchParams({
|
|
98
|
+
client_id: CLIENT_ID,
|
|
99
|
+
client_secret: antigravityClientSecret(),
|
|
100
|
+
code,
|
|
101
|
+
grant_type: "authorization_code",
|
|
102
|
+
redirect_uri: redirectUri,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) throw new Error(`Antigravity token exchange failed (HTTP ${res.status}): ${await res.text()}`);
|
|
106
|
+
const data = (await res.json()) as { access_token: string; refresh_token?: string; expires_in: number };
|
|
107
|
+
if (!data.refresh_token) throw new Error("No refresh token received from Google. Try again with prompt=consent.");
|
|
108
|
+
const email = await getUserEmail(data.access_token);
|
|
109
|
+
let projectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID || undefined;
|
|
110
|
+
if (!projectId) {
|
|
111
|
+
// Project discovery is what makes the login usable — but keep login
|
|
112
|
+
// best-effort: the adapter retries discovery lazily at call time.
|
|
113
|
+
try {
|
|
114
|
+
projectId = await discoverAntigravityProjectId(data.access_token);
|
|
115
|
+
} catch {
|
|
116
|
+
projectId = undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
access: data.access_token,
|
|
121
|
+
refresh: data.refresh_token,
|
|
122
|
+
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
123
|
+
email,
|
|
124
|
+
projectId,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function loginAntigravity(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
130
|
+
return new AntigravityOAuthFlow(ctrl).login();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function refreshAntigravityToken(refreshToken: string): Promise<OAuthCredentials> {
|
|
134
|
+
const res = await fetch(TOKEN_URL, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
137
|
+
body: new URLSearchParams({
|
|
138
|
+
client_id: CLIENT_ID,
|
|
139
|
+
client_secret: antigravityClientSecret(),
|
|
140
|
+
refresh_token: refreshToken,
|
|
141
|
+
grant_type: "refresh_token",
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) throw new Error(`Antigravity token refresh failed (HTTP ${res.status}): ${await res.text()}`);
|
|
145
|
+
const data = (await res.json()) as { access_token: string; expires_in: number; refresh_token?: string };
|
|
146
|
+
return {
|
|
147
|
+
access: data.access_token,
|
|
148
|
+
refresh: data.refresh_token || refreshToken,
|
|
149
|
+
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
150
|
+
};
|
|
151
|
+
}
|