@senso-ai/cli 0.8.0 → 0.8.2

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 +475 -85
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -159,12 +159,30 @@ async function getLatestVersion() {
159
159
 
160
160
  // src/commands/auth.ts
161
161
  import * as p from "@clack/prompts";
162
+ import pc4 from "picocolors";
163
+
164
+ // src/lib/api-client.ts
162
165
  import pc3 from "picocolors";
163
166
 
167
+ // src/utils/logger.ts
168
+ import pc2 from "picocolors";
169
+ function success(msg) {
170
+ console.log(` ${pc2.green("\u2713")} ${msg}`);
171
+ }
172
+ function error(msg) {
173
+ console.error(` ${pc2.red("\u2717")} ${msg}`);
174
+ }
175
+ function warn(msg) {
176
+ console.error(` ${pc2.yellow("!")} ${msg}`);
177
+ }
178
+ function info(msg) {
179
+ console.log(` ${pc2.cyan("\u2139")} ${msg}`);
180
+ }
181
+
164
182
  // src/lib/api-client.ts
165
183
  var ApiError = class extends Error {
166
184
  constructor(status, statusText, body) {
167
- const msg = typeof body === "object" && body ? "error" in body ? body.error : "message" in body ? body.message : statusText : statusText;
185
+ const msg = extractErrorMessage(body, statusText);
168
186
  super(msg);
169
187
  this.status = status;
170
188
  this.statusText = statusText;
@@ -172,6 +190,19 @@ var ApiError = class extends Error {
172
190
  this.name = "ApiError";
173
191
  }
174
192
  };
193
+ function extractErrorMessage(body, fallback) {
194
+ if (typeof body !== "object" || !body) return fallback;
195
+ const b = body;
196
+ if (typeof b.error === "string") return b.error;
197
+ if (typeof b.message === "string") return b.message;
198
+ if (typeof b.detail === "string") return b.detail;
199
+ if (Array.isArray(b.errors) && b.errors.length > 0) {
200
+ return b.errors.map(
201
+ (e) => e.field ? `${e.field}: ${e.message}` : String(e.message || e)
202
+ ).join("; ");
203
+ }
204
+ return fallback;
205
+ }
175
206
  async function apiRequest(opts) {
176
207
  const apiKey = getApiKey({ apiKey: opts.apiKey });
177
208
  if (!apiKey) {
@@ -204,20 +235,20 @@ async function apiRequest(opts) {
204
235
  });
205
236
  if (!res.ok) {
206
237
  let body;
207
- const text3 = await res.text();
238
+ const text5 = await res.text();
208
239
  try {
209
- body = JSON.parse(text3);
240
+ body = JSON.parse(text5);
210
241
  } catch {
211
- body = text3;
242
+ body = text5;
212
243
  }
213
244
  throw new ApiError(res.status, res.statusText, body);
214
245
  }
215
246
  if (res.status === 204) {
216
247
  return void 0;
217
248
  }
218
- const text2 = await res.text();
249
+ const text4 = await res.text();
219
250
  try {
220
- return JSON.parse(text2);
251
+ return JSON.parse(text4);
221
252
  } catch {
222
253
  throw new Error(`Invalid JSON response from ${opts.path}`);
223
254
  }
@@ -225,6 +256,87 @@ async function apiRequest(opts) {
225
256
  clearTimeout(timeout);
226
257
  }
227
258
  }
259
+ async function apiStreamRequest(opts) {
260
+ const apiKey = getApiKey({ apiKey: opts.apiKey });
261
+ if (!apiKey) {
262
+ throw new Error(
263
+ "No API key found. Run `senso login` or set SENSO_API_KEY."
264
+ );
265
+ }
266
+ const baseUrl = getBaseUrl({ baseUrl: opts.baseUrl });
267
+ const url = new URL(`${baseUrl}${opts.path}`);
268
+ const res = await fetch(url.toString(), {
269
+ method: opts.method || "POST",
270
+ headers: {
271
+ "X-API-Key": apiKey,
272
+ Accept: "text/event-stream",
273
+ ...opts.body ? { "Content-Type": "application/json" } : {},
274
+ "User-Agent": `senso-cli/${version}`
275
+ },
276
+ body: opts.body ? JSON.stringify(opts.body) : void 0
277
+ });
278
+ if (!res.ok) {
279
+ let body;
280
+ const text4 = await res.text();
281
+ try {
282
+ body = JSON.parse(text4);
283
+ } catch {
284
+ body = text4;
285
+ }
286
+ throw new ApiError(res.status, res.statusText, body);
287
+ }
288
+ return res;
289
+ }
290
+ function uploadStatusToReason(status, error2) {
291
+ switch (status) {
292
+ case "conflict":
293
+ return "A file with the same content already exists in your knowledge base.";
294
+ case "duplicate":
295
+ return "This file has already been uploaded.";
296
+ case "invalid":
297
+ return error2 || "This file type is not supported.";
298
+ default:
299
+ return error2 || `Unexpected status: ${status}`;
300
+ }
301
+ }
302
+ function printUploadSummary(uploaded, failed, items) {
303
+ const total = items.length;
304
+ console.log();
305
+ console.log(` ${pc3.bold("Upload Summary")} \u2014 ${uploaded}/${total} file(s) uploaded`);
306
+ console.log();
307
+ if (uploaded > 0) {
308
+ for (const item of items) {
309
+ if (item.status === "upload_pending" && !failed.find((f) => f.filename === item.filename)) {
310
+ success(`${item.filename}`);
311
+ }
312
+ }
313
+ }
314
+ if (failed.length > 0) {
315
+ for (const f of failed) {
316
+ error(`${f.filename} \u2014 ${f.reason}`);
317
+ }
318
+ }
319
+ if (uploaded > 0) {
320
+ console.log();
321
+ info("Background processing will parse, chunk, and embed the uploaded files.");
322
+ }
323
+ if (uploaded === 0 && total > 0) {
324
+ console.log();
325
+ error("No files were uploaded. Please review the issues above and try again.");
326
+ }
327
+ }
328
+ function handleUploadError(err) {
329
+ if (err instanceof ApiError && err.body && typeof err.body === "object" && "results" in err.body) {
330
+ const errorResponse = err.body;
331
+ for (const item of errorResponse.results ?? []) {
332
+ const reason = uploadStatusToReason(item.status, item.error);
333
+ error(`${item.filename} \u2014 ${reason}`);
334
+ }
335
+ error("No files were uploaded. Please review the issues above and try again.");
336
+ } else {
337
+ error(formatApiError(err));
338
+ }
339
+ }
228
340
  function formatApiError(err) {
229
341
  if (err instanceof ApiError) {
230
342
  switch (err.status) {
@@ -257,21 +369,6 @@ function formatApiError(err) {
257
369
  return String(err);
258
370
  }
259
371
 
260
- // src/utils/logger.ts
261
- import pc2 from "picocolors";
262
- function success(msg) {
263
- console.log(` ${pc2.green("\u2713")} ${msg}`);
264
- }
265
- function error(msg) {
266
- console.error(` ${pc2.red("\u2717")} ${msg}`);
267
- }
268
- function warn(msg) {
269
- console.error(` ${pc2.yellow("!")} ${msg}`);
270
- }
271
- function info(msg) {
272
- console.log(` ${pc2.cyan("\u2139")} ${msg}`);
273
- }
274
-
275
372
  // src/commands/auth.ts
276
373
  async function verifyApiKey(apiKey, baseUrl) {
277
374
  return apiRequest({
@@ -284,10 +381,10 @@ function registerAuthCommands(program2) {
284
381
  program2.command("login").description("Authenticate with Senso. Paste your API key and it will be validated against your organization, then stored locally.").action(async () => {
285
382
  const opts = program2.opts();
286
383
  banner();
287
- console.log(` ${pc3.bold("Welcome to Senso CLI!")}
384
+ console.log(` ${pc4.bold("Welcome to Senso CLI!")}
288
385
  `);
289
- console.log(` ${pc3.dim("1.")} Go to ${pc3.cyan("https://docs.senso.ai")} to create an account`);
290
- console.log(` ${pc3.dim("2.")} Generate an API key from your dashboard
386
+ console.log(` ${pc4.dim("1.")} Go to ${pc4.cyan("https://docs.senso.ai")} to create an account`);
387
+ console.log(` ${pc4.dim("2.")} Generate an API key from your dashboard
291
388
  `);
292
389
  const result = await p.text({
293
390
  message: "Paste your API key:",
@@ -314,8 +411,8 @@ function registerAuthCommands(program2) {
314
411
  orgSlug: org.slug,
315
412
  isFreeTier: org.is_free_tier
316
413
  });
317
- success(`Authenticated as ${pc3.bold(`"${org.name}"`)} (${pc3.dim(org.org_id)})`);
318
- success(`Config saved to ${pc3.dim(getConfigPath())}`);
414
+ success(`Authenticated as ${pc4.bold(`"${org.name}"`)} (${pc4.dim(org.org_id)})`);
415
+ success(`Config saved to ${pc4.dim(getConfigPath())}`);
319
416
  console.log();
320
417
  } catch (err) {
321
418
  spin.stop("Verification failed");
@@ -350,19 +447,19 @@ function registerAuthCommands(program2) {
350
447
  );
351
448
  } else {
352
449
  console.log();
353
- console.log(` ${pc3.bold("Organization:")} ${org.name}`);
354
- console.log(` ${pc3.bold("Org ID:")} ${org.org_id}`);
355
- console.log(` ${pc3.bold("Slug:")} ${org.slug}`);
356
- console.log(` ${pc3.bold("Tier:")} ${org.is_free_tier ? "Free" : "Paid"}`);
357
- console.log(` ${pc3.bold("API Key:")} ${apiKey.slice(0, 8)}...`);
358
- console.log(` ${pc3.bold("Config:")} ${getConfigPath()}`);
450
+ console.log(` ${pc4.bold("Organization:")} ${org.name}`);
451
+ console.log(` ${pc4.bold("Org ID:")} ${org.org_id}`);
452
+ console.log(` ${pc4.bold("Slug:")} ${org.slug}`);
453
+ console.log(` ${pc4.bold("Tier:")} ${org.is_free_tier ? "Free" : "Paid"}`);
454
+ console.log(` ${pc4.bold("API Key:")} ${apiKey.slice(0, 8)}...`);
455
+ console.log(` ${pc4.bold("Config:")} ${getConfigPath()}`);
359
456
  console.log();
360
457
  }
361
458
  } catch (err) {
362
459
  if (config.orgName) {
363
460
  warn(`Could not reach API: ${formatApiError(err)}`);
364
- console.log(` ${pc3.bold("Organization:")} ${config.orgName} ${pc3.dim("(cached)")}`);
365
- console.log(` ${pc3.bold("Org ID:")} ${config.orgId}`);
461
+ console.log(` ${pc4.bold("Organization:")} ${config.orgName} ${pc4.dim("(cached)")}`);
462
+ console.log(` ${pc4.bold("Org ID:")} ${config.orgId}`);
366
463
  } else {
367
464
  error(formatApiError(err));
368
465
  process.exit(1);
@@ -615,16 +712,16 @@ function registerApiKeyCommands(program2) {
615
712
  }
616
713
 
617
714
  // src/commands/search.ts
618
- import pc5 from "picocolors";
715
+ import pc6 from "picocolors";
619
716
 
620
717
  // src/lib/output.ts
621
- import pc4 from "picocolors";
718
+ import pc5 from "picocolors";
622
719
  function outputJson(data) {
623
720
  console.log(JSON.stringify(data, null, 2));
624
721
  }
625
722
  function outputTable(rows, columns) {
626
723
  if (rows.length === 0) {
627
- console.log(pc4.dim(" No results."));
724
+ console.log(pc5.dim(" No results."));
628
725
  return;
629
726
  }
630
727
  const cols = columns || Object.keys(rows[0]);
@@ -635,7 +732,7 @@ function outputTable(rows, columns) {
635
732
  );
636
733
  return Math.max(col.length, maxVal);
637
734
  });
638
- const header = cols.map((col, i) => pc4.bold(col.padEnd(widths[i]))).join(" ");
735
+ const header = cols.map((col, i) => pc5.bold(col.padEnd(widths[i]))).join(" ");
639
736
  console.log(` ${header}`);
640
737
  console.log(` ${widths.map((w) => "\u2500".repeat(w)).join(" ")}`);
641
738
  for (const row of rows) {
@@ -702,12 +799,12 @@ function registerSearchCommands(program2) {
702
799
  } : void 0,
703
800
  plain: [
704
801
  "",
705
- res.answer ? ` ${pc5.bold("Answer:")} ${res.answer}` : "",
802
+ res.answer ? ` ${pc6.bold("Answer:")} ${res.answer}` : "",
706
803
  "",
707
804
  ...(res.results || []).map(
708
- (r, i) => ` ${pc5.dim(`${i + 1}.`)} ${pc5.bold(String(r.title || "Untitled"))}
805
+ (r, i) => ` ${pc6.dim(`${i + 1}.`)} ${pc6.bold(String(r.title || "Untitled"))}
709
806
  ${String(r.chunk_text || "").slice(0, 120)}
710
- ${pc5.dim(`ID: ${r.content_id}`)}`
807
+ ${pc6.dim(`ID: ${r.content_id}`)}`
711
808
  ),
712
809
  ""
713
810
  ].filter(Boolean)
@@ -774,6 +871,86 @@ function registerSearchCommands(program2) {
774
871
  process.exit(1);
775
872
  }
776
873
  });
874
+ search.command("stream <query>").description("Streaming search \u2014 returns AI answer tokens in real-time via SSE, followed by source chunks. Use this for a responsive, live search experience.").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) => {
875
+ const opts = program2.opts();
876
+ const body = { query, max_results: parseMaxResults(cmdOpts.maxResults) };
877
+ if (cmdOpts.contentIds) body.content_ids = cmdOpts.contentIds;
878
+ if (cmdOpts.requireScopedIds) body.require_scoped_ids = true;
879
+ try {
880
+ const res = await apiStreamRequest({
881
+ method: "POST",
882
+ path: "/org/search/stream",
883
+ body,
884
+ apiKey: opts.apiKey,
885
+ baseUrl: opts.baseUrl
886
+ });
887
+ if (!res.body) {
888
+ error("No response body received.");
889
+ process.exit(1);
890
+ }
891
+ const reader = res.body.getReader();
892
+ const decoder = new TextDecoder();
893
+ let buffer = "";
894
+ let eventType = null;
895
+ let answerStarted = false;
896
+ while (true) {
897
+ const { done, value } = await reader.read();
898
+ if (done) break;
899
+ buffer += decoder.decode(value, { stream: true });
900
+ const lines = buffer.split("\n");
901
+ buffer = lines.pop() || "";
902
+ for (const line of lines) {
903
+ if (line.startsWith("event: ")) {
904
+ eventType = line.slice(7).trim();
905
+ } else if (line.startsWith("data: ") && eventType) {
906
+ const data = JSON.parse(line.slice(6));
907
+ switch (eventType) {
908
+ case "token":
909
+ if (!answerStarted) {
910
+ answerStarted = true;
911
+ process.stdout.write(`
912
+ ${pc6.bold("Answer:")} `);
913
+ }
914
+ process.stdout.write(data.token);
915
+ break;
916
+ case "sources": {
917
+ if (answerStarted) process.stdout.write("\n");
918
+ console.log();
919
+ const results = data.results || [];
920
+ if (results.length > 0) {
921
+ console.log(` ${pc6.bold("Sources:")} (${results.length})`);
922
+ for (let i = 0; i < results.length; i++) {
923
+ const r = results[i];
924
+ console.log();
925
+ console.log(` ${pc6.dim(`${i + 1}.`)} ${pc6.bold(r.title || "Untitled")} ${pc6.dim(`(${r.content_id})`)}`);
926
+ if (r.chunk_text) {
927
+ console.log(` ${pc6.dim("Snippet:")} ${r.chunk_text}`);
928
+ }
929
+ }
930
+ } else {
931
+ console.log(` ${pc6.dim("No sources found.")}`);
932
+ }
933
+ console.log();
934
+ if (opts.output === "json") {
935
+ console.log(JSON.stringify(data, null, 2));
936
+ }
937
+ break;
938
+ }
939
+ case "error":
940
+ error(`Stream error: ${data.error}`);
941
+ break;
942
+ case "done":
943
+ break;
944
+ }
945
+ eventType = null;
946
+ }
947
+ }
948
+ }
949
+ } catch (err) {
950
+ error(formatApiError(err));
951
+ process.exit(1);
952
+ }
953
+ });
777
954
  }
778
955
  function outputByFormat(format, data) {
779
956
  if (format === "json") {
@@ -785,8 +962,168 @@ function outputByFormat(format, data) {
785
962
 
786
963
  // src/commands/ingest.ts
787
964
  import { createHash } from "crypto";
788
- import { readFile, stat } from "fs/promises";
965
+ import { access, readFile, stat } from "fs/promises";
789
966
  import { basename, resolve } from "path";
967
+ import * as p3 from "@clack/prompts";
968
+ import pc8 from "picocolors";
969
+
970
+ // src/lib/folder-picker.ts
971
+ import * as p2 from "@clack/prompts";
972
+ import pc7 from "picocolors";
973
+ var PAGE_SIZE = 50;
974
+ async function fetchFolders(parentId, offset, opts) {
975
+ if (parentId) {
976
+ return apiRequest({
977
+ path: `/org/kb/nodes/${parentId}/children`,
978
+ params: { type: "folder", limit: PAGE_SIZE, offset },
979
+ apiKey: opts.apiKey,
980
+ baseUrl: opts.baseUrl
981
+ });
982
+ }
983
+ return apiRequest({
984
+ path: "/org/kb/my-files",
985
+ params: { type: "folder", limit: PAGE_SIZE, offset },
986
+ apiKey: opts.apiKey,
987
+ baseUrl: opts.baseUrl
988
+ });
989
+ }
990
+ async function createFolder(name, parentId, opts) {
991
+ const body = { name };
992
+ if (parentId) body.parent_id = parentId;
993
+ try {
994
+ const data = await apiRequest({
995
+ method: "POST",
996
+ path: "/org/kb/folders",
997
+ body,
998
+ apiKey: opts.apiKey,
999
+ baseUrl: opts.baseUrl
1000
+ });
1001
+ success(`Folder "${name}" created.`);
1002
+ return { folderId: data.kb_node_id, folderName: name };
1003
+ } catch (err) {
1004
+ if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
1005
+ error(
1006
+ "Verify with the org admin that you have the proper scope to create under the org's root folder."
1007
+ );
1008
+ process.exit(1);
1009
+ }
1010
+ throw err;
1011
+ }
1012
+ }
1013
+ async function promptCreateFolder(parentId, opts) {
1014
+ const name = await p2.text({
1015
+ message: "Enter a name for your new folder:",
1016
+ validate: (val) => {
1017
+ if (!val || val.trim().length === 0) return "Folder name is required";
1018
+ }
1019
+ });
1020
+ if (p2.isCancel(name)) {
1021
+ p2.cancel("Upload cancelled.");
1022
+ process.exit(0);
1023
+ }
1024
+ return createFolder(name.trim(), parentId, opts);
1025
+ }
1026
+ async function pickFolder(opts) {
1027
+ const navigationStack = [];
1028
+ let currentParentId = null;
1029
+ let currentOffset = 0;
1030
+ let loadedFolders = [];
1031
+ let totalCount = 0;
1032
+ let needsFetch = true;
1033
+ while (true) {
1034
+ if (needsFetch) {
1035
+ const spin = p2.spinner();
1036
+ spin.start("Loading folders...");
1037
+ try {
1038
+ const response = await fetchFolders(currentParentId, currentOffset, opts);
1039
+ if (currentOffset === 0) {
1040
+ loadedFolders = response.nodes;
1041
+ } else {
1042
+ loadedFolders = [...loadedFolders, ...response.nodes];
1043
+ }
1044
+ totalCount = response.total;
1045
+ spin.stop(
1046
+ `${loadedFolders.length} folder(s) loaded${loadedFolders.length < totalCount ? ` of ${totalCount}` : ""}`
1047
+ );
1048
+ needsFetch = false;
1049
+ if (loadedFolders.length === 0 && currentParentId === null) {
1050
+ info("No folders found. Let's create one.");
1051
+ return promptCreateFolder(null, opts);
1052
+ }
1053
+ } catch (err) {
1054
+ spin.stop("Failed to load folders");
1055
+ error(formatApiError(err));
1056
+ process.exit(1);
1057
+ }
1058
+ }
1059
+ const breadcrumb = navigationStack.length === 0 ? "My Files" : "My Files > " + navigationStack.map((s) => s.name).join(" > ");
1060
+ const currentFolderName = navigationStack.length > 0 ? navigationStack[navigationStack.length - 1].name : null;
1061
+ console.log();
1062
+ console.log(` ${pc7.bold("Location:")} ${breadcrumb}`);
1063
+ console.log();
1064
+ if (currentFolderName) {
1065
+ console.log(` ${pc7.dim("Use arrow keys to navigate, Enter to select.")}`);
1066
+ console.log(` ${pc7.dim("Pick a folder to open it, or choose an action below the list.")}`);
1067
+ } else {
1068
+ console.log(` ${pc7.dim("Use arrow keys to navigate, Enter to select a folder to open it.")}`);
1069
+ }
1070
+ console.log();
1071
+ const options = [];
1072
+ for (const folder2 of loadedFolders) {
1073
+ options.push({ value: folder2.kb_node_id, label: `\u{1F4C1} ${folder2.name}` });
1074
+ }
1075
+ if (loadedFolders.length < totalCount) {
1076
+ options.push({ value: "__LOAD_MORE__", label: pc7.dim("Load more...") });
1077
+ }
1078
+ if (currentFolderName) {
1079
+ options.push({ value: "__SELECT_CURRENT__", label: pc7.green(`\u2713 Select "${currentFolderName}"`) });
1080
+ options.push({ value: "__BACK__", label: pc7.dim("\u2190 Go back") });
1081
+ }
1082
+ options.push({
1083
+ value: "__NEW_FOLDER__",
1084
+ label: pc7.cyan("+ Create new folder here")
1085
+ });
1086
+ const choice = await p2.select({
1087
+ message: "Choose a folder or action:",
1088
+ options
1089
+ });
1090
+ if (p2.isCancel(choice)) {
1091
+ p2.cancel("Upload cancelled.");
1092
+ process.exit(0);
1093
+ }
1094
+ const selected = choice;
1095
+ if (selected === "__BACK__") {
1096
+ navigationStack.pop();
1097
+ currentParentId = navigationStack.length > 0 ? navigationStack[navigationStack.length - 1].id : null;
1098
+ currentOffset = 0;
1099
+ loadedFolders = [];
1100
+ needsFetch = true;
1101
+ continue;
1102
+ }
1103
+ if (selected === "__LOAD_MORE__") {
1104
+ currentOffset += PAGE_SIZE;
1105
+ needsFetch = true;
1106
+ continue;
1107
+ }
1108
+ if (selected === "__SELECT_CURRENT__") {
1109
+ const current = navigationStack[navigationStack.length - 1];
1110
+ return { folderId: current.id, folderName: current.name };
1111
+ }
1112
+ if (selected === "__NEW_FOLDER__") {
1113
+ return promptCreateFolder(currentParentId, opts);
1114
+ }
1115
+ const folder = loadedFolders.find((f) => f.kb_node_id === selected);
1116
+ if (folder) {
1117
+ navigationStack.push({ id: folder.kb_node_id, name: folder.name });
1118
+ currentParentId = folder.kb_node_id;
1119
+ currentOffset = 0;
1120
+ loadedFolders = [];
1121
+ needsFetch = true;
1122
+ }
1123
+ }
1124
+ }
1125
+
1126
+ // src/commands/ingest.ts
790
1127
  var MIME_TYPES = {
791
1128
  ".pdf": "application/pdf",
792
1129
  ".txt": "text/plain",
@@ -834,49 +1171,98 @@ async function uploadToS3(url, buffer, contentType) {
834
1171
  }
835
1172
  function registerIngestCommands(program2) {
836
1173
  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.");
837
- 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) => {
1174
+ 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.").option("--folder-id <id>", "Destination folder ID (skip interactive prompt)").action(async (files, cmdOpts) => {
838
1175
  const opts = program2.opts();
839
1176
  if (files.length > 10) {
840
1177
  error("Maximum 10 files per upload request.");
841
1178
  process.exit(1);
842
1179
  }
843
- try {
1180
+ for (const file of files) {
1181
+ try {
1182
+ await access(resolve(file));
1183
+ } catch {
1184
+ error(`File not found: "${file}". Please check the file name and try again.`);
1185
+ process.exit(1);
1186
+ }
1187
+ }
1188
+ const prepSpin = p3.spinner();
1189
+ try {
1190
+ let kbFolderNodeId;
1191
+ if (cmdOpts.folderId) {
1192
+ kbFolderNodeId = cmdOpts.folderId;
1193
+ } else if (process.stdin.isTTY) {
1194
+ const folder = await pickFolder({ apiKey: opts.apiKey, baseUrl: opts.baseUrl });
1195
+ const fileList = files.map((f) => basename(f)).join(", ");
1196
+ const answer = await p3.text({
1197
+ message: `You want to upload ${pc8.bold(`"${fileList}"`)} to the folder ${pc8.bold(pc8.cyan(`"${folder.folderName}"`))}? Type 'yes' or 'no' to continue:`,
1198
+ validate: (val) => {
1199
+ const v = val.trim().toLowerCase();
1200
+ if (v !== "yes" && v !== "no") return "Please type 'yes' or 'no'";
1201
+ }
1202
+ });
1203
+ if (p3.isCancel(answer) || answer.trim().toLowerCase() === "no") {
1204
+ p3.cancel("Upload cancelled.");
1205
+ process.exit(0);
1206
+ }
1207
+ kbFolderNodeId = folder.folderId;
1208
+ }
844
1209
  const fileData = await Promise.all(files.map(getFileMetadata));
1210
+ const emptyFiles = fileData.filter((f) => f.meta.file_size_bytes < 1);
1211
+ if (emptyFiles.length > 0) {
1212
+ for (const f of emptyFiles) {
1213
+ error(`File "${f.meta.filename}" is empty. Please select a valid file with content.`);
1214
+ }
1215
+ process.exit(1);
1216
+ }
1217
+ const body = { files: fileData.map((f) => f.meta) };
1218
+ if (kbFolderNodeId) body.kb_folder_node_id = kbFolderNodeId;
1219
+ prepSpin.start("Preparing upload...");
845
1220
  const response = await apiRequest({
846
1221
  method: "POST",
847
1222
  path: "/org/kb/upload",
848
- body: { files: fileData.map((f) => f.meta) },
1223
+ body,
849
1224
  apiKey: opts.apiKey,
850
1225
  baseUrl: opts.baseUrl
851
1226
  });
852
1227
  const items = response.results ?? [];
1228
+ const pendingCount = items.filter((i) => i.status === "upload_pending" && i.upload_url).length;
1229
+ prepSpin.stop(`${pendingCount} file(s) ready for upload`);
853
1230
  let uploaded = 0;
1231
+ const failed = [];
854
1232
  for (const item of items) {
855
1233
  if (item.status === "upload_pending" && item.upload_url) {
856
1234
  const match = fileData.find((f) => f.meta.filename === item.filename);
857
1235
  if (!match) {
858
- error(`Could not match server filename "${item.filename}" to a local file \u2014 skipping upload.`);
1236
+ failed.push({ filename: item.filename, reason: "Could not match to a local file." });
859
1237
  continue;
860
1238
  }
1239
+ const uploadSpin = p3.spinner();
1240
+ uploadSpin.start(`Uploading ${item.filename}...`);
861
1241
  try {
862
1242
  await uploadToS3(item.upload_url, match.buffer, match.meta.content_type);
863
1243
  uploaded++;
864
- success(`Uploaded ${item.filename} (content_id: ${item.content_id})`);
1244
+ uploadSpin.stop(`Uploaded ${item.filename}`);
865
1245
  } catch (uploadErr) {
866
- error(`S3 upload failed for ${item.filename}: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`);
1246
+ uploadSpin.stop(`Failed to upload ${item.filename}`);
1247
+ failed.push({
1248
+ filename: item.filename,
1249
+ reason: `Upload failed: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
1250
+ });
867
1251
  }
868
1252
  } else {
869
- warn(`Skipped ${item.filename}: ${item.status}${item.error ? ` \u2014 ${item.error}` : ""}`);
1253
+ failed.push({
1254
+ filename: item.filename,
1255
+ reason: uploadStatusToReason(item.status, item.error)
1256
+ });
870
1257
  }
871
1258
  }
872
- if (uploaded > 0) {
873
- success(`${uploaded} file(s) uploaded. Background processing will parse, chunk, and embed them.`);
874
- }
1259
+ printUploadSummary(uploaded, failed, items);
875
1260
  if (opts.output === "json") {
876
1261
  console.log(JSON.stringify(response, null, 2));
877
1262
  }
878
1263
  } catch (err) {
879
- error(formatApiError(err));
1264
+ prepSpin.stop("Upload failed");
1265
+ handleUploadError(err);
880
1266
  process.exit(1);
881
1267
  }
882
1268
  });
@@ -908,7 +1294,7 @@ function registerIngestCommands(program2) {
908
1294
  }
909
1295
 
910
1296
  // src/commands/content.ts
911
- import pc6 from "picocolors";
1297
+ import pc9 from "picocolors";
912
1298
  function registerContentCommands(program2) {
913
1299
  const content = program2.command("content").description("Manage content items in the knowledge base. List, inspect, delete, unpublish, and manage the verification workflow and ownership of content.");
914
1300
  content.command("list").description("List top-level files and folders in the knowledge base. Use 'kb my-files' for the same result with richer KB node output.").option("--limit <n>", "Items per page", "10").option("--offset <n>", "Pagination offset", "0").action(async (cmdOpts) => {
@@ -934,7 +1320,7 @@ function registerContentCommands(program2) {
934
1320
  columns: ["id", "name", "type", "status"]
935
1321
  },
936
1322
  plain: rows.length ? rows.map(
937
- (r) => ` ${pc6.bold(String(r.name || "Untitled"))} ${pc6.dim(`(${r.kb_node_id})`)} ${r.type ? pc6.dim(`[${r.type}]`) : ""}`
1323
+ (r) => ` ${pc9.bold(String(r.name || "Untitled"))} ${pc9.dim(`(${r.kb_node_id})`)} ${r.type ? pc9.dim(`[${r.type}]`) : ""}`
938
1324
  ) : [" No content found."]
939
1325
  });
940
1326
  } catch (err) {
@@ -1493,12 +1879,12 @@ import { execFile } from "child_process";
1493
1879
  import { promisify } from "util";
1494
1880
  var execFileAsync = promisify(execFile);
1495
1881
  var SENSO_SKILLS = [
1496
- "@senso/senso-search",
1497
- "@senso/senso-ingest",
1498
- "@senso/senso-content-gen",
1499
- "@senso/senso-brand-setup",
1500
- "@senso/senso-kb-organize",
1501
- "@senso/senso-review-publish"
1882
+ "senso-ai/senso-search",
1883
+ "senso-ai/senso-ingest",
1884
+ "senso-ai/senso-content-gen",
1885
+ "senso-ai/senso-brand-setup",
1886
+ "senso-ai/senso-kb-organize",
1887
+ "senso-ai/senso-review-publish"
1502
1888
  ];
1503
1889
  var AGENT_FLAGS = {
1504
1890
  claude: "--claude",
@@ -1542,7 +1928,7 @@ function registerSkillsCommands(program2) {
1542
1928
  } else {
1543
1929
  skillPackages = names.map((n) => {
1544
1930
  if (n.startsWith("@")) return n;
1545
- return `@senso/senso-${n}`;
1931
+ return `senso-ai/senso-${n}`;
1546
1932
  });
1547
1933
  }
1548
1934
  let agentFlags;
@@ -1567,10 +1953,10 @@ function registerSkillsCommands(program2) {
1567
1953
  if (stdout.trim()) console.log(stdout.trim());
1568
1954
  if (stderr.trim()) console.error(stderr.trim());
1569
1955
  }
1570
- const shortName = pkg.replace("@senso/senso-", "");
1956
+ const shortName = pkg.replace("senso-ai/senso-", "");
1571
1957
  success(`Installed ${shortName}`);
1572
1958
  } catch (err) {
1573
- const shortName = pkg.replace("@senso/senso-", "");
1959
+ const shortName = pkg.replace("senso-ai/senso-", "");
1574
1960
  const msg = err instanceof Error ? err.message : String(err);
1575
1961
  error(`Failed to install ${shortName}: ${msg}`);
1576
1962
  }
@@ -1601,7 +1987,7 @@ function registerSkillsCommands(program2) {
1601
1987
  const opts = program2.opts();
1602
1988
  const available = SENSO_SKILLS.map((pkg) => ({
1603
1989
  package: pkg,
1604
- shortName: pkg.replace("@senso/senso-", "")
1990
+ shortName: pkg.replace("senso-ai/senso-", "")
1605
1991
  }));
1606
1992
  if (opts.output === "json") {
1607
1993
  console.log(JSON.stringify(available, null, 2));
@@ -1616,7 +2002,7 @@ function registerSkillsCommands(program2) {
1616
2002
  });
1617
2003
  skills.command("remove <name>").description("Remove an installed Senso skill. Use the short name (e.g., search, ingest, content-gen).").option("--global", "Remove from global install").action(async (name, cmdOpts) => {
1618
2004
  const opts = program2.opts();
1619
- const pkg = name.startsWith("@") ? name : `@senso/senso-${name}`;
2005
+ const pkg = name.startsWith("@") ? name : `senso-ai/senso-${name}`;
1620
2006
  const globalFlag = cmdOpts.global ? ["--global"] : [];
1621
2007
  try {
1622
2008
  const { stdout, stderr } = await runShipables(["uninstall", pkg, ...globalFlag, "--yes"]);
@@ -1624,7 +2010,7 @@ function registerSkillsCommands(program2) {
1624
2010
  if (stdout.trim()) console.log(stdout.trim());
1625
2011
  if (stderr.trim()) console.error(stderr.trim());
1626
2012
  }
1627
- const shortName = pkg.replace("@senso/senso-", "");
2013
+ const shortName = pkg.replace("senso-ai/senso-", "");
1628
2014
  success(`Removed ${shortName}`);
1629
2015
  } catch (err) {
1630
2016
  error(err instanceof Error ? err.message : String(err));
@@ -1781,12 +2167,12 @@ function registerKBCommands(program2) {
1781
2167
  process.exit(1);
1782
2168
  }
1783
2169
  });
1784
- kb.command("my-files").description("List top-level files and folders in the knowledge base.").option("--limit <n>", "Items per page", "50").option("--offset <n>", "Pagination offset", "0").action(async (cmdOpts) => {
2170
+ kb.command("my-files").description("List top-level files and folders in the knowledge base.").option("--limit <n>", "Items per page", "50").option("--offset <n>", "Pagination offset", "0").option("--type <type>", "Filter by node type (folder or content)").action(async (cmdOpts) => {
1785
2171
  const opts = program2.opts();
1786
2172
  try {
1787
2173
  const data = await apiRequest({
1788
2174
  path: "/org/kb/my-files",
1789
- params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
2175
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset, type: cmdOpts.type },
1790
2176
  apiKey: opts.apiKey,
1791
2177
  baseUrl: opts.baseUrl
1792
2178
  });
@@ -1796,12 +2182,12 @@ function registerKBCommands(program2) {
1796
2182
  process.exit(1);
1797
2183
  }
1798
2184
  });
1799
- kb.command("find").description("Search KB nodes by name.").requiredOption("--query <q>", "Name search query").option("--limit <n>", "Items per page", "20").option("--offset <n>", "Pagination offset", "0").action(async (cmdOpts) => {
2185
+ kb.command("find").description("Search KB nodes by name.").requiredOption("--query <q>", "Name search query").option("--limit <n>", "Items per page", "20").option("--offset <n>", "Pagination offset", "0").option("--type <type>", "Filter by node type (folder or content)").action(async (cmdOpts) => {
1800
2186
  const opts = program2.opts();
1801
2187
  try {
1802
2188
  const data = await apiRequest({
1803
2189
  path: "/org/kb/find",
1804
- params: { q: cmdOpts.query, limit: cmdOpts.limit, offset: cmdOpts.offset },
2190
+ params: { q: cmdOpts.query, limit: cmdOpts.limit, offset: cmdOpts.offset, type: cmdOpts.type },
1805
2191
  apiKey: opts.apiKey,
1806
2192
  baseUrl: opts.baseUrl
1807
2193
  });
@@ -1831,12 +2217,12 @@ function registerKBCommands(program2) {
1831
2217
  process.exit(1);
1832
2218
  }
1833
2219
  });
1834
- kb.command("children <id>").description("List children of a KB folder node.").option("--limit <n>", "Items per page", "50").option("--offset <n>", "Pagination offset", "0").action(async (id, cmdOpts) => {
2220
+ kb.command("children <id>").description("List children of a KB folder node.").option("--limit <n>", "Items per page", "50").option("--offset <n>", "Pagination offset", "0").option("--type <type>", "Filter by node type (folder or content)").action(async (id, cmdOpts) => {
1835
2221
  const opts = program2.opts();
1836
2222
  try {
1837
2223
  const data = await apiRequest({
1838
2224
  path: `/org/kb/nodes/${id}/children`,
1839
- params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
2225
+ params: { limit: cmdOpts.limit, offset: cmdOpts.offset, type: cmdOpts.type },
1840
2226
  apiKey: opts.apiKey,
1841
2227
  baseUrl: opts.baseUrl
1842
2228
  });
@@ -1986,30 +2372,34 @@ function registerKBCommands(program2) {
1986
2372
  });
1987
2373
  const items = response.results ?? [];
1988
2374
  let uploaded = 0;
2375
+ const failed = [];
1989
2376
  for (const item of items) {
1990
2377
  if (item.status === "upload_pending" && item.upload_url) {
1991
2378
  const match = fileData.find((f) => f.meta.filename === item.filename);
1992
2379
  if (!match) {
1993
- error(`Could not match server filename "${item.filename}" to a local file \u2014 skipping upload.`);
2380
+ failed.push({ filename: item.filename, reason: "Could not match to a local file." });
1994
2381
  continue;
1995
2382
  }
1996
2383
  try {
1997
2384
  await uploadToS32(item.upload_url, match.buffer, match.meta.content_type);
1998
2385
  uploaded++;
1999
- success(`Uploaded ${item.filename} (content_id: ${item.content_id})`);
2000
2386
  } catch (uploadErr) {
2001
- error(`S3 upload failed for ${item.filename}: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`);
2387
+ failed.push({
2388
+ filename: item.filename,
2389
+ reason: `Upload failed: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
2390
+ });
2002
2391
  }
2003
2392
  } else {
2004
- warn(`Skipped ${item.filename}: ${item.status}${item.error ? ` \u2014 ${item.error}` : ""}`);
2393
+ failed.push({
2394
+ filename: item.filename,
2395
+ reason: uploadStatusToReason(item.status, item.error)
2396
+ });
2005
2397
  }
2006
2398
  }
2007
- if (uploaded > 0) {
2008
- success(`${uploaded} file(s) uploaded. Background processing will parse, chunk, and embed them.`);
2009
- }
2399
+ printUploadSummary(uploaded, failed, items);
2010
2400
  if (opts.output === "json") console.log(JSON.stringify(response, null, 2));
2011
2401
  } catch (err) {
2012
- error(formatApiError(err));
2402
+ handleUploadError(err);
2013
2403
  process.exit(1);
2014
2404
  }
2015
2405
  });
@@ -2055,12 +2445,12 @@ function registerPermissionsCommands(program2) {
2055
2445
 
2056
2446
  // src/commands/update.ts
2057
2447
  import semver2 from "semver";
2058
- import pc7 from "picocolors";
2448
+ import pc10 from "picocolors";
2059
2449
  import { execSync } from "child_process";
2060
2450
  var NPM_PACKAGE2 = "@senso-ai/cli";
2061
2451
  function registerUpdateCommand(program2) {
2062
2452
  program2.command("update").description("Update CLI to the latest version").action(async () => {
2063
- info(`Current version: ${pc7.bold(version)}`);
2453
+ info(`Current version: ${pc10.bold(version)}`);
2064
2454
  info("Checking npm for updates...");
2065
2455
  const latest = await getLatestVersion();
2066
2456
  if (!latest) {
@@ -2071,7 +2461,7 @@ function registerUpdateCommand(program2) {
2071
2461
  success(`Already on the latest version (${version}).`);
2072
2462
  return;
2073
2463
  }
2074
- info(`New version available: ${pc7.bold(latest)}`);
2464
+ info(`New version available: ${pc10.bold(latest)}`);
2075
2465
  info("Updating...");
2076
2466
  try {
2077
2467
  execSync(`npm install -g ${NPM_PACKAGE2}@latest`, {
@@ -2081,7 +2471,7 @@ function registerUpdateCommand(program2) {
2081
2471
  } catch {
2082
2472
  error("Update failed. Please reinstall manually:");
2083
2473
  console.log(
2084
- ` ${pc7.cyan(`npm install -g ${NPM_PACKAGE2}`)}`
2474
+ ` ${pc10.cyan(`npm install -g ${NPM_PACKAGE2}`)}`
2085
2475
  );
2086
2476
  process.exit(1);
2087
2477
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senso-ai/cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
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": {