@senso-ai/cli 0.3.0 → 0.5.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 (2) hide show
  1. package/dist/cli.js +91 -54
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -113,7 +113,7 @@ function getConfigPath() {
113
113
  }
114
114
 
115
115
  // src/utils/updater.ts
116
- var GITHUB_REPO = "AI-Template-SDK/senso-user-cli";
116
+ var NPM_PACKAGE = "@senso-ai/cli";
117
117
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
118
118
  async function checkForUpdate(quiet) {
119
119
  if (process.env.SENSO_NO_UPDATE_CHECK === "1" || quiet) {
@@ -128,16 +128,8 @@ async function checkForUpdate(quiet) {
128
128
  return;
129
129
  }
130
130
  try {
131
- const res = await fetch(
132
- `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`,
133
- {
134
- headers: { Accept: "application/vnd.github.v3+json" },
135
- signal: AbortSignal.timeout(5e3)
136
- }
137
- );
138
- if (!res.ok) return;
139
- const release = await res.json();
140
- const latest = release.tag_name.replace(/^v/, "");
131
+ const latest = await getLatestVersion();
132
+ if (!latest) return;
141
133
  updateConfig({
142
134
  lastUpdateCheck: (/* @__PURE__ */ new Date()).toISOString(),
143
135
  latestVersion: latest
@@ -148,17 +140,18 @@ async function checkForUpdate(quiet) {
148
140
  } catch {
149
141
  }
150
142
  }
151
- async function getLatestRelease() {
143
+ async function getLatestVersion() {
152
144
  try {
153
145
  const res = await fetch(
154
- `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`,
146
+ `https://registry.npmjs.org/${NPM_PACKAGE}`,
155
147
  {
156
- headers: { Accept: "application/vnd.github.v3+json" },
148
+ headers: { Accept: "application/json" },
157
149
  signal: AbortSignal.timeout(1e4)
158
150
  }
159
151
  );
160
152
  if (!res.ok) return null;
161
- return await res.json();
153
+ const data = await res.json();
154
+ return data["dist-tags"]?.latest ?? null;
162
155
  } catch {
163
156
  return null;
164
157
  }
@@ -643,13 +636,16 @@ function output(format, data) {
643
636
  // src/commands/search.ts
644
637
  function registerSearchCommands(program2) {
645
638
  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.");
646
- search.argument("<query>", "Search query").option("--max-results <n>", "Maximum number of results", "5").action(async (query, cmdOpts) => {
639
+ search.argument("<query>", "Search query").option("--max-results <n>", "Maximum number of results", "5").option("--content-ids <ids...>", "Restrict search to specific content item IDs (space-separated UUIDs)").option("--require-scoped-ids", "Only return results from the specified --content-ids (omit to allow fallback to all content)").action(async (query, cmdOpts) => {
647
640
  const opts = program2.opts();
648
641
  try {
642
+ const body = { query, max_results: parseInt(cmdOpts.maxResults) };
643
+ if (cmdOpts.contentIds) body.content_ids = cmdOpts.contentIds;
644
+ if (cmdOpts.requireScopedIds) body.require_scoped_ids = true;
649
645
  const data = await apiRequest({
650
646
  method: "POST",
651
647
  path: "/org/search",
652
- body: { query, max_results: parseInt(cmdOpts.maxResults) },
648
+ body,
653
649
  apiKey: opts.apiKey,
654
650
  baseUrl: opts.baseUrl
655
651
  });
@@ -682,13 +678,16 @@ function registerSearchCommands(program2) {
682
678
  process.exit(1);
683
679
  }
684
680
  });
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) => {
681
+ search.command("context <query>").description("Search the knowledge base \u2014 returns matching content chunks only, without AI answer generation. Use this to feed verified chunks into your own LLM pipeline instead of using Senso's generated answer.").option("--max-results <n>", "Maximum results", "5").option("--content-ids <ids...>", "Restrict search to specific content item IDs (space-separated UUIDs)").option("--require-scoped-ids", "Only return results from the specified --content-ids").action(async (query, cmdOpts) => {
686
682
  const opts = program2.opts();
687
683
  try {
684
+ const body = { query, max_results: parseInt(cmdOpts.maxResults) };
685
+ if (cmdOpts.contentIds) body.content_ids = cmdOpts.contentIds;
686
+ if (cmdOpts.requireScopedIds) body.require_scoped_ids = true;
688
687
  const data = await apiRequest({
689
688
  method: "POST",
690
689
  path: "/org/search/context",
691
- body: { query, max_results: parseInt(cmdOpts.maxResults) },
690
+ body,
692
691
  apiKey: opts.apiKey,
693
692
  baseUrl: opts.baseUrl
694
693
  });
@@ -698,13 +697,16 @@ function registerSearchCommands(program2) {
698
697
  process.exit(1);
699
698
  }
700
699
  });
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) => {
700
+ search.command("content <query>").description("Search the knowledge base \u2014 returns deduplicated content IDs and titles only. Use this to discover which documents are relevant before fetching full content with 'content get <id>'.").option("--max-results <n>", "Maximum results", "5").option("--content-ids <ids...>", "Restrict search to specific content item IDs (space-separated UUIDs)").option("--require-scoped-ids", "Only return results from the specified --content-ids").action(async (query, cmdOpts) => {
702
701
  const opts = program2.opts();
703
702
  try {
703
+ const body = { query, max_results: parseInt(cmdOpts.maxResults) };
704
+ if (cmdOpts.contentIds) body.content_ids = cmdOpts.contentIds;
705
+ if (cmdOpts.requireScopedIds) body.require_scoped_ids = true;
704
706
  const data = await apiRequest({
705
707
  method: "POST",
706
708
  path: "/org/search/content",
707
- body: { query, max_results: parseInt(cmdOpts.maxResults) },
709
+ body,
708
710
  apiKey: opts.apiKey,
709
711
  baseUrl: opts.baseUrl
710
712
  });
@@ -774,7 +776,7 @@ async function uploadToS3(url, buffer, contentType) {
774
776
  }
775
777
  function registerIngestCommands(program2) {
776
778
  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) => {
779
+ 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. Poll 'senso content get <content-id>' until processing_status is 'complete' before searching the uploaded content.").action(async (files) => {
778
780
  const opts = program2.opts();
779
781
  if (files.length > 10) {
780
782
  error("Maximum 10 files per upload request.");
@@ -1007,7 +1009,7 @@ function registerGenerateCommands(program2) {
1007
1009
  process.exit(1);
1008
1010
  }
1009
1011
  });
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) => {
1012
+ 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. Use 'prompts list' to find a prompt ID, and 'content-types list' to find a content-type ID.").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 (use 'content-types list' to find)").option("--destination <dest>", "Publisher slug to publish to immediately after generation. Omit to save as draft only.").action(async (cmdOpts) => {
1011
1013
  const opts = program2.opts();
1012
1014
  try {
1013
1015
  const body = {
@@ -1030,7 +1032,7 @@ function registerGenerateCommands(program2) {
1030
1032
  process.exit(1);
1031
1033
  }
1032
1034
  });
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) => {
1035
+ gen.command("run").description("Trigger a content generation run. Processes all prompts (or a specific subset) through the content engine. Runs asynchronously \u2014 use 'content verification' to monitor generated drafts after the run completes.").option("--prompt-ids <ids...>", "Optional list of prompt IDs to process (omit to run all)").action(async (cmdOpts) => {
1034
1036
  const opts = program2.opts();
1035
1037
  try {
1036
1038
  const body = cmdOpts.promptIds ? { prompt_ids: cmdOpts.promptIds } : void 0;
@@ -1098,7 +1100,7 @@ function registerBrandKitCommands(program2) {
1098
1100
  process.exit(1);
1099
1101
  }
1100
1102
  });
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) => {
1103
+ bk.command("set").description("Replace the entire brand kit (PUT). All existing fields are overwritten \u2014 run 'brand-kit get' first to preserve fields you are not changing. For a safe partial update, use 'brand-kit patch'.").requiredOption("--data <json>", 'JSON: { "guidelines": { "brand_name": "Acme", "voice_and_tone": "...", "author_persona": "...", "global_writing_rules": [] } }').action(async (cmdOpts) => {
1102
1104
  const opts = program2.opts();
1103
1105
  try {
1104
1106
  const body = JSON.parse(cmdOpts.data);
@@ -1116,6 +1118,24 @@ function registerBrandKitCommands(program2) {
1116
1118
  process.exit(1);
1117
1119
  }
1118
1120
  });
1121
+ bk.command("patch").description("Partially update the brand kit (PATCH). Only the fields you provide are changed \u2014 existing fields are preserved. Preferred over 'set' for targeted updates.").requiredOption("--data <json>", 'JSON: { "guidelines": { "voice_and_tone": "Warm and approachable" } }').action(async (cmdOpts) => {
1122
+ const opts = program2.opts();
1123
+ try {
1124
+ const body = JSON.parse(cmdOpts.data);
1125
+ const data = await apiRequest({
1126
+ method: "PATCH",
1127
+ path: "/org/brand-kit",
1128
+ body,
1129
+ apiKey: opts.apiKey,
1130
+ baseUrl: opts.baseUrl
1131
+ });
1132
+ success("Brand kit updated.");
1133
+ console.log(JSON.stringify(data, null, 2));
1134
+ } catch (err) {
1135
+ error(err instanceof SyntaxError ? "Invalid JSON in --data" : formatApiError(err));
1136
+ process.exit(1);
1137
+ }
1138
+ });
1119
1139
  }
1120
1140
 
1121
1141
  // src/commands/content-types.ts
@@ -1164,7 +1184,7 @@ function registerContentTypeCommands(program2) {
1164
1184
  process.exit(1);
1165
1185
  }
1166
1186
  });
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) => {
1187
+ ct.command("update <id>").description("Replace a content type's name and config (PUT). Both fields are required \u2014 run 'get <id>' first to preserve existing values. For single-field updates, use 'content-types patch <id>'.").requiredOption("--data <json>", 'JSON: { "name": "Updated Name", "config": { "template": "...", "cta_text": "...", "cta_destination": "...", "writing_rules": [] } }').action(async (id, cmdOpts) => {
1168
1188
  const opts = program2.opts();
1169
1189
  try {
1170
1190
  const body = JSON.parse(cmdOpts.data);
@@ -1182,6 +1202,24 @@ function registerContentTypeCommands(program2) {
1182
1202
  process.exit(1);
1183
1203
  }
1184
1204
  });
1205
+ ct.command("patch <id>").description("Partially update a content type (PATCH). Only the fields you provide are changed \u2014 existing fields are preserved. Preferred over 'update' for targeted changes like updating just the template.").requiredOption("--data <json>", 'JSON: { "config": { "template": "Updated template instruction" } }').action(async (id, cmdOpts) => {
1206
+ const opts = program2.opts();
1207
+ try {
1208
+ const body = JSON.parse(cmdOpts.data);
1209
+ const data = await apiRequest({
1210
+ method: "PATCH",
1211
+ path: `/org/content-types/${id}`,
1212
+ body,
1213
+ apiKey: opts.apiKey,
1214
+ baseUrl: opts.baseUrl
1215
+ });
1216
+ success(`Content type ${id} updated.`);
1217
+ console.log(JSON.stringify(data, null, 2));
1218
+ } catch (err) {
1219
+ error(err instanceof SyntaxError ? "Invalid JSON in --data" : formatApiError(err));
1220
+ process.exit(1);
1221
+ }
1222
+ });
1185
1223
  ct.command("delete <id>").description("Delete a content type. This cannot be undone.").action(async (id) => {
1186
1224
  const opts = program2.opts();
1187
1225
  try {
@@ -1196,7 +1234,7 @@ function registerContentTypeCommands(program2) {
1196
1234
 
1197
1235
  // src/commands/prompts.ts
1198
1236
  function registerPromptCommands(program2) {
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.");
1237
+ const prompts = program2.command("prompts").description("Manage prompts (GEO questions). Each prompt is a question that drives both AI content generation (use with 'generate sample --prompt-id') and brand visibility monitoring \u2014 tracking how AI models mention your brand, products, and competitors.");
1200
1238
  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) => {
1201
1239
  const opts = program2.opts();
1202
1240
  try {
@@ -1372,20 +1410,35 @@ function registerNotificationCommands(program2) {
1372
1410
  });
1373
1411
  }
1374
1412
 
1413
+ // src/commands/credits.ts
1414
+ function registerCreditsCommands(program2) {
1415
+ const credits = program2.command("credits").description("View your organisation's credit balance. Credits are consumed by AI content generation and search operations.");
1416
+ credits.command("balance").description("Get the current credit balance for the organisation. Returns available credits and any spend limit configured.").action(async () => {
1417
+ const opts = program2.opts();
1418
+ try {
1419
+ const data = await apiRequest({ path: "/org/credits/balance", apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1420
+ console.log(JSON.stringify(data, null, 2));
1421
+ } catch (err) {
1422
+ error(formatApiError(err));
1423
+ process.exit(1);
1424
+ }
1425
+ });
1426
+ }
1427
+
1375
1428
  // src/commands/update.ts
1376
1429
  import semver2 from "semver";
1377
1430
  import pc7 from "picocolors";
1378
1431
  import { execSync } from "child_process";
1432
+ var NPM_PACKAGE2 = "@senso-ai/cli";
1379
1433
  function registerUpdateCommand(program2) {
1380
1434
  program2.command("update").description("Update CLI to the latest version").action(async () => {
1381
1435
  info(`Current version: ${pc7.bold(version)}`);
1382
- info("Checking for updates...");
1383
- const release = await getLatestRelease();
1384
- if (!release) {
1436
+ info("Checking npm for updates...");
1437
+ const latest = await getLatestVersion();
1438
+ if (!latest) {
1385
1439
  error("Could not check for updates. Try again later.");
1386
1440
  process.exit(1);
1387
1441
  }
1388
- const latest = release.tag_name.replace(/^v/, "");
1389
1442
  if (!semver2.gt(latest, version)) {
1390
1443
  success(`Already on the latest version (${version}).`);
1391
1444
  return;
@@ -1393,33 +1446,16 @@ function registerUpdateCommand(program2) {
1393
1446
  info(`New version available: ${pc7.bold(latest)}`);
1394
1447
  info("Updating...");
1395
1448
  try {
1396
- execSync("npm install -g senso-user-cli@latest", {
1449
+ execSync(`npm install -g ${NPM_PACKAGE2}@latest`, {
1397
1450
  stdio: "inherit"
1398
1451
  });
1399
1452
  success(`Updated to v${latest}.`);
1400
- if (release.body) {
1401
- console.log();
1402
- console.log(pc7.dim("Release notes:"));
1403
- console.log(pc7.dim(release.body.slice(0, 500)));
1404
- }
1405
1453
  } catch {
1406
- warn("Global npm install failed. Trying npx reinstall...");
1407
- try {
1408
- execSync(
1409
- "npx --yes github:AI-Template-SDK/senso-user-cli --version",
1410
- { stdio: "inherit" }
1411
- );
1412
- success("Updated via npx cache refresh.");
1413
- } catch {
1414
- error("Update failed. Please reinstall manually:");
1415
- console.log(
1416
- ` ${pc7.cyan("npm install -g senso-user-cli")}`
1417
- );
1418
- console.log(
1419
- ` ${pc7.dim("or")} ${pc7.cyan("npx github:AI-Template-SDK/senso-user-cli")}`
1420
- );
1421
- process.exit(1);
1422
- }
1454
+ error("Update failed. Please reinstall manually:");
1455
+ console.log(
1456
+ ` ${pc7.cyan(`npm install -g ${NPM_PACKAGE2}`)}`
1457
+ );
1458
+ process.exit(1);
1423
1459
  }
1424
1460
  });
1425
1461
  }
@@ -1447,6 +1483,7 @@ registerPromptCommands(program);
1447
1483
  registerRunConfigCommands(program);
1448
1484
  registerMemberCommands(program);
1449
1485
  registerNotificationCommands(program);
1486
+ registerCreditsCommands(program);
1450
1487
  registerUpdateCommand(program);
1451
1488
  async function main() {
1452
1489
  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.3.0",
3
+ "version": "0.5.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": {