@kiipu/cli 0.0.7 → 0.0.9
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/README.md +50 -20
- package/dist/commands/ask.js +229 -0
- package/dist/commands/auth.js +1 -1
- package/dist/commands/doctor.js +9 -3
- package/dist/commands/help.js +85 -48
- package/dist/commands/{post.js → note.js} +68 -64
- package/dist/config/load-env.js +4 -1
- package/dist/index.js +17 -4
- package/dist/lib/ask-client.js +278 -0
- package/dist/lib/ask-formatters.js +114 -0
- package/dist/lib/kiipu-integration-client.js +26 -20
- package/dist/lib/kiipu-user-client.js +14 -14
- package/dist/lib/{post-actions.js → note-actions.js} +19 -19
- package/dist/lib/{post-formatters.js → note-formatters.js} +26 -23
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatNoteCollection, formatNoteDetail } from '../lib/note-formatters.js';
|
|
2
2
|
import { KiipuUserApiClient } from '../lib/kiipu-user-client.js';
|
|
3
|
-
import {
|
|
3
|
+
import { executeNoteAction } from '../lib/note-actions.js';
|
|
4
4
|
import { hasFlag, readFlag } from '../utils/args.js';
|
|
5
5
|
const actions = new Set([
|
|
6
6
|
'create',
|
|
@@ -18,21 +18,21 @@ const sortValues = new Set(['updatedAt', 'createdAt', 'title']);
|
|
|
18
18
|
function usage(action) {
|
|
19
19
|
switch (action) {
|
|
20
20
|
case 'create':
|
|
21
|
-
return 'Usage: kiipu
|
|
21
|
+
return 'Usage: kiipu note create --content "<text>"\n or: kiipu note create "<text>"';
|
|
22
22
|
case 'list':
|
|
23
|
-
return 'Usage: kiipu
|
|
23
|
+
return 'Usage: kiipu note list [--tag <tag>] [--sort <updatedAt|createdAt|title>] [--starred] [--deleted]';
|
|
24
24
|
case 'search':
|
|
25
|
-
return 'Usage: kiipu
|
|
25
|
+
return 'Usage: kiipu note search <query>\n or: kiipu note search --query "<query>"';
|
|
26
26
|
case 'show':
|
|
27
|
-
return 'Usage: kiipu
|
|
27
|
+
return 'Usage: kiipu note show --id <noteId>';
|
|
28
28
|
case 'update':
|
|
29
|
-
return 'Usage: kiipu
|
|
29
|
+
return 'Usage: kiipu note update --id <noteId> --content "<text>" [--title "<title>"] [--visibility public|private] [--tags <a,b,c>]';
|
|
30
30
|
case 'star':
|
|
31
|
-
return 'Usage: kiipu
|
|
31
|
+
return 'Usage: kiipu note star --id <noteId>';
|
|
32
32
|
case 'pin':
|
|
33
|
-
return 'Usage: kiipu
|
|
33
|
+
return 'Usage: kiipu note pin --id <noteId>';
|
|
34
34
|
default:
|
|
35
|
-
return `Usage: kiipu
|
|
35
|
+
return `Usage: kiipu note ${action} --id <noteId>`;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
function error(message) {
|
|
@@ -81,9 +81,9 @@ function validateSort(sort) {
|
|
|
81
81
|
}
|
|
82
82
|
return sortValues.has(sort) ? sort : null;
|
|
83
83
|
}
|
|
84
|
-
function
|
|
85
|
-
const
|
|
86
|
-
return
|
|
84
|
+
function parseNoteId(args, action) {
|
|
85
|
+
const noteId = readFlag(args, '--id')?.trim();
|
|
86
|
+
return noteId ? noteId : error(usage(action));
|
|
87
87
|
}
|
|
88
88
|
function parseTags(input) {
|
|
89
89
|
if (!input) {
|
|
@@ -105,19 +105,19 @@ function parseTags(input) {
|
|
|
105
105
|
}
|
|
106
106
|
return Array.from(unique.values());
|
|
107
107
|
}
|
|
108
|
-
function
|
|
108
|
+
function toCliNote(note) {
|
|
109
109
|
return {
|
|
110
|
-
id:
|
|
111
|
-
title:
|
|
112
|
-
rawText:
|
|
113
|
-
finalText:
|
|
114
|
-
visibility:
|
|
115
|
-
tags:
|
|
116
|
-
folder:
|
|
117
|
-
isPinned:
|
|
118
|
-
isStarred:
|
|
119
|
-
createdAt:
|
|
120
|
-
updatedAt:
|
|
110
|
+
id: note.id,
|
|
111
|
+
title: note.title,
|
|
112
|
+
rawText: note.rawText,
|
|
113
|
+
finalText: note.finalText,
|
|
114
|
+
visibility: note.visibility,
|
|
115
|
+
tags: note.tags,
|
|
116
|
+
folder: note.folder ?? null,
|
|
117
|
+
isPinned: note.isPinned,
|
|
118
|
+
isStarred: note.isStarred,
|
|
119
|
+
createdAt: note.createdAt,
|
|
120
|
+
updatedAt: note.updatedAt,
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
async function handleMutationAction(config, action, args) {
|
|
@@ -126,13 +126,13 @@ async function handleMutationAction(config, action, args) {
|
|
|
126
126
|
if (!content) {
|
|
127
127
|
return error(usage('create'));
|
|
128
128
|
}
|
|
129
|
-
return
|
|
129
|
+
return executeNoteAction(config, { action: 'create', content });
|
|
130
130
|
}
|
|
131
|
-
const
|
|
132
|
-
if (typeof
|
|
133
|
-
return
|
|
131
|
+
const noteId = parseNoteId(args, action);
|
|
132
|
+
if (typeof noteId !== 'string') {
|
|
133
|
+
return noteId;
|
|
134
134
|
}
|
|
135
|
-
return
|
|
135
|
+
return executeNoteAction(config, { action, noteId });
|
|
136
136
|
}
|
|
137
137
|
async function handleList(config, args) {
|
|
138
138
|
const { client, error: clientError } = requireUserClient(config);
|
|
@@ -149,36 +149,36 @@ async function handleList(config, args) {
|
|
|
149
149
|
return error(`--starred and --deleted cannot be used together.\n${usage('list')}`);
|
|
150
150
|
}
|
|
151
151
|
const tag = readFlag(args, '--tag');
|
|
152
|
-
let
|
|
152
|
+
let notes;
|
|
153
153
|
let responseData;
|
|
154
154
|
if (starred) {
|
|
155
|
-
const response = await client.
|
|
155
|
+
const response = await client.listStarredNotes({ tag, sort });
|
|
156
156
|
if (!response.ok) {
|
|
157
157
|
return error(response.error.message);
|
|
158
158
|
}
|
|
159
|
-
|
|
159
|
+
notes = response.data.map((entry) => toCliNote(entry.note));
|
|
160
160
|
responseData = response.data;
|
|
161
161
|
}
|
|
162
162
|
else if (deleted) {
|
|
163
|
-
const response = await client.
|
|
163
|
+
const response = await client.listDeletedNotes({ sort });
|
|
164
164
|
if (!response.ok) {
|
|
165
165
|
return error(response.error.message);
|
|
166
166
|
}
|
|
167
|
-
|
|
167
|
+
notes = response.data.map(toCliNote);
|
|
168
168
|
responseData = response.data;
|
|
169
169
|
}
|
|
170
170
|
else {
|
|
171
|
-
const response = await client.
|
|
171
|
+
const response = await client.listNotes({ tag, sort });
|
|
172
172
|
if (!response.ok) {
|
|
173
173
|
return error(response.error.message);
|
|
174
174
|
}
|
|
175
|
-
|
|
175
|
+
notes = response.data.map(toCliNote);
|
|
176
176
|
responseData = response.data;
|
|
177
177
|
}
|
|
178
|
-
const title = starred ? 'Starred
|
|
178
|
+
const title = starred ? 'Starred notes' : deleted ? 'Deleted notes' : 'Notes';
|
|
179
179
|
return {
|
|
180
180
|
ok: true,
|
|
181
|
-
message:
|
|
181
|
+
message: formatNoteCollection(title, notes),
|
|
182
182
|
data: responseData,
|
|
183
183
|
};
|
|
184
184
|
}
|
|
@@ -191,13 +191,13 @@ async function handleSearch(config, args) {
|
|
|
191
191
|
if (!query) {
|
|
192
192
|
return error(usage('search'));
|
|
193
193
|
}
|
|
194
|
-
const response = await client.
|
|
194
|
+
const response = await client.searchNotes(query);
|
|
195
195
|
if (!response.ok) {
|
|
196
196
|
return error(response.error.message);
|
|
197
197
|
}
|
|
198
198
|
return {
|
|
199
199
|
ok: true,
|
|
200
|
-
message:
|
|
200
|
+
message: formatNoteCollection(`Search results for "${query}"`, response.data.map(toCliNote)),
|
|
201
201
|
data: response.data,
|
|
202
202
|
};
|
|
203
203
|
}
|
|
@@ -206,17 +206,17 @@ async function handleShow(config, args) {
|
|
|
206
206
|
if (!client) {
|
|
207
207
|
return clientError;
|
|
208
208
|
}
|
|
209
|
-
const
|
|
210
|
-
if (typeof
|
|
211
|
-
return
|
|
209
|
+
const noteId = parseNoteId(args, 'show');
|
|
210
|
+
if (typeof noteId !== 'string') {
|
|
211
|
+
return noteId;
|
|
212
212
|
}
|
|
213
|
-
const response = await client.
|
|
213
|
+
const response = await client.getNote(noteId);
|
|
214
214
|
if (!response.ok) {
|
|
215
215
|
return error(response.error.message);
|
|
216
216
|
}
|
|
217
217
|
return {
|
|
218
218
|
ok: true,
|
|
219
|
-
message:
|
|
219
|
+
message: formatNoteDetail(toCliNote(response.data)),
|
|
220
220
|
data: response.data,
|
|
221
221
|
};
|
|
222
222
|
}
|
|
@@ -225,9 +225,9 @@ async function handleUpdate(config, args) {
|
|
|
225
225
|
if (!client) {
|
|
226
226
|
return clientError;
|
|
227
227
|
}
|
|
228
|
-
const
|
|
229
|
-
if (typeof
|
|
230
|
-
return
|
|
228
|
+
const noteId = parseNoteId(args, 'update');
|
|
229
|
+
if (typeof noteId !== 'string') {
|
|
230
|
+
return noteId;
|
|
231
231
|
}
|
|
232
232
|
const content = readFlag(args, '--content')?.trim();
|
|
233
233
|
if (!content) {
|
|
@@ -239,7 +239,7 @@ async function handleUpdate(config, args) {
|
|
|
239
239
|
}
|
|
240
240
|
const title = readFlag(args, '--title');
|
|
241
241
|
const tags = readFlag(args, '--tags');
|
|
242
|
-
const response = await client.
|
|
242
|
+
const response = await client.updateNote(noteId, {
|
|
243
243
|
rawText: content,
|
|
244
244
|
...(title !== undefined ? { title: title || null } : {}),
|
|
245
245
|
...(visibility === 'public' || visibility === 'private' ? { visibility } : {}),
|
|
@@ -250,7 +250,7 @@ async function handleUpdate(config, args) {
|
|
|
250
250
|
}
|
|
251
251
|
return {
|
|
252
252
|
ok: true,
|
|
253
|
-
message: `
|
|
253
|
+
message: `Note updated.\n\n${formatNoteDetail(toCliNote(response.data))}`,
|
|
254
254
|
data: response.data,
|
|
255
255
|
};
|
|
256
256
|
}
|
|
@@ -259,17 +259,19 @@ async function handleStar(config, args) {
|
|
|
259
259
|
if (!client) {
|
|
260
260
|
return clientError;
|
|
261
261
|
}
|
|
262
|
-
const
|
|
263
|
-
if (typeof
|
|
264
|
-
return
|
|
262
|
+
const noteId = parseNoteId(args, 'star');
|
|
263
|
+
if (typeof noteId !== 'string') {
|
|
264
|
+
return noteId;
|
|
265
265
|
}
|
|
266
|
-
const response = await client.toggleStar(
|
|
266
|
+
const response = await client.toggleStar(noteId);
|
|
267
267
|
if (!response.ok) {
|
|
268
268
|
return error(response.error.message);
|
|
269
269
|
}
|
|
270
270
|
return {
|
|
271
271
|
ok: true,
|
|
272
|
-
message: response.data.isStarred
|
|
272
|
+
message: response.data.isStarred
|
|
273
|
+
? `Note starred. ${response.data.id}`
|
|
274
|
+
: `Note unstarred. ${response.data.id}`,
|
|
273
275
|
data: response.data,
|
|
274
276
|
};
|
|
275
277
|
}
|
|
@@ -278,24 +280,26 @@ async function handlePin(config, args) {
|
|
|
278
280
|
if (!client) {
|
|
279
281
|
return clientError;
|
|
280
282
|
}
|
|
281
|
-
const
|
|
282
|
-
if (typeof
|
|
283
|
-
return
|
|
283
|
+
const noteId = parseNoteId(args, 'pin');
|
|
284
|
+
if (typeof noteId !== 'string') {
|
|
285
|
+
return noteId;
|
|
284
286
|
}
|
|
285
|
-
const response = await client.togglePin(
|
|
287
|
+
const response = await client.togglePin(noteId);
|
|
286
288
|
if (!response.ok) {
|
|
287
289
|
return error(response.error.message);
|
|
288
290
|
}
|
|
289
291
|
return {
|
|
290
292
|
ok: true,
|
|
291
|
-
message: response.data.isPinned
|
|
293
|
+
message: response.data.isPinned
|
|
294
|
+
? `Note pinned. ${response.data.id}`
|
|
295
|
+
: `Note unpinned. ${response.data.id}`,
|
|
292
296
|
data: response.data,
|
|
293
297
|
};
|
|
294
298
|
}
|
|
295
|
-
export async function
|
|
299
|
+
export async function runNoteCommand(config, args) {
|
|
296
300
|
const action = args[1];
|
|
297
301
|
if (!action || !actions.has(action)) {
|
|
298
|
-
return error('Usage: kiipu
|
|
302
|
+
return error('Usage: kiipu note <create|delete|restore|purge|list|search|show|update|star|pin> [options]');
|
|
299
303
|
}
|
|
300
304
|
const actionArgs = args.slice(2);
|
|
301
305
|
switch (action) {
|
|
@@ -317,5 +321,5 @@ export async function runPostCommand(config, args) {
|
|
|
317
321
|
case 'pin':
|
|
318
322
|
return handlePin(config, actionArgs);
|
|
319
323
|
}
|
|
320
|
-
return error('Unsupported
|
|
324
|
+
return error('Unsupported note action.');
|
|
321
325
|
}
|
package/dist/config/load-env.js
CHANGED
|
@@ -13,7 +13,10 @@ function parseEnvFile(content) {
|
|
|
13
13
|
if (!key || process.env[key] !== undefined) {
|
|
14
14
|
continue;
|
|
15
15
|
}
|
|
16
|
-
const value = line
|
|
16
|
+
const value = line
|
|
17
|
+
.slice(separatorIndex + 1)
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/^['"]|['"]$/g, '');
|
|
17
20
|
process.env[key] = value;
|
|
18
21
|
}
|
|
19
22
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import './config/load-env.js';
|
|
3
3
|
import { createDefaultConfig, loadKiipuConfig } from './config/config.js';
|
|
4
|
+
import { runAskCommand } from './commands/ask.js';
|
|
4
5
|
import { runAuthCommand } from './commands/auth.js';
|
|
5
6
|
import { runDoctorCommand } from './commands/doctor.js';
|
|
6
7
|
import { getHelpResult } from './commands/help.js';
|
|
7
|
-
import {
|
|
8
|
+
import { runNoteCommand } from './commands/note.js';
|
|
8
9
|
import { runSkillsCommand } from './commands/skills.js';
|
|
9
10
|
import { readFlag, hasFlag } from './utils/args.js';
|
|
10
11
|
import { CLI_VERSION } from './version.js';
|
|
11
12
|
function printResult(result, asJson) {
|
|
12
13
|
if (asJson) {
|
|
13
|
-
console.log(JSON.stringify(result, null, 2));
|
|
14
|
+
console.log(JSON.stringify(result.json ?? result, null, 2));
|
|
14
15
|
}
|
|
15
16
|
else {
|
|
16
17
|
console.log(result.message);
|
|
@@ -44,6 +45,11 @@ async function main() {
|
|
|
44
45
|
'--title',
|
|
45
46
|
'--visibility',
|
|
46
47
|
'--tags',
|
|
48
|
+
'--question',
|
|
49
|
+
'--conversation-id',
|
|
50
|
+
'--top-k',
|
|
51
|
+
'--source-mode',
|
|
52
|
+
'--limit',
|
|
47
53
|
]);
|
|
48
54
|
const positionalArgs = normalizedArgs.filter((arg, index, all) => {
|
|
49
55
|
if (arg === '--json' ||
|
|
@@ -80,8 +86,15 @@ async function main() {
|
|
|
80
86
|
result = await runDoctorCommand(await loadKiipuConfig());
|
|
81
87
|
return printResult(result, asJson);
|
|
82
88
|
}
|
|
83
|
-
if (command === '
|
|
84
|
-
result = await
|
|
89
|
+
if (command === 'note') {
|
|
90
|
+
result = await runNoteCommand(config, commandArgs);
|
|
91
|
+
return printResult(result, asJson);
|
|
92
|
+
}
|
|
93
|
+
if (command === 'ask') {
|
|
94
|
+
result = await runAskCommand(config, commandArgs, {
|
|
95
|
+
stream: !asJson,
|
|
96
|
+
write: (chunk) => process.stdout.write(chunk),
|
|
97
|
+
});
|
|
85
98
|
return printResult(result, asJson);
|
|
86
99
|
}
|
|
87
100
|
if (command === 'auth') {
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { TextDecoder } from 'node:util';
|
|
2
|
+
function isRecord(input) {
|
|
3
|
+
return typeof input === 'object' && input !== null;
|
|
4
|
+
}
|
|
5
|
+
function buildError(message, code = 'request_failed') {
|
|
6
|
+
return {
|
|
7
|
+
ok: false,
|
|
8
|
+
error: {
|
|
9
|
+
code,
|
|
10
|
+
message,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function getErrorMessage(payload, fallback) {
|
|
15
|
+
if (!isRecord(payload)) {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
if (typeof payload.message === 'string') {
|
|
19
|
+
return payload.message;
|
|
20
|
+
}
|
|
21
|
+
if (isRecord(payload.message) && typeof payload.message.message === 'string') {
|
|
22
|
+
return payload.message.message;
|
|
23
|
+
}
|
|
24
|
+
if (isRecord(payload.error) && typeof payload.error.message === 'string') {
|
|
25
|
+
return payload.error.message;
|
|
26
|
+
}
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
function getErrorCode(payload, fallback = 'request_failed') {
|
|
30
|
+
if (!isRecord(payload)) {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
if (typeof payload.code === 'string') {
|
|
34
|
+
return payload.code;
|
|
35
|
+
}
|
|
36
|
+
if (isRecord(payload.message) && typeof payload.message.code === 'string') {
|
|
37
|
+
return payload.message.code;
|
|
38
|
+
}
|
|
39
|
+
if (isRecord(payload.error) && typeof payload.error.code === 'string') {
|
|
40
|
+
return payload.error.code;
|
|
41
|
+
}
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
function parseSource(input) {
|
|
45
|
+
if (!isRecord(input)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const noteId = typeof input.noteId === 'string' ? input.noteId : '';
|
|
49
|
+
const snippet = typeof input.snippet === 'string' ? input.snippet : '';
|
|
50
|
+
if (!noteId || !snippet) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
index: typeof input.index === 'number' ? input.index : 0,
|
|
55
|
+
noteId,
|
|
56
|
+
title: typeof input.title === 'string' ? input.title : null,
|
|
57
|
+
snippet,
|
|
58
|
+
score: typeof input.score === 'number' ? input.score : 0,
|
|
59
|
+
createdAt: typeof input.createdAt === 'string' ? input.createdAt : undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function parseSources(input) {
|
|
63
|
+
return Array.isArray(input)
|
|
64
|
+
? input.map(parseSource).filter((source) => Boolean(source))
|
|
65
|
+
: [];
|
|
66
|
+
}
|
|
67
|
+
function parseAskEvent(input) {
|
|
68
|
+
if (!isRecord(input) || typeof input.type !== 'string') {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
switch (input.type) {
|
|
72
|
+
case 'meta':
|
|
73
|
+
if (typeof input.conversationId !== 'string') {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
type: 'meta',
|
|
78
|
+
conversationId: input.conversationId,
|
|
79
|
+
isNew: Boolean(input.isNew),
|
|
80
|
+
title: typeof input.title === 'string' ? input.title : null,
|
|
81
|
+
};
|
|
82
|
+
case 'sources':
|
|
83
|
+
return {
|
|
84
|
+
type: 'sources',
|
|
85
|
+
sources: parseSources(input.sources),
|
|
86
|
+
locked: Boolean(input.locked),
|
|
87
|
+
retrievalMode: input.retrievalMode === 'semantic' || input.retrievalMode === 'temporal_list'
|
|
88
|
+
? input.retrievalMode
|
|
89
|
+
: undefined,
|
|
90
|
+
totalCount: typeof input.totalCount === 'number' ? input.totalCount : undefined,
|
|
91
|
+
truncated: typeof input.truncated === 'boolean' ? input.truncated : undefined,
|
|
92
|
+
};
|
|
93
|
+
case 'delta':
|
|
94
|
+
return typeof input.text === 'string' ? { type: 'delta', text: input.text } : null;
|
|
95
|
+
case 'done':
|
|
96
|
+
if (typeof input.turnId !== 'string') {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
type: 'done',
|
|
101
|
+
turnId: input.turnId,
|
|
102
|
+
inputTokens: typeof input.inputTokens === 'number' ? input.inputTokens : 0,
|
|
103
|
+
outputTokens: typeof input.outputTokens === 'number' ? input.outputTokens : 0,
|
|
104
|
+
latencyMs: typeof input.latencyMs === 'number' ? input.latencyMs : 0,
|
|
105
|
+
};
|
|
106
|
+
case 'title':
|
|
107
|
+
if (typeof input.conversationId !== 'string' || typeof input.title !== 'string') {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return { type: 'title', conversationId: input.conversationId, title: input.title };
|
|
111
|
+
case 'error':
|
|
112
|
+
return {
|
|
113
|
+
type: 'error',
|
|
114
|
+
code: typeof input.code === 'string' ? input.code : 'unknown',
|
|
115
|
+
message: typeof input.message === 'string' ? input.message : 'Unknown Ask error.',
|
|
116
|
+
};
|
|
117
|
+
default:
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function extractDataField(frame) {
|
|
122
|
+
const dataLines = [];
|
|
123
|
+
for (const line of frame.split('\n')) {
|
|
124
|
+
const normalized = line.endsWith('\r') ? line.slice(0, -1) : line;
|
|
125
|
+
if (normalized.startsWith('data:')) {
|
|
126
|
+
dataLines.push(normalized.slice(5).replace(/^ /, ''));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return dataLines.length > 0 ? dataLines.join('\n') : null;
|
|
130
|
+
}
|
|
131
|
+
async function readJson(response) {
|
|
132
|
+
try {
|
|
133
|
+
return await response.json();
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export class KiipuAskClient {
|
|
140
|
+
config;
|
|
141
|
+
constructor(config) {
|
|
142
|
+
this.config = config;
|
|
143
|
+
}
|
|
144
|
+
async requestJson(path, init) {
|
|
145
|
+
let response;
|
|
146
|
+
try {
|
|
147
|
+
response = await fetch(`${this.config.apiBaseUrl}${path}`, {
|
|
148
|
+
...init,
|
|
149
|
+
headers: {
|
|
150
|
+
'Content-Type': 'application/json',
|
|
151
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
152
|
+
...(init.headers ?? {}),
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return buildError(`Kiipu API is unreachable at ${this.config.apiBaseUrl}.`, 'api_unreachable');
|
|
158
|
+
}
|
|
159
|
+
const payload = await readJson(response);
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
return buildError(getErrorMessage(payload, `Request failed with ${response.status}.`), getErrorCode(payload, `http_${response.status}`));
|
|
162
|
+
}
|
|
163
|
+
const data = isRecord(payload) && 'data' in payload ? payload.data : payload;
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
data: data,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
listConversations(input) {
|
|
170
|
+
const search = new URLSearchParams();
|
|
171
|
+
if (input.query) {
|
|
172
|
+
search.set('q', input.query);
|
|
173
|
+
}
|
|
174
|
+
if (input.limit) {
|
|
175
|
+
search.set('limit', String(input.limit));
|
|
176
|
+
}
|
|
177
|
+
if (input.archived) {
|
|
178
|
+
search.set('view', 'archived');
|
|
179
|
+
}
|
|
180
|
+
const query = search.toString();
|
|
181
|
+
return this.requestJson(`/ai/conversations${query ? `?${query}` : ''}`, { method: 'GET' });
|
|
182
|
+
}
|
|
183
|
+
getConversation(id) {
|
|
184
|
+
return this.requestJson(`/ai/conversations/${encodeURIComponent(id)}`, {
|
|
185
|
+
method: 'GET',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async *streamMessage(input) {
|
|
189
|
+
const body = {
|
|
190
|
+
question: input.question,
|
|
191
|
+
...(input.topK ? { topK: input.topK } : {}),
|
|
192
|
+
...(input.sourceMode ? { sourceMode: input.sourceMode } : {}),
|
|
193
|
+
};
|
|
194
|
+
let response;
|
|
195
|
+
try {
|
|
196
|
+
response = await fetch(`${this.config.apiBaseUrl}/ai/conversations/${encodeURIComponent(input.conversationId)}/messages`, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
'Content-Type': 'application/json',
|
|
200
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
201
|
+
Accept: 'text/event-stream',
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify(body),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
yield {
|
|
208
|
+
type: 'error',
|
|
209
|
+
code: 'network_error',
|
|
210
|
+
message: error instanceof Error ? error.message : 'Network request failed.',
|
|
211
|
+
};
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!response.ok || !response.body) {
|
|
215
|
+
const payload = await readJson(response);
|
|
216
|
+
yield {
|
|
217
|
+
type: 'error',
|
|
218
|
+
code: getErrorCode(payload, `http_${response.status}`),
|
|
219
|
+
message: getErrorMessage(payload, `Request failed with ${response.status}.`),
|
|
220
|
+
};
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
let settled = false;
|
|
224
|
+
const decoder = new TextDecoder();
|
|
225
|
+
const reader = response.body.getReader();
|
|
226
|
+
let buffer = '';
|
|
227
|
+
try {
|
|
228
|
+
while (true) {
|
|
229
|
+
const { value, done } = await reader.read();
|
|
230
|
+
if (done) {
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
buffer += decoder.decode(value, { stream: true });
|
|
234
|
+
let separator = buffer.indexOf('\n\n');
|
|
235
|
+
while (separator !== -1) {
|
|
236
|
+
const frame = buffer.slice(0, separator);
|
|
237
|
+
buffer = buffer.slice(separator + 2);
|
|
238
|
+
const data = extractDataField(frame);
|
|
239
|
+
if (data && data !== '[DONE]') {
|
|
240
|
+
try {
|
|
241
|
+
const event = parseAskEvent(JSON.parse(data));
|
|
242
|
+
if (event) {
|
|
243
|
+
if (event.type === 'done' || event.type === 'error') {
|
|
244
|
+
settled = true;
|
|
245
|
+
}
|
|
246
|
+
yield event;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Ignore malformed SSE frames from intermediaries.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
separator = buffer.indexOf('\n\n');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (!settled) {
|
|
259
|
+
yield {
|
|
260
|
+
type: 'error',
|
|
261
|
+
code: 'stream_error',
|
|
262
|
+
message: error instanceof Error ? error.message : 'Stream interrupted.',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
reader.releaseLock();
|
|
269
|
+
}
|
|
270
|
+
if (!settled) {
|
|
271
|
+
yield {
|
|
272
|
+
type: 'error',
|
|
273
|
+
code: 'stream_truncated',
|
|
274
|
+
message: 'The response ended unexpectedly. Please try again.',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|