@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.
Files changed (54) hide show
  1. package/README.md +63 -23
  2. package/dist/batch.js +22 -2
  3. package/dist/cli-info.js +3 -0
  4. package/dist/command-settings.js +19 -0
  5. package/dist/commands.js +11 -14
  6. package/dist/config.js +2 -0
  7. package/dist/controller.js +3 -0
  8. package/dist/conversation.js +208 -0
  9. package/dist/credential-commands.js +63 -80
  10. package/dist/credentials.js +7 -1
  11. package/dist/launch.js +19 -0
  12. package/dist/model-command.js +171 -0
  13. package/dist/permission-command.js +52 -53
  14. package/dist/provider-commands.js +71 -228
  15. package/dist/provider-errors.js +4 -3
  16. package/dist/provider-label.js +2 -2
  17. package/dist/providers/ollama.js +8 -3
  18. package/dist/sessions/codec.js +344 -0
  19. package/dist/sessions/lease.js +76 -0
  20. package/dist/sessions/runtime.js +73 -0
  21. package/dist/sessions/store.js +368 -0
  22. package/dist/settings-command.js +43 -98
  23. package/dist/start.js +67 -4
  24. package/dist/transcript-types.js +6 -0
  25. package/dist/tui/activity.js +3 -0
  26. package/dist/tui/app-input.js +9 -6
  27. package/dist/tui/app-workflows.js +37 -8
  28. package/dist/tui/app.js +72 -20
  29. package/dist/tui/components/composer.js +1 -1
  30. package/dist/tui/components/footer.js +1 -1
  31. package/dist/tui/components/menu.js +27 -15
  32. package/dist/tui/components/messages.js +1 -15
  33. package/dist/tui/components/prompt.js +2 -2
  34. package/dist/tui/components/status.js +5 -2
  35. package/dist/tui/components/tool.js +22 -5
  36. package/dist/tui/editor.js +54 -7
  37. package/dist/tui/feedback.js +5 -2
  38. package/dist/tui/field.js +1 -1
  39. package/dist/tui/help.js +4 -1
  40. package/dist/tui/input.js +4 -0
  41. package/dist/tui/keys.js +14 -3
  42. package/dist/tui/overlay.js +6 -0
  43. package/dist/tui/picker.js +26 -10
  44. package/dist/tui/resume.js +24 -0
  45. package/dist/tui/session-view.js +2 -2
  46. package/dist/tui/turn.js +2 -3
  47. package/dist/tui/view.js +1 -1
  48. package/dist/ui/diff.js +2 -0
  49. package/dist/ui/inline.js +2 -2
  50. package/dist/ui/markdown.js +7 -7
  51. package/dist/ui/render.js +2 -0
  52. package/dist/ui/theme.js +4 -4
  53. package/dist/usage.js +9 -0
  54. package/package.json +1 -1
@@ -1,10 +1,7 @@
1
- // Credential command flows shared by settings and provider selection.
2
- import { heading } from "./tui/picker.js";
1
+ // Masked API-key interaction shared by provider connection flows.
2
+ import { credentialSource, forgetSaved, forgetSession, hasSaved, hold, keep, storeLabel, } from "./credentials.js";
3
3
  import { EMPTY } from "./tui/editor.js";
4
- import { PROVIDERS } from "./providers/index.js";
5
- import { openAIAccountHint } from "./openai-account.js";
6
- import { ensureOpenAIAccount, openAIAccountCommand, } from "./openai-account-command.js";
7
- import { credentialSource, forgetSaved, hasSaved, hold, keep, storeLabel, } from "./credentials.js";
4
+ import { heading } from "./tui/picker.js";
8
5
  /** Ask for a key, then ask separately whether it may be written to disk. */
9
6
  export async function askForKey(name, host, pal) {
10
7
  if (host.type === undefined || host.choose === undefined)
@@ -43,109 +40,95 @@ export async function askForKey(name, host, pal) {
43
40
  return true;
44
41
  }
45
42
  catch (error) {
46
- host.emit({ kind: "notice", text: `could not save API key · ${error.message}`, tone: "error" });
43
+ host.emit({
44
+ kind: "notice",
45
+ text: `could not save API key · ${error.message}`,
46
+ tone: "error",
47
+ });
47
48
  return false;
48
49
  }
49
50
  }
50
51
  return false;
51
52
  }
52
- export async function credentialsCommand(session, host) {
53
+ /** Manage one provider key without ever placing its value on screen. */
54
+ export async function apiKeyCommand(name, label, session, host) {
53
55
  const choose = chooser(host);
54
56
  if (choose === undefined)
55
57
  return;
56
- const index = await choose({
57
- title: heading("authentication", "secrets are never shown", session.palette),
58
- options: PROVIDERS.map((provider) => ({
59
- label: provider.auth.kind === "api-key" ? provider.auth.keyVar : `${provider.auth.label} account`,
60
- hint: provider.auth.kind === "api-key"
61
- ? credentialSource(provider.auth.keyVar) ?? "missing"
62
- : openAIAccountHint(),
63
- })),
64
- index: Math.max(0, PROVIDERS.findIndex((provider) => provider.id === session.provider.id)),
65
- });
66
- if (index === undefined)
67
- return;
68
- const provider = PROVIDERS[index];
69
- if (provider === undefined)
70
- return;
71
- if (provider.auth.kind === "oauth") {
72
- await openAIAccountCommand(session, host);
73
- return;
74
- }
75
- const name = provider.auth.keyVar;
76
58
  const source = credentialSource(name);
77
59
  if (source === "environment") {
78
- host.emit({
79
- kind: "notice",
80
- text: `${name} comes from the environment · restart after changing it`,
81
- tone: "info",
60
+ if (!hasSaved(name)) {
61
+ host.emit({
62
+ kind: "notice",
63
+ text: `${label} API key comes from the environment · restart after changing it`,
64
+ tone: "info",
65
+ });
66
+ return;
67
+ }
68
+ const action = await choose({
69
+ title: heading(`${label} API key`, `${name} · environment`, session.palette),
70
+ description: "The environment value is read-only. A saved copy is currently shadowed.",
71
+ options: [{ label: "forget saved copy", hint: storeLabel(), key: "f" }],
72
+ index: 0,
82
73
  });
83
- if (hasSaved(name))
84
- await offerForget(name, session, host, "a saved copy is currently shadowed");
74
+ if (action === 0)
75
+ await forgetSavedKey(name, host);
85
76
  return;
86
77
  }
87
78
  const actions = [
88
- { label: source === undefined ? "add credential" : "replace credential", key: "r" },
89
- ...(hasSaved(name) ? [{ label: "forget saved copy", hint: storeLabel(), key: "f" }] : []),
79
+ {
80
+ label: source === undefined ? "add API key" : "replace API key",
81
+ hint: source === undefined ? "session or owner-only file" : `currently ${source}`,
82
+ key: "r",
83
+ },
84
+ ...(source === "session"
85
+ ? [{ label: "clear session key", hint: "this process only", key: "c" }]
86
+ : []),
87
+ ...(hasSaved(name)
88
+ ? [{ label: "forget saved copy", hint: storeLabel(), key: "f" }]
89
+ : []),
90
90
  ];
91
- const action = await choose({
92
- title: heading(name, source ?? "missing", session.palette),
91
+ const index = await choose({
92
+ title: heading(`${label} API key`, `${name} · ${source ?? "missing"}`, session.palette),
93
93
  options: actions,
94
94
  index: 0,
95
95
  });
96
- if (action === undefined)
97
- return;
98
- if (actions[action]?.key === "f") {
99
- await forget(name, host);
100
- return;
101
- }
102
- await askForKey(name, host, session.palette);
96
+ const action = index === undefined ? undefined : actions[index]?.key;
97
+ if (action === "r")
98
+ await askForKey(name, host, session.palette);
99
+ else if (action === "c")
100
+ clearSessionKey(name, host);
101
+ else if (action === "f")
102
+ await forgetSavedKey(name, host);
103
103
  }
104
- /** Offer the authentication flow owned by a provider, if that is its blocker. */
105
- export async function ensureProviderAuthentication(provider, session, host) {
106
- const blocked = provider.blocked();
107
- if (blocked === undefined)
108
- return true;
109
- if (provider.auth.kind === "oauth") {
110
- return provider.auth.account === "openai-codex"
111
- ? ensureOpenAIAccount(session, host)
112
- : false;
113
- }
114
- if (!blocked.startsWith(`${provider.auth.keyVar} `)) {
115
- host.emit({ kind: "notice", text: blocked, tone: "error" });
116
- return false;
117
- }
118
- await askForKey(provider.auth.keyVar, host, session.palette);
119
- return provider.blocked() === undefined;
120
- }
121
- export function authenticationNeed(provider) {
122
- return provider.auth.kind === "oauth" ? `${provider.auth.label} sign-in` : "an API key";
123
- }
124
- async function offerForget(name, session, host, hint) {
125
- if (host.choose === undefined)
126
- return;
127
- const index = await host.choose({
128
- title: heading(name, hint, session.palette),
129
- options: [
130
- { label: "keep saved copy", key: "k" },
131
- { label: "forget saved copy", hint: storeLabel(), key: "f" },
132
- ],
133
- index: 0,
104
+ function clearSessionKey(name, host) {
105
+ const removed = forgetSession(name);
106
+ const fallback = credentialSource(name);
107
+ host.emit({
108
+ kind: "notice",
109
+ text: removed
110
+ ? fallback === undefined
111
+ ? "session API key removed"
112
+ : `session API key removed · ${fallback} copy now active`
113
+ : "no session API key",
114
+ tone: removed ? "info" : "warn",
134
115
  });
135
- if (index === 1)
136
- await forget(name, host);
137
116
  }
138
- async function forget(name, host) {
117
+ async function forgetSavedKey(name, host) {
139
118
  try {
140
119
  const removed = await forgetSaved(name);
141
120
  host.emit({
142
121
  kind: "notice",
143
- text: removed ? "API key removed" : "no saved API key",
122
+ text: removed ? "saved API key removed" : "no saved API key",
144
123
  tone: removed ? "info" : "warn",
145
124
  });
146
125
  }
147
126
  catch (error) {
148
- host.emit({ kind: "notice", text: `could not remove API key · ${error.message}`, tone: "error" });
127
+ host.emit({
128
+ kind: "notice",
129
+ text: `could not remove API key · ${error.message}`,
130
+ tone: "error",
131
+ });
149
132
  }
150
133
  }
151
134
  function chooser(host) {
@@ -60,6 +60,10 @@ export function hasSaved(name) {
60
60
  export function hold(name, value) {
61
61
  held.set(name, value);
62
62
  }
63
+ /** Remove only the value held by this process. Saved and environment values remain. */
64
+ export function forgetSession(name) {
65
+ return held.delete(name);
66
+ }
63
67
  /**
64
68
  * Take a key and write it down, returning the path it went to.
65
69
  *
@@ -74,8 +78,10 @@ export async function keep(name, value) {
74
78
  return withStoreLock(file, async () => {
75
79
  const all = { ...readSavedStore(), [name]: value };
76
80
  await persist(file, all);
77
- hold(name, value);
78
81
  saved = all;
82
+ // A newly saved replacement must become active immediately. Otherwise an
83
+ // older session-only value would keep shadowing the value just written.
84
+ held.delete(name);
79
85
  return file;
80
86
  });
81
87
  }
package/dist/launch.js ADDED
@@ -0,0 +1,19 @@
1
+ export function parseLaunch(argv) {
2
+ const first = argv[0];
3
+ if (first !== undefined && !first.startsWith("--") && first !== "-h" && first !== "-v") {
4
+ if (first !== "resume")
5
+ throw new Error(`unknown command ${first}`);
6
+ const rest = argv.slice(1);
7
+ const latest = rest.filter((value) => value === "--latest").length;
8
+ if (latest > 1)
9
+ throw new Error("--latest may be passed only once");
10
+ return {
11
+ kind: "resume",
12
+ latest: latest === 1,
13
+ configArgs: rest.filter((value) => value !== "--latest"),
14
+ };
15
+ }
16
+ if (argv.includes("--latest"))
17
+ throw new Error("--latest requires `jecode resume`");
18
+ return { kind: "new", latest: false, configArgs: [...argv] };
19
+ }
@@ -0,0 +1,171 @@
1
+ // Model-first runtime selection across every provider that can answer now.
2
+ import { saveCommandSettings } from "./command-settings.js";
3
+ import { compatibleEffort } from "./effort.js";
4
+ import { providerFailure } from "./provider-errors.js";
5
+ import { providerLabel } from "./provider-label.js";
6
+ import { PROVIDERS } from "./providers/index.js";
7
+ import { readSettings } from "./settings.js";
8
+ /** Build one searchable catalogue from every provider that is usable now. */
9
+ export async function modelsCommand(session, host, behavior = {}, providers = PROVIDERS) {
10
+ const choose = chooser(host);
11
+ if (choose === undefined)
12
+ return false;
13
+ const availability = providers.map((provider) => ({
14
+ provider,
15
+ blocked: provider.blocked(),
16
+ }));
17
+ const connected = availability
18
+ .filter((entry) => entry.blocked === undefined)
19
+ .map((entry) => entry.provider);
20
+ const disconnected = availability
21
+ .filter((entry) => entry.blocked !== undefined)
22
+ .map((entry) => entry.provider);
23
+ if (connected.length === 0) {
24
+ host.emit({
25
+ kind: "notice",
26
+ text: "no providers are connected · use /providers",
27
+ tone: "warn",
28
+ });
29
+ return false;
30
+ }
31
+ throwIfAborted(host.signal);
32
+ host.status?.("Loading model catalogs");
33
+ let settled;
34
+ try {
35
+ settled = await Promise.allSettled(connected.map(async (provider) => ({
36
+ provider,
37
+ models: await provider.models(host.signal, (status) => host.status?.(`${providerLabel(provider.id)} · ${status}`)),
38
+ })));
39
+ // allSettled deliberately preserves partial provider failures, but an
40
+ // explicit command cancellation is not a catalogue failure.
41
+ throwIfAborted(host.signal);
42
+ }
43
+ finally {
44
+ host.status?.(undefined);
45
+ }
46
+ const choices = [];
47
+ const failed = [];
48
+ for (let index = 0; index < settled.length; index++) {
49
+ const result = settled[index];
50
+ const provider = connected[index];
51
+ if (result === undefined || provider === undefined)
52
+ continue;
53
+ if (result.status === "rejected") {
54
+ failed.push({
55
+ provider,
56
+ error: result.reason instanceof Error ? result.reason : new Error(String(result.reason)),
57
+ });
58
+ continue;
59
+ }
60
+ for (const model of result.value.models)
61
+ choices.push({ provider, model });
62
+ }
63
+ if (choices.length === 0) {
64
+ host.emit({
65
+ kind: "notice",
66
+ text: emptyCatalogMessage(connected, failed),
67
+ tone: failed.length === connected.length ? "error" : "warn",
68
+ });
69
+ return false;
70
+ }
71
+ const description = catalogDescription(disconnected, failed);
72
+ const index = await choose({
73
+ title: [],
74
+ ...(description === undefined ? {} : { description }),
75
+ options: choices.map((choice) => ({
76
+ label: choice.model,
77
+ // Provider identity is part of the choice, not optional help: keep it
78
+ // visible even when the terminal is only at the supported minimum.
79
+ value: providerLabel(choice.provider.id),
80
+ })),
81
+ searchable: true,
82
+ query: "",
83
+ index: Math.max(0, choices.findIndex((choice) => choice.provider.id === session.provider.id && choice.model === session.model)),
84
+ });
85
+ throwIfAborted(host.signal);
86
+ const chosen = index === undefined ? undefined : choices[index];
87
+ if (chosen === undefined)
88
+ return false;
89
+ const beforeEffort = session.config.effort;
90
+ const alignment = await alignEffort(chosen.provider, chosen.model, beforeEffort, host);
91
+ if (!alignment.ok)
92
+ return false;
93
+ if (behavior.save !== false) {
94
+ const saved = readSettings();
95
+ const patch = {
96
+ provider: chosen.provider.id,
97
+ models: { ...saved.models, [chosen.provider.id]: chosen.model },
98
+ ...(alignment.effort === beforeEffort ? {} : { effort: alignment.effort }),
99
+ };
100
+ if (!(await saveCommandSettings(host, patch)))
101
+ return false;
102
+ }
103
+ // Commit the runtime selection only after both provider validation and
104
+ // persistence succeed, so neither the footer nor a later turn can observe a
105
+ // half-applied provider/model pair.
106
+ session.provider = chosen.provider;
107
+ session.model = chosen.model;
108
+ session.config.providerId = chosen.provider.id;
109
+ session.config.model = chosen.model;
110
+ session.config.effort = alignment.effort;
111
+ if (behavior.announce !== false) {
112
+ host.emit({
113
+ kind: "notice",
114
+ text: `model · ${providerLabel(chosen.provider.id)} · ${chosen.model}`,
115
+ tone: "info",
116
+ });
117
+ }
118
+ return true;
119
+ }
120
+ async function alignEffort(provider, model, effort, host) {
121
+ if (provider.efforts === undefined)
122
+ return { ok: true, effort };
123
+ try {
124
+ throwIfAborted(host.signal);
125
+ const supported = await provider.efforts(model, host.signal, (status) => host.status?.(status));
126
+ throwIfAborted(host.signal);
127
+ return { ok: true, effort: compatibleEffort(effort, supported) ?? effort };
128
+ }
129
+ catch (error) {
130
+ throwIfAborted(host.signal);
131
+ host.emit({
132
+ kind: "notice",
133
+ text: providerFailure(provider, error, true),
134
+ tone: "error",
135
+ });
136
+ return { ok: false };
137
+ }
138
+ }
139
+ function catalogDescription(disconnected, failed) {
140
+ const parts = [
141
+ disconnected.length === 0
142
+ ? undefined
143
+ : `Not connected: ${disconnected.map((provider) => providerLabel(provider.id)).join(", ")}`,
144
+ failed.length === 0
145
+ ? undefined
146
+ : `Unavailable: ${failed.map((entry) => providerLabel(entry.provider.id)).join(", ")}`,
147
+ ].filter((part) => part !== undefined);
148
+ return parts.length === 0 ? undefined : `${parts.join(" · ")} · manage in /providers`;
149
+ }
150
+ function emptyCatalogMessage(connected, failed) {
151
+ if (failed.length === 1 && connected.length === 1) {
152
+ const one = failed[0];
153
+ if (one !== undefined)
154
+ return providerFailure(one.provider, one.error, true);
155
+ }
156
+ if (failed.length > 0) {
157
+ return `model catalogs unavailable: ${failed.map((entry) => providerLabel(entry.provider.id)).join(", ")} · /providers`;
158
+ }
159
+ return "connected providers offer no models · check /providers";
160
+ }
161
+ function chooser(host) {
162
+ if (host.choose === undefined) {
163
+ host.emit({ kind: "notice", text: "that command needs the screen", tone: "warn" });
164
+ }
165
+ return host.choose;
166
+ }
167
+ function throwIfAborted(signal) {
168
+ if (signal?.aborted !== true)
169
+ return;
170
+ throw signal.reason instanceof Error ? signal.reason : new Error("interrupted");
171
+ }
@@ -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
  }