@kiipu/cli 0.0.6 → 0.0.8

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.
@@ -0,0 +1,276 @@
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 postId = typeof input.postId === 'string' ? input.postId : '';
49
+ const snippet = typeof input.snippet === 'string' ? input.snippet : '';
50
+ if (!postId || !snippet) {
51
+ return null;
52
+ }
53
+ return {
54
+ index: typeof input.index === 'number' ? input.index : 0,
55
+ postId,
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) ? input.map(parseSource).filter((source) => Boolean(source)) : [];
64
+ }
65
+ function parseAskEvent(input) {
66
+ if (!isRecord(input) || typeof input.type !== 'string') {
67
+ return null;
68
+ }
69
+ switch (input.type) {
70
+ case 'meta':
71
+ if (typeof input.conversationId !== 'string') {
72
+ return null;
73
+ }
74
+ return {
75
+ type: 'meta',
76
+ conversationId: input.conversationId,
77
+ isNew: Boolean(input.isNew),
78
+ title: typeof input.title === 'string' ? input.title : null,
79
+ };
80
+ case 'sources':
81
+ return {
82
+ type: 'sources',
83
+ sources: parseSources(input.sources),
84
+ locked: Boolean(input.locked),
85
+ retrievalMode: input.retrievalMode === 'semantic' || input.retrievalMode === 'temporal_list'
86
+ ? input.retrievalMode
87
+ : undefined,
88
+ totalCount: typeof input.totalCount === 'number' ? input.totalCount : undefined,
89
+ truncated: typeof input.truncated === 'boolean' ? input.truncated : undefined,
90
+ };
91
+ case 'delta':
92
+ return typeof input.text === 'string' ? { type: 'delta', text: input.text } : null;
93
+ case 'done':
94
+ if (typeof input.turnId !== 'string') {
95
+ return null;
96
+ }
97
+ return {
98
+ type: 'done',
99
+ turnId: input.turnId,
100
+ inputTokens: typeof input.inputTokens === 'number' ? input.inputTokens : 0,
101
+ outputTokens: typeof input.outputTokens === 'number' ? input.outputTokens : 0,
102
+ latencyMs: typeof input.latencyMs === 'number' ? input.latencyMs : 0,
103
+ };
104
+ case 'title':
105
+ if (typeof input.conversationId !== 'string' || typeof input.title !== 'string') {
106
+ return null;
107
+ }
108
+ return { type: 'title', conversationId: input.conversationId, title: input.title };
109
+ case 'error':
110
+ return {
111
+ type: 'error',
112
+ code: typeof input.code === 'string' ? input.code : 'unknown',
113
+ message: typeof input.message === 'string' ? input.message : 'Unknown Ask error.',
114
+ };
115
+ default:
116
+ return null;
117
+ }
118
+ }
119
+ function extractDataField(frame) {
120
+ const dataLines = [];
121
+ for (const line of frame.split('\n')) {
122
+ const normalized = line.endsWith('\r') ? line.slice(0, -1) : line;
123
+ if (normalized.startsWith('data:')) {
124
+ dataLines.push(normalized.slice(5).replace(/^ /, ''));
125
+ }
126
+ }
127
+ return dataLines.length > 0 ? dataLines.join('\n') : null;
128
+ }
129
+ async function readJson(response) {
130
+ try {
131
+ return await response.json();
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ }
137
+ export class KiipuAskClient {
138
+ config;
139
+ constructor(config) {
140
+ this.config = config;
141
+ }
142
+ async requestJson(path, init) {
143
+ let response;
144
+ try {
145
+ response = await fetch(`${this.config.apiBaseUrl}${path}`, {
146
+ ...init,
147
+ headers: {
148
+ 'Content-Type': 'application/json',
149
+ Authorization: `Bearer ${this.config.apiKey}`,
150
+ ...(init.headers ?? {}),
151
+ },
152
+ });
153
+ }
154
+ catch {
155
+ return buildError(`Kiipu API is unreachable at ${this.config.apiBaseUrl}.`, 'api_unreachable');
156
+ }
157
+ const payload = await readJson(response);
158
+ if (!response.ok) {
159
+ return buildError(getErrorMessage(payload, `Request failed with ${response.status}.`), getErrorCode(payload, `http_${response.status}`));
160
+ }
161
+ const data = isRecord(payload) && 'data' in payload ? payload.data : payload;
162
+ return {
163
+ ok: true,
164
+ data: data,
165
+ };
166
+ }
167
+ listConversations(input) {
168
+ const search = new URLSearchParams();
169
+ if (input.query) {
170
+ search.set('q', input.query);
171
+ }
172
+ if (input.limit) {
173
+ search.set('limit', String(input.limit));
174
+ }
175
+ if (input.archived) {
176
+ search.set('view', 'archived');
177
+ }
178
+ const query = search.toString();
179
+ return this.requestJson(`/ai/conversations${query ? `?${query}` : ''}`, { method: 'GET' });
180
+ }
181
+ getConversation(id) {
182
+ return this.requestJson(`/ai/conversations/${encodeURIComponent(id)}`, {
183
+ method: 'GET',
184
+ });
185
+ }
186
+ async *streamMessage(input) {
187
+ const body = {
188
+ question: input.question,
189
+ ...(input.topK ? { topK: input.topK } : {}),
190
+ ...(input.sourceMode ? { sourceMode: input.sourceMode } : {}),
191
+ };
192
+ let response;
193
+ try {
194
+ response = await fetch(`${this.config.apiBaseUrl}/ai/conversations/${encodeURIComponent(input.conversationId)}/messages`, {
195
+ method: 'POST',
196
+ headers: {
197
+ 'Content-Type': 'application/json',
198
+ Authorization: `Bearer ${this.config.apiKey}`,
199
+ Accept: 'text/event-stream',
200
+ },
201
+ body: JSON.stringify(body),
202
+ });
203
+ }
204
+ catch (error) {
205
+ yield {
206
+ type: 'error',
207
+ code: 'network_error',
208
+ message: error instanceof Error ? error.message : 'Network request failed.',
209
+ };
210
+ return;
211
+ }
212
+ if (!response.ok || !response.body) {
213
+ const payload = await readJson(response);
214
+ yield {
215
+ type: 'error',
216
+ code: getErrorCode(payload, `http_${response.status}`),
217
+ message: getErrorMessage(payload, `Request failed with ${response.status}.`),
218
+ };
219
+ return;
220
+ }
221
+ let settled = false;
222
+ const decoder = new TextDecoder();
223
+ const reader = response.body.getReader();
224
+ let buffer = '';
225
+ try {
226
+ while (true) {
227
+ const { value, done } = await reader.read();
228
+ if (done) {
229
+ break;
230
+ }
231
+ buffer += decoder.decode(value, { stream: true });
232
+ let separator = buffer.indexOf('\n\n');
233
+ while (separator !== -1) {
234
+ const frame = buffer.slice(0, separator);
235
+ buffer = buffer.slice(separator + 2);
236
+ const data = extractDataField(frame);
237
+ if (data && data !== '[DONE]') {
238
+ try {
239
+ const event = parseAskEvent(JSON.parse(data));
240
+ if (event) {
241
+ if (event.type === 'done' || event.type === 'error') {
242
+ settled = true;
243
+ }
244
+ yield event;
245
+ }
246
+ }
247
+ catch {
248
+ // Ignore malformed SSE frames from intermediaries.
249
+ }
250
+ }
251
+ separator = buffer.indexOf('\n\n');
252
+ }
253
+ }
254
+ }
255
+ catch (error) {
256
+ if (!settled) {
257
+ yield {
258
+ type: 'error',
259
+ code: 'stream_error',
260
+ message: error instanceof Error ? error.message : 'Stream interrupted.',
261
+ };
262
+ }
263
+ return;
264
+ }
265
+ finally {
266
+ reader.releaseLock();
267
+ }
268
+ if (!settled) {
269
+ yield {
270
+ type: 'error',
271
+ code: 'stream_truncated',
272
+ message: 'The response ended unexpectedly. Please try again.',
273
+ };
274
+ }
275
+ }
276
+ }
@@ -0,0 +1,114 @@
1
+ function formatTimestamp(value) {
2
+ if (!value) {
3
+ return 'unknown time';
4
+ }
5
+ const parsed = new Date(value);
6
+ if (Number.isNaN(parsed.getTime())) {
7
+ return value;
8
+ }
9
+ return parsed.toLocaleString('en-US', {
10
+ year: 'numeric',
11
+ month: 'short',
12
+ day: '2-digit',
13
+ hour: '2-digit',
14
+ minute: '2-digit',
15
+ });
16
+ }
17
+ function truncate(value, maxLength) {
18
+ const normalized = value.trim().replace(/\s+/g, ' ');
19
+ if (normalized.length <= maxLength) {
20
+ return normalized;
21
+ }
22
+ return `${normalized.slice(0, maxLength - 3)}...`;
23
+ }
24
+ function formatSourceLine(source) {
25
+ const title = source.title?.trim() || '(untitled)';
26
+ const score = Number.isFinite(source.score) ? ` score ${source.score.toFixed(3)}` : '';
27
+ return `[${source.index}] ${title} (${source.postId})${score}`;
28
+ }
29
+ export function formatAskFooter(input) {
30
+ const lines = ['', 'Sources:'];
31
+ if (input.sources.length === 0) {
32
+ lines.push(' none');
33
+ }
34
+ else {
35
+ for (const source of input.sources) {
36
+ lines.push(` ${formatSourceLine(source)}`);
37
+ lines.push(` ${truncate(source.snippet, 140)}`);
38
+ }
39
+ }
40
+ const usage = input.usage;
41
+ if (usage) {
42
+ lines.push('', `Usage: input ${usage.inputTokens}, output ${usage.outputTokens}, latency ${usage.latencyMs}ms`);
43
+ }
44
+ if (input.conversationId) {
45
+ lines.push(`Conversation: ${input.conversationId}`);
46
+ }
47
+ if (input.title) {
48
+ lines.push(`Title: ${input.title}`);
49
+ }
50
+ if (input.turnId) {
51
+ lines.push(`Turn: ${input.turnId}`);
52
+ }
53
+ return lines.join('\n');
54
+ }
55
+ export function formatConversationHistory(title, conversations, nextCursor) {
56
+ const lines = [title, ''];
57
+ if (conversations.length === 0) {
58
+ lines.push('No Ask conversations found.');
59
+ return lines.join('\n');
60
+ }
61
+ for (const item of conversations) {
62
+ const label = item.title?.trim() || truncate(item.preview, 80) || '(untitled)';
63
+ const flags = [item.pinnedAt ? 'pinned' : '', item.archivedAt ? 'archived' : '']
64
+ .filter(Boolean)
65
+ .join(', ');
66
+ lines.push(`${item.id} ${formatTimestamp(item.lastMessageAt)}`);
67
+ lines.push(` ${label}`);
68
+ if (flags) {
69
+ lines.push(` flags: ${flags}`);
70
+ }
71
+ lines.push('');
72
+ }
73
+ if (nextCursor) {
74
+ lines.push(`Next cursor: ${nextCursor}`);
75
+ }
76
+ if (!nextCursor && lines[lines.length - 1] === '') {
77
+ lines.pop();
78
+ }
79
+ return lines.join('\n');
80
+ }
81
+ function formatTurn(turn) {
82
+ const role = turn.role === 'user' ? 'User' : 'Assistant';
83
+ const lines = [`${role} ${turn.id} ${formatTimestamp(turn.createdAt)} status: ${turn.status}`];
84
+ if (turn.errorCode) {
85
+ lines.push(`error: ${turn.errorCode}`);
86
+ }
87
+ lines.push(turn.content.trim() || '(empty)');
88
+ if (turn.sources && turn.sources.length > 0) {
89
+ lines.push('sources:');
90
+ for (const source of turn.sources) {
91
+ lines.push(` ${formatSourceLine(source)}`);
92
+ }
93
+ }
94
+ return lines.join('\n');
95
+ }
96
+ export function formatConversationDetail(detail) {
97
+ const lines = [
98
+ detail.title?.trim() || '(untitled Ask conversation)',
99
+ '',
100
+ `id: ${detail.id}`,
101
+ `created: ${formatTimestamp(detail.createdAt)}`,
102
+ `updated: ${formatTimestamp(detail.lastMessageAt)}`,
103
+ '',
104
+ ];
105
+ if (detail.turns.length === 0) {
106
+ lines.push('No turns found.');
107
+ }
108
+ else {
109
+ for (const turn of detail.turns) {
110
+ lines.push(formatTurn(turn), '');
111
+ }
112
+ }
113
+ return lines.slice(0, -1).join('\n');
114
+ }
@@ -12,6 +12,19 @@ export class KiipuUserApiClient {
12
12
  constructor(config) {
13
13
  this.config = config;
14
14
  }
15
+ buildPath(path, params) {
16
+ if (!params) {
17
+ return path;
18
+ }
19
+ const search = new URLSearchParams();
20
+ for (const [key, value] of Object.entries(params)) {
21
+ if (typeof value === 'string' && value.length > 0) {
22
+ search.set(key, value);
23
+ }
24
+ }
25
+ const query = search.toString();
26
+ return query ? `${path}?${query}` : path;
27
+ }
15
28
  async request(path, init) {
16
29
  let response;
17
30
  try {
@@ -59,4 +72,35 @@ export class KiipuUserApiClient {
59
72
  body: JSON.stringify(input),
60
73
  });
61
74
  }
75
+ listPosts(input) {
76
+ return this.request(this.buildPath('/posts/me', input));
77
+ }
78
+ searchPosts(query) {
79
+ return this.request(this.buildPath('/posts/me/search', { q: query }));
80
+ }
81
+ listStarredPosts(input) {
82
+ return this.request(this.buildPath('/posts/me/starred', input));
83
+ }
84
+ listDeletedPosts(input) {
85
+ return this.request(this.buildPath('/posts/me/deleted', input));
86
+ }
87
+ getPost(id) {
88
+ return this.request(`/posts/${id}`);
89
+ }
90
+ updatePost(id, input) {
91
+ return this.request(`/posts/${id}/content`, {
92
+ method: 'PATCH',
93
+ body: JSON.stringify(input),
94
+ });
95
+ }
96
+ toggleStar(id) {
97
+ return this.request(`/posts/${id}/star`, {
98
+ method: 'PATCH',
99
+ });
100
+ }
101
+ togglePin(id) {
102
+ return this.request(`/posts/${id}/pin`, {
103
+ method: 'PATCH',
104
+ });
105
+ }
62
106
  }
@@ -0,0 +1,74 @@
1
+ function formatTimestamp(value) {
2
+ if (!value) {
3
+ return 'unknown time';
4
+ }
5
+ const parsed = new Date(value);
6
+ if (Number.isNaN(parsed.getTime())) {
7
+ return value;
8
+ }
9
+ return parsed.toLocaleString('en-US', {
10
+ year: 'numeric',
11
+ month: 'short',
12
+ day: '2-digit',
13
+ hour: '2-digit',
14
+ minute: '2-digit',
15
+ });
16
+ }
17
+ function formatTags(tags, limit) {
18
+ const normalized = Array.isArray(tags)
19
+ ? tags
20
+ .map((tag) => tag.tagName?.trim())
21
+ .filter((tag) => Boolean(tag))
22
+ .slice(0, limit)
23
+ : [];
24
+ return normalized.length > 0 ? normalized.map((tag) => `#${tag}`).join(', ') : 'none';
25
+ }
26
+ function getPostPreview(post) {
27
+ const content = (post.title?.trim() || post.finalText?.trim() || post.rawText?.trim() || '').replace(/\s+/g, ' ');
28
+ if (!content) {
29
+ return '(empty)';
30
+ }
31
+ return content.length > 100 ? `${content.slice(0, 97)}...` : content;
32
+ }
33
+ function getStatusFlags(post) {
34
+ const flags = [];
35
+ if (post.isPinned) {
36
+ flags.push('pinned');
37
+ }
38
+ if (post.isStarred) {
39
+ flags.push('starred');
40
+ }
41
+ return flags.length > 0 ? flags.join(', ') : 'none';
42
+ }
43
+ export function formatPostCollection(title, posts) {
44
+ const lines = [title, ''];
45
+ if (posts.length === 0) {
46
+ lines.push('No posts found.');
47
+ return lines.join('\n');
48
+ }
49
+ for (const post of posts) {
50
+ lines.push(`${post.id} ${formatTimestamp(post.updatedAt ?? post.createdAt)}`);
51
+ lines.push(` flags: ${getStatusFlags(post)} visibility: ${post.visibility} tags: ${formatTags(post.tags, 3)}`);
52
+ lines.push(` ${getPostPreview(post)}`);
53
+ lines.push('');
54
+ }
55
+ return lines.slice(0, -1).join('\n');
56
+ }
57
+ export function formatPostDetail(post) {
58
+ const title = post.title?.trim() || '(untitled)';
59
+ const body = post.finalText?.trim() || post.rawText?.trim() || '(empty)';
60
+ return [
61
+ title,
62
+ '',
63
+ body,
64
+ '',
65
+ `id: ${post.id}`,
66
+ `visibility: ${post.visibility}`,
67
+ `created: ${formatTimestamp(post.createdAt)}`,
68
+ `updated: ${formatTimestamp(post.updatedAt)}`,
69
+ `tags: ${formatTags(post.tags)}`,
70
+ `folder: ${post.folder ? `${post.folder.name} (${post.folder.id})` : 'none'}`,
71
+ `pinned: ${post.isPinned ? 'yes' : 'no'}`,
72
+ `starred: ${post.isStarred ? 'yes' : 'no'}`,
73
+ ].join('\n');
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kiipu/cli",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Kiipu CLI for local authentication, doctor checks, and direct post actions.",
5
5
  "license": "MIT",
6
6
  "type": "module",