tokwise 0.1.2 → 0.1.4

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
@@ -39,7 +39,7 @@ async function answerWithOllama(question, results, options) {
39
39
  .join("\n\n");
40
40
  const prompt = [
41
41
  "Answer the user's question using only the saved clip evidence below.",
42
- "Cite clips by their readable reference (e.g. @author \u00b7 Mon YYYY \u2014 \"title\") when making claims. If evidence is thin, say so.",
42
+ "Cite clips by their readable reference (e.g. @author (Mon YYYY)) when making claims; include the clip's Source URL when a claim needs to be traceable. If evidence is thin, say so.",
43
43
  "",
44
44
  `Question: ${question}`,
45
45
  "",
@@ -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/dist/reference.js CHANGED
@@ -23,18 +23,14 @@ export function videoReference(video) {
23
23
  ? `@${video.author.username}`
24
24
  : video.author?.displayName || "unknown";
25
25
  const date = referenceDate(video);
26
- const title = videoTitle(video);
27
- const head = [author, date].filter(Boolean).join(" \u00b7 ");
28
- return `${head} \u2014 "${title}" #${shortId(video)}`;
26
+ return date ? `${author} (${date})` : author;
29
27
  }
30
28
  export function formatReference(video) {
31
29
  const author = video.author?.username
32
30
  ? c.accent(`@${video.author.username}`)
33
31
  : c.muted(video.author?.displayName || "unknown");
34
32
  const date = referenceDate(video);
35
- const datePart = date ? ` ${c.muted("\u00b7")} ${c.muted(date)}` : "";
36
- const title = c.value(`"${videoTitle(video)}"`);
37
- return `${author}${datePart} ${c.muted("\u2014")} ${title} ${c.muted(`#${shortId(video)}`)}`;
33
+ return date ? `${author} ${c.muted(`(${date})`)}` : author;
38
34
  }
39
35
  function referenceDate(video) {
40
36
  const iso = video.createdAt ?? video.savedAt;
package/dist/skill.js CHANGED
@@ -24,7 +24,7 @@ export function skillContent() {
24
24
  "",
25
25
  "## Grounding",
26
26
  "",
27
- "Cite clips by their readable reference (`@author \u00b7 Mon YYYY \u2014 \"title\"`, optionally with the trailing short id like `#49952278`) or by Markdown page path when drawing conclusions. Run `tokwise show <short-id-or-url>` to pull a clip back up. Treat transcripts as user-owned local context and do not assume videos are public.",
27
+ "Cite clips by their readable reference (`@author (Mon YYYY)`) and the clip URL, or by Markdown page path, when drawing conclusions. Run `tokwise show <id-or-url>` to pull a clip back up. Treat transcripts as user-owned local context and do not assume videos are public.",
28
28
  "",
29
29
  ].join("\n");
30
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokwise",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",