jeo-code 0.1.0 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.ja.md +160 -0
  2. package/README.ko.md +160 -0
  3. package/README.md +115 -297
  4. package/README.zh.md +160 -0
  5. package/package.json +11 -6
  6. package/scripts/install.sh +28 -28
  7. package/scripts/uninstall.sh +17 -15
  8. package/src/AGENTS.md +50 -0
  9. package/src/agent/AGENTS.md +49 -0
  10. package/src/agent/bash-fixups.ts +103 -0
  11. package/src/agent/compaction.ts +410 -19
  12. package/src/agent/config-schema.ts +119 -5
  13. package/src/agent/context-files.ts +314 -17
  14. package/src/agent/dev/AGENTS.md +36 -0
  15. package/src/agent/dev/advanced-analyzer.ts +12 -0
  16. package/src/agent/dev/evolution-bridge.ts +82 -0
  17. package/src/agent/dev/evolution-logger.ts +41 -0
  18. package/src/agent/dev/self-analysis.ts +64 -0
  19. package/src/agent/dev/self-improve.ts +24 -0
  20. package/src/agent/dev/spec-automation.ts +49 -0
  21. package/src/agent/engine.ts +808 -54
  22. package/src/agent/hooks.ts +273 -0
  23. package/src/agent/loop.ts +21 -1
  24. package/src/agent/memory.ts +201 -0
  25. package/src/agent/model-recency.ts +32 -0
  26. package/src/agent/output-minimizer.ts +108 -0
  27. package/src/agent/output-util.ts +64 -0
  28. package/src/agent/plan.ts +187 -0
  29. package/src/agent/seed.ts +52 -0
  30. package/src/agent/session.ts +235 -21
  31. package/src/agent/state.ts +286 -39
  32. package/src/agent/step-budget.ts +232 -0
  33. package/src/agent/subagents.ts +223 -26
  34. package/src/agent/task-tool.ts +272 -0
  35. package/src/agent/todo-tool.ts +87 -0
  36. package/src/agent/tokenizer.ts +117 -0
  37. package/src/agent/tool-registry.ts +54 -0
  38. package/src/agent/tools.ts +624 -103
  39. package/src/agent/web-search.ts +538 -0
  40. package/src/ai/AGENTS.md +44 -0
  41. package/src/ai/index.ts +1 -0
  42. package/src/ai/model-catalog-compat.ts +3 -1
  43. package/src/ai/model-catalog.ts +74 -9
  44. package/src/ai/model-discovery.ts +215 -17
  45. package/src/ai/model-manager.ts +346 -32
  46. package/src/ai/model-picker.ts +1 -1
  47. package/src/ai/model-registry.ts +4 -2
  48. package/src/ai/pricing.ts +84 -0
  49. package/src/ai/provider-registry.ts +23 -0
  50. package/src/ai/provider-status.ts +60 -16
  51. package/src/ai/providers/AGENTS.md +42 -0
  52. package/src/ai/providers/anthropic.ts +250 -31
  53. package/src/ai/providers/antigravity.ts +219 -0
  54. package/src/ai/providers/errors.ts +15 -1
  55. package/src/ai/providers/gemini.ts +196 -13
  56. package/src/ai/providers/ollama.ts +37 -7
  57. package/src/ai/providers/openai-responses.ts +173 -0
  58. package/src/ai/providers/openai.ts +64 -12
  59. package/src/ai/sse.ts +4 -1
  60. package/src/ai/types.ts +18 -1
  61. package/src/auth/AGENTS.md +41 -0
  62. package/src/auth/callback-server.ts +6 -1
  63. package/src/auth/flows/AGENTS.md +32 -0
  64. package/src/auth/flows/antigravity.ts +151 -0
  65. package/src/auth/flows/google-project.ts +190 -0
  66. package/src/auth/flows/google.ts +39 -18
  67. package/src/auth/flows/index.ts +15 -5
  68. package/src/auth/flows/openai.ts +2 -2
  69. package/src/auth/oauth.ts +8 -0
  70. package/src/auth/refresh.ts +44 -27
  71. package/src/auth/storage.ts +149 -26
  72. package/src/auth/types.ts +1 -1
  73. package/src/autopilot.ts +362 -0
  74. package/src/bun-imports.d.ts +4 -0
  75. package/src/cli/AGENTS.md +39 -0
  76. package/src/cli/runner.ts +148 -14
  77. package/src/cli.ts +13 -4
  78. package/src/commands/AGENTS.md +40 -0
  79. package/src/commands/approve.ts +62 -3
  80. package/src/commands/auth.ts +167 -25
  81. package/src/commands/chat.ts +37 -8
  82. package/src/commands/deep-interview.ts +633 -175
  83. package/src/commands/doctor.ts +84 -37
  84. package/src/commands/evolve-core.ts +18 -0
  85. package/src/commands/evolve.ts +2 -1
  86. package/src/commands/export.ts +176 -0
  87. package/src/commands/gjc.ts +52 -0
  88. package/src/commands/launch.ts +3549 -240
  89. package/src/commands/mcp.ts +3 -3
  90. package/src/commands/ooo-seed.ts +19 -0
  91. package/src/commands/ralplan.ts +253 -35
  92. package/src/commands/resume.ts +1 -1
  93. package/src/commands/session.ts +183 -0
  94. package/src/commands/setup-helpers.ts +10 -3
  95. package/src/commands/setup.ts +57 -16
  96. package/src/commands/skills.ts +78 -18
  97. package/src/commands/state.ts +198 -0
  98. package/src/commands/status.ts +84 -0
  99. package/src/commands/team.ts +340 -212
  100. package/src/commands/ultragoal.ts +122 -61
  101. package/src/commands/update.ts +244 -0
  102. package/src/ledger.ts +270 -0
  103. package/src/mcp/AGENTS.md +38 -0
  104. package/src/mcp/server.ts +115 -14
  105. package/src/mcp/tools.ts +42 -22
  106. package/src/md-modules.d.ts +4 -0
  107. package/src/prompts/AGENTS.md +41 -0
  108. package/src/prompts/agents/AGENTS.md +35 -0
  109. package/src/prompts/agents/architect.md +35 -0
  110. package/src/prompts/agents/critic.md +37 -0
  111. package/src/prompts/agents/executor.md +36 -0
  112. package/src/prompts/agents/planner.md +37 -0
  113. package/src/prompts/skills/AGENTS.md +36 -0
  114. package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
  115. package/src/prompts/skills/deep-dive/SKILL.md +13 -0
  116. package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
  117. package/src/prompts/skills/deep-interview/SKILL.md +12 -0
  118. package/src/prompts/skills/gjc/AGENTS.md +31 -0
  119. package/src/prompts/skills/gjc/SKILL.md +15 -0
  120. package/src/prompts/skills/ralplan/AGENTS.md +31 -0
  121. package/src/prompts/skills/ralplan/SKILL.md +11 -0
  122. package/src/prompts/skills/team/AGENTS.md +31 -0
  123. package/src/prompts/skills/team/SKILL.md +11 -0
  124. package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
  125. package/src/prompts/skills/ultragoal/SKILL.md +11 -0
  126. package/src/skills/AGENTS.md +38 -0
  127. package/src/skills/catalog.ts +565 -31
  128. package/src/tui/AGENTS.md +43 -0
  129. package/src/tui/app.ts +1181 -92
  130. package/src/tui/components/AGENTS.md +42 -0
  131. package/src/tui/components/ascii-art.ts +257 -15
  132. package/src/tui/components/autocomplete.ts +98 -16
  133. package/src/tui/components/autopilot-status.ts +65 -0
  134. package/src/tui/components/category-index.ts +49 -0
  135. package/src/tui/components/code-view.ts +54 -11
  136. package/src/tui/components/color.ts +171 -2
  137. package/src/tui/components/config-panel.ts +82 -15
  138. package/src/tui/components/duration.ts +38 -0
  139. package/src/tui/components/evolution.ts +3 -3
  140. package/src/tui/components/footer.ts +91 -42
  141. package/src/tui/components/forge.ts +426 -31
  142. package/src/tui/components/hints.ts +54 -0
  143. package/src/tui/components/hud.ts +73 -0
  144. package/src/tui/components/index.ts +4 -0
  145. package/src/tui/components/input-box.ts +150 -0
  146. package/src/tui/components/layout.ts +11 -3
  147. package/src/tui/components/live-model-picker.ts +108 -0
  148. package/src/tui/components/markdown-table.ts +140 -0
  149. package/src/tui/components/markdown-text.ts +97 -0
  150. package/src/tui/components/meter.ts +4 -1
  151. package/src/tui/components/model-picker.ts +3 -2
  152. package/src/tui/components/provider-picker.ts +3 -2
  153. package/src/tui/components/section.ts +70 -0
  154. package/src/tui/components/select-list.ts +40 -10
  155. package/src/tui/components/skill-picker.ts +25 -0
  156. package/src/tui/components/slash.ts +244 -21
  157. package/src/tui/components/status.ts +272 -11
  158. package/src/tui/components/step-timeline.ts +218 -0
  159. package/src/tui/components/stream.ts +26 -9
  160. package/src/tui/components/themes.ts +212 -6
  161. package/src/tui/components/todo-card.ts +47 -0
  162. package/src/tui/components/tool-list.ts +58 -12
  163. package/src/tui/components/transcript.ts +120 -0
  164. package/src/tui/components/update-box.ts +31 -0
  165. package/src/tui/components/welcome.ts +162 -0
  166. package/src/tui/components/width.ts +163 -0
  167. package/src/tui/monitoring/AGENTS.md +31 -0
  168. package/src/tui/monitoring/hud-view.ts +55 -0
  169. package/src/tui/renderer.ts +112 -3
  170. package/src/tui/terminal.ts +40 -33
  171. package/src/util/AGENTS.md +39 -0
  172. package/src/util/clipboard-image.ts +118 -0
  173. package/src/util/env.ts +12 -0
  174. package/src/util/provider-error.ts +78 -0
  175. package/src/util/retry.ts +91 -6
  176. package/src/util/update-check.ts +64 -0
  177. 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 resolvedModel = options.model.startsWith("openai/") ? options.model.slice(7) : options.model;
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: string }[] = [];
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 !== "system") openaiMessages.push({ role: msg.role, content: msg.content });
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
- return result.choices[0]?.message?.content ?? "";
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
- const response = await fetch(url, { method: "POST", headers, body, signal: options.signal });
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) yield 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
- reader.releaseLock();
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>joc — login complete</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
+ }