@senso-ai/cli 0.6.1 → 0.7.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 +55 -51
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -164,7 +164,7 @@ import pc3 from "picocolors";
164
164
  // src/lib/api-client.ts
165
165
  var ApiError = class extends Error {
166
166
  constructor(status, statusText, body) {
167
- const msg = typeof body === "object" && body && "error" in body ? body.error : statusText;
167
+ const msg = typeof body === "object" && body ? "error" in body ? body.error : "message" in body ? body.message : statusText : statusText;
168
168
  super(msg);
169
169
  this.status = status;
170
170
  this.statusText = statusText;
@@ -230,8 +230,10 @@ function formatApiError(err) {
230
230
  switch (err.status) {
231
231
  case 401:
232
232
  return "Authentication failed. Run `senso login` to update your API key.";
233
+ case 402:
234
+ return "Insufficient credits or spending limit reached. Check your plan at https://app.senso.ai.";
233
235
  case 403:
234
- return "Permission denied. Check your API key permissions.";
236
+ return `Permission denied: ${err.message}`;
235
237
  case 404:
236
238
  return "Resource not found.";
237
239
  case 409:
@@ -666,12 +668,17 @@ function output(format, data) {
666
668
  }
667
669
 
668
670
  // src/commands/search.ts
671
+ function parseMaxResults(value) {
672
+ const n = parseInt(value);
673
+ if (isNaN(n) || n < 1) return 5;
674
+ return Math.min(n, 20);
675
+ }
669
676
  function registerSearchCommands(program2) {
670
677
  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.");
671
- 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) => {
678
+ search.argument("<query>", "Search query").option("--max-results <n>", "Maximum number of results (max: 20)", "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) => {
672
679
  const opts = program2.opts();
673
680
  try {
674
- const body = { query, max_results: parseInt(cmdOpts.maxResults) };
681
+ const body = { query, max_results: parseMaxResults(cmdOpts.maxResults) };
675
682
  if (cmdOpts.contentIds) body.content_ids = cmdOpts.contentIds;
676
683
  if (cmdOpts.requireScopedIds) body.require_scoped_ids = true;
677
684
  const data = await apiRequest({
@@ -710,10 +717,10 @@ function registerSearchCommands(program2) {
710
717
  process.exit(1);
711
718
  }
712
719
  });
713
- 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) => {
720
+ 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 (max: 20)", "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) => {
714
721
  const opts = program2.opts();
715
722
  try {
716
- const body = { query, max_results: parseInt(cmdOpts.maxResults) };
723
+ const body = { query, max_results: parseMaxResults(cmdOpts.maxResults) };
717
724
  if (cmdOpts.contentIds) body.content_ids = cmdOpts.contentIds;
718
725
  if (cmdOpts.requireScopedIds) body.require_scoped_ids = true;
719
726
  const data = await apiRequest({
@@ -729,10 +736,10 @@ function registerSearchCommands(program2) {
729
736
  process.exit(1);
730
737
  }
731
738
  });
732
- 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) => {
739
+ 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 (max: 20)", "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) => {
733
740
  const opts = program2.opts();
734
741
  try {
735
- const body = { query, max_results: parseInt(cmdOpts.maxResults) };
742
+ const body = { query, max_results: parseMaxResults(cmdOpts.maxResults) };
736
743
  if (cmdOpts.contentIds) body.content_ids = cmdOpts.contentIds;
737
744
  if (cmdOpts.requireScopedIds) body.require_scoped_ids = true;
738
745
  const data = await apiRequest({
@@ -748,10 +755,10 @@ function registerSearchCommands(program2) {
748
755
  process.exit(1);
749
756
  }
750
757
  });
751
- search.command("full <query>").description("Alias for the default search \u2014 returns AI answer plus matching chunks. Equivalent to 'senso search <query>'.").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) => {
758
+ search.command("full <query>").description("Alias for the default search \u2014 returns AI answer plus matching chunks. Equivalent to 'senso search <query>'.").option("--max-results <n>", "Maximum results (max: 20)", "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) => {
752
759
  const opts = program2.opts();
753
760
  try {
754
- const body = { query, max_results: parseInt(cmdOpts.maxResults) };
761
+ const body = { query, max_results: parseMaxResults(cmdOpts.maxResults) };
755
762
  if (cmdOpts.contentIds) body.content_ids = cmdOpts.contentIds;
756
763
  if (cmdOpts.requireScopedIds) body.require_scoped_ids = true;
757
764
  const data = await apiRequest({
@@ -835,14 +842,14 @@ function registerIngestCommands(program2) {
835
842
  }
836
843
  try {
837
844
  const fileData = await Promise.all(files.map(getFileMetadata));
838
- const results = await apiRequest({
845
+ const response = await apiRequest({
839
846
  method: "POST",
840
847
  path: "/org/kb/upload",
841
848
  body: { files: fileData.map((f) => f.meta) },
842
849
  apiKey: opts.apiKey,
843
850
  baseUrl: opts.baseUrl
844
851
  });
845
- const items = Array.isArray(results) ? results : [];
852
+ const items = response.results ?? [];
846
853
  let uploaded = 0;
847
854
  for (const item of items) {
848
855
  if (item.status === "upload_pending" && item.upload_url) {
@@ -859,42 +866,39 @@ function registerIngestCommands(program2) {
859
866
  error(`S3 upload failed for ${item.filename}: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`);
860
867
  }
861
868
  } else {
862
- warn(`Skipped ${item.filename}: ${item.status}${item.message ? ` \u2014 ${item.message}` : ""}`);
869
+ warn(`Skipped ${item.filename}: ${item.status}${item.error ? ` \u2014 ${item.error}` : ""}`);
863
870
  }
864
871
  }
865
872
  if (uploaded > 0) {
866
873
  success(`${uploaded} file(s) uploaded. Background processing will parse, chunk, and embed them.`);
867
874
  }
868
875
  if (opts.output === "json") {
869
- console.log(JSON.stringify(results, null, 2));
876
+ console.log(JSON.stringify(response, null, 2));
870
877
  }
871
878
  } catch (err) {
872
879
  error(formatApiError(err));
873
880
  process.exit(1);
874
881
  }
875
882
  });
876
- 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) => {
883
+ ingest.command("reprocess <nodeId> <file>").description("Re-ingest an existing document with a new file version. Provide the KB node ID (kb_node_id) and the path to the replacement file.").action(async (nodeId, file) => {
877
884
  const opts = program2.opts();
878
885
  try {
879
886
  const { meta, buffer } = await getFileMetadata(file);
880
- const results = await apiRequest({
887
+ const item = await apiRequest({
881
888
  method: "PUT",
882
- path: `/org/kb/nodes/${contentId}/file`,
889
+ path: `/org/kb/nodes/${nodeId}/file`,
883
890
  body: { file: meta },
884
891
  apiKey: opts.apiKey,
885
892
  baseUrl: opts.baseUrl
886
893
  });
887
- const items = Array.isArray(results) ? results : [];
888
- for (const item of items) {
889
- if (item.status === "upload_pending" && item.upload_url) {
890
- await uploadToS3(item.upload_url, buffer, meta.content_type);
891
- success(`Uploaded ${meta.filename} for content ${contentId}. Background re-processing started.`);
892
- } else {
893
- warn(`Skipped: ${item.status}${item.message ? ` \u2014 ${item.message}` : ""}`);
894
- }
894
+ if (item.status === "upload_pending" && item.upload_url) {
895
+ await uploadToS3(item.upload_url, buffer, meta.content_type);
896
+ success(`Uploaded ${meta.filename} for node ${nodeId}. Background re-processing started.`);
897
+ } else {
898
+ warn(`Skipped: ${item.status}${item.error ? ` \u2014 ${item.error}` : ""}`);
895
899
  }
896
900
  if (opts.output === "json") {
897
- console.log(JSON.stringify(results, null, 2));
901
+ console.log(JSON.stringify(item, null, 2));
898
902
  }
899
903
  } catch (err) {
900
904
  error(formatApiError(err));
@@ -922,7 +926,7 @@ function registerContentCommands(program2) {
922
926
  json: data,
923
927
  table: {
924
928
  rows: rows.map((r) => ({
925
- id: r.node_id,
929
+ id: r.kb_node_id,
926
930
  name: r.name,
927
931
  type: r.type,
928
932
  status: r.processing_status
@@ -930,7 +934,7 @@ function registerContentCommands(program2) {
930
934
  columns: ["id", "name", "type", "status"]
931
935
  },
932
936
  plain: rows.length ? rows.map(
933
- (r) => ` ${pc6.bold(String(r.name || "Untitled"))} ${pc6.dim(`(${r.node_id})`)} ${r.type ? pc6.dim(`[${r.type}]`) : ""}`
937
+ (r) => ` ${pc6.bold(String(r.name || "Untitled"))} ${pc6.dim(`(${r.kb_node_id})`)} ${r.type ? pc6.dim(`[${r.type}]`) : ""}`
934
938
  ) : [" No content found."]
935
939
  });
936
940
  } catch (err) {
@@ -1055,7 +1059,7 @@ function registerGenerateCommands(program2) {
1055
1059
  process.exit(1);
1056
1060
  }
1057
1061
  });
1058
- 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) => {
1062
+ 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], "selected_content_type_id": "<uuid>" }').action(async (cmdOpts) => {
1059
1063
  const opts = program2.opts();
1060
1064
  try {
1061
1065
  const body = JSON.parse(cmdOpts.data);
@@ -1090,11 +1094,14 @@ function registerGenerateCommands(program2) {
1090
1094
  process.exit(1);
1091
1095
  }
1092
1096
  });
1093
- gen.command("run").description("Trigger a content generation run. Processes all prompts (or a specific subset) through the content engine. Runs asynchronously \u2014 use 'generate runs-list' to monitor progress.").option("--prompt-ids <ids...>", "Optional list of prompt IDs to process (omit to run all)").action(async (cmdOpts) => {
1097
+ gen.command("run").description("Trigger a content generation run. Processes all prompts (or a specific subset) through the content engine. Runs asynchronously \u2014 use 'generate runs-list' to monitor progress.").option("--prompt-ids <ids...>", "Optional list of prompt IDs to process (omit to run all)").option("--content-type-id <id>", "Override the org's default content type for this run").option("--publisher-ids <ids...>", "Restrict publishing to specific publisher IDs").action(async (cmdOpts) => {
1094
1098
  const opts = program2.opts();
1095
1099
  try {
1096
- const body = cmdOpts.promptIds ? { prompt_ids: cmdOpts.promptIds } : void 0;
1097
- const data = await apiRequest({ method: "POST", path: "/org/content-generation/run", body, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1100
+ const body = {};
1101
+ if (cmdOpts.promptIds) body.prompt_ids = cmdOpts.promptIds;
1102
+ if (cmdOpts.contentTypeId) body.content_type_id = cmdOpts.contentTypeId;
1103
+ if (cmdOpts.publisherIds) body.publisher_ids = cmdOpts.publisherIds;
1104
+ const data = await apiRequest({ method: "POST", path: "/org/content-generation/run", body: Object.keys(body).length > 0 ? body : {}, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1098
1105
  success("Content generation run triggered.");
1099
1106
  console.log(JSON.stringify(data, null, 2));
1100
1107
  } catch (err) {
@@ -1738,7 +1745,7 @@ function registerKBCommands(program2) {
1738
1745
  const opts = program2.opts();
1739
1746
  try {
1740
1747
  const body = { name: cmdOpts.name };
1741
- if (cmdOpts.parentId) body.parent_node_id = cmdOpts.parentId;
1748
+ if (cmdOpts.parentId) body.parent_id = cmdOpts.parentId;
1742
1749
  const data = await apiRequest({ method: "POST", path: "/org/kb/folders", body, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1743
1750
  success(`Folder "${cmdOpts.name}" created.`);
1744
1751
  console.log(JSON.stringify(data, null, 2));
@@ -1761,7 +1768,7 @@ function registerKBCommands(program2) {
1761
1768
  kb.command("move <id>").description("Move a KB node to a different parent folder.").requiredOption("--parent-id <parentId>", "Target parent folder node ID").action(async (id, cmdOpts) => {
1762
1769
  const opts = program2.opts();
1763
1770
  try {
1764
- const data = await apiRequest({ method: "PATCH", path: `/org/kb/nodes/${id}/move`, body: { parent_node_id: cmdOpts.parentId }, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1771
+ const data = await apiRequest({ method: "PATCH", path: `/org/kb/nodes/${id}/move`, body: { new_parent_id: cmdOpts.parentId }, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1765
1772
  success(`Node ${id} moved.`);
1766
1773
  console.log(JSON.stringify(data, null, 2));
1767
1774
  } catch (err) {
@@ -1779,7 +1786,7 @@ function registerKBCommands(program2) {
1779
1786
  process.exit(1);
1780
1787
  }
1781
1788
  });
1782
- kb.command("create-raw").description("Create a raw (text/markdown) content item in the knowledge base.").requiredOption("--data <json>", 'JSON: { "name": "My doc", "text": "# Hello", "kb_folder_node_id": "<uuid>" }').action(async (cmdOpts) => {
1789
+ kb.command("create-raw").description("Create a raw (text/markdown) content item in the knowledge base.").requiredOption("--data <json>", 'JSON: { "title": "My doc", "text": "# Hello", "kb_folder_node_id": "<uuid>", "tag_ids": ["<uuid>"] }').action(async (cmdOpts) => {
1783
1790
  const opts = program2.opts();
1784
1791
  try {
1785
1792
  const body = JSON.parse(cmdOpts.data);
@@ -1791,7 +1798,7 @@ function registerKBCommands(program2) {
1791
1798
  process.exit(1);
1792
1799
  }
1793
1800
  });
1794
- kb.command("update-raw <id>").description("Fully replace the text content of a raw KB node (creates a new version).").requiredOption("--data <json>", 'JSON: { "text": "# Updated content" }').action(async (id, cmdOpts) => {
1801
+ kb.command("update-raw <id>").description("Fully replace the text content of a raw KB node (creates a new version).").requiredOption("--data <json>", 'JSON: { "title": "Title", "text": "# Updated content", "tag_ids": ["<uuid>"] }').action(async (id, cmdOpts) => {
1795
1802
  const opts = program2.opts();
1796
1803
  try {
1797
1804
  const body = JSON.parse(cmdOpts.data);
@@ -1803,7 +1810,7 @@ function registerKBCommands(program2) {
1803
1810
  process.exit(1);
1804
1811
  }
1805
1812
  });
1806
- kb.command("patch-raw <id>").description("Partially update the text content of a raw KB node.").requiredOption("--data <json>", 'JSON: { "text": "Updated text", "name": "New name" }').action(async (id, cmdOpts) => {
1813
+ kb.command("patch-raw <id>").description("Partially update the text content of a raw KB node.").requiredOption("--data <json>", 'JSON: { "title": "New title", "text": "Updated text", "summary": "...", "tag_ids": ["<uuid>"] }').action(async (id, cmdOpts) => {
1807
1814
  const opts = program2.opts();
1808
1815
  try {
1809
1816
  const body = JSON.parse(cmdOpts.data);
@@ -1825,14 +1832,14 @@ function registerKBCommands(program2) {
1825
1832
  const fileData = await Promise.all(files.map(getFileMetadata2));
1826
1833
  const body = { files: fileData.map((f) => f.meta) };
1827
1834
  if (cmdOpts.folderId) body.kb_folder_node_id = cmdOpts.folderId;
1828
- const results = await apiRequest({
1835
+ const response = await apiRequest({
1829
1836
  method: "POST",
1830
1837
  path: "/org/kb/upload",
1831
1838
  body,
1832
1839
  apiKey: opts.apiKey,
1833
1840
  baseUrl: opts.baseUrl
1834
1841
  });
1835
- const items = Array.isArray(results) ? results : [];
1842
+ const items = response.results ?? [];
1836
1843
  let uploaded = 0;
1837
1844
  for (const item of items) {
1838
1845
  if (item.status === "upload_pending" && item.upload_url) {
@@ -1849,13 +1856,13 @@ function registerKBCommands(program2) {
1849
1856
  error(`S3 upload failed for ${item.filename}: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`);
1850
1857
  }
1851
1858
  } else {
1852
- warn(`Skipped ${item.filename}: ${item.status}${item.message ? ` \u2014 ${item.message}` : ""}`);
1859
+ warn(`Skipped ${item.filename}: ${item.status}${item.error ? ` \u2014 ${item.error}` : ""}`);
1853
1860
  }
1854
1861
  }
1855
1862
  if (uploaded > 0) {
1856
1863
  success(`${uploaded} file(s) uploaded. Background processing will parse, chunk, and embed them.`);
1857
1864
  }
1858
- if (opts.output === "json") console.log(JSON.stringify(results, null, 2));
1865
+ if (opts.output === "json") console.log(JSON.stringify(response, null, 2));
1859
1866
  } catch (err) {
1860
1867
  error(formatApiError(err));
1861
1868
  process.exit(1);
@@ -1865,23 +1872,20 @@ function registerKBCommands(program2) {
1865
1872
  const opts = program2.opts();
1866
1873
  try {
1867
1874
  const { meta, buffer } = await getFileMetadata2(file);
1868
- const results = await apiRequest({
1875
+ const item = await apiRequest({
1869
1876
  method: "PUT",
1870
1877
  path: `/org/kb/nodes/${id}/file`,
1871
1878
  body: { file: meta },
1872
1879
  apiKey: opts.apiKey,
1873
1880
  baseUrl: opts.baseUrl
1874
1881
  });
1875
- const items = Array.isArray(results) ? results : [];
1876
- for (const item of items) {
1877
- if (item.status === "upload_pending" && item.upload_url) {
1878
- await uploadToS32(item.upload_url, buffer, meta.content_type);
1879
- success(`Uploaded ${meta.filename} for node ${id}. Background re-processing started.`);
1880
- } else {
1881
- warn(`Skipped: ${item.status}${item.message ? ` \u2014 ${item.message}` : ""}`);
1882
- }
1882
+ if (item.status === "upload_pending" && item.upload_url) {
1883
+ await uploadToS32(item.upload_url, buffer, meta.content_type);
1884
+ success(`Uploaded ${meta.filename} for node ${id}. Background re-processing started.`);
1885
+ } else {
1886
+ warn(`Skipped: ${item.status}${item.error ? ` \u2014 ${item.error}` : ""}`);
1883
1887
  }
1884
- if (opts.output === "json") console.log(JSON.stringify(results, null, 2));
1888
+ if (opts.output === "json") console.log(JSON.stringify(item, null, 2));
1885
1889
  } catch (err) {
1886
1890
  error(formatApiError(err));
1887
1891
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senso-ai/cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.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": {