tokwise 0.1.2 → 0.1.3

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/dist/ask.js CHANGED
@@ -49,10 +49,56 @@ async function answerWithOllama(question, results, options) {
49
49
  const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/generate`, {
50
50
  method: "POST",
51
51
  headers: { "content-type": "application/json" },
52
- body: JSON.stringify({ model, prompt, stream: false }),
52
+ body: JSON.stringify({ model, prompt, stream: true }),
53
53
  });
54
54
  if (!response.ok)
55
55
  throw new Error(`Ollama ask failed: ${response.status} ${response.statusText}`);
56
- const body = (await response.json());
57
- return body.response?.trim() || "Ollama returned an empty answer.";
56
+ if (!response.body)
57
+ throw new Error("Ollama ask failed: empty response body");
58
+ let full = "";
59
+ let firstToken = false;
60
+ for await (const chunk of iterateNdjson(response.body)) {
61
+ const piece = chunk.response ?? "";
62
+ if (!piece) {
63
+ if (chunk.done)
64
+ break;
65
+ continue;
66
+ }
67
+ if (!firstToken) {
68
+ firstToken = true;
69
+ options.onFirstToken?.();
70
+ }
71
+ full += piece;
72
+ options.onToken?.(piece);
73
+ if (chunk.done)
74
+ break;
75
+ }
76
+ return full.trim() || "Ollama returned an empty answer.";
77
+ }
78
+ async function* iterateNdjson(body) {
79
+ const reader = body.getReader();
80
+ const decoder = new TextDecoder();
81
+ let buffer = "";
82
+ try {
83
+ for (;;) {
84
+ const { done, value } = await reader.read();
85
+ if (done)
86
+ break;
87
+ buffer += decoder.decode(value, { stream: true });
88
+ const lines = buffer.split("\n");
89
+ buffer = lines.pop() ?? "";
90
+ for (const line of lines) {
91
+ const trimmed = line.trim();
92
+ if (!trimmed)
93
+ continue;
94
+ yield JSON.parse(trimmed);
95
+ }
96
+ }
97
+ const trailing = buffer.trim();
98
+ if (trailing)
99
+ yield JSON.parse(trailing);
100
+ }
101
+ finally {
102
+ reader.releaseLock();
103
+ }
58
104
  }
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ import { createCommand, createLibraryPage, deleteLibraryPage, listCommands, sear
22
22
  import { installSkill, skillContent, uninstallSkill } from "./skill.js";
23
23
  import { barChart, box, c, kvList, setColorEnabled, table, truncate } from "./render.js";
24
24
  import { formatReference } from "./reference.js";
25
- import { createProgress } from "./progress.js";
25
+ import { createProgress, createSpinner } from "./progress.js";
26
26
  const require = createRequire(import.meta.url);
27
27
  function version() {
28
28
  try {
@@ -482,12 +482,30 @@ export function buildCli() {
482
482
  const prefs = await loadPreferences();
483
483
  const { videos, index } = await requireIndex();
484
484
  const results = searchWithIndex(videos, index, { query: question, limit: Number(options.limit) });
485
- const answer = await answerQuestion(question, results, {
486
- engine: options.engine ?? prefs.askEngine ?? "extractive",
487
- model: options.model ?? prefs.model,
488
- ollamaBaseUrl: options.ollamaUrl ?? prefs.ollamaBaseUrl,
489
- });
490
- console.log(answer);
485
+ const engine = options.engine ?? prefs.askEngine ?? "extractive";
486
+ const spinner = engine === "ollama" ? createSpinner() : undefined;
487
+ spinner?.start();
488
+ let streamed = false;
489
+ let answer;
490
+ try {
491
+ answer = await answerQuestion(question, results, {
492
+ engine,
493
+ model: options.model ?? prefs.model,
494
+ ollamaBaseUrl: options.ollamaUrl ?? prefs.ollamaBaseUrl,
495
+ onFirstToken: () => spinner?.stop(),
496
+ onToken: (chunk) => {
497
+ streamed = true;
498
+ process.stdout.write(chunk);
499
+ },
500
+ });
501
+ }
502
+ finally {
503
+ spinner?.stop();
504
+ }
505
+ if (streamed)
506
+ process.stdout.write("\n");
507
+ else
508
+ console.log(answer);
491
509
  if (options.save) {
492
510
  const file = path.join(libraryDir(), "answers", `${Date.now()}-${slug(question)}.md`);
493
511
  await fs.mkdir(path.dirname(file), { recursive: true });
package/dist/progress.js CHANGED
@@ -54,3 +54,72 @@ export function createProgress(options) {
54
54
  },
55
55
  };
56
56
  }
57
+ const DEFAULT_SPINNER_MESSAGES = [
58
+ "Thinking",
59
+ "Pondering",
60
+ "Rummaging through your bookmarks",
61
+ "Connecting the dots",
62
+ "Cogitating",
63
+ "Synthesizing",
64
+ "Consulting the archive",
65
+ "Musing",
66
+ "Distilling insights",
67
+ ];
68
+ const MESSAGE_INTERVAL_MS = 2500;
69
+ export function createSpinner(options = {}) {
70
+ const messages = options.messages ?? DEFAULT_SPINNER_MESSAGES;
71
+ const messageIntervalMs = options.messageIntervalMs ?? MESSAGE_INTERVAL_MS;
72
+ const stream = process.stderr;
73
+ const tty = Boolean(stream.isTTY);
74
+ let running = false;
75
+ let frame = 0;
76
+ let messageIndex = 0;
77
+ let startedAt = 0;
78
+ let frameTimer;
79
+ let messageTimer;
80
+ function draw() {
81
+ const elapsedSec = Math.floor((Date.now() - startedAt) / 1000);
82
+ const spinner = c.accent(FRAMES[frame % FRAMES.length] ?? "");
83
+ const message = messages[messageIndex % messages.length] ?? "Thinking";
84
+ stream.write(`${CLEAR_LINE}${spinner} ${message}\u2026 ${c.muted(`(${elapsedSec}s)`)}`);
85
+ }
86
+ function clearTimers() {
87
+ if (frameTimer) {
88
+ clearInterval(frameTimer);
89
+ frameTimer = undefined;
90
+ }
91
+ if (messageTimer) {
92
+ clearInterval(messageTimer);
93
+ messageTimer = undefined;
94
+ }
95
+ }
96
+ return {
97
+ start() {
98
+ if (running || !tty)
99
+ return;
100
+ running = true;
101
+ startedAt = Date.now();
102
+ frame = 0;
103
+ messageIndex = 0;
104
+ draw();
105
+ frameTimer = setInterval(() => {
106
+ frame += 1;
107
+ draw();
108
+ }, FRAME_INTERVAL_MS);
109
+ frameTimer.unref();
110
+ messageTimer = setInterval(() => {
111
+ messageIndex += 1;
112
+ draw();
113
+ }, messageIntervalMs);
114
+ messageTimer.unref();
115
+ },
116
+ stop() {
117
+ if (!running)
118
+ return;
119
+ running = false;
120
+ clearTimers();
121
+ if (tty)
122
+ stream.write(CLEAR_LINE);
123
+ },
124
+ };
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokwise",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Local-first CLI for syncing, downloading, transcribing, searching, and analyzing saved short-form videos.",
5
5
  "type": "module",
6
6
  "author": "Sebastian Crossa",