@giovannijecha/jecode 0.3.2 → 0.5.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 +63 -23
- package/dist/batch.js +22 -2
- package/dist/cli-info.js +3 -0
- package/dist/command-settings.js +19 -0
- package/dist/commands.js +11 -14
- package/dist/config.js +2 -0
- package/dist/controller.js +3 -0
- package/dist/conversation.js +208 -0
- package/dist/credential-commands.js +63 -80
- package/dist/credentials.js +7 -1
- package/dist/launch.js +19 -0
- package/dist/model-command.js +171 -0
- package/dist/permission-command.js +52 -53
- package/dist/provider-commands.js +71 -228
- package/dist/provider-errors.js +4 -3
- package/dist/provider-label.js +2 -2
- package/dist/providers/ollama.js +8 -3
- package/dist/sessions/codec.js +344 -0
- package/dist/sessions/lease.js +76 -0
- package/dist/sessions/runtime.js +73 -0
- package/dist/sessions/store.js +368 -0
- package/dist/settings-command.js +43 -98
- package/dist/start.js +67 -4
- package/dist/transcript-types.js +6 -0
- package/dist/tui/activity.js +3 -0
- package/dist/tui/app-input.js +9 -6
- package/dist/tui/app-workflows.js +37 -8
- package/dist/tui/app.js +72 -20
- package/dist/tui/components/composer.js +1 -1
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/components/menu.js +27 -15
- package/dist/tui/components/messages.js +1 -15
- package/dist/tui/components/prompt.js +2 -2
- package/dist/tui/components/status.js +5 -2
- package/dist/tui/components/tool.js +22 -5
- package/dist/tui/editor.js +54 -7
- package/dist/tui/feedback.js +5 -2
- package/dist/tui/field.js +1 -1
- package/dist/tui/help.js +4 -1
- package/dist/tui/input.js +4 -0
- package/dist/tui/keys.js +14 -3
- package/dist/tui/overlay.js +6 -0
- package/dist/tui/picker.js +26 -10
- package/dist/tui/resume.js +24 -0
- package/dist/tui/session-view.js +2 -2
- package/dist/tui/turn.js +2 -3
- package/dist/tui/view.js +1 -1
- package/dist/ui/diff.js +2 -0
- package/dist/ui/inline.js +2 -2
- package/dist/ui/markdown.js +7 -7
- package/dist/ui/render.js +2 -0
- package/dist/ui/theme.js +4 -4
- package/dist/usage.js +9 -0
- package/package.json +1 -1
|
@@ -1,227 +1,81 @@
|
|
|
1
|
-
// Provider and
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
// Provider access and connection management. Runtime selection lives in /models.
|
|
2
|
+
import { saveCommandSettings } from "./command-settings.js";
|
|
3
|
+
import { apiKeyCommand } from "./credential-commands.js";
|
|
4
|
+
import { credentialSource } from "./credentials.js";
|
|
5
|
+
import { ollamaConnectionHint, ollamaConnectionSetting, } from "./ollama-settings-command.js";
|
|
6
|
+
import { openAIAccountHint } from "./openai-account.js";
|
|
7
|
+
import { openAIAccountCommand } from "./openai-account-command.js";
|
|
7
8
|
import { providerLabel } from "./provider-label.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* than the row would be worth hidden. A blocked choice opens the same masked
|
|
15
|
-
* credential flow used by settings, and cancellation leaves the old choice.
|
|
16
|
-
*/
|
|
17
|
-
export async function providersCommand(session, host, behavior = {}) {
|
|
9
|
+
import { PROVIDERS } from "./providers/index.js";
|
|
10
|
+
import { ollamaConnection } from "./providers/ollama.js";
|
|
11
|
+
import { heading } from "./tui/picker.js";
|
|
12
|
+
const OLLAMA_KEY = "OLLAMA_API_KEY";
|
|
13
|
+
/** The single control plane for API keys, OAuth accounts, and Ollama endpoints. */
|
|
14
|
+
export async function providersCommand(session, host) {
|
|
18
15
|
const choose = chooser(host);
|
|
19
16
|
if (choose === undefined)
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
if (index === undefined)
|
|
32
|
-
return false;
|
|
33
|
-
const chosen = PROVIDERS[index];
|
|
34
|
-
if (chosen === undefined)
|
|
35
|
-
return false;
|
|
36
|
-
// Picking the provider already in use is not a no-op when it cannot run:
|
|
37
|
-
// it is how the user asks to fix the reason it cannot.
|
|
38
|
-
if (chosen.id === session.provider.id) {
|
|
39
|
-
if (!(await ensureProviderAuthentication(chosen, session, host)))
|
|
40
|
-
return false;
|
|
41
|
-
return session.model === ""
|
|
42
|
-
? modelsCommand(session, host, { announce: false, save: behavior.save })
|
|
43
|
-
: true;
|
|
44
|
-
}
|
|
45
|
-
// A provider that cannot run is worth one offer to fix it, here, rather
|
|
46
|
-
// than a note telling the user to leave and export something.
|
|
47
|
-
const blocked = chosen.blocked();
|
|
48
|
-
if (blocked !== undefined) {
|
|
49
|
-
await ensureProviderAuthentication(chosen, session, host);
|
|
50
|
-
const still = chosen.blocked();
|
|
51
|
-
if (still !== undefined) {
|
|
52
|
-
host.emit({
|
|
53
|
-
kind: "notice",
|
|
54
|
-
text: `${providerLabel(chosen.id)} still needs ${authenticationNeed(chosen)} · provider unchanged`,
|
|
55
|
-
tone: "warn",
|
|
56
|
-
});
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const before = {
|
|
61
|
-
provider: session.provider,
|
|
62
|
-
model: session.model,
|
|
63
|
-
providerId: session.config.providerId,
|
|
64
|
-
configModel: session.config.model,
|
|
65
|
-
effort: session.config.effort,
|
|
66
|
-
};
|
|
67
|
-
session.provider = chosen;
|
|
68
|
-
// The model belonged to the old provider. Carrying it across would send
|
|
69
|
-
// `claude-sonnet-5` to OpenAI and call the 404 a bug in the provider.
|
|
70
|
-
session.model = readSettings().models?.[chosen.id] ?? chosen.defaultModel;
|
|
71
|
-
session.config.providerId = chosen.id;
|
|
72
|
-
session.config.model = session.model;
|
|
73
|
-
if (session.model === "") {
|
|
74
|
-
if (!(await modelsCommand(session, host, { announce: false, save: false }))) {
|
|
75
|
-
restoreProvider(session, before);
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
const alignment = await alignEffort(session, host);
|
|
81
|
-
if (!alignment.ok) {
|
|
82
|
-
restoreProvider(session, before);
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (behavior.save !== false) {
|
|
87
|
-
const saved = readSettings();
|
|
88
|
-
const models = { ...saved.models };
|
|
89
|
-
if (session.model !== "")
|
|
90
|
-
models[chosen.id] = session.model;
|
|
91
|
-
const patch = {
|
|
92
|
-
provider: chosen.id,
|
|
93
|
-
models,
|
|
94
|
-
...(session.config.effort === before.effort ? {} : { effort: session.config.effort }),
|
|
95
|
-
};
|
|
96
|
-
if (!(await saveDefaults(host, patch))) {
|
|
97
|
-
restoreProvider(session, before);
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (behavior.announce !== false) {
|
|
102
|
-
host.emit({
|
|
103
|
-
kind: "notice",
|
|
104
|
-
text: session.model === "" ? `provider · ${chosen.id} · pick a model` : `provider · ${chosen.id}`,
|
|
105
|
-
tone: "info",
|
|
17
|
+
return;
|
|
18
|
+
let selected = Math.max(0, PROVIDERS.findIndex((provider) => provider.id === session.provider.id));
|
|
19
|
+
while (true) {
|
|
20
|
+
const index = await choose({
|
|
21
|
+
title: [],
|
|
22
|
+
options: PROVIDERS.map((provider) => ({
|
|
23
|
+
label: providerLabel(provider.id),
|
|
24
|
+
value: providerAccessHint(provider),
|
|
25
|
+
})),
|
|
26
|
+
index: selected,
|
|
106
27
|
});
|
|
28
|
+
if (index === undefined)
|
|
29
|
+
return;
|
|
30
|
+
const provider = PROVIDERS[index];
|
|
31
|
+
if (provider === undefined)
|
|
32
|
+
return;
|
|
33
|
+
selected = index;
|
|
34
|
+
await manageProvider(provider, session, host);
|
|
35
|
+
// Esc closes only the nested provider flow. Ctrl+C also settles that
|
|
36
|
+
// picker, but aborts the command signal and must not reopen the parent.
|
|
37
|
+
throwIfAborted(host.signal);
|
|
107
38
|
}
|
|
108
|
-
return true;
|
|
109
39
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
host.emit({
|
|
126
|
-
kind: "notice",
|
|
127
|
-
text: `${providerLabel(provider.id)} still needs ${authenticationNeed(provider)}`,
|
|
128
|
-
tone: "warn",
|
|
129
|
-
});
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
host.status?.(`Asking ${provider.id}`);
|
|
134
|
-
let ids;
|
|
135
|
-
try {
|
|
136
|
-
ids = await provider.models(host.signal, (status) => host.status?.(status));
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
host.emit({
|
|
140
|
-
kind: "notice",
|
|
141
|
-
text: providerFailure(provider, error, true),
|
|
142
|
-
tone: "error",
|
|
143
|
-
});
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
finally {
|
|
147
|
-
host.status?.(undefined);
|
|
148
|
-
}
|
|
149
|
-
if (ids.length === 0) {
|
|
150
|
-
host.emit({ kind: "notice", text: `${provider.id} offers no models`, tone: "warn" });
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
const index = await choose({
|
|
154
|
-
title: heading("model", provider.id, session.palette),
|
|
155
|
-
options: ids.map((id) => ({ label: id })),
|
|
156
|
-
searchable: true,
|
|
157
|
-
query: "",
|
|
158
|
-
index: Math.max(0, ids.indexOf(session.model)),
|
|
159
|
-
});
|
|
160
|
-
if (index === undefined)
|
|
161
|
-
return false;
|
|
162
|
-
const chosen = ids[index];
|
|
163
|
-
if (chosen === undefined)
|
|
164
|
-
return false;
|
|
165
|
-
const before = {
|
|
166
|
-
model: session.model,
|
|
167
|
-
configModel: session.config.model,
|
|
168
|
-
effort: session.config.effort,
|
|
169
|
-
};
|
|
170
|
-
session.model = chosen;
|
|
171
|
-
session.config.model = chosen;
|
|
172
|
-
const alignment = await alignEffort(session, host);
|
|
173
|
-
if (!alignment.ok) {
|
|
174
|
-
session.model = before.model;
|
|
175
|
-
session.config.model = before.configModel;
|
|
176
|
-
session.config.effort = before.effort;
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
if (behavior.save !== false) {
|
|
180
|
-
const saved = readSettings();
|
|
181
|
-
const models = { ...saved.models, [provider.id]: chosen };
|
|
182
|
-
const patch = {
|
|
183
|
-
models,
|
|
184
|
-
...(session.config.effort === before.effort ? {} : { effort: session.config.effort }),
|
|
185
|
-
};
|
|
186
|
-
if (!(await saveDefaults(host, patch))) {
|
|
187
|
-
session.model = before.model;
|
|
188
|
-
session.config.model = before.configModel;
|
|
189
|
-
session.config.effort = before.effort;
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
40
|
+
export function providerAccessHint(provider) {
|
|
41
|
+
if (provider.id === "ollama") {
|
|
42
|
+
const connection = ollamaConnection();
|
|
43
|
+
if (connection.loopback)
|
|
44
|
+
return `${ollamaConnectionHint()} · no key needed`;
|
|
45
|
+
return `${ollamaConnectionHint()} · API key ${credentialSource(OLLAMA_KEY) ?? "missing"}`;
|
|
46
|
+
}
|
|
47
|
+
if (provider.auth.kind === "oauth")
|
|
48
|
+
return openAIAccountHint();
|
|
49
|
+
return `API key · ${credentialSource(provider.auth.keyVar) ?? "missing"}`;
|
|
50
|
+
}
|
|
51
|
+
async function manageProvider(provider, session, host) {
|
|
52
|
+
if (provider.id === "ollama") {
|
|
53
|
+
await ollamaProviderCommand(session, host);
|
|
54
|
+
return;
|
|
192
55
|
}
|
|
193
|
-
if (
|
|
194
|
-
|
|
56
|
+
if (provider.auth.kind === "oauth") {
|
|
57
|
+
await openAIAccountCommand(session, host);
|
|
58
|
+
return;
|
|
195
59
|
}
|
|
196
|
-
|
|
60
|
+
await apiKeyCommand(provider.auth.keyVar, providerLabel(provider.id), session, host);
|
|
197
61
|
}
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
62
|
+
async function ollamaProviderCommand(session, host) {
|
|
63
|
+
if (host.choose === undefined)
|
|
64
|
+
return;
|
|
65
|
+
const index = await host.choose({
|
|
66
|
+
title: heading("Ollama", "access and connection", session.palette),
|
|
67
|
+
options: [
|
|
68
|
+
{ label: "connection", hint: ollamaConnectionHint() },
|
|
69
|
+
{ label: "API key", hint: credentialSource(OLLAMA_KEY) ?? "missing" },
|
|
70
|
+
],
|
|
71
|
+
index: 0,
|
|
72
|
+
});
|
|
73
|
+
if (index === 0) {
|
|
74
|
+
await ollamaConnectionSetting(session, host, (patch) => saveCommandSettings(host, patch));
|
|
211
75
|
}
|
|
212
|
-
|
|
213
|
-
host
|
|
214
|
-
kind: "notice",
|
|
215
|
-
text: providerFailure(session.provider, error, true),
|
|
216
|
-
tone: "error",
|
|
217
|
-
});
|
|
218
|
-
return { ok: false };
|
|
76
|
+
else if (index === 1) {
|
|
77
|
+
await apiKeyCommand(OLLAMA_KEY, "Ollama", session, host);
|
|
219
78
|
}
|
|
220
|
-
const adjusted = compatibleEffort(session.config.effort, supported);
|
|
221
|
-
if (adjusted === undefined || adjusted === session.config.effort)
|
|
222
|
-
return { ok: true };
|
|
223
|
-
session.config.effort = adjusted;
|
|
224
|
-
return { ok: true, adjusted };
|
|
225
79
|
}
|
|
226
80
|
function chooser(host) {
|
|
227
81
|
if (host.choose === undefined) {
|
|
@@ -229,19 +83,8 @@ function chooser(host) {
|
|
|
229
83
|
}
|
|
230
84
|
return host.choose;
|
|
231
85
|
}
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
await host.saveSettings(patch);
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
catch (error) {
|
|
240
|
-
host.emit({
|
|
241
|
-
kind: "notice",
|
|
242
|
-
text: `could not save settings · ${error.message}`,
|
|
243
|
-
tone: "error",
|
|
244
|
-
});
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
86
|
+
function throwIfAborted(signal) {
|
|
87
|
+
if (signal?.aborted !== true)
|
|
88
|
+
return;
|
|
89
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("interrupted");
|
|
247
90
|
}
|
package/dist/provider-errors.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Actionable provider failures for user-facing command and turn surfaces.
|
|
2
|
+
import { providerLabel } from "./provider-label.js";
|
|
2
3
|
const CONNECTION_FAILURE = /network error calling|timed out waiting for response headers|response body was idle/i;
|
|
3
4
|
export function providerFailure(provider, error, labelProvider = false) {
|
|
4
5
|
if (provider.id === "ollama" && CONNECTION_FAILURE.test(error.message)) {
|
|
5
6
|
return provider.location?.() === "local"
|
|
6
|
-
? "Ollama is not reachable on this computer · start Ollama or choose cloud in /
|
|
7
|
-
: "Ollama is not reachable · check its connection in /
|
|
7
|
+
? "Ollama is not reachable on this computer · start Ollama or choose cloud in /providers"
|
|
8
|
+
: "Ollama is not reachable · check its connection in /providers";
|
|
8
9
|
}
|
|
9
|
-
return labelProvider ? `${provider.id}: ${error.message}` : error.message;
|
|
10
|
+
return labelProvider ? `${providerLabel(provider.id)}: ${error.message}` : error.message;
|
|
10
11
|
}
|
package/dist/provider-label.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
export function providerLabel(id) {
|
|
3
3
|
switch (id) {
|
|
4
4
|
case "anthropic": return "Anthropic";
|
|
5
|
-
case "openai": return "OpenAI";
|
|
6
|
-
case "openai-codex": return "
|
|
5
|
+
case "openai": return "OpenAI API";
|
|
6
|
+
case "openai-codex": return "ChatGPT";
|
|
7
7
|
case "ollama": return "Ollama";
|
|
8
8
|
default: return id === "" ? "Provider" : `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
|
|
9
9
|
}
|
package/dist/providers/ollama.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// daemon. There is no default model — the catalogue is whatever the host has
|
|
6
6
|
// pulled or the subscription grants — so the model has to be named with
|
|
7
7
|
// --model.
|
|
8
|
+
import { requireSupportedEffort } from "../effort.js";
|
|
8
9
|
import { postSse } from "./http.js";
|
|
9
10
|
import { listModels } from "./catalog.js";
|
|
10
11
|
import { keyFor } from "../credentials.js";
|
|
@@ -12,6 +13,8 @@ import { assembleOllama } from "./ollama-stream.js";
|
|
|
12
13
|
import { OLLAMA_CLOUD_HOST, OLLAMA_LOCAL_HOST, ollamaConnectionKind, parseOllamaEndpoint, } from "./ollama-endpoint.js";
|
|
13
14
|
import { fromWireReply, stopNotice, toWireMessages, toWireTool } from "./ollama-wire.js";
|
|
14
15
|
const KEY = "OLLAMA_API_KEY";
|
|
16
|
+
// Ollama also accepts `none`; Jecode's product-wide reasoning floor is `low`.
|
|
17
|
+
const OLLAMA_EFFORTS = ["low", "medium", "high"];
|
|
15
18
|
let configuredHost;
|
|
16
19
|
/** Set the endpoint selected for this process. Undefined restores key-aware inference. */
|
|
17
20
|
export function configureOllama(host) {
|
|
@@ -46,7 +49,7 @@ export const ollama = {
|
|
|
46
49
|
return listModels(`${at.baseUrl}/v1/models`, headers(at), signal, onStatus);
|
|
47
50
|
},
|
|
48
51
|
async efforts() {
|
|
49
|
-
return
|
|
52
|
+
return OLLAMA_EFFORTS;
|
|
50
53
|
},
|
|
51
54
|
location: () => {
|
|
52
55
|
try {
|
|
@@ -58,13 +61,15 @@ export const ollama = {
|
|
|
58
61
|
},
|
|
59
62
|
async send(req) {
|
|
60
63
|
const at = endpoint();
|
|
61
|
-
|
|
62
|
-
//
|
|
64
|
+
const effort = requireSupportedEffort(req.model, req.effort, OLLAMA_EFFORTS);
|
|
65
|
+
// The OpenAI-compatible endpoint accepts this vocabulary for thinking
|
|
66
|
+
// models. Invalid levels are rejected locally instead of being rewritten.
|
|
63
67
|
const events = await postSse(`${at.baseUrl}/v1/chat/completions`, headers(at), {
|
|
64
68
|
model: req.model,
|
|
65
69
|
messages: toWireMessages(req.system, req.messages),
|
|
66
70
|
tools: req.tools.map(toWireTool),
|
|
67
71
|
max_tokens: req.maxTokens,
|
|
72
|
+
reasoning_effort: effort,
|
|
68
73
|
stream: true,
|
|
69
74
|
}, req.signal, req.onStatus);
|
|
70
75
|
const reply = await assembleOllama(events, req.onStream);
|