@senso-ai/cli 0.11.0 → 0.11.1

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 +35 -3
  2. package/dist/cli.js +350 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -124,9 +124,11 @@ Options: `--max-results <n>`
124
124
  ```
125
125
  senso content list List all knowledge base items
126
126
  senso content get <id> Get full content detail by ID
127
+ senso content versions <id> List version history for a content item
127
128
  senso content delete <id> Delete content (knowledge base + external)
128
129
  senso content unpublish <id> Unpublish and revert to draft
129
130
  senso content verification List items in verification workflow
131
+ senso content verification-counts Counts by status + published-domain summaries
130
132
  senso content reject <versionId> Reject a content version
131
133
  senso content restore <versionId> Restore rejected version to draft
132
134
  senso content owners <id> List owners of a content item
@@ -134,7 +136,16 @@ senso content set-owners <id> Replace owners (--user-ids)
134
136
  senso content remove-owner <id> <userId> Remove a single owner
135
137
  ```
136
138
 
137
- Options: `content list` supports `--limit`, `--offset`, `--search`, `--sort`. `content verification` supports `--limit`, `--offset`, `--search`, `--status`. `content reject` supports `--reason`.
139
+ Options: `content list` supports `--limit`, `--offset`, `--search`, `--sort`. `content verification` supports `--limit`, `--offset`, `--search`, `--status`, `--substatus`. `content reject` supports `--reason`.
140
+
141
+ ### Generated Content (GEO)
142
+
143
+ ```
144
+ senso generated-content list List published or draft generated content (--status published|drafts)
145
+ senso generated-content get <id> Get a generated content item with its rendered body
146
+ ```
147
+
148
+ Options: `generated-content list` supports `--status`, `--limit`, `--offset`, `--search`.
138
149
 
139
150
  ### Content Generation
140
151
 
@@ -152,6 +163,24 @@ senso engine publish Publish content to external destinations (-
152
163
  senso engine draft Save content as draft for review (--data)
153
164
  ```
154
165
 
166
+ ### Tracked Competitors & Sources
167
+
168
+ ```
169
+ senso competitors list List tracked competitors
170
+ senso competitors add Add a competitor (--name, --url)
171
+ senso competitors suggest Get AI-generated competitor suggestions
172
+ senso competitors batch-add Add up to 50 competitors (--data)
173
+ senso competitors update <id> Update a competitor (--name, --url)
174
+ senso competitors delete <id> Remove a competitor
175
+
176
+ senso tracked-sources list List citation-classification rules
177
+ senso tracked-sources add Add a rule (--pattern, --match-type, --tier)
178
+ senso tracked-sources update <id> Replace a rule (--pattern, --match-type, --tier)
179
+ senso tracked-sources delete <id> Remove a rule
180
+ ```
181
+
182
+ Options: `tracked-sources add`/`update` support `--category`, `--label`, `--priority`; `update` also supports `--active`/`--no-active`. `--match-type` is one of `domain | host | path_prefix | exact_url`; `--tier` is one of `primary | tracked | secondary`.
183
+
155
184
  ### Ingestion
156
185
 
157
186
  ```
@@ -344,12 +373,15 @@ npm test
344
373
  ```
345
374
  src/
346
375
  ├── cli.ts # Entry point — arg parsing, command dispatch
347
- ├── commands/ # One file per command group (16 files)
376
+ ├── commands/ # One file per command group
348
377
  │ ├── auth.ts # login, logout, whoami
349
378
  │ ├── search.ts # search, search context, search content
350
- │ ├── content.ts # CRUD + verification + owners
379
+ │ ├── content.ts # CRUD + versions + verification + owners
380
+ │ ├── generated-content.ts # GEO generated content (list, get)
351
381
  │ ├── generate.ts # content generation settings + triggers
352
382
  │ ├── engine.ts # publish, draft
383
+ │ ├── competitors.ts # tracked competitors CRUD + suggest
384
+ │ ├── tracked-sources.ts # citation-classification rules CRUD
353
385
  │ ├── ingest.ts # upload, reprocess (with S3 upload)
354
386
  │ ├── brand-kit.ts # get, set
355
387
  │ ├── content-types.ts # CRUD
package/dist/cli.js CHANGED
@@ -499,6 +499,29 @@ function registerOrgCommands(program2) {
499
499
  process.exit(1);
500
500
  }
501
501
  });
502
+ org.command("set-runs").description("Toggle the org-wide runs master switch. Pause every scheduled prompt run and content-generation run, or re-enable them.").requiredOption("--enabled <bool>", "Set to true or false").action(async (cmdOpts) => {
503
+ const opts = program2.opts();
504
+ const raw = cmdOpts.enabled.toLowerCase();
505
+ if (raw !== "true" && raw !== "false") {
506
+ error("--enabled must be `true` or `false`.");
507
+ process.exit(1);
508
+ }
509
+ const enable = raw === "true";
510
+ try {
511
+ const data = await apiRequest({
512
+ method: "PATCH",
513
+ path: "/org/me/runs-enabled",
514
+ body: { enable_runs: enable },
515
+ apiKey: opts.apiKey,
516
+ baseUrl: opts.baseUrl
517
+ });
518
+ success(`Org-wide runs ${enable ? "enabled" : "disabled"}.`);
519
+ console.log(JSON.stringify(data, null, 2));
520
+ } catch (err) {
521
+ error(formatApiError(err));
522
+ process.exit(1);
523
+ }
524
+ });
502
525
  }
503
526
 
504
527
  // src/commands/users.ts
@@ -591,6 +614,50 @@ function registerUserCommands(program2) {
591
614
  process.exit(1);
592
615
  }
593
616
  });
617
+ users.command("invite").description("Invite a brand-new user by email. Creates the user (in Clerk and Senso) and adds them to the organization with the given role. Use `roles list` to find a role_id. If the email already belongs to a Senso user, use `users invite-existing` instead.").requiredOption("--email <email>", "User's email address").requiredOption("--given-name <name>", "First name").requiredOption("--family-name <name>", "Last name").requiredOption("--role-id <uuid>", "Role to assign \u2014 resolve with `senso roles list`").option("--is-current", "Make this org the new user's current org").action(async (cmdOpts) => {
618
+ const opts = program2.opts();
619
+ try {
620
+ const data = await apiRequest({
621
+ method: "POST",
622
+ path: "/org/users/invite",
623
+ body: {
624
+ email: cmdOpts.email,
625
+ given_name: cmdOpts.givenName,
626
+ family_name: cmdOpts.familyName,
627
+ role_id: cmdOpts.roleId,
628
+ is_current: cmdOpts.isCurrent ?? false
629
+ },
630
+ apiKey: opts.apiKey,
631
+ baseUrl: opts.baseUrl
632
+ });
633
+ success(`Invited ${cmdOpts.email} to the organization.`);
634
+ console.log(JSON.stringify(data, null, 2));
635
+ } catch (err) {
636
+ error(formatApiError(err));
637
+ process.exit(1);
638
+ }
639
+ });
640
+ users.command("invite-existing").description("Add an existing Senso user to the organization by email. Returns 404 if no user with that email exists \u2014 use `users invite` for brand-new users.").requiredOption("--email <email>", "Email of an existing Senso user").requiredOption("--role-id <uuid>", "Role to assign \u2014 resolve with `senso roles list`").option("--is-current", "Make this org the user's current org").action(async (cmdOpts) => {
641
+ const opts = program2.opts();
642
+ try {
643
+ const data = await apiRequest({
644
+ method: "POST",
645
+ path: "/org/users/invite/existing",
646
+ body: {
647
+ email: cmdOpts.email,
648
+ role_id: cmdOpts.roleId,
649
+ is_current: cmdOpts.isCurrent ?? false
650
+ },
651
+ apiKey: opts.apiKey,
652
+ baseUrl: opts.baseUrl
653
+ });
654
+ success(`Added ${cmdOpts.email} to the organization.`);
655
+ console.log(JSON.stringify(data, null, 2));
656
+ } catch (err) {
657
+ error(formatApiError(err));
658
+ process.exit(1);
659
+ }
660
+ });
594
661
  }
595
662
 
596
663
  // src/commands/api-keys.ts
@@ -1391,12 +1458,40 @@ function registerContentCommands(program2) {
1391
1458
  process.exit(1);
1392
1459
  }
1393
1460
  });
1394
- 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) => {
1461
+ 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").option("--substatus <substatus>", "Narrow further (only valid with --status published): pending_draft").action(async (cmdOpts) => {
1395
1462
  const opts = program2.opts();
1396
1463
  try {
1397
1464
  const data = await apiRequest({
1398
1465
  path: "/org/content/verification",
1399
- params: { limit: cmdOpts.limit, offset: cmdOpts.offset, search: cmdOpts.search, status: cmdOpts.status },
1466
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset, search: cmdOpts.search, status: cmdOpts.status, substatus: cmdOpts.substatus },
1467
+ apiKey: opts.apiKey,
1468
+ baseUrl: opts.baseUrl
1469
+ });
1470
+ console.log(JSON.stringify(data, null, 2));
1471
+ } catch (err) {
1472
+ error(formatApiError(err));
1473
+ process.exit(1);
1474
+ }
1475
+ });
1476
+ content.command("verification-counts").description("Get counts of content by editorial status (draft, published, rejected, pending published-draft) plus per-destination published-domain summaries. A lightweight alternative to paging through 'content verification'.").action(async () => {
1477
+ const opts = program2.opts();
1478
+ try {
1479
+ const data = await apiRequest({
1480
+ path: "/org/content/verification/counts",
1481
+ apiKey: opts.apiKey,
1482
+ baseUrl: opts.baseUrl
1483
+ });
1484
+ console.log(JSON.stringify(data, null, 2));
1485
+ } catch (err) {
1486
+ error(formatApiError(err));
1487
+ process.exit(1);
1488
+ }
1489
+ });
1490
+ content.command("versions <id>").description("List the version history for a content item, newest first. The current version is flagged with is_current.").action(async (id) => {
1491
+ const opts = program2.opts();
1492
+ try {
1493
+ const data = await apiRequest({
1494
+ path: `/org/content/${id}/versions`,
1400
1495
  apiKey: opts.apiKey,
1401
1496
  baseUrl: opts.baseUrl
1402
1497
  });
@@ -1669,12 +1764,12 @@ function registerGenerateCommands(program2) {
1669
1764
  process.exit(1);
1670
1765
  }
1671
1766
  });
1672
- gen.command("runs-items <runId>").description("List individual prompt items within a content generation run and their per-item status.").option("--limit <n>", "Items per page", "100").option("--offset <n>", "Pagination offset", "0").action(async (runId, cmdOpts) => {
1767
+ gen.command("runs-items <runId>").description("List individual prompt items within a content generation run and their per-item status.").option("--limit <n>", "Items per page", "100").option("--offset <n>", "Pagination offset", "0").option("--status <status>", "Filter by item status: pending, running, succeeded, failed, skipped, stopped").action(async (runId, cmdOpts) => {
1673
1768
  const opts = program2.opts();
1674
1769
  try {
1675
1770
  const data = await apiRequest({
1676
1771
  path: `/org/content-generation/runs/${runId}/items`,
1677
- params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
1772
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset, status: cmdOpts.status },
1678
1773
  apiKey: opts.apiKey,
1679
1774
  baseUrl: opts.baseUrl
1680
1775
  });
@@ -1727,7 +1822,7 @@ function sleep(ms) {
1727
1822
  // src/commands/engine.ts
1728
1823
  function registerEngineCommands(program2) {
1729
1824
  const engine = program2.command("engine").description("Publish or draft content through the content engine. Used to push AI-generated content to external destinations (citeables by default) or save it as a draft for review.");
1730
- engine.command("publish").description("Publish content to external destinations via the content engine. Requires geo_question_id, raw_markdown, and seo_title. By default publishes to every destination currently selected for generation (citeables is the default for most orgs \u2014 see 'senso destinations list'). Pass --publisher-ids to restrict publishing to a specific subset, or include 'publisher_ids' inside --data.").requiredOption("--data <json>", 'JSON: { "geo_question_id": "uuid", "raw_markdown": "...", "seo_title": "...", "summary": "...", "publisher_ids": ["<uuid>", ...] }').option("--publisher-ids <ids...>", "Restrict publishing to specific publisher IDs. Overrides any publisher_ids present in --data. Omit to publish to all configured destinations (citeables by default).").action(async (cmdOpts) => {
1825
+ engine.command("publish").description("Publish content to external destinations via the content engine. Requires geo_question_id, raw_markdown, and seo_title. By default publishes to every destination currently selected for generation (citeables is the default for most orgs \u2014 see 'senso destinations list'). Pass --publisher-ids to restrict publishing to a specific subset, or include 'publisher_ids' inside --data. To record content as already published externally rather than pushing it to destinations, set mark_as_published (and optionally manual_published_at) in --data.").requiredOption("--data <json>", 'JSON: { "geo_question_id": "uuid", "raw_markdown": "...", "seo_title": "...", "summary": "...", "publisher_ids": ["<uuid>", ...], "mark_as_published": false, "manual_published_at": "2026-06-11T00:00:00Z" }').option("--publisher-ids <ids...>", "Restrict publishing to specific publisher IDs. Overrides any publisher_ids present in --data. Omit to publish to all configured destinations (citeables by default).").action(async (cmdOpts) => {
1731
1826
  const opts = program2.opts();
1732
1827
  try {
1733
1828
  const body = JSON.parse(cmdOpts.data);
@@ -1847,14 +1942,13 @@ function registerPublishRecordsCommands(program2) {
1847
1942
  pr.command("retry <publishRecordId>").description("Retry a failed publish record. Re-runs the publish for that specific content+destination pair and flips the record's state based on the new attempt. Only works on records currently in the 'failed' state.").action(async (publishRecordId) => {
1848
1943
  const opts = program2.opts();
1849
1944
  try {
1850
- const data = await apiRequest({
1945
+ await apiRequest({
1851
1946
  method: "POST",
1852
1947
  path: `/org/publish-records/${publishRecordId}/retry`,
1853
1948
  apiKey: opts.apiKey,
1854
1949
  baseUrl: opts.baseUrl
1855
1950
  });
1856
- success(`Publish record ${publishRecordId} retry triggered.`);
1857
- console.log(JSON.stringify(data, null, 2));
1951
+ success(`Publish record ${publishRecordId} retry completed.`);
1858
1952
  } catch (err) {
1859
1953
  error(formatApiError(err));
1860
1954
  process.exit(1);
@@ -1864,7 +1958,7 @@ function registerPublishRecordsCommands(program2) {
1864
1958
 
1865
1959
  // src/commands/brand-kit.ts
1866
1960
  function registerBrandKitCommands(program2) {
1867
- 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.");
1961
+ const bk = program2.command("brand-kit").description("Manage the organization's brand kit guidelines that inform AI content generation about your brand voice, tone, and style. The guidelines object accepts a defined set of keys: brand_name, brand_domain, brand_description, voice_and_tone, author_persona, and global_writing_rules (array). Unknown keys are rejected.");
1868
1962
  bk.command("get").description("Get the current brand kit guidelines.").action(async () => {
1869
1963
  const opts = program2.opts();
1870
1964
  try {
@@ -1931,7 +2025,7 @@ function registerContentTypeCommands(program2) {
1931
2025
  process.exit(1);
1932
2026
  }
1933
2027
  });
1934
- 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) => {
2028
+ ct.command("create").description("Create a new content type. Requires a name and a config defining the output structure. config accepts a defined set of keys: template, template_spec, cta_text, cta_destination, writing_rules (array). Unknown keys are rejected.").requiredOption("--data <json>", 'JSON: { "name": "Blog Post", "config": { "template": "...", "cta_text": "...", "cta_destination": "...", "writing_rules": [] } }').action(async (cmdOpts) => {
1935
2029
  const opts = program2.opts();
1936
2030
  try {
1937
2031
  const body = JSON.parse(cmdOpts.data);
@@ -2415,7 +2509,7 @@ function registerQuestionsCommands(program2) {
2415
2509
  process.exit(1);
2416
2510
  }
2417
2511
  });
2418
- questions.command("patch <questionId>").description("Partially update a question. Currently supports updating tag associations.").requiredOption("--data <json>", 'JSON: { "tag_ids": ["<uuid>", ...] } \u2014 pass null to clear all tags').action(async (questionId, cmdOpts) => {
2512
+ questions.command("patch <questionId>").description("Partially update a question. Supports updating tag associations and/or the funnel stage (type). At least one of tag_ids or type must be provided.").requiredOption("--data <json>", 'JSON: { "tag_ids": ["<uuid>", ...], "type": "decision|consideration|awareness|evaluation" } \u2014 pass tag_ids: null to clear all tags').action(async (questionId, cmdOpts) => {
2419
2513
  const opts = program2.opts();
2420
2514
  try {
2421
2515
  const body = JSON.parse(cmdOpts.data);
@@ -3021,6 +3115,247 @@ function registerTagsCommands(program2) {
3021
3115
  });
3022
3116
  }
3023
3117
 
3118
+ // src/commands/roles.ts
3119
+ function registerRolesCommands(program2) {
3120
+ const roles = program2.command("roles").description("Inspect the roles defined for your organization. Each organization has its own per-org role_ids \u2014 resolve a role name to its UUID here before passing role_id to `users invite`, `users add`, or `users update`.");
3121
+ roles.command("list").description("List every role for the current organization, including the built-in admin/collaborator/viewer roles and any custom roles.").action(async () => {
3122
+ const opts = program2.opts();
3123
+ try {
3124
+ const data = await apiRequest({
3125
+ path: "/org/roles",
3126
+ apiKey: opts.apiKey,
3127
+ baseUrl: opts.baseUrl
3128
+ });
3129
+ console.log(JSON.stringify(data, null, 2));
3130
+ } catch (err) {
3131
+ error(formatApiError(err));
3132
+ process.exit(1);
3133
+ }
3134
+ });
3135
+ }
3136
+
3137
+ // src/commands/competitors.ts
3138
+ function registerCompetitorsCommands(program2) {
3139
+ const competitors = program2.command("competitors").description("Manage the curated list of competitor brands your organization tracks. Tracked competitors feed downstream share-of-voice analytics and inform content-generation prompts.");
3140
+ competitors.command("list").description("List every tracked competitor for the current organization.").action(async () => {
3141
+ const opts = program2.opts();
3142
+ try {
3143
+ const data = await apiRequest({
3144
+ path: "/org/competitors",
3145
+ apiKey: opts.apiKey,
3146
+ baseUrl: opts.baseUrl
3147
+ });
3148
+ console.log(JSON.stringify(data, null, 2));
3149
+ } catch (err) {
3150
+ error(formatApiError(err));
3151
+ process.exit(1);
3152
+ }
3153
+ });
3154
+ competitors.command("add").description("Add a single tracked competitor.").requiredOption("--name <name>", "Competitor brand name").option("--url <url>", "Competitor website URL").action(async (cmdOpts) => {
3155
+ const opts = program2.opts();
3156
+ try {
3157
+ const body = { name: cmdOpts.name };
3158
+ if (cmdOpts.url) body.url = cmdOpts.url;
3159
+ const data = await apiRequest({
3160
+ method: "POST",
3161
+ path: "/org/competitors",
3162
+ body,
3163
+ apiKey: opts.apiKey,
3164
+ baseUrl: opts.baseUrl
3165
+ });
3166
+ success(`Tracked competitor "${cmdOpts.name}" added.`);
3167
+ console.log(JSON.stringify(data, null, 2));
3168
+ } catch (err) {
3169
+ error(formatApiError(err));
3170
+ process.exit(1);
3171
+ }
3172
+ });
3173
+ competitors.command("batch-add").description("Add up to 50 tracked competitors in one call. Designed for accepting AI-generated suggestions returned by `competitors suggest`.").requiredOption("--data <json>", 'JSON: { "items": [{ "name": "...", "url": "...", "source": "manual|suggested_run_text|suggested_web_search", "rationale": "...", "confidence": 0.85 }, ...] }').action(async (cmdOpts) => {
3174
+ const opts = program2.opts();
3175
+ try {
3176
+ const body = JSON.parse(cmdOpts.data);
3177
+ const data = await apiRequest({
3178
+ method: "POST",
3179
+ path: "/org/competitors/batch",
3180
+ body,
3181
+ apiKey: opts.apiKey,
3182
+ baseUrl: opts.baseUrl
3183
+ });
3184
+ success("Tracked competitors added.");
3185
+ console.log(JSON.stringify(data, null, 2));
3186
+ } catch (err) {
3187
+ error(err instanceof SyntaxError ? "Invalid JSON in --data" : formatApiError(err));
3188
+ process.exit(1);
3189
+ }
3190
+ });
3191
+ competitors.command("suggest").description("Get AI-generated competitor suggestions seeded from your org's website and recent prompt-run results. Pipe accepted suggestions into `competitors batch-add`.").action(async () => {
3192
+ const opts = program2.opts();
3193
+ try {
3194
+ const data = await apiRequest({
3195
+ method: "POST",
3196
+ path: "/org/competitors/suggest",
3197
+ apiKey: opts.apiKey,
3198
+ baseUrl: opts.baseUrl
3199
+ });
3200
+ console.log(JSON.stringify(data, null, 2));
3201
+ } catch (err) {
3202
+ error(formatApiError(err));
3203
+ process.exit(1);
3204
+ }
3205
+ });
3206
+ competitors.command("update <competitorId>").description("Update a tracked competitor's name or URL.").requiredOption("--name <name>", "Competitor brand name").option("--url <url>", "Competitor website URL").action(async (competitorId, cmdOpts) => {
3207
+ const opts = program2.opts();
3208
+ try {
3209
+ const body = { name: cmdOpts.name };
3210
+ if (cmdOpts.url) body.url = cmdOpts.url;
3211
+ const data = await apiRequest({
3212
+ method: "PUT",
3213
+ path: `/org/competitors/${competitorId}`,
3214
+ body,
3215
+ apiKey: opts.apiKey,
3216
+ baseUrl: opts.baseUrl
3217
+ });
3218
+ success(`Tracked competitor ${competitorId} updated.`);
3219
+ console.log(JSON.stringify(data, null, 2));
3220
+ } catch (err) {
3221
+ error(formatApiError(err));
3222
+ process.exit(1);
3223
+ }
3224
+ });
3225
+ competitors.command("delete <competitorId>").description("Remove a tracked competitor.").action(async (competitorId) => {
3226
+ const opts = program2.opts();
3227
+ try {
3228
+ await apiRequest({
3229
+ method: "DELETE",
3230
+ path: `/org/competitors/${competitorId}`,
3231
+ apiKey: opts.apiKey,
3232
+ baseUrl: opts.baseUrl
3233
+ });
3234
+ success(`Tracked competitor ${competitorId} removed.`);
3235
+ } catch (err) {
3236
+ error(formatApiError(err));
3237
+ process.exit(1);
3238
+ }
3239
+ });
3240
+ }
3241
+
3242
+ // src/commands/tracked-sources.ts
3243
+ var MATCH_TYPES = "domain | host | path_prefix | exact_url";
3244
+ var TIERS = "primary (Owned) | tracked | secondary (External)";
3245
+ var CATEGORIES = "affiliated_domain | published_content | social | press";
3246
+ function buildSourceBody(cmdOpts) {
3247
+ const body = {};
3248
+ if (cmdOpts.pattern !== void 0) body.pattern = cmdOpts.pattern;
3249
+ if (cmdOpts.matchType !== void 0) body.match_type = cmdOpts.matchType;
3250
+ if (cmdOpts.tier !== void 0) body.tier = cmdOpts.tier;
3251
+ if (cmdOpts.category !== void 0) body.category = cmdOpts.category;
3252
+ if (cmdOpts.label !== void 0) body.label = cmdOpts.label;
3253
+ if (cmdOpts.priority !== void 0) body.priority = Number(cmdOpts.priority);
3254
+ if (cmdOpts.active !== void 0) body.active = cmdOpts.active;
3255
+ return body;
3256
+ }
3257
+ function registerTrackedSourcesCommands(program2) {
3258
+ const sources = program2.command("tracked-sources").description("Manage citation-classification rules that tier each cited URL as Owned (primary), Tracked, or External (secondary). Tracked sources drive share-of-voice and citation analytics. Rules created from published content are read-only.");
3259
+ sources.command("list").description("List every tracked source rule for the current organization.").action(async () => {
3260
+ const opts = program2.opts();
3261
+ try {
3262
+ const data = await apiRequest({
3263
+ path: "/org/tracked-sources",
3264
+ apiKey: opts.apiKey,
3265
+ baseUrl: opts.baseUrl
3266
+ });
3267
+ console.log(JSON.stringify(data, null, 2));
3268
+ } catch (err) {
3269
+ error(formatApiError(err));
3270
+ process.exit(1);
3271
+ }
3272
+ });
3273
+ sources.command("add").description("Add a tracked source rule. New rules are always created active.").requiredOption("--pattern <pattern>", "Value to match cited URLs against, interpreted per --match-type").requiredOption("--match-type <type>", `Match strategy: ${MATCH_TYPES}`).requiredOption("--tier <tier>", `Classification tier: ${TIERS}`).option("--category <category>", `Optional sub-category (only meaningful for the 'tracked' tier): ${CATEGORIES}`).option("--label <label>", "Optional human-readable label").option("--priority <n>", "Optional ordering priority (integer)").action(async (cmdOpts) => {
3274
+ const opts = program2.opts();
3275
+ try {
3276
+ const data = await apiRequest({
3277
+ method: "POST",
3278
+ path: "/org/tracked-sources",
3279
+ body: buildSourceBody(cmdOpts),
3280
+ apiKey: opts.apiKey,
3281
+ baseUrl: opts.baseUrl
3282
+ });
3283
+ success(`Tracked source "${cmdOpts.pattern}" added.`);
3284
+ console.log(JSON.stringify(data, null, 2));
3285
+ } catch (err) {
3286
+ error(formatApiError(err));
3287
+ process.exit(1);
3288
+ }
3289
+ });
3290
+ sources.command("update <sourceId>").description("Replace a tracked source rule (PUT). Pattern, match type, and tier are required. Published rules are read-only.").requiredOption("--pattern <pattern>", "Value to match cited URLs against, interpreted per --match-type").requiredOption("--match-type <type>", `Match strategy: ${MATCH_TYPES}`).requiredOption("--tier <tier>", `Classification tier: ${TIERS}`).option("--category <category>", `Optional sub-category (only meaningful for the 'tracked' tier): ${CATEGORIES}`).option("--label <label>", "Optional human-readable label").option("--priority <n>", "Optional ordering priority (integer)").option("--active", "Mark the rule active").option("--no-active", "Mark the rule inactive").action(async (sourceId, cmdOpts) => {
3291
+ const opts = program2.opts();
3292
+ try {
3293
+ const data = await apiRequest({
3294
+ method: "PUT",
3295
+ path: `/org/tracked-sources/${sourceId}`,
3296
+ body: buildSourceBody(cmdOpts),
3297
+ apiKey: opts.apiKey,
3298
+ baseUrl: opts.baseUrl
3299
+ });
3300
+ success(`Tracked source ${sourceId} updated.`);
3301
+ console.log(JSON.stringify(data, null, 2));
3302
+ } catch (err) {
3303
+ error(formatApiError(err));
3304
+ process.exit(1);
3305
+ }
3306
+ });
3307
+ sources.command("delete <sourceId>").description("Remove a tracked source rule.").action(async (sourceId) => {
3308
+ const opts = program2.opts();
3309
+ try {
3310
+ await apiRequest({
3311
+ method: "DELETE",
3312
+ path: `/org/tracked-sources/${sourceId}`,
3313
+ apiKey: opts.apiKey,
3314
+ baseUrl: opts.baseUrl
3315
+ });
3316
+ success(`Tracked source ${sourceId} removed.`);
3317
+ } catch (err) {
3318
+ error(formatApiError(err));
3319
+ process.exit(1);
3320
+ }
3321
+ });
3322
+ }
3323
+
3324
+ // src/commands/generated-content.ts
3325
+ function registerGeneratedContentCommands(program2) {
3326
+ const gc = program2.command("generated-content").description("Browse AI-generated content (GEO). List published or draft generated items, or fetch a single item with its rendered body. Requires the GEO product and read:content permission.");
3327
+ gc.command("list").description("List generated content. Use --status to switch between published and draft items.").option("--status <status>", "Which items to list: published | drafts", "published").option("--limit <n>", "Items per page (max 100)", "10").option("--offset <n>", "Pagination offset", "0").option("--search <query>", "Filter by title").action(async (cmdOpts) => {
3328
+ const opts = program2.opts();
3329
+ const status = cmdOpts.status === "drafts" || cmdOpts.status === "draft" ? "drafts" : "published";
3330
+ try {
3331
+ const data = await apiRequest({
3332
+ path: `/org/generated-content/${status}`,
3333
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset, search: cmdOpts.search },
3334
+ apiKey: opts.apiKey,
3335
+ baseUrl: opts.baseUrl
3336
+ });
3337
+ console.log(JSON.stringify(data, null, 2));
3338
+ } catch (err) {
3339
+ error(formatApiError(err));
3340
+ process.exit(1);
3341
+ }
3342
+ });
3343
+ gc.command("get <id>").description("Get a single generated content item including its question text and rendered body.").action(async (id) => {
3344
+ const opts = program2.opts();
3345
+ try {
3346
+ const data = await apiRequest({
3347
+ path: `/org/generated-content/${id}`,
3348
+ apiKey: opts.apiKey,
3349
+ baseUrl: opts.baseUrl
3350
+ });
3351
+ console.log(JSON.stringify(data, null, 2));
3352
+ } catch (err) {
3353
+ error(formatApiError(err));
3354
+ process.exit(1);
3355
+ }
3356
+ });
3357
+ }
3358
+
3024
3359
  // src/commands/update.ts
3025
3360
  import semver2 from "semver";
3026
3361
  import pc10 from "picocolors";
@@ -3087,6 +3422,10 @@ registerKBCommands(program);
3087
3422
  registerPermissionsCommands(program);
3088
3423
  registerTagsCommands(program);
3089
3424
  registerProductLineCommands(program);
3425
+ registerRolesCommands(program);
3426
+ registerCompetitorsCommands(program);
3427
+ registerTrackedSourcesCommands(program);
3428
+ registerGeneratedContentCommands(program);
3090
3429
  registerUpdateCommand(program);
3091
3430
  async function main() {
3092
3431
  const quiet = process.argv.includes("--quiet") || process.argv.includes("--output") && process.argv[process.argv.indexOf("--output") + 1] === "json";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senso-ai/cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
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": {