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 +50 -4
- package/dist/cli.js +25 -7
- package/dist/progress.js +69 -0
- package/dist/reference.js +2 -6
- package/dist/skill.js +1 -1
- package/package.json +1 -1
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
|
|
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:
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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