@giovannijecha/jecode 0.1.9 → 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 +32 -15
- package/dist/account-lock.js +112 -0
- package/dist/accounts.js +100 -0
- package/dist/cli-info.js +2 -2
- package/dist/commands.js +1 -1
- package/dist/controller.js +3 -0
- package/dist/credential-commands.js +32 -4
- 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/provider-commands.js +18 -30
- 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 +11 -3
- package/dist/tui/app-workflows.js +5 -0
- package/dist/tui/feedback.js +10 -6
- package/dist/tui/session-view.js +10 -4
- package/dist/version.js +14 -0
- package/docs/assets/brand/jeco-256.png +0 -0
- package/package.json +5 -1
|
@@ -2,8 +2,9 @@
|
|
|
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
|
*
|
|
@@ -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 };
|
|
@@ -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;
|
|
@@ -182,12 +176,6 @@ function chooser(host) {
|
|
|
182
176
|
}
|
|
183
177
|
return host.choose;
|
|
184
178
|
}
|
|
185
|
-
function providerName(id) {
|
|
186
|
-
return id === "" ? "Provider" : `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
|
|
187
|
-
}
|
|
188
|
-
function isCredentialBlocker(provider, blocked) {
|
|
189
|
-
return blocked.startsWith(`${provider.keyVar} `);
|
|
190
|
-
}
|
|
191
179
|
async function saveDefaults(host, patch) {
|
|
192
180
|
if (host.saveSettings === undefined)
|
|
193
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
|
@@ -75,13 +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
|
-
{
|
|
89
|
+
{
|
|
90
|
+
action: "credentials",
|
|
91
|
+
option: { label: "authentication", hint: "manage API keys and accounts" },
|
|
92
|
+
},
|
|
85
93
|
];
|
|
86
94
|
}
|
|
87
95
|
function settingsValues(session) {
|
|
@@ -90,7 +98,7 @@ function settingsValues(session) {
|
|
|
90
98
|
model: session.model,
|
|
91
99
|
...(session.provider.id === "ollama" ? { ollamaConnection: ollamaConnectionHint() } : {}),
|
|
92
100
|
effort: session.config.effort,
|
|
93
|
-
maxTokens: session.config.maxTokens,
|
|
101
|
+
...(session.provider.id === "openai-codex" ? {} : { maxTokens: session.config.maxTokens }),
|
|
94
102
|
maxSteps: session.config.maxSteps,
|
|
95
103
|
reducedMotion: session.config.reducedMotion,
|
|
96
104
|
};
|
|
@@ -5,6 +5,7 @@ import { updateSettings } from "../settings.js";
|
|
|
5
5
|
import { saveTranscript } from "../transcript-export.js";
|
|
6
6
|
import { recordUsage } from "../usage.js";
|
|
7
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";
|
|
@@ -26,6 +27,10 @@ export function appWorkflows(options) {
|
|
|
26
27
|
state.open = { picker, settle: resolve };
|
|
27
28
|
options.render();
|
|
28
29
|
}),
|
|
30
|
+
dismiss: () => {
|
|
31
|
+
state.open = state.open === undefined ? undefined : cancelOpen(state.open);
|
|
32
|
+
options.render();
|
|
33
|
+
},
|
|
29
34
|
type: (field) => new Promise((resolve) => {
|
|
30
35
|
state.open = { field, settle: resolve };
|
|
31
36
|
options.render();
|
package/dist/tui/feedback.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Operational feedback belongs in the footer, not in the conversation.
|
|
2
|
+
import { providerLabel } from "../provider-label.js";
|
|
2
3
|
const INFO_MS = 2_200;
|
|
3
4
|
const WARN_MS = 4_200;
|
|
4
5
|
const ERROR_MS = 6_000;
|
|
@@ -50,23 +51,26 @@ export function commandFeedback(block) {
|
|
|
50
51
|
/** Explain why a model turn cannot start, without exposing configuration internals. */
|
|
51
52
|
export function turnBlocker(session) {
|
|
52
53
|
const blocked = session.provider.blocked();
|
|
53
|
-
|
|
54
|
+
const auth = session.provider.auth;
|
|
55
|
+
const expected = auth.kind === "api-key"
|
|
56
|
+
? blocked?.startsWith(`${auth.keyVar} `) === true
|
|
57
|
+
: blocked !== undefined;
|
|
58
|
+
if (blocked !== undefined && !expected) {
|
|
54
59
|
return { text: blocked, tone: "error" };
|
|
55
60
|
}
|
|
56
61
|
if (blocked !== undefined) {
|
|
57
62
|
return {
|
|
58
|
-
text:
|
|
63
|
+
text: auth.kind === "oauth"
|
|
64
|
+
? `${providerLabel(session.provider.id)} needs ${auth.label} sign-in · /settings`
|
|
65
|
+
: `${providerLabel(session.provider.id)} needs an API key · /settings`,
|
|
59
66
|
tone: "warn",
|
|
60
67
|
};
|
|
61
68
|
}
|
|
62
69
|
if (session.model === "") {
|
|
63
70
|
return {
|
|
64
|
-
text: `${
|
|
71
|
+
text: `${providerLabel(session.provider.id)} needs a model · /models`,
|
|
65
72
|
tone: "warn",
|
|
66
73
|
};
|
|
67
74
|
}
|
|
68
75
|
return undefined;
|
|
69
76
|
}
|
|
70
|
-
function providerName(id) {
|
|
71
|
-
return id === "" ? "Provider" : `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
|
|
72
|
-
}
|
package/dist/tui/session-view.js
CHANGED
|
@@ -26,10 +26,16 @@ export function turnFailure(session, error, aborted) {
|
|
|
26
26
|
return { kind: "notice", text: "[interrupted]", tone: "warn" };
|
|
27
27
|
let text = providerFailure(session.provider, error);
|
|
28
28
|
if (/\b401\b/.test(text)) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const auth = session.provider.auth;
|
|
30
|
+
if (auth.kind === "oauth") {
|
|
31
|
+
text += ` · reconnect ${auth.label} in /settings`;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const source = credentialSource(auth.keyVar);
|
|
35
|
+
text += source === "environment"
|
|
36
|
+
? ` · update ${auth.keyVar} in the environment and restart`
|
|
37
|
+
: " · check credentials with /settings";
|
|
38
|
+
}
|
|
33
39
|
}
|
|
34
40
|
return { kind: "notice", text, tone: "error" };
|
|
35
41
|
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The package version is runtime identity, not configuration.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
let cached;
|
|
5
|
+
export function applicationVersion() {
|
|
6
|
+
if (cached !== undefined)
|
|
7
|
+
return cached;
|
|
8
|
+
const manifest = JSON.parse(readFileSync(path.resolve(import.meta.dirname, "..", "package.json"), "utf8"));
|
|
9
|
+
if (typeof manifest.version !== "string" || manifest.version === "") {
|
|
10
|
+
throw new Error("package version is missing");
|
|
11
|
+
}
|
|
12
|
+
cached = manifest.version;
|
|
13
|
+
return cached;
|
|
14
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@giovannijecha/jecode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "An owned coding agent with zero external runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,12 +16,16 @@
|
|
|
16
16
|
"terminal",
|
|
17
17
|
"tui",
|
|
18
18
|
"ollama",
|
|
19
|
+
"openai",
|
|
20
|
+
"chatgpt",
|
|
21
|
+
"oauth",
|
|
19
22
|
"typescript"
|
|
20
23
|
],
|
|
21
24
|
"type": "module",
|
|
22
25
|
"files": [
|
|
23
26
|
"bin/",
|
|
24
27
|
"dist/",
|
|
28
|
+
"docs/assets/brand/jeco-256.png",
|
|
25
29
|
"LICENSE",
|
|
26
30
|
"README.md"
|
|
27
31
|
],
|