@senso-ai/cli 0.6.0 → 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 +69 -53
  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,60 +842,63 @@ 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) {
849
856
  const match = fileData.find((f) => f.meta.filename === item.filename);
850
- if (match) {
857
+ if (!match) {
858
+ error(`Could not match server filename "${item.filename}" to a local file \u2014 skipping upload.`);
859
+ continue;
860
+ }
861
+ try {
851
862
  await uploadToS3(item.upload_url, match.buffer, match.meta.content_type);
852
863
  uploaded++;
853
864
  success(`Uploaded ${item.filename} (content_id: ${item.content_id})`);
865
+ } catch (uploadErr) {
866
+ error(`S3 upload failed for ${item.filename}: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`);
854
867
  }
855
868
  } else {
856
- warn(`Skipped ${item.filename}: ${item.status}${item.message ? ` \u2014 ${item.message}` : ""}`);
869
+ warn(`Skipped ${item.filename}: ${item.status}${item.error ? ` \u2014 ${item.error}` : ""}`);
857
870
  }
858
871
  }
859
872
  if (uploaded > 0) {
860
873
  success(`${uploaded} file(s) uploaded. Background processing will parse, chunk, and embed them.`);
861
874
  }
862
875
  if (opts.output === "json") {
863
- console.log(JSON.stringify(results, null, 2));
876
+ console.log(JSON.stringify(response, null, 2));
864
877
  }
865
878
  } catch (err) {
866
879
  error(formatApiError(err));
867
880
  process.exit(1);
868
881
  }
869
882
  });
870
- 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) => {
871
884
  const opts = program2.opts();
872
885
  try {
873
886
  const { meta, buffer } = await getFileMetadata(file);
874
- const results = await apiRequest({
887
+ const item = await apiRequest({
875
888
  method: "PUT",
876
- path: `/org/kb/nodes/${contentId}/file`,
889
+ path: `/org/kb/nodes/${nodeId}/file`,
877
890
  body: { file: meta },
878
891
  apiKey: opts.apiKey,
879
892
  baseUrl: opts.baseUrl
880
893
  });
881
- const items = Array.isArray(results) ? results : [];
882
- for (const item of items) {
883
- if (item.status === "upload_pending" && item.upload_url) {
884
- await uploadToS3(item.upload_url, buffer, meta.content_type);
885
- success(`Uploaded ${meta.filename} for content ${contentId}. Background re-processing started.`);
886
- } else {
887
- warn(`Skipped: ${item.status}${item.message ? ` \u2014 ${item.message}` : ""}`);
888
- }
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}` : ""}`);
889
899
  }
890
900
  if (opts.output === "json") {
891
- console.log(JSON.stringify(results, null, 2));
901
+ console.log(JSON.stringify(item, null, 2));
892
902
  }
893
903
  } catch (err) {
894
904
  error(formatApiError(err));
@@ -916,7 +926,7 @@ function registerContentCommands(program2) {
916
926
  json: data,
917
927
  table: {
918
928
  rows: rows.map((r) => ({
919
- id: r.node_id,
929
+ id: r.kb_node_id,
920
930
  name: r.name,
921
931
  type: r.type,
922
932
  status: r.processing_status
@@ -924,7 +934,7 @@ function registerContentCommands(program2) {
924
934
  columns: ["id", "name", "type", "status"]
925
935
  },
926
936
  plain: rows.length ? rows.map(
927
- (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}]`) : ""}`
928
938
  ) : [" No content found."]
929
939
  });
930
940
  } catch (err) {
@@ -1049,7 +1059,7 @@ function registerGenerateCommands(program2) {
1049
1059
  process.exit(1);
1050
1060
  }
1051
1061
  });
1052
- 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) => {
1053
1063
  const opts = program2.opts();
1054
1064
  try {
1055
1065
  const body = JSON.parse(cmdOpts.data);
@@ -1084,11 +1094,14 @@ function registerGenerateCommands(program2) {
1084
1094
  process.exit(1);
1085
1095
  }
1086
1096
  });
1087
- 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) => {
1088
1098
  const opts = program2.opts();
1089
1099
  try {
1090
- const body = cmdOpts.promptIds ? { prompt_ids: cmdOpts.promptIds } : void 0;
1091
- 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 });
1092
1105
  success("Content generation run triggered.");
1093
1106
  console.log(JSON.stringify(data, null, 2));
1094
1107
  } catch (err) {
@@ -1732,7 +1745,7 @@ function registerKBCommands(program2) {
1732
1745
  const opts = program2.opts();
1733
1746
  try {
1734
1747
  const body = { name: cmdOpts.name };
1735
- if (cmdOpts.parentId) body.parent_node_id = cmdOpts.parentId;
1748
+ if (cmdOpts.parentId) body.parent_id = cmdOpts.parentId;
1736
1749
  const data = await apiRequest({ method: "POST", path: "/org/kb/folders", body, apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1737
1750
  success(`Folder "${cmdOpts.name}" created.`);
1738
1751
  console.log(JSON.stringify(data, null, 2));
@@ -1755,7 +1768,7 @@ function registerKBCommands(program2) {
1755
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) => {
1756
1769
  const opts = program2.opts();
1757
1770
  try {
1758
- 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 });
1759
1772
  success(`Node ${id} moved.`);
1760
1773
  console.log(JSON.stringify(data, null, 2));
1761
1774
  } catch (err) {
@@ -1773,7 +1786,7 @@ function registerKBCommands(program2) {
1773
1786
  process.exit(1);
1774
1787
  }
1775
1788
  });
1776
- 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) => {
1777
1790
  const opts = program2.opts();
1778
1791
  try {
1779
1792
  const body = JSON.parse(cmdOpts.data);
@@ -1785,7 +1798,7 @@ function registerKBCommands(program2) {
1785
1798
  process.exit(1);
1786
1799
  }
1787
1800
  });
1788
- 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) => {
1789
1802
  const opts = program2.opts();
1790
1803
  try {
1791
1804
  const body = JSON.parse(cmdOpts.data);
@@ -1797,7 +1810,7 @@ function registerKBCommands(program2) {
1797
1810
  process.exit(1);
1798
1811
  }
1799
1812
  });
1800
- 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) => {
1801
1814
  const opts = program2.opts();
1802
1815
  try {
1803
1816
  const body = JSON.parse(cmdOpts.data);
@@ -1819,31 +1832,37 @@ function registerKBCommands(program2) {
1819
1832
  const fileData = await Promise.all(files.map(getFileMetadata2));
1820
1833
  const body = { files: fileData.map((f) => f.meta) };
1821
1834
  if (cmdOpts.folderId) body.kb_folder_node_id = cmdOpts.folderId;
1822
- const results = await apiRequest({
1835
+ const response = await apiRequest({
1823
1836
  method: "POST",
1824
1837
  path: "/org/kb/upload",
1825
1838
  body,
1826
1839
  apiKey: opts.apiKey,
1827
1840
  baseUrl: opts.baseUrl
1828
1841
  });
1829
- const items = Array.isArray(results) ? results : [];
1842
+ const items = response.results ?? [];
1830
1843
  let uploaded = 0;
1831
1844
  for (const item of items) {
1832
1845
  if (item.status === "upload_pending" && item.upload_url) {
1833
1846
  const match = fileData.find((f) => f.meta.filename === item.filename);
1834
- if (match) {
1847
+ if (!match) {
1848
+ error(`Could not match server filename "${item.filename}" to a local file \u2014 skipping upload.`);
1849
+ continue;
1850
+ }
1851
+ try {
1835
1852
  await uploadToS32(item.upload_url, match.buffer, match.meta.content_type);
1836
1853
  uploaded++;
1837
1854
  success(`Uploaded ${item.filename} (content_id: ${item.content_id})`);
1855
+ } catch (uploadErr) {
1856
+ error(`S3 upload failed for ${item.filename}: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`);
1838
1857
  }
1839
1858
  } else {
1840
- warn(`Skipped ${item.filename}: ${item.status}${item.message ? ` \u2014 ${item.message}` : ""}`);
1859
+ warn(`Skipped ${item.filename}: ${item.status}${item.error ? ` \u2014 ${item.error}` : ""}`);
1841
1860
  }
1842
1861
  }
1843
1862
  if (uploaded > 0) {
1844
1863
  success(`${uploaded} file(s) uploaded. Background processing will parse, chunk, and embed them.`);
1845
1864
  }
1846
- if (opts.output === "json") console.log(JSON.stringify(results, null, 2));
1865
+ if (opts.output === "json") console.log(JSON.stringify(response, null, 2));
1847
1866
  } catch (err) {
1848
1867
  error(formatApiError(err));
1849
1868
  process.exit(1);
@@ -1853,23 +1872,20 @@ function registerKBCommands(program2) {
1853
1872
  const opts = program2.opts();
1854
1873
  try {
1855
1874
  const { meta, buffer } = await getFileMetadata2(file);
1856
- const results = await apiRequest({
1875
+ const item = await apiRequest({
1857
1876
  method: "PUT",
1858
1877
  path: `/org/kb/nodes/${id}/file`,
1859
1878
  body: { file: meta },
1860
1879
  apiKey: opts.apiKey,
1861
1880
  baseUrl: opts.baseUrl
1862
1881
  });
1863
- const items = Array.isArray(results) ? results : [];
1864
- for (const item of items) {
1865
- if (item.status === "upload_pending" && item.upload_url) {
1866
- await uploadToS32(item.upload_url, buffer, meta.content_type);
1867
- success(`Uploaded ${meta.filename} for node ${id}. Background re-processing started.`);
1868
- } else {
1869
- warn(`Skipped: ${item.status}${item.message ? ` \u2014 ${item.message}` : ""}`);
1870
- }
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}` : ""}`);
1871
1887
  }
1872
- if (opts.output === "json") console.log(JSON.stringify(results, null, 2));
1888
+ if (opts.output === "json") console.log(JSON.stringify(item, null, 2));
1873
1889
  } catch (err) {
1874
1890
  error(formatApiError(err));
1875
1891
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senso-ai/cli",
3
- "version": "0.6.0",
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": {