@giovannijecha/jecode 0.3.2 → 0.4.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.
@@ -9,59 +9,55 @@ export async function permissionsCommand(session, host) {
9
9
  }
10
10
  let selected = 0;
11
11
  while (true) {
12
- const tools = control.listTools();
13
- const index = await choose(permissionsPicker(tools, session.palette, selected));
12
+ const index = await choose(permissionControlPicker(control, host, selected));
14
13
  if (index === undefined)
15
14
  return;
16
- const tool = tools[index];
15
+ const tool = control.listTools()[index];
17
16
  if (tool === undefined)
18
17
  return;
19
18
  selected = index;
20
- await configureTool(tool, control, choose, session.palette);
19
+ if (tool.locked) {
20
+ lockedNotice(tool.name, host);
21
+ continue;
22
+ }
23
+ if (tool.remembered > 0) {
24
+ await reviewGrants(tool.name, control, choose, session.palette);
25
+ }
21
26
  }
22
27
  }
23
- export function permissionsPicker(tools, pal, index = 0) {
24
- const launchOverride = tools.some((tool) => tool.locked);
28
+ export function permissionsPicker(tools, index = 0, adjust) {
25
29
  return {
26
- title: heading("permissions", launchOverride ? "session only · auto approve at launch" : "session only", pal),
27
- description: "Changes apply now · /new resets them",
28
- options: tools.map((tool) => ({ label: tool.name, hint: toolHint(tool) })),
30
+ title: [],
31
+ options: tools.map((tool) => {
32
+ const description = toolDescription(tool);
33
+ return {
34
+ label: tool.name,
35
+ ...(description === undefined ? {} : { description }),
36
+ value: tool.locked ? `${tool.mode} · locked` : tool.mode,
37
+ adjustable: !tool.locked,
38
+ };
39
+ }),
40
+ visible: tools.length,
41
+ ...(adjust === undefined ? {} : { adjust }),
29
42
  index: Math.min(Math.max(0, index), Math.max(0, tools.length - 1)),
30
43
  };
31
44
  }
32
- async function configureTool(tool, control, choose, pal) {
33
- if (tool.locked) {
34
- await choose({
35
- title: heading(tool.name, "launch override", pal),
36
- description: "Restart without --auto-approve to change this tool",
37
- options: [{ label: "allow", hint: "locked for this process" }],
38
- index: 0,
39
- });
40
- return;
41
- }
42
- const modes = tool.dangerous ? ["ask", "allow", "deny"] : ["allow", "deny"];
43
- const grants = control.listGrants(tool.name);
44
- const index = await choose({
45
- title: heading(tool.name, tool.dangerous ? "dangerous tool" : "read-only tool", pal),
46
- description: tool.dangerous
47
- ? "Session only · ask is the safe default"
48
- : "Session only · deny hides this tool from the model",
49
- options: [
50
- ...modes.map((mode) => ({ label: mode, hint: modeHint(mode, tool.dangerous) })),
51
- ...(grants.length === 0
52
- ? []
53
- : [{ label: "remembered approvals", hint: String(grants.length) }]),
54
- ],
55
- index: Math.max(0, modes.indexOf(tool.mode)),
45
+ function permissionControlPicker(control, host, selected) {
46
+ return permissionsPicker(control.listTools(), selected, (index, step) => {
47
+ const tool = control.listTools()[index];
48
+ if (tool === undefined)
49
+ return permissionControlPicker(control, host, selected);
50
+ if (tool.locked) {
51
+ lockedNotice(tool.name, host);
52
+ return permissionControlPicker(control, host, index);
53
+ }
54
+ const modes = modesFor(tool);
55
+ const at = Math.max(0, modes.indexOf(tool.mode));
56
+ const next = modes[(at + step + modes.length) % modes.length];
57
+ if (next !== undefined)
58
+ control.set(tool.name, next);
59
+ return permissionControlPicker(control, host, index);
56
60
  });
57
- if (index === undefined)
58
- return;
59
- const mode = modes[index];
60
- if (mode !== undefined) {
61
- control.set(tool.name, mode);
62
- return;
63
- }
64
- await reviewGrants(tool.name, control, choose, pal);
65
61
  }
66
62
  async function reviewGrants(tool, control, choose, pal) {
67
63
  let selected = 0;
@@ -75,7 +71,6 @@ async function reviewGrants(tool, control, choose, pal) {
75
71
  ];
76
72
  const index = await choose({
77
73
  title: heading("remembered", tool, pal),
78
- description: "Allowed without asking · this session",
79
74
  options,
80
75
  index: Math.min(selected, options.length - 1),
81
76
  });
@@ -92,16 +87,20 @@ async function reviewGrants(tool, control, choose, pal) {
92
87
  selected = index;
93
88
  }
94
89
  }
95
- function toolHint(tool) {
96
- const kind = tool.dangerous ? "" : " · read only";
97
- const remembered = tool.remembered === 0 ? "" : ` · ${tool.remembered} remembered`;
98
- const locked = tool.locked ? " · launch override" : "";
99
- return `${tool.mode}${kind}${remembered}${locked}`;
90
+ function modesFor(tool) {
91
+ return tool.dangerous ? ["ask", "allow", "deny"] : ["allow", "deny"];
92
+ }
93
+ function toolDescription(tool) {
94
+ const parts = [
95
+ tool.dangerous ? undefined : "read only",
96
+ tool.remembered === 0 ? undefined : `${tool.remembered} remembered`,
97
+ ].filter((part) => part !== undefined);
98
+ return parts.length === 0 ? undefined : parts.join(" · ");
100
99
  }
101
- function modeHint(mode, dangerous) {
102
- if (mode === "deny")
103
- return "hide from the model";
104
- if (mode === "ask")
105
- return "prompt when needed";
106
- return dangerous ? "every call this session" : "offer to the model";
100
+ function lockedNotice(tool, host) {
101
+ host.emit({
102
+ kind: "notice",
103
+ text: `restart without --auto-approve to change ${tool}`,
104
+ tone: "warn",
105
+ });
107
106
  }
@@ -1,227 +1,81 @@
1
- // Provider and model command flows.
2
- import { heading } from "./tui/picker.js";
3
- import { PROVIDERS } from "./providers/index.js";
4
- import { readSettings } from "./settings.js";
5
- import { authenticationNeed, ensureProviderAuthentication, } from "./credential-commands.js";
6
- import { providerFailure } from "./provider-errors.js";
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 { compatibleEffort } from "./effort.js";
9
- /**
10
- * The provider menu.
11
- *
12
- * Every provider is offered, including the ones that cannot run: the reason
13
- * one is unusable — the variable it wants, by name — is worth more on screen
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 false;
21
- const options = PROVIDERS.map((provider) => ({
22
- label: provider.id,
23
- hint: provider.blocked() ?? "ready",
24
- }));
25
- const at = PROVIDERS.findIndex((provider) => provider.id === session.provider.id);
26
- const index = await choose({
27
- title: heading("provider", "where the next turn runs", session.palette),
28
- options,
29
- index: Math.max(0, at),
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
- /** The model menu, built from what the provider says it has right now. */
111
- export async function modelsCommand(session, host, behavior = {}) {
112
- // Before the network, not after: asking a provider for a list nothing can
113
- // be picked from is a request spent on a menu that will never open.
114
- const choose = chooser(host);
115
- if (choose === undefined)
116
- return false;
117
- const provider = session.provider;
118
- // Offer the key rather than only naming what is missing: the user came here
119
- // to pick a model, and "go and export a variable" is not an answer.
120
- const blocked = provider.blocked();
121
- if (blocked !== undefined) {
122
- await ensureProviderAuthentication(provider, session, host);
123
- const still = provider.blocked();
124
- if (still !== undefined) {
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 (behavior.announce !== false) {
194
- host.emit({ kind: "notice", text: `model · ${chosen}`, tone: "info" });
56
+ if (provider.auth.kind === "oauth") {
57
+ await openAIAccountCommand(session, host);
58
+ return;
195
59
  }
196
- return true;
60
+ await apiKeyCommand(provider.auth.keyVar, providerLabel(provider.id), session, host);
197
61
  }
198
- function restoreProvider(session, before) {
199
- session.provider = before.provider;
200
- session.model = before.model;
201
- session.config.providerId = before.providerId;
202
- session.config.model = before.configModel;
203
- session.config.effort = before.effort;
204
- }
205
- async function alignEffort(session, host) {
206
- if (session.provider.efforts === undefined)
207
- return { ok: true };
208
- let supported;
209
- try {
210
- supported = await session.provider.efforts(session.model, host.signal, (status) => host.status?.(status));
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
- catch (error) {
213
- host.emit({
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
- async function saveDefaults(host, patch) {
233
- if (host.saveSettings === undefined)
234
- return true;
235
- try {
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
  }
@@ -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 /settings"
7
- : "Ollama is not reachable · check its connection in /settings";
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
  }
@@ -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 "OpenAI Codex";
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
  }