@giovannijecha/jecode 0.8.5 → 0.8.6

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 (64) hide show
  1. package/README.md +22 -5
  2. package/dist/batch-view.js +27 -2
  3. package/dist/batch.js +2 -0
  4. package/dist/cli-info.js +0 -1
  5. package/dist/config.js +14 -6
  6. package/dist/credential-commands.js +3 -3
  7. package/dist/openai-account-command.js +14 -12
  8. package/dist/openai-account.js +7 -5
  9. package/dist/openai-oauth-callback.js +13 -4
  10. package/dist/openai-oauth-tokens.js +9 -7
  11. package/dist/openai-oauth.js +8 -6
  12. package/dist/permission-command.js +1 -1
  13. package/dist/provider-commands.js +1 -33
  14. package/dist/provider-errors.js +1 -10
  15. package/dist/provider-label.js +3 -10
  16. package/dist/providers/anthropic.js +0 -1
  17. package/dist/providers/index.js +1 -5
  18. package/dist/providers/ollama-context.js +42 -0
  19. package/dist/providers/ollama-endpoint.js +7 -34
  20. package/dist/providers/ollama.js +13 -147
  21. package/dist/providers/openai-codex.js +4 -4
  22. package/dist/providers/openai.js +0 -1
  23. package/dist/sessions/bucket.js +55 -0
  24. package/dist/sessions/catalog-io.js +162 -0
  25. package/dist/sessions/catalog.js +3 -1
  26. package/dist/sessions/codec-messages.js +122 -0
  27. package/dist/sessions/codec-transcript.js +93 -0
  28. package/dist/sessions/codec-values.js +52 -0
  29. package/dist/sessions/codec.js +4 -257
  30. package/dist/sessions/files.js +158 -0
  31. package/dist/sessions/load.js +90 -0
  32. package/dist/sessions/snapshot.js +33 -0
  33. package/dist/sessions/store.js +42 -461
  34. package/dist/settings-command.js +10 -5
  35. package/dist/settings.js +16 -15
  36. package/dist/start.js +1 -2
  37. package/dist/tools/file-read.js +192 -0
  38. package/dist/tools/file-summary.js +9 -0
  39. package/dist/tools/{fs.js → file-write.js} +5 -193
  40. package/dist/tools/glob.js +107 -0
  41. package/dist/tools/index.js +2 -1
  42. package/dist/tools/search.js +1 -105
  43. package/dist/tui/app-workflows.js +8 -361
  44. package/dist/tui/approve.js +5 -3
  45. package/dist/tui/blocks.js +8 -7
  46. package/dist/tui/command-workflow.js +106 -0
  47. package/dist/tui/components/command-menu.js +7 -10
  48. package/dist/tui/components/menu.js +74 -43
  49. package/dist/tui/components/messages.js +16 -9
  50. package/dist/tui/components/tool-evidence.js +107 -0
  51. package/dist/tui/components/tool-motion.js +32 -0
  52. package/dist/tui/components/tool.js +48 -202
  53. package/dist/tui/help.js +1 -1
  54. package/dist/tui/picker-layout.js +40 -0
  55. package/dist/tui/picker.js +7 -71
  56. package/dist/tui/tool-details.js +135 -0
  57. package/dist/tui/transcript-grammar.js +8 -1
  58. package/dist/tui/transcript-view.js +26 -112
  59. package/dist/tui/turn-workflow.js +264 -0
  60. package/dist/tui/turn.js +7 -140
  61. package/dist/tui/workflow-types.js +2 -0
  62. package/package.json +12 -12
  63. package/dist/ollama-settings-command.js +0 -74
  64. package/dist/tui/motion.js +0 -32
@@ -0,0 +1,264 @@
1
+ // One model turn, including steering, compaction, durable settlement, and recovery.
2
+ import { runTurn } from "../controller.js";
3
+ import { resolveContextPolicy } from "../context/capacity.js";
4
+ import { automaticCompactionKey } from "../context/automatic.js";
5
+ import { compactContext } from "../context/compactor.js";
6
+ import { isContextOverflow } from "../context/policy.js";
7
+ import { steeringInbox } from "../steering.js";
8
+ import { recordAuxiliaryUsage, recordRequestInput, recordUsage } from "../usage.js";
9
+ import { toolSpecs } from "../tools/index.js";
10
+ import { requestIdentityForSession } from "../request-identity.js";
11
+ import { transition } from "./activity.js";
12
+ import { answerAt } from "./approve.js";
13
+ import * as edit from "./editor.js";
14
+ import { controllerOptions, turnFailure } from "./session-view.js";
15
+ import { transcribe } from "./turn.js";
16
+ const WAITING = "Waiting";
17
+ export function turnWorkflow(options, automaticCompaction) {
18
+ const { session, state, permissions, feedback } = options;
19
+ let activeSteering;
20
+ async function turn(text) {
21
+ const activity = options.startActivity("turn", WAITING);
22
+ if (activity === undefined)
23
+ return;
24
+ const inbox = steeringInbox((pending, accepting) => {
25
+ if (activeSteering !== inbox)
26
+ return;
27
+ state.steering = accepting ? pending : undefined;
28
+ options.render();
29
+ });
30
+ activeSteering = inbox;
31
+ state.steering = 0;
32
+ const status = (label) => transition(activity, label);
33
+ const parentId = session.conversation.activeNodeId;
34
+ const history = session.conversation.history;
35
+ const modelHistory = session.conversation.contextHistory;
36
+ const historyStart = history.length;
37
+ const blockStart = state.blocks.length;
38
+ const createdAt = new Date().toISOString();
39
+ const prospectiveNodeId = session.conversation.nodes.length + 1;
40
+ let nodeId;
41
+ let context;
42
+ const unpersistedSteering = [];
43
+ const turnTools = permissions.availableTools();
44
+ const specs = toolSpecs(turnTools);
45
+ let firstPolicy = true;
46
+ const policy = () => {
47
+ let visible = firstPolicy;
48
+ firstPolicy = false;
49
+ if (visible) {
50
+ status("Checking context");
51
+ options.render();
52
+ }
53
+ return resolveContextPolicy({
54
+ provider: session.provider,
55
+ model: session.model,
56
+ compactionPercent: session.config.compactionPercent,
57
+ signal: activity.control.signal,
58
+ onStatus: (said) => {
59
+ visible = true;
60
+ status(said);
61
+ options.render();
62
+ },
63
+ }).finally(() => {
64
+ if (visible) {
65
+ status(WAITING);
66
+ options.render();
67
+ }
68
+ });
69
+ };
70
+ options.emit({ kind: "user", text });
71
+ const user = { role: "user", content: [{ kind: "text", text }] };
72
+ history.push(user);
73
+ modelHistory.push(structuredClone(user));
74
+ const events = transcribe({
75
+ emit: options.emit,
76
+ render: options.render,
77
+ palette: session.palette,
78
+ approved: (call) => permissions.approved(call),
79
+ remember: (call) => permissions.remember(call),
80
+ ask: (prompt, settle) => {
81
+ state.open = { picker: prompt, settle: (index) => settle(answerAt(index)) };
82
+ options.render();
83
+ },
84
+ status: (text) => {
85
+ status(text);
86
+ },
87
+ usage: (usage) => recordUsage(session.usage, usage),
88
+ requestInput: (inputTokens) => recordRequestInput(session.usage, inputTokens),
89
+ });
90
+ const persist = async (checkpoint, settlement, failure) => {
91
+ const next = session.conversation.commit({
92
+ ...(nodeId === undefined ? {} : { nodeId }),
93
+ parentId,
94
+ createdAt,
95
+ identity: {
96
+ providerId: session.provider.id,
97
+ model: session.model,
98
+ effort: session.config.effort,
99
+ },
100
+ messages: checkpoint.slice(historyStart),
101
+ blocks: state.blocks.slice(blockStart),
102
+ ...(context === undefined ? {} : { context }),
103
+ ...(failure === undefined ? {} : { failure }),
104
+ }, settlement);
105
+ await session.persistence?.checkpoint(next);
106
+ session.conversation = next;
107
+ nodeId = next.activeNodeId;
108
+ state.committedNodeId = next.activeNodeId;
109
+ unpersistedSteering.length = 0;
110
+ };
111
+ const compact = async (checkpoint, projected, request) => {
112
+ if (request.reason === "overflow" &&
113
+ (request.error === undefined || !isContextOverflow(request.error))) {
114
+ return undefined;
115
+ }
116
+ const force = request.reason === "overflow" || request.projectionSaturated;
117
+ const key = automaticCompactionKey(session.provider.id, session.model, nodeId ?? prospectiveNodeId, checkpoint.length);
118
+ const attempt = { key, reason: request.reason };
119
+ if (!automaticCompaction.allows(attempt))
120
+ return undefined;
121
+ let attempted = false;
122
+ const result = await compactContext({
123
+ provider: session.provider,
124
+ model: session.model,
125
+ effort: session.config.effort,
126
+ context: projected,
127
+ turn: checkpoint.slice(historyStart),
128
+ nodeId: nodeId ?? prospectiveNodeId,
129
+ coveredMessages: context?.messageCount ?? 0,
130
+ lastInputTokens: Math.max(session.usage.lastInputTokens, request.inputTokens),
131
+ estimatedInputTokens: request.inputTokens,
132
+ signal: activity.control.signal,
133
+ force,
134
+ policy: request.policy,
135
+ requestEnvelope: {
136
+ system: session.system,
137
+ tools: specs,
138
+ maxOutputTokens: session.config.maxTokens,
139
+ },
140
+ requestIdentity: requestIdentityForSession(session),
141
+ onBegin: () => {
142
+ attempted = true;
143
+ status("Compacting");
144
+ options.render();
145
+ },
146
+ onEnd: () => {
147
+ status(WAITING);
148
+ options.render();
149
+ },
150
+ });
151
+ if (result === undefined) {
152
+ if (attempted && !activity.control.signal.aborted)
153
+ automaticCompaction.failed(attempt);
154
+ return undefined;
155
+ }
156
+ automaticCompaction.succeeded(attempt);
157
+ context = result.anchor;
158
+ if (result.usage !== undefined)
159
+ recordAuxiliaryUsage(session.usage, result.usage);
160
+ return result.messages;
161
+ };
162
+ events.onContext = compact;
163
+ events.onSteering = (guidance) => {
164
+ unpersistedSteering.push(guidance);
165
+ options.emit({ kind: "user", text: guidance });
166
+ options.render();
167
+ };
168
+ events.onCheckpoint = async (checkpoint, settlement, projected) => {
169
+ await persist(checkpoint, settlement);
170
+ const compacted = await compact(checkpoint, projected, {
171
+ reason: "budget",
172
+ policy: await policy(),
173
+ inputTokens: session.usage.lastInputTokens,
174
+ projectionSaturated: false,
175
+ });
176
+ if (compacted !== undefined)
177
+ await persist(checkpoint, settlement);
178
+ return compacted;
179
+ };
180
+ let finishReason;
181
+ let failed;
182
+ try {
183
+ await runTurn(history, controllerOptions(session, policy, turnTools, inbox), events, activity.control.signal, modelHistory);
184
+ }
185
+ catch (error) {
186
+ const interrupted = activity.control.signal.aborted;
187
+ const completed = nodeId !== undefined && session.conversation.activeNodeId === nodeId &&
188
+ session.conversation.activeNode?.settlement === "completed";
189
+ if (completed) {
190
+ const notice = turnFailure(session, error, interrupted);
191
+ feedback.show({ text: notice.text, tone: notice.tone, timeoutMs: 6_000 });
192
+ }
193
+ else {
194
+ finishReason = interrupted ? "interrupted" : "failed";
195
+ failed = { error: error, interrupted };
196
+ }
197
+ }
198
+ finally {
199
+ let pendingSteering = inbox.close();
200
+ if (activeSteering === inbox)
201
+ activeSteering = undefined;
202
+ state.steering = undefined;
203
+ try {
204
+ events.finish(finishReason);
205
+ if (failed !== undefined) {
206
+ const notice = turnFailure(session, failed.error, failed.interrupted);
207
+ const settlement = failed.interrupted ? "interrupted" : "failed";
208
+ const failure = {
209
+ text: notice.text,
210
+ tone: failed.interrupted ? "warn" : "error",
211
+ };
212
+ options.emit(notice);
213
+ try {
214
+ await persist(closeFailedTurn(history, settlement), settlement, failure);
215
+ }
216
+ catch (error) {
217
+ // A failed persistence boundary cannot remain visible as if it had
218
+ // been saved. Revert to the last durable path and return the input
219
+ // to the composer so the user can retry without losing it.
220
+ options.replaceTranscript();
221
+ state.editor = edit.of([
222
+ ...(nodeId === undefined ? [text] : []),
223
+ ...unpersistedSteering,
224
+ ...pendingSteering,
225
+ state.editor.text,
226
+ ].filter((part) => part !== "").join("\n\n"));
227
+ state.completing = undefined;
228
+ pendingSteering = [];
229
+ feedback.show({
230
+ text: error.message,
231
+ tone: "error",
232
+ timeoutMs: 6_000,
233
+ });
234
+ }
235
+ }
236
+ }
237
+ finally {
238
+ restorePendingSteering(state, pendingSteering);
239
+ options.finishActivity(activity);
240
+ }
241
+ }
242
+ }
243
+ function steer(text) {
244
+ return activeSteering?.offer(text) ?? "unavailable";
245
+ }
246
+ return { turn, steer };
247
+ }
248
+ function restorePendingSteering(state, messages) {
249
+ if (messages.length === 0)
250
+ return;
251
+ const pending = messages.join("\n\n");
252
+ state.editor = edit.of(state.editor.text === "" ? pending : `${pending}\n\n${state.editor.text}`);
253
+ state.completing = undefined;
254
+ }
255
+ function closeFailedTurn(history, settlement) {
256
+ const closed = [...history];
257
+ if (closed.at(-1)?.role === "assistant")
258
+ return closed;
259
+ const text = settlement === "interrupted"
260
+ ? "The previous attempt was interrupted by the user before completion."
261
+ : "The previous attempt failed before completion.";
262
+ closed.push({ role: "assistant", content: [{ kind: "text", text }] });
263
+ return closed;
264
+ }
package/dist/tui/turn.js CHANGED
@@ -3,17 +3,14 @@
3
3
  // The controller speaks in stream events and tool results; the screen speaks in
4
4
  // blocks. This is the whole of the translation, kept out of the shell so that
5
5
  // neither has to know how the other is built.
6
- import { graphemes } from "../text-boundary.js";
7
- import { condense, diff } from "../ui/diff.js";
8
6
  import { promptFor } from "./approve.js";
7
+ import { toolTarget, previewDetails, producedDetails, outputDetails } from "./tool-details.js";
9
8
  // Semantic activity labels feed the footer's compact state and timer while
10
9
  // reasoning and tools keep the detailed work visible in the transcript.
11
10
  const WAITING = "Waiting";
12
11
  const THINKING = "Thinking";
13
12
  const RESPONDING = "Responding";
14
13
  const ASKING = "Waiting for you";
15
- /** Unchanged rows kept either side of a change. */
16
- const CONTEXT = 2;
17
14
  export function transcribe(stage) {
18
15
  // The block the stream is currently filling. A change of kind starts a new
19
16
  // one, which is what keeps reasoning and answer from running together.
@@ -104,10 +101,10 @@ export function transcribe(stage) {
104
101
  const block = {
105
102
  kind: "tool",
106
103
  name: call.name,
107
- target: target(call.input),
104
+ target: toolTarget(call.input),
108
105
  right: "ready",
109
106
  tone: "pending",
110
- body: preview(call, look),
107
+ body: previewDetails(call, look),
111
108
  };
112
109
  tools.set(call.id, block);
113
110
  stage.emit(block);
@@ -117,7 +114,7 @@ export function transcribe(stage) {
117
114
  const block = tools.get(call.id);
118
115
  if (block === undefined || block.kind !== "tool" || block.tone !== "pending")
119
116
  return;
120
- block.body = details(output, "out");
117
+ block.body = outputDetails(output, "out");
121
118
  stage.render(block);
122
119
  },
123
120
  onToolResult(call, result, summary) {
@@ -138,8 +135,8 @@ export function transcribe(stage) {
138
135
  // A failure replaces the preview: what the call was going to do stops
139
136
  // being the interesting part the moment it did not do it.
140
137
  const outcome = result.isError
141
- ? details(result.output, "out")
142
- : produced(call, result.output);
138
+ ? outputDetails(result.output, "out")
139
+ : producedDetails(call, result.output);
143
140
  if (outcome !== undefined)
144
141
  block.body = outcome;
145
142
  stage.render(block);
@@ -158,7 +155,7 @@ export function transcribe(stage) {
158
155
  stage.render(block);
159
156
  }
160
157
  return new Promise((resolve) => {
161
- stage.ask(promptFor(call, target(call.input), stage.palette), (answer) => {
158
+ stage.ask(promptFor(call, toolTarget(call.input), stage.palette), (answer) => {
162
159
  if (answer === "always")
163
160
  stage.remember(call);
164
161
  const approved = answer !== "no";
@@ -204,133 +201,3 @@ function settleDuration(block) {
204
201
  }
205
202
  block.startedAt = undefined;
206
203
  }
207
- /** The argument worth showing: the thing the call acts on. */
208
- function target(input) {
209
- const { path, command } = input;
210
- if (typeof command === "string")
211
- return command;
212
- if (typeof path === "string")
213
- return path;
214
- const rest = JSON.stringify(input);
215
- return rest === "{}" ? "" : rest;
216
- }
217
- /**
218
- * What the call is about to do, drawn before it is allowed to do it.
219
- *
220
- * The tool is asked first, because only it knows what is already on disk: a
221
- * write against an existing file is a replacement, and showing it as a page of
222
- * additions hides exactly the part worth approving. The fallback diffs what is
223
- * in the arguments, which is all there is when a tool has nothing to say.
224
- */
225
- function preview(call, look) {
226
- if (look !== undefined)
227
- return changes(look.before, look.after);
228
- const input = call.input;
229
- if (call.name === "write_file" && typeof input.content === "string") {
230
- return changes("", input.content);
231
- }
232
- if (call.name === "edit_file" && typeof input.old_text === "string") {
233
- return changes(input.old_text, typeof input.new_text === "string" ? input.new_text : "");
234
- }
235
- return undefined;
236
- }
237
- /** Two texts as the rows of their difference, unchanged runs summed up. */
238
- function changes(before, after) {
239
- let oldLine = 1;
240
- let newLine = 1;
241
- const rows = [];
242
- for (const changed of condense(diff(before, after), CONTEXT)) {
243
- if (changed.kind === "gap") {
244
- rows.push({ kind: "gap", text: `… ${changed.skipped} unchanged` });
245
- oldLine += changed.skipped;
246
- newLine += changed.skipped;
247
- continue;
248
- }
249
- if (changed.kind === "keep") {
250
- rows.push({ kind: "keep", text: tabs(changed.text), oldLine, newLine });
251
- oldLine++;
252
- newLine++;
253
- continue;
254
- }
255
- if (changed.kind === "del") {
256
- rows.push({ kind: "del", text: tabs(changed.text), oldLine });
257
- oldLine++;
258
- continue;
259
- }
260
- rows.push({ kind: "add", text: tabs(changed.text), newLine });
261
- newLine++;
262
- }
263
- emphasizePairs(rows);
264
- return rows.length === 0 ? undefined : rows;
265
- }
266
- function emphasizePairs(rows) {
267
- for (let index = 0; index < rows.length - 1; index++) {
268
- const removed = rows[index];
269
- const added = rows[index + 1];
270
- if (removed?.kind !== "del" || added?.kind !== "add")
271
- continue;
272
- if (rows[index - 1]?.kind === "del" || rows[index + 2]?.kind === "add")
273
- continue;
274
- const removedClusters = graphemes(removed.text);
275
- const addedClusters = graphemes(added.text);
276
- let prefix = 0;
277
- let start = 0;
278
- while (prefix < removedClusters.length &&
279
- prefix < addedClusters.length &&
280
- removedClusters[prefix] === addedClusters[prefix]) {
281
- start += removedClusters[prefix].length;
282
- prefix++;
283
- }
284
- let suffix = 0;
285
- let removedSuffix = 0;
286
- let addedSuffix = 0;
287
- while (suffix < removedClusters.length - prefix &&
288
- suffix < addedClusters.length - prefix &&
289
- removedClusters[removedClusters.length - 1 - suffix] ===
290
- addedClusters[addedClusters.length - 1 - suffix]) {
291
- removedSuffix += removedClusters[removedClusters.length - 1 - suffix].length;
292
- addedSuffix += addedClusters[addedClusters.length - 1 - suffix].length;
293
- suffix++;
294
- }
295
- while (suffix > 0 &&
296
- (!wordBoundary(removed.text, removedSuffix) || !wordBoundary(added.text, addedSuffix))) {
297
- removedSuffix -= removedClusters[removedClusters.length - suffix].length;
298
- addedSuffix -= addedClusters[addedClusters.length - suffix].length;
299
- suffix--;
300
- }
301
- const removedLength = removed.text.length - start - removedSuffix;
302
- const addedLength = added.text.length - start - addedSuffix;
303
- if (removedLength > 0)
304
- removed.emphasis = { start, length: removedLength };
305
- if (addedLength > 0)
306
- added.emphasis = { start, length: addedLength };
307
- }
308
- }
309
- function wordBoundary(text, suffix) {
310
- const at = text.length - suffix;
311
- if (at <= 0 || at >= text.length)
312
- return true;
313
- return /\w/.test(text[at - 1]) !== /\w/.test(text[at]);
314
- }
315
- /** What the call left behind, for the calls whose output is worth a look. */
316
- function produced(call, output) {
317
- if (call.name === "run_command")
318
- return details(output, "out");
319
- // A read or a listing is already summed up on the right of its own row, and
320
- // the model is about to say what was in it. Printing it twice helps nobody.
321
- return undefined;
322
- }
323
- function details(text, kind) {
324
- const rows = split(text).map((line) => ({ kind, text: tabs(line) }));
325
- return rows.length === 0 ? undefined : rows;
326
- }
327
- function split(text) {
328
- const lines = text.replace(/\r\n?/g, "\n").split("\n");
329
- while (lines.length > 0 && lines[lines.length - 1] === "")
330
- lines.pop();
331
- return lines.map(tabs);
332
- }
333
- // A tab is a width the terminal decides and the row measurement cannot see.
334
- function tabs(line) {
335
- return line.replace(/\t/g, " ");
336
- }
@@ -0,0 +1,2 @@
1
+ // Host capabilities shared by foreground command and turn workflows.
2
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,22 +41,22 @@
41
41
  "registry": "https://registry.npmjs.org/"
42
42
  },
43
43
  "scripts": {
44
- "build:release": "node dev/build-release.ts",
44
+ "build:release": "node scripts/build-release.ts",
45
45
  "pack:release": "npm run build:release && npm pack --ignore-scripts",
46
46
  "start": "node src/main.ts",
47
- "tui:lab": "node dev/tui-lab.ts",
48
- "bench:context": "node dev/benchmark-context.ts",
49
- "bench:redaction": "node dev/benchmark-redaction.ts",
50
- "bench:search": "node dev/benchmark-search.ts",
51
- "bench:session": "node dev/benchmark-session.ts",
52
- "bench:transcript": "node dev/benchmark-transcript.ts",
47
+ "tui:lab": "node dev/tui/main.ts",
48
+ "bench:context": "node dev/benchmarks/context.ts",
49
+ "bench:redaction": "node dev/benchmarks/redaction.ts",
50
+ "bench:search": "node dev/benchmarks/search.ts",
51
+ "bench:session": "node dev/benchmarks/session.ts",
52
+ "bench:transcript": "node dev/benchmarks/transcript.ts",
53
53
  "typecheck": "tsc --noEmit",
54
54
  "test": "npm run build:release && node --test",
55
55
  "coverage": "npm run build:release && node --test --experimental-test-coverage --test-coverage-include=\"src/**/*.ts\" --test-coverage-lines=80 --test-coverage-branches=75 --test-coverage-functions=75",
56
- "check:source-tree": "node dev/check-source-tree.ts",
57
- "check:release-tag": "node dev/check-release-tag.ts",
58
- "check:package": "npm run build:release -- --quiet && node dev/check-package.ts",
59
- "check:install": "npm run build:release -- --quiet && node dev/check-installed-cli.ts",
56
+ "check:source-tree": "node scripts/check-source-tree.ts",
57
+ "check:release-tag": "node scripts/check-release-tag.ts",
58
+ "check:package": "npm run build:release -- --quiet && node scripts/check-package.ts",
59
+ "check:install": "npm run build:release -- --quiet && node scripts/check-installed-cli.ts",
60
60
  "check": "npm run check:source-tree && npm run typecheck && npm run coverage && npm run check:package && npm run check:install"
61
61
  },
62
62
  "devDependencies": {
@@ -1,74 +0,0 @@
1
- // Ollama connection choices inside the persistent settings hub.
2
- import { askForKey } from "./credential-commands.js";
3
- import { keyFor } from "./credentials.js";
4
- import { of } from "./tui/editor.js";
5
- import { heading } from "./tui/picker.js";
6
- import { OLLAMA_CLOUD_HOST, OLLAMA_LOCAL_HOST, parseOllamaEndpoint, } from "./providers/ollama-endpoint.js";
7
- import { configureOllama, ollamaConnection } from "./providers/ollama.js";
8
- const KEY = "OLLAMA_API_KEY";
9
- export function ollamaConnectionHint() {
10
- const connection = ollamaConnection();
11
- const origin = new URL(connection.baseUrl).host;
12
- const automatic = connection.inferred ? " · automatic" : "";
13
- if (connection.kind === "local")
14
- return `local · this computer${automatic}`;
15
- return `${connection.kind} · ${origin}${automatic}`;
16
- }
17
- export async function ollamaConnectionSetting(session, host, persist) {
18
- if (host.choose === undefined)
19
- return;
20
- const current = ollamaConnection();
21
- const index = await host.choose({
22
- title: heading("Ollama connection", "where Ollama requests run", session.palette),
23
- options: [
24
- { label: "cloud", hint: "ollama.com · API key" },
25
- { label: "local", hint: "this computer · no API key" },
26
- { label: "custom", hint: "HTTPS or loopback endpoint" },
27
- ],
28
- index: current.kind === "cloud" ? 0 : current.kind === "local" ? 1 : 2,
29
- });
30
- if (index === undefined)
31
- return;
32
- let nextHost;
33
- if (index === 0)
34
- nextHost = OLLAMA_CLOUD_HOST;
35
- else if (index === 1)
36
- nextHost = OLLAMA_LOCAL_HOST;
37
- else {
38
- const custom = await customEndpoint(session, host, current.kind === "custom" ? current.baseUrl : "https://");
39
- if (custom === undefined)
40
- return;
41
- nextHost = custom;
42
- }
43
- const endpoint = parseOllamaEndpoint(nextHost);
44
- if (!endpoint.loopback && keyFor(KEY) === undefined) {
45
- const accepted = await askForKey(KEY, host, session.palette);
46
- if (!accepted)
47
- return;
48
- }
49
- if (!(await persist({ ollamaHost: endpoint.baseUrl })))
50
- return;
51
- configureOllama(endpoint.baseUrl);
52
- session.config.ollamaHost = endpoint.baseUrl;
53
- }
54
- async function customEndpoint(session, host, initial) {
55
- if (host.type === undefined)
56
- return undefined;
57
- const field = {
58
- title: heading("Ollama endpoint", "HTTPS or exact loopback URL", session.palette),
59
- right: "enter save · esc back",
60
- editor: of(initial),
61
- secret: false,
62
- note: "Remote endpoints must use HTTPS. API keys are stored separately.",
63
- };
64
- const value = await host.type(field);
65
- if (value === undefined)
66
- return undefined;
67
- try {
68
- return parseOllamaEndpoint(value).baseUrl;
69
- }
70
- catch (error) {
71
- host.emit({ kind: "notice", text: error.message, tone: "error" });
72
- return undefined;
73
- }
74
- }
@@ -1,32 +0,0 @@
1
- // Small deterministic motion primitives for terminal cells.
2
- export const TOOL_BIRTH_MS = 300;
3
- export const TOOL_SETTLE_MS = 700;
4
- export const TOOL_ROW_ARRIVAL_MS = 420;
5
- export const TOOL_LEADER_MAX_MS = 1_500;
6
- export function interval(now, start, duration) {
7
- if (start === undefined)
8
- return 1;
9
- if (duration <= 0)
10
- return 1;
11
- return Math.max(0, Math.min(1, (now - start) / duration));
12
- }
13
- export function easeOut(value) {
14
- const rest = 1 - Math.max(0, Math.min(1, value));
15
- return 1 - rest * rest * rest;
16
- }
17
- export function easeInOut(value) {
18
- const at = Math.max(0, Math.min(1, value));
19
- return at < 0.5 ? 4 * at * at * at : 1 - Math.pow(-2 * at + 2, 3) / 2;
20
- }
21
- export function mix(from, to, amount) {
22
- const at = Math.max(0, Math.min(1, amount));
23
- return [
24
- Math.round(from[0] + (to[0] - from[0]) * at),
25
- Math.round(from[1] + (to[1] - from[1]) * at),
26
- Math.round(from[2] + (to[2] - from[2]) * at),
27
- ];
28
- }
29
- /** A restrained pulse that never reaches either endpoint. */
30
- export function breathe(now, period = 1_400) {
31
- return 0.2 + ((Math.sin((now / period) * Math.PI * 2) + 1) / 2) * 0.6;
32
- }