@senso-ai/cli 0.2.3 → 0.3.0

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.
Files changed (3) hide show
  1. package/README.md +53 -63
  2. package/dist/cli.js +249 -292
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -104,7 +104,7 @@ For non-interactive use (CI, agents), set the env var or pass the flag.
104
104
  ### Authentication
105
105
 
106
106
  ```
107
- senso login Save API key (interactive)
107
+ senso login Authenticate with Senso (interactive)
108
108
  senso logout Remove stored credentials
109
109
  senso whoami Show current org and auth status
110
110
  ```
@@ -112,9 +112,9 @@ senso whoami Show current org and auth status
112
112
  ### Search
113
113
 
114
114
  ```
115
- senso search <query> Semantic search with AI-generated answer
116
- senso search context <query> Semantic search chunks only
117
- senso search content <query> Semantic search — content IDs only
115
+ senso search <query> Semantic search with AI-generated answer + source chunks
116
+ senso search context <query> Chunks only (no AI answer, faster)
117
+ senso search content <query> Content IDs only (deduplicated)
118
118
  ```
119
119
 
120
120
  Options: `--max-results <n>`
@@ -122,111 +122,101 @@ Options: `--max-results <n>`
122
122
  ### Content
123
123
 
124
124
  ```
125
- senso content list List content items
126
- senso content get <id> Get content item by ID
127
- senso content delete <id> Delete content (local + external)
128
- senso content unpublish <id> Unpublish content
129
- senso content verification List content awaiting verification
125
+ senso content list List all knowledge base items
126
+ senso content get <id> Get full content detail by ID
127
+ senso content delete <id> Delete content (knowledge base + external)
128
+ senso content unpublish <id> Unpublish and revert to draft
129
+ senso content verification List items in verification workflow
130
130
  senso content reject <versionId> Reject a content version
131
- senso content restore <versionId> Restore a content version to draft
132
- senso content owners <id> List content owners
133
- senso content set-owners <id> Replace content owners
134
- senso content remove-owner <id> <userId> Remove content owner
131
+ senso content restore <versionId> Restore rejected version to draft
132
+ senso content owners <id> List owners of a content item
133
+ senso content set-owners <id> Replace owners (--user-ids)
134
+ senso content remove-owner <id> <userId> Remove a single owner
135
135
  ```
136
136
 
137
+ Options: `content list` supports `--limit`, `--offset`, `--search`, `--sort`. `content verification` supports `--limit`, `--offset`, `--search`, `--status`. `content reject` supports `--reason`.
138
+
137
139
  ### Content Generation
138
140
 
139
141
  ```
140
- senso generate settings Get content generation settings
141
- senso generate update-settings Update content generation settings
142
- senso generate sample Generate ad hoc content sample
143
- senso generate run Trigger content engine run
142
+ senso generate settings Get generation settings
143
+ senso generate update-settings Update generation settings (--data)
144
+ senso generate sample Generate sample for a prompt (--prompt-id, --content-type-id)
145
+ senso generate run Trigger a content engine run (--prompt-ids)
144
146
  ```
145
147
 
146
148
  ### Content Engine
147
149
 
148
150
  ```
149
- senso engine publish Publish content via content engine
150
- senso engine draft Save content as draft
151
+ senso engine publish Publish content to external destinations (--data)
152
+ senso engine draft Save content as draft for review (--data)
151
153
  ```
152
154
 
153
155
  ### Ingestion
154
156
 
155
157
  ```
156
- senso ingest upload Request presigned S3 upload URLs
157
- senso ingest reprocess <contentId> Re-ingest existing content
158
- ```
159
-
160
- ### Categories & Topics
161
-
162
- ```
163
- senso categories list List categories
164
- senso categories list-all List all categories with their topics
165
- senso categories create <name> Create a category
166
- senso categories get <id> Get category by ID
167
- senso categories update <id> Update a category
168
- senso categories delete <id> Delete a category
169
- senso categories batch-create Batch create categories with topics
170
-
171
- senso topics list <categoryId> List topics in a category
172
- senso topics create <categoryId> Create a topic
173
- senso topics get <catId> <topicId> Get a topic
174
- senso topics update <catId> <topicId> Update a topic
175
- senso topics delete <catId> <topicId> Delete a topic
176
- senso topics batch-create <catId> Batch create topics
158
+ senso ingest upload <files...> Upload files to knowledge base (up to 10)
159
+ senso ingest reprocess <contentId> <file> Re-ingest content with new file
177
160
  ```
178
161
 
179
162
  ### Brand Kit & Content Types
180
163
 
181
164
  ```
182
- senso brand-kit get Get brand kit
183
- senso brand-kit set Upsert brand kit
165
+ senso brand-kit get Get brand kit guidelines
166
+ senso brand-kit set Create or replace brand kit (--data)
184
167
 
185
168
  senso content-types list List content types
186
- senso content-types create Create a content type
187
- senso content-types get <id> Get content type
188
- senso content-types update <id> Update content type
189
- senso content-types delete <id> Delete content type
169
+ senso content-types create Create a content type (--data)
170
+ senso content-types get <id> Get content type by ID
171
+ senso content-types update <id> Update a content type (--data)
172
+ senso content-types delete <id> Delete a content type
190
173
  ```
191
174
 
175
+ Options: `content-types list` supports `--limit`, `--offset`.
176
+
192
177
  ### Prompts
193
178
 
194
179
  ```
195
- senso prompts list List prompts
196
- senso prompts create Create a prompt
197
- senso prompts get <promptId> Get prompt with full run history
180
+ senso prompts list List prompts (geo questions)
181
+ senso prompts create Create a prompt (--data)
182
+ senso prompts get <promptId> Get prompt with run history
198
183
  senso prompts delete <promptId> Delete a prompt
199
184
  ```
200
185
 
186
+ Options: `prompts list` supports `--limit`, `--offset`, `--search`, `--sort`.
187
+
201
188
  ### Organization
202
189
 
203
190
  ```
204
191
  senso org get Get organization details
205
- senso org update Update organization details
192
+ senso org update Update organization details (--data)
206
193
 
207
194
  senso users list List users
208
- senso users add Add a user
209
- senso users get <userId> Get a user
210
- senso users update <userId> Update a user's role
195
+ senso users add Add a user (--data)
196
+ senso users get <userId> Get user details
197
+ senso users update <userId> Update a user's role (--data)
211
198
  senso users remove <userId> Remove a user
199
+ senso users set-current <userId> Set org as current for a user
212
200
 
213
201
  senso api-keys list List API keys
214
- senso api-keys create Create API key
202
+ senso api-keys create Create API key (--data)
215
203
  senso api-keys get <keyId> Get API key details
216
- senso api-keys update <keyId> Update API key
204
+ senso api-keys update <keyId> Update API key (--data)
217
205
  senso api-keys delete <keyId> Delete API key
218
206
  senso api-keys revoke <keyId> Revoke API key
219
207
 
220
208
  senso members list List organization members
221
209
  ```
222
210
 
211
+ Options: `users list` supports `--limit`, `--offset`. `api-keys list` supports `--limit`, `--offset`. `members list` supports `--limit`, `--offset`, `--search`, `--sort`.
212
+
223
213
  ### Run Configuration
224
214
 
225
215
  ```
226
216
  senso run-config models Get configured AI models
227
- senso run-config set-models Set AI models
228
- senso run-config schedule Get run schedule
229
- senso run-config set-schedule Set run schedule
217
+ senso run-config set-models Set AI models (--data)
218
+ senso run-config schedule Get run schedule (days of week)
219
+ senso run-config set-schedule Set run schedule (--data)
230
220
  ```
231
221
 
232
222
  ### Notifications
@@ -236,6 +226,8 @@ senso notifications list List notifications
236
226
  senso notifications read <id> Mark notification as read
237
227
  ```
238
228
 
229
+ Options: `notifications list` supports `--limit`, `--offset`, `--unread-only`.
230
+
239
231
  ### CLI Management
240
232
 
241
233
  ```
@@ -352,20 +344,18 @@ npm test
352
344
  ```
353
345
  src/
354
346
  ├── cli.ts # Entry point — arg parsing, command dispatch
355
- ├── commands/ # One file per command group (18 files)
347
+ ├── commands/ # One file per command group (16 files)
356
348
  │ ├── auth.ts # login, logout, whoami
357
349
  │ ├── search.ts # search, search context, search content
358
350
  │ ├── content.ts # CRUD + verification + owners
359
351
  │ ├── generate.ts # content generation settings + triggers
360
352
  │ ├── engine.ts # publish, draft
361
- │ ├── ingest.ts # upload, reprocess
362
- │ ├── categories.ts # CRUD + batch
363
- │ ├── topics.ts # CRUD + batch
353
+ │ ├── ingest.ts # upload, reprocess (with S3 upload)
364
354
  │ ├── brand-kit.ts # get, set
365
355
  │ ├── content-types.ts # CRUD
366
356
  │ ├── prompts.ts # CRUD
367
357
  │ ├── org.ts # get, update
368
- │ ├── users.ts # CRUD
358
+ │ ├── users.ts # CRUD + set-current
369
359
  │ ├── api-keys.ts # CRUD + revoke
370
360
  │ ├── members.ts # list
371
361
  │ ├── run-config.ts # models, schedule
package/dist/cli.js CHANGED
@@ -211,17 +211,23 @@ async function apiRequest(opts) {
211
211
  });
212
212
  if (!res.ok) {
213
213
  let body;
214
+ const text3 = await res.text();
214
215
  try {
215
- body = await res.json();
216
+ body = JSON.parse(text3);
216
217
  } catch {
217
- body = await res.text();
218
+ body = text3;
218
219
  }
219
220
  throw new ApiError(res.status, res.statusText, body);
220
221
  }
221
222
  if (res.status === 204) {
222
223
  return void 0;
223
224
  }
224
- return await res.json();
225
+ const text2 = await res.text();
226
+ try {
227
+ return JSON.parse(text2);
228
+ } catch {
229
+ throw new Error(`Invalid JSON response from ${opts.path}`);
230
+ }
225
231
  } finally {
226
232
  clearTimeout(timeout);
227
233
  }
@@ -280,7 +286,7 @@ async function verifyApiKey(apiKey, baseUrl) {
280
286
  });
281
287
  }
282
288
  function registerAuthCommands(program2) {
283
- program2.command("login").description("Save API key to config (validates via GET /org/me)").action(async () => {
289
+ program2.command("login").description("Authenticate with Senso. Paste your API key and it will be validated against your organization, then stored locally.").action(async () => {
284
290
  const opts = program2.opts();
285
291
  banner();
286
292
  console.log(` ${pc3.bold("Welcome to Senso CLI!")}
@@ -322,11 +328,11 @@ function registerAuthCommands(program2) {
322
328
  process.exit(1);
323
329
  }
324
330
  });
325
- program2.command("logout").description("Remove stored credentials").action(() => {
331
+ program2.command("logout").description("Remove stored API key and organization info from local config.").action(() => {
326
332
  clearConfig();
327
333
  success("Credentials removed.");
328
334
  });
329
- program2.command("whoami").description("Show current auth status and org info").action(async () => {
335
+ program2.command("whoami").description("Show which organization you are authenticated as, including org ID, slug, tier, and API key prefix.").action(async () => {
330
336
  const opts = program2.opts();
331
337
  const apiKey = getApiKey({ apiKey: opts.apiKey });
332
338
  if (!apiKey) {
@@ -372,8 +378,8 @@ function registerAuthCommands(program2) {
372
378
 
373
379
  // src/commands/org.ts
374
380
  function registerOrgCommands(program2) {
375
- const org = program2.command("org").description("Organization management");
376
- org.command("get").description("Get organization details").action(async () => {
381
+ const org = program2.command("org").description("View and update organization profile and settings. Includes name, slug, logo, websites, locations, and tier information.");
382
+ org.command("get").description("Get full organization details including name, slug, tier, websites, locations, configured AI models, publishers, and schedule.").action(async () => {
377
383
  const opts = program2.opts();
378
384
  try {
379
385
  const data = await apiRequest({ path: "/org/me", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -383,12 +389,12 @@ function registerOrgCommands(program2) {
383
389
  process.exit(1);
384
390
  }
385
391
  });
386
- org.command("update").description("Update organization details").requiredOption("--data <json>", "JSON org fields to update").action(async (cmdOpts) => {
392
+ org.command("update").description("Update organization details. All fields are optional \u2014 only provided fields are changed. Pass an empty array for websites/locations to clear them.").requiredOption("--data <json>", 'JSON: { "name": "...", "slug": "...", "logo_url": "...", "websites": [...], "locations": [...] }').action(async (cmdOpts) => {
387
393
  const opts = program2.opts();
388
394
  try {
389
395
  const body = JSON.parse(cmdOpts.data);
390
396
  const data = await apiRequest({
391
- method: "PATCH",
397
+ method: "PUT",
392
398
  path: "/org/me",
393
399
  body,
394
400
  apiKey: opts.apiKey,
@@ -405,18 +411,23 @@ function registerOrgCommands(program2) {
405
411
 
406
412
  // src/commands/users.ts
407
413
  function registerUserCommands(program2) {
408
- const users = program2.command("users").description("Manage users in organization");
409
- users.command("list").description("List users in organization").action(async () => {
414
+ const users = program2.command("users").description("Manage users within the organization. Add, update roles, remove users, or set the active organization for a user.");
415
+ users.command("list").description("List all users in the organization. Returns user IDs, roles, and membership status.").option("--limit <n>", "Maximum number of users to return").option("--offset <n>", "Number of users to skip (for pagination)").action(async (cmdOpts) => {
410
416
  const opts = program2.opts();
411
417
  try {
412
- const data = await apiRequest({ path: "/org/users", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
418
+ const data = await apiRequest({
419
+ path: "/org/users",
420
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
421
+ apiKey: opts.apiKey,
422
+ baseUrl: opts.baseUrl
423
+ });
413
424
  console.log(JSON.stringify(data, null, 2));
414
425
  } catch (err) {
415
426
  error(formatApiError(err));
416
427
  process.exit(1);
417
428
  }
418
429
  });
419
- users.command("add").description("Add user to organization").requiredOption("--data <json>", "JSON user data").action(async (cmdOpts) => {
430
+ users.command("add").description("Add an existing platform user to the organization. Requires user_id and role_id.").requiredOption("--data <json>", 'JSON: { "user_id": "uuid", "role_id": "uuid", "is_current": false }').action(async (cmdOpts) => {
420
431
  const opts = program2.opts();
421
432
  try {
422
433
  const body = JSON.parse(cmdOpts.data);
@@ -434,7 +445,7 @@ function registerUserCommands(program2) {
434
445
  process.exit(1);
435
446
  }
436
447
  });
437
- users.command("get <userId>").description("Get a user in the organization").action(async (userId) => {
448
+ users.command("get <userId>").description("Get a user's details including their role and membership status in the organization.").action(async (userId) => {
438
449
  const opts = program2.opts();
439
450
  try {
440
451
  const data = await apiRequest({ path: `/org/users/${userId}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -444,12 +455,12 @@ function registerUserCommands(program2) {
444
455
  process.exit(1);
445
456
  }
446
457
  });
447
- users.command("update <userId>").description("Update a user's role").requiredOption("--data <json>", "JSON user update data").action(async (userId, cmdOpts) => {
458
+ users.command("update <userId>").description("Update a user's role in the organization. Requires role_id in the JSON body.").requiredOption("--data <json>", 'JSON: { "role_id": "uuid", "is_current": true }').action(async (userId, cmdOpts) => {
448
459
  const opts = program2.opts();
449
460
  try {
450
461
  const body = JSON.parse(cmdOpts.data);
451
462
  const data = await apiRequest({
452
- method: "PATCH",
463
+ method: "PUT",
453
464
  path: `/org/users/${userId}`,
454
465
  body,
455
466
  apiKey: opts.apiKey,
@@ -462,7 +473,7 @@ function registerUserCommands(program2) {
462
473
  process.exit(1);
463
474
  }
464
475
  });
465
- users.command("remove <userId>").description("Remove a user from the organization").action(async (userId) => {
476
+ users.command("remove <userId>").description("Remove a user from the organization. This does not delete the platform user account.").action(async (userId) => {
466
477
  const opts = program2.opts();
467
478
  try {
468
479
  await apiRequest({ method: "DELETE", path: `/org/users/${userId}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -472,82 +483,17 @@ function registerUserCommands(program2) {
472
483
  process.exit(1);
473
484
  }
474
485
  });
475
- }
476
-
477
- // src/commands/api-keys.ts
478
- function registerApiKeyCommands(program2) {
479
- const keys = program2.command("api-keys").description("Manage API keys");
480
- keys.command("list").description("List API keys").action(async () => {
481
- const opts = program2.opts();
482
- try {
483
- const data = await apiRequest({ path: "/org/api-keys", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
484
- console.log(JSON.stringify(data, null, 2));
485
- } catch (err) {
486
- error(formatApiError(err));
487
- process.exit(1);
488
- }
489
- });
490
- keys.command("create").description("Create API key").requiredOption("--data <json>", "JSON key configuration").action(async (cmdOpts) => {
486
+ users.command("set-current <userId>").description("Set this organization as the current (active) organization for a user.").action(async (userId) => {
491
487
  const opts = program2.opts();
492
488
  try {
493
- const body = JSON.parse(cmdOpts.data);
494
- const data = await apiRequest({
495
- method: "POST",
496
- path: "/org/api-keys",
497
- body,
498
- apiKey: opts.apiKey,
499
- baseUrl: opts.baseUrl
500
- });
501
- success("API key created.");
502
- console.log(JSON.stringify(data, null, 2));
503
- } catch (err) {
504
- error(err instanceof SyntaxError ? "Invalid JSON in --data" : formatApiError(err));
505
- process.exit(1);
506
- }
507
- });
508
- keys.command("get <keyId>").description("Get API key details").action(async (keyId) => {
509
- const opts = program2.opts();
510
- try {
511
- const data = await apiRequest({ path: `/org/api-keys/${keyId}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
512
- console.log(JSON.stringify(data, null, 2));
513
- } catch (err) {
514
- error(formatApiError(err));
515
- process.exit(1);
516
- }
517
- });
518
- keys.command("update <keyId>").description("Update API key").requiredOption("--data <json>", "JSON key updates").action(async (keyId, cmdOpts) => {
519
- const opts = program2.opts();
520
- try {
521
- const body = JSON.parse(cmdOpts.data);
522
- const data = await apiRequest({
489
+ await apiRequest({
523
490
  method: "PATCH",
524
- path: `/org/api-keys/${keyId}`,
525
- body,
491
+ path: `/org/users/${userId}/current`,
492
+ body: { is_current: true },
526
493
  apiKey: opts.apiKey,
527
494
  baseUrl: opts.baseUrl
528
495
  });
529
- success(`API key ${keyId} updated.`);
530
- console.log(JSON.stringify(data, null, 2));
531
- } catch (err) {
532
- error(err instanceof SyntaxError ? "Invalid JSON in --data" : formatApiError(err));
533
- process.exit(1);
534
- }
535
- });
536
- keys.command("delete <keyId>").description("Delete API key").action(async (keyId) => {
537
- const opts = program2.opts();
538
- try {
539
- await apiRequest({ method: "DELETE", path: `/org/api-keys/${keyId}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
540
- success(`API key ${keyId} deleted.`);
541
- } catch (err) {
542
- error(formatApiError(err));
543
- process.exit(1);
544
- }
545
- });
546
- keys.command("revoke <keyId>").description("Revoke API key").action(async (keyId) => {
547
- const opts = program2.opts();
548
- try {
549
- await apiRequest({ method: "POST", path: `/org/api-keys/${keyId}/revoke`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
550
- success(`API key ${keyId} revoked.`);
496
+ success(`Organization set as current for user ${userId}.`);
551
497
  } catch (err) {
552
498
  error(formatApiError(err));
553
499
  process.exit(1);
@@ -555,201 +501,90 @@ function registerApiKeyCommands(program2) {
555
501
  });
556
502
  }
557
503
 
558
- // src/commands/categories.ts
559
- function registerCategoryCommands(program2) {
560
- const cat = program2.command("categories").description("Manage categories");
561
- cat.command("list").description("List categories").action(async () => {
562
- const opts = program2.opts();
563
- try {
564
- const data = await apiRequest({ path: "/org/categories", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
565
- console.log(JSON.stringify(data, null, 2));
566
- } catch (err) {
567
- error(formatApiError(err));
568
- process.exit(1);
569
- }
570
- });
571
- cat.command("list-all").description("List all categories with their topics").action(async () => {
572
- const opts = program2.opts();
573
- try {
574
- const data = await apiRequest({ path: "/org/categories/all", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
575
- console.log(JSON.stringify(data, null, 2));
576
- } catch (err) {
577
- error(formatApiError(err));
578
- process.exit(1);
579
- }
580
- });
581
- cat.command("create <name>").description("Create a category").action(async (name) => {
582
- const opts = program2.opts();
583
- try {
584
- const data = await apiRequest({
585
- method: "POST",
586
- path: "/org/categories",
587
- body: { name },
588
- apiKey: opts.apiKey,
589
- baseUrl: opts.baseUrl
590
- });
591
- success(`Category "${name}" created.`);
592
- console.log(JSON.stringify(data, null, 2));
593
- } catch (err) {
594
- error(formatApiError(err));
595
- process.exit(1);
596
- }
597
- });
598
- cat.command("get <id>").description("Get category by ID").action(async (id) => {
599
- const opts = program2.opts();
600
- try {
601
- const data = await apiRequest({ path: `/org/categories/${id}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
602
- console.log(JSON.stringify(data, null, 2));
603
- } catch (err) {
604
- error(formatApiError(err));
605
- process.exit(1);
606
- }
607
- });
608
- cat.command("update <id>").description("Update a category").requiredOption("--name <name>", "New category name").action(async (id, cmdOpts) => {
504
+ // src/commands/api-keys.ts
505
+ function registerApiKeyCommands(program2) {
506
+ const keys = program2.command("api-keys").description("Manage org-scoped API keys. Create, rotate, revoke, or list API keys used to authenticate with the Senso API.");
507
+ keys.command("list").description("List all API keys for the organization. Shows name, expiry, revocation status, and last usage.").option("--limit <n>", "Maximum number of keys to return").option("--offset <n>", "Number of keys to skip (for pagination)").action(async (cmdOpts) => {
609
508
  const opts = program2.opts();
610
509
  try {
611
510
  const data = await apiRequest({
612
- method: "PATCH",
613
- path: `/org/categories/${id}`,
614
- body: { name: cmdOpts.name },
511
+ path: "/org/api-keys",
512
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
615
513
  apiKey: opts.apiKey,
616
514
  baseUrl: opts.baseUrl
617
515
  });
618
- success(`Category ${id} updated.`);
619
516
  console.log(JSON.stringify(data, null, 2));
620
517
  } catch (err) {
621
518
  error(formatApiError(err));
622
519
  process.exit(1);
623
520
  }
624
521
  });
625
- cat.command("delete <id>").description("Delete a category").action(async (id) => {
626
- const opts = program2.opts();
627
- try {
628
- await apiRequest({ method: "DELETE", path: `/org/categories/${id}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
629
- success(`Category ${id} deleted.`);
630
- } catch (err) {
631
- error(formatApiError(err));
632
- process.exit(1);
633
- }
634
- });
635
- cat.command("batch-create").description("Batch create categories with topics").requiredOption("--data <json>", "JSON array of categories with topics").action(async (cmdOpts) => {
522
+ keys.command("create").description("Create a new API key. The key value is returned only once \u2014 store it securely.").requiredOption("--data <json>", 'JSON: { "name": "my-key", "expires_at": "2025-12-31T00:00:00Z" }').action(async (cmdOpts) => {
636
523
  const opts = program2.opts();
637
524
  try {
638
525
  const body = JSON.parse(cmdOpts.data);
639
526
  const data = await apiRequest({
640
527
  method: "POST",
641
- path: "/org/categories/batch",
528
+ path: "/org/api-keys",
642
529
  body,
643
530
  apiKey: opts.apiKey,
644
531
  baseUrl: opts.baseUrl
645
532
  });
646
- success("Batch create completed.");
533
+ success("API key created.");
647
534
  console.log(JSON.stringify(data, null, 2));
648
535
  } catch (err) {
649
536
  error(err instanceof SyntaxError ? "Invalid JSON in --data" : formatApiError(err));
650
537
  process.exit(1);
651
538
  }
652
539
  });
653
- }
654
-
655
- // src/commands/topics.ts
656
- function registerTopicCommands(program2) {
657
- const topics = program2.command("topics").description("Manage topics within categories");
658
- topics.command("list <categoryId>").description("List topics for a category").action(async (categoryId) => {
659
- const opts = program2.opts();
660
- try {
661
- const data = await apiRequest({
662
- path: `/org/categories/${categoryId}/topics`,
663
- apiKey: opts.apiKey,
664
- baseUrl: opts.baseUrl
665
- });
666
- console.log(JSON.stringify(data, null, 2));
667
- } catch (err) {
668
- error(formatApiError(err));
669
- process.exit(1);
670
- }
671
- });
672
- topics.command("create <categoryId>").description("Create topic in category").requiredOption("--name <name>", "Topic name").action(async (categoryId, cmdOpts) => {
540
+ keys.command("get <keyId>").description("Get details for a specific API key including name, expiry, and last used timestamp.").action(async (keyId) => {
673
541
  const opts = program2.opts();
674
542
  try {
675
- const data = await apiRequest({
676
- method: "POST",
677
- path: `/org/categories/${categoryId}/topics`,
678
- body: { name: cmdOpts.name },
679
- apiKey: opts.apiKey,
680
- baseUrl: opts.baseUrl
681
- });
682
- success(`Topic "${cmdOpts.name}" created.`);
543
+ const data = await apiRequest({ path: `/org/api-keys/${keyId}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
683
544
  console.log(JSON.stringify(data, null, 2));
684
545
  } catch (err) {
685
546
  error(formatApiError(err));
686
547
  process.exit(1);
687
548
  }
688
549
  });
689
- topics.command("get <categoryId> <topicId>").description("Get topic by ID").action(async (categoryId, topicId) => {
550
+ keys.command("update <keyId>").description("Update an API key's name or expiry date.").requiredOption("--data <json>", 'JSON: { "name": "new-name", "expires_at": "2026-06-01T00:00:00Z" }').action(async (keyId, cmdOpts) => {
690
551
  const opts = program2.opts();
691
552
  try {
553
+ const body = JSON.parse(cmdOpts.data);
692
554
  const data = await apiRequest({
693
- path: `/org/categories/${categoryId}/topics/${topicId}`,
555
+ method: "PUT",
556
+ path: `/org/api-keys/${keyId}`,
557
+ body,
694
558
  apiKey: opts.apiKey,
695
559
  baseUrl: opts.baseUrl
696
560
  });
561
+ success(`API key ${keyId} updated.`);
697
562
  console.log(JSON.stringify(data, null, 2));
698
563
  } catch (err) {
699
- error(formatApiError(err));
564
+ error(err instanceof SyntaxError ? "Invalid JSON in --data" : formatApiError(err));
700
565
  process.exit(1);
701
566
  }
702
567
  });
703
- topics.command("update <categoryId> <topicId>").description("Update a topic").requiredOption("--name <name>", "New topic name").action(async (categoryId, topicId, cmdOpts) => {
568
+ keys.command("delete <keyId>").description("Permanently delete an API key. This cannot be undone.").action(async (keyId) => {
704
569
  const opts = program2.opts();
705
570
  try {
706
- const data = await apiRequest({
707
- method: "PATCH",
708
- path: `/org/categories/${categoryId}/topics/${topicId}`,
709
- body: { name: cmdOpts.name },
710
- apiKey: opts.apiKey,
711
- baseUrl: opts.baseUrl
712
- });
713
- success(`Topic ${topicId} updated.`);
714
- console.log(JSON.stringify(data, null, 2));
571
+ await apiRequest({ method: "DELETE", path: `/org/api-keys/${keyId}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
572
+ success(`API key ${keyId} deleted.`);
715
573
  } catch (err) {
716
574
  error(formatApiError(err));
717
575
  process.exit(1);
718
576
  }
719
577
  });
720
- topics.command("delete <categoryId> <topicId>").description("Delete a topic").action(async (categoryId, topicId) => {
578
+ keys.command("revoke <keyId>").description("Revoke an API key. The key remains visible but can no longer be used for authentication.").action(async (keyId) => {
721
579
  const opts = program2.opts();
722
580
  try {
723
- await apiRequest({
724
- method: "DELETE",
725
- path: `/org/categories/${categoryId}/topics/${topicId}`,
726
- apiKey: opts.apiKey,
727
- baseUrl: opts.baseUrl
728
- });
729
- success(`Topic ${topicId} deleted.`);
581
+ await apiRequest({ method: "POST", path: `/org/api-keys/${keyId}/revoke`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
582
+ success(`API key ${keyId} revoked.`);
730
583
  } catch (err) {
731
584
  error(formatApiError(err));
732
585
  process.exit(1);
733
586
  }
734
587
  });
735
- topics.command("batch-create <categoryId>").description("Batch create topics in a category").requiredOption("--data <json>", "JSON array of topics").action(async (categoryId, cmdOpts) => {
736
- const opts = program2.opts();
737
- try {
738
- const body = JSON.parse(cmdOpts.data);
739
- const data = await apiRequest({
740
- method: "POST",
741
- path: `/org/categories/${categoryId}/topics/batch`,
742
- body,
743
- apiKey: opts.apiKey,
744
- baseUrl: opts.baseUrl
745
- });
746
- success("Batch create completed.");
747
- console.log(JSON.stringify(data, null, 2));
748
- } catch (err) {
749
- error(err instanceof SyntaxError ? "Invalid JSON in --data" : formatApiError(err));
750
- process.exit(1);
751
- }
752
- });
753
588
  }
754
589
 
755
590
  // src/commands/search.ts
@@ -807,7 +642,7 @@ function output(format, data) {
807
642
 
808
643
  // src/commands/search.ts
809
644
  function registerSearchCommands(program2) {
810
- const search = program2.command("search").description("Semantic search over your knowledge base");
645
+ const search = program2.command("search").description("Search the knowledge base with natural language queries. Returns AI-generated answers synthesised from matching content chunks, or raw chunks/content IDs.");
811
646
  search.argument("<query>", "Search query").option("--max-results <n>", "Maximum number of results", "5").action(async (query, cmdOpts) => {
812
647
  const opts = program2.opts();
813
648
  try {
@@ -847,7 +682,7 @@ function registerSearchCommands(program2) {
847
682
  process.exit(1);
848
683
  }
849
684
  });
850
- search.command("context <query>").description("Semantic search \u2014 chunks only").option("--max-results <n>", "Maximum results", "5").action(async (query, cmdOpts) => {
685
+ search.command("context <query>").description("Search the knowledge base \u2014 returns matching content chunks only, without AI answer generation. Faster than full search.").option("--max-results <n>", "Maximum results", "5").action(async (query, cmdOpts) => {
851
686
  const opts = program2.opts();
852
687
  try {
853
688
  const data = await apiRequest({
@@ -863,7 +698,7 @@ function registerSearchCommands(program2) {
863
698
  process.exit(1);
864
699
  }
865
700
  });
866
- search.command("content <query>").description("Semantic search \u2014 content IDs only").option("--max-results <n>", "Maximum results", "5").action(async (query, cmdOpts) => {
701
+ search.command("content <query>").description("Search the knowledge base \u2014 returns deduplicated content IDs and titles only. No chunks or AI answer.").option("--max-results <n>", "Maximum results", "5").action(async (query, cmdOpts) => {
867
702
  const opts = program2.opts();
868
703
  try {
869
704
  const data = await apiRequest({
@@ -889,34 +724,119 @@ function outputByFormat(format, data) {
889
724
  }
890
725
 
891
726
  // src/commands/ingest.ts
727
+ import { createHash } from "crypto";
728
+ import { readFile, stat } from "fs/promises";
729
+ import { basename, resolve } from "path";
730
+ var MIME_TYPES = {
731
+ ".pdf": "application/pdf",
732
+ ".txt": "text/plain",
733
+ ".csv": "text/csv",
734
+ ".doc": "application/msword",
735
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
736
+ ".xls": "application/vnd.ms-excel",
737
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
738
+ ".ppt": "application/vnd.ms-powerpoint",
739
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
740
+ ".html": "text/html",
741
+ ".htm": "text/html",
742
+ ".md": "text/markdown",
743
+ ".json": "application/json",
744
+ ".xml": "application/xml"
745
+ };
746
+ function getMimeType(filename) {
747
+ const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
748
+ return MIME_TYPES[ext] || "application/octet-stream";
749
+ }
750
+ async function getFileMetadata(filePath) {
751
+ const absPath = resolve(filePath);
752
+ const buffer = await readFile(absPath);
753
+ const stats = await stat(absPath);
754
+ const hash = createHash("md5").update(buffer).digest("hex");
755
+ return {
756
+ meta: {
757
+ filename: basename(absPath),
758
+ file_size_bytes: stats.size,
759
+ content_type: getMimeType(basename(absPath)),
760
+ content_hash_md5: hash
761
+ },
762
+ buffer
763
+ };
764
+ }
765
+ async function uploadToS3(url, buffer, contentType) {
766
+ const res = await fetch(url, {
767
+ method: "PUT",
768
+ headers: { "Content-Type": contentType },
769
+ body: buffer
770
+ });
771
+ if (!res.ok) {
772
+ throw new Error(`S3 upload failed: ${res.status} ${res.statusText}`);
773
+ }
774
+ }
892
775
  function registerIngestCommands(program2) {
893
- const ingest = program2.command("ingest").description("Content ingestion (upload, reprocess)");
894
- ingest.command("upload").description("Request presigned S3 upload URLs").requiredOption("--files <filenames...>", "File names to get upload URLs for").action(async (cmdOpts) => {
776
+ const ingest = program2.command("ingest").description("Ingest files into the knowledge base. Upload documents (PDF, TXT, DOCX, etc.) to be parsed, chunked, and embedded for semantic search.");
777
+ ingest.command("upload <files...>").description("Upload files to the knowledge base. Accepts local file paths (up to 10). Files are hashed, uploaded to S3, then parsed and embedded by a background worker.").action(async (files) => {
895
778
  const opts = program2.opts();
779
+ if (files.length > 10) {
780
+ error("Maximum 10 files per upload request.");
781
+ process.exit(1);
782
+ }
896
783
  try {
897
- const data = await apiRequest({
784
+ const fileData = await Promise.all(files.map(getFileMetadata));
785
+ const results = await apiRequest({
898
786
  method: "POST",
899
787
  path: "/org/ingestion/upload",
900
- body: { files: cmdOpts.files },
788
+ body: { files: fileData.map((f) => f.meta) },
901
789
  apiKey: opts.apiKey,
902
790
  baseUrl: opts.baseUrl
903
791
  });
904
- console.log(JSON.stringify(data, null, 2));
792
+ const items = Array.isArray(results) ? results : [];
793
+ let uploaded = 0;
794
+ for (const item of items) {
795
+ if (item.status === "upload_pending" && item.upload_url) {
796
+ const match = fileData.find((f) => f.meta.filename === item.filename);
797
+ if (match) {
798
+ await uploadToS3(item.upload_url, match.buffer, match.meta.content_type);
799
+ uploaded++;
800
+ success(`Uploaded ${item.filename} (content_id: ${item.content_id})`);
801
+ }
802
+ } else {
803
+ warn(`Skipped ${item.filename}: ${item.status}${item.message ? ` \u2014 ${item.message}` : ""}`);
804
+ }
805
+ }
806
+ if (uploaded > 0) {
807
+ success(`${uploaded} file(s) uploaded. Background processing will parse, chunk, and embed them.`);
808
+ }
809
+ if (opts.output === "json") {
810
+ console.log(JSON.stringify(results, null, 2));
811
+ }
905
812
  } catch (err) {
906
813
  error(formatApiError(err));
907
814
  process.exit(1);
908
815
  }
909
816
  });
910
- ingest.command("reprocess <contentId>").description("Request re-ingestion of existing content").action(async (contentId) => {
817
+ ingest.command("reprocess <contentId> <file>").description("Re-ingest an existing content item with a new file version. Provide the content ID and the path to the replacement file.").action(async (contentId, file) => {
911
818
  const opts = program2.opts();
912
819
  try {
913
- await apiRequest({
914
- method: "POST",
915
- path: `/org/ingestion/${contentId}/reprocess`,
820
+ const { meta, buffer } = await getFileMetadata(file);
821
+ const results = await apiRequest({
822
+ method: "PUT",
823
+ path: `/org/ingestion/content/${contentId}`,
824
+ body: { file: meta },
916
825
  apiKey: opts.apiKey,
917
826
  baseUrl: opts.baseUrl
918
827
  });
919
- success(`Reprocess triggered for content ${contentId}.`);
828
+ const items = Array.isArray(results) ? results : [];
829
+ for (const item of items) {
830
+ if (item.status === "upload_pending" && item.upload_url) {
831
+ await uploadToS3(item.upload_url, buffer, meta.content_type);
832
+ success(`Uploaded ${meta.filename} for content ${contentId}. Background re-processing started.`);
833
+ } else {
834
+ warn(`Skipped: ${item.status}${item.message ? ` \u2014 ${item.message}` : ""}`);
835
+ }
836
+ }
837
+ if (opts.output === "json") {
838
+ console.log(JSON.stringify(results, null, 2));
839
+ }
920
840
  } catch (err) {
921
841
  error(formatApiError(err));
922
842
  process.exit(1);
@@ -927,13 +847,13 @@ function registerIngestCommands(program2) {
927
847
  // src/commands/content.ts
928
848
  import pc6 from "picocolors";
929
849
  function registerContentCommands(program2) {
930
- const content = program2.command("content").description("Manage content items");
931
- content.command("list").description("List content items").option("--limit <n>", "Items per page", "10").option("--offset <n>", "Pagination offset", "0").action(async (cmdOpts) => {
850
+ const content = program2.command("content").description("Manage content items in the knowledge base. List, inspect, delete, unpublish, and manage the verification workflow and ownership of content.");
851
+ content.command("list").description("List all content items in the knowledge base. Returns title, status, and ID for each item. Use --search to filter by title, --sort to order results.").option("--limit <n>", "Items per page", "10").option("--offset <n>", "Pagination offset", "0").option("--search <query>", "Filter content by title").option("--sort <order>", "Sort order: title_asc, title_desc, created_asc, created_desc").action(async (cmdOpts) => {
932
852
  const opts = program2.opts();
933
853
  try {
934
854
  const data = await apiRequest({
935
855
  path: "/org/content",
936
- params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
856
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset, search: cmdOpts.search, sort: cmdOpts.sort },
937
857
  apiKey: opts.apiKey,
938
858
  baseUrl: opts.baseUrl
939
859
  });
@@ -958,7 +878,7 @@ function registerContentCommands(program2) {
958
878
  process.exit(1);
959
879
  }
960
880
  });
961
- content.command("get <id>").description("Get content item by ID").action(async (id) => {
881
+ content.command("get <id>").description("Get a content item by ID. Returns the full content detail including versions, metadata, and publish status.").action(async (id) => {
962
882
  const opts = program2.opts();
963
883
  try {
964
884
  const data = await apiRequest({ path: `/org/content/${id}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -968,7 +888,7 @@ function registerContentCommands(program2) {
968
888
  process.exit(1);
969
889
  }
970
890
  });
971
- content.command("delete <id>").description("Delete content (local + external)").action(async (id) => {
891
+ content.command("delete <id>").description("Delete a content item from the knowledge base and any external publish destinations. This cannot be undone.").action(async (id) => {
972
892
  const opts = program2.opts();
973
893
  try {
974
894
  await apiRequest({ method: "DELETE", path: `/org/content/${id}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -978,7 +898,7 @@ function registerContentCommands(program2) {
978
898
  process.exit(1);
979
899
  }
980
900
  });
981
- content.command("unpublish <id>").description("Unpublish content (external delete + set draft)").action(async (id) => {
901
+ content.command("unpublish <id>").description("Unpublish a content item. Removes it from external destinations and sets its status back to draft.").action(async (id) => {
982
902
  const opts = program2.opts();
983
903
  try {
984
904
  await apiRequest({ method: "POST", path: `/org/content/${id}/unpublish`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -988,27 +908,33 @@ function registerContentCommands(program2) {
988
908
  process.exit(1);
989
909
  }
990
910
  });
991
- content.command("verification").description("List content awaiting verification").action(async () => {
911
+ content.command("verification").description("List content items in the verification workflow. Filter by editorial status (draft, review, rejected, published) to manage the review pipeline.").option("--limit <n>", "Maximum items to return").option("--offset <n>", "Number of items to skip (for pagination)").option("--search <query>", "Filter by title").option("--status <status>", "Filter by status: all, draft, review, rejected, published").action(async (cmdOpts) => {
992
912
  const opts = program2.opts();
993
913
  try {
994
- const data = await apiRequest({ path: "/org/content/verification", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
914
+ const data = await apiRequest({
915
+ path: "/org/content/verification",
916
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset, search: cmdOpts.search, status: cmdOpts.status },
917
+ apiKey: opts.apiKey,
918
+ baseUrl: opts.baseUrl
919
+ });
995
920
  console.log(JSON.stringify(data, null, 2));
996
921
  } catch (err) {
997
922
  error(formatApiError(err));
998
923
  process.exit(1);
999
924
  }
1000
925
  });
1001
- content.command("reject <versionId>").description("Reject a content version").action(async (versionId) => {
926
+ content.command("reject <versionId>").description("Reject a content version in the verification workflow. Optionally provide a reason for the rejection.").option("--reason <text>", "Reason for rejection").action(async (versionId, cmdOpts) => {
1002
927
  const opts = program2.opts();
1003
928
  try {
1004
- await apiRequest({ method: "POST", path: `/org/content/versions/${versionId}/reject`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
929
+ const body = cmdOpts.reason ? { reason: cmdOpts.reason } : void 0;
930
+ await apiRequest({ method: "POST", path: `/org/content/versions/${versionId}/reject`, body, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1005
931
  success(`Version ${versionId} rejected.`);
1006
932
  } catch (err) {
1007
933
  error(formatApiError(err));
1008
934
  process.exit(1);
1009
935
  }
1010
936
  });
1011
- content.command("restore <versionId>").description("Restore a content version to draft").action(async (versionId) => {
937
+ content.command("restore <versionId>").description("Restore a rejected content version back to draft status for further editing.").action(async (versionId) => {
1012
938
  const opts = program2.opts();
1013
939
  try {
1014
940
  await apiRequest({ method: "POST", path: `/org/content/versions/${versionId}/restore`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1018,7 +944,7 @@ function registerContentCommands(program2) {
1018
944
  process.exit(1);
1019
945
  }
1020
946
  });
1021
- content.command("owners <id>").description("List content owners").action(async (id) => {
947
+ content.command("owners <id>").description("List the owners assigned to a content item. Owners are responsible for reviewing and approving content.").action(async (id) => {
1022
948
  const opts = program2.opts();
1023
949
  try {
1024
950
  const data = await apiRequest({ path: `/org/content/${id}/owners`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1028,7 +954,7 @@ function registerContentCommands(program2) {
1028
954
  process.exit(1);
1029
955
  }
1030
956
  });
1031
- content.command("set-owners <id>").description("Replace content owners").requiredOption("--user-ids <ids...>", "User IDs to set as owners").action(async (id, cmdOpts) => {
957
+ content.command("set-owners <id>").description("Replace all owners of a content item with a new set of user IDs.").requiredOption("--user-ids <ids...>", "User IDs to set as owners").action(async (id, cmdOpts) => {
1032
958
  const opts = program2.opts();
1033
959
  try {
1034
960
  await apiRequest({
@@ -1044,7 +970,7 @@ function registerContentCommands(program2) {
1044
970
  process.exit(1);
1045
971
  }
1046
972
  });
1047
- content.command("remove-owner <id> <userId>").description("Remove content owner").action(async (id, userId) => {
973
+ content.command("remove-owner <id> <userId>").description("Remove a single owner from a content item.").action(async (id, userId) => {
1048
974
  const opts = program2.opts();
1049
975
  try {
1050
976
  await apiRequest({ method: "DELETE", path: `/org/content/${id}/owners/${userId}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1058,8 +984,8 @@ function registerContentCommands(program2) {
1058
984
 
1059
985
  // src/commands/generate.ts
1060
986
  function registerGenerateCommands(program2) {
1061
- const gen = program2.command("generate").description("Content generation settings and triggers");
1062
- gen.command("settings").description("Get content generation settings").action(async () => {
987
+ const gen = program2.command("generate").description("AI content generation. Configure settings, generate content samples from prompts, or trigger full content engine runs.");
988
+ gen.command("settings").description("Get content generation settings. Shows whether generation and auto-publish are enabled, the content schedule, and configured publishers.").action(async () => {
1063
989
  const opts = program2.opts();
1064
990
  try {
1065
991
  const data = await apiRequest({ path: "/org/content-generation", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1069,24 +995,32 @@ function registerGenerateCommands(program2) {
1069
995
  process.exit(1);
1070
996
  }
1071
997
  });
1072
- gen.command("update-settings").description("Update content generation settings").requiredOption("--data <json>", "JSON settings to update").action(async (cmdOpts) => {
998
+ gen.command("update-settings").description("Update content generation settings. Control auto-publish, generation toggle, and schedule (days of week 0-6).").requiredOption("--data <json>", 'JSON settings: { "enable_content_generation": bool, "content_auto_publish": bool, "content_schedule": [0-6] }').action(async (cmdOpts) => {
1073
999
  const opts = program2.opts();
1074
1000
  try {
1075
1001
  const body = JSON.parse(cmdOpts.data);
1076
1002
  const data = await apiRequest({ method: "PATCH", path: "/org/content-generation", body, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1003
+ success("Content generation settings updated.");
1077
1004
  console.log(JSON.stringify(data, null, 2));
1078
1005
  } catch (err) {
1079
1006
  error(err instanceof SyntaxError ? "Invalid JSON in --data" : formatApiError(err));
1080
1007
  process.exit(1);
1081
1008
  }
1082
1009
  });
1083
- gen.command("sample").description("Generate ad hoc content sample").requiredOption("--instructions <text>", "Instructions for generation").action(async (cmdOpts) => {
1010
+ gen.command("sample").description("Generate an ad hoc content sample for a specific prompt and content type. Returns the generated markdown, SEO title, and publish results.").requiredOption("--prompt-id <id>", "Prompt (geo question) ID to generate content for").requiredOption("--content-type-id <id>", "Content type ID that defines the output format").option("--destination <dest>", "Publish destination (e.g. citeables)").action(async (cmdOpts) => {
1084
1011
  const opts = program2.opts();
1085
1012
  try {
1013
+ const body = {
1014
+ geo_question_id: cmdOpts.promptId,
1015
+ content_type_id: cmdOpts.contentTypeId
1016
+ };
1017
+ if (cmdOpts.destination) {
1018
+ body.publish_destination = cmdOpts.destination;
1019
+ }
1086
1020
  const data = await apiRequest({
1087
1021
  method: "POST",
1088
1022
  path: "/org/content-generation/sample",
1089
- body: { instructions: cmdOpts.instructions },
1023
+ body,
1090
1024
  apiKey: opts.apiKey,
1091
1025
  baseUrl: opts.baseUrl
1092
1026
  });
@@ -1096,10 +1030,11 @@ function registerGenerateCommands(program2) {
1096
1030
  process.exit(1);
1097
1031
  }
1098
1032
  });
1099
- gen.command("run").description("Trigger content engine run").action(async () => {
1033
+ gen.command("run").description("Trigger a content generation run. Processes all prompts (or a specific subset) through the content engine. Runs asynchronously.").option("--prompt-ids <ids...>", "Optional list of prompt IDs to process (omit to run all)").action(async (cmdOpts) => {
1100
1034
  const opts = program2.opts();
1101
1035
  try {
1102
- const data = await apiRequest({ method: "POST", path: "/org/content-generation/run", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1036
+ const body = cmdOpts.promptIds ? { prompt_ids: cmdOpts.promptIds } : void 0;
1037
+ const data = await apiRequest({ method: "POST", path: "/org/content-generation/run", body, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1103
1038
  success("Content generation run triggered.");
1104
1039
  console.log(JSON.stringify(data, null, 2));
1105
1040
  } catch (err) {
@@ -1111,8 +1046,8 @@ function registerGenerateCommands(program2) {
1111
1046
 
1112
1047
  // src/commands/engine.ts
1113
1048
  function registerEngineCommands(program2) {
1114
- const engine = program2.command("engine").description("Content engine operations (publish/draft)");
1115
- engine.command("publish").description("Publish content via content engine").requiredOption("--data <json>", "JSON payload for publish").action(async (cmdOpts) => {
1049
+ const engine = program2.command("engine").description("Publish or draft content through the content engine. Used to push AI-generated content to external destinations or save it as a draft for review.");
1050
+ engine.command("publish").description("Publish content to external destinations via the content engine. Requires geo_question_id, raw_markdown, and seo_title.").requiredOption("--data <json>", 'JSON: { "geo_question_id": "uuid", "raw_markdown": "...", "seo_title": "...", "summary": "..." }').action(async (cmdOpts) => {
1116
1051
  const opts = program2.opts();
1117
1052
  try {
1118
1053
  const body = JSON.parse(cmdOpts.data);
@@ -1130,7 +1065,7 @@ function registerEngineCommands(program2) {
1130
1065
  process.exit(1);
1131
1066
  }
1132
1067
  });
1133
- engine.command("draft").description("Save content as draft via content engine").requiredOption("--data <json>", "JSON payload for draft").action(async (cmdOpts) => {
1068
+ engine.command("draft").description("Save content as a draft for review before publishing. Requires geo_question_id, raw_markdown, and seo_title.").requiredOption("--data <json>", 'JSON: { "geo_question_id": "uuid", "raw_markdown": "...", "seo_title": "...", "summary": "..." }').action(async (cmdOpts) => {
1134
1069
  const opts = program2.opts();
1135
1070
  try {
1136
1071
  const body = JSON.parse(cmdOpts.data);
@@ -1152,8 +1087,8 @@ function registerEngineCommands(program2) {
1152
1087
 
1153
1088
  // src/commands/brand-kit.ts
1154
1089
  function registerBrandKitCommands(program2) {
1155
- const bk = program2.command("brand-kit").description("Manage brand kit");
1156
- bk.command("get").description("Get brand kit").action(async () => {
1090
+ const bk = program2.command("brand-kit").description("Manage the organization's brand kit guidelines. The brand kit is a free-form JSON object that informs AI content generation about your brand voice, tone, and style.");
1091
+ bk.command("get").description("Get the current brand kit guidelines.").action(async () => {
1157
1092
  const opts = program2.opts();
1158
1093
  try {
1159
1094
  const data = await apiRequest({ path: "/org/brand-kit", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1163,7 +1098,7 @@ function registerBrandKitCommands(program2) {
1163
1098
  process.exit(1);
1164
1099
  }
1165
1100
  });
1166
- bk.command("set").description("Upsert brand kit").requiredOption("--data <json>", "JSON brand kit data").action(async (cmdOpts) => {
1101
+ bk.command("set").description("Create or replace the brand kit. The guidelines field is a free-form JSON object defining your brand voice.").requiredOption("--data <json>", 'JSON: { "guidelines": { "tone": "professional", "voice": "..." } }').action(async (cmdOpts) => {
1167
1102
  const opts = program2.opts();
1168
1103
  try {
1169
1104
  const body = JSON.parse(cmdOpts.data);
@@ -1185,18 +1120,23 @@ function registerBrandKitCommands(program2) {
1185
1120
 
1186
1121
  // src/commands/content-types.ts
1187
1122
  function registerContentTypeCommands(program2) {
1188
- const ct = program2.command("content-types").description("Manage content types");
1189
- ct.command("list").description("List content types").action(async () => {
1123
+ const ct = program2.command("content-types").description("Manage content type configurations. Content types define the output format and structure for AI-generated content (e.g. blog post, FAQ, landing page).");
1124
+ ct.command("list").description("List all content types configured for the organization.").option("--limit <n>", "Maximum number of content types to return (default: 50)").option("--offset <n>", "Number of items to skip (for pagination)").action(async (cmdOpts) => {
1190
1125
  const opts = program2.opts();
1191
1126
  try {
1192
- const data = await apiRequest({ path: "/org/content-types", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1127
+ const data = await apiRequest({
1128
+ path: "/org/content-types",
1129
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
1130
+ apiKey: opts.apiKey,
1131
+ baseUrl: opts.baseUrl
1132
+ });
1193
1133
  console.log(JSON.stringify(data, null, 2));
1194
1134
  } catch (err) {
1195
1135
  error(formatApiError(err));
1196
1136
  process.exit(1);
1197
1137
  }
1198
1138
  });
1199
- ct.command("create").description("Create a content type").requiredOption("--data <json>", "JSON content type definition").action(async (cmdOpts) => {
1139
+ ct.command("create").description("Create a new content type. Requires a name and configuration defining the output structure.").requiredOption("--data <json>", 'JSON: { "name": "Blog Post", "config": { ... } }').action(async (cmdOpts) => {
1200
1140
  const opts = program2.opts();
1201
1141
  try {
1202
1142
  const body = JSON.parse(cmdOpts.data);
@@ -1214,7 +1154,7 @@ function registerContentTypeCommands(program2) {
1214
1154
  process.exit(1);
1215
1155
  }
1216
1156
  });
1217
- ct.command("get <id>").description("Get content type by ID").action(async (id) => {
1157
+ ct.command("get <id>").description("Get a content type by ID, including its full configuration.").action(async (id) => {
1218
1158
  const opts = program2.opts();
1219
1159
  try {
1220
1160
  const data = await apiRequest({ path: `/org/content-types/${id}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1224,12 +1164,12 @@ function registerContentTypeCommands(program2) {
1224
1164
  process.exit(1);
1225
1165
  }
1226
1166
  });
1227
- ct.command("update <id>").description("Update a content type").requiredOption("--data <json>", "JSON content type updates").action(async (id, cmdOpts) => {
1167
+ ct.command("update <id>").description("Update a content type's name or configuration.").requiredOption("--data <json>", 'JSON: { "name": "Updated Name", "config": { ... } }').action(async (id, cmdOpts) => {
1228
1168
  const opts = program2.opts();
1229
1169
  try {
1230
1170
  const body = JSON.parse(cmdOpts.data);
1231
1171
  const data = await apiRequest({
1232
- method: "PATCH",
1172
+ method: "PUT",
1233
1173
  path: `/org/content-types/${id}`,
1234
1174
  body,
1235
1175
  apiKey: opts.apiKey,
@@ -1242,7 +1182,7 @@ function registerContentTypeCommands(program2) {
1242
1182
  process.exit(1);
1243
1183
  }
1244
1184
  });
1245
- ct.command("delete <id>").description("Delete a content type").action(async (id) => {
1185
+ ct.command("delete <id>").description("Delete a content type. This cannot be undone.").action(async (id) => {
1246
1186
  const opts = program2.opts();
1247
1187
  try {
1248
1188
  await apiRequest({ method: "DELETE", path: `/org/content-types/${id}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1256,18 +1196,23 @@ function registerContentTypeCommands(program2) {
1256
1196
 
1257
1197
  // src/commands/prompts.ts
1258
1198
  function registerPromptCommands(program2) {
1259
- const prompts = program2.command("prompts").description("Manage prompts (geo questions)");
1260
- prompts.command("list").description("List prompts").action(async () => {
1199
+ const prompts = program2.command("prompts").description("Manage prompts (geo questions). Prompts are the questions that drive AI content generation \u2014 each prompt is run against configured AI models to track brand mentions, claims, and competitor visibility.");
1200
+ prompts.command("list").description("List all prompts in the organization. Use --search to filter by question text, --sort to order results.").option("--limit <n>", "Maximum prompts to return (max: 100)").option("--offset <n>", "Number of prompts to skip (for pagination)").option("--search <query>", "Filter prompts by question text").option("--sort <order>", "Sort order: created_desc, created_asc, text_asc, text_desc, type_asc, type_desc").action(async (cmdOpts) => {
1261
1201
  const opts = program2.opts();
1262
1202
  try {
1263
- const data = await apiRequest({ path: "/org/prompts", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1203
+ const data = await apiRequest({
1204
+ path: "/org/prompts",
1205
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset, search: cmdOpts.search, sort: cmdOpts.sort },
1206
+ apiKey: opts.apiKey,
1207
+ baseUrl: opts.baseUrl
1208
+ });
1264
1209
  console.log(JSON.stringify(data, null, 2));
1265
1210
  } catch (err) {
1266
1211
  error(formatApiError(err));
1267
1212
  process.exit(1);
1268
1213
  }
1269
1214
  });
1270
- prompts.command("create").description("Create a prompt").requiredOption("--data <json>", "JSON prompt definition").action(async (cmdOpts) => {
1215
+ prompts.command("create").description("Create a new prompt. Type must be one of: decision, consideration, awareness, evaluation.").requiredOption("--data <json>", 'JSON: { "question_text": "What are the best...", "type": "decision" }').action(async (cmdOpts) => {
1271
1216
  const opts = program2.opts();
1272
1217
  try {
1273
1218
  const body = JSON.parse(cmdOpts.data);
@@ -1285,7 +1230,7 @@ function registerPromptCommands(program2) {
1285
1230
  process.exit(1);
1286
1231
  }
1287
1232
  });
1288
- prompts.command("get <promptId>").description("Get prompt with full run history").action(async (promptId) => {
1233
+ prompts.command("get <promptId>").description("Get a prompt with its full run history. Includes all question runs with mentions, claims, citations, and competitor data.").action(async (promptId) => {
1289
1234
  const opts = program2.opts();
1290
1235
  try {
1291
1236
  const data = await apiRequest({ path: `/org/prompts/${promptId}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1295,7 +1240,7 @@ function registerPromptCommands(program2) {
1295
1240
  process.exit(1);
1296
1241
  }
1297
1242
  });
1298
- prompts.command("delete <promptId>").description("Delete a prompt").action(async (promptId) => {
1243
+ prompts.command("delete <promptId>").description("Delete a prompt and all its associated run history. This cannot be undone.").action(async (promptId) => {
1299
1244
  const opts = program2.opts();
1300
1245
  try {
1301
1246
  await apiRequest({ method: "DELETE", path: `/org/prompts/${promptId}`, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1309,8 +1254,8 @@ function registerPromptCommands(program2) {
1309
1254
 
1310
1255
  // src/commands/run-config.ts
1311
1256
  function registerRunConfigCommands(program2) {
1312
- const rc = program2.command("run-config").description("Run configuration (models, schedule)");
1313
- rc.command("models").description("Get configured AI models").action(async () => {
1257
+ const rc = program2.command("run-config").description("Configure which AI models are used for question runs and on which days they run. Models include chatgpt, gemini, etc.");
1258
+ rc.command("models").description("Get the AI models currently configured for question runs (e.g. chatgpt, gemini).").action(async () => {
1314
1259
  const opts = program2.opts();
1315
1260
  try {
1316
1261
  const data = await apiRequest({ path: "/org/run-models", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1320,7 +1265,7 @@ function registerRunConfigCommands(program2) {
1320
1265
  process.exit(1);
1321
1266
  }
1322
1267
  });
1323
- rc.command("set-models").description("Set AI models").requiredOption("--data <json>", "JSON model configuration").action(async (cmdOpts) => {
1268
+ rc.command("set-models").description("Replace the configured AI models for question runs. At least one model name is required.").requiredOption("--data <json>", 'JSON: { "models": ["chatgpt", "gemini"] }').action(async (cmdOpts) => {
1324
1269
  const opts = program2.opts();
1325
1270
  try {
1326
1271
  const body = JSON.parse(cmdOpts.data);
@@ -1338,7 +1283,7 @@ function registerRunConfigCommands(program2) {
1338
1283
  process.exit(1);
1339
1284
  }
1340
1285
  });
1341
- rc.command("schedule").description("Get run schedule").action(async () => {
1286
+ rc.command("schedule").description("Get the days of the week when question runs are triggered (0=Sunday, 1=Monday, ..., 6=Saturday).").action(async () => {
1342
1287
  const opts = program2.opts();
1343
1288
  try {
1344
1289
  const data = await apiRequest({ path: "/org/run-schedule", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
@@ -1348,7 +1293,7 @@ function registerRunConfigCommands(program2) {
1348
1293
  process.exit(1);
1349
1294
  }
1350
1295
  });
1351
- rc.command("set-schedule").description("Set run schedule").requiredOption("--data <json>", "JSON schedule configuration").action(async (cmdOpts) => {
1296
+ rc.command("set-schedule").description("Set which days of the week question runs are triggered. Values must be 0-6 (Sunday-Saturday).").requiredOption("--data <json>", 'JSON: { "schedule": [1, 3, 5] }').action(async (cmdOpts) => {
1352
1297
  const opts = program2.opts();
1353
1298
  try {
1354
1299
  const body = JSON.parse(cmdOpts.data);
@@ -1370,11 +1315,16 @@ function registerRunConfigCommands(program2) {
1370
1315
 
1371
1316
  // src/commands/members.ts
1372
1317
  function registerMemberCommands(program2) {
1373
- const members = program2.command("members").description("Organization members");
1374
- members.command("list").description("List organization members").action(async () => {
1318
+ const members = program2.command("members").description("View the organization member directory. Lists all users who belong to the organization with their names and emails.");
1319
+ members.command("list").description("List all organization members. Use --search to filter by name or email, --sort to order results.").option("--limit <n>", "Maximum members to return (max: 1000)").option("--offset <n>", "Number of members to skip (for pagination)").option("--search <query>", "Filter by name or email").option("--sort <order>", "Sort order: name_asc, name_desc, email_asc, email_desc, created_asc, created_desc").action(async (cmdOpts) => {
1375
1320
  const opts = program2.opts();
1376
1321
  try {
1377
- const data = await apiRequest({ path: "/org/members", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1322
+ const data = await apiRequest({
1323
+ path: "/org/members",
1324
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset, search: cmdOpts.search, sort: cmdOpts.sort },
1325
+ apiKey: opts.apiKey,
1326
+ baseUrl: opts.baseUrl
1327
+ });
1378
1328
  console.log(JSON.stringify(data, null, 2));
1379
1329
  } catch (err) {
1380
1330
  error(formatApiError(err));
@@ -1385,22 +1335,31 @@ function registerMemberCommands(program2) {
1385
1335
 
1386
1336
  // src/commands/notifications.ts
1387
1337
  function registerNotificationCommands(program2) {
1388
- const notif = program2.command("notifications").description("Manage notifications");
1389
- notif.command("list").description("List notifications").action(async () => {
1338
+ const notif = program2.command("notifications").description("View and manage user notifications. Notifications are triggered by content verification, generation runs, and other system events.");
1339
+ notif.command("list").description("List notifications for the current user. Use --unread-only to filter to unread notifications.").option("--limit <n>", "Maximum notifications to return (default: 50, max: 200)").option("--offset <n>", "Number of notifications to skip (for pagination)").option("--unread-only", "Only return unread notifications").action(async (cmdOpts) => {
1390
1340
  const opts = program2.opts();
1391
1341
  try {
1392
- const data = await apiRequest({ path: "/app/v1/notifications", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1342
+ const data = await apiRequest({
1343
+ path: "/app/v1/notifications",
1344
+ params: {
1345
+ limit: cmdOpts.limit,
1346
+ offset: cmdOpts.offset,
1347
+ unread_only: cmdOpts.unreadOnly ? "true" : void 0
1348
+ },
1349
+ apiKey: opts.apiKey,
1350
+ baseUrl: opts.baseUrl
1351
+ });
1393
1352
  console.log(JSON.stringify(data, null, 2));
1394
1353
  } catch (err) {
1395
1354
  error(formatApiError(err));
1396
1355
  process.exit(1);
1397
1356
  }
1398
1357
  });
1399
- notif.command("read <id>").description("Mark notification as read").action(async (id) => {
1358
+ notif.command("read <id>").description("Mark a notification as read.").action(async (id) => {
1400
1359
  const opts = program2.opts();
1401
1360
  try {
1402
1361
  await apiRequest({
1403
- method: "POST",
1362
+ method: "PATCH",
1404
1363
  path: `/app/v1/notifications/${id}/read`,
1405
1364
  apiKey: opts.apiKey,
1406
1365
  baseUrl: opts.baseUrl
@@ -1477,8 +1436,6 @@ registerAuthCommands(program);
1477
1436
  registerOrgCommands(program);
1478
1437
  registerUserCommands(program);
1479
1438
  registerApiKeyCommands(program);
1480
- registerCategoryCommands(program);
1481
- registerTopicCommands(program);
1482
1439
  registerSearchCommands(program);
1483
1440
  registerIngestCommands(program);
1484
1441
  registerContentCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senso-ai/cli",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Senso CLI — Infrastructure for the Agentic Web. Interact with your Senso knowledge base from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {