@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.
@@ -1,27 +1,321 @@
1
+ import { formatPostCollection, formatPostDetail } from '../lib/post-formatters.js';
2
+ import { KiipuUserApiClient } from '../lib/kiipu-user-client.js';
1
3
  import { executePostAction } from '../lib/post-actions.js';
2
- export async function runPostCommand(config, input) {
3
- if (input.action === 'create') {
4
- const content = input.content?.trim();
5
- if (!content) {
6
- return {
7
- ok: false,
8
- message: 'Usage: kiipu post create --content "<text>"\n or: kiipu post create "<text>"',
9
- };
10
- }
11
- return executePostAction(config, {
12
- action: 'create',
13
- content,
14
- });
15
- }
16
- const postId = input.postId?.trim();
17
- if (!postId) {
4
+ import { hasFlag, readFlag } from '../utils/args.js';
5
+ const actions = new Set([
6
+ 'create',
7
+ 'delete',
8
+ 'restore',
9
+ 'purge',
10
+ 'list',
11
+ 'search',
12
+ 'show',
13
+ 'update',
14
+ 'star',
15
+ 'pin',
16
+ ]);
17
+ const sortValues = new Set(['updatedAt', 'createdAt', 'title']);
18
+ function usage(action) {
19
+ switch (action) {
20
+ case 'create':
21
+ return 'Usage: kiipu post create --content "<text>"\n or: kiipu post create "<text>"';
22
+ case 'list':
23
+ return 'Usage: kiipu post list [--tag <tag>] [--sort <updatedAt|createdAt|title>] [--starred] [--deleted]';
24
+ case 'search':
25
+ return 'Usage: kiipu post search <query>\n or: kiipu post search --query "<query>"';
26
+ case 'show':
27
+ return 'Usage: kiipu post show --id <postId>';
28
+ case 'update':
29
+ return 'Usage: kiipu post update --id <postId> --content "<text>" [--title "<title>"] [--visibility public|private] [--tags <a,b,c>]';
30
+ case 'star':
31
+ return 'Usage: kiipu post star --id <postId>';
32
+ case 'pin':
33
+ return 'Usage: kiipu post pin --id <postId>';
34
+ default:
35
+ return `Usage: kiipu post ${action} --id <postId>`;
36
+ }
37
+ }
38
+ function error(message) {
39
+ return { ok: false, message };
40
+ }
41
+ function getUserClient(config) {
42
+ const apiKey = config.apiKey ?? process.env.KIIPU_API_KEY ?? '';
43
+ if (!apiKey) {
44
+ return null;
45
+ }
46
+ return new KiipuUserApiClient({
47
+ apiBaseUrl: config.apiBaseUrl,
48
+ apiKey,
49
+ });
50
+ }
51
+ function requireUserClient(config) {
52
+ const client = getUserClient(config);
53
+ if (!client) {
18
54
  return {
19
- ok: false,
20
- message: `Usage: kiipu post ${input.action} --id <postId>`,
55
+ error: error('Kiipu API key is missing. Run `kiipu auth login` first.'),
21
56
  };
22
57
  }
23
- return executePostAction(config, {
24
- action: input.action,
25
- postId,
58
+ return { client };
59
+ }
60
+ function stripKnownFlags(args, flagsWithValues) {
61
+ const positional = [];
62
+ const flags = new Set(flagsWithValues);
63
+ for (let index = 0; index < args.length; index += 1) {
64
+ const arg = args[index];
65
+ if (!arg) {
66
+ continue;
67
+ }
68
+ if (flags.has(arg)) {
69
+ index += 1;
70
+ continue;
71
+ }
72
+ if (!arg.startsWith('--')) {
73
+ positional.push(arg);
74
+ }
75
+ }
76
+ return positional;
77
+ }
78
+ function validateSort(sort) {
79
+ if (!sort) {
80
+ return undefined;
81
+ }
82
+ return sortValues.has(sort) ? sort : null;
83
+ }
84
+ function parsePostId(args, action) {
85
+ const postId = readFlag(args, '--id')?.trim();
86
+ return postId ? postId : error(usage(action));
87
+ }
88
+ function parseTags(input) {
89
+ if (!input) {
90
+ return undefined;
91
+ }
92
+ const unique = new Map();
93
+ for (const chunk of input.split(',')) {
94
+ const cleaned = chunk.trim().replace(/^#+/, '').replace(/\s+/g, ' ');
95
+ if (!cleaned) {
96
+ continue;
97
+ }
98
+ const normalized = cleaned.toLowerCase();
99
+ if (!unique.has(normalized)) {
100
+ unique.set(normalized, cleaned);
101
+ }
102
+ if (unique.size === 8) {
103
+ break;
104
+ }
105
+ }
106
+ return Array.from(unique.values());
107
+ }
108
+ function toCliPost(post) {
109
+ return {
110
+ id: post.id,
111
+ title: post.title,
112
+ rawText: post.rawText,
113
+ finalText: post.finalText,
114
+ visibility: post.visibility,
115
+ tags: post.tags,
116
+ folder: post.folder ?? null,
117
+ isPinned: post.isPinned,
118
+ isStarred: post.isStarred,
119
+ createdAt: post.createdAt,
120
+ updatedAt: post.updatedAt,
121
+ };
122
+ }
123
+ async function handleMutationAction(config, action, args) {
124
+ if (action === 'create') {
125
+ const content = (readFlag(args, '--content') ?? stripKnownFlags(args, ['--content'])[0])?.trim();
126
+ if (!content) {
127
+ return error(usage('create'));
128
+ }
129
+ return executePostAction(config, { action: 'create', content });
130
+ }
131
+ const postId = parsePostId(args, action);
132
+ if (typeof postId !== 'string') {
133
+ return postId;
134
+ }
135
+ return executePostAction(config, { action, postId });
136
+ }
137
+ async function handleList(config, args) {
138
+ const { client, error: clientError } = requireUserClient(config);
139
+ if (!client) {
140
+ return clientError;
141
+ }
142
+ const sort = validateSort(readFlag(args, '--sort'));
143
+ if (sort === null) {
144
+ return error(`Invalid --sort value. ${usage('list')}`);
145
+ }
146
+ const starred = hasFlag(args, '--starred');
147
+ const deleted = hasFlag(args, '--deleted');
148
+ if (starred && deleted) {
149
+ return error(`--starred and --deleted cannot be used together.\n${usage('list')}`);
150
+ }
151
+ const tag = readFlag(args, '--tag');
152
+ let posts;
153
+ let responseData;
154
+ if (starred) {
155
+ const response = await client.listStarredPosts({ tag, sort });
156
+ if (!response.ok) {
157
+ return error(response.error.message);
158
+ }
159
+ posts = response.data.map((entry) => toCliPost(entry.post));
160
+ responseData = response.data;
161
+ }
162
+ else if (deleted) {
163
+ const response = await client.listDeletedPosts({ sort });
164
+ if (!response.ok) {
165
+ return error(response.error.message);
166
+ }
167
+ posts = response.data.map(toCliPost);
168
+ responseData = response.data;
169
+ }
170
+ else {
171
+ const response = await client.listPosts({ tag, sort });
172
+ if (!response.ok) {
173
+ return error(response.error.message);
174
+ }
175
+ posts = response.data.map(toCliPost);
176
+ responseData = response.data;
177
+ }
178
+ const title = starred ? 'Starred posts' : deleted ? 'Deleted posts' : 'Posts';
179
+ return {
180
+ ok: true,
181
+ message: formatPostCollection(title, posts),
182
+ data: responseData,
183
+ };
184
+ }
185
+ async function handleSearch(config, args) {
186
+ const { client, error: clientError } = requireUserClient(config);
187
+ if (!client) {
188
+ return clientError;
189
+ }
190
+ const query = (readFlag(args, '--query') ?? stripKnownFlags(args, ['--query'])[0])?.trim();
191
+ if (!query) {
192
+ return error(usage('search'));
193
+ }
194
+ const response = await client.searchPosts(query);
195
+ if (!response.ok) {
196
+ return error(response.error.message);
197
+ }
198
+ return {
199
+ ok: true,
200
+ message: formatPostCollection(`Search results for "${query}"`, response.data.map(toCliPost)),
201
+ data: response.data,
202
+ };
203
+ }
204
+ async function handleShow(config, args) {
205
+ const { client, error: clientError } = requireUserClient(config);
206
+ if (!client) {
207
+ return clientError;
208
+ }
209
+ const postId = parsePostId(args, 'show');
210
+ if (typeof postId !== 'string') {
211
+ return postId;
212
+ }
213
+ const response = await client.getPost(postId);
214
+ if (!response.ok) {
215
+ return error(response.error.message);
216
+ }
217
+ return {
218
+ ok: true,
219
+ message: formatPostDetail(toCliPost(response.data)),
220
+ data: response.data,
221
+ };
222
+ }
223
+ async function handleUpdate(config, args) {
224
+ const { client, error: clientError } = requireUserClient(config);
225
+ if (!client) {
226
+ return clientError;
227
+ }
228
+ const postId = parsePostId(args, 'update');
229
+ if (typeof postId !== 'string') {
230
+ return postId;
231
+ }
232
+ const content = readFlag(args, '--content')?.trim();
233
+ if (!content) {
234
+ return error(usage('update'));
235
+ }
236
+ const visibility = readFlag(args, '--visibility');
237
+ if (visibility && visibility !== 'public' && visibility !== 'private') {
238
+ return error(`Invalid --visibility value. ${usage('update')}`);
239
+ }
240
+ const title = readFlag(args, '--title');
241
+ const tags = readFlag(args, '--tags');
242
+ const response = await client.updatePost(postId, {
243
+ rawText: content,
244
+ ...(title !== undefined ? { title: title || null } : {}),
245
+ ...(visibility === 'public' || visibility === 'private' ? { visibility } : {}),
246
+ ...(tags ? { tags: parseTags(tags) } : {}),
26
247
  });
248
+ if (!response.ok) {
249
+ return error(response.error.message);
250
+ }
251
+ return {
252
+ ok: true,
253
+ message: `Post updated.\n\n${formatPostDetail(toCliPost(response.data))}`,
254
+ data: response.data,
255
+ };
256
+ }
257
+ async function handleStar(config, args) {
258
+ const { client, error: clientError } = requireUserClient(config);
259
+ if (!client) {
260
+ return clientError;
261
+ }
262
+ const postId = parsePostId(args, 'star');
263
+ if (typeof postId !== 'string') {
264
+ return postId;
265
+ }
266
+ const response = await client.toggleStar(postId);
267
+ if (!response.ok) {
268
+ return error(response.error.message);
269
+ }
270
+ return {
271
+ ok: true,
272
+ message: response.data.isStarred ? `Post starred. ${response.data.id}` : `Post unstarred. ${response.data.id}`,
273
+ data: response.data,
274
+ };
275
+ }
276
+ async function handlePin(config, args) {
277
+ const { client, error: clientError } = requireUserClient(config);
278
+ if (!client) {
279
+ return clientError;
280
+ }
281
+ const postId = parsePostId(args, 'pin');
282
+ if (typeof postId !== 'string') {
283
+ return postId;
284
+ }
285
+ const response = await client.togglePin(postId);
286
+ if (!response.ok) {
287
+ return error(response.error.message);
288
+ }
289
+ return {
290
+ ok: true,
291
+ message: response.data.isPinned ? `Post pinned. ${response.data.id}` : `Post unpinned. ${response.data.id}`,
292
+ data: response.data,
293
+ };
294
+ }
295
+ export async function runPostCommand(config, args) {
296
+ const action = args[1];
297
+ if (!action || !actions.has(action)) {
298
+ return error('Usage: kiipu post <create|delete|restore|purge|list|search|show|update|star|pin> [options]');
299
+ }
300
+ const actionArgs = args.slice(2);
301
+ switch (action) {
302
+ case 'create':
303
+ case 'delete':
304
+ case 'restore':
305
+ case 'purge':
306
+ return handleMutationAction(config, action, actionArgs);
307
+ case 'list':
308
+ return handleList(config, actionArgs);
309
+ case 'search':
310
+ return handleSearch(config, actionArgs);
311
+ case 'show':
312
+ return handleShow(config, actionArgs);
313
+ case 'update':
314
+ return handleUpdate(config, actionArgs);
315
+ case 'star':
316
+ return handleStar(config, actionArgs);
317
+ case 'pin':
318
+ return handlePin(config, actionArgs);
319
+ }
320
+ return error('Unsupported post action.');
27
321
  }
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
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';
@@ -10,7 +11,7 @@ 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);
@@ -22,27 +23,48 @@ function printResult(result, asJson) {
22
23
  async function main() {
23
24
  const args = process.argv.slice(2);
24
25
  const normalizedArgs = args[0] === '--' ? args.slice(1) : args;
26
+ const commandArgs = normalizedArgs.filter((arg) => arg !== '--json' && arg !== '--help' && arg !== '-h' && arg !== '--version' && arg !== '-v');
25
27
  const asJson = hasFlag(normalizedArgs, '--json');
26
28
  const wantsHelp = hasFlag(normalizedArgs, '--help') || hasFlag(normalizedArgs, '-h');
27
29
  const wantsVersion = hasFlag(normalizedArgs, '--version') || hasFlag(normalizedArgs, '-v');
30
+ const flagsWithValues = new Set([
31
+ '--scheduled-at',
32
+ '--package-root',
33
+ '--skills-dir',
34
+ '--text',
35
+ '--content',
36
+ '--id',
37
+ '--api-key',
38
+ '--device-name',
39
+ '--config-path',
40
+ '--wrapper-path',
41
+ '--conversation-id',
42
+ '--tag',
43
+ '--sort',
44
+ '--query',
45
+ '--title',
46
+ '--visibility',
47
+ '--tags',
48
+ '--question',
49
+ '--conversation-id',
50
+ '--top-k',
51
+ '--source-mode',
52
+ '--limit',
53
+ ]);
28
54
  const positionalArgs = normalizedArgs.filter((arg, index, all) => {
29
- if (arg === '--json' || arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v' || arg === '--no-browser')
55
+ if (arg === '--json' ||
56
+ arg === '--help' ||
57
+ arg === '-h' ||
58
+ arg === '--version' ||
59
+ arg === '-v' ||
60
+ arg === '--no-browser' ||
61
+ arg.startsWith('--')) {
30
62
  return false;
63
+ }
31
64
  const prev = all[index - 1];
32
- return (prev !== '--scheduled-at' &&
33
- prev !== '--package-root' &&
34
- prev !== '--skills-dir' &&
35
- prev !== '--text' &&
36
- prev !== '--content' &&
37
- prev !== '--id' &&
38
- prev !== '--api-key' &&
39
- prev !== '--device-name' &&
40
- prev !== '--config-path' &&
41
- prev !== '--wrapper-path' &&
42
- prev !== '--conversation-id' &&
43
- prev !== '--device-name');
65
+ return prev ? !flagsWithValues.has(prev) : true;
44
66
  });
45
- const [command, subcommand, subject] = positionalArgs;
67
+ const [command, subcommand] = positionalArgs;
46
68
  if (wantsVersion) {
47
69
  console.log(CLI_VERSION);
48
70
  return;
@@ -65,19 +87,14 @@ async function main() {
65
87
  return printResult(result, asJson);
66
88
  }
67
89
  if (command === 'post') {
68
- const action = subcommand;
69
- if (!action || !['create', 'delete', 'restore', 'purge'].includes(action)) {
70
- result = getHelpResult('post');
71
- return printResult(result, asJson);
72
- }
73
- result = await runPostCommand(config, {
74
- action,
75
- content: readFlag(normalizedArgs, '--content') ?? subject,
76
- postId: readFlag(normalizedArgs, '--id'),
90
+ result = await runPostCommand(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),
77
97
  });
78
- if (!result.ok) {
79
- return printResult(action === 'create' && !(readFlag(normalizedArgs, '--content') ?? subject) ? getHelpResult('post') : result, asJson);
80
- }
81
98
  return printResult(result, asJson);
82
99
  }
83
100
  if (command === 'auth') {