@giovannijecha/jecode 0.1.8 → 0.2.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/README.md +41 -27
- package/dist/account-lock.js +112 -0
- package/dist/accounts.js +100 -0
- package/dist/cli-info.js +2 -2
- package/dist/commands.js +27 -102
- package/dist/controller.js +3 -0
- package/dist/credential-commands.js +40 -14
- package/dist/credential-safety.js +2 -1
- package/dist/external-browser.js +53 -0
- package/dist/oauth-http.js +114 -0
- package/dist/openai-account-command.js +124 -0
- package/dist/openai-account.js +91 -0
- package/dist/openai-oauth-callback.js +186 -0
- package/dist/openai-oauth-tokens.js +65 -0
- package/dist/openai-oauth.js +203 -0
- package/dist/permission-command.js +107 -0
- package/dist/permissions.js +113 -0
- package/dist/provider-commands.js +22 -62
- package/dist/provider-label.js +10 -0
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/index.js +2 -1
- package/dist/providers/ollama.js +1 -1
- package/dist/providers/openai-codex.js +114 -0
- package/dist/providers/openai-stream.js +13 -9
- package/dist/providers/openai-wire.js +4 -4
- package/dist/providers/openai.js +2 -2
- package/dist/providers/sse.js +1 -1
- package/dist/settings-command.js +13 -6
- package/dist/tools/shell.js +1 -1
- package/dist/transcript.js +0 -3
- package/dist/tui/app-workflows.js +17 -17
- package/dist/tui/app.js +6 -11
- package/dist/tui/approve.js +6 -34
- package/dist/tui/blocks.js +1 -3
- package/dist/tui/complete.js +4 -4
- package/dist/tui/components/menu.js +4 -3
- package/dist/tui/components/misc.js +0 -6
- package/dist/tui/components/status.js +6 -7
- package/dist/tui/feedback.js +11 -9
- package/dist/tui/help.js +29 -0
- package/dist/tui/modal.js +22 -11
- package/dist/tui/overlay.js +15 -4
- package/dist/tui/picker.js +1 -1
- package/dist/tui/session-view.js +12 -6
- package/dist/version.js +14 -0
- package/docs/assets/brand/jeco-256.png +0 -0
- package/package.json +5 -1
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
// Provider
|
|
1
|
+
// Provider and model command flows.
|
|
2
2
|
import { heading } from "./tui/picker.js";
|
|
3
3
|
import { PROVIDERS } from "./providers/index.js";
|
|
4
4
|
import { readSettings } from "./settings.js";
|
|
5
|
-
import {
|
|
5
|
+
import { authenticationNeed, ensureProviderAuthentication, } from "./credential-commands.js";
|
|
6
6
|
import { providerFailure } from "./provider-errors.js";
|
|
7
|
+
import { providerLabel } from "./provider-label.js";
|
|
7
8
|
/**
|
|
8
9
|
* The provider menu.
|
|
9
10
|
*
|
|
10
11
|
* Every provider is offered, including the ones that cannot run: the reason
|
|
11
12
|
* one is unusable — the variable it wants, by name — is worth more on screen
|
|
12
13
|
* than the row would be worth hidden. A blocked choice opens the same masked
|
|
13
|
-
* credential flow used by
|
|
14
|
+
* credential flow used by settings, and cancellation leaves the old choice.
|
|
14
15
|
*/
|
|
15
16
|
export async function providersCommand(session, host, behavior = {}) {
|
|
16
17
|
const choose = chooser(host);
|
|
@@ -34,32 +35,22 @@ export async function providersCommand(session, host, behavior = {}) {
|
|
|
34
35
|
// Picking the provider already in use is not a no-op when it cannot run:
|
|
35
36
|
// it is how the user asks to fix the reason it cannot.
|
|
36
37
|
if (chosen.id === session.provider.id) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
await askForKey(chosen.keyVar, host, session.palette);
|
|
44
|
-
if (chosen.blocked() !== undefined)
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
return true;
|
|
38
|
+
if (!(await ensureProviderAuthentication(chosen, session, host)))
|
|
39
|
+
return false;
|
|
40
|
+
return session.model === ""
|
|
41
|
+
? modelsCommand(session, host, { announce: false, save: behavior.save })
|
|
42
|
+
: true;
|
|
48
43
|
}
|
|
49
44
|
// A provider that cannot run is worth one offer to fix it, here, rather
|
|
50
45
|
// than a note telling the user to leave and export something.
|
|
51
46
|
const blocked = chosen.blocked();
|
|
52
47
|
if (blocked !== undefined) {
|
|
53
|
-
|
|
54
|
-
host.emit({ kind: "notice", text: blocked, tone: "error" });
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
await askForKey(chosen.keyVar, host, session.palette);
|
|
48
|
+
await ensureProviderAuthentication(chosen, session, host);
|
|
58
49
|
const still = chosen.blocked();
|
|
59
50
|
if (still !== undefined) {
|
|
60
51
|
host.emit({
|
|
61
52
|
kind: "notice",
|
|
62
|
-
text: `${
|
|
53
|
+
text: `${providerLabel(chosen.id)} still needs ${authenticationNeed(chosen)} · provider unchanged`,
|
|
63
54
|
tone: "warn",
|
|
64
55
|
});
|
|
65
56
|
return false;
|
|
@@ -77,6 +68,13 @@ export async function providersCommand(session, host, behavior = {}) {
|
|
|
77
68
|
session.model = readSettings().models?.[chosen.id] ?? chosen.defaultModel;
|
|
78
69
|
session.config.providerId = chosen.id;
|
|
79
70
|
session.config.model = session.model;
|
|
71
|
+
if (session.model === "" && !(await modelsCommand(session, host, { announce: false, save: false }))) {
|
|
72
|
+
session.provider = before.provider;
|
|
73
|
+
session.model = before.model;
|
|
74
|
+
session.config.providerId = before.providerId;
|
|
75
|
+
session.config.model = before.configModel;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
80
78
|
if (behavior.save !== false) {
|
|
81
79
|
const saved = readSettings();
|
|
82
80
|
const models = { ...saved.models };
|
|
@@ -93,7 +91,7 @@ export async function providersCommand(session, host, behavior = {}) {
|
|
|
93
91
|
if (behavior.announce !== false) {
|
|
94
92
|
host.emit({
|
|
95
93
|
kind: "notice",
|
|
96
|
-
text: session.model === "" ?
|
|
94
|
+
text: session.model === "" ? `provider · ${chosen.id} · pick a model` : `provider · ${chosen.id}`,
|
|
97
95
|
tone: "info",
|
|
98
96
|
});
|
|
99
97
|
}
|
|
@@ -111,16 +109,12 @@ export async function modelsCommand(session, host, behavior = {}) {
|
|
|
111
109
|
// to pick a model, and "go and export a variable" is not an answer.
|
|
112
110
|
const blocked = provider.blocked();
|
|
113
111
|
if (blocked !== undefined) {
|
|
114
|
-
|
|
115
|
-
host.emit({ kind: "notice", text: blocked, tone: "error" });
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
await askForKey(provider.keyVar, host, session.palette);
|
|
112
|
+
await ensureProviderAuthentication(provider, session, host);
|
|
119
113
|
const still = provider.blocked();
|
|
120
114
|
if (still !== undefined) {
|
|
121
115
|
host.emit({
|
|
122
116
|
kind: "notice",
|
|
123
|
-
text: `${
|
|
117
|
+
text: `${providerLabel(provider.id)} still needs ${authenticationNeed(provider)}`,
|
|
124
118
|
tone: "warn",
|
|
125
119
|
});
|
|
126
120
|
return false;
|
|
@@ -171,38 +165,10 @@ export async function modelsCommand(session, host, behavior = {}) {
|
|
|
171
165
|
}
|
|
172
166
|
}
|
|
173
167
|
if (behavior.announce !== false) {
|
|
174
|
-
host.emit({ kind: "notice", text:
|
|
168
|
+
host.emit({ kind: "notice", text: `model · ${chosen}`, tone: "info" });
|
|
175
169
|
}
|
|
176
170
|
return true;
|
|
177
171
|
}
|
|
178
|
-
/** Make the provider selected by flags/environment usable without leaving the TUI. */
|
|
179
|
-
export async function setupCommand(session, host) {
|
|
180
|
-
const blocked = session.provider.blocked();
|
|
181
|
-
if (blocked !== undefined) {
|
|
182
|
-
if (!isCredentialBlocker(session.provider, blocked)) {
|
|
183
|
-
host.emit({ kind: "notice", text: blocked, tone: "error" });
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const accepted = await askForKey(session.provider.keyVar, host, session.palette);
|
|
187
|
-
if (!accepted || session.provider.blocked() !== undefined) {
|
|
188
|
-
host.emit({
|
|
189
|
-
kind: "notice",
|
|
190
|
-
text: `${providerName(session.provider.id)} still needs an API key · /setup`,
|
|
191
|
-
tone: "warn",
|
|
192
|
-
});
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (session.model === "") {
|
|
197
|
-
await modelsCommand(session, host);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
host.emit({
|
|
201
|
-
kind: "notice",
|
|
202
|
-
text: `${session.provider.id} · ${session.model} · ${session.provider.location?.() ?? "cloud"} · ready`,
|
|
203
|
-
tone: "info",
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
172
|
/** The way to put a menu up, or nothing — and the reason, already said. */
|
|
207
173
|
function chooser(host) {
|
|
208
174
|
if (host.choose === undefined) {
|
|
@@ -210,12 +176,6 @@ function chooser(host) {
|
|
|
210
176
|
}
|
|
211
177
|
return host.choose;
|
|
212
178
|
}
|
|
213
|
-
function providerName(id) {
|
|
214
|
-
return id === "" ? "Provider" : `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
|
|
215
|
-
}
|
|
216
|
-
function isCredentialBlocker(provider, blocked) {
|
|
217
|
-
return blocked.startsWith(`${provider.keyVar} `);
|
|
218
|
-
}
|
|
219
179
|
async function saveDefaults(host, patch) {
|
|
220
180
|
if (host.saveSettings === undefined)
|
|
221
181
|
return true;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Stable human-facing names for provider identifiers.
|
|
2
|
+
export function providerLabel(id) {
|
|
3
|
+
switch (id) {
|
|
4
|
+
case "anthropic": return "Anthropic";
|
|
5
|
+
case "openai": return "OpenAI";
|
|
6
|
+
case "openai-codex": return "OpenAI Codex";
|
|
7
|
+
case "ollama": return "Ollama";
|
|
8
|
+
default: return id === "" ? "Provider" : `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -27,7 +27,7 @@ export const anthropic = {
|
|
|
27
27
|
// Sonnet is the default because it is the one that can be left running.
|
|
28
28
|
// Opus via `--model claude-opus-5`, Haiku via `--model claude-haiku-4-5`.
|
|
29
29
|
defaultModel: "claude-sonnet-5",
|
|
30
|
-
keyVar: KEY,
|
|
30
|
+
auth: { kind: "api-key", keyVar: KEY },
|
|
31
31
|
blocked() {
|
|
32
32
|
return apiKey() === undefined ? `${KEY} is not set` : undefined;
|
|
33
33
|
},
|
package/dist/providers/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { anthropic } from "./anthropic.js";
|
|
2
2
|
import { openai } from "./openai.js";
|
|
3
|
+
import { openaiCodex } from "./openai-codex.js";
|
|
3
4
|
import { configureOllama, ollama } from "./ollama.js";
|
|
4
|
-
export const PROVIDERS = [anthropic, openai, ollama];
|
|
5
|
+
export const PROVIDERS = [anthropic, openai, openaiCodex, ollama];
|
|
5
6
|
export function providerNames() {
|
|
6
7
|
return PROVIDERS.map((provider) => provider.id);
|
|
7
8
|
}
|
package/dist/providers/ollama.js
CHANGED
|
@@ -25,7 +25,7 @@ export function ollamaConnection() {
|
|
|
25
25
|
export const ollama = {
|
|
26
26
|
id: "ollama",
|
|
27
27
|
defaultModel: "",
|
|
28
|
-
keyVar: KEY,
|
|
28
|
+
auth: { kind: "api-key", keyVar: KEY },
|
|
29
29
|
// The only provider whose key is conditional: a daemon on this machine is
|
|
30
30
|
// reached over loopback and asks for nothing, so demanding a key there
|
|
31
31
|
// would be an invented requirement.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// ChatGPT-backed Codex Responses, kept separate from the OpenAI API provider.
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { openAICodexAccount } from "../accounts.js";
|
|
4
|
+
import { openAIAuthorization } from "../openai-account.js";
|
|
5
|
+
import { applicationVersion } from "../version.js";
|
|
6
|
+
import { getJson, postSse } from "./http.js";
|
|
7
|
+
import { assembleOpenAI } from "./openai-stream.js";
|
|
8
|
+
import { fromWireResponse, stopNotice, toWireItems, toWireTool, } from "./openai-wire.js";
|
|
9
|
+
const ID = "openai-codex";
|
|
10
|
+
const BASE = "https://chatgpt.com/backend-api/codex";
|
|
11
|
+
// Jecode's product version is unrelated to the Codex protocol gate. OpenAI's
|
|
12
|
+
// own catalogue updater uses this sentinel to request the complete current
|
|
13
|
+
// manifest; Jecode then keeps only entries explicitly visible in that manifest.
|
|
14
|
+
const CATALOG_COMPATIBILITY_VERSION = "99.99.99";
|
|
15
|
+
const SESSION_ID = randomUUID();
|
|
16
|
+
const MAX_CATALOG_ITEMS = 4_000;
|
|
17
|
+
const MAX_MODELS = 1_000;
|
|
18
|
+
const MAX_MODEL_CHARS = 256;
|
|
19
|
+
export const openaiCodex = {
|
|
20
|
+
id: ID,
|
|
21
|
+
defaultModel: "",
|
|
22
|
+
auth: { kind: "oauth", account: ID, label: "ChatGPT" },
|
|
23
|
+
blocked() {
|
|
24
|
+
return openAICodexAccount() === undefined ? "ChatGPT account is not connected" : undefined;
|
|
25
|
+
},
|
|
26
|
+
location: () => "cloud",
|
|
27
|
+
async models(signal, onStatus) {
|
|
28
|
+
return withAuthorization(async (authorization) => {
|
|
29
|
+
const body = await getJson(`${BASE}/models?client_version=${CATALOG_COMPATIBILITY_VERSION}`, headers(authorization, randomUUID()), signal, onStatus);
|
|
30
|
+
return modelIds(body);
|
|
31
|
+
}, signal, onStatus);
|
|
32
|
+
},
|
|
33
|
+
async send(req) {
|
|
34
|
+
return withAuthorization(async (authorization) => {
|
|
35
|
+
const events = await postSse(`${BASE}/responses`, {
|
|
36
|
+
...headers(authorization, randomUUID()),
|
|
37
|
+
"openai-beta": "responses=experimental",
|
|
38
|
+
}, {
|
|
39
|
+
model: req.model,
|
|
40
|
+
store: false,
|
|
41
|
+
stream: true,
|
|
42
|
+
instructions: req.system,
|
|
43
|
+
input: req.messages.flatMap((message) => toWireItems(message, ID)),
|
|
44
|
+
tools: req.tools.map(toWireTool),
|
|
45
|
+
tool_choice: "auto",
|
|
46
|
+
parallel_tool_calls: true,
|
|
47
|
+
reasoning: { effort: req.effort, summary: "auto" },
|
|
48
|
+
text: { verbosity: "low" },
|
|
49
|
+
include: ["reasoning.encrypted_content"],
|
|
50
|
+
prompt_cache_key: SESSION_ID,
|
|
51
|
+
}, req.signal, req.onStatus);
|
|
52
|
+
const data = await assembleOpenAI(events, req.onStream);
|
|
53
|
+
const notice = stopNotice(data);
|
|
54
|
+
if (notice !== undefined)
|
|
55
|
+
req.onStream?.({ kind: "text", text: `\n${notice}` });
|
|
56
|
+
return fromWireResponse(data, ID);
|
|
57
|
+
}, req.signal, req.onStatus);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
async function withAuthorization(operation, signal, onStatus) {
|
|
61
|
+
let authorization = await openAIAuthorization(undefined, signal, onStatus);
|
|
62
|
+
try {
|
|
63
|
+
return await operation(authorization);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (statusOf(error) !== 401)
|
|
67
|
+
throw error;
|
|
68
|
+
authorization = await openAIAuthorization(authorization.accessToken, signal, onStatus);
|
|
69
|
+
return operation(authorization);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function headers(authorization, requestId) {
|
|
73
|
+
const version = applicationVersion();
|
|
74
|
+
return {
|
|
75
|
+
authorization: `Bearer ${authorization.accessToken}`,
|
|
76
|
+
"chatgpt-account-id": authorization.accountId,
|
|
77
|
+
originator: "jecode",
|
|
78
|
+
"user-agent": `jecode/${version} (${process.platform}; ${process.arch})`,
|
|
79
|
+
"session-id": SESSION_ID,
|
|
80
|
+
"x-client-request-id": requestId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function modelIds(value) {
|
|
84
|
+
const source = record(value) && Array.isArray(value["models"]) ? value["models"] : undefined;
|
|
85
|
+
if (source === undefined)
|
|
86
|
+
throw new Error("OpenAI Codex did not return a model list");
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
return source
|
|
89
|
+
.slice(0, MAX_CATALOG_ITEMS)
|
|
90
|
+
.flatMap((entry) => {
|
|
91
|
+
if (!record(entry))
|
|
92
|
+
return [];
|
|
93
|
+
const id = entry["slug"];
|
|
94
|
+
if (typeof id !== "string" ||
|
|
95
|
+
id === "" ||
|
|
96
|
+
id.length > MAX_MODEL_CHARS ||
|
|
97
|
+
entry["visibility"] !== "list" ||
|
|
98
|
+
seen.has(id))
|
|
99
|
+
return [];
|
|
100
|
+
seen.add(id);
|
|
101
|
+
return [{ id, priority: typeof entry["priority"] === "number" ? entry["priority"] : 0 }];
|
|
102
|
+
})
|
|
103
|
+
.sort((left, right) => left.priority - right.priority)
|
|
104
|
+
.slice(0, MAX_MODELS)
|
|
105
|
+
.map((entry) => entry.id);
|
|
106
|
+
}
|
|
107
|
+
function statusOf(error) {
|
|
108
|
+
return typeof error === "object" && error !== null && "status" in error
|
|
109
|
+
? error.status
|
|
110
|
+
: undefined;
|
|
111
|
+
}
|
|
112
|
+
function record(value) {
|
|
113
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
114
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// Reassembling an OpenAI Responses reply from its event stream.
|
|
2
2
|
//
|
|
3
|
-
// Unlike Anthropic,
|
|
4
|
-
// `response.completed
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Unlike Anthropic, a standard Responses stream ends with the whole finished
|
|
4
|
+
// response in `response.completed`. The ChatGPT Codex backend can instead send
|
|
5
|
+
// an empty final `output` after complete `response.output_item.done` events, so
|
|
6
|
+
// those streamed items remain the fallback when the final envelope is empty.
|
|
7
7
|
export async function assembleOpenAI(events, onStream) {
|
|
8
8
|
const items = [];
|
|
9
|
-
let completed;
|
|
10
9
|
let refusal = false;
|
|
11
10
|
for await (const raw of events) {
|
|
12
11
|
const event = raw;
|
|
@@ -29,11 +28,10 @@ export async function assembleOpenAI(events, onStream) {
|
|
|
29
28
|
if (event.item !== undefined)
|
|
30
29
|
items.push(event.item);
|
|
31
30
|
break;
|
|
31
|
+
case "response.done":
|
|
32
32
|
case "response.completed":
|
|
33
33
|
case "response.incomplete":
|
|
34
|
-
|
|
35
|
-
completed = event.response;
|
|
36
|
-
break;
|
|
34
|
+
return reconcileOutput(event.response, items);
|
|
37
35
|
case "response.failed": {
|
|
38
36
|
const response = event.response;
|
|
39
37
|
throw new Error(`openai stream error: ${response?.error?.message ?? "unspecified"}`);
|
|
@@ -44,5 +42,11 @@ export async function assembleOpenAI(events, onStream) {
|
|
|
44
42
|
break;
|
|
45
43
|
}
|
|
46
44
|
}
|
|
47
|
-
return
|
|
45
|
+
return { output: items };
|
|
46
|
+
}
|
|
47
|
+
function reconcileOutput(completed, items) {
|
|
48
|
+
if (completed === undefined)
|
|
49
|
+
return { output: items };
|
|
50
|
+
const finalCount = Array.isArray(completed.output) ? completed.output.length : 0;
|
|
51
|
+
return items.length > finalCount ? { ...completed, output: items } : completed;
|
|
48
52
|
}
|
|
@@ -14,8 +14,8 @@ export function toWireTool(tool) {
|
|
|
14
14
|
parameters: tool.input,
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
export function toWireItems(message) {
|
|
18
|
-
if (message.rawFrom ===
|
|
17
|
+
export function toWireItems(message, providerId = "openai") {
|
|
18
|
+
if (message.rawFrom === providerId && Array.isArray(message.raw)) {
|
|
19
19
|
return message.raw;
|
|
20
20
|
}
|
|
21
21
|
const items = [];
|
|
@@ -50,7 +50,7 @@ export function stopNotice(data) {
|
|
|
50
50
|
? "[truncated: hit max_output_tokens — raise --max-tokens]"
|
|
51
51
|
: `[incomplete: ${reason}]`;
|
|
52
52
|
}
|
|
53
|
-
export function fromWireResponse(data) {
|
|
53
|
+
export function fromWireResponse(data, providerId = "openai") {
|
|
54
54
|
const raw = Array.isArray(data.output) ? data.output : [];
|
|
55
55
|
const content = [];
|
|
56
56
|
for (const entry of raw) {
|
|
@@ -81,7 +81,7 @@ export function fromWireResponse(data) {
|
|
|
81
81
|
const notice = stopNotice(data);
|
|
82
82
|
if (notice !== undefined)
|
|
83
83
|
content.push({ kind: "text", text: notice });
|
|
84
|
-
return { role: "assistant", content, raw, rawFrom:
|
|
84
|
+
return { role: "assistant", content, raw, rawFrom: providerId, usage: normalizeUsage(data) };
|
|
85
85
|
}
|
|
86
86
|
function normalizeUsage(data) {
|
|
87
87
|
const usage = data.usage;
|
package/dist/providers/openai.js
CHANGED
|
@@ -23,7 +23,7 @@ const NON_TEXT_MODE = /(?:^|[-_])(audio|realtime|transcribe|tts)(?:[-_]|$)/;
|
|
|
23
23
|
export const openai = {
|
|
24
24
|
id: "openai",
|
|
25
25
|
defaultModel: "gpt-5",
|
|
26
|
-
keyVar: KEY,
|
|
26
|
+
auth: { kind: "api-key", keyVar: KEY },
|
|
27
27
|
blocked() {
|
|
28
28
|
return apiKey() === undefined ? `${KEY} is not set` : undefined;
|
|
29
29
|
},
|
|
@@ -41,7 +41,7 @@ export const openai = {
|
|
|
41
41
|
const events = await postSse(ENDPOINT, headers(key), {
|
|
42
42
|
model: req.model,
|
|
43
43
|
instructions: req.system,
|
|
44
|
-
input: req.messages.flatMap(toWireItems),
|
|
44
|
+
input: req.messages.flatMap((message) => toWireItems(message)),
|
|
45
45
|
tools: req.tools.map(toWireTool),
|
|
46
46
|
max_output_tokens: req.maxTokens,
|
|
47
47
|
reasoning: { effort: normalizeEffort(req.effort), summary: "auto" },
|
package/dist/providers/sse.js
CHANGED
package/dist/settings-command.js
CHANGED
|
@@ -25,7 +25,7 @@ export async function settingsCommand(session, host) {
|
|
|
25
25
|
if (index === undefined)
|
|
26
26
|
return;
|
|
27
27
|
const action = items[index]?.action;
|
|
28
|
-
if (action === undefined
|
|
28
|
+
if (action === undefined)
|
|
29
29
|
return;
|
|
30
30
|
selected = index;
|
|
31
31
|
switch (action) {
|
|
@@ -75,14 +75,21 @@ function settingsItems(values) {
|
|
|
75
75
|
}]),
|
|
76
76
|
{ action: "model", option: { label: "model", hint: values.model || "choose a model" } },
|
|
77
77
|
{ action: "effort", option: { label: "effort", hint: values.effort } },
|
|
78
|
-
|
|
78
|
+
...(values.maxTokens === undefined
|
|
79
|
+
? []
|
|
80
|
+
: [{
|
|
81
|
+
action: "maxTokens",
|
|
82
|
+
option: { label: "max output tokens", hint: String(values.maxTokens) },
|
|
83
|
+
}]),
|
|
79
84
|
{ action: "maxSteps", option: { label: "max tool steps", hint: String(values.maxSteps) } },
|
|
80
85
|
{
|
|
81
86
|
action: "reducedMotion",
|
|
82
87
|
option: { label: "reduced motion", hint: values.reducedMotion ? "on" : "off" },
|
|
83
88
|
},
|
|
84
|
-
{
|
|
85
|
-
|
|
89
|
+
{
|
|
90
|
+
action: "credentials",
|
|
91
|
+
option: { label: "authentication", hint: "manage API keys and accounts" },
|
|
92
|
+
},
|
|
86
93
|
];
|
|
87
94
|
}
|
|
88
95
|
function settingsValues(session) {
|
|
@@ -91,7 +98,7 @@ function settingsValues(session) {
|
|
|
91
98
|
model: session.model,
|
|
92
99
|
...(session.provider.id === "ollama" ? { ollamaConnection: ollamaConnectionHint() } : {}),
|
|
93
100
|
effort: session.config.effort,
|
|
94
|
-
maxTokens: session.config.maxTokens,
|
|
101
|
+
...(session.provider.id === "openai-codex" ? {} : { maxTokens: session.config.maxTokens }),
|
|
95
102
|
maxSteps: session.config.maxSteps,
|
|
96
103
|
reducedMotion: session.config.reducedMotion,
|
|
97
104
|
};
|
|
@@ -187,7 +194,7 @@ async function persist(host, patch) {
|
|
|
187
194
|
return true;
|
|
188
195
|
}
|
|
189
196
|
catch (error) {
|
|
190
|
-
host.emit({ kind: "notice", text: `could not save settings
|
|
197
|
+
host.emit({ kind: "notice", text: `could not save settings · ${error.message}`, tone: "error" });
|
|
191
198
|
return false;
|
|
192
199
|
}
|
|
193
200
|
}
|
package/dist/tools/shell.js
CHANGED
|
@@ -8,7 +8,7 @@ const MAX_OUTPUT_CHARS = 30_000;
|
|
|
8
8
|
export const runCommand = {
|
|
9
9
|
name: "run_command",
|
|
10
10
|
description: "Run a shell command starting in the workspace root and return its combined stdout " +
|
|
11
|
-
"and stderr. The shell is not a filesystem sandbox, so
|
|
11
|
+
"and stderr. The shell is not a filesystem sandbox, so calls ask for approval by default. Output is " +
|
|
12
12
|
"truncated past 30000 characters.",
|
|
13
13
|
dangerous: true,
|
|
14
14
|
input: {
|
package/dist/transcript.js
CHANGED
|
@@ -25,9 +25,6 @@ export function transcriptMarkdown(blocks) {
|
|
|
25
25
|
case "notice":
|
|
26
26
|
out.push(`> ${block.tone.toUpperCase()}: ${safeMultiline(block.text).replaceAll("\n", "\n> ")}`, "");
|
|
27
27
|
break;
|
|
28
|
-
case "list":
|
|
29
|
-
out.push(...block.items.map((item) => safeInline(item.text)), "");
|
|
30
|
-
break;
|
|
31
28
|
}
|
|
32
29
|
}
|
|
33
30
|
while (out[out.length - 1] === "")
|
|
@@ -4,24 +4,33 @@ import { runTurn } from "../controller.js";
|
|
|
4
4
|
import { updateSettings } from "../settings.js";
|
|
5
5
|
import { saveTranscript } from "../transcript-export.js";
|
|
6
6
|
import { recordUsage } from "../usage.js";
|
|
7
|
-
import { answerAt
|
|
7
|
+
import { answerAt } from "./approve.js";
|
|
8
|
+
import { cancel as cancelOpen } from "./overlay.js";
|
|
8
9
|
import { controllerOptions, turnFailure } from "./session-view.js";
|
|
9
10
|
import { transcribe } from "./turn.js";
|
|
10
11
|
const WAITING = "Waiting";
|
|
11
12
|
export function appWorkflows(options) {
|
|
12
|
-
const { session, state,
|
|
13
|
+
const { session, state, permissions, feedback } = options;
|
|
13
14
|
async function command(text) {
|
|
14
15
|
const activity = options.startActivity("command", `Running ${text.split(/\s+/)[0]}`);
|
|
15
16
|
if (activity === undefined)
|
|
16
17
|
return;
|
|
17
18
|
try {
|
|
18
19
|
const outcome = await handleCommand(text, session, {
|
|
19
|
-
emit: options.
|
|
20
|
+
emit: options.commandNotice,
|
|
20
21
|
signal: activity.control.signal,
|
|
22
|
+
showHelp: () => new Promise((resolve) => {
|
|
23
|
+
state.open = { help: true, settle: resolve };
|
|
24
|
+
options.render();
|
|
25
|
+
}),
|
|
21
26
|
choose: (picker) => new Promise((resolve) => {
|
|
22
27
|
state.open = { picker, settle: resolve };
|
|
23
28
|
options.render();
|
|
24
29
|
}),
|
|
30
|
+
dismiss: () => {
|
|
31
|
+
state.open = state.open === undefined ? undefined : cancelOpen(state.open);
|
|
32
|
+
options.render();
|
|
33
|
+
},
|
|
25
34
|
type: (field) => new Promise((resolve) => {
|
|
26
35
|
state.open = { field, settle: resolve };
|
|
27
36
|
options.render();
|
|
@@ -33,19 +42,13 @@ export function appWorkflows(options) {
|
|
|
33
42
|
reset: () => {
|
|
34
43
|
state.blocks.splice(0);
|
|
35
44
|
state.past.length = 0;
|
|
36
|
-
|
|
45
|
+
permissions.reset();
|
|
37
46
|
state.scroll = 0;
|
|
38
47
|
state.follow = true;
|
|
39
48
|
state.unseen = 0;
|
|
40
49
|
state.lastMaxScroll = 0;
|
|
41
50
|
},
|
|
42
|
-
permissions
|
|
43
|
-
revokePermission: (key) => {
|
|
44
|
-
if (key === undefined)
|
|
45
|
-
allowed.clear();
|
|
46
|
-
else
|
|
47
|
-
allowed.delete(key);
|
|
48
|
-
},
|
|
51
|
+
permissions,
|
|
49
52
|
exportTranscript: () => saveTranscript(options.transcriptRoot, state.blocks),
|
|
50
53
|
saveSettings: async (patch) => {
|
|
51
54
|
await updateSettings(patch);
|
|
@@ -76,11 +79,8 @@ export function appWorkflows(options) {
|
|
|
76
79
|
emit: options.emit,
|
|
77
80
|
render: options.render,
|
|
78
81
|
palette: session.palette,
|
|
79
|
-
approved: (call) =>
|
|
80
|
-
remember: (call) =>
|
|
81
|
-
const scope = scopeFor(call);
|
|
82
|
-
allowed.set(scope.key, scope.summary);
|
|
83
|
-
},
|
|
82
|
+
approved: (call) => permissions.approved(call),
|
|
83
|
+
remember: (call) => permissions.remember(call),
|
|
84
84
|
ask: (prompt, settle) => {
|
|
85
85
|
state.open = { picker: prompt, settle: (index) => settle(answerAt(index)) };
|
|
86
86
|
options.render();
|
|
@@ -92,7 +92,7 @@ export function appWorkflows(options) {
|
|
|
92
92
|
});
|
|
93
93
|
let finishReason;
|
|
94
94
|
try {
|
|
95
|
-
await runTurn(session.history, controllerOptions(session), events, activity.control.signal);
|
|
95
|
+
await runTurn(session.history, controllerOptions(session, permissions.availableTools()), events, activity.control.signal);
|
|
96
96
|
}
|
|
97
97
|
catch (error) {
|
|
98
98
|
const interrupted = activity.control.signal.aborted;
|
package/dist/tui/app.js
CHANGED
|
@@ -18,6 +18,7 @@ import { transcriptRenderer } from "./transcript-view.js";
|
|
|
18
18
|
import { appState } from "./app-state.js";
|
|
19
19
|
import { appInput } from "./app-input.js";
|
|
20
20
|
import { appWorkflows } from "./app-workflows.js";
|
|
21
|
+
import { sessionPermissions } from "../permissions.js";
|
|
21
22
|
const FRAME_MS = 16;
|
|
22
23
|
const SPIN_MS = 80;
|
|
23
24
|
/** How long a lone escape waits to prove it is not the start of a sequence. */
|
|
@@ -29,9 +30,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
29
30
|
const transcript = transcriptRenderer();
|
|
30
31
|
const workspace = await workspaceLabel(session.config.root);
|
|
31
32
|
const state = appState();
|
|
32
|
-
|
|
33
|
-
// a permission granted once, in one conversation, is not a setting.
|
|
34
|
-
const allowed = new Map();
|
|
33
|
+
const permissions = sessionPermissions(session.tools, session.config.autoApprove);
|
|
35
34
|
let closed;
|
|
36
35
|
let frameTimer;
|
|
37
36
|
let spinTimer;
|
|
@@ -100,12 +99,8 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
100
99
|
else
|
|
101
100
|
state.unseen++;
|
|
102
101
|
};
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
if (next === undefined)
|
|
106
|
-
emit(block);
|
|
107
|
-
else
|
|
108
|
-
feedback.show(next);
|
|
102
|
+
const commandNotice = (notice) => {
|
|
103
|
+
feedback.show(commandFeedback(notice));
|
|
109
104
|
};
|
|
110
105
|
const scrollBy = (amount) => {
|
|
111
106
|
state.scroll = Math.max(0, state.scroll + amount);
|
|
@@ -177,10 +172,10 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
177
172
|
session,
|
|
178
173
|
transcriptRoot,
|
|
179
174
|
state,
|
|
180
|
-
|
|
175
|
+
permissions,
|
|
181
176
|
feedback,
|
|
182
177
|
emit,
|
|
183
|
-
|
|
178
|
+
commandNotice,
|
|
184
179
|
render,
|
|
185
180
|
refreshSettings: () => {
|
|
186
181
|
terminal.setReducedMotion(session.config.reducedMotion);
|